diff --git a/packages/cli/package.json b/packages/cli/package.json index 377be96..2129f6b 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -34,6 +34,7 @@ "chalk": "^5.3.0", "chokidar": "^4.0.0", "commander": "^12.0.0", + "execa": "^9.5.0", "fs-extra": "^11.2.0", "glob": "^11.0.0", "ora": "^8.1.0", diff --git a/packages/cli/src/commands/build.ts b/packages/cli/src/commands/build.ts index 8586b25..45034e0 100644 --- a/packages/cli/src/commands/build.ts +++ b/packages/cli/src/commands/build.ts @@ -4,7 +4,7 @@ * Builds UI components and compiles TypeScript for production deployment. * Does NOT start the server - use 'leanmcp start' or 'node dist/main.js' after building. */ -import { spawn } from 'child_process'; +import { execa } from 'execa'; import ora from 'ora'; import path from 'path'; import fs from 'fs-extra'; @@ -12,6 +12,23 @@ import { logger } from '../logger'; import { scanUIApp, buildUIComponent, writeUIManifest } from '../vite'; import { generateSchemaMetadata } from '../schema-extractor'; +/** + * Run TypeScript compiler with reliable cross-platform output capture + */ +async function runTypeScriptCompiler(cwd: string): Promise { + try { + await execa('npx', ['tsc'], { + cwd, + preferLocal: true, + all: true, // Combine stdout + stderr into error.all + }); + } catch (error: any) { + // execa includes full output in error.all + const output = error.all || error.stdout || error.stderr || error.message; + throw new Error(output || `tsc exited with code ${error.exitCode}`); + } +} + export async function buildCommand() { const cwd = process.cwd(); @@ -79,37 +96,24 @@ export async function buildCommand() { const tscSpinner = ora('Compiling TypeScript...').start(); try { - await new Promise((resolve, reject) => { - const tsc = spawn('npx', ['tsc'], { - cwd, - stdio: 'pipe', - shell: true, - }); - - let stdout = ''; - let stderr = ''; - - tsc.stdout?.on('data', (data) => { - stdout += data; - }); - - tsc.stderr?.on('data', (data) => { - stderr += data; - }); - - tsc.on('close', (code) => { - if (code === 0) resolve(); - // TypeScript outputs compilation errors to stdout, not stderr - else reject(new Error(stdout || stderr || `tsc exited with code ${code}`)); - }); - - tsc.on('error', reject); - }); - + await runTypeScriptCompiler(cwd); tscSpinner.succeed('TypeScript compiled'); } catch (error) { tscSpinner.fail('TypeScript compilation failed'); - logger.error(error instanceof Error ? error.message : String(error)); + + // Display the full error output with proper formatting + const errorMessage = error instanceof Error ? error.message : String(error); + + // Log each line for proper formatting + const lines = errorMessage.split('\n'); + for (const line of lines) { + if (line.includes('error TS')) { + logger.error(line); + } else if (line.trim()) { + logger.log(line); + } + } + process.exit(1); } diff --git a/packages/cli/src/commands/deploy.ts b/packages/cli/src/commands/deploy.ts index 4bb9f46..35cff98 100644 --- a/packages/cli/src/commands/deploy.ts +++ b/packages/cli/src/commands/deploy.ts @@ -397,14 +397,22 @@ export async function deployCommand(folderPath: string, options: DeployOptions = logger.gray(` URL: ${existingConfig.url}`); logger.gray(` Last deployed: ${existingConfig.lastDeployedAt}\n`); - const choice = await select({ - message: 'What would you like to do?', - choices: [ - { value: 'update', name: `Update existing deployment '${existingConfig.projectName}'` }, - { value: 'new', name: 'Create a new project with a random name' }, - { value: 'cancel', name: 'Cancel deployment' }, - ], - }); + let choice: 'update' | 'new' | 'cancel'; + + if (options.skipConfirm) { + // Non-interactive: auto-update + choice = 'update'; + logger.info('Auto-updating existing deployment (--yes flag)\n'); + } else { + choice = await select({ + message: 'What would you like to do?', + choices: [ + { value: 'update', name: `Update existing deployment '${existingConfig.projectName}'` }, + { value: 'new', name: 'Create a new project with a random name' }, + { value: 'cancel', name: 'Cancel deployment' }, + ], + }); + } if (choice === 'cancel') { logger.gray('\nDeployment cancelled.\n'); @@ -416,8 +424,10 @@ export async function deployCommand(folderPath: string, options: DeployOptions = projectName = existingConfig.projectName; subdomain = existingConfig.subdomain; isUpdate = true; - logger.warn('\nUpdating existing deployment...'); - logger.gray('The previous version will be replaced.\n'); + if (!options.skipConfirm) { + logger.warn('\nUpdating existing deployment...'); + logger.gray('The previous version will be replaced.\n'); + } } else { // Generate new random name projectName = generateProjectName(); @@ -456,14 +466,22 @@ export async function deployCommand(folderPath: string, options: DeployOptions = if (matchingProject) { logger.warn(`Project '${folderName}' already exists.\n`); - const choice = await select({ - message: 'What would you like to do?', - choices: [ - { value: 'update', name: `Update existing project '${folderName}'` }, - { value: 'new', name: 'Create a new project with a random name' }, - { value: 'cancel', name: 'Cancel deployment' }, - ], - }); + let choice: 'update' | 'new' | 'cancel'; + + if (options.skipConfirm) { + // Non-interactive: auto-update + choice = 'update'; + logger.info('Auto-updating existing project (--yes flag)\n'); + } else { + choice = await select({ + message: 'What would you like to do?', + choices: [ + { value: 'update', name: `Update existing project '${folderName}'` }, + { value: 'new', name: 'Create a new project with a random name' }, + { value: 'cancel', name: 'Cancel deployment' }, + ], + }); + } if (choice === 'cancel') { logger.gray('\nDeployment cancelled.\n'); @@ -474,8 +492,10 @@ export async function deployCommand(folderPath: string, options: DeployOptions = existingProject = matchingProject; projectName = matchingProject.name; isUpdate = true; - logger.warn('\nWARNING: This will replace the existing deployment.'); - logger.gray('The previous version will be overwritten.\n'); + if (!options.skipConfirm) { + logger.warn('\nWARNING: This will replace the existing deployment.'); + logger.gray('The previous version will be overwritten.\n'); + } } else { // Generate new random name projectName = generateProjectName();