diff --git a/docs/plans/state.json b/docs/plans/state.json index 57de584..1d0fd6e 100644 --- a/docs/plans/state.json +++ b/docs/plans/state.json @@ -135,6 +135,18 @@ "test_status": "pnpm test:run src/frontends/tui/minimal.login.test.ts src/frontends/tui/minimal.test.ts + pnpm typecheck passing" }, + "anthropic-auth-mode-flag": { + "status": "completed", + "date": "2026-02-16", + "updated": "2026-02-16", + "summary": "Added `--mode api|token` to `anthropic-auth` for parity with explicit mode-based auth commands, while keeping `--token` backward compatible. Includes conflict validation for `--token --mode api`.", + "files_modified": [ + "src/cli/anthropic-auth.ts", + "src/cli/anthropic-auth.test.ts" + ], + "test_status": "pnpm test:run src/cli/anthropic-auth.test.ts src/cli/index.test.ts + pnpm typecheck passing" + }, + "deployment-port-env-override": { "status": "completed", "date": "2026-02-16", diff --git a/src/cli/anthropic-auth.test.ts b/src/cli/anthropic-auth.test.ts index 628fd63..f4d6e97 100644 --- a/src/cli/anthropic-auth.test.ts +++ b/src/cli/anthropic-auth.test.ts @@ -129,4 +129,56 @@ describe('anthropic-auth command', () => { consoleLog.mockRestore(); consoleError.mockRestore(); }); + + it('accepts --mode api and stores API key', async () => { + mockLoadStoredAnthropicAuth.mockReturnValue(null); + mockReadlineAnswers(['sk-ant-from-mode']); + + const program = new Command(); + const { registerAnthropicAuthCommand } = await import('./anthropic-auth.js'); + registerAnthropicAuthCommand(program); + + const consoleLog = vi.spyOn(console, 'log').mockImplementation(() => undefined); + const consoleError = vi.spyOn(console, 'error').mockImplementation(() => undefined); + + await program.parseAsync(['node', 'test', 'anthropic-auth', '--mode', 'api']); + + expect(mockStoreAnthropicAuth).toHaveBeenCalledWith('sk-ant-from-mode'); + expect(mockStoreAnthropicAuthToken).not.toHaveBeenCalled(); + expect(consoleError).not.toHaveBeenCalled(); + + consoleLog.mockRestore(); + consoleError.mockRestore(); + }); + + it('accepts --mode token and stores auth token', async () => { + mockLoadStoredAnthropicAuthToken.mockReturnValue(null); + mockReadlineAnswers(['tok-from-mode']); + + const program = new Command(); + const { registerAnthropicAuthCommand } = await import('./anthropic-auth.js'); + registerAnthropicAuthCommand(program); + + const consoleLog = vi.spyOn(console, 'log').mockImplementation(() => undefined); + const consoleError = vi.spyOn(console, 'error').mockImplementation(() => undefined); + + await program.parseAsync(['node', 'test', 'anthropic-auth', '--mode', 'token']); + + expect(mockStoreAnthropicAuthToken).toHaveBeenCalledWith('tok-from-mode'); + expect(mockStoreAnthropicAuth).not.toHaveBeenCalled(); + expect(consoleError).not.toHaveBeenCalled(); + + consoleLog.mockRestore(); + consoleError.mockRestore(); + }); + + it('fails on conflicting --token and --mode api options', async () => { + const program = new Command(); + const { registerAnthropicAuthCommand } = await import('./anthropic-auth.js'); + registerAnthropicAuthCommand(program); + + await expect( + program.parseAsync(['node', 'test', 'anthropic-auth', '--token', '--mode', 'api']), + ).rejects.toThrow(/Conflicting options/); + }); }); diff --git a/src/cli/anthropic-auth.ts b/src/cli/anthropic-auth.ts index 8a1fde8..fa1f93d 100644 --- a/src/cli/anthropic-auth.ts +++ b/src/cli/anthropic-auth.ts @@ -7,6 +7,8 @@ import { storeAnthropicAuthToken, } from '../auth/index.js'; +type AnthropicAuthMode = 'api' | 'token'; + async function promptHidden(question: string): Promise { const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true }); const rlAny = rl as unknown as { stdoutMuted?: boolean; _writeToOutput?: (s: string) => void }; @@ -39,14 +41,37 @@ async function promptYesNo(question: string): Promise { return normalized === 'y' || normalized === 'yes'; } +function parseAnthropicAuthMode(value: string): AnthropicAuthMode { + const mode = value.trim().toLowerCase(); + if (mode === 'api' || mode === 'token') { + return mode; + } + throw new Error(`Invalid mode "${value}". Expected: api or token.`); +} + +function resolveAuthMode(opts: { token?: boolean; mode?: AnthropicAuthMode }): AnthropicAuthMode { + if (opts.mode) { + if (opts.token && opts.mode !== 'token') { + throw new Error('Conflicting options: --token implies --mode token, but --mode api was provided.'); + } + return opts.mode; + } + if (opts.token) { + return 'token'; + } + return 'api'; +} + export function registerAnthropicAuthCommand(program: Command): void { program .command('anthropic-auth') .description('Store an Anthropic API key or auth token (auth.json)') + .option('--mode ', 'Credential mode: api or token', parseAnthropicAuthMode) .option('--token', 'Store an Anthropic auth token instead of an API key') - .action(async (opts: { token?: boolean }) => { + .action(async (opts: { token?: boolean; mode?: AnthropicAuthMode }) => { + const mode = resolveAuthMode(opts); - if (opts.token) { + if (mode === 'token') { if (loadStoredAnthropicAuthToken()) { console.log('Anthropic auth token already exists.'); const confirmed = await promptYesNo('Re-authenticate and replace it? (y/N): '); @@ -72,7 +97,7 @@ export function registerAnthropicAuthCommand(program: Command): void { console.log(''); try { - if (opts.token) { + if (mode === 'token') { const token = await promptHidden('Enter Anthropic auth token: '); storeAnthropicAuthToken(token); } else {