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) { if (!handler) {
console.error(`[X] Unknown bar subcommand: ${subcommand}`); console.error(`[X] Unknown bar subcommand: ${subcommand}`);
console.error('[i] Usage: ccs bar [launch|serve|stop|status|install|uninstall|version|--help]'); console.error('[i] Usage: ccs bar [launch|serve|stop|status|install|uninstall|version|--help]');
process.exitCode = 1;
return; 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) // 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') { if (firstArg !== 'migrate') {
const { autoMigrate } = await import('../config/migration-manager'); try {
await autoMigrate(); 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) // 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', { cliLogger.warn('recovery.failed', 'Auto-recovery failed during CLI startup', {
message: (err as Error).message, message: (err as Error).message,
}); });
// Recovery is best-effort - don't block basic CLI functionality // Recovery is best-effort - don't block basic CLI functionality. Use only the
console.warn('[!] Recovery failed:', (err as Error).message); // 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.) // 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) // 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) { if (firstArg === 'copilot' && args.length > 1) {
const copilotToken = args[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 { handleCopilotCommand } = await import('../commands/copilot-command');
const exitCode = await handleCopilotCommand(args.slice(1)); const exitCode = await handleCopilotCommand(args.slice(1));
process.exit(exitCode); process.exit(exitCode);
@@ -125,14 +142,11 @@ export async function runPreDispatchHandlers(ctx: PreDispatchContext): Promise<b
} }
// Special case: explicit legacy Cursor bridge namespace. // 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) { if (firstArg === LEGACY_CURSOR_PROFILE_NAME && args.length > 1) {
const { handleCursorCommand } = await import('../commands/cursor-command'); const { handleCursorCommand } = await import('../commands/cursor-command');
const cursorToken = args[1]; const exitCode = await handleCursorCommand(args.slice(1));
process.exit(exitCode);
if (isCursorSubcommandToken(cursorToken)) {
const exitCode = await handleCursorCommand(args.slice(1));
process.exit(exitCode);
}
} }
// Compatibility shim: old `ccs cursor <subcommand>` still forwards to the legacy bridge // Compatibility shim: old `ccs cursor <subcommand>` still forwards to the legacy bridge