diff --git a/src/cliproxy/model-config.ts b/src/cliproxy/model-config.ts index ab9e62b5..ddec180b 100644 --- a/src/cliproxy/model-config.ts +++ b/src/cliproxy/model-config.ts @@ -6,6 +6,7 @@ */ import * as fs from 'fs'; +import * as os from 'os'; import * as path from 'path'; import { InteractivePrompt } from '../utils/prompt'; import { getProviderCatalog, supportsModelConfig, ModelEntry } from './model-catalog'; @@ -14,7 +15,7 @@ import { CLIProxyProvider } from './types'; import { initUI, color, bold, dim, ok, info, header } from '../utils/ui'; /** CCS directory */ -const CCS_DIR = path.join(process.env.HOME || process.env.USERPROFILE || '', '.ccs'); +const CCS_DIR = path.join(os.homedir(), '.ccs'); /** * Check if provider has user settings configured @@ -34,7 +35,7 @@ export function getCurrentModel( customSettingsPath?: string ): string | undefined { const settingsPath = customSettingsPath - ? customSettingsPath.replace(/^~/, process.env.HOME || process.env.USERPROFILE || '') + ? customSettingsPath.replace(/^~/, os.homedir()) : getProviderSettingsPath(provider); if (!fs.existsSync(settingsPath)) return undefined; @@ -93,7 +94,7 @@ export async function configureProviderModel( // Use custom settings path for CLIProxy variants, otherwise use default provider path const settingsPath = customSettingsPath - ? customSettingsPath.replace(/^~/, process.env.HOME || process.env.USERPROFILE || '') + ? customSettingsPath.replace(/^~/, os.homedir()) : getProviderSettingsPath(provider); // Skip if already configured (unless --config flag) diff --git a/src/cliproxy/services/variant-service.ts b/src/cliproxy/services/variant-service.ts index 7911ab1e..58e4e05d 100644 --- a/src/cliproxy/services/variant-service.ts +++ b/src/cliproxy/services/variant-service.ts @@ -5,6 +5,7 @@ * Supports both unified config (config.yaml) and legacy JSON format. */ +import * as os from 'os'; import * as path from 'path'; import { CLIProxyProfileName } from '../../auth/profile-detector'; import { CLIProxyProvider } from '../types'; @@ -177,7 +178,7 @@ export function updateVariant(name: string, updates: UpdateVariantOptions): Vari // Update model in settings file if provided if (updates.model !== undefined && existing.settings) { - const settingsPath = existing.settings.replace(/^~/, process.env.HOME || ''); + const settingsPath = existing.settings.replace(/^~/, os.homedir()); updateSettingsModel(settingsPath, updates.model); } diff --git a/src/management/checks/profile-check.ts b/src/management/checks/profile-check.ts index cc449042..7d80a4df 100644 --- a/src/management/checks/profile-check.ts +++ b/src/management/checks/profile-check.ts @@ -11,7 +11,7 @@ import { HealthCheck, IHealthChecker, createSpinner } from './types'; const ora = createSpinner(); /** - * Check profile configurations in config.json + * Check profile configurations in config.yaml (preferred) or config.json (legacy) */ export class ProfilesChecker implements IHealthChecker { name = 'Profiles'; @@ -23,47 +23,102 @@ export class ProfilesChecker implements IHealthChecker { run(results: HealthCheck): void { const spinner = ora('Checking profiles').start(); - const configPath = path.join(this.ccsDir, 'config.json'); + const configYamlPath = path.join(this.ccsDir, 'config.yaml'); + const configJsonPath = path.join(this.ccsDir, 'config.json'); - if (!fs.existsSync(configPath)) { - spinner.info(); - console.log(` ${info('Profiles'.padEnd(22))} config.json not found`); - return; - } + const yamlExists = fs.existsSync(configYamlPath); + const jsonExists = fs.existsSync(configJsonPath); - try { - const config = JSON.parse(fs.readFileSync(configPath, 'utf8')); - - if (!config.profiles || typeof config.profiles !== 'object') { + // Check config.yaml first (preferred format) + if (yamlExists) { + try { + const yaml = require('js-yaml'); + const content = fs.readFileSync(configYamlPath, 'utf8'); + const config = yaml.load(content) as Record; + this.validateProfiles(config, 'config.yaml', spinner, results); + return; + } catch (e) { spinner.fail(); - console.log(` ${fail('Profiles'.padEnd(22))} Missing profiles object`); + console.log( + ` ${fail('Profiles'.padEnd(22))} Invalid config.yaml: ${(e as Error).message}` + ); results.addCheck( 'Profiles', 'error', - 'config.json missing profiles object', - 'Run: npm install -g @kaitranntt/ccs --force', - { status: 'ERROR', info: 'Missing profiles object' } + `Invalid config.yaml: ${(e as Error).message}`, + undefined, + { + status: 'ERROR', + info: (e as Error).message, + } ); return; } - - const profileCount = Object.keys(config.profiles).length; - const profileNames = Object.keys(config.profiles).join(', '); - - spinner.succeed(); - console.log(` ${ok('Profiles'.padEnd(22))} ${profileCount} configured (${profileNames})`); - results.addCheck('Profiles', 'success', `${profileCount} profiles configured`, undefined, { - status: 'OK', - info: `${profileCount} configured (${profileNames.length > 30 ? profileNames.substring(0, 27) + '...' : profileNames})`, - }); - } catch (e) { - spinner.fail(); - console.log(` ${fail('Profiles'.padEnd(22))} ${(e as Error).message}`); - results.addCheck('Profiles', 'error', (e as Error).message, undefined, { - status: 'ERROR', - info: (e as Error).message, - }); } + + // Fallback to config.json (legacy format) + if (jsonExists) { + try { + const config = JSON.parse(fs.readFileSync(configJsonPath, 'utf8')); + this.validateProfiles(config, 'config.json', spinner, results); + return; + } catch (e) { + spinner.fail(); + console.log( + ` ${fail('Profiles'.padEnd(22))} Invalid config.json: ${(e as Error).message}` + ); + results.addCheck( + 'Profiles', + 'error', + `Invalid config.json: ${(e as Error).message}`, + undefined, + { + status: 'ERROR', + info: (e as Error).message, + } + ); + return; + } + } + + // Neither exists + spinner.info(); + console.log( + ` ${info('Profiles'.padEnd(22))} No config file found (config.yaml or config.json)` + ); + } + + /** + * Validate profiles object from parsed config + */ + private validateProfiles( + config: Record, + configFileName: string, + spinner: ReturnType['start']>, + results: HealthCheck + ): void { + if (!config.profiles || typeof config.profiles !== 'object') { + spinner.fail(); + console.log(` ${fail('Profiles'.padEnd(22))} Missing profiles object in ${configFileName}`); + results.addCheck( + 'Profiles', + 'error', + `${configFileName} missing profiles object`, + 'Run: npm install -g @kaitranntt/ccs --force', + { status: 'ERROR', info: 'Missing profiles object' } + ); + return; + } + + const profileCount = Object.keys(config.profiles as object).length; + const profileNames = Object.keys(config.profiles as object).join(', '); + + spinner.succeed(); + console.log(` ${ok('Profiles'.padEnd(22))} ${profileCount} configured (${profileNames})`); + results.addCheck('Profiles', 'success', `${profileCount} profiles configured`, undefined, { + status: 'OK', + info: `${profileCount} configured (${profileNames.length > 30 ? profileNames.substring(0, 27) + '...' : profileNames})`, + }); } } diff --git a/src/management/recovery-manager.ts b/src/management/recovery-manager.ts index ec2169a2..8977870c 100644 --- a/src/management/recovery-manager.ts +++ b/src/management/recovery-manager.ts @@ -153,89 +153,6 @@ class RecoveryManager { return created; } - /** - * Ensure GLM settings file exists - */ - ensureGlmSettings(): boolean { - const settingsPath = path.join(this.ccsDir, 'glm.settings.json'); - if (fs.existsSync(settingsPath)) return false; - - const settings = { - env: { - ANTHROPIC_BASE_URL: 'https://api.z.ai/api/anthropic', - ANTHROPIC_AUTH_TOKEN: 'YOUR_GLM_API_KEY_HERE', - ANTHROPIC_MODEL: 'glm-4.6', - ANTHROPIC_DEFAULT_OPUS_MODEL: 'glm-4.6', - ANTHROPIC_DEFAULT_SONNET_MODEL: 'glm-4.6', - ANTHROPIC_DEFAULT_HAIKU_MODEL: 'glm-4.6', - }, - }; - - const tmpPath = `${settingsPath}.tmp`; - fs.writeFileSync(tmpPath, JSON.stringify(settings, null, 2) + '\n', 'utf8'); - fs.renameSync(tmpPath, settingsPath); - this.recovered.push('Created ~/.ccs/glm.settings.json'); - return true; - } - - /** - * Ensure GLMT settings file exists - */ - ensureGlmtSettings(): boolean { - const settingsPath = path.join(this.ccsDir, 'glmt.settings.json'); - if (fs.existsSync(settingsPath)) return false; - - const settings = { - env: { - ANTHROPIC_BASE_URL: 'https://api.z.ai/api/coding/paas/v4/chat/completions', - ANTHROPIC_AUTH_TOKEN: 'YOUR_GLM_API_KEY_HERE', - ANTHROPIC_MODEL: 'glm-4.6', - ANTHROPIC_DEFAULT_OPUS_MODEL: 'glm-4.6', - ANTHROPIC_DEFAULT_SONNET_MODEL: 'glm-4.6', - ANTHROPIC_DEFAULT_HAIKU_MODEL: 'glm-4.6', - ANTHROPIC_TEMPERATURE: '0.2', - ANTHROPIC_MAX_TOKENS: '65536', - MAX_THINKING_TOKENS: '32768', - ENABLE_STREAMING: 'true', - ANTHROPIC_SAFE_MODE: 'false', - API_TIMEOUT_MS: '3000000', - }, - alwaysThinkingEnabled: true, - }; - - const tmpPath = `${settingsPath}.tmp`; - fs.writeFileSync(tmpPath, JSON.stringify(settings, null, 2) + '\n', 'utf8'); - fs.renameSync(tmpPath, settingsPath); - this.recovered.push('Created ~/.ccs/glmt.settings.json'); - return true; - } - - /** - * Ensure Kimi settings file exists - */ - ensureKimiSettings(): boolean { - const settingsPath = path.join(this.ccsDir, 'kimi.settings.json'); - if (fs.existsSync(settingsPath)) return false; - - const settings = { - env: { - ANTHROPIC_BASE_URL: 'https://api.kimi.com/coding/', - ANTHROPIC_AUTH_TOKEN: 'YOUR_KIMI_API_KEY_HERE', - ANTHROPIC_MODEL: 'kimi-k2-thinking-turbo', - ANTHROPIC_DEFAULT_OPUS_MODEL: 'kimi-k2-thinking-turbo', - ANTHROPIC_DEFAULT_SONNET_MODEL: 'kimi-k2-thinking-turbo', - ANTHROPIC_DEFAULT_HAIKU_MODEL: 'kimi-k2-thinking-turbo', - }, - alwaysThinkingEnabled: true, - }; - - const tmpPath = `${settingsPath}.tmp`; - fs.writeFileSync(tmpPath, JSON.stringify(settings, null, 2) + '\n', 'utf8'); - fs.renameSync(tmpPath, settingsPath); - this.recovered.push('Created ~/.ccs/kimi.settings.json'); - return true; - } - /** * Install shell completion files */ @@ -327,23 +244,6 @@ class RecoveryManager { console.log(info('Auto-recovery completed:')); this.recovered.forEach((msg) => console.log(` - ${msg}`)); - // Show API key hints if created profile settings - const createdGlm = this.recovered.some((msg) => msg.includes('glm.settings.json')); - const createdKimi = this.recovered.some((msg) => msg.includes('kimi.settings.json')); - - if (createdGlm || createdKimi) { - console.log(''); - console.log(info('Configure API keys:')); - if (createdGlm) { - console.log(' GLM: Edit ~/.ccs/glm.settings.json'); - console.log(' Get key from: https://api.z.ai'); - } - if (createdKimi) { - console.log(' Kimi: Edit ~/.ccs/kimi.settings.json'); - console.log(' Get key from: https://www.kimi.com/coding'); - } - } - // Show login hint if created Claude settings if (this.recovered.some((msg) => msg.includes('~/.claude/settings.json'))) { console.log(''); diff --git a/src/web-server/health/config-checks.ts b/src/web-server/health/config-checks.ts index 45c0c2c8..5f7ca34a 100644 --- a/src/web-server/health/config-checks.ts +++ b/src/web-server/health/config-checks.ts @@ -6,6 +6,7 @@ */ import * as fs from 'fs'; +import * as os from 'os'; import * as path from 'path'; import { getConfigPath } from '../../utils/config-manager'; import { isUnifiedMode, hasUnifiedConfig } from '../../config/unified-config-loader'; @@ -17,7 +18,7 @@ import type { HealthCheck } from './types'; export function checkConfigFile(): HealthCheck { // In unified mode, check config.yaml if (isUnifiedMode() || hasUnifiedConfig()) { - const ccsDir = path.join(process.env.HOME || '', '.ccs'); + const ccsDir = path.join(os.homedir(), '.ccs'); const yamlPath = path.join(ccsDir, 'config.yaml'); if (!fs.existsSync(yamlPath)) { diff --git a/src/web-server/routes/settings-routes.ts b/src/web-server/routes/settings-routes.ts index aadfaed2..12e175aa 100644 --- a/src/web-server/routes/settings-routes.ts +++ b/src/web-server/routes/settings-routes.ts @@ -4,6 +4,7 @@ import { Router, Request, Response } from 'express'; import * as fs from 'fs'; +import * as os from 'os'; import * as path from 'path'; import { getCcsDir, loadSettings } from '../../utils/config-manager'; import { isSensitiveKey, maskSensitiveValue } from '../../utils/sensitive-keys'; @@ -33,7 +34,7 @@ function resolveSettingsPath(profileOrVariant: string): string { const variant = variants[profileOrVariant]; if (variant?.settings) { // Variant settings path (e.g., ~/.ccs/agy-g3.settings.json) - return variant.settings.replace(/^~/, process.env.HOME || ''); + return variant.settings.replace(/^~/, os.homedir()); } // Regular profile settings