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)
This commit is contained in:
kaitranntt
2025-12-26 13:37:54 -05:00
parent 2093ae2cc8
commit 0c69740694
7 changed files with 74 additions and 15 deletions
+4 -4
View File
@@ -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);
}
+2 -2
View File
@@ -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();
@@ -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<string, VariantConfig> {
return result;
}
const config = loadConfig();
const config = loadConfigSafe();
const variants = config.cliproxy || {};
const result: Record<string, VariantConfig> = {};
for (const name of Object.keys(variants)) {
+1
View File
@@ -12,6 +12,7 @@ export type {
EnvValue,
ProfileMetadata,
ProfilesRegistry,
CLIProxyVariantsConfig,
} from './config';
export { isConfig, isSettings } from './config';
+58 -1
View File
@@ -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<string, string> = {};
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,
+2 -2
View File
@@ -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;
+4 -3
View File
@@ -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: {} };
}