diff --git a/src/commands/setup-command.ts b/src/commands/setup-command.ts index 5b7acf5a..bd17af07 100644 --- a/src/commands/setup-command.ts +++ b/src/commands/setup-command.ts @@ -113,12 +113,14 @@ async function selectOption( } /** - * Check if this is a first-time install (config exists but is empty/unconfigured) - * Returns true if user should be prompted to run setup wizard + * Check if this is a first-time install (config exists but is empty/unconfigured). + * Returns true if user should be prompted to run setup wizard. * - * IMPORTANT: Also checks legacy config.json for existing profiles to avoid - * treating users with existing GLM/Kimi setups as "first-time installs" - * (Fix for issue #195 - GLM auth persistence regression) + * Detection priority: + * 1. setup_completed flag (explicit wizard completion) + * 2. Unified config content (profiles, accounts, variants, oauth_accounts, remote proxy) + * 3. Legacy config.json profiles (GLM, Kimi) + * 4. Legacy profiles.json accounts */ export function isFirstTimeInstall(): boolean { // Check unified config first (config.yaml) @@ -132,6 +134,11 @@ export function isFirstTimeInstall(): boolean { return false; } + // If setup wizard was completed, NOT first-time install + if (loaded.setup_completed) { + return false; + } + // Check for any meaningful configuration in unified config const hasProfiles = Object.keys(loaded.profiles || {}).length > 0; const hasAccounts = Object.keys(loaded.accounts || {}).length > 0; @@ -370,6 +377,7 @@ async function runSetupWizard(force: boolean = false): Promise { } // Save config + config.setup_completed = true; saveUnifiedConfig(config); // Final summary diff --git a/src/config/unified-config-loader.ts b/src/config/unified-config-loader.ts index a495b7e3..4cf39d60 100644 --- a/src/config/unified-config-loader.ts +++ b/src/config/unified-config-loader.ts @@ -139,6 +139,7 @@ function mergeWithDefaults(partial: Partial): UnifiedConfig { const defaults = createEmptyUnifiedConfig(); return { version: partial.version ?? defaults.version, + setup_completed: partial.setup_completed, default: partial.default ?? defaults.default, accounts: partial.accounts ?? defaults.accounts, profiles: partial.profiles ?? defaults.profiles, @@ -326,6 +327,9 @@ function generateYamlWithComments(config: UnifiedConfig): string { // Version lines.push(`version: ${config.version}`); + if (config.setup_completed !== undefined) { + lines.push(`setup_completed: ${config.setup_completed}`); + } lines.push(''); // Default diff --git a/src/config/unified-config-types.ts b/src/config/unified-config-types.ts index 2315f67d..f2d3618e 100644 --- a/src/config/unified-config-types.ts +++ b/src/config/unified-config-types.ts @@ -521,6 +521,8 @@ export const DEFAULT_DASHBOARD_AUTH_CONFIG: DashboardAuthConfig = { export interface UnifiedConfig { /** Config version (7 for quota management) */ version: number; + /** Flag indicating setup wizard has been completed */ + setup_completed?: boolean; /** Default profile name to use when none specified */ default?: string; /** Account-based profiles (isolated Claude instances) */ diff --git a/tests/unit/commands/setup-command.test.ts b/tests/unit/commands/setup-command.test.ts index e4bb37b4..8277aefb 100644 --- a/tests/unit/commands/setup-command.test.ts +++ b/tests/unit/commands/setup-command.test.ts @@ -115,6 +115,51 @@ cliproxy_server: }); }); + describe('setup_completed flag detection', () => { + it('should detect setup_completed flag in unified config', () => { + createConfigYaml(` +version: 8 +setup_completed: true +profiles: {} +accounts: {} +`); + + const content = fs.readFileSync(path.join(testDir, 'config.yaml'), 'utf8'); + const hasSetupCompleted = content.includes('setup_completed: true'); + + expect(hasSetupCompleted).toBe(true); + }); + + it('should treat missing setup_completed as first-time eligible', () => { + createConfigYaml(` +version: 8 +profiles: {} +accounts: {} +`); + + const content = fs.readFileSync(path.join(testDir, 'config.yaml'), 'utf8'); + const hasSetupCompleted = content.includes('setup_completed: true'); + + expect(hasSetupCompleted).toBe(false); + }); + + it('should treat setup_completed: false as first-time eligible', () => { + createConfigYaml(` +version: 8 +setup_completed: false +profiles: {} +accounts: {} +`); + + const content = fs.readFileSync(path.join(testDir, 'config.yaml'), 'utf8'); + const hasSetupCompletedTrue = content.includes('setup_completed: true'); + const hasSetupCompletedFalse = content.includes('setup_completed: false'); + + expect(hasSetupCompletedTrue).toBe(false); + expect(hasSetupCompletedFalse).toBe(true); + }); + }); + describe('corrupted config handling', () => { it('should handle corrupted config.json gracefully', () => { fs.writeFileSync(path.join(testDir, 'config.json'), 'not valid json{{{', 'utf8');