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();
}
// 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)
if (firstArg === 'version' || firstArg === '--version' || firstArg === '-v') {
handleVersionCommand();
@@ -455,16 +467,6 @@ async function main(): Promise<void> {
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
// Check independently of recovery status (user may have empty config.yaml)
// 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);
}
@@ -377,7 +378,7 @@ export function regenerateConfig(port: number = CLIPROXY_DEFAULT_PORT): string {
* @returns true if config should be regenerated
*/
export function configNeedsRegeneration(): boolean {
const configPath = getConfigPath();
const configPath = getCliproxyConfigPath();
if (!fs.existsSync(configPath)) {
return false; // Will be created on first use
}
+1 -1
View File
@@ -65,7 +65,7 @@ export {
getCliproxyDir,
getProviderAuthDir,
getAuthDir,
getConfigPath,
getCliproxyConfigPath,
getBinDir,
configExists,
deleteConfig,
+3 -3
View File
@@ -7,7 +7,7 @@
import * as fs from 'fs';
import * as yaml from 'js-yaml';
import { getConfigPath } from './config-generator';
import { getCliproxyConfigPath } from './config-generator';
/** Model alias configuration */
export interface OpenAICompatModel {
@@ -48,7 +48,7 @@ interface ConfigYaml {
* Load current config.yaml
*/
function loadConfig(): ConfigYaml {
const configPath = getConfigPath();
const configPath = getCliproxyConfigPath();
if (!fs.existsSync(configPath)) {
return {};
}
@@ -65,7 +65,7 @@ function loadConfig(): ConfigYaml {
* Save config.yaml with proper formatting
*/
function saveConfig(config: ConfigYaml): void {
const configPath = getConfigPath();
const configPath = getCliproxyConfigPath();
const content = yaml.dump(config, {
lineWidth: -1, // Disable line wrapping
quotingType: '"',
+2 -1
View File
@@ -1,6 +1,7 @@
import * as fs from 'fs';
import * as path from 'path';
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)
const VERSION = JSON.parse(
@@ -234,7 +235,7 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim();
// Configuration
printConfigSection('Configuration', [
['Config File:', '~/.ccs/config.json'],
['Config File:', isUnifiedMode() ? '~/.ccs/config.yaml' : '~/.ccs/config.json'],
['Profiles:', '~/.ccs/profiles.json'],
['Instances:', '~/.ccs/instances/'],
['Settings:', '~/.ccs/*.settings.json'],
+2 -2
View File
@@ -8,7 +8,7 @@ import {
isCLIProxyInstalled,
getCLIProxyPath,
getAllAuthStatus,
getConfigPath,
getCliproxyConfigPath,
getInstalledCliproxyVersion,
CLIPROXY_DEFAULT_PORT,
configNeedsRegeneration,
@@ -60,7 +60,7 @@ export class CLIProxyConfigChecker implements IHealthChecker {
run(results: HealthCheck): void {
const spinner = ora('Checking CLIProxy config').start();
const configPath = getConfigPath();
const configPath = getCliproxyConfigPath();
if (fs.existsSync(configPath)) {
// 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 {
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
*/
@@ -126,7 +140,8 @@ export function loadConfigSafe(): Config {
const configPath = getConfigPath();
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');
+1 -1
View File
@@ -9,7 +9,7 @@ import {
isCLIProxyInstalled,
getInstalledCliproxyVersion,
getCLIProxyPath,
getConfigPath as getCliproxyConfigPath,
getCliproxyConfigPath,
getAllAuthStatus,
CLIPROXY_DEFAULT_PORT,
} from '../../cliproxy';
@@ -14,7 +14,7 @@ import {
} from '../../cliproxy/stats-fetcher';
import {
getCliproxyWritablePath,
getConfigPath,
getCliproxyConfigPath,
getAuthDir,
} from '../../cliproxy/config-generator';
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> => {
try {
const configPath = getConfigPath();
const configPath = getCliproxyConfigPath();
if (!fs.existsSync(configPath)) {
res.status(404).json({ error: 'Config file not found' });
return;
@@ -299,7 +299,7 @@ router.put('/config.yaml', async (req: Request, res: Response): Promise<void> =>
return;
}
const configPath = getConfigPath();
const configPath = getCliproxyConfigPath();
// Ensure parent directory exists
const configDir = path.dirname(configPath);