diff --git a/src/commands/proxy-command.ts b/src/commands/proxy-command.ts index 336472f0..5a2451c1 100644 --- a/src/commands/proxy-command.ts +++ b/src/commands/proxy-command.ts @@ -22,6 +22,20 @@ function parseOptionValue(args: string[], key: string): string | undefined { return withEquals ? withEquals.slice(prefix.length) : undefined; } +function findPositionalArg(args: string[], optionsWithValues: string[] = []): string | undefined { + for (let i = 0; i < args.length; i += 1) { + const arg = args[i]; + if (arg.startsWith('-')) { + if (optionsWithValues.includes(arg)) { + i += 1; + } + continue; + } + return arg; + } + return undefined; +} + function showHelp(): number { console.log('OpenAI-Compatible Proxy'); console.log(''); @@ -63,7 +77,7 @@ function resolveProfile(profileName: string) { } async function handleStart(args: string[]): Promise { - const profileName = args.find((arg) => !arg.startsWith('-')); + const profileName = findPositionalArg(args, ['--port', '--host']); if (!profileName) { console.error( fail('Usage: ccs proxy start [--port ] [--host ] [--insecure]') @@ -102,7 +116,7 @@ async function handleStart(args: string[]): Promise { } async function handleStatus(args: string[] = []): Promise { - const profileName = args.find((arg) => !arg.startsWith('-')); + const profileName = findPositionalArg(args); const running = profileName ? [await getOpenAICompatProxyStatus(profileName)].filter((status) => status.running) : (await listOpenAICompatProxyStatuses()).filter((status) => status.running); @@ -130,12 +144,7 @@ async function handleStatus(args: string[] = []): Promise { } async function handleActivate(args: string[]): Promise { - const profileName = args.find((arg) => !arg.startsWith('-')); - const status = await getOpenAICompatProxyStatus(profileName); - if (!status.running || !status.profileName || !status.port || !status.authToken) { - console.error(fail('Proxy is not running. Start it with: ccs proxy start ')); - return 1; - } + const profileName = findPositionalArg(args, ['--shell']); if (!profileName) { const running = (await listOpenAICompatProxyStatuses()).filter((entry) => entry.running); if (running.length > 1) { @@ -145,6 +154,11 @@ async function handleActivate(args: string[]): Promise { return 1; } } + const status = await getOpenAICompatProxyStatus(profileName); + if (!status.running || !status.profileName || !status.port || !status.authToken) { + console.error(fail('Proxy is not running. Start it with: ccs proxy start ')); + return 1; + } const shell = detectShell(args.includes('--fish') ? 'fish' : parseOptionValue(args, '--shell')); let profile; diff --git a/src/proxy/proxy-daemon.ts b/src/proxy/proxy-daemon.ts index b2cf668c..ac3f3599 100644 --- a/src/proxy/proxy-daemon.ts +++ b/src/proxy/proxy-daemon.ts @@ -185,7 +185,7 @@ export async function getOpenAICompatProxyStatus( const statuses = await listOpenAICompatProxyStatuses(); const running = statuses.filter((status) => status.running); if (running.length === 1) { - return running[0] || { running: false }; + return running[0]; } if (running.length > 1) { return { running: true }; diff --git a/tests/e2e/proxy-command.e2e.test.ts b/tests/e2e/proxy-command.e2e.test.ts index 021a0919..47029bb5 100644 --- a/tests/e2e/proxy-command.e2e.test.ts +++ b/tests/e2e/proxy-command.e2e.test.ts @@ -107,4 +107,50 @@ describe('proxy command e2e', () => { const stopped = runCli(['proxy', 'stop']); expect(stopped.status).toBe(0); }, 35000); + + it('requires an explicit profile when activating with multiple running proxies', async () => { + const firstPort = await getPort(); + const ccsDir = path.join(tempDir, '.ccs'); + fs.mkdirSync(ccsDir, { recursive: true }); + const firstSettingsPath = path.join(ccsDir, 'ccg.settings.json'); + const secondSettingsPath = path.join(ccsDir, 'ccgm.settings.json'); + fs.writeFileSync( + path.join(ccsDir, 'config.json'), + JSON.stringify({ profiles: { ccg: firstSettingsPath, ccgm: secondSettingsPath } }, null, 2), + 'utf8' + ); + fs.writeFileSync( + firstSettingsPath, + JSON.stringify({ + env: { + ANTHROPIC_BASE_URL: 'http://127.0.0.1:11434', + ANTHROPIC_AUTH_TOKEN: 'ollama-ccg', + ANTHROPIC_MODEL: 'qwen3-coder', + CCS_DROID_PROVIDER: 'generic-chat-completion-api', + }, + }), + 'utf8' + ); + fs.writeFileSync( + secondSettingsPath, + JSON.stringify({ + env: { + ANTHROPIC_BASE_URL: 'https://api.openai.com/v1', + ANTHROPIC_AUTH_TOKEN: 'sk-ccgm', + ANTHROPIC_MODEL: 'gpt-4.1', + }, + }), + 'utf8' + ); + + expect(runCli(['proxy', 'start', 'ccg', '--port', String(firstPort)]).status).toBe(0); + const secondPort = await getPort(); + expect(runCli(['proxy', 'start', 'ccgm', '--port', String(secondPort)]).status).toBe(0); + + const activate = runCli(['proxy', 'activate', '--shell', 'bash']); + expect(activate.status).toBe(1); + expect(activate.stderr).toContain( + 'Multiple proxies are running. Specify a profile: ccs proxy activate ' + ); + }, 35000); });