diff --git a/src/api/services/profile-reader.ts b/src/api/services/profile-reader.ts index 7e6dd129..2773e0ad 100644 --- a/src/api/services/profile-reader.ts +++ b/src/api/services/profile-reader.ts @@ -7,7 +7,7 @@ import * as fs from 'fs'; import * as path from 'path'; -import { getCcsDir, loadConfig } from '../../utils/config-manager'; +import { getCcsDir, loadConfigSafe } from '../../utils/config-manager'; import { loadOrCreateUnifiedConfig, isUnifiedMode } from '../../config/unified-config-loader'; import type { ApiProfileInfo, CliproxyVariantInfo, ApiListResult } from './profile-types'; @@ -20,7 +20,7 @@ export function apiProfileExists(name: string): boolean { const config = loadOrCreateUnifiedConfig(); return name in config.profiles; } - const config = loadConfig(); + const config = loadConfigSafe(); return name in config.profiles; } catch { return false; @@ -79,7 +79,7 @@ export function listApiProfiles(): ApiListResult { }); } } else { - const config = loadConfig(); + const config = loadConfigSafe(); for (const [name, settingsPath] of Object.entries(config.profiles)) { // Skip 'default' profile - it's the user's native Claude settings if (name === 'default' && (settingsPath as string).includes('.claude/settings.json')) { @@ -116,7 +116,7 @@ export function getApiProfileNames(): string[] { const config = loadOrCreateUnifiedConfig(); return Object.keys(config.profiles); } - const config = loadConfig(); + const config = loadConfigSafe(); return Object.keys(config.profiles); } diff --git a/src/api/services/profile-writer.ts b/src/api/services/profile-writer.ts index fbc42a71..48c34ff1 100644 --- a/src/api/services/profile-writer.ts +++ b/src/api/services/profile-writer.ts @@ -5,7 +5,7 @@ import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; -import { getCcsDir, getConfigPath, loadConfig } from '../../utils/config-manager'; +import { getCcsDir, getConfigPath, loadConfigSafe } from '../../utils/config-manager'; import { expandPath } from '../../utils/helpers'; import { loadOrCreateUnifiedConfig, @@ -166,7 +166,7 @@ function removeApiProfileUnified(name: string): void { /** Remove API profile from legacy config */ function removeApiProfileLegacy(name: string): void { - const config = loadConfig(); + const config = loadConfigSafe(); delete config.profiles[name]; const configPath = getConfigPath(); diff --git a/src/cliproxy/services/variant-config-adapter.ts b/src/cliproxy/services/variant-config-adapter.ts index d4b8331e..4294b956 100644 --- a/src/cliproxy/services/variant-config-adapter.ts +++ b/src/cliproxy/services/variant-config-adapter.ts @@ -5,7 +5,7 @@ */ import * as fs from 'fs'; -import { getConfigPath, loadConfig } from '../../utils/config-manager'; +import { getConfigPath, loadConfigSafe } from '../../utils/config-manager'; import { CLIProxyProvider } from '../types'; import { loadOrCreateUnifiedConfig, @@ -38,7 +38,7 @@ export function variantExistsInConfig(name: string): boolean { const config = loadOrCreateUnifiedConfig(); return !!(config.cliproxy?.variants && name in config.cliproxy.variants); } - const config = loadConfig(); + const config = loadConfigSafe(); return !!(config.cliproxy && name in config.cliproxy); } catch { return false; @@ -94,7 +94,7 @@ export function listVariantsFromConfig(): Record { return result; } - const config = loadConfig(); + const config = loadConfigSafe(); const variants = config.cliproxy || {}; const result: Record = {}; for (const name of Object.keys(variants)) { diff --git a/src/types/index.ts b/src/types/index.ts index 648ae97f..5ed78646 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -12,6 +12,7 @@ export type { EnvValue, ProfileMetadata, ProfilesRegistry, + CLIProxyVariantsConfig, } from './config'; export { isConfig, isSettings } from './config'; diff --git a/src/utils/config-manager.ts b/src/utils/config-manager.ts index 034a47ec..02b4e6db 100644 --- a/src/utils/config-manager.ts +++ b/src/utils/config-manager.ts @@ -1,7 +1,7 @@ import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; -import { Config, isConfig, Settings, isSettings } from '../types'; +import { Config, isConfig, Settings, isSettings, CLIProxyVariantsConfig } from '../types'; import { expandPath, error } from './helpers'; import { info } from './ui'; import { isUnifiedMode, loadOrCreateUnifiedConfig } from '../config/unified-config-loader'; @@ -82,6 +82,63 @@ export function readConfig(): Config { return loadConfig(); } +/** + * Load config safely with unified mode support. + * Returns Config with profiles from unified config.yaml or legacy config.json. + * Throws on error (catchable) instead of process.exit. + * Use this in web server routes where try/catch is needed. + */ +export function loadConfigSafe(): Config { + // Unified mode: extract profiles from config.yaml + if (isUnifiedMode()) { + const unifiedConfig = loadOrCreateUnifiedConfig(); + + // Convert unified profiles to legacy format for compatibility + const profiles: Record = {}; + for (const [name, profile] of Object.entries(unifiedConfig.profiles)) { + if (profile.settings) { + profiles[name] = profile.settings; + } + } + + // Convert unified cliproxy variants to legacy format + let cliproxy: CLIProxyVariantsConfig | undefined; + if (unifiedConfig.cliproxy?.variants) { + 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 || '', + account: variant.account, + port: variant.port, + }; + } + } + + return { + profiles, + cliproxy, + }; + } + + // Legacy mode: read config.json + const configPath = getConfigPath(); + + if (!fs.existsSync(configPath)) { + throw new Error(`Config not found: ${configPath}`); + } + + const raw = fs.readFileSync(configPath, 'utf8'); + const parsed: unknown = JSON.parse(raw); + + if (!isConfig(parsed)) { + throw new Error(`Invalid config format: ${configPath}`); + } + + return parsed; +} + /** * Get settings path for profile. * In unified mode (config.yaml exists), reads from config.yaml first, diff --git a/src/web-server/overview-routes.ts b/src/web-server/overview-routes.ts index b2cd6525..2b064ce8 100644 --- a/src/web-server/overview-routes.ts +++ b/src/web-server/overview-routes.ts @@ -5,7 +5,7 @@ */ import { Router, Request, Response } from 'express'; -import { loadConfig } from '../utils/config-manager'; +import { loadConfigSafe } from '../utils/config-manager'; import { runHealthChecks } from './health-service'; import { getAllAuthStatus, initializeAccounts } from '../cliproxy/auth-handler'; import { getVersion } from '../utils/version'; @@ -18,7 +18,7 @@ export const overviewRoutes = Router(); */ overviewRoutes.get('/', async (_req: Request, res: Response) => { try { - const config = loadConfig(); + const config = loadConfigSafe(); const profileCount = Object.keys(config.profiles).length; const cliproxyVariantCount = Object.keys(config.cliproxy || {}).length; diff --git a/src/web-server/routes/route-helpers.ts b/src/web-server/routes/route-helpers.ts index 4478059a..186ba8bc 100644 --- a/src/web-server/routes/route-helpers.ts +++ b/src/web-server/routes/route-helpers.ts @@ -4,7 +4,7 @@ import * as fs from 'fs'; import * as path from 'path'; -import { getCcsDir, getConfigPath, loadConfig, loadSettings } from '../../utils/config-manager'; +import { getCcsDir, getConfigPath, loadConfigSafe, loadSettings } from '../../utils/config-manager'; import { expandPath } from '../../utils/helpers'; import type { Config, Settings } from '../../types/config'; @@ -17,11 +17,12 @@ export interface ModelMapping { } /** - * Read config safely with fallback + * Read config safely with fallback. + * Uses loadConfigSafe which supports both unified (config.yaml) and legacy (config.json). */ export function readConfigSafe(): Config { try { - return loadConfig(); + return loadConfigSafe(); } catch { return { profiles: {} }; }