From ec9741c8b3ff6edeed6d29b3ef5f98e41e578968 Mon Sep 17 00:00:00 2001 From: SaibazKhan Date: Tue, 23 Jun 2026 13:25:51 +0530 Subject: [PATCH 01/13] add new command and make url encoded --- src/commands/agent.command.ts | 4 +- .../cleanup-removed-fields.command.ts | 71 +++++++++++++++++++ src/commands/mcp.command.ts | 11 ++- src/helper.ts | 39 ++++++++++ src/main.ts | 2 + 5 files changed, 123 insertions(+), 4 deletions(-) create mode 100644 src/commands/cleanup-removed-fields.command.ts diff --git a/src/commands/agent.command.ts b/src/commands/agent.command.ts index f8f1b1e3..8e6e517f 100644 --- a/src/commands/agent.command.ts +++ b/src/commands/agent.command.ts @@ -4,7 +4,7 @@ import chalk from 'chalk'; import path from 'path'; import readline from 'readline'; import { config as loadDotenv } from 'dotenv'; -import { validateProjectRoot } from '../helper'; +import { normalizeDatabaseUrl, validateProjectRoot } from '../helper'; import { ensureAgentInstalled, ensureAgentInstalledLocal, ensureAgentUIInstalled } from './agent-helper'; type AgentServiceName = 'agent' | 'ui'; @@ -35,7 +35,7 @@ type AgentStartOptions = { * SQL Server using mssql+pyodbc. Otherwise it defaults to PostgreSQL. */ function resolveDatabaseUrl(): string | undefined { - if (process.env.DATABASE_URL) return process.env.DATABASE_URL; + if (process.env.DATABASE_URL) return normalizeDatabaseUrl(process.env.DATABASE_URL); const host = process.env.DEFAULT_DATABASE_HOST; const port = process.env.DEFAULT_DATABASE_PORT; diff --git a/src/commands/cleanup-removed-fields.command.ts b/src/commands/cleanup-removed-fields.command.ts new file mode 100644 index 00000000..63d45e2b --- /dev/null +++ b/src/commands/cleanup-removed-fields.command.ts @@ -0,0 +1,71 @@ +import { spawnSync } from 'child_process'; +import { Command } from 'commander'; +import fs from 'fs'; +import path from 'path'; +import { validateProjectRoot } from '../helper'; + +type CleanupRemovedFieldsOptions = { + name?: string; + apply?: boolean; +}; + +function fail(message: string): never { + console.error(`❌ ${message}`); + process.exit(1); +} + +export function registerCleanupRemovedFieldsCommand(program: Command) { + program + .command('cleanup-removed-fields') + .description('Clean up fields marked for removal by delegating to the solid-api CLI') + .requiredOption('-n, --name ', 'Model singularName to clean up') + .option('--apply', 'Apply the cleanup instead of running a dry-run preview') + .addHelpText('after', ` +Examples: + npx @solidxai/solidctl cleanup-removed-fields -n coverageProduct + npx @solidxai/solidctl cleanup-removed-fields -n coverageProduct --apply`) + .action((options: CleanupRemovedFieldsOptions) => { + validateProjectRoot(); + + + const projectRoot = process.cwd(); + const solidApiDir = path.join(projectRoot, 'solid-api'); + const mainCliPath = path.join(solidApiDir, 'dist', 'main-cli.js'); + + if (!options.name) { + fail('Option --name is required.'); + } + + if (!fs.existsSync(mainCliPath)) { + fail(`solid-api CLI not found at ${mainCliPath}. Run "npx @solidxai/solidctl build" or "cd solid-api && npm run build" first.`); + } + + const args = [ + path.relative(solidApiDir, mainCliPath), + 'migrate-removed-fields', + '-n', + options.name, + ]; + + if (options.apply) { + args.push('-d', 'false'); + } + + console.log(`▶ Running removed-field cleanup for model "${options.name}"${options.apply ? ' (apply)' : ' (dry-run)'}`); + const result = spawnSync(process.execPath, args, { + cwd: solidApiDir, + stdio: 'inherit', + env: process.env, + }); + + if (result.error) { + fail(`Failed to run cleanup-removed-fields: ${result.error.message}`); + } + + if (result.status !== 0) { + fail(`cleanup-removed-fields exited with code ${result.status}`); + } + + console.log(`✔ cleanup-removed-fields completed for model "${options.name}"`); + }); +} diff --git a/src/commands/mcp.command.ts b/src/commands/mcp.command.ts index 52a1263e..7f45c685 100644 --- a/src/commands/mcp.command.ts +++ b/src/commands/mcp.command.ts @@ -2,7 +2,7 @@ import { Command } from 'commander'; import { spawnSync } from 'child_process'; import path from 'path'; import { config as loadDotenv } from 'dotenv'; -import { validateProjectRoot } from '../helper'; +import { normalizeDatabaseUrl, validateProjectRoot } from '../helper'; import { ensureAgentInstalled, ensureAgentInstalledLocal } from './agent-helper'; /** @@ -13,7 +13,12 @@ import { ensureAgentInstalled, ensureAgentInstalledLocal } from './agent-helper' * SQL Server using mssql+pyodbc. Otherwise it defaults to PostgreSQL. */ function resolveDatabaseUrl(): string | undefined { - if (process.env.DATABASE_URL) return process.env.DATABASE_URL; + console.log('Before resolving DATABASE_URL', process.env.DATABASE_URL); + if (process.env.DATABASE_URL){ + const resolvedUrl = normalizeDatabaseUrl(process.env.DATABASE_URL); + console.log('After resolving DATABASE_URL',resolvedUrl); + return resolvedUrl; + } const host = process.env.DEFAULT_DATABASE_HOST; const port = process.env.DEFAULT_DATABASE_PORT; @@ -35,6 +40,8 @@ function resolveDatabaseUrl(): string | undefined { } // Default to PostgreSQL + console.log(`Final Url postgresql://${user}${encodedPw}@${host}:${port}/${name}`); + return `postgresql://${user}${encodedPw}@${host}:${port}/${name}`; } diff --git a/src/helper.ts b/src/helper.ts index 18078e03..c9d67bb3 100644 --- a/src/helper.ts +++ b/src/helper.ts @@ -3,6 +3,7 @@ import os from 'os'; import path from 'path'; const requiredProjectFiles = ['solid-api/package.json', 'solid-ui/package.json'] as const; +const validPercentEscapePattern = /^%[0-9A-Fa-f]{2}$/; export function validateProjectRoot() { const cwd = process.cwd(); @@ -48,3 +49,41 @@ export function getSolidCommandEnv() { PATH: `${solidctlBinDir}${path.delimiter}${currentPath}`, }; } + +function encodeUserInfoComponent(value: string) { + return value + .split(/(%[0-9A-Fa-f]{2})/) + .map((part) => (validPercentEscapePattern.test(part) ? part : encodeURIComponent(part))) + .join(''); +} + +export function normalizeDatabaseUrl(urlValue: string) { + const trimmedUrl = urlValue.trim(); + const schemeMatch = trimmedUrl.match(/^([a-z][a-z0-9+.-]*:\/\/)([\s\S]+)$/i); + + if (!schemeMatch) return trimmedUrl; + + const [, scheme, remainder] = schemeMatch; + const userInfoSeparatorIndex = remainder.lastIndexOf('@'); + + if (userInfoSeparatorIndex < 0) return trimmedUrl; + + const userInfo = remainder.slice(0, userInfoSeparatorIndex); + const hostAndSuffix = remainder.slice(userInfoSeparatorIndex + 1); + const authorityEndIndex = ['/', '?', '#'] + .map((separator) => hostAndSuffix.indexOf(separator)) + .filter((index) => index >= 0) + .sort((a, b) => a - b)[0] ?? hostAndSuffix.length; + const hostInfo = hostAndSuffix.slice(0, authorityEndIndex); + const suffix = hostAndSuffix.slice(authorityEndIndex); + const passwordSeparatorIndex = userInfo.indexOf(':'); + + if (passwordSeparatorIndex < 0) { + return `${scheme}${encodeUserInfoComponent(userInfo)}@${hostInfo}${suffix}`; + } + + const username = userInfo.slice(0, passwordSeparatorIndex); + const password = userInfo.slice(passwordSeparatorIndex + 1); + + return `${scheme}${encodeUserInfoComponent(username)}:${encodeUserInfoComponent(password)}@${hostInfo}${suffix}`; +} diff --git a/src/main.ts b/src/main.ts index 0dcc07da..54cc8da4 100644 --- a/src/main.ts +++ b/src/main.ts @@ -17,6 +17,7 @@ import { registerGenerateCommand } from './commands/generate.command'; import { registerMcpCommand } from './commands/mcp.command'; import { registerAgentCommand } from './commands/agent.command'; import { registerMigrationCommand } from './commands/migration.command'; +import { registerCleanupRemovedFieldsCommand } from './commands/cleanup-removed-fields.command'; import { registerStartCommand } from './commands/start.command'; function getCliVersion(): string { @@ -54,6 +55,7 @@ async function bootstrap() { registerLegacyMigrateCommand(program); registerGenerateCommand(program); registerMigrationCommand(program); + registerCleanupRemovedFieldsCommand(program); registerMcpCommand(program); registerAgentCommand(program); registerStartCommand(program); From 36f803c836348163a54c5e763bc7e3276566326a Mon Sep 17 00:00:00 2001 From: Pathik Date: Wed, 24 Jun 2026 20:21:36 +0530 Subject: [PATCH 02/13] restricted db pool again to 1 for embedded db as it was causing "unnamed prepared statement does not exist" error --- src/commands/create-app/helpers.ts | 6 +++++- src/db/embedded.ts | 2 +- src/db/pglite-server.ts | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/commands/create-app/helpers.ts b/src/commands/create-app/helpers.ts index 4ceb4f6a..69235b96 100644 --- a/src/commands/create-app/helpers.ts +++ b/src/commands/create-app/helpers.ts @@ -272,7 +272,11 @@ export function getBackendEnvConfig(answers: SetupAnswers, properAppName: string answers.solidApiDatabaseSynchronize === 'Yes' ? 'true' : 'false', DEFAULT_DATABASE_LOGGING: 'false', // Marks an embedded PGlite project so solidctl manages the database lifecycle. - ...(isEmbedded ? { DEFAULT_DATABASE_DRIVER: 'pglite' } : {}), + // The PGlite socket server exposes a single backend, so app-side pooling must + // stay at one connection to avoid protocol-level prepared-statement collisions. + ...(isEmbedded + ? { DEFAULT_DATABASE_DRIVER: 'pglite', DEFAULT_DATABASE_POOL_MAX: '1' } + : {}), }, 'IAM Registration': { IAM_PASSWORD_LESS_REGISTRATION: 'false', diff --git a/src/db/embedded.ts b/src/db/embedded.ts index b56c72c2..5701e2b9 100644 --- a/src/db/embedded.ts +++ b/src/db/embedded.ts @@ -114,7 +114,7 @@ export function startEmbeddedServer( SOLIDX_PGLITE_HOST: config.host, SOLIDX_PGLITE_PORT: config.port, SOLIDX_PGLITE_DATA: config.dataDir, - SOLIDX_PGLITE_MAX_CONNECTIONS: String(options.maxConnections ?? 50), + SOLIDX_PGLITE_MAX_CONNECTIONS: String(options.maxConnections ?? 1), }, stdio: ['ignore', 'pipe', 'pipe'], }); diff --git a/src/db/pglite-server.ts b/src/db/pglite-server.ts index 54c5a701..2593ba89 100644 --- a/src/db/pglite-server.ts +++ b/src/db/pglite-server.ts @@ -16,7 +16,7 @@ async function main(): Promise { const port = Number(process.env.SOLIDX_PGLITE_PORT || '54329'); const dataDir = process.env.SOLIDX_PGLITE_DATA; const maxConnections = Number( - process.env.SOLIDX_PGLITE_MAX_CONNECTIONS || '50', + process.env.SOLIDX_PGLITE_MAX_CONNECTIONS || '1', ); if (!dataDir) { From b67aba471c89f8f046e27a463d14c4e27afb24c5 Mon Sep 17 00:00:00 2001 From: SaibazKhan Date: Thu, 25 Jun 2026 12:02:05 +0530 Subject: [PATCH 03/13] move code from generation to migration file --- .../cleanup-removed-fields.command.ts | 71 ---------------- src/commands/migration.command.ts | 82 +++++++++++++++++-- src/main.ts | 2 - 3 files changed, 75 insertions(+), 80 deletions(-) delete mode 100644 src/commands/cleanup-removed-fields.command.ts diff --git a/src/commands/cleanup-removed-fields.command.ts b/src/commands/cleanup-removed-fields.command.ts deleted file mode 100644 index 63d45e2b..00000000 --- a/src/commands/cleanup-removed-fields.command.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { spawnSync } from 'child_process'; -import { Command } from 'commander'; -import fs from 'fs'; -import path from 'path'; -import { validateProjectRoot } from '../helper'; - -type CleanupRemovedFieldsOptions = { - name?: string; - apply?: boolean; -}; - -function fail(message: string): never { - console.error(`❌ ${message}`); - process.exit(1); -} - -export function registerCleanupRemovedFieldsCommand(program: Command) { - program - .command('cleanup-removed-fields') - .description('Clean up fields marked for removal by delegating to the solid-api CLI') - .requiredOption('-n, --name ', 'Model singularName to clean up') - .option('--apply', 'Apply the cleanup instead of running a dry-run preview') - .addHelpText('after', ` -Examples: - npx @solidxai/solidctl cleanup-removed-fields -n coverageProduct - npx @solidxai/solidctl cleanup-removed-fields -n coverageProduct --apply`) - .action((options: CleanupRemovedFieldsOptions) => { - validateProjectRoot(); - - - const projectRoot = process.cwd(); - const solidApiDir = path.join(projectRoot, 'solid-api'); - const mainCliPath = path.join(solidApiDir, 'dist', 'main-cli.js'); - - if (!options.name) { - fail('Option --name is required.'); - } - - if (!fs.existsSync(mainCliPath)) { - fail(`solid-api CLI not found at ${mainCliPath}. Run "npx @solidxai/solidctl build" or "cd solid-api && npm run build" first.`); - } - - const args = [ - path.relative(solidApiDir, mainCliPath), - 'migrate-removed-fields', - '-n', - options.name, - ]; - - if (options.apply) { - args.push('-d', 'false'); - } - - console.log(`▶ Running removed-field cleanup for model "${options.name}"${options.apply ? ' (apply)' : ' (dry-run)'}`); - const result = spawnSync(process.execPath, args, { - cwd: solidApiDir, - stdio: 'inherit', - env: process.env, - }); - - if (result.error) { - fail(`Failed to run cleanup-removed-fields: ${result.error.message}`); - } - - if (result.status !== 0) { - fail(`cleanup-removed-fields exited with code ${result.status}`); - } - - console.log(`✔ cleanup-removed-fields completed for model "${options.name}"`); - }); -} diff --git a/src/commands/migration.command.ts b/src/commands/migration.command.ts index a2f860ee..43aeee06 100644 --- a/src/commands/migration.command.ts +++ b/src/commands/migration.command.ts @@ -5,8 +5,10 @@ import path from 'path'; import { validateProjectRoot } from '../helper'; type MigrationOptions = { - datasource: string; + datasource?: string; module?: string; + name?: string; + apply?: boolean; }; function getNpxCommand() { @@ -88,23 +90,32 @@ function runTypeormCli(args: string[], solidApiDir: string, failureLabel: string export function registerMigrationCommand(program: Command) { program .command('migration [migrationName]') - .description('Generate, run, or revert TypeORM migrations for a datasource') - .requiredOption('-d, --datasource ', 'Datasource name (maps to src/typeorm--datasource.ts)') + .description('Generate, run, revert, or remove-field TypeORM migrations for a datasource') + .option('-d, --datasource ', 'Datasource name (maps to src/typeorm--datasource.ts), required for generate/run/revert') .option('-m, --module ', 'Module name, required for generate') + .option('-n, --name ', 'Model name (singularName), required for remove-field') + .option('--apply', 'Apply the cleanup instead of running a dry-run preview (used with remove-field)') .addHelpText('after', ` Examples: npx @solidxai/solidctl migration -d default -m onboarding generate AddPreApplicationMaster npx @solidxai/solidctl migration -d default run - npx @solidxai/solidctl migration -d default revert`) + npx @solidxai/solidctl migration -d default revert + npx @solidxai/solidctl migration -n book remove-field + npx @solidxai/solidctl migration -n book remove-field --apply`) .action((action: string, migrationName: string | undefined, options: MigrationOptions) => { validateProjectRoot(); const normalizedAction = action.trim().toLowerCase(); const projectRoot = process.cwd(); const solidApiDir = path.join(projectRoot, 'solid-api'); - ensureDatasourceFile(solidApiDir, options.datasource); if (normalizedAction === 'generate') { + if (!options.datasource) { + fail('Option --datasource is required for generate.'); + } + + ensureDatasourceFile(solidApiDir, options.datasource); + const { migrationTargetPath } = validateGenerateInputs( solidApiDir, options.datasource, @@ -127,6 +138,12 @@ Examples: } if (normalizedAction === 'run') { + if (!options.datasource) { + fail('Option --datasource is required for run.'); + } + + ensureDatasourceFile(solidApiDir, options.datasource); + console.log(`✅ Using datasource: src/typeorm-${options.datasource}-datasource.ts`); console.log('➡ Running migrations...'); runTypeormCli( @@ -138,6 +155,12 @@ Examples: } if (normalizedAction === 'revert') { + if (!options.datasource) { + fail('Option --datasource is required for revert.'); + } + + ensureDatasourceFile(solidApiDir, options.datasource); + console.log(`✅ Using datasource: src/typeorm-${options.datasource}-datasource.ts`); console.log('➡ Reverting last migration...'); runTypeormCli( @@ -148,6 +171,51 @@ Examples: return; } - fail(`Unknown action "${action}". Expected generate, run, or revert.`); + if (normalizedAction === 'remove-field') { + if (!options.name) { + console.error('Option --name is required for remove-field.'); + process.exit(1); + } + + const mainCliPath = path.join(solidApiDir, 'dist', 'main-cli.js'); + + if (!fs.existsSync(mainCliPath)) { + console.error(`solid-api CLI not found at ${mainCliPath}. Run "npx @solidxai/solidctl build" or "cd solid-api && npm run build" first.`); + process.exit(1); + } + + const args = [ + path.relative(solidApiDir, mainCliPath), + 'migrate-removed-fields', + '-n', + options.name, + ]; + + if (options.apply) { + args.push('-d', 'false'); + } + + console.log(`▶ Running removed-field cleanup for model "${options.name}"${options.apply ? ' (apply)' : ' (dry-run)'}`); + const result = spawnSync(process.execPath, args, { + cwd: solidApiDir, + stdio: 'inherit', + env: process.env, + }); + + if (result.error) { + console.error(`Failed to run cleanup-removed-fields: ${result.error.message}`); + process.exit(1); + } + + if (result.status !== 0) { + console.error(`cleanup-removed-fields exited with code ${result.status}`); + process.exit(result.status ?? 1); + } + + console.log(`✔ cleanup-removed-fields completed for model "${options.name}"`); + return; + } + + fail(`Unknown action "${action}". Expected generate, run, revert, or remove-field.`); }); -} +} \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 62c1b109..83801943 100644 --- a/src/main.ts +++ b/src/main.ts @@ -18,7 +18,6 @@ import { registerGenerateCommand } from './commands/generate.command'; import { registerMcpCommand } from './commands/mcp.command'; import { registerAgentCommand } from './commands/agent.command'; import { registerMigrationCommand } from './commands/migration.command'; -import { registerCleanupRemovedFieldsCommand } from './commands/cleanup-removed-fields.command'; import { registerStartCommand } from './commands/start.command'; function getCliVersion(): string { @@ -58,7 +57,6 @@ async function bootstrap() { registerLegacyMigrateCommand(program); registerGenerateCommand(program); registerMigrationCommand(program); - registerCleanupRemovedFieldsCommand(program); registerMcpCommand(program); registerAgentCommand(program); registerStartCommand(program); From c1520d923812c2dfdcf3e5d0f08a07fc19987222 Mon Sep 17 00:00:00 2001 From: Pathik Date: Thu, 25 Jun 2026 13:26:51 +0530 Subject: [PATCH 04/13] added version check for agent and mcp sub commands --- src/commands/agent-helper.ts | 55 +++++++++++++++++++++++++++++++++++ src/commands/agent.command.ts | 16 +++++++++- src/commands/mcp.command.ts | 14 ++++++++- src/main.ts | 6 ++++ 4 files changed, 89 insertions(+), 2 deletions(-) diff --git a/src/commands/agent-helper.ts b/src/commands/agent-helper.ts index 26369721..e0b45132 100644 --- a/src/commands/agent-helper.ts +++ b/src/commands/agent-helper.ts @@ -183,6 +183,61 @@ function ensureVenv(pythonCmd: string, uvCmd: string | null): boolean { return result.status === 0; } +/** + * Resolve the Python interpreter that backs a given solidx-agent binary. + * + * The agent exposes no `--version` flag, so we instead ask the interpreter + * that owns it for the installed `solidx-ai-agent` package metadata. Works for + * the managed venv (`~/.solidx/venv/bin/`), local installs (`/.venv/bin/`), + * and PATH binaries (via the entrypoint script's shebang). + */ +function resolveAgentPython(binary: string): string | null { + const dir = path.dirname(binary); + const pyInDir = path.join(dir, process.platform === 'win32' ? 'python.exe' : 'python'); + if (fs.existsSync(pyInDir)) return pyInDir; + + try { + const content = fs.readFileSync(binary, 'utf-8'); + const firstLine = content.split(/\r?\n/)[0] || ''; + if (firstLine.startsWith('#!')) { + const interp = firstLine.slice(2).trim().split(/\s+/)[0]; + if (interp && fs.existsSync(interp)) return interp; + } + } catch { + // ignore unreadable shebangs + } + + return null; +} + +/** + * Probe the installed solidx-ai-agent package version backing a solidx-agent + * binary by asking its owning Python interpreter for the package metadata. + * + * Read-only: no install is triggered. Returns the trimmed version, or + * 'not installed' / 'unknown' when the binary is missing or the probe fails. + */ +export function getAgentVersion(agentCommand?: string): string { + const binary = agentCommand || findAgentBinary(); + if (!binary) return 'not installed'; + const pythonBin = resolveAgentPython(binary); + if (!pythonBin) return 'unknown'; + const result = spawnSync( + pythonBin, + ['-c', "import importlib.metadata as m; print(m.version('solidx-ai-agent'))"], + { stdio: 'pipe' }, + ); + if (result.status !== 0) return 'unknown'; + return (result.stdout?.toString() || '').trim() || 'unknown'; +} + +/** + * Print `solidx-ai-agent v` to stdout. + */ +export function printAgentVersion(agentCommand?: string): void { + console.log(`solidx-ai-agent v${getAgentVersion(agentCommand)}`); +} + /** * Install solidx-ai-agent into the dedicated venv. * Prefers uv (faster) but falls back to pip. diff --git a/src/commands/agent.command.ts b/src/commands/agent.command.ts index f8f1b1e3..49baefa8 100644 --- a/src/commands/agent.command.ts +++ b/src/commands/agent.command.ts @@ -5,7 +5,7 @@ import path from 'path'; import readline from 'readline'; import { config as loadDotenv } from 'dotenv'; import { validateProjectRoot } from '../helper'; -import { ensureAgentInstalled, ensureAgentInstalledLocal, ensureAgentUIInstalled } from './agent-helper'; +import { ensureAgentInstalled, ensureAgentInstalledLocal, ensureAgentUIInstalled, getAgentVersion, printAgentVersion } from './agent-helper'; type AgentServiceName = 'agent' | 'ui'; @@ -506,6 +506,17 @@ export function registerAgentCommand(program: Command) { .command('agent') .description('SolidX AI Agent — start the server or run a single task'); + agent + .option('--version', 'Print the solidx-ai-agent version and exit') + .action((options: { version?: boolean }) => { + if (options.version) { + const agentCommand = ensureAgentInstalled(); + console.log(`solidx-ai-agent v${getAgentVersion(agentCommand)}`); + process.exit(0); + } + agent.help(); + }); + agent .command('start') .description('Start the AI agent server and chat UI in a single supervisor') @@ -524,6 +535,8 @@ export function registerAgentCommand(program: Command) { const agentCommand = options.local ? ensureAgentInstalledLocal() : ensureAgentInstalled(); const agentUiDir = ensureAgentUIInstalled(); + printAgentVersion(agentCommand); + const supervisor = new AgentSupervisor(projectRoot, agentCommand, agentUiDir, options); await supervisor.start(); }); @@ -560,6 +573,7 @@ export function registerAgentCommand(program: Command) { // Match the MCP startup flow: do dependency resolution first so Ubuntu // users do not see a "running" banner when Python/bootstrap fails. const agentCommand = options.local ? ensureAgentInstalledLocal() : ensureAgentInstalled(); + printAgentVersion(agentCommand); const bridgedKeys = ['DATABASE_URL', 'SOLIDX_PROJECT_ROOT', 'BASE_URL', 'APP_ENCRYPTION_KEY']; const bridged = bridgedKeys.filter((k) => env[k]); const missing = bridgedKeys.filter((k) => !env[k]); diff --git a/src/commands/mcp.command.ts b/src/commands/mcp.command.ts index 52a1263e..73d47452 100644 --- a/src/commands/mcp.command.ts +++ b/src/commands/mcp.command.ts @@ -3,7 +3,7 @@ import { spawnSync } from 'child_process'; import path from 'path'; import { config as loadDotenv } from 'dotenv'; import { validateProjectRoot } from '../helper'; -import { ensureAgentInstalled, ensureAgentInstalledLocal } from './agent-helper'; +import { ensureAgentInstalled, ensureAgentInstalledLocal, getAgentVersion, printAgentVersion } from './agent-helper'; /** * Build a DATABASE_URL from the consuming project's individual DB env vars, @@ -79,6 +79,17 @@ export function registerMcpCommand(program: Command) { .command('mcp') .description('SolidX MCP Server (Streamable HTTP) — for remote clients (Cursor, Codex, cloud desktops)'); + mcp + .option('--version', 'Print the solidx-ai-agent version and exit') + .action((options: { version?: boolean }) => { + if (options.version) { + const agentCommand = ensureAgentInstalled(); + console.log(`solidx-ai-agent v${getAgentVersion(agentCommand)}`); + process.exit(0); + } + mcp.help(); + }); + mcp .command('start') .description('Start the MCP server using Streamable HTTP transport') @@ -104,6 +115,7 @@ export function registerMcpCommand(program: Command) { // This keeps the CLI honest on Ubuntu: if Python/venv/bootstrap fails, we // should not imply that the MCP server actually started listening yet. const agentCommand = options.local ? ensureAgentInstalledLocal() : ensureAgentInstalled(); + printAgentVersion(agentCommand); printBridgeSummary(env); console.log(`▶ Starting SolidX MCP Server on ${options.host}:${options.port}`); const result = spawnSync( diff --git a/src/main.ts b/src/main.ts index 83801943..3ea67c2c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -41,6 +41,12 @@ async function bootstrap() { const program = new Command(); + // Ensure options appearing after a subcommand name belong to that subcommand, + // not the root program. Without this, the root program's `--version` would + // intercept `solidctl agent --version` / `solidctl mcp --version` and print + // the CLI version instead of letting the subcommand handle it. + program.enablePositionalOptions(); + program .name('solidctl') .description('Solidctl tool') From 8b8fe7cd1277efc9bc7d86639fde6a871aa41367 Mon Sep 17 00:00:00 2001 From: Pathik Date: Thu, 25 Jun 2026 14:09:03 +0530 Subject: [PATCH 05/13] added agent and mcp auto version check and update feature --- src/commands/agent-helper.ts | 158 +++++++++++++++++++++++++++++++++- src/commands/agent.command.ts | 11 ++- src/commands/mcp.command.ts | 8 +- 3 files changed, 167 insertions(+), 10 deletions(-) diff --git a/src/commands/agent-helper.ts b/src/commands/agent-helper.ts index e0b45132..857b6a13 100644 --- a/src/commands/agent-helper.ts +++ b/src/commands/agent-helper.ts @@ -2,6 +2,10 @@ import { spawnSync } from 'child_process'; import fs from 'fs'; import path from 'path'; import os from 'os'; +import semver from 'semver'; +import chalk from 'chalk'; +import inquirer from 'inquirer'; +import ora from 'ora'; const AGENT_PACKAGE = 'solidx-ai-agent'; const AGENT_UI_PACKAGE = '@solidxai/agent-ui'; @@ -238,6 +242,146 @@ export function printAgentVersion(agentCommand?: string): void { console.log(`solidx-ai-agent v${getAgentVersion(agentCommand)}`); } +const PYPI_TIMEOUT_MS = 10_000; + +/** + * Determine which agent track to use based on the installed solidctl version. + * Any solidctl prerelease (alpha/beta/rc) maps to the agent beta track; a + * stable solidctl maps to the agent stable track. + */ +export function getSolidctlAgentTrack(): 'stable' | 'beta' { + // agent-helper.ts lives in src/commands/ → dist/commands/, so the consuming + // package.json is two levels up (repo root in dev, package root when installed). + const packageJsonPath = path.resolve(__dirname, '..', '..', 'package.json'); + try { + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')) as { version?: string }; + const version = packageJson.version || '0.0.0'; + return semver.prerelease(version) ? 'beta' : 'stable'; + } catch { + return 'stable'; + } +} + +/** + * Normalize a PEP 440 agent version into a semver-parseable string. + * `0.2.3b1` → `0.2.3-b1`; everything else passes through unchanged. + */ +function toSemver(version: string): string { + return version.replace(/^(\d+\.\d+\.\d+)b(\d+)$/, '$1-b$2'); +} + +/** + * Ask PyPI for the highest published solidx-ai-agent release on the given track. + * Returns null on any network/parse failure (swallowed silently by callers). + */ +async function getLatestAgentRelease(track: 'stable' | 'beta'): Promise { + try { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), PYPI_TIMEOUT_MS); + const response = await fetch('https://pypi.org/pypi/solidx-ai-agent/json', { + signal: controller.signal, + }); + clearTimeout(timer); + if (!response.ok) return null; + const data = (await response.json()) as { releases?: Record }; + const candidates = Object.keys(data.releases || {}) + .map((v) => toSemver(v)) + .filter((v) => semver.valid(v) && (track === 'beta' ? !!semver.prerelease(v) : !semver.prerelease(v))); + if (candidates.length === 0) return null; + candidates.sort((a, b) => semver.rcompare(a, b)); + return candidates[0]; + } catch { + return null; + } +} + +/** + * Check PyPI for a newer solidx-ai-agent in the same track as the installed + * solidctl, and prompt the user to upgrade into the managed ~/.solidx/venv. + * + * Skipped entirely when `isLocal` is true (the user is developing against a + * local agent checkout). Silent on any network/parse failure. Mirrors the UX + * of the solidctl self-update check in src/version-check.ts. + */ +export async function checkAgentUpdate(opts: { isLocal: boolean }): Promise { + if (opts.isLocal) return; + const currentRaw = getAgentVersion(); + const current = toSemver(currentRaw); + if (!semver.valid(current)) return; // 'not installed' / 'unknown' / unparseable + + const track = getSolidctlAgentTrack(); + const spinner = ora(`Checking for ${track} agent updates...`).start(); + const latestRaw = await getLatestAgentRelease(track); + spinner.stop(); + if (!latestRaw) return; + const latest = toSemver(latestRaw); + if (!semver.valid(latest)) return; + if (!semver.gt(latest, current)) return; + + console.log( + chalk.yellow( + `\n⚠️ A newer version of ${AGENT_PACKAGE} is available: ${latestRaw} (you have ${currentRaw})`, + ), + ); + + let upgrade = false; + try { + const answer = await inquirer.prompt<{ upgrade: boolean }>([ + { + type: 'confirm', + name: 'upgrade', + message: `Would you like to upgrade to ${latestRaw}?`, + default: false, + }, + ]); + upgrade = answer.upgrade; + } catch { + // Non-interactive (no TTY) or prompt cancelled → don't upgrade + } + + const preFlag = track === 'beta' ? ['--pre'] : []; + const manual = `pip install ${preFlag.length ? '--pre ' : ''}--upgrade ${AGENT_PACKAGE}`; + + if (!upgrade) { + console.log(chalk.dim(` You can upgrade later with: ${manual}\n`)); + return; + } + + const uvCmd = findUv(); + const venvPython = path.join(VENV_BIN, process.platform === 'win32' ? 'python.exe' : 'python'); + const pipBin = path.join(VENV_BIN, process.platform === 'win32' ? 'pip.exe' : 'pip'); + + console.log(chalk.cyan(`\n▶ Upgrading ${AGENT_PACKAGE}...\n`)); + let ok = false; + if (uvCmd) { + const result = spawnSync( + uvCmd, + ['pip', 'install', ...preFlag, '--upgrade', AGENT_PACKAGE, '--python', venvPython], + { stdio: 'inherit' }, + ); + ok = result.status === 0; + if (!ok) console.warn('⚠ uv install failed, falling back to pip'); + } + if (!ok) { + const pipCmd = fs.existsSync(pipBin) ? pipBin : venvPython; + const pipArgs = fs.existsSync(pipBin) + ? ['install', ...preFlag, '--upgrade', AGENT_PACKAGE] + : ['-m', 'pip', 'install', ...preFlag, '--upgrade', AGENT_PACKAGE]; + const result = spawnSync(pipCmd, pipArgs, { + stdio: 'inherit', + shell: process.platform === 'win32', + }); + ok = result.status === 0; + } + + if (ok) { + console.log(chalk.green(`\n✔ Upgraded to ${latestRaw}. Please re-run your command.\n`)); + process.exit(0); + } else { + console.error(chalk.red(`\n❌ Upgrade failed. You can manually run: ${manual}\n`)); + } +} + /** * Install solidx-ai-agent into the dedicated venv. * Prefers uv (faster) but falls back to pip. @@ -252,14 +396,20 @@ function installAgent(pythonCmd: string, uvCmd: string | null): boolean { process.platform === 'win32' ? 'pip.exe' : 'pip', ); - console.log(`📦 Installing ${AGENT_PACKAGE}...`); + // Install the agent on the same track as the installed solidctl: a beta + // solidctl pulls the latest beta from PyPI (via --pre), a stable solidctl + // pulls the latest stable. + const track = getSolidctlAgentTrack(); + const preFlag = track === 'beta' ? ['--pre'] : []; + + console.log(`📦 Installing ${AGENT_PACKAGE}${track === 'beta' ? ' (beta)' : ''}...`); if (uvCmd) { // Pass the venv's Python interpreter to uv so it targets the correct environment. // Using --python is the correct uv flag (not the pip binary). const result = spawnSync( uvCmd, - ['pip', 'install', AGENT_PACKAGE, '--python', venvPython], + ['pip', 'install', ...preFlag, AGENT_PACKAGE, '--python', venvPython], { stdio: 'inherit', }, @@ -272,8 +422,8 @@ function installAgent(pythonCmd: string, uvCmd: string | null): boolean { // which works as long as the venv itself is functional. const pipCmd = fs.existsSync(pipBin) ? pipBin : venvPython; const pipArgs = fs.existsSync(pipBin) - ? ['install', AGENT_PACKAGE] - : ['-m', 'pip', 'install', AGENT_PACKAGE]; + ? ['install', ...preFlag, AGENT_PACKAGE] + : ['-m', 'pip', 'install', ...preFlag, AGENT_PACKAGE]; const result = spawnSync(pipCmd, pipArgs, { stdio: 'inherit', diff --git a/src/commands/agent.command.ts b/src/commands/agent.command.ts index 49baefa8..faba2971 100644 --- a/src/commands/agent.command.ts +++ b/src/commands/agent.command.ts @@ -5,7 +5,7 @@ import path from 'path'; import readline from 'readline'; import { config as loadDotenv } from 'dotenv'; import { validateProjectRoot } from '../helper'; -import { ensureAgentInstalled, ensureAgentInstalledLocal, ensureAgentUIInstalled, getAgentVersion, printAgentVersion } from './agent-helper'; +import { checkAgentUpdate, ensureAgentInstalled, ensureAgentInstalledLocal, ensureAgentUIInstalled, getAgentVersion, printAgentVersion } from './agent-helper'; type AgentServiceName = 'agent' | 'ui'; @@ -508,8 +508,9 @@ export function registerAgentCommand(program: Command) { agent .option('--version', 'Print the solidx-ai-agent version and exit') - .action((options: { version?: boolean }) => { + .action(async (options: { version?: boolean }) => { if (options.version) { + await checkAgentUpdate({ isLocal: false }); const agentCommand = ensureAgentInstalled(); console.log(`solidx-ai-agent v${getAgentVersion(agentCommand)}`); process.exit(0); @@ -532,6 +533,8 @@ export function registerAgentCommand(program: Command) { // Load consuming project's .env (lives in solid-api/) loadDotenv({ path: path.join(projectRoot, 'solid-api', '.env') }); + await checkAgentUpdate({ isLocal: Boolean(options.local) }); + const agentCommand = options.local ? ensureAgentInstalledLocal() : ensureAgentInstalled(); const agentUiDir = ensureAgentUIInstalled(); @@ -548,13 +551,15 @@ export function registerAgentCommand(program: Command) { .option('-m, --mode ', 'Tool mode: native or mcp') .option('-l, --log-level ', 'Logging level', 'INFO') .option('--local', 'Install agent from local source (pip install -e .[full]) instead of PyPI') - .action((task, options) => { + .action(async (task, options) => { validateProjectRoot(); const projectRoot = process.cwd(); // Load consuming project's .env (lives in solid-api/) loadDotenv({ path: path.join(projectRoot, 'solid-api', '.env') }); + await checkAgentUpdate({ isLocal: Boolean(options.local) }); + const databaseUrl = resolveDatabaseUrl(); const env: Record = { ...process.env as Record, diff --git a/src/commands/mcp.command.ts b/src/commands/mcp.command.ts index 73d47452..b6130010 100644 --- a/src/commands/mcp.command.ts +++ b/src/commands/mcp.command.ts @@ -3,7 +3,7 @@ import { spawnSync } from 'child_process'; import path from 'path'; import { config as loadDotenv } from 'dotenv'; import { validateProjectRoot } from '../helper'; -import { ensureAgentInstalled, ensureAgentInstalledLocal, getAgentVersion, printAgentVersion } from './agent-helper'; +import { checkAgentUpdate, ensureAgentInstalled, ensureAgentInstalledLocal, getAgentVersion, printAgentVersion } from './agent-helper'; /** * Build a DATABASE_URL from the consuming project's individual DB env vars, @@ -81,8 +81,9 @@ export function registerMcpCommand(program: Command) { mcp .option('--version', 'Print the solidx-ai-agent version and exit') - .action((options: { version?: boolean }) => { + .action(async (options: { version?: boolean }) => { if (options.version) { + await checkAgentUpdate({ isLocal: false }); const agentCommand = ensureAgentInstalled(); console.log(`solidx-ai-agent v${getAgentVersion(agentCommand)}`); process.exit(0); @@ -98,8 +99,9 @@ export function registerMcpCommand(program: Command) { .option('-l, --log-level ', 'Logging level', 'INFO') .option('--mount-path ', 'Path under which to mount the MCP app', '/mcp') .option('--local', 'Install agent from local source (pip install -e .[full]) instead of PyPI') - .action((options: { port: string; host: string; logLevel: string; mountPath: string; local?: boolean }) => { + .action(async (options: { port: string; host: string; logLevel: string; mountPath: string; local?: boolean }) => { validateProjectRoot(); + await checkAgentUpdate({ isLocal: Boolean(options.local) }); const env = buildBridgedEnv(); if (!env.DATABASE_URL) { From 5fee076175aee78e325f48b512bf0dcf1c44fe2d Mon Sep 17 00:00:00 2001 From: Oswald Rodrigues Date: Thu, 25 Jun 2026 14:18:22 +0530 Subject: [PATCH 06/13] 0.1.46-beta.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 175b5cfa..4693f8f4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@solidxai/solidctl", - "version": "0.1.45", + "version": "0.1.46-beta.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@solidxai/solidctl", - "version": "0.1.45", + "version": "0.1.46-beta.0", "license": "BUSL-1.1", "dependencies": { "@electric-sql/pglite": "^0.5.3", diff --git a/package.json b/package.json index fff477de..5c4b22c0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@solidxai/solidctl", - "version": "0.1.45", + "version": "0.1.46-beta.0", "description": "", "author": "", "private": false, From 9ff5c62e2cb677d83a5d4aa806042886c50c5c31 Mon Sep 17 00:00:00 2001 From: Oswald Rodrigues Date: Thu, 25 Jun 2026 14:34:32 +0530 Subject: [PATCH 07/13] refactor: detect solidx version and tag and accordingly install the corresponding depdency tags --- src/commands/create-app/create-app.command.ts | 11 +++++++++-- src/commands/create-app/setup-questions.ts | 7 ------- src/version-check.ts | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/commands/create-app/create-app.command.ts b/src/commands/create-app/create-app.command.ts index 8c117c02..31639990 100644 --- a/src/commands/create-app/create-app.command.ts +++ b/src/commands/create-app/create-app.command.ts @@ -38,6 +38,12 @@ import { startEmbeddedServer, stopEmbeddedServer, } from '../../db/embedded'; +import { getCurrentVersion, getDistTag } from '../../version-check'; + +function detectSolidxVersion(): string { + const tag = getDistTag(getCurrentVersion()); + return tag === 'beta' ? 'beta' : 'stable'; +} function buildAnswersFromOptions(options: Record): SetupAnswers { @@ -68,7 +74,7 @@ function buildAnswersFromOptions(options: Record', `Auto-sync DB schema: Yes or No (default: ${SETUP_DEFAULTS.solidApiDatabaseSynchronize})`) .option('--db-exists ', `Database already exists: Yes or No (default: ${SETUP_DEFAULTS.databaseExists})`) .option('--ui-port ', `Frontend port (default: ${SETUP_DEFAULTS.solidUiPort})`) - .option('--beta', 'Use beta release channel for @solidxai/* packages (default: stable)') + .option('--beta', 'Force beta release channel for @solidxai/* packages (default: auto-detected from installed solidctl)') .action(async (options) => { try { const showLogs: boolean = options.verbose || false; @@ -138,6 +144,7 @@ export function registerCreateAppCommand(program: Command) { } else { console.log(chalk.cyan("Hello, Let's setup your SolidX project!")); answers = await inquirer.prompt(setupQuestions); + answers.solidxVersion = detectSolidxVersion(); if (answers.databaseMode === 'embedded') { answers = applyEmbeddedDatabaseDefaults(answers); } diff --git a/src/commands/create-app/setup-questions.ts b/src/commands/create-app/setup-questions.ts index 926ec9c6..68d321ca 100644 --- a/src/commands/create-app/setup-questions.ts +++ b/src/commands/create-app/setup-questions.ts @@ -45,13 +45,6 @@ export const setupQuestions = [ message: 'What is the name of your project?', default: 'my-solid-app', }, - { - type: 'list', - name: 'solidxVersion', - message: 'Which SolidX version would you like to use?', - choices: ['stable', 'beta'], - default: 'stable', - }, { type: 'input', name: 'solidApiPort', diff --git a/src/version-check.ts b/src/version-check.ts index 245843b7..eb5ae5a4 100644 --- a/src/version-check.ts +++ b/src/version-check.ts @@ -9,7 +9,7 @@ import ora from 'ora'; const PACKAGE_NAME = '@solidxai/solidctl'; const NPM_TIMEOUT_MS = 10_000; -function getCurrentVersion(): string { +export function getCurrentVersion(): string { const packageJsonPath = path.resolve(__dirname, '..', 'package.json'); try { const packageJson = JSON.parse( @@ -21,7 +21,7 @@ function getCurrentVersion(): string { } } -function getDistTag(version: string): string { +export function getDistTag(version: string): string { const prerelease = semver.prerelease(version); if (prerelease && prerelease.length > 0) { return String(prerelease[0]); From 9cd3c1a8a944289c36f5bec42f110b7d8e6ead10 Mon Sep 17 00:00:00 2001 From: Pathik Date: Thu, 25 Jun 2026 16:19:06 +0530 Subject: [PATCH 08/13] version check added before verifying project root --- src/commands/agent.command.ts | 8 ++++---- src/commands/mcp.command.ts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/commands/agent.command.ts b/src/commands/agent.command.ts index faba2971..ef95db67 100644 --- a/src/commands/agent.command.ts +++ b/src/commands/agent.command.ts @@ -527,14 +527,14 @@ export function registerAgentCommand(program: Command) { .option('--plain', 'Disable interactive controls and print merged logs only') .option('--local', 'Install agent from local source (pip install -e .[full]) instead of PyPI') .action(async (options: { port: string; host: string; logLevel: string; plain?: boolean; local?: boolean }) => { + await checkAgentUpdate({ isLocal: Boolean(options.local) }); + validateProjectRoot(); const projectRoot = process.cwd(); // Load consuming project's .env (lives in solid-api/) loadDotenv({ path: path.join(projectRoot, 'solid-api', '.env') }); - await checkAgentUpdate({ isLocal: Boolean(options.local) }); - const agentCommand = options.local ? ensureAgentInstalledLocal() : ensureAgentInstalled(); const agentUiDir = ensureAgentUIInstalled(); @@ -552,14 +552,14 @@ export function registerAgentCommand(program: Command) { .option('-l, --log-level ', 'Logging level', 'INFO') .option('--local', 'Install agent from local source (pip install -e .[full]) instead of PyPI') .action(async (task, options) => { + await checkAgentUpdate({ isLocal: Boolean(options.local) }); + validateProjectRoot(); const projectRoot = process.cwd(); // Load consuming project's .env (lives in solid-api/) loadDotenv({ path: path.join(projectRoot, 'solid-api', '.env') }); - await checkAgentUpdate({ isLocal: Boolean(options.local) }); - const databaseUrl = resolveDatabaseUrl(); const env: Record = { ...process.env as Record, diff --git a/src/commands/mcp.command.ts b/src/commands/mcp.command.ts index b6130010..5f7e4d81 100644 --- a/src/commands/mcp.command.ts +++ b/src/commands/mcp.command.ts @@ -100,8 +100,8 @@ export function registerMcpCommand(program: Command) { .option('--mount-path ', 'Path under which to mount the MCP app', '/mcp') .option('--local', 'Install agent from local source (pip install -e .[full]) instead of PyPI') .action(async (options: { port: string; host: string; logLevel: string; mountPath: string; local?: boolean }) => { - validateProjectRoot(); await checkAgentUpdate({ isLocal: Boolean(options.local) }); + validateProjectRoot(); const env = buildBridgedEnv(); if (!env.DATABASE_URL) { From 5dc0589e1a10ac4527c4ab3c979ca86bd499cff5 Mon Sep 17 00:00:00 2001 From: Oswald Rodrigues Date: Thu, 25 Jun 2026 18:02:49 +0530 Subject: [PATCH 09/13] added metadata folder to nodemon ignore path --- templates/api-template/nodemon.json | 1 + 1 file changed, 1 insertion(+) diff --git a/templates/api-template/nodemon.json b/templates/api-template/nodemon.json index c11a3242..35632355 100644 --- a/templates/api-template/nodemon.json +++ b/templates/api-template/nodemon.json @@ -6,6 +6,7 @@ "ignore": [ "src/**/*.spec.ts", "src/**/migrations/**", + "src/**/metadata/**/*.json", "dist", "node_modules" ], From ab4c8de615815ae5fa56d95abe8f56829845ac88 Mon Sep 17 00:00:00 2001 From: Oswald Rodrigues Date: Fri, 26 Jun 2026 14:14:13 +0530 Subject: [PATCH 10/13] added postinstall post upgrading ui, to ensure themes are copied properly --- src/commands/local-upgrade.command.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/commands/local-upgrade.command.ts b/src/commands/local-upgrade.command.ts index b936b95c..e1ad6450 100644 --- a/src/commands/local-upgrade.command.ts +++ b/src/commands/local-upgrade.command.ts @@ -151,6 +151,8 @@ Required environment variables: if (doUi) { console.log('\n=== solid-ui → solid-ui ==='); packAndInstall(process.env.SOLID_UI_PATH!, './solid-ui', showNpmLogs); + console.log('\n▶ Syncing UI resources (postinstall)'); + exec('npm run postinstall', './solid-ui'); } if (doCodeBuilder) { From 0223b25a20fd736e80d125df21c43abb40778d15 Mon Sep 17 00:00:00 2001 From: Oswald Rodrigues Date: Fri, 26 Jun 2026 14:14:25 +0530 Subject: [PATCH 11/13] 0.1.46-beta.1 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4693f8f4..4f9e382f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@solidxai/solidctl", - "version": "0.1.46-beta.0", + "version": "0.1.46-beta.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@solidxai/solidctl", - "version": "0.1.46-beta.0", + "version": "0.1.46-beta.1", "license": "BUSL-1.1", "dependencies": { "@electric-sql/pglite": "^0.5.3", diff --git a/package.json b/package.json index 5c4b22c0..57a88220 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@solidxai/solidctl", - "version": "0.1.46-beta.0", + "version": "0.1.46-beta.1", "description": "", "author": "", "private": false, From 6cd585ee18c69cde351500455bfcfdeae2b7c556 Mon Sep 17 00:00:00 2001 From: SaibazKhan Date: Mon, 29 Jun 2026 11:03:18 +0530 Subject: [PATCH 12/13] remove comments --- src/commands/mcp.command.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands/mcp.command.ts b/src/commands/mcp.command.ts index 7f45c685..f146f928 100644 --- a/src/commands/mcp.command.ts +++ b/src/commands/mcp.command.ts @@ -13,10 +13,10 @@ import { ensureAgentInstalled, ensureAgentInstalledLocal } from './agent-helper' * SQL Server using mssql+pyodbc. Otherwise it defaults to PostgreSQL. */ function resolveDatabaseUrl(): string | undefined { - console.log('Before resolving DATABASE_URL', process.env.DATABASE_URL); + // console.log('Before resolving DATABASE_URL', process.env.DATABASE_URL); if (process.env.DATABASE_URL){ const resolvedUrl = normalizeDatabaseUrl(process.env.DATABASE_URL); - console.log('After resolving DATABASE_URL',resolvedUrl); + // console.log('After resolving DATABASE_URL',resolvedUrl); return resolvedUrl; } From 779fdba9ae0e7345c51b252641d29e67847df64f Mon Sep 17 00:00:00 2001 From: Pathik Date: Mon, 29 Jun 2026 18:11:19 +0530 Subject: [PATCH 13/13] Extract MCP launch logic and integrate MCP into start supervisor --- src/commands/agent-helper.ts | 51 +++++--- src/commands/mcp-launch.spec.ts | 41 +++++++ src/commands/mcp-launch.ts | 181 +++++++++++++++++++++++++++ src/commands/mcp.command.ts | 81 +------------ src/commands/start.command.ts | 209 +++++++++++++++++++++++++------- 5 files changed, 427 insertions(+), 136 deletions(-) create mode 100644 src/commands/mcp-launch.spec.ts create mode 100644 src/commands/mcp-launch.ts diff --git a/src/commands/agent-helper.ts b/src/commands/agent-helper.ts index 857b6a13..a8c41df5 100644 --- a/src/commands/agent-helper.ts +++ b/src/commands/agent-helper.ts @@ -16,6 +16,25 @@ const AGENT_UI_DIR = path.join(os.homedir(), '.solidx', 'agent-ui'); const AGENT_UI_PKG_DIR = path.join(AGENT_UI_DIR, 'node_modules', '@solidxai', 'agent-ui'); const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm'; +type ExitMode = 'exit' | 'throw'; + +export class AgentCommandExit extends Error { + constructor( + public readonly exitCode: number, + message?: string, + ) { + super(message ?? `Command exited with code ${exitCode}`); + this.name = 'AgentCommandExit'; + } +} + +function exitCommand(exitCode: number, exitMode: ExitMode, message?: string): never { + if (exitMode === 'throw') { + throw new AgentCommandExit(exitCode, message); + } + process.exit(exitCode); +} + /** * Check if a command exists in PATH and returns exit code 0. */ @@ -303,7 +322,7 @@ async function getLatestAgentRelease(track: 'stable' | 'beta'): Promise { +export async function checkAgentUpdate(opts: { isLocal: boolean; exitMode?: ExitMode }): Promise { if (opts.isLocal) return; const currentRaw = getAgentVersion(); const current = toSemver(currentRaw); @@ -376,7 +395,7 @@ export async function checkAgentUpdate(opts: { isLocal: boolean }): Promise { + it('exposes the expected default values', () => { + expect(MCP_DEFAULT_OPTIONS).toEqual({ + port: '9000', + host: '0.0.0.0', + logLevel: 'INFO', + mountPath: '/mcp', + }); + }); +}); + +describe('buildBridgedEnv', () => { + it('sets SOLIDX_PROJECT_ROOT to the provided project root', () => { + const env = buildBridgedEnv('/tmp/fake-project'); + expect(env.SOLIDX_PROJECT_ROOT).toBe('/tmp/fake-project'); + }); + + it('includes DATABASE_URL when already present in process.env', () => { + const original = process.env.DATABASE_URL; + process.env.DATABASE_URL = 'postgresql://user:pass@localhost:5432/db'; + try { + const env = buildBridgedEnv('/tmp/fake-project'); + expect(env.DATABASE_URL).toBeDefined(); + } finally { + if (original === undefined) delete process.env.DATABASE_URL; + else process.env.DATABASE_URL = original; + } + }); + + it('returns an object that includes process.env keys', () => { + process.env.MCP_LAUNCH_TEST_VAR = 'hello'; + try { + const env = buildBridgedEnv('/tmp/fake-project'); + expect(env.MCP_LAUNCH_TEST_VAR).toBe('hello'); + } finally { + delete process.env.MCP_LAUNCH_TEST_VAR; + } + }); +}); diff --git a/src/commands/mcp-launch.ts b/src/commands/mcp-launch.ts new file mode 100644 index 00000000..17b2370c --- /dev/null +++ b/src/commands/mcp-launch.ts @@ -0,0 +1,181 @@ +import { spawn } from 'child_process'; +import path from 'path'; +import { config as loadDotenv } from 'dotenv'; +import { normalizeDatabaseUrl } from '../helper'; +import { + checkAgentUpdate, + ensureAgentInstalled, + ensureAgentInstalledLocal, + getAgentVersion, + printAgentVersion, +} from './agent-helper'; + +export interface McpOptions { + port: string; + host: string; + logLevel: string; + mountPath: string; + local?: boolean; +} + +export const MCP_DEFAULT_OPTIONS: McpOptions = { + port: '9000', + host: '0.0.0.0', + logLevel: 'INFO', + mountPath: '/mcp', +}; + +/** + * Build a DATABASE_URL from the consuming project's individual DB env vars, + * unless DATABASE_URL is already explicitly set. + * + * If SOLID_CORE_DB_TYPE is set to "mssql", the URL is built for + * SQL Server using mssql+pyodbc. Otherwise it defaults to PostgreSQL. + */ +function resolveDatabaseUrl(): string | undefined { + if (process.env.DATABASE_URL) { + return normalizeDatabaseUrl(process.env.DATABASE_URL); + } + + const host = process.env.DEFAULT_DATABASE_HOST; + const port = process.env.DEFAULT_DATABASE_PORT; + const user = process.env.DEFAULT_DATABASE_USER; + const password = process.env.DEFAULT_DATABASE_PASSWORD; + const name = process.env.DEFAULT_DATABASE_NAME; + const dbType = (process.env.SOLID_CORE_DB_TYPE || 'postgres').toLowerCase(); + + if (!host || !port || !user || !name) return undefined; + + const encodedPw = password ? ':' + encodeURIComponent(password) : ''; + + if (dbType === 'mssql') { + return `mssql+pyodbc://${user}${encodedPw}@${host}:${port}/${name}?driver=ODBC+Driver+18+for+SQL+Server&TrustServerCertificate=yes&Encrypt=no`; + } + + if (dbType === 'mysql') { + return `mysql+pymysql://${user}${encodedPw}@${host}:${port}/${name}`; + } + + return `postgresql://${user}${encodedPw}@${host}:${port}/${name}`; +} + +/** + * Build the bridged environment object for the MCP server. + * + * Loads the consuming project's solid-api/.env, resolves DATABASE_URL from + * individual DB vars if needed, and sets SOLIDX_PROJECT_ROOT to the consuming + * project root. Returns a new object instead of mutating process.env so it is + * safe to reuse inside a long-running supervisor. + */ +export function buildBridgedEnv( + projectRoot = process.cwd(), +): Record { + loadDotenv({ path: path.join(projectRoot, 'solid-api', '.env') }); + + const databaseUrl = resolveDatabaseUrl(); + + return { + ...(process.env as Record), + SOLIDX_PROJECT_ROOT: projectRoot, + ...(databaseUrl ? { DATABASE_URL: databaseUrl } : {}), + ...(process.env.BASE_URL ? { BASE_URL: process.env.BASE_URL } : {}), + ...(process.env.FRONTEND_BASE_URL + ? { FRONTEND_BASE_URL: process.env.FRONTEND_BASE_URL } + : {}), + ...(process.env.APP_ENCRYPTION_KEY + ? { APP_ENCRYPTION_KEY: process.env.APP_ENCRYPTION_KEY } + : {}), + }; +} + +/** + * Print which critical env vars were bridged vs. missing. + */ +export function printBridgeSummary(env: Record): void { + const bridgedKeys = [ + 'DATABASE_URL', + 'SOLIDX_PROJECT_ROOT', + 'BASE_URL', + 'FRONTEND_BASE_URL', + 'APP_ENCRYPTION_KEY', + ]; + const bridged = bridgedKeys.filter((k) => env[k]); + const missing = bridgedKeys.filter((k) => !env[k]); + console.log(`✔ Bridged env: ${bridged.join(', ') || 'none'}`); + if (missing.length) console.warn(`⚠ Missing env: ${missing.join(', ')}`); +} + +export interface McpLaunchConfig { + command: string; + args: string[]; + cwd: string; + env: Record; +} + +/** + * Resolve the agent binary, build the bridged env, and return the full launch + * config for the MCP server. Performs agent update check and install before + * returning. Validates that DATABASE_URL is present. + * + * Throws if DATABASE_URL is missing. + */ +export async function resolveMcpLaunchConfig( + options: McpOptions, + projectRoot = process.cwd(), +): Promise { + await checkAgentUpdate({ + isLocal: Boolean(options.local), + exitMode: 'throw', + }); + + const env = buildBridgedEnv(projectRoot); + + if (!env.DATABASE_URL) { + throw new Error( + 'DATABASE_URL is required for MCP mode.\n' + + ' It is needed to validate API keys and write audit logs.\n' + + ' Set it in your solid-api/.env or environment.', + ); + } + + const agentCommand = options.local + ? ensureAgentInstalledLocal('throw') + : ensureAgentInstalled('throw'); + printAgentVersion(agentCommand); + + return { + command: agentCommand, + args: [ + 'mcp-remote', + '--host', + options.host, + '--port', + options.port, + '--log-level', + options.logLevel, + '--mount-path', + options.mountPath, + ], + cwd: env.SOLIDX_PROJECT_ROOT, + env, + }; +} + +/** + * Print agent version via the shared helper (re-exported for convenience). + */ +export { getAgentVersion, printAgentVersion }; + +/** + * Spawn the MCP server as a long-running child process with piped stdio. + * Intended for use inside a supervisor that manages the process lifecycle. + */ +export function spawnMcpServer(launchConfig: McpLaunchConfig) { + return spawn(launchConfig.command, launchConfig.args, { + cwd: launchConfig.cwd, + env: launchConfig.env, + stdio: ['ignore', 'pipe', 'pipe'], + shell: process.platform === 'win32', + detached: process.platform !== 'win32', + }); +} diff --git a/src/commands/mcp.command.ts b/src/commands/mcp.command.ts index fab23dec..46f81aab 100644 --- a/src/commands/mcp.command.ts +++ b/src/commands/mcp.command.ts @@ -1,85 +1,8 @@ import { Command } from 'commander'; import { spawnSync } from 'child_process'; -import path from 'path'; -import { config as loadDotenv } from 'dotenv'; -import { normalizeDatabaseUrl, validateProjectRoot } from '../helper'; +import { validateProjectRoot } from '../helper'; import { checkAgentUpdate, ensureAgentInstalled, ensureAgentInstalledLocal, getAgentVersion, printAgentVersion } from './agent-helper'; - -/** - * Build a DATABASE_URL from the consuming project's individual DB env vars, - * unless DATABASE_URL is already explicitly set. - * - * If SOLID_CORE_DB_TYPE is set to "mssql", the URL is built for - * SQL Server using mssql+pyodbc. Otherwise it defaults to PostgreSQL. - */ -function resolveDatabaseUrl(): string | undefined { - // console.log('Before resolving DATABASE_URL', process.env.DATABASE_URL); - if (process.env.DATABASE_URL){ - const resolvedUrl = normalizeDatabaseUrl(process.env.DATABASE_URL); - // console.log('After resolving DATABASE_URL',resolvedUrl); - return resolvedUrl; - } - - const host = process.env.DEFAULT_DATABASE_HOST; - const port = process.env.DEFAULT_DATABASE_PORT; - const user = process.env.DEFAULT_DATABASE_USER; - const password = process.env.DEFAULT_DATABASE_PASSWORD; - const name = process.env.DEFAULT_DATABASE_NAME; - const dbType = (process.env.SOLID_CORE_DB_TYPE || 'postgres').toLowerCase(); - - if (!host || !port || !user || !name) return undefined; - - const encodedPw = password ? ':' + encodeURIComponent(password) : ''; - - if (dbType === 'mssql') { - return `mssql+pyodbc://${user}${encodedPw}@${host}:${port}/${name}?driver=ODBC+Driver+18+for+SQL+Server&TrustServerCertificate=yes&Encrypt=no`; - } - - if (dbType === 'mysql') { - return `mysql+pymysql://${user}${encodedPw}@${host}:${port}/${name}`; - } - - // Default to PostgreSQL - console.log(`Final Url postgresql://${user}${encodedPw}@${host}:${port}/${name}`); - - return `postgresql://${user}${encodedPw}@${host}:${port}/${name}`; -} - -/** - * Build the bridged environment object for the MCP server. - * - * Mirrors the env-bridging logic in agent.command.ts — loads the consuming - * project's solid-api/.env, resolves DATABASE_URL from individual DB vars - * if needed, and sets SOLIDX_PROJECT_ROOT to the consuming project root. - */ -function buildBridgedEnv(): Record { - const projectRoot = process.cwd(); - - // Load consuming project's .env (lives in solid-api/) - loadDotenv({ path: path.join(projectRoot, 'solid-api', '.env') }); - - const databaseUrl = resolveDatabaseUrl(); - - return { - ...(process.env as Record), - SOLIDX_PROJECT_ROOT: projectRoot, - ...(databaseUrl ? { DATABASE_URL: databaseUrl } : {}), - ...(process.env.BASE_URL ? { BASE_URL: process.env.BASE_URL } : {}), - ...(process.env.FRONTEND_BASE_URL ? { FRONTEND_BASE_URL: process.env.FRONTEND_BASE_URL } : {}), - ...(process.env.APP_ENCRYPTION_KEY ? { APP_ENCRYPTION_KEY: process.env.APP_ENCRYPTION_KEY } : {}), - }; -} - -/** - * Print which critical env vars were bridged vs. missing. - */ -function printBridgeSummary(env: Record): void { - const bridgedKeys = ['DATABASE_URL', 'SOLIDX_PROJECT_ROOT', 'BASE_URL', 'FRONTEND_BASE_URL', 'APP_ENCRYPTION_KEY']; - const bridged = bridgedKeys.filter((k) => env[k]); - const missing = bridgedKeys.filter((k) => !env[k]); - console.log(`✔ Bridged env: ${bridged.join(', ') || 'none'}`); - if (missing.length) console.warn(`⚠ Missing env: ${missing.join(', ')}`); -} +import { buildBridgedEnv, printBridgeSummary } from './mcp-launch'; export function registerMcpCommand(program: Command) { const mcp = program diff --git a/src/commands/start.command.ts b/src/commands/start.command.ts index 3821f57c..1ec87099 100644 --- a/src/commands/start.command.ts +++ b/src/commands/start.command.ts @@ -12,10 +12,15 @@ import { startEmbeddedServer, stopEmbeddedServer, } from '../db/embedded'; +import { AgentCommandExit } from './agent-helper'; +import { MCP_DEFAULT_OPTIONS, resolveMcpLaunchConfig, spawnMcpServer, type McpLaunchConfig } from './mcp-launch'; -type ServiceName = 'api' | 'ui'; +type ServiceName = 'api' | 'ui' | 'mcp'; -type ServiceScripts = Record; +/** Services that are launched via an npm script. MCP is spawned directly. */ +type ScriptServiceName = 'api' | 'ui'; + +type ServiceScripts = Record; type ServiceConfig = { cwd: string; @@ -36,6 +41,7 @@ type StartOptions = { controls?: boolean; plain?: boolean; ui?: boolean; + mcp?: boolean; }; class StartSupervisor { @@ -48,8 +54,13 @@ class StartSupervisor { private readonly isInteractive: boolean; private readonly npmCommand: string; private readonly isEmbedded: boolean; + private readonly supportsMcp: boolean; + private mcpPort: string; private embeddedServer: EmbeddedServerHandle | null = null; + private mcpLaunchConfig: McpLaunchConfig | null = null; private shuttingDown = false; + private startupPhase = true; + private startupAbortInProgress = false; private exitCode = 0; private stdinWasRaw = false; @@ -57,14 +68,17 @@ class StartSupervisor { private readonly projectRoot: string, serviceScripts: ServiceScripts, options: StartOptions, + supportsMcp = false, ) { this.isInteractive = Boolean(process.stdout.isTTY && process.stdin.isTTY && !options.plain); this.npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm'; this.isEmbedded = isEmbeddedProject(projectRoot); + this.supportsMcp = supportsMcp; this.serviceScripts = serviceScripts; this.activeServices = this.resolveActiveServices(options); this.apiPort = this.resolveApiPort(); this.uiPort = this.resolveUiPort(); + this.mcpPort = MCP_DEFAULT_OPTIONS.port; this.serviceConfigs = { api: { @@ -77,40 +91,67 @@ class StartSupervisor { label: 'ui', color: chalk.magenta, }, + mcp: { + cwd: projectRoot, + label: 'mcp', + color: chalk.green, + }, }; this.serviceStates = { api: this.createInitialState(), ui: this.createInitialState(), + mcp: this.createInitialState(), }; } async start() { this.attachSignalHandlers(); - this.attachKeyboardControls(); this.printStatus('Starting SolidX dev processes'); - if (this.isEmbedded) { - await this.startEmbeddedDatabase(); - } + let shouldStartServices = true; - for (const serviceName of this.activeServices) { - this.spawnService(serviceName); - } - this.renderFooter(); + try { + if (this.isEmbedded) { + await this.startEmbeddedDatabase(); + shouldStartServices = !this.shuttingDown; + } - await new Promise((resolve) => { - const poll = setInterval(() => { - if (!this.hasRunningChildren()) { - clearInterval(poll); - this.cleanupTerminal(); - resolve(); - } - }, 100); - }); + if (shouldStartServices && this.activeServices.includes('mcp')) { + await this.startMcpServer(); + shouldStartServices = !this.shuttingDown; + } - await this.stopEmbeddedDatabase(); + if (shouldStartServices) { + this.startupPhase = false; + this.attachKeyboardControls(); + for (const serviceName of this.activeServices) { + this.spawnService(serviceName); + } + this.renderFooter(); + + await new Promise((resolve) => { + const poll = setInterval(() => { + if (!this.hasRunningChildren()) { + clearInterval(poll); + resolve(); + } + }, 100); + }); + } + } catch (error) { + if (error instanceof AgentCommandExit) { + this.exitCode = error.exitCode; + } else if (this.exitCode === 0) { + this.exitCode = 1; + } + this.shuttingDown = true; + } finally { + this.startupPhase = false; + await this.stopEmbeddedDatabase(); + this.cleanupTerminal(); + } process.exit(this.exitCode); } @@ -129,12 +170,17 @@ class StartSupervisor { try { await this.embeddedServer.ready; + if (this.shuttingDown) { + return; + } this.printStatus(`Embedded database ready on ${config.host}:${config.port}`); } catch (error) { + if (this.shuttingDown) { + return; + } const message = error instanceof Error ? error.message : String(error); this.printStatus(`Embedded database failed to start: ${message}`); - this.embeddedServer = null; - process.exit(1); + throw error; } } @@ -147,6 +193,31 @@ class StartSupervisor { this.embeddedServer = null; } + private async startMcpServer() { + this.printStatus('Starting MCP server'); + + try { + this.mcpLaunchConfig = await resolveMcpLaunchConfig( + { ...MCP_DEFAULT_OPTIONS }, + this.projectRoot, + ); + this.mcpPort = this.mcpLaunchConfig.args[ + this.mcpLaunchConfig.args.indexOf('--port') + 1 + ]; + this.printStatus(`MCP server config resolved (port ${this.mcpPort})`); + } catch (error) { + if (this.shuttingDown) { + return; + } + if (error instanceof AgentCommandExit) { + throw error; + } + const message = error instanceof Error ? error.message : String(error); + this.printStatus(`MCP server failed to initialize: ${message}`); + throw error; + } + } + private createInitialState(): ServiceState { return { child: null, @@ -168,24 +239,48 @@ class StartSupervisor { selectedServices.push('ui'); } - return selectedServices.length ? selectedServices : ['api', 'ui']; + if (this.supportsMcp && options.mcp) { + selectedServices.push('mcp'); + } + + if (selectedServices.length) { + return selectedServices; + } + + // No explicit selection: default to all available services. + const defaults: ServiceName[] = ['api', 'ui']; + if (this.supportsMcp) { + defaults.push('mcp'); + } + return defaults; } private spawnService(serviceName: ServiceName) { const config = this.serviceConfigs[serviceName]; const state = this.serviceStates[serviceName]; - const scriptName = this.serviceScripts[serviceName]; state.restartRequested = false; state.stoppingForShutdown = false; - const child = spawn(this.npmCommand, ['run', scriptName], { - cwd: config.cwd, - env: process.env, - stdio: ['ignore', 'pipe', 'pipe'], - shell: false, - detached: process.platform !== 'win32', - }); + let child: ChildProcess; + + if (serviceName === 'mcp') { + if (!this.mcpLaunchConfig) { + this.printLog(serviceName, 'MCP launch config not available', true); + this.handleUnexpectedExit(serviceName, 1); + return; + } + child = spawnMcpServer(this.mcpLaunchConfig); + } else { + const scriptName = this.serviceScripts[serviceName as ScriptServiceName]; + child = spawn(this.npmCommand, ['run', scriptName], { + cwd: config.cwd, + env: process.env, + stdio: ['ignore', 'pipe', 'pipe'], + shell: false, + detached: process.platform !== 'win32', + }); + } state.child = child; this.printStatus(`Started ${config.label}`); @@ -279,13 +374,15 @@ class StartSupervisor { const controlSegments = ['q quit', 'c clear']; if (this.activeServices.includes('api')) { - serviceSegments.push(`API (a restart, d open docs, :${this.apiPort})`); - controlSegments.push('a restart API'); + serviceSegments.push(`API (a restart, d docs, :${this.apiPort})`); } if (this.activeServices.includes('ui')) { serviceSegments.push(`UI (u restart, o open, :${this.uiPort})`); - controlSegments.push('u restart UI'); + } + + if (this.activeServices.includes('mcp')) { + serviceSegments.push(`MCP (m restart, :${this.mcpPort})`); } if (this.activeServices.length > 1) { @@ -409,6 +506,11 @@ class StartSupervisor { this.restartService('ui'); } break; + case 'm': + if (this.activeServices.includes('mcp')) { + this.restartService('mcp'); + } + break; case 'r': for (const serviceName of this.activeServices) { this.restartService(serviceName); @@ -532,6 +634,14 @@ class StartSupervisor { this.shuttingDown = true; this.printStatus('Shutting down'); + if (this.startupPhase) { + if (!this.startupAbortInProgress) { + this.startupAbortInProgress = true; + void this.abortStartup(); + } + return; + } + for (const serviceName of this.activeServices) { const state = this.serviceStates[serviceName]; state.stoppingForShutdown = true; @@ -542,6 +652,12 @@ class StartSupervisor { this.renderFooter(); } + private async abortStartup() { + await this.stopEmbeddedDatabase(); + this.cleanupTerminal(); + process.exit(this.exitCode); + } + private hasRunningChildren() { return this.activeServices.some((serviceName) => { return this.serviceStates[serviceName].child !== null; @@ -639,42 +755,53 @@ export function registerStartCommand(program: Command) { commandName: string, description: string, serviceScripts: ServiceScripts, + supportsMcp = false, ) => { - program + const command = program .command(commandName) .description(description) .option('--api', 'Only supervise solid-api') .option('--controls', 'Enable interactive controls with pinned footer and keyboard shortcuts (default on TTYs)') .option('--plain', 'Disable interactive controls and print merged logs only') - .option('--ui', 'Only supervise solid-ui') - .action(async (options: StartOptions) => { + .option('--ui', 'Only supervise solid-ui'); + + if (supportsMcp) { + command.option('--mcp', 'Only supervise the MCP server'); + } + + command.action(async (options: StartOptions) => { validateProjectRoot(); const selectedServices = [ ...(options.api ? (['api'] as const) : []), ...(options.ui ? (['ui'] as const) : []), + ...(supportsMcp && options.mcp ? (['mcp'] as const) : []), ]; - const activeServices = selectedServices.length ? selectedServices : (['api', 'ui'] as const); + const activeServices = selectedServices.length ? selectedServices : ( + supportsMcp ? (['api', 'ui', 'mcp'] as const) : (['api', 'ui'] as const) + ); for (const serviceName of activeServices) { + if (serviceName === 'mcp') continue; validateProjectScript( serviceName === 'api' ? 'solid-api' : 'solid-ui', - serviceScripts[serviceName], + serviceScripts[serviceName as ScriptServiceName], ); } - const supervisor = new StartSupervisor(process.cwd(), serviceScripts, options); + const supervisor = new StartSupervisor(process.cwd(), serviceScripts, options, supportsMcp); await supervisor.start(); }); }; registerSupervisorCommand( 'start:dev', - 'Start solid-api and solid-ui dev processes in a single supervisor', + 'Start solid-api, solid-ui, and MCP dev processes in a single supervisor', { api: 'solidx:dev', ui: 'solidx:dev', }, + true, ); registerSupervisorCommand(