mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-07 22:20:18 +00:00
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:
+12
-10
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export {
|
|||||||
getCliproxyDir,
|
getCliproxyDir,
|
||||||
getProviderAuthDir,
|
getProviderAuthDir,
|
||||||
getAuthDir,
|
getAuthDir,
|
||||||
getConfigPath,
|
getCliproxyConfigPath,
|
||||||
getBinDir,
|
getBinDir,
|
||||||
configExists,
|
configExists,
|
||||||
deleteConfig,
|
deleteConfig,
|
||||||
|
|||||||
@@ -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: '"',
|
||||||
|
|||||||
@@ -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'],
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user