mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-03 18:16:29 +00:00
Merge pull request #215 from kaitranntt/fix/214-config-recovery-before-commands
fix: run RecoveryManager before early-exit commands and improve config handling
This commit is contained in:
+18
-10
@@ -291,6 +291,24 @@ 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
|
||||
// Wrapped in try-catch to prevent blocking --version/--help on permission errors
|
||||
try {
|
||||
const RecoveryManagerModule = await import('./management/recovery-manager');
|
||||
const RecoveryManager = RecoveryManagerModule.default;
|
||||
const recovery = new RecoveryManager();
|
||||
const recovered = recovery.recoverAll();
|
||||
|
||||
if (recovered) {
|
||||
recovery.showRecoveryHints();
|
||||
}
|
||||
} catch (err) {
|
||||
// Recovery is best-effort - don't block basic CLI functionality
|
||||
console.warn('[!] Recovery failed:', (err as Error).message);
|
||||
}
|
||||
|
||||
// Special case: version command (check BEFORE profile detection)
|
||||
if (firstArg === 'version' || firstArg === '--version' || firstArg === '-v') {
|
||||
handleVersionCommand();
|
||||
@@ -455,16 +473,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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ export {
|
||||
getCliproxyDir,
|
||||
getProviderAuthDir,
|
||||
getAuthDir,
|
||||
getConfigPath,
|
||||
getCliproxyConfigPath,
|
||||
getBinDir,
|
||||
configExists,
|
||||
deleteConfig,
|
||||
|
||||
@@ -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: '"',
|
||||
|
||||
@@ -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(
|
||||
@@ -235,7 +236,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'],
|
||||
|
||||
@@ -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)
|
||||
|
||||
+4
-4
@@ -17,10 +17,10 @@ export interface ProfilesConfig {
|
||||
* Example: "flash" → gemini provider with gemini-2.5-flash model
|
||||
*/
|
||||
export interface CLIProxyVariantConfig {
|
||||
/** CLIProxy provider to use (gemini, codex, agy, qwen) */
|
||||
provider: 'gemini' | 'codex' | 'agy' | 'qwen';
|
||||
/** Path to settings.json with custom model configuration */
|
||||
settings: string;
|
||||
/** CLIProxy provider to use */
|
||||
provider: 'gemini' | 'codex' | 'agy' | 'qwen' | 'iflow' | 'kiro' | 'ghcp';
|
||||
/** Path to settings.json with custom model configuration (optional) */
|
||||
settings?: string;
|
||||
/** Account identifier for multi-account support (optional, defaults to 'default') */
|
||||
account?: string;
|
||||
/** Unique port for variant isolation (8318-8417) */
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
@@ -107,9 +121,8 @@ export function loadConfigSafe(): Config {
|
||||
cliproxy = {};
|
||||
for (const [name, variant] of Object.entries(unifiedConfig.cliproxy.variants)) {
|
||||
cliproxy[name] = {
|
||||
// Cast provider - unified has more providers than legacy type
|
||||
provider: variant.provider as 'gemini' | 'codex' | 'agy' | 'qwen',
|
||||
settings: variant.settings || '',
|
||||
provider: variant.provider,
|
||||
settings: variant.settings,
|
||||
account: variant.account,
|
||||
port: variant.port,
|
||||
};
|
||||
@@ -126,11 +139,18 @@ 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');
|
||||
const parsed: unknown = JSON.parse(raw);
|
||||
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(raw);
|
||||
} catch (e) {
|
||||
throw new Error(`Malformed JSON in config: ${configPath} - ${(e as Error).message}`);
|
||||
}
|
||||
|
||||
if (!isConfig(parsed)) {
|
||||
throw new Error(`Invalid config format: ${configPath}`);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -19,7 +19,7 @@ process.env.CCS_HOME = testHome;
|
||||
|
||||
const {
|
||||
getConfigPathForPort,
|
||||
getConfigPath,
|
||||
getCliproxyConfigPath,
|
||||
generateConfig,
|
||||
regenerateConfig,
|
||||
configExists,
|
||||
@@ -98,9 +98,9 @@ describe('Config Generator Port', function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getConfigPath', function () {
|
||||
describe('getCliproxyConfigPath', function () {
|
||||
it('returns path for default port', function () {
|
||||
const configPath = getConfigPath();
|
||||
const configPath = getCliproxyConfigPath();
|
||||
const defaultPath = getConfigPathForPort(CLIPROXY_DEFAULT_PORT);
|
||||
assert.strictEqual(configPath, defaultPath);
|
||||
});
|
||||
|
||||
@@ -362,7 +362,7 @@ auth-dir: "/test"
|
||||
let testDir;
|
||||
let originalCcsHome;
|
||||
let regenerateConfig;
|
||||
let getConfigPath;
|
||||
let getCliproxyConfigPath;
|
||||
|
||||
beforeEach(() => {
|
||||
// Create a temporary test directory
|
||||
@@ -375,7 +375,7 @@ auth-dir: "/test"
|
||||
delete require.cache[require.resolve('../../../dist/utils/config-manager')];
|
||||
const configGenerator = require('../../../dist/cliproxy/config-generator');
|
||||
regenerateConfig = configGenerator.regenerateConfig;
|
||||
getConfigPath = configGenerator.getConfigPath;
|
||||
getCliproxyConfigPath = configGenerator.getCliproxyConfigPath;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
@@ -27,14 +27,7 @@ describe('Beta Channel Implementation (Phase 3)', function () {
|
||||
let httpsRequests = [];
|
||||
|
||||
beforeAll(async function () {
|
||||
// Build the project first
|
||||
const { execSync } = require('child_process');
|
||||
try {
|
||||
execSync('bun run build', { cwd: path.resolve(__dirname, '../../..'), stdio: 'pipe' });
|
||||
} catch (error) {
|
||||
console.warn('Build failed, tests may not work:', error.message);
|
||||
}
|
||||
|
||||
// Note: Build is handled by CI before tests run (bun run build:all)
|
||||
// Import the built module
|
||||
updateCheckerModule = await import('../../../dist/utils/update-checker.js');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user