fix(cli): add bootstrap fast paths (#6188)

* fix(cli): add bootstrap fast paths

* fix(cli): address bootstrap review feedback

* test(core): make MCP retry backoff test deterministic

* fix(cli): address bootstrap validation feedback

* fix(cli): keep global-flag MCP invocations on full parser

* fix(cli): harden bootstrap review gaps

* fix(cli): copy package wrapper from script directory

* fix(cli): cover bootstrap review edge cases

* test(cli): cover bootstrap fallback paths

* fix(cli): minimize wrapper version imports

---------

Co-authored-by: 易良 <1204183885@qq.com>
This commit is contained in:
Gaurav 2026-07-03 03:58:11 +05:30 committed by GitHub
parent e6e939e020
commit 2cb0031160
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 1168 additions and 206 deletions

View file

@ -82,7 +82,7 @@ const external = [
const BUNDLE_CHUNK_DIR = 'chunks';
const mainBuild = esbuild.build({
entryPoints: { cli: 'packages/cli/index.ts' },
entryPoints: { cli: 'packages/cli/src/cli.ts' },
bundle: true,
outdir: 'dist',
entryNames: '[name]',

View file

@ -6,145 +6,8 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { initStartupProfiler } from './src/utils/startupProfiler.js';
import { isServeFastPathArgv } from './src/serve/fast-path-argv.js';
// Must run before any other imports to capture the earliest possible T0.
initStartupProfiler();
import { initCpuProfiler } from './src/utils/cpuProfiler.js';
// Initialize early to register SIGUSR1 handler and start recording when
// QWEN_CODE_CPU_PROFILE=1, capturing as much of the startup as possible.
initCpuProfiler();
import { runCliEntryPoint } from './src/cli.js';
// --- Global Entry Point ---
function writeStderrLine(line: string): void {
process.stderr.write(line.endsWith('\n') ? line : `${line}\n`);
}
// Suppress known race conditions in @lydell/node-pty.
//
// PTY errors that are expected due to timing races between process exit
// and I/O operations. These should not crash the app.
//
// References:
// - https://github.com/microsoft/node-pty/issues/178 (EIO on macOS/Linux)
// - https://github.com/microsoft/node-pty/issues/827 (resize on Windows)
const getErrnoCode = (error: unknown): string | undefined => {
if (!error || typeof error !== 'object') {
return undefined;
}
const code = (error as { code?: unknown }).code;
return typeof code === 'string' ? code : undefined;
};
const isExpectedPtyRaceError = (error: unknown): boolean => {
if (!(error instanceof Error)) {
return false;
}
const message = error.message;
const code = getErrnoCode(error);
// EIO: PTY read race on macOS/Linux - code + PTY context required
// https://github.com/microsoft/node-pty/issues/178
if (
(code === 'EIO' && message.includes('read')) ||
message.includes('read EIO')
) {
return true;
}
// EAGAIN: transient non-blocking read error from PTY fd
if (
(code === 'EAGAIN' && message.includes('read')) ||
message.includes('read EAGAIN')
) {
return true;
}
// PTY-specific resize/exit race errors - require PTY context in message
if (
message.includes('ioctl(2) failed, EBADF') ||
message.includes('Cannot resize a pty that has already exited')
) {
return true;
}
return false;
};
async function runCliEntry(): Promise<void> {
if (isServeFastPathArgv(process.argv.slice(2))) {
const { tryRunServeFastPath } = await import('./src/serve/fast-path.js');
if (await tryRunServeFastPath()) return;
}
const { main } = await import('./src/gemini.js');
await main();
}
async function handleCriticalError(error: unknown): Promise<void> {
const [{ FatalError }, { AlreadyReportedError }] = await Promise.all([
import('@qwen-code/qwen-code-core'),
import('./src/utils/errors.js'),
]);
if (error instanceof FatalError) {
let errorMessage = error.message;
if (!process.env['NO_COLOR']) {
errorMessage = `\x1b[31m${errorMessage}\x1b[0m`;
}
console.error(errorMessage);
process.exit(error.exitCode);
}
// AlreadyReportedError means an upstream layer (e.g. the non-interactive
// stream-error handler) has already written the user-facing message to
// stderr and just wants to surface a non-zero exit code. Don't print
// "An unexpected critical error occurred:" with a stack trace — that
// framing is for genuinely unexpected, programmer-level bugs, and a
// routine 4xx from an upstream API does not qualify.
if (error instanceof AlreadyReportedError) {
process.exit(error.exitCode);
}
console.error('An unexpected critical error occurred:');
if (error instanceof Error) {
console.error(error.stack);
} else {
console.error(String(error));
}
process.exit(1);
}
process.on('uncaughtException', (error) => {
if (isExpectedPtyRaceError(error)) {
return;
}
if (error instanceof Error) {
writeStderrLine(error.stack ?? error.message);
} else {
writeStderrLine(String(error));
}
process.exit(1);
});
runCliEntry().catch((error: unknown) => {
void handleCriticalError(error).catch((handlerError: unknown) => {
console.error('An unexpected critical error occurred:');
console.error('Original error:');
if (error instanceof Error) {
console.error(error.stack);
} else {
console.error(String(error));
}
console.error('Error handler failed:');
if (handlerError instanceof Error) {
console.error(handlerError.stack);
} else {
console.error(String(handlerError));
}
process.exit(1);
});
});
void runCliEntryPoint();

View file

@ -0,0 +1,602 @@
/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import type { Argv } from 'yargs';
import { beforeEach, afterEach, describe, expect, it, vi } from 'vitest';
import {
copyFileSync,
mkdtempSync,
readFileSync,
rmSync,
writeFileSync,
} from 'node:fs';
import { execFileSync } from 'node:child_process';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { FatalError } from '@qwen-code/qwen-code-core';
import { AlreadyReportedError } from './utils/errors.js';
import {
MCP_COMMANDS,
TOP_LEVEL_COMMANDS,
handleCriticalError,
isExpectedPtyRaceError,
resolveBootstrapRoute,
runCliEntry,
runCliEntryPoint,
} from './cli.js';
const mocks = vi.hoisted(() => ({
main: vi.fn(),
tryRunServeFastPath: vi.fn(),
initStartupProfiler: vi.fn(),
initCpuProfiler: vi.fn(),
mcpHandler: vi.fn(),
mcpBuilder: vi.fn(),
mcpListHandler: vi.fn(),
mcpAddHandler: vi.fn(),
getCliVersion: vi.fn(),
}));
vi.mock('./gemini.js', () => ({
main: mocks.main,
}));
vi.mock('./serve/fast-path.js', () => ({
tryRunServeFastPath: mocks.tryRunServeFastPath,
}));
vi.mock('./utils/startupProfiler.js', () => ({
initStartupProfiler: mocks.initStartupProfiler,
}));
vi.mock('./utils/cpuProfiler.js', () => ({
initCpuProfiler: mocks.initCpuProfiler,
}));
vi.mock('./utils/version.js', () => ({
getCliVersion: mocks.getCliVersion,
}));
vi.mock('./commands/mcp.js', () => ({
mcpCommand: {
command: 'mcp',
describe: 'Manage MCP servers',
builder: (yargs: Argv) => {
mocks.mcpBuilder();
return yargs
.command({
command: 'list',
describe: 'List all configured MCP servers',
handler: mocks.mcpListHandler,
})
.command({
command: 'add <name>',
describe: 'Add a server',
handler: mocks.mcpAddHandler,
})
.demandCommand(1, 'You need at least one command before continuing.');
},
handler: mocks.mcpHandler,
},
}));
describe('resolveBootstrapRoute', () => {
it('routes top-level help, version, serve, and mcp correctly', async () => {
expect(resolveBootstrapRoute(['--help'])).toBe('help');
expect(resolveBootstrapRoute(['--version'])).toBe('version');
expect(resolveBootstrapRoute(['mcp', '--version'])).toBe('version');
expect(resolveBootstrapRoute(['serve', '--help'])).toBe('serve');
expect(resolveBootstrapRoute(['mcp', '--help'])).toBe('mcp');
});
it('keeps bundled entrypoint paths out of the route detection', async () => {
expect(resolveBootstrapRoute(['/repo/dist/cli.js', '--help'])).toBe('help');
expect(
resolveBootstrapRoute(['C:\\repo\\dist\\cli.js', 'mcp', '--help']),
).toBe('mcp');
});
it('falls back to the default route for normal interactive startup', async () => {
expect(resolveBootstrapRoute([])).toBe('default');
expect(resolveBootstrapRoute(['--model', 'gpt-4', 'Hello'])).toBe(
'default',
);
expect(resolveBootstrapRoute(['--safe-mode', 'mcp', 'list'])).toBe(
'default',
);
});
it('does not treat values for global flags as positional commands or bootstrap flags', () => {
expect(resolveBootstrapRoute(['--model', 'gpt-4', '--help'])).toBe('help');
expect(resolveBootstrapRoute(['-p', 'hello', '--help'])).toBe('help');
expect(resolveBootstrapRoute(['--model', '-v'])).toBe('default');
});
it('does not treat flags after -- as bootstrap flags', () => {
expect(resolveBootstrapRoute(['--', '--version'])).toBe('default');
expect(resolveBootstrapRoute(['mcp', '--', '--version'])).toBe('mcp');
});
});
describe('runCliEntry', () => {
const savedEnv = {
CLI_VERSION: process.env['CLI_VERSION'],
};
let stdout: string[];
let stderr: string[];
let savedExitCode: string | number | null | undefined;
beforeEach(() => {
stdout = [];
stderr = [];
savedExitCode = process.exitCode;
process.exitCode = undefined;
vi.clearAllMocks();
mocks.tryRunServeFastPath.mockResolvedValue(false);
mocks.getCliVersion.mockResolvedValue('fallback-version');
process.env['CLI_VERSION'] = '9.9.9';
vi.spyOn(process.stdout, 'write').mockImplementation((chunk) => {
stdout.push(String(chunk));
return true;
});
vi.spyOn(process.stderr, 'write').mockImplementation((chunk) => {
stderr.push(String(chunk));
return true;
});
});
afterEach(() => {
process.exitCode = savedExitCode;
if (savedEnv.CLI_VERSION === undefined) {
delete process.env['CLI_VERSION'];
} else {
process.env['CLI_VERSION'] = savedEnv.CLI_VERSION;
}
vi.restoreAllMocks();
});
it('prints the version without loading the full CLI graph', async () => {
await runCliEntry(['--version']);
expect(stdout.join('')).toContain('9.9.9');
expect(mocks.main).not.toHaveBeenCalled();
expect(mocks.tryRunServeFastPath).not.toHaveBeenCalled();
expect(mocks.initStartupProfiler).not.toHaveBeenCalled();
expect(mocks.initCpuProfiler).not.toHaveBeenCalled();
});
it('falls back to getCliVersion when CLI_VERSION is unset', async () => {
delete process.env['CLI_VERSION'];
await runCliEntry(['--version']);
expect(stdout.join('')).toContain('fallback-version');
expect(mocks.getCliVersion).toHaveBeenCalledTimes(1);
expect(mocks.main).not.toHaveBeenCalled();
expect(mocks.tryRunServeFastPath).not.toHaveBeenCalled();
});
it('prints top-level help without loading the full CLI graph', async () => {
await runCliEntry(['--help']);
const helpText = stdout.join('');
expect(helpText).toContain('Usage: qwen [options] [command]');
expect(helpText).toContain('Manage Qwen Code hooks');
expect(helpText).toContain('Manage MCP servers');
expect(helpText).toContain('Run Qwen Code as a local HTTP daemon');
expect(helpText).toContain('--model');
expect(helpText).toContain('-p, --prompt');
expect(helpText).toContain('--safe-mode');
expect(helpText).toContain('-s, --sandbox');
expect(helpText).toContain('-o, --output-format');
expect(helpText).toContain('-r, --resume');
expect(mocks.main).not.toHaveBeenCalled();
expect(mocks.tryRunServeFastPath).not.toHaveBeenCalled();
expect(mocks.initStartupProfiler).not.toHaveBeenCalled();
expect(mocks.initCpuProfiler).not.toHaveBeenCalled();
});
it('routes the MCP help path without booting gemini', async () => {
await runCliEntry(['mcp', '--help']);
expect(stdout.join('')).toContain('Manage MCP servers');
expect(mocks.main).not.toHaveBeenCalled();
expect(mocks.tryRunServeFastPath).not.toHaveBeenCalled();
expect(mocks.initStartupProfiler).not.toHaveBeenCalled();
expect(mocks.initCpuProfiler).not.toHaveBeenCalled();
expect(mocks.mcpBuilder).not.toHaveBeenCalled();
});
it('does not execute MCP subcommands when showing subcommand help', async () => {
await runCliEntry(['mcp', 'list', '--help']);
const helpText = stdout.join('');
expect(helpText).toContain('List all configured MCP servers');
expect(mocks.mcpListHandler).not.toHaveBeenCalled();
expect(mocks.main).not.toHaveBeenCalled();
expect(mocks.initStartupProfiler).not.toHaveBeenCalled();
expect(mocks.initCpuProfiler).not.toHaveBeenCalled();
});
it('executes MCP subcommands through the fast path', async () => {
await runCliEntry(['mcp', 'list']);
expect(mocks.mcpListHandler).toHaveBeenCalledTimes(1);
expect(mocks.main).not.toHaveBeenCalled();
expect(mocks.initStartupProfiler).not.toHaveBeenCalled();
expect(mocks.initCpuProfiler).not.toHaveBeenCalled();
});
it('executes MCP subcommands after -- through the fast path', async () => {
await runCliEntry(['mcp', '--', 'list']);
expect(mocks.mcpListHandler).toHaveBeenCalledTimes(1);
expect(mocks.main).not.toHaveBeenCalled();
expect(mocks.initStartupProfiler).not.toHaveBeenCalled();
expect(mocks.initCpuProfiler).not.toHaveBeenCalled();
});
it('uses the full CLI when global flags precede MCP commands', async () => {
await runCliEntry(['--safe-mode', 'mcp', 'list']);
expect(mocks.main).toHaveBeenCalledTimes(1);
expect(mocks.mcpListHandler).not.toHaveBeenCalled();
});
it('fails MCP fast-path validation without loading the full CLI', async () => {
await runCliEntry(['mcp', 'doesnotexist']);
expect(process.exitCode).toBe(1);
expect(stderr.join('')).toContain('Unknown command: doesnotexist');
expect(mocks.mcpListHandler).not.toHaveBeenCalled();
expect(mocks.main).not.toHaveBeenCalled();
expect(mocks.initStartupProfiler).not.toHaveBeenCalled();
expect(mocks.initCpuProfiler).not.toHaveBeenCalled();
});
it('does not run MCP subcommands with unknown options', async () => {
await runCliEntry(['mcp', 'list', '--unknown']);
expect(process.exitCode).toBe(1);
expect(stderr.join('')).toContain('Unknown argument: unknown');
expect(mocks.mcpListHandler).not.toHaveBeenCalled();
expect(mocks.main).not.toHaveBeenCalled();
});
it('reports routine MCP argument errors without loading the full CLI', async () => {
await runCliEntry(['mcp', 'add']);
expect(process.exitCode).toBe(1);
expect(stderr.join('')).toContain('Not enough non-option arguments');
expect(mocks.mcpAddHandler).not.toHaveBeenCalled();
expect(mocks.main).not.toHaveBeenCalled();
});
it('keeps the serve fast path ahead of the full CLI startup', async () => {
mocks.tryRunServeFastPath.mockResolvedValue(true);
await runCliEntry(['serve']);
expect(mocks.tryRunServeFastPath).toHaveBeenCalledWith(['serve']);
expect(mocks.main).not.toHaveBeenCalled();
});
it('initializes profilers once when the serve fast path falls back', async () => {
mocks.tryRunServeFastPath.mockResolvedValue(false);
await runCliEntry(['serve']);
expect(mocks.tryRunServeFastPath).toHaveBeenCalledWith(['serve']);
expect(mocks.main).toHaveBeenCalledTimes(1);
});
it('loads gemini on the default path', async () => {
await runCliEntry([]);
expect(mocks.main).toHaveBeenCalledTimes(1);
});
});
describe('bootstrap import boundaries', () => {
it('keeps fast-path-only dependencies out of static imports', () => {
const source = readFileSync('src/cli.ts', 'utf8');
expect(source).not.toContain("import yargs from 'yargs'");
expect(source).not.toContain("from '@qwen-code/qwen-code-core'");
expect(source).not.toContain("import './gemini.js'");
expect(source).not.toContain("import { main } from './gemini.js'");
});
it('initializes profilers during bootstrap module evaluation', () => {
const source = readFileSync('src/cli.ts', 'utf8');
expect(source).toContain(
"import { initStartupProfiler } from './utils/startupProfiler.js'",
);
expect(source).toContain(
"import { initCpuProfiler } from './utils/cpuProfiler.js'",
);
expect(source.indexOf('initStartupProfiler();')).toBeLessThan(
source.indexOf('export async function runCliEntry('),
);
expect(source.indexOf('initCpuProfiler();')).toBeLessThan(
source.indexOf('export async function runCliEntry('),
);
});
it('uses the bootstrap file as the production bundle entry', () => {
const source = readFileSync('../../esbuild.config.js', 'utf8');
expect(source).toContain("entryPoints: { cli: 'packages/cli/src/cli.ts' }");
});
it('keeps bootstrap fast paths in-process in the npm bin wrapper', () => {
const source = readFileSync('../../scripts/cli-entry.js', 'utf8');
expect(source).toContain('function isInProcessFastPath()');
expect(source).toContain("first === 'serve'");
expect(source).toContain("first === 'mcp'");
expect(source).toContain("hasFlag('--help', '-h')");
expect(source).toContain("hasFlag('--version', '-v')");
});
it('prints CLI_VERSION from the npm bin wrapper version shortcut', () => {
const output = execFileSync(
process.execPath,
['../../scripts/cli-entry.js', '--version'],
{
encoding: 'utf8',
env: { ...process.env, CLI_VERSION: '7.7.7-test' },
},
);
expect(output).toBe('7.7.7-test\n');
});
it('reads package.json from the npm bin wrapper version shortcut', () => {
const expectedVersion = JSON.parse(
readFileSync('../../package.json', 'utf8'),
).version;
const env = { ...process.env };
delete env['CLI_VERSION'];
const output = execFileSync(
process.execPath,
['../../scripts/cli-entry.js', '--version'],
{
encoding: 'utf8',
env,
},
);
expect(output).toBe(`${expectedVersion}\n`);
});
it('falls through to cli.js when wrapper package.json parsing fails', () => {
const tempDir = mkdtempSync(path.join(tmpdir(), 'qwen-cli-entry-'));
try {
copyFileSync(
'../../scripts/cli-entry.js',
path.join(tempDir, 'cli-entry.mjs'),
);
writeFileSync(
path.join(tempDir, 'cli.js'),
"process.stdout.write('fallback-cli\\n');\n",
);
const env = { ...process.env };
delete env['CLI_VERSION'];
const output = execFileSync(
process.execPath,
[path.join(tempDir, 'cli-entry.mjs'), '--version'],
{
encoding: 'utf8',
env,
},
);
expect(output).toBe('fallback-cli\n');
} finally {
rmSync(tempDir, { recursive: true, force: true });
}
});
it('copies the npm bin wrapper into the package instead of duplicating it', () => {
const source = readFileSync('../../scripts/prepare-package.js', 'utf8');
expect(source).toContain(
"fs.copyFileSync(path.join(__dirname, 'cli-entry.js'), cliEntryPath)",
);
expect(source).not.toContain('const cliEntryContent = `');
});
it('keeps bootstrap top-level help commands aligned with config registrations', () => {
const configSource = readFileSync('src/config/config.ts', 'utf8');
const commandNameByIdentifier = new Map([
['authCommand', 'auth'],
['channelCommand', 'channel'],
['extensionsCommand', 'extensions'],
['hooksCommand', 'hooks'],
['mcpCommand', 'mcp'],
['reviewCommand', 'review'],
['serveCommand', 'serve'],
['sessionsCommand', 'sessions'],
]);
const registeredIdentifiers = [
...configSource.matchAll(/\.command\((\w+Command)\)/g),
].map((match) => match[1]!);
const bootstrapCommands = new Set(
TOP_LEVEL_COMMANDS.map(([command]) => command.split(' ')[0]),
);
expect(registeredIdentifiers).toHaveLength(commandNameByIdentifier.size);
for (const identifier of registeredIdentifiers) {
const commandName = commandNameByIdentifier.get(identifier);
expect(commandName, `missing mapping for ${identifier}`).toBeDefined();
expect(bootstrapCommands).toContain(commandName);
}
});
it('keeps bootstrap MCP help commands aligned with MCP registrations', () => {
const mcpSource = readFileSync('src/commands/mcp.ts', 'utf8');
const commandNameByIdentifier = new Map([
['addCommand', 'add'],
['removeCommand', 'remove'],
['listCommand', 'list'],
['reconnectCommand', 'reconnect'],
['approveCommand', 'approve'],
['rejectCommand', 'reject'],
]);
const registeredIdentifiers = [
...mcpSource.matchAll(/\.command\((\w+Command)\)/g),
].map((match) => match[1]!);
const bootstrapCommands = new Set(
MCP_COMMANDS.map(([command]) => command.split(' ')[0]),
);
expect(registeredIdentifiers).toHaveLength(commandNameByIdentifier.size);
for (const identifier of registeredIdentifiers) {
const commandName = commandNameByIdentifier.get(identifier);
expect(commandName, `missing mapping for ${identifier}`).toBeDefined();
expect(bootstrapCommands).toContain(commandName);
}
});
});
describe('bootstrap error handling', () => {
const savedEnv = {
NO_COLOR: process.env['NO_COLOR'],
};
let stderr: string[];
beforeEach(() => {
stderr = [];
vi.spyOn(process.stderr, 'write').mockImplementation((chunk) => {
stderr.push(String(chunk));
return true;
});
vi.spyOn(process, 'exit').mockImplementation(((code) => {
throw new Error(`process.exit:${String(code)}`);
}) as typeof process.exit);
});
afterEach(() => {
if (savedEnv.NO_COLOR === undefined) {
delete process.env['NO_COLOR'];
} else {
process.env['NO_COLOR'] = savedEnv.NO_COLOR;
}
vi.restoreAllMocks();
});
it('prints FatalError messages and exits with their code', async () => {
process.env['NO_COLOR'] = '1';
await expect(
handleCriticalError(new FatalError('fatal boom', 42)),
).rejects.toThrow('process.exit:42');
const output = stderr.join('');
expect(output).toContain('fatal boom');
expect(output).not.toContain('\x1b[31m');
});
it('prints FatalError messages in red when color is enabled', async () => {
delete process.env['NO_COLOR'];
await expect(
handleCriticalError(new FatalError('fatal color', 42)),
).rejects.toThrow('process.exit:42');
expect(stderr.join('')).toContain('\x1b[31mfatal color\x1b[0m');
});
it('exits AlreadyReportedError without printing another error', async () => {
await expect(
handleCriticalError(new AlreadyReportedError('already printed', 7)),
).rejects.toThrow('process.exit:7');
expect(stderr.join('')).toBe('');
});
it('prints unexpected errors with the generic critical header', async () => {
await expect(
handleCriticalError(new Error('generic boom')),
).rejects.toThrow('process.exit:1');
const output = stderr.join('');
expect(output).toContain('An unexpected critical error occurred:');
expect(output).toContain('generic boom');
});
it('recognizes expected PTY race errors', () => {
expect(
isExpectedPtyRaceError(
Object.assign(new Error('read EIO'), { code: 'EIO' }),
),
).toBe(true);
expect(isExpectedPtyRaceError(new Error('read EAGAIN'))).toBe(true);
expect(
isExpectedPtyRaceError(
new Error('Cannot resize a pty that has already exited'),
),
).toBe(true);
expect(isExpectedPtyRaceError(new Error('other failure'))).toBe(false);
});
it('wires uncaughtException PTY race suppression without exiting', async () => {
let uncaughtHandler: ((error: Error) => void) | undefined;
vi.spyOn(process, 'on').mockImplementation(((
event: string | symbol,
listener: (...args: unknown[]) => void,
) => {
if (event === 'uncaughtException') {
uncaughtHandler = listener as (error: Error) => void;
}
return process;
}) as typeof process.on);
await runCliEntryPoint(vi.fn(async () => {}));
expect(uncaughtHandler).toBeDefined();
uncaughtHandler?.(Object.assign(new Error('read EIO'), { code: 'EIO' }));
expect(process.exit).not.toHaveBeenCalled();
expect(stderr.join('')).toBe('');
});
it('routes run failures through the critical error handler', async () => {
const error = new Error('run failed');
const run = vi.fn(async () => {
throw error;
});
const handleError = vi.fn(async () => {});
await runCliEntryPoint(run, handleError);
expect(handleError).toHaveBeenCalledWith(error);
});
it('reports when the critical error handler itself fails', async () => {
const run = vi.fn(async () => {
throw new Error('run failed');
});
const handleError = vi.fn(async () => {
throw new Error('handler failed');
});
await expect(runCliEntryPoint(run, handleError)).rejects.toThrow(
'process.exit:1',
);
const output = stderr.join('');
expect(output).toContain('Original error:');
expect(output).toContain('run failed');
expect(output).toContain('Error handler failed:');
expect(output).toContain('handler failed');
});
});

469
packages/cli/src/cli.ts Normal file
View file

@ -0,0 +1,469 @@
/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { pathToFileURL } from 'node:url';
import type { ArgumentsCamelCase, Argv, Options } from 'yargs';
import { normalizeServeFastPathArgv } from './serve/fast-path-argv.js';
import { initStartupProfiler } from './utils/startupProfiler.js';
import { initCpuProfiler } from './utils/cpuProfiler.js';
// Preserve the old entrypoint's profiling baseline before route-specific
// dynamic imports or command handling shift startup measurements.
initStartupProfiler();
initCpuProfiler();
type BootstrapRoute = 'serve' | 'mcp' | 'help' | 'version' | 'default';
export const TOP_LEVEL_COMMANDS = [
['auth', 'Configure authentication (removed)'],
['channel <command>', 'Manage messaging channels (Telegram, Discord, etc.)'],
['extensions <command>', 'Manage Qwen Code extensions.'],
['hooks', 'Manage Qwen Code hooks (use /hooks in interactive mode).'],
['mcp', 'Manage MCP servers'],
[
'review <command>',
'Internal helpers used by the /review skill (PR worktree setup, context fetch, rules loading, presubmit checks, cleanup)',
],
[
'serve',
'Run Qwen Code as a local HTTP daemon (Stage 1 experimental: --http-bridge)',
],
['sessions <command>', 'Manage Qwen Code sessions'],
] as const;
export const MCP_COMMANDS = [
['add <name> <commandOrUrl> [args...]', 'Add a server'],
['remove <name>', 'Remove a server'],
['list', 'List all configured MCP servers'],
['reconnect [server-name]', 'Reconnect to MCP servers'],
['approve [name]', 'Approve a pending MCP server'],
['reject [name]', 'Reject a pending MCP server'],
] as const;
const TOP_LEVEL_HELP_OPTIONS = [
['model', { alias: 'm', type: 'string', description: 'Model' }],
[
'prompt',
{
alias: 'p',
type: 'string',
description: 'Prompt. Appended to input on stdin (if any).',
},
],
[
'prompt-interactive',
{
alias: 'i',
type: 'string',
description:
'Execute the provided prompt and continue in interactive mode',
},
],
[
'safe-mode',
{
type: 'boolean',
description:
'Disable all customizations (context files, hooks, extensions, skills, MCP servers) for troubleshooting.',
},
],
[
'sandbox',
{
alias: 's',
type: 'boolean',
description: 'Run in sandbox?',
},
],
[
'output-format',
{
alias: 'o',
type: 'string',
choices: ['text', 'json', 'stream-json'],
description: 'The format of the CLI output.',
},
],
[
'continue',
{
alias: 'c',
type: 'boolean',
description: 'Resume the most recent session for the current project.',
},
],
[
'resume',
{
alias: 'r',
type: 'string',
description:
'Resume a specific session by its ID. Use without an ID to show session picker.',
},
],
] as const satisfies ReadonlyArray<readonly [string, Options]>;
const VALUE_FLAGS = new Set([
'--model',
'-m',
'--prompt',
'-p',
'--prompt-interactive',
'-i',
'--output-format',
'-o',
'--resume',
'-r',
]);
function writeStdoutLine(line: string): void {
process.stdout.write(line.endsWith('\n') ? line : `${line}\n`);
}
function hasFlag(
argv: readonly string[],
long: string,
short: string,
): boolean {
for (let i = 0; i < argv.length; i++) {
const arg = argv[i]!;
if (arg === '--') {
return false;
}
if (VALUE_FLAGS.has(arg)) {
i++;
continue;
}
if (arg === long || arg === short) {
return true;
}
}
return false;
}
async function buildTopLevelHelpParser() {
const { default: yargs } = await import('yargs');
const parser = yargs([])
.scriptName('qwen')
.usage(
'Usage: qwen [options] [command]\n\nQwen Code - Launch an interactive CLI, use -p/--prompt for non-interactive mode',
)
.version(process.env['CLI_VERSION'] || 'unknown')
.alias('v', 'version')
.help()
.alias('h', 'help')
.strict()
.demandCommand(0, 0);
for (const [option, config] of TOP_LEVEL_HELP_OPTIONS) {
parser.option(option, config);
}
for (const [command, description] of TOP_LEVEL_COMMANDS) {
parser.command(command, description);
}
return parser;
}
function firstPositionalArg(argv: readonly string[]): string | undefined {
for (let i = 0; i < argv.length; i++) {
const arg = argv[i]!;
if (arg === '--') {
return undefined;
}
if (VALUE_FLAGS.has(arg)) {
i++;
continue;
}
if (!arg.startsWith('-')) {
return arg;
}
}
return undefined;
}
function normalizeMcpFastPathArgv(argv: readonly string[]): readonly string[] {
if (argv[0] === 'mcp' && argv[1] === '--') {
return [argv[0], ...argv.slice(2)];
}
return argv;
}
export function resolveBootstrapRoute(
rawArgv: readonly string[],
): BootstrapRoute {
const argv = normalizeServeFastPathArgv(rawArgv);
if (hasFlag(argv, '--version', '-v')) {
return 'version';
}
const firstArg = argv[0];
if (firstArg === 'serve') {
return 'serve';
}
if (firstArg === 'mcp') {
return 'mcp';
}
const firstPositional = firstPositionalArg(argv);
if (hasFlag(argv, '--help', '-h') && firstPositional === undefined) {
return 'help';
}
return 'default';
}
async function printTopLevelHelp(): Promise<void> {
const help = await (await buildTopLevelHelpParser()).getHelp();
writeStdoutLine(help);
}
function printMcpHelp(): void {
const lines = [
'Usage: qwen mcp <command>',
'',
'Manage MCP servers',
'',
'Commands:',
...MCP_COMMANDS.map(
([command, description]) => ` qwen mcp ${command} ${description}`,
),
];
writeStdoutLine(lines.join('\n'));
}
async function printBootstrapVersion(): Promise<void> {
if (process.env['CLI_VERSION']) {
writeStdoutLine(process.env['CLI_VERSION']);
return;
}
const { getCliVersion } = await import('./utils/version.js');
writeStdoutLine(await getCliVersion());
}
async function runMcpFastPath(rawArgv: readonly string[]): Promise<void> {
const argv = normalizeMcpFastPathArgv(normalizeServeFastPathArgv(rawArgv));
const hasSubcommand = argv.length > 1 && !argv[1]!.startsWith('-');
if (!hasSubcommand) {
printMcpHelp();
return;
}
const [{ default: yargsInstance }, { mcpCommand }] = await Promise.all([
import('yargs'),
import('./commands/mcp.js'),
]);
const parser = yargsInstance([])
.scriptName('qwen')
.command(mcpCommand)
.version(false)
.help()
.alias('h', 'help')
.strict()
.strictCommands()
.demandCommand(1, 'You need at least one command before continuing.')
.fail((message: string | null, error: Error | undefined, yargs: Argv) => {
writeStderrLine(message || error?.message || 'Unknown argument error');
yargs.showHelp();
process.exitCode = 1;
})
.exitProcess(false);
if (hasFlag(argv.slice(2), '--help', '-h')) {
await parseYargsHelp(parser, argv);
return;
}
await parseYargsCommand(parser, argv);
}
async function parseYargsHelp(
parser: Argv,
argv: readonly string[],
): Promise<void> {
await new Promise<void>((resolve, reject) => {
parser.parse(
argv,
(error: Error | undefined, _argv: ArgumentsCamelCase, output: string) => {
if (output) {
writeStdoutLine(output);
}
if (error) {
reject(error);
return;
}
resolve();
},
);
});
}
async function parseYargsCommand(
parser: Argv,
argv: readonly string[],
): Promise<void> {
await new Promise<void>((resolve) => {
parser.parse(
argv,
(error: Error | undefined, _argv: ArgumentsCamelCase, output: string) => {
if (output) {
writeStdoutLine(output);
}
if (error) {
writeStderrLine(error.message);
process.exitCode = 1;
}
resolve();
},
);
});
}
export async function runCliEntry(
rawArgv: readonly string[] = process.argv.slice(2),
): Promise<void> {
const argv = normalizeServeFastPathArgv(rawArgv);
const route = resolveBootstrapRoute(argv);
if (route === 'version') {
await printBootstrapVersion();
return;
}
if (route === 'serve') {
const { tryRunServeFastPath } = await import('./serve/fast-path.js');
if (await tryRunServeFastPath(argv)) {
return;
}
} else if (route === 'mcp') {
await runMcpFastPath(argv);
return;
} else if (route === 'help') {
await printTopLevelHelp();
return;
}
const { main } = await import('./gemini.js');
await main();
}
function getErrnoCode(error: unknown): string | undefined {
if (!error || typeof error !== 'object') {
return undefined;
}
const code = (error as { code?: unknown }).code;
return typeof code === 'string' ? code : undefined;
}
export function isExpectedPtyRaceError(error: unknown): boolean {
if (!(error instanceof Error)) {
return false;
}
const message = error.message;
const code = getErrnoCode(error);
if (
(code === 'EIO' && message.includes('read')) ||
message.includes('read EIO')
) {
return true;
}
if (
(code === 'EAGAIN' && message.includes('read')) ||
message.includes('read EAGAIN')
) {
return true;
}
return (
message.includes('ioctl(2) failed, EBADF') ||
message.includes('Cannot resize a pty that has already exited')
);
}
export async function handleCriticalError(error: unknown): Promise<void> {
const [{ FatalError }, { AlreadyReportedError }] = await Promise.all([
import('@qwen-code/qwen-code-core'),
import('./utils/errors.js'),
]);
if (error instanceof FatalError) {
let errorMessage = error.message;
if (!process.env['NO_COLOR']) {
errorMessage = `\x1b[31m${errorMessage}\x1b[0m`;
}
writeStderrLine(errorMessage);
process.exit(error.exitCode);
}
if (error instanceof AlreadyReportedError) {
process.exit(error.exitCode);
}
writeStderrLine('An unexpected critical error occurred:');
if (error instanceof Error) {
writeStderrLine(error.stack ?? error.message);
} else {
writeStderrLine(String(error));
}
process.exit(1);
}
function writeStderrLine(line: string): void {
process.stderr.write(line.endsWith('\n') ? line : `${line}\n`);
}
export async function runCliEntryPoint(
run: () => Promise<void> = runCliEntry,
handleError: (error: unknown) => Promise<void> = handleCriticalError,
): Promise<void> {
process.on('uncaughtException', (error) => {
if (isExpectedPtyRaceError(error)) {
return;
}
if (error instanceof Error) {
writeStderrLine(error.stack ?? error.message);
} else {
writeStderrLine(String(error));
}
process.exit(1);
});
try {
await run();
} catch (error) {
try {
await handleError(error);
} catch (handlerError) {
writeStderrLine('An unexpected critical error occurred:');
writeStderrLine('Original error:');
if (error instanceof Error) {
writeStderrLine(error.stack ?? error.message);
} else {
writeStderrLine(String(error));
}
writeStderrLine('Error handler failed:');
if (handlerError instanceof Error) {
writeStderrLine(handlerError.stack ?? handlerError.message);
} else {
writeStderrLine(String(handlerError));
}
process.exit(1);
}
}
}
if (
process.argv[1] !== undefined &&
import.meta.url === pathToFileURL(process.argv[1]).href
) {
void runCliEntryPoint();
}

View file

@ -363,13 +363,12 @@ afterEach(() => {
describe('CLI entry import boundary', () => {
it('does not statically import the full gemini entry before the serve fast path can run', () => {
const indexSource = readFileSync('index.ts', 'utf8');
const cliSource = readFileSync('src/cli.ts', 'utf8');
expect(indexSource).not.toContain("import './src/gemini.js'");
expect(indexSource).not.toContain("import { main } from './src/gemini.js'");
expect(indexSource).not.toContain("process.argv[2] === 'serve'");
expect(indexSource).toContain('import { isServeFastPathArgv }');
expect(indexSource).toContain("await import('./src/serve/fast-path.js')");
expect(cliSource).not.toContain("import './gemini.js'");
expect(cliSource).not.toContain("import { main } from './gemini.js'");
expect(cliSource).not.toContain("process.argv[2] === 'serve'");
expect(cliSource).toContain("await import('./serve/fast-path.js')");
});
it('does not import the full settings loader on the serve fast path', () => {

View file

@ -198,25 +198,32 @@ describe('retryWithBackoff', () => {
expect(fn).toHaveBeenCalledTimes(1);
});
it('uses exponential backoff delay (observed timing)', async () => {
const timestamps: number[] = [];
it('uses exponential backoff delay', async () => {
vi.useFakeTimers();
const fn = vi.fn(async () => {
timestamps.push(Date.now());
throw new Error('ETIMEDOUT');
});
await expect(
retryWithBackoff(fn, 'test-backoff', {
try {
const promise = retryWithBackoff(fn, 'test-backoff', {
maxRetries: 2,
baseDelayMs: 50,
}),
).rejects.toThrow('ETIMEDOUT');
});
const errorPromise = promise.catch((error: unknown) => error);
expect(fn).toHaveBeenCalledTimes(3);
const gap1 = timestamps[1]! - timestamps[0]!;
const gap2 = timestamps[2]! - timestamps[1]!;
expect(gap1).toBeGreaterThanOrEqual(50);
expect(gap2).toBeGreaterThanOrEqual(gap1);
await vi.waitFor(() => expect(fn).toHaveBeenCalledTimes(1));
await vi.advanceTimersByTimeAsync(50);
await vi.waitFor(() => expect(fn).toHaveBeenCalledTimes(2));
await vi.advanceTimersByTimeAsync(100);
const error = await errorPromise;
expect(error).toBeInstanceOf(Error);
expect((error as Error).message).toBe('ETIMEDOUT');
expect(fn).toHaveBeenCalledTimes(3);
} finally {
vi.useRealTimers();
}
});
it('succeeds after transient 503 then success', async () => {

View file

@ -13,29 +13,82 @@
* global.gc() is available for the memory-pressure monitor's critical-tier
* cleanup.
*
* For `qwen serve`: imports cli.js directly in-process, skipping the
* spawnSync overhead (~370ms on EDR-instrumented hosts). The daemon host
* process never calls global.gc() only its ACP children do, and they
* independently add --expose-gc via spawnChannel.ts.
* For bootstrap fast paths: imports cli.js directly in-process, skipping the
* spawnSync overhead. These paths do not need global.gc(); the normal
* interactive path still relaunches with --expose-gc for the memory-pressure
* monitor.
*/
import module from 'node:module';
import { spawnSync } from 'node:child_process';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { dirname, join } from 'node:path';
const __dirname = dirname(fileURLToPath(import.meta.url));
const cliPath = join(__dirname, '..', 'dist', 'cli.js');
function isServeCommand() {
return process.argv[2] === 'serve';
function hasFlag(flag, alias) {
for (const arg of process.argv.slice(2)) {
if (arg === '--') {
return false;
}
if (arg === flag || arg === alias) {
return true;
}
}
return false;
}
if (isServeCommand()) {
function isInProcessFastPath() {
const first = process.argv[2];
if (first === 'serve' || first === 'mcp') {
return true;
}
if (first === undefined || first.startsWith('-')) {
return hasFlag('--help', '-h') || hasFlag('--version', '-v');
}
return false;
}
const isTopLevelVersion =
(process.argv[2] === undefined || process.argv[2].startsWith('-')) &&
hasFlag('--version', '-v');
if (isTopLevelVersion && process.env['CLI_VERSION']) {
process.stdout.write(`${process.env['CLI_VERSION']}\n`);
process.exit(0);
}
const { existsSync } = await import('node:fs');
const { fileURLToPath, pathToFileURL } = await import('node:url');
const { dirname, join } = await import('node:path');
const __dirname = dirname(fileURLToPath(import.meta.url));
const cliPathCandidates = [
join(__dirname, 'cli.js'),
join(__dirname, '..', 'dist', 'cli.js'),
];
const packageJsonPathCandidates = [
join(__dirname, 'package.json'),
join(__dirname, '..', 'package.json'),
];
const cliPath =
cliPathCandidates.find((candidate) => existsSync(candidate)) ??
cliPathCandidates[0];
const packageJsonPath =
packageJsonPathCandidates.find((candidate) => existsSync(candidate)) ??
packageJsonPathCandidates[0];
if (isTopLevelVersion) {
try {
const { readFileSync } = await import('node:fs');
const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
process.stdout.write(`${pkg.version || 'unknown'}\n`);
process.exit(0);
} catch {
// Fall through to cli.js, which has its own version fallback.
}
}
if (isInProcessFastPath()) {
const { default: module } = await import('node:module');
module.enableCompileCache?.();
process.argv[1] = cliPath;
await import(pathToFileURL(cliPath).href);
} else {
const { spawnSync } = await import('node:child_process');
const result = spawnSync(
process.execPath,
['--expose-gc', cliPath, ...process.argv.slice(2)],

View file

@ -299,40 +299,9 @@ function writeDistPackageJson(
) {
console.log('Creating package.json for distribution...');
const cliEntryContent = `#!/usr/bin/env node
import module from 'node:module';
import { spawnSync } from 'node:child_process';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { dirname, join } from 'node:path';
const __dirname = dirname(fileURLToPath(import.meta.url));
const cliPath = join(__dirname, 'cli.js');
function isServeCommand() {
return process.argv[2] === 'serve';
}
if (isServeCommand()) {
module.enableCompileCache?.();
process.argv[1] = cliPath;
await import(pathToFileURL(cliPath).href);
} else {
const result = spawnSync(
process.execPath,
['--expose-gc', cliPath, ...process.argv.slice(2)],
{ stdio: 'inherit' },
);
if (result.signal) {
process.kill(process.pid, result.signal);
} else {
process.exit(result.status ?? 1);
}
}
`;
const cliEntryPath = path.join(distDir, 'cli-entry.js');
fs.writeFileSync(cliEntryPath, cliEntryContent, { mode: 0o755 });
fs.copyFileSync(path.join(__dirname, 'cli-entry.js'), cliEntryPath);
fs.chmodSync(cliEntryPath, 0o755);
console.log('Created dist cli-entry.js wrapper');
const rootPackageJson = JSON.parse(