From 0c697406947ef37f194db26e31d5822cc7e12463 Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Fri, 26 Dec 2025 13:37:54 -0500 Subject: [PATCH 1/2] fix(dashboard): support unified config.yaml in web server routes Add loadConfigSafe() function that handles both unified (config.yaml) and legacy (config.json) config formats. Uses throwable errors instead of process.exit() so try/catch blocks work properly in web server routes. Updated files to use loadConfigSafe(): - overview-routes.ts: Dashboard overview API - route-helpers.ts: readConfigSafe() helper - profile-reader.ts: API profile reading - profile-writer.ts: API profile writing - variant-config-adapter.ts: CLIProxy variant config Fixes #206 (Problem 2: config.json not found when user has config.yaml) --- src/api/services/profile-reader.ts | 8 +-- src/api/services/profile-writer.ts | 4 +- .../services/variant-config-adapter.ts | 6 +- src/types/index.ts | 1 + src/utils/config-manager.ts | 59 ++++++++++++++++++- src/web-server/overview-routes.ts | 4 +- src/web-server/routes/route-helpers.ts | 7 ++- 7 files changed, 74 insertions(+), 15 deletions(-) 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: {} }; } From a4a473ac93a3adf9b51a0e9371bbd48fa7363157 Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Fri, 26 Dec 2025 13:47:17 -0500 Subject: [PATCH 2/2] fix(config): use safe inline logic in getSettingsPath() legacy fallback The try/catch around loadConfig() in getSettingsPath() was ineffective because process.exit() cannot be caught by try/catch - it terminates the process immediately. Replace with inline safe logic that: 1. Checks if config.json exists with fs.existsSync() 2. Reads and parses JSON manually 3. Uses isConfig() type guard for validation 4. Properly catches JSON parse errors This ensures unified mode users don't crash when falling back to check legacy config.json for profiles not found in config.yaml. --- src/utils/config-manager.ts | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/utils/config-manager.ts b/src/utils/config-manager.ts index 02b4e6db..a28938e5 100644 --- a/src/utils/config-manager.ts +++ b/src/utils/config-manager.ts @@ -163,19 +163,24 @@ export function getSettingsPath(profile: string): string { // If not found in unified config, try legacy config.json as fallback if (!settingsPath) { - try { - const legacyConfig = loadConfig(); - if (legacyConfig.profiles[profile]) { - settingsPath = legacyConfig.profiles[profile]; - // Merge legacy profiles into available list (avoid duplicates) - for (const p of Object.keys(legacyConfig.profiles)) { - if (!availableProfiles.includes(p)) { - availableProfiles.push(p); + // Use inline safe logic - loadConfig() calls process.exit() which cannot be caught + const configPath = getConfigPath(); + if (fs.existsSync(configPath)) { + try { + const raw = fs.readFileSync(configPath, 'utf8'); + const legacyConfig: unknown = JSON.parse(raw); + if (isConfig(legacyConfig) && legacyConfig.profiles[profile]) { + settingsPath = legacyConfig.profiles[profile]; + // Merge legacy profiles into available list (avoid duplicates) + for (const p of Object.keys(legacyConfig.profiles)) { + if (!availableProfiles.includes(p)) { + availableProfiles.push(p); + } } } + } catch { + // Legacy config is invalid JSON - that's OK in unified mode } - } catch { - // Legacy config doesn't exist or is invalid - that's OK in unified mode } } } else {