diff --git a/src/ccs.ts b/src/ccs.ts index e858ab1d..25c27c11 100644 --- a/src/ccs.ts +++ b/src/ccs.ts @@ -30,7 +30,7 @@ import { getGlobalEnvConfig } from './config/unified-config-loader'; import { ensureProfileHooks as ensureImageAnalyzerHooks } from './utils/hooks/image-analyzer-profile-hook-injector'; import { getImageAnalysisHookEnv } from './utils/hooks'; import { fail, info, warn } from './utils/ui'; -import { isCopilotSubcommandToken, isLikelyCopilotFlagAlias } from './copilot/constants'; +import { isCopilotSubcommandToken } from './copilot/constants'; // Import centralized error handling import { handleError, runCleanup } from './errors'; @@ -573,13 +573,10 @@ async function main(): Promise { } // Special case: copilot command (GitHub Copilot integration) - // Route known subcommands and likely mistyped aliases (`--statu`) to command handler. - // Non-command Claude args still pass through profile flow. + // Route known subcommands to command handler, keep all other args as profile passthrough. if (firstArg === 'copilot' && args.length > 1) { const copilotToken = args[1]; - const shouldRouteToCopilotCommand = - isCopilotSubcommandToken(copilotToken) || - (args.length === 2 && isLikelyCopilotFlagAlias(copilotToken)); + const shouldRouteToCopilotCommand = isCopilotSubcommandToken(copilotToken); if (shouldRouteToCopilotCommand) { const { handleCopilotCommand } = await import('./commands/copilot-command'); diff --git a/src/commands/copilot-command.ts b/src/commands/copilot-command.ts index 2bdb2b1d..7b6620ee 100644 --- a/src/commands/copilot-command.ts +++ b/src/commands/copilot-command.ts @@ -49,7 +49,8 @@ export async function handleCopilotCommand(args: string[]): Promise { default: console.error(fail(`Unknown subcommand: ${subcommand}`)); console.error(''); - return handleHelp(); + handleHelp(); + return 1; } } diff --git a/src/copilot/constants.ts b/src/copilot/constants.ts index cc594afd..c5bb8b07 100644 --- a/src/copilot/constants.ts +++ b/src/copilot/constants.ts @@ -45,23 +45,3 @@ export function normalizeCopilotSubcommand(token?: string): string | undefined { export function isCopilotSubcommandToken(token?: string): boolean { return Boolean(token) && COPILOT_SUBCOMMAND_TOKENS.includes(token as string); } - -/** - * Detect likely mistyped copilot flag aliases (e.g. `--statu`). - * This helps entrypoint routing show command help instead of falling through - * to profile execution for obvious copilot-command typos. - */ -export function isLikelyCopilotFlagAlias(token?: string): boolean { - if (!token || !token.startsWith('--') || token === '--') { - return false; - } - - const alias = token.slice(2).toLowerCase(); - if (!/^[a-z][a-z0-9-]*$/.test(alias)) { - return false; - } - - return COPILOT_SUBCOMMANDS.some( - (subcommand) => subcommand.startsWith(alias) || alias.startsWith(subcommand) - ); -} diff --git a/src/copilot/copilot-daemon.ts b/src/copilot/copilot-daemon.ts index c8cc09fe..4ec2d75b 100644 --- a/src/copilot/copilot-daemon.ts +++ b/src/copilot/copilot-daemon.ts @@ -289,12 +289,11 @@ export async function stopDaemon(): Promise<{ success: boolean; error?: string } try { const ownership = verifyProcessOwnership(pid, (commandLine) => { const lower = commandLine.toLowerCase(); - const hasCopilotApiBinary = /(^|[\\/\s])copilot-api(\.cmd|\.exe)?(\s|$)/.test(lower); + const hasCopilotApiBinary = /copilot-api(\.cmd|\.exe)?/.test(lower); const hasStartCommand = /\bstart\b/.test(lower); - const hasExpectedPort = - lower.includes(`--port ${configuredPort}`) || lower.includes(`--port=${configuredPort}`); + const hasPortArgument = /--port(?:\s+|=)\d+\b/.test(lower); // copilot-api is launched as `... copilot-api start --port ` - return hasCopilotApiBinary && hasStartCommand && hasExpectedPort; + return hasCopilotApiBinary && hasStartCommand && hasPortArgument; }); if (ownership === 'not-running') { removePidFile(); diff --git a/src/web-server/routes/copilot-routes.ts b/src/web-server/routes/copilot-routes.ts index 31cf897c..625c160c 100644 --- a/src/web-server/routes/copilot-routes.ts +++ b/src/web-server/routes/copilot-routes.ts @@ -92,6 +92,25 @@ router.put('/config', (req: Request, res: Response): void => { return; } const payload = updates as Record; + const allowedKeys = new Set([ + 'enabled', + 'auto_start', + 'port', + 'account_type', + 'rate_limit', + 'wait_on_limit', + 'model', + 'opus_model', + 'sonnet_model', + 'haiku_model', + ]); + const unknownKeys = Object.keys(payload).filter((key) => !allowedKeys.has(key)); + if (unknownKeys.length > 0) { + res.status(400).json({ + error: `Unknown copilot config field(s): ${unknownKeys.join(', ')}`, + }); + return; + } if ('port' in payload) { if (typeof payload.port !== 'number' || !Number.isInteger(payload.port)) { diff --git a/tests/unit/copilot/copilot-command-aliases.test.ts b/tests/unit/copilot/copilot-command-aliases.test.ts index cb8c19ca..4cc4b7ae 100644 --- a/tests/unit/copilot/copilot-command-aliases.test.ts +++ b/tests/unit/copilot/copilot-command-aliases.test.ts @@ -2,7 +2,6 @@ import { describe, expect, it } from 'bun:test'; import { COPILOT_SUBCOMMANDS, COPILOT_SUBCOMMAND_TOKENS, - isLikelyCopilotFlagAlias, normalizeCopilotSubcommand, } from '../../../src/copilot/constants'; @@ -24,13 +23,6 @@ describe('copilot command aliases', () => { expect(normalizeCopilotSubcommand('unknown')).toBe('unknown'); }); - it('detects likely mistyped command aliases for entrypoint routing', () => { - expect(isLikelyCopilotFlagAlias('--statu')).toBe(true); - expect(isLikelyCopilotFlagAlias('--enabl')).toBe(true); - expect(isLikelyCopilotFlagAlias('--print')).toBe(false); - expect(isLikelyCopilotFlagAlias('--')).toBe(false); - }); - it('exposes complete routing token list for ccs entrypoint', () => { for (const subcommand of COPILOT_SUBCOMMANDS) { expect(COPILOT_SUBCOMMAND_TOKENS).toContain(subcommand);