fix: run RecoveryManager before early-exit commands and improve config handling

Fixes #214 - Fresh install fails with 'Config not found' when running
ccs config before RecoveryManager runs.

Changes:
- Move RecoveryManager.recoverAll() before all early-exit commands in ccs.ts
- Fix loadConfigSafe() to return empty config instead of throwing in legacy mode
- Add getActiveConfigPath() for mode-aware config path resolution
- Rename cliproxy getConfigPath() to getCliproxyConfigPath() to avoid confusion
- Update help-command.ts to show correct config path based on mode

This ensures all commands benefit from auto-recovery on fresh installs,
and gracefully handles missing config files in all code paths.
This commit is contained in:
kaitranntt
2025-12-27 00:05:31 -05:00
parent 8833a5a77f
commit 0be3977845
9 changed files with 45 additions and 26 deletions
+12 -10
View File
@@ -291,6 +291,18 @@ async function main(): Promise<void> {
await autoMigrate(); await autoMigrate();
} }
// Auto-recovery for missing configuration (BEFORE any early-exit commands)
// This ensures ALL commands benefit from auto-recovery, not just profile-switching flow
// Recovery is safe to run early - it only creates missing files with safe defaults
const RecoveryManagerModule = await import('./management/recovery-manager');
const RecoveryManager = RecoveryManagerModule.default;
const recovery = new RecoveryManager();
const recovered = recovery.recoverAll();
if (recovered) {
recovery.showRecoveryHints();
}
// Special case: version command (check BEFORE profile detection) // Special case: version command (check BEFORE profile detection)
if (firstArg === 'version' || firstArg === '--version' || firstArg === '-v') { if (firstArg === 'version' || firstArg === '--version' || firstArg === '-v') {
handleVersionCommand(); handleVersionCommand();
@@ -455,16 +467,6 @@ async function main(): Promise<void> {
return; return;
} }
// Auto-recovery for missing configuration
const RecoveryManagerModule = await import('./management/recovery-manager');
const RecoveryManager = RecoveryManagerModule.default;
const recovery = new RecoveryManager();
const recovered = recovery.recoverAll();
if (recovered) {
recovery.showRecoveryHints();
}
// First-time install: offer setup wizard for interactive users // First-time install: offer setup wizard for interactive users
// Check independently of recovery status (user may have empty config.yaml) // Check independently of recovery status (user may have empty config.yaml)
// Skip if headless, CI, or non-TTY environment // Skip if headless, CI, or non-TTY environment
+4 -3
View File
@@ -129,9 +129,10 @@ export function getConfigPathForPort(port: number): string {
} }
/** /**
* Get config file path (default port) * Get CLIProxy config file path (default port)
* Named distinctly from config-manager's getConfigPath to avoid confusion.
*/ */
export function getConfigPath(): string { export function getCliproxyConfigPath(): string {
return getConfigPathForPort(CLIPROXY_DEFAULT_PORT); return getConfigPathForPort(CLIPROXY_DEFAULT_PORT);
} }
@@ -377,7 +378,7 @@ export function regenerateConfig(port: number = CLIPROXY_DEFAULT_PORT): string {
* @returns true if config should be regenerated * @returns true if config should be regenerated
*/ */
export function configNeedsRegeneration(): boolean { export function configNeedsRegeneration(): boolean {
const configPath = getConfigPath(); const configPath = getCliproxyConfigPath();
if (!fs.existsSync(configPath)) { if (!fs.existsSync(configPath)) {
return false; // Will be created on first use return false; // Will be created on first use
} }
+1 -1
View File
@@ -65,7 +65,7 @@ export {
getCliproxyDir, getCliproxyDir,
getProviderAuthDir, getProviderAuthDir,
getAuthDir, getAuthDir,
getConfigPath, getCliproxyConfigPath,
getBinDir, getBinDir,
configExists, configExists,
deleteConfig, deleteConfig,
+3 -3
View File
@@ -7,7 +7,7 @@
import * as fs from 'fs'; import * as fs from 'fs';
import * as yaml from 'js-yaml'; import * as yaml from 'js-yaml';
import { getConfigPath } from './config-generator'; import { getCliproxyConfigPath } from './config-generator';
/** Model alias configuration */ /** Model alias configuration */
export interface OpenAICompatModel { export interface OpenAICompatModel {
@@ -48,7 +48,7 @@ interface ConfigYaml {
* Load current config.yaml * Load current config.yaml
*/ */
function loadConfig(): ConfigYaml { function loadConfig(): ConfigYaml {
const configPath = getConfigPath(); const configPath = getCliproxyConfigPath();
if (!fs.existsSync(configPath)) { if (!fs.existsSync(configPath)) {
return {}; return {};
} }
@@ -65,7 +65,7 @@ function loadConfig(): ConfigYaml {
* Save config.yaml with proper formatting * Save config.yaml with proper formatting
*/ */
function saveConfig(config: ConfigYaml): void { function saveConfig(config: ConfigYaml): void {
const configPath = getConfigPath(); const configPath = getCliproxyConfigPath();
const content = yaml.dump(config, { const content = yaml.dump(config, {
lineWidth: -1, // Disable line wrapping lineWidth: -1, // Disable line wrapping
quotingType: '"', quotingType: '"',
+2 -1
View File
@@ -1,6 +1,7 @@
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import { initUI, box, color, dim, sectionHeader, subheader } from '../utils/ui'; import { initUI, box, color, dim, sectionHeader, subheader } from '../utils/ui';
import { isUnifiedMode } from '../config/unified-config-loader';
// Get version from package.json (same as version-command.ts) // Get version from package.json (same as version-command.ts)
const VERSION = JSON.parse( const VERSION = JSON.parse(
@@ -234,7 +235,7 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim();
// Configuration // Configuration
printConfigSection('Configuration', [ printConfigSection('Configuration', [
['Config File:', '~/.ccs/config.json'], ['Config File:', isUnifiedMode() ? '~/.ccs/config.yaml' : '~/.ccs/config.json'],
['Profiles:', '~/.ccs/profiles.json'], ['Profiles:', '~/.ccs/profiles.json'],
['Instances:', '~/.ccs/instances/'], ['Instances:', '~/.ccs/instances/'],
['Settings:', '~/.ccs/*.settings.json'], ['Settings:', '~/.ccs/*.settings.json'],
+2 -2
View File
@@ -8,7 +8,7 @@ import {
isCLIProxyInstalled, isCLIProxyInstalled,
getCLIProxyPath, getCLIProxyPath,
getAllAuthStatus, getAllAuthStatus,
getConfigPath, getCliproxyConfigPath,
getInstalledCliproxyVersion, getInstalledCliproxyVersion,
CLIPROXY_DEFAULT_PORT, CLIPROXY_DEFAULT_PORT,
configNeedsRegeneration, configNeedsRegeneration,
@@ -60,7 +60,7 @@ export class CLIProxyConfigChecker implements IHealthChecker {
run(results: HealthCheck): void { run(results: HealthCheck): void {
const spinner = ora('Checking CLIProxy config').start(); const spinner = ora('Checking CLIProxy config').start();
const configPath = getConfigPath(); const configPath = getCliproxyConfigPath();
if (fs.existsSync(configPath)) { if (fs.existsSync(configPath)) {
// Check if config needs regeneration (version mismatch or missing features) // Check if config needs regeneration (version mismatch or missing features)
+17 -2
View File
@@ -27,12 +27,26 @@ export function getCcsDir(): string {
} }
/** /**
* Get config file path * Get config file path (legacy JSON path)
* @deprecated Use getActiveConfigPath() for mode-aware config path
*/ */
export function getConfigPath(): string { export function getConfigPath(): string {
return process.env.CCS_CONFIG || path.join(getCcsHome(), '.ccs', 'config.json'); return process.env.CCS_CONFIG || path.join(getCcsHome(), '.ccs', 'config.json');
} }
/**
* Get the active config file path based on current mode.
* Returns config.yaml in unified mode, config.json in legacy mode.
* @returns Path to the active config file
*/
export function getActiveConfigPath(): string {
const ccsDir = getCcsDir();
if (isUnifiedMode()) {
return path.join(ccsDir, 'config.yaml');
}
return path.join(ccsDir, 'config.json');
}
/** /**
* Load and validate config.json * Load and validate config.json
*/ */
@@ -126,7 +140,8 @@ export function loadConfigSafe(): Config {
const configPath = getConfigPath(); const configPath = getConfigPath();
if (!fs.existsSync(configPath)) { if (!fs.existsSync(configPath)) {
throw new Error(`Config not found: ${configPath}`); // Return empty config for graceful degradation (matches unified mode behavior)
return { profiles: {} };
} }
const raw = fs.readFileSync(configPath, 'utf8'); const raw = fs.readFileSync(configPath, 'utf8');
+1 -1
View File
@@ -9,7 +9,7 @@ import {
isCLIProxyInstalled, isCLIProxyInstalled,
getInstalledCliproxyVersion, getInstalledCliproxyVersion,
getCLIProxyPath, getCLIProxyPath,
getConfigPath as getCliproxyConfigPath, getCliproxyConfigPath,
getAllAuthStatus, getAllAuthStatus,
CLIPROXY_DEFAULT_PORT, CLIPROXY_DEFAULT_PORT,
} from '../../cliproxy'; } from '../../cliproxy';
@@ -14,7 +14,7 @@ import {
} from '../../cliproxy/stats-fetcher'; } from '../../cliproxy/stats-fetcher';
import { import {
getCliproxyWritablePath, getCliproxyWritablePath,
getConfigPath, getCliproxyConfigPath,
getAuthDir, getAuthDir,
} from '../../cliproxy/config-generator'; } from '../../cliproxy/config-generator';
import { getProxyStatus as getProxyProcessStatus, stopProxy } from '../../cliproxy/session-tracker'; import { getProxyStatus as getProxyProcessStatus, stopProxy } from '../../cliproxy/session-tracker';
@@ -272,7 +272,7 @@ router.get('/error-logs/:name', async (req: Request, res: Response): Promise<voi
*/ */
router.get('/config.yaml', async (_req: Request, res: Response): Promise<void> => { router.get('/config.yaml', async (_req: Request, res: Response): Promise<void> => {
try { try {
const configPath = getConfigPath(); const configPath = getCliproxyConfigPath();
if (!fs.existsSync(configPath)) { if (!fs.existsSync(configPath)) {
res.status(404).json({ error: 'Config file not found' }); res.status(404).json({ error: 'Config file not found' });
return; return;
@@ -299,7 +299,7 @@ router.put('/config.yaml', async (req: Request, res: Response): Promise<void> =>
return; return;
} }
const configPath = getConfigPath(); const configPath = getCliproxyConfigPath();
// Ensure parent directory exists // Ensure parent directory exists
const configDir = path.dirname(configPath); const configDir = path.dirname(configPath);