fix(dispatch): route unknown bar/copilot/cursor subcommands and survive corrupt config

Pre-dispatch only routed known subcommand tokens, so unknown bar/copilot/cursor
subcommands never reached their handlers (no error, no non-zero exit). Route them
through. Also wrap autoMigrate() so a corrupt config.yaml no longer crashes
startup, and print only the first line of a best-effort recovery failure.
This commit is contained in:
Tam Nhu Tran committed 2026-06-22 12:06:58 -04:00
1 parent 9258aed1ff
commit 2900743767
2 files changed
+29 -14

No files matched your search

+1
View File
@@ -66,6 +66,7 @@ export async function handleBarCommand(args: string[]): Promise<void> {
if (!handler) {
console.error(`[X] Unknown bar subcommand: ${subcommand}`);
console.error('[i] Usage: ccs bar [launch|serve|stop|status|install|uninstall|version|--help]');
process.exitCode = 1;
return;
}
+28 -14
View File
@@ -68,10 +68,19 @@ export async function runPreDispatchHandlers(ctx: PreDispatchContext): Promise<b
}
// Auto-migrate to unified config format (silent if already migrated)
// Skip if user is explicitly running migrate command
// Skip if user is explicitly running migrate command.
// Wrapped in try-catch so a corrupt config.yaml does not crash pre-dispatch
// before doctor/setup can run and report the problem to the user.
if (firstArg !== 'migrate') {
const { autoMigrate } = await import('../config/migration-manager');
await autoMigrate();
try {
const { autoMigrate } = await import('../config/migration-manager');
await autoMigrate();
} catch (err) {
cliLogger.warn('migration.failed', 'Auto-migration failed (config may be corrupt)', {
message: (err as Error).message,
});
// Do not print the error again — loadUnifiedConfig already printed the YAML details.
}
}
// Auto-recovery for missing configuration (BEFORE any early-exit commands)
@@ -90,8 +99,10 @@ export async function runPreDispatchHandlers(ctx: PreDispatchContext): Promise<b
cliLogger.warn('recovery.failed', 'Auto-recovery failed during CLI startup', {
message: (err as Error).message,
});
// Recovery is best-effort - don't block basic CLI functionality
console.warn('[!] Recovery failed:', (err as Error).message);
// Recovery is best-effort - don't block basic CLI functionality. Use only the
// first line: a YAML parse error embeds a multi-line snippet the loader already
// printed, so re-emitting it here just duplicates the noise.
console.warn('[!] Recovery failed:', (err as Error).message.split('\n')[0].trim());
}
// Root command router (handles --help, --version, config, doctor, etc.)
@@ -112,12 +123,18 @@ export async function runPreDispatchHandlers(ctx: PreDispatchContext): Promise<b
}
// Special case: copilot command (GitHub Copilot integration)
// Route known subcommands to command handler, keep all other args as profile passthrough.
// Route known subcommands AND unknown tokens to the command handler so the
// default case can print "[X] Unknown subcommand: <token>" and exit 1.
// Unknown tokens that are NOT subcommand-like are kept as profile passthrough.
if (firstArg === 'copilot' && args.length > 1) {
const copilotToken = args[1];
const shouldRouteToCopilotCommand = isCopilotSubcommandToken(copilotToken);
const isKnownSubcommand = isCopilotSubcommandToken(copilotToken);
// Route any non-flag, non-profile-looking second token to the handler so
// the default branch fires with the correct error message.
const looksLikeCopilotSubcmd =
isKnownSubcommand || (typeof copilotToken === 'string' && !copilotToken.startsWith('-'));
if (shouldRouteToCopilotCommand) {
if (looksLikeCopilotSubcmd) {
const { handleCopilotCommand } = await import('../commands/copilot-command');
const exitCode = await handleCopilotCommand(args.slice(1));
process.exit(exitCode);
@@ -125,14 +142,11 @@ export async function runPreDispatchHandlers(ctx: PreDispatchContext): Promise<b
}
// Special case: explicit legacy Cursor bridge namespace.
// Route ALL tokens (known and unknown) so the default case can report the error.
if (firstArg === LEGACY_CURSOR_PROFILE_NAME && args.length > 1) {
const { handleCursorCommand } = await import('../commands/cursor-command');
const cursorToken = args[1];
if (isCursorSubcommandToken(cursorToken)) {
const exitCode = await handleCursorCommand(args.slice(1));
process.exit(exitCode);
}
const exitCode = await handleCursorCommand(args.slice(1));
process.exit(exitCode);
}
// Compatibility shim: old `ccs cursor <subcommand>` still forwards to the legacy bridge