From 0be397784525275d0bbcc94877942f8963ca3d33 Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Sat, 27 Dec 2025 00:05:31 -0500 Subject: [PATCH 1/5] fix: run RecoveryManager before early-exit commands and improve config handling Fixes #214 - Fresh install fails with 'Config not found' when running ccs config before RecoveryManager runs. Changes: - Move RecoveryManager.recoverAll() before all early-exit commands in ccs.ts - Fix loadConfigSafe() to return empty config instead of throwing in legacy mode - Add getActiveConfigPath() for mode-aware config path resolution - Rename cliproxy getConfigPath() to getCliproxyConfigPath() to avoid confusion - Update help-command.ts to show correct config path based on mode This ensures all commands benefit from auto-recovery on fresh installs, and gracefully handles missing config files in all code paths. --- src/ccs.ts | 22 ++++++++++--------- src/cliproxy/config-generator.ts | 7 +++--- src/cliproxy/index.ts | 2 +- src/cliproxy/openai-compat-manager.ts | 6 ++--- src/commands/help-command.ts | 3 ++- src/management/checks/cliproxy-check.ts | 4 ++-- src/utils/config-manager.ts | 19 ++++++++++++++-- src/web-server/health/cliproxy-checks.ts | 2 +- .../routes/cliproxy-stats-routes.ts | 6 ++--- 9 files changed, 45 insertions(+), 26 deletions(-) diff --git a/src/ccs.ts b/src/ccs.ts index 572d1a60..02550848 100644 --- a/src/ccs.ts +++ b/src/ccs.ts @@ -291,6 +291,18 @@ async function main(): Promise { await autoMigrate(); } + // Auto-recovery for missing configuration (BEFORE any early-exit commands) + // This ensures ALL commands benefit from auto-recovery, not just profile-switching flow + // Recovery is safe to run early - it only creates missing files with safe defaults + const RecoveryManagerModule = await import('./management/recovery-manager'); + const RecoveryManager = RecoveryManagerModule.default; + const recovery = new RecoveryManager(); + const recovered = recovery.recoverAll(); + + if (recovered) { + recovery.showRecoveryHints(); + } + // Special case: version command (check BEFORE profile detection) if (firstArg === 'version' || firstArg === '--version' || firstArg === '-v') { handleVersionCommand(); @@ -455,16 +467,6 @@ async function main(): Promise { return; } - // Auto-recovery for missing configuration - const RecoveryManagerModule = await import('./management/recovery-manager'); - const RecoveryManager = RecoveryManagerModule.default; - const recovery = new RecoveryManager(); - const recovered = recovery.recoverAll(); - - if (recovered) { - recovery.showRecoveryHints(); - } - // First-time install: offer setup wizard for interactive users // Check independently of recovery status (user may have empty config.yaml) // Skip if headless, CI, or non-TTY environment diff --git a/src/cliproxy/config-generator.ts b/src/cliproxy/config-generator.ts index 81d42c37..c2d4b83d 100644 --- a/src/cliproxy/config-generator.ts +++ b/src/cliproxy/config-generator.ts @@ -129,9 +129,10 @@ export function getConfigPathForPort(port: number): string { } /** - * Get config file path (default port) + * Get CLIProxy config file path (default port) + * Named distinctly from config-manager's getConfigPath to avoid confusion. */ -export function getConfigPath(): string { +export function getCliproxyConfigPath(): string { return getConfigPathForPort(CLIPROXY_DEFAULT_PORT); } @@ -377,7 +378,7 @@ export function regenerateConfig(port: number = CLIPROXY_DEFAULT_PORT): string { * @returns true if config should be regenerated */ export function configNeedsRegeneration(): boolean { - const configPath = getConfigPath(); + const configPath = getCliproxyConfigPath(); if (!fs.existsSync(configPath)) { return false; // Will be created on first use } diff --git a/src/cliproxy/index.ts b/src/cliproxy/index.ts index edbf9fd0..08bc50a2 100644 --- a/src/cliproxy/index.ts +++ b/src/cliproxy/index.ts @@ -65,7 +65,7 @@ export { getCliproxyDir, getProviderAuthDir, getAuthDir, - getConfigPath, + getCliproxyConfigPath, getBinDir, configExists, deleteConfig, diff --git a/src/cliproxy/openai-compat-manager.ts b/src/cliproxy/openai-compat-manager.ts index 5254a156..9bb8dfd4 100644 --- a/src/cliproxy/openai-compat-manager.ts +++ b/src/cliproxy/openai-compat-manager.ts @@ -7,7 +7,7 @@ import * as fs from 'fs'; import * as yaml from 'js-yaml'; -import { getConfigPath } from './config-generator'; +import { getCliproxyConfigPath } from './config-generator'; /** Model alias configuration */ export interface OpenAICompatModel { @@ -48,7 +48,7 @@ interface ConfigYaml { * Load current config.yaml */ function loadConfig(): ConfigYaml { - const configPath = getConfigPath(); + const configPath = getCliproxyConfigPath(); if (!fs.existsSync(configPath)) { return {}; } @@ -65,7 +65,7 @@ function loadConfig(): ConfigYaml { * Save config.yaml with proper formatting */ function saveConfig(config: ConfigYaml): void { - const configPath = getConfigPath(); + const configPath = getCliproxyConfigPath(); const content = yaml.dump(config, { lineWidth: -1, // Disable line wrapping quotingType: '"', diff --git a/src/commands/help-command.ts b/src/commands/help-command.ts index 82f70a86..4115a622 100644 --- a/src/commands/help-command.ts +++ b/src/commands/help-command.ts @@ -1,6 +1,7 @@ import * as fs from 'fs'; import * as path from 'path'; import { initUI, box, color, dim, sectionHeader, subheader } from '../utils/ui'; +import { isUnifiedMode } from '../config/unified-config-loader'; // Get version from package.json (same as version-command.ts) const VERSION = JSON.parse( @@ -234,7 +235,7 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim(); // Configuration printConfigSection('Configuration', [ - ['Config File:', '~/.ccs/config.json'], + ['Config File:', isUnifiedMode() ? '~/.ccs/config.yaml' : '~/.ccs/config.json'], ['Profiles:', '~/.ccs/profiles.json'], ['Instances:', '~/.ccs/instances/'], ['Settings:', '~/.ccs/*.settings.json'], diff --git a/src/management/checks/cliproxy-check.ts b/src/management/checks/cliproxy-check.ts index a4424f30..4a4998b6 100644 --- a/src/management/checks/cliproxy-check.ts +++ b/src/management/checks/cliproxy-check.ts @@ -8,7 +8,7 @@ import { isCLIProxyInstalled, getCLIProxyPath, getAllAuthStatus, - getConfigPath, + getCliproxyConfigPath, getInstalledCliproxyVersion, CLIPROXY_DEFAULT_PORT, configNeedsRegeneration, @@ -60,7 +60,7 @@ export class CLIProxyConfigChecker implements IHealthChecker { run(results: HealthCheck): void { const spinner = ora('Checking CLIProxy config').start(); - const configPath = getConfigPath(); + const configPath = getCliproxyConfigPath(); if (fs.existsSync(configPath)) { // Check if config needs regeneration (version mismatch or missing features) diff --git a/src/utils/config-manager.ts b/src/utils/config-manager.ts index a28938e5..08368f45 100644 --- a/src/utils/config-manager.ts +++ b/src/utils/config-manager.ts @@ -27,12 +27,26 @@ export function getCcsDir(): string { } /** - * Get config file path + * Get config file path (legacy JSON path) + * @deprecated Use getActiveConfigPath() for mode-aware config path */ export function getConfigPath(): string { return process.env.CCS_CONFIG || path.join(getCcsHome(), '.ccs', 'config.json'); } +/** + * Get the active config file path based on current mode. + * Returns config.yaml in unified mode, config.json in legacy mode. + * @returns Path to the active config file + */ +export function getActiveConfigPath(): string { + const ccsDir = getCcsDir(); + if (isUnifiedMode()) { + return path.join(ccsDir, 'config.yaml'); + } + return path.join(ccsDir, 'config.json'); +} + /** * Load and validate config.json */ @@ -126,7 +140,8 @@ export function loadConfigSafe(): Config { const configPath = getConfigPath(); if (!fs.existsSync(configPath)) { - throw new Error(`Config not found: ${configPath}`); + // Return empty config for graceful degradation (matches unified mode behavior) + return { profiles: {} }; } const raw = fs.readFileSync(configPath, 'utf8'); diff --git a/src/web-server/health/cliproxy-checks.ts b/src/web-server/health/cliproxy-checks.ts index 80f33e8e..5c1ee2f1 100644 --- a/src/web-server/health/cliproxy-checks.ts +++ b/src/web-server/health/cliproxy-checks.ts @@ -9,7 +9,7 @@ import { isCLIProxyInstalled, getInstalledCliproxyVersion, getCLIProxyPath, - getConfigPath as getCliproxyConfigPath, + getCliproxyConfigPath, getAllAuthStatus, CLIPROXY_DEFAULT_PORT, } from '../../cliproxy'; diff --git a/src/web-server/routes/cliproxy-stats-routes.ts b/src/web-server/routes/cliproxy-stats-routes.ts index a4ac196c..53e32c9a 100644 --- a/src/web-server/routes/cliproxy-stats-routes.ts +++ b/src/web-server/routes/cliproxy-stats-routes.ts @@ -14,7 +14,7 @@ import { } from '../../cliproxy/stats-fetcher'; import { getCliproxyWritablePath, - getConfigPath, + getCliproxyConfigPath, getAuthDir, } from '../../cliproxy/config-generator'; import { getProxyStatus as getProxyProcessStatus, stopProxy } from '../../cliproxy/session-tracker'; @@ -272,7 +272,7 @@ router.get('/error-logs/:name', async (req: Request, res: Response): Promise => { try { - const configPath = getConfigPath(); + const configPath = getCliproxyConfigPath(); if (!fs.existsSync(configPath)) { res.status(404).json({ error: 'Config file not found' }); return; @@ -299,7 +299,7 @@ router.put('/config.yaml', async (req: Request, res: Response): Promise => return; } - const configPath = getConfigPath(); + const configPath = getCliproxyConfigPath(); // Ensure parent directory exists const configDir = path.dirname(configPath); From ec2ee0a36d8498fb596d2e3ef793ce89a9f254f8 Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Sat, 27 Dec 2025 01:14:54 -0500 Subject: [PATCH 2/5] fix(tests): update test files for renamed getCliproxyConfigPath function Update tests to use renamed function after refactoring getConfigPath to getCliproxyConfigPath in cliproxy/config-generator.ts Files updated: - tests/unit/cliproxy/config-generator-port.test.js - tests/unit/cliproxy/config-generator.test.js --- tests/unit/cliproxy/config-generator-port.test.js | 6 +++--- tests/unit/cliproxy/config-generator.test.js | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/unit/cliproxy/config-generator-port.test.js b/tests/unit/cliproxy/config-generator-port.test.js index 2e4c6f0a..dfbb76d5 100644 --- a/tests/unit/cliproxy/config-generator-port.test.js +++ b/tests/unit/cliproxy/config-generator-port.test.js @@ -19,7 +19,7 @@ process.env.CCS_HOME = testHome; const { getConfigPathForPort, - getConfigPath, + getCliproxyConfigPath, generateConfig, regenerateConfig, configExists, @@ -98,9 +98,9 @@ describe('Config Generator Port', function () { }); }); - describe('getConfigPath', function () { + describe('getCliproxyConfigPath', function () { it('returns path for default port', function () { - const configPath = getConfigPath(); + const configPath = getCliproxyConfigPath(); const defaultPath = getConfigPathForPort(CLIPROXY_DEFAULT_PORT); assert.strictEqual(configPath, defaultPath); }); diff --git a/tests/unit/cliproxy/config-generator.test.js b/tests/unit/cliproxy/config-generator.test.js index eab0743d..703b5fe6 100644 --- a/tests/unit/cliproxy/config-generator.test.js +++ b/tests/unit/cliproxy/config-generator.test.js @@ -362,7 +362,7 @@ auth-dir: "/test" let testDir; let originalCcsHome; let regenerateConfig; - let getConfigPath; + let getCliproxyConfigPath; beforeEach(() => { // Create a temporary test directory @@ -375,7 +375,7 @@ auth-dir: "/test" delete require.cache[require.resolve('../../../dist/utils/config-manager')]; const configGenerator = require('../../../dist/cliproxy/config-generator'); regenerateConfig = configGenerator.regenerateConfig; - getConfigPath = configGenerator.getConfigPath; + getCliproxyConfigPath = configGenerator.getCliproxyConfigPath; }); afterEach(() => { From 2fff770b6bc67616e855cc8dc940751bd1267a67 Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Sat, 27 Dec 2025 12:25:59 -0500 Subject: [PATCH 3/5] fix: wrap RecoveryManager in try-catch to prevent blocking CLI commands Recovery failures (permission errors, disk full) no longer block --version/--help commands. Recovery is best-effort - warns on failure but allows basic CLI functionality to continue. Fixes edge case identified in PR #215 code review. --- src/ccs.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/ccs.ts b/src/ccs.ts index 02550848..dc2df697 100644 --- a/src/ccs.ts +++ b/src/ccs.ts @@ -294,13 +294,19 @@ async function main(): Promise { // Auto-recovery for missing configuration (BEFORE any early-exit commands) // This ensures ALL commands benefit from auto-recovery, not just profile-switching flow // Recovery is safe to run early - it only creates missing files with safe defaults - const RecoveryManagerModule = await import('./management/recovery-manager'); - const RecoveryManager = RecoveryManagerModule.default; - const recovery = new RecoveryManager(); - const recovered = recovery.recoverAll(); + // Wrapped in try-catch to prevent blocking --version/--help on permission errors + try { + const RecoveryManagerModule = await import('./management/recovery-manager'); + const RecoveryManager = RecoveryManagerModule.default; + const recovery = new RecoveryManager(); + const recovered = recovery.recoverAll(); - if (recovered) { - recovery.showRecoveryHints(); + if (recovered) { + recovery.showRecoveryHints(); + } + } catch (err) { + // Recovery is best-effort - don't block basic CLI functionality + console.warn('[!] Recovery failed:', (err as Error).message); } // Special case: version command (check BEFORE profile detection) From 8a3c5a446beb197148a132900a88f09043cbab55 Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Sat, 27 Dec 2025 12:28:59 -0500 Subject: [PATCH 4/5] fix: improve type safety and error handling in config-manager - Wrap JSON.parse in try-catch with clear error message for malformed JSON - Add iflow, kiro, ghcp providers to CLIProxyVariantConfig type - Make settings optional in CLIProxyVariantConfig (was required with empty string fallback) - Remove unsafe type cast in loadConfigSafe() Addresses additional edge cases from PR #215 code review. --- src/types/config.ts | 8 ++++---- src/utils/config-manager.ts | 13 +++++++++---- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/types/config.ts b/src/types/config.ts index fe3d661c..a00195a3 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -17,10 +17,10 @@ export interface ProfilesConfig { * Example: "flash" → gemini provider with gemini-2.5-flash model */ export interface CLIProxyVariantConfig { - /** CLIProxy provider to use (gemini, codex, agy, qwen) */ - provider: 'gemini' | 'codex' | 'agy' | 'qwen'; - /** Path to settings.json with custom model configuration */ - settings: string; + /** CLIProxy provider to use */ + provider: 'gemini' | 'codex' | 'agy' | 'qwen' | 'iflow' | 'kiro' | 'ghcp'; + /** Path to settings.json with custom model configuration (optional) */ + settings?: string; /** Account identifier for multi-account support (optional, defaults to 'default') */ account?: string; /** Unique port for variant isolation (8318-8417) */ diff --git a/src/utils/config-manager.ts b/src/utils/config-manager.ts index 08368f45..e1169137 100644 --- a/src/utils/config-manager.ts +++ b/src/utils/config-manager.ts @@ -121,9 +121,8 @@ export function loadConfigSafe(): Config { cliproxy = {}; for (const [name, variant] of Object.entries(unifiedConfig.cliproxy.variants)) { cliproxy[name] = { - // Cast provider - unified has more providers than legacy type - provider: variant.provider as 'gemini' | 'codex' | 'agy' | 'qwen', - settings: variant.settings || '', + provider: variant.provider, + settings: variant.settings, account: variant.account, port: variant.port, }; @@ -145,7 +144,13 @@ export function loadConfigSafe(): Config { } const raw = fs.readFileSync(configPath, 'utf8'); - const parsed: unknown = JSON.parse(raw); + + let parsed: unknown; + try { + parsed = JSON.parse(raw); + } catch (e) { + throw new Error(`Malformed JSON in config: ${configPath} - ${(e as Error).message}`); + } if (!isConfig(parsed)) { throw new Error(`Invalid config format: ${configPath}`); From 67a48a8305125959ecab468f117cc9de0badddd5 Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Sat, 27 Dec 2025 16:30:25 -0500 Subject: [PATCH 5/5] fix(test): remove redundant build from beforeAll hook CI already runs bun run build:all before validate step. Duplicate build in test hook was causing timeout in CI. --- tests/unit/utils/update-checker-beta-channel.test.js | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/tests/unit/utils/update-checker-beta-channel.test.js b/tests/unit/utils/update-checker-beta-channel.test.js index 4cca9820..964cfc75 100644 --- a/tests/unit/utils/update-checker-beta-channel.test.js +++ b/tests/unit/utils/update-checker-beta-channel.test.js @@ -27,14 +27,7 @@ describe('Beta Channel Implementation (Phase 3)', function () { let httpsRequests = []; beforeAll(async function () { - // Build the project first - const { execSync } = require('child_process'); - try { - execSync('bun run build', { cwd: path.resolve(__dirname, '../../..'), stdio: 'pipe' }); - } catch (error) { - console.warn('Build failed, tests may not work:', error.message); - } - + // Note: Build is handled by CI before tests run (bun run build:all) // Import the built module updateCheckerModule = await import('../../../dist/utils/update-checker.js'); });