feat(tui): implement /login <provider> mode <value> auth mode switching
Export AUTH_MODE_PROVIDERS and applyAuthModeToConfig from minimal.ts. Wire mode fast-path into handleLoginCommand so /login anthropic mode oauth persists auth_mode to config without entering the credential flow. After successful credential entry for anthropic/openai, prompt to set auth_mode immediately. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { formatPrompt, parseCommand } from './minimal.js';
|
||||
|
||||
const { mockPersistConfig } = vi.hoisted(() => ({ mockPersistConfig: vi.fn() }));
|
||||
vi.mock('../../config/persistence.js', () => ({ persistConfig: mockPersistConfig }));
|
||||
|
||||
import { formatPrompt, parseCommand, applyAuthModeToConfig, AUTH_MODE_PROVIDERS } from './minimal.js';
|
||||
import type { ModelConfig } from '../../config/schema.js';
|
||||
import type { ManagedSession } from '../../session/index.js';
|
||||
import type { ModelClient } from '../../models/types.js';
|
||||
@@ -796,3 +800,61 @@ describe('MinimalTui prompt cancellation', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('applyAuthModeToConfig', () => {
|
||||
it('sets auth_mode on all tiers whose provider matches', () => {
|
||||
const config = {
|
||||
models: {
|
||||
default: { provider: 'anthropic', model: 'claude-sonnet-4' },
|
||||
fast: { provider: 'openai', model: 'gpt-4o-mini' },
|
||||
complex: { provider: 'anthropic', model: 'claude-opus-4' },
|
||||
},
|
||||
} as unknown as import('../../config/schema.js').Config;
|
||||
|
||||
const updated = applyAuthModeToConfig(config, 'anthropic', 'oauth');
|
||||
|
||||
expect(updated.models.default.auth_mode).toBe('oauth');
|
||||
expect(updated.models.complex!.auth_mode).toBe('oauth');
|
||||
expect((updated.models.fast as { auth_mode?: string }).auth_mode).toBeUndefined();
|
||||
});
|
||||
|
||||
it('updates local_providers entries that match', () => {
|
||||
const config = {
|
||||
models: {
|
||||
default: { provider: 'openai', model: 'gpt-4o' },
|
||||
local_providers: {
|
||||
myAnthropic: { provider: 'anthropic', model: 'claude-haiku' },
|
||||
},
|
||||
},
|
||||
} as unknown as import('../../config/schema.js').Config;
|
||||
|
||||
const updated = applyAuthModeToConfig(config, 'anthropic', 'api_key');
|
||||
|
||||
expect(updated.models.local_providers!['myAnthropic'].auth_mode).toBe('api_key');
|
||||
expect((updated.models.default as { auth_mode?: string }).auth_mode).toBeUndefined();
|
||||
});
|
||||
|
||||
it('does not mutate the original config', () => {
|
||||
const config = {
|
||||
models: {
|
||||
default: { provider: 'anthropic', model: 'claude-sonnet-4' },
|
||||
},
|
||||
} as unknown as import('../../config/schema.js').Config;
|
||||
|
||||
applyAuthModeToConfig(config, 'anthropic', 'oauth');
|
||||
|
||||
expect((config.models.default as { auth_mode?: string }).auth_mode).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('AUTH_MODE_PROVIDERS', () => {
|
||||
it('contains anthropic and openai', () => {
|
||||
expect(AUTH_MODE_PROVIDERS.has('anthropic')).toBe(true);
|
||||
expect(AUTH_MODE_PROVIDERS.has('openai')).toBe(true);
|
||||
});
|
||||
|
||||
it('does not contain zhipuai, gemini, etc.', () => {
|
||||
expect(AUTH_MODE_PROVIDERS.has('zhipuai')).toBe(false);
|
||||
expect(AUTH_MODE_PROVIDERS.has('gemini')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -31,6 +31,41 @@ import { getElevationStatusMessage, setElevationFromInput } from '../../security
|
||||
|
||||
export { parseCommand, type Command };
|
||||
|
||||
/** Providers that honour auth_mode at runtime. All others get a warning. */
|
||||
export const AUTH_MODE_PROVIDERS: ReadonlySet<string> = new Set(['anthropic', 'openai']);
|
||||
|
||||
/**
|
||||
* Return a new Config with auth_mode set on every model tier whose provider
|
||||
* matches targetProvider. Does not mutate the original.
|
||||
*/
|
||||
export function applyAuthModeToConfig(
|
||||
config: Config,
|
||||
targetProvider: string,
|
||||
mode: 'api_key' | 'oauth' | 'auto',
|
||||
): Config {
|
||||
const applyToTier = (tier: ModelConfig): ModelConfig =>
|
||||
tier.provider === targetProvider ? { ...tier, auth_mode: mode } : tier;
|
||||
|
||||
const updatedModels = { ...config.models };
|
||||
|
||||
updatedModels.default = applyToTier(updatedModels.default);
|
||||
for (const key of ['fast', 'complex', 'local'] as const) {
|
||||
const tier = updatedModels[key];
|
||||
if (tier) {
|
||||
updatedModels[key] = applyToTier(tier);
|
||||
}
|
||||
}
|
||||
if (updatedModels.local_providers) {
|
||||
const updatedLocalProviders: Record<string, ModelConfig> = {};
|
||||
for (const [name, tier] of Object.entries(updatedModels.local_providers)) {
|
||||
updatedLocalProviders[name] = applyToTier(tier);
|
||||
}
|
||||
updatedModels.local_providers = updatedLocalProviders;
|
||||
}
|
||||
|
||||
return { ...config, models: updatedModels };
|
||||
}
|
||||
|
||||
// ANSI color codes
|
||||
const colors = {
|
||||
reset: '\x1b[0m',
|
||||
@@ -537,7 +572,7 @@ export class MinimalTui {
|
||||
break;
|
||||
|
||||
case 'login':
|
||||
await this.handleLoginCommand(command.provider);
|
||||
await this.handleLoginCommand(command.provider, command.mode);
|
||||
break;
|
||||
|
||||
case 'pair':
|
||||
@@ -983,7 +1018,32 @@ export class MinimalTui {
|
||||
}
|
||||
}
|
||||
|
||||
private async handleLoginCommand(provider?: string): Promise<void> {
|
||||
private async handleLoginCommand(
|
||||
provider?: string,
|
||||
mode?: 'api_key' | 'oauth' | 'auto',
|
||||
): Promise<void> {
|
||||
if (mode !== undefined) {
|
||||
const resolvedProvider = provider ?? 'anthropic';
|
||||
if (!AUTH_MODE_PROVIDERS.has(resolvedProvider)) {
|
||||
console.log(
|
||||
`${colors.gray}auth_mode has no effect for ${resolvedProvider}. ` +
|
||||
`It is only supported for: ${[...AUTH_MODE_PROVIDERS].join(', ')}.${colors.reset}\n`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!this.config.currentConfig || !this.config.configPath) {
|
||||
console.log(`${colors.gray}Config not available — cannot persist auth_mode.${colors.reset}\n`);
|
||||
return;
|
||||
}
|
||||
const updated = applyAuthModeToConfig(this.config.currentConfig, resolvedProvider, mode);
|
||||
persistConfig(this.config.configPath, updated);
|
||||
console.log(
|
||||
`${colors.gray}auth_mode for ${resolvedProvider} set to ${colors.reset}${mode}` +
|
||||
`${colors.gray}. Restart Flynn for the change to take effect.${colors.reset}\n`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const target = provider ?? 'github';
|
||||
const confirmReplace = async (): Promise<boolean> => {
|
||||
const answer = (await this.prompt(
|
||||
@@ -1131,6 +1191,18 @@ export class MinimalTui {
|
||||
this.rl.resume();
|
||||
}
|
||||
|
||||
// Offer to set auth_mode if config is available
|
||||
if (this.config.currentConfig && this.config.configPath) {
|
||||
const modeInput = (await this.prompt(
|
||||
`${colors.orange}Set active auth mode?${colors.reset} ${colors.gray}[api_key/oauth/auto/skip] (default: skip):${colors.reset} `,
|
||||
)).trim().toLowerCase();
|
||||
if (modeInput === 'api_key' || modeInput === 'oauth' || modeInput === 'auto') {
|
||||
const updated = applyAuthModeToConfig(this.config.currentConfig, 'openai', modeInput);
|
||||
persistConfig(this.config.configPath, updated);
|
||||
console.log(`${colors.gray}auth_mode set to ${modeInput}. Restart Flynn to apply.${colors.reset}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1161,6 +1233,18 @@ export class MinimalTui {
|
||||
console.log(`${colors.gray}OpenAI login failed:${colors.reset} ${message}\n`);
|
||||
}
|
||||
|
||||
// Offer to set auth_mode if config is available
|
||||
if (this.config.currentConfig && this.config.configPath) {
|
||||
const modeInput = (await this.prompt(
|
||||
`${colors.orange}Set active auth mode?${colors.reset} ${colors.gray}[api_key/oauth/auto/skip] (default: skip):${colors.reset} `,
|
||||
)).trim().toLowerCase();
|
||||
if (modeInput === 'api_key' || modeInput === 'oauth' || modeInput === 'auto') {
|
||||
const updated = applyAuthModeToConfig(this.config.currentConfig, 'openai', modeInput);
|
||||
persistConfig(this.config.configPath, updated);
|
||||
console.log(`${colors.gray}auth_mode set to ${modeInput}. Restart Flynn to apply.${colors.reset}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1199,6 +1283,18 @@ export class MinimalTui {
|
||||
this.rl.resume();
|
||||
}
|
||||
|
||||
// Offer to set auth_mode if config is available
|
||||
if (this.config.currentConfig && this.config.configPath) {
|
||||
const modeInput = (await this.prompt(
|
||||
`${colors.orange}Set active auth mode?${colors.reset} ${colors.gray}[api_key/oauth/auto/skip] (default: skip):${colors.reset} `,
|
||||
)).trim().toLowerCase();
|
||||
if (modeInput === 'api_key' || modeInput === 'oauth' || modeInput === 'auto') {
|
||||
const updated = applyAuthModeToConfig(this.config.currentConfig, 'anthropic', modeInput);
|
||||
persistConfig(this.config.configPath, updated);
|
||||
console.log(`${colors.gray}auth_mode set to ${modeInput}. Restart Flynn to apply.${colors.reset}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1228,6 +1324,18 @@ export class MinimalTui {
|
||||
this.rl.resume();
|
||||
}
|
||||
|
||||
// Offer to set auth_mode if config is available
|
||||
if (this.config.currentConfig && this.config.configPath) {
|
||||
const modeInput = (await this.prompt(
|
||||
`${colors.orange}Set active auth mode?${colors.reset} ${colors.gray}[api_key/oauth/auto/skip] (default: skip):${colors.reset} `,
|
||||
)).trim().toLowerCase();
|
||||
if (modeInput === 'api_key' || modeInput === 'oauth' || modeInput === 'auto') {
|
||||
const updated = applyAuthModeToConfig(this.config.currentConfig, 'anthropic', modeInput);
|
||||
persistConfig(this.config.configPath, updated);
|
||||
console.log(`${colors.gray}auth_mode set to ${modeInput}. Restart Flynn to apply.${colors.reset}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user