mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-13 02:14:32 +00:00
fix(doctor): comprehensive health check fixes
- profile-check.ts: check config.yaml first, fallback to config.json - recovery-manager.ts: remove dead code (ensureGlm/Glmt/Kimi methods) - 4 files: use os.homedir() for cross-platform compatibility Fixes: - ProfilesChecker now respects unified config format (yaml) - Removed ~100 lines of dead code that contradicted install policy - Windows compatibility for home directory detection Files modified: - src/management/checks/profile-check.ts - src/management/recovery-manager.ts - src/cliproxy/model-config.ts - src/cliproxy/services/variant-service.ts - src/web-server/health/config-checks.ts - src/web-server/routes/settings-routes.ts
This commit is contained in:
1 parent
4fca7d16ed
commit
ac745503e2
6 files changed
+97
-138
No files matched your search
@@ -6,6 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
|
import * as os from 'os';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { InteractivePrompt } from '../utils/prompt';
|
import { InteractivePrompt } from '../utils/prompt';
|
||||||
import { getProviderCatalog, supportsModelConfig, ModelEntry } from './model-catalog';
|
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';
|
import { initUI, color, bold, dim, ok, info, header } from '../utils/ui';
|
||||||
|
|
||||||
/** CCS directory */
|
/** 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
|
* Check if provider has user settings configured
|
||||||
@@ -34,7 +35,7 @@ export function getCurrentModel(
|
|||||||
customSettingsPath?: string
|
customSettingsPath?: string
|
||||||
): string | undefined {
|
): string | undefined {
|
||||||
const settingsPath = customSettingsPath
|
const settingsPath = customSettingsPath
|
||||||
? customSettingsPath.replace(/^~/, process.env.HOME || process.env.USERPROFILE || '')
|
? customSettingsPath.replace(/^~/, os.homedir())
|
||||||
: getProviderSettingsPath(provider);
|
: getProviderSettingsPath(provider);
|
||||||
if (!fs.existsSync(settingsPath)) return undefined;
|
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
|
// Use custom settings path for CLIProxy variants, otherwise use default provider path
|
||||||
const settingsPath = customSettingsPath
|
const settingsPath = customSettingsPath
|
||||||
? customSettingsPath.replace(/^~/, process.env.HOME || process.env.USERPROFILE || '')
|
? customSettingsPath.replace(/^~/, os.homedir())
|
||||||
: getProviderSettingsPath(provider);
|
: getProviderSettingsPath(provider);
|
||||||
|
|
||||||
// Skip if already configured (unless --config flag)
|
// Skip if already configured (unless --config flag)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
* Supports both unified config (config.yaml) and legacy JSON format.
|
* Supports both unified config (config.yaml) and legacy JSON format.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import * as os from 'os';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { CLIProxyProfileName } from '../../auth/profile-detector';
|
import { CLIProxyProfileName } from '../../auth/profile-detector';
|
||||||
import { CLIProxyProvider } from '../types';
|
import { CLIProxyProvider } from '../types';
|
||||||
@@ -177,7 +178,7 @@ export function updateVariant(name: string, updates: UpdateVariantOptions): Vari
|
|||||||
|
|
||||||
// Update model in settings file if provided
|
// Update model in settings file if provided
|
||||||
if (updates.model !== undefined && existing.settings) {
|
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);
|
updateSettingsModel(settingsPath, updates.model);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { HealthCheck, IHealthChecker, createSpinner } from './types';
|
|||||||
const ora = createSpinner();
|
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 {
|
export class ProfilesChecker implements IHealthChecker {
|
||||||
name = 'Profiles';
|
name = 'Profiles';
|
||||||
@@ -23,47 +23,102 @@ export class ProfilesChecker implements IHealthChecker {
|
|||||||
|
|
||||||
run(results: HealthCheck): void {
|
run(results: HealthCheck): void {
|
||||||
const spinner = ora('Checking profiles').start();
|
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)) {
|
const yamlExists = fs.existsSync(configYamlPath);
|
||||||
spinner.info();
|
const jsonExists = fs.existsSync(configJsonPath);
|
||||||
console.log(` ${info('Profiles'.padEnd(22))} config.json not found`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
// Check config.yaml first (preferred format)
|
||||||
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
if (yamlExists) {
|
||||||
|
try {
|
||||||
if (!config.profiles || typeof config.profiles !== 'object') {
|
const yaml = require('js-yaml');
|
||||||
|
const content = fs.readFileSync(configYamlPath, 'utf8');
|
||||||
|
const config = yaml.load(content) as Record<string, unknown>;
|
||||||
|
this.validateProfiles(config, 'config.yaml', spinner, results);
|
||||||
|
return;
|
||||||
|
} catch (e) {
|
||||||
spinner.fail();
|
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(
|
results.addCheck(
|
||||||
'Profiles',
|
'Profiles',
|
||||||
'error',
|
'error',
|
||||||
'config.json missing profiles object',
|
`Invalid config.yaml: ${(e as Error).message}`,
|
||||||
'Run: npm install -g @kaitranntt/ccs --force',
|
undefined,
|
||||||
{ status: 'ERROR', info: 'Missing profiles object' }
|
{
|
||||||
|
status: 'ERROR',
|
||||||
|
info: (e as Error).message,
|
||||||
|
}
|
||||||
);
|
);
|
||||||
return;
|
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<string, unknown>,
|
||||||
|
configFileName: string,
|
||||||
|
spinner: ReturnType<ReturnType<typeof ora>['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})`,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -153,89 +153,6 @@ class RecoveryManager {
|
|||||||
return created;
|
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
|
* Install shell completion files
|
||||||
*/
|
*/
|
||||||
@@ -327,23 +244,6 @@ class RecoveryManager {
|
|||||||
console.log(info('Auto-recovery completed:'));
|
console.log(info('Auto-recovery completed:'));
|
||||||
this.recovered.forEach((msg) => console.log(` - ${msg}`));
|
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
|
// Show login hint if created Claude settings
|
||||||
if (this.recovered.some((msg) => msg.includes('~/.claude/settings.json'))) {
|
if (this.recovered.some((msg) => msg.includes('~/.claude/settings.json'))) {
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
|
import * as os from 'os';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { getConfigPath } from '../../utils/config-manager';
|
import { getConfigPath } from '../../utils/config-manager';
|
||||||
import { isUnifiedMode, hasUnifiedConfig } from '../../config/unified-config-loader';
|
import { isUnifiedMode, hasUnifiedConfig } from '../../config/unified-config-loader';
|
||||||
@@ -17,7 +18,7 @@ import type { HealthCheck } from './types';
|
|||||||
export function checkConfigFile(): HealthCheck {
|
export function checkConfigFile(): HealthCheck {
|
||||||
// In unified mode, check config.yaml
|
// In unified mode, check config.yaml
|
||||||
if (isUnifiedMode() || hasUnifiedConfig()) {
|
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');
|
const yamlPath = path.join(ccsDir, 'config.yaml');
|
||||||
|
|
||||||
if (!fs.existsSync(yamlPath)) {
|
if (!fs.existsSync(yamlPath)) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
import { Router, Request, Response } from 'express';
|
import { Router, Request, Response } from 'express';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
|
import * as os from 'os';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { getCcsDir, loadSettings } from '../../utils/config-manager';
|
import { getCcsDir, loadSettings } from '../../utils/config-manager';
|
||||||
import { isSensitiveKey, maskSensitiveValue } from '../../utils/sensitive-keys';
|
import { isSensitiveKey, maskSensitiveValue } from '../../utils/sensitive-keys';
|
||||||
@@ -33,7 +34,7 @@ function resolveSettingsPath(profileOrVariant: string): string {
|
|||||||
const variant = variants[profileOrVariant];
|
const variant = variants[profileOrVariant];
|
||||||
if (variant?.settings) {
|
if (variant?.settings) {
|
||||||
// Variant settings path (e.g., ~/.ccs/agy-g3.settings.json)
|
// 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
|
// Regular profile settings
|
||||||
|
|||||||
Reference in new issue
Block a user