Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
62 changes: 33 additions & 29 deletions packages/cli/src/commands/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,31 @@
* 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';
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<void> {
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();

Expand Down Expand Up @@ -79,37 +96,24 @@ export async function buildCommand() {
const tscSpinner = ora('Compiling TypeScript...').start();

try {
await new Promise<void>((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);
}

Expand Down
60 changes: 40 additions & 20 deletions packages/cli/src/commands/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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();
Expand Down Expand Up @@ -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');
Expand All @@ -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();
Expand Down
Loading