feat(cli): add --browser flag to anthropic-auth command
This commit is contained in:
@@ -6,11 +6,15 @@ const {
|
||||
mockLoadStoredAnthropicAuthToken,
|
||||
mockStoreAnthropicAuth,
|
||||
mockStoreAnthropicAuthToken,
|
||||
mockLoginAnthropicOAuth,
|
||||
mockOpenBrowser,
|
||||
} = vi.hoisted(() => ({
|
||||
mockLoadStoredAnthropicAuth: vi.fn(),
|
||||
mockLoadStoredAnthropicAuthToken: vi.fn(),
|
||||
mockStoreAnthropicAuth: vi.fn(),
|
||||
mockStoreAnthropicAuthToken: vi.fn(),
|
||||
mockLoginAnthropicOAuth: vi.fn(),
|
||||
mockOpenBrowser: vi.fn(),
|
||||
}));
|
||||
|
||||
const { mockCreateInterface } = vi.hoisted(() => ({
|
||||
@@ -20,6 +24,8 @@ const { mockCreateInterface } = vi.hoisted(() => ({
|
||||
vi.mock('../auth/index.js', () => ({
|
||||
loadStoredAnthropicAuth: mockLoadStoredAnthropicAuth,
|
||||
loadStoredAnthropicAuthToken: mockLoadStoredAnthropicAuthToken,
|
||||
loginAnthropicOAuth: mockLoginAnthropicOAuth,
|
||||
openBrowser: mockOpenBrowser,
|
||||
storeAnthropicAuth: mockStoreAnthropicAuth,
|
||||
storeAnthropicAuthToken: mockStoreAnthropicAuthToken,
|
||||
}));
|
||||
@@ -181,4 +187,70 @@ describe('anthropic-auth command', () => {
|
||||
program.parseAsync(['node', 'test', 'anthropic-auth', '--token', '--mode', 'api']),
|
||||
).rejects.toThrow(/Conflicting options/);
|
||||
});
|
||||
|
||||
it('--browser triggers browser OAuth flow', async () => {
|
||||
mockLoginAnthropicOAuth.mockResolvedValue('tok-browser');
|
||||
mockLoadStoredAnthropicAuthToken.mockReturnValue(null);
|
||||
|
||||
const program = new Command();
|
||||
const { registerAnthropicAuthCommand } = await import('./anthropic-auth.js');
|
||||
registerAnthropicAuthCommand(program);
|
||||
|
||||
const consoleLog = vi.spyOn(console, 'log').mockImplementation(() => undefined);
|
||||
|
||||
await program.parseAsync(['node', 'test', 'anthropic-auth', '--browser']);
|
||||
|
||||
expect(mockLoginAnthropicOAuth).toHaveBeenCalled();
|
||||
expect(consoleLog).toHaveBeenCalledWith('Anthropic auth token stored in ~/.config/flynn/auth.json');
|
||||
|
||||
consoleLog.mockRestore();
|
||||
});
|
||||
|
||||
it('--browser with existing token prompts for confirmation and cancels on no', async () => {
|
||||
mockLoadStoredAnthropicAuthToken.mockReturnValue('existing-tok');
|
||||
mockReadlineAnswers(['n']);
|
||||
|
||||
const program = new Command();
|
||||
const { registerAnthropicAuthCommand } = await import('./anthropic-auth.js');
|
||||
registerAnthropicAuthCommand(program);
|
||||
|
||||
const consoleLog = vi.spyOn(console, 'log').mockImplementation(() => undefined);
|
||||
const exitSpy = vi.spyOn(process, 'exit').mockImplementation(((code?: number) => {
|
||||
throw new Error(`EXIT:${code ?? 0}`);
|
||||
}) as never);
|
||||
|
||||
await expect(program.parseAsync(['node', 'test', 'anthropic-auth', '--browser'])).rejects.toThrow('EXIT:0');
|
||||
expect(mockLoginAnthropicOAuth).not.toHaveBeenCalled();
|
||||
expect(consoleLog).toHaveBeenCalledWith('Cancelled.');
|
||||
|
||||
exitSpy.mockRestore();
|
||||
consoleLog.mockRestore();
|
||||
});
|
||||
|
||||
it('--mode browser triggers browser OAuth flow', async () => {
|
||||
mockLoginAnthropicOAuth.mockResolvedValue('tok-mode-browser');
|
||||
mockLoadStoredAnthropicAuthToken.mockReturnValue(null);
|
||||
|
||||
const program = new Command();
|
||||
const { registerAnthropicAuthCommand } = await import('./anthropic-auth.js');
|
||||
registerAnthropicAuthCommand(program);
|
||||
|
||||
const consoleLog = vi.spyOn(console, 'log').mockImplementation(() => undefined);
|
||||
|
||||
await program.parseAsync(['node', 'test', 'anthropic-auth', '--mode', 'browser']);
|
||||
|
||||
expect(mockLoginAnthropicOAuth).toHaveBeenCalled();
|
||||
|
||||
consoleLog.mockRestore();
|
||||
});
|
||||
|
||||
it('--browser and --token conflict', async () => {
|
||||
const program = new Command();
|
||||
const { registerAnthropicAuthCommand } = await import('./anthropic-auth.js');
|
||||
registerAnthropicAuthCommand(program);
|
||||
|
||||
await expect(
|
||||
program.parseAsync(['node', 'test', 'anthropic-auth', '--browser', '--token']),
|
||||
).rejects.toThrow(/Conflicting/);
|
||||
});
|
||||
});
|
||||
|
||||
+45
-10
@@ -3,11 +3,13 @@ import readline from 'readline';
|
||||
import {
|
||||
loadStoredAnthropicAuth,
|
||||
loadStoredAnthropicAuthToken,
|
||||
loginAnthropicOAuth,
|
||||
openBrowser,
|
||||
storeAnthropicAuth,
|
||||
storeAnthropicAuthToken,
|
||||
} from '../auth/index.js';
|
||||
|
||||
type AnthropicAuthMode = 'api' | 'token';
|
||||
type AnthropicAuthMode = 'api' | 'token' | 'browser';
|
||||
|
||||
async function promptHidden(question: string): Promise<string> {
|
||||
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
||||
@@ -43,22 +45,27 @@ async function promptYesNo(question: string): Promise<boolean> {
|
||||
|
||||
function parseAnthropicAuthMode(value: string): AnthropicAuthMode {
|
||||
const mode = value.trim().toLowerCase();
|
||||
if (mode === 'api' || mode === 'token') {
|
||||
if (mode === 'api' || mode === 'token' || mode === 'browser') {
|
||||
return mode;
|
||||
}
|
||||
throw new Error(`Invalid mode "${value}". Expected: api or token.`);
|
||||
throw new Error(`Invalid mode "${value}". Expected: api, token, or browser.`);
|
||||
}
|
||||
|
||||
function resolveAuthMode(opts: { token?: boolean; mode?: AnthropicAuthMode }): AnthropicAuthMode {
|
||||
function resolveAuthMode(opts: { token?: boolean; browser?: boolean; mode?: AnthropicAuthMode }): AnthropicAuthMode {
|
||||
if (opts.token && opts.browser) {
|
||||
throw new Error('Conflicting options: --token and --browser cannot be used together.');
|
||||
}
|
||||
if (opts.mode) {
|
||||
if (opts.token && opts.mode !== 'token') {
|
||||
throw new Error('Conflicting options: --token implies --mode token, but --mode api was provided.');
|
||||
throw new Error('Conflicting options: --token implies --mode token, but a different mode was provided.');
|
||||
}
|
||||
if (opts.browser && opts.mode !== 'browser') {
|
||||
throw new Error('Conflicting options: --browser implies --mode browser, but a different mode was provided.');
|
||||
}
|
||||
return opts.mode;
|
||||
}
|
||||
if (opts.token) {
|
||||
return 'token';
|
||||
}
|
||||
if (opts.browser) return 'browser';
|
||||
if (opts.token) return 'token';
|
||||
return 'api';
|
||||
}
|
||||
|
||||
@@ -66,11 +73,39 @@ export function registerAnthropicAuthCommand(program: Command): void {
|
||||
program
|
||||
.command('anthropic-auth')
|
||||
.description('Store an Anthropic API key or auth token (auth.json)')
|
||||
.option('--mode <mode>', 'Credential mode: api or token', parseAnthropicAuthMode)
|
||||
.option('--mode <mode>', 'Credential mode: api, token, or browser', parseAnthropicAuthMode)
|
||||
.option('--token', 'Store an Anthropic auth token instead of an API key')
|
||||
.action(async (opts: { token?: boolean; mode?: AnthropicAuthMode }) => {
|
||||
.option('--browser', 'Obtain auth token via browser OAuth flow (Claude Pro/Max)')
|
||||
.action(async (opts: { token?: boolean; browser?: boolean; mode?: AnthropicAuthMode }) => {
|
||||
const mode = resolveAuthMode(opts);
|
||||
|
||||
if (mode === 'browser') {
|
||||
if (loadStoredAnthropicAuthToken()) {
|
||||
console.log('Anthropic auth token already exists.');
|
||||
const confirmed = await promptYesNo('Re-authenticate and replace it? (y/N): ');
|
||||
if (!confirmed) {
|
||||
console.log('Cancelled.');
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Starting Anthropic browser OAuth...');
|
||||
try {
|
||||
await loginAnthropicOAuth((url) => {
|
||||
openBrowser(url);
|
||||
console.log(`If browser didn't open, visit:\n${url}`);
|
||||
console.log('Waiting for authentication...');
|
||||
});
|
||||
console.log('');
|
||||
console.log('Anthropic auth token stored in ~/.config/flynn/auth.json');
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
console.error(`Anthropic browser OAuth failed: ${message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (mode === 'token') {
|
||||
if (loadStoredAnthropicAuthToken()) {
|
||||
console.log('Anthropic auth token already exists.');
|
||||
|
||||
Reference in New Issue
Block a user