diff --git a/src/ccs.ts b/src/ccs.ts index 572d1a60..dc2df697 100644 --- a/src/ccs.ts +++ b/src/ccs.ts @@ -291,6 +291,24 @@ 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 + // 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(); + } + } 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) if (firstArg === 'version' || firstArg === '--version' || firstArg === '-v') { handleVersionCommand(); @@ -455,16 +473,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 57e86446..abb4ee01 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( @@ -235,7 +236,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/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 a28938e5..e1169137 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 */ @@ -107,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, }; @@ -126,11 +139,18 @@ 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'); - 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}`); 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); 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(() => { 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'); });