refactor(config): DRY precedence logic, dynamic recovery messages, CCS_HOME cloud warning

- Extract shared _resolveCcsDir() to prevent getCcsDir/getCcsDirSource divergence
- Replace all hardcoded ~/.ccs/ and ~/.claude/ in recovery-manager.ts with
  dynamic paths from instance properties (ccsDir, claudeDir, sharedDir, etc.)
- Add cloud sync detection for CCS_HOME env var (parity with CCS_DIR/--config-dir)
- Cache getSessionSecretPath() in local var in auth-middleware.ts
- Cache getCacheDir() in local var in disk-cache.ts ensureCacheDir()
This commit is contained in:
Tam Nhu Tran
2026-02-11 11:52:30 +07:00
parent c8800b418a
commit 9699e01725
5 changed files with 41 additions and 24 deletions
+8
View File
@@ -318,6 +318,14 @@ async function main(): Promise<void> {
console.error(warn(`CCS directory is under ${cloudService}.`));
console.error(' OAuth tokens in cliproxy/auth/ will be synced to cloud.');
}
} else if (process.env.CCS_HOME) {
// Also warn for CCS_HOME env var pointing to cloud sync
const { detectCloudSyncPath } = await import('./utils/config-manager');
const cloudService = detectCloudSyncPath(process.env.CCS_HOME);
if (cloudService) {
console.error(warn(`CCS directory is under ${cloudService}.`));
console.error(' OAuth tokens in cliproxy/auth/ will be synced to cloud.');
}
}
const firstArg = args[0];
+9 -9
View File
@@ -41,7 +41,7 @@ class RecoveryManager {
ensureCcsDirectory(): boolean {
if (!fs.existsSync(this.ccsDir)) {
fs.mkdirSync(this.ccsDir, { recursive: true, mode: 0o755 });
this.recovered.push('Created ~/.ccs/ directory');
this.recovered.push(`Created ${this.ccsDir} directory`);
return true;
}
return false;
@@ -60,7 +60,7 @@ class RecoveryManager {
return false; // Config exists and is valid
}
// Config exists but is corrupted - will be recreated below
this.recovered.push('Detected corrupted ~/.ccs/config.yaml');
this.recovered.push(`Detected corrupted ${this.ccsDir}/config.yaml`);
}
// Check for legacy config.json - if exists, let autoMigrate handle it
@@ -76,7 +76,7 @@ class RecoveryManager {
try {
saveUnifiedConfig(config);
this.recovered.push('Created ~/.ccs/config.yaml');
this.recovered.push(`Created ${this.ccsDir}/config.yaml`);
return true;
} catch (_saveErr) {
// Fallback: create minimal config.json for backward compat
@@ -85,7 +85,7 @@ class RecoveryManager {
const tmpPath = `${legacyConfigPath}.tmp`;
fs.writeFileSync(tmpPath, JSON.stringify(fallbackConfig, null, 2) + '\n', 'utf8');
fs.renameSync(tmpPath, legacyConfigPath);
this.recovered.push('Created ~/.ccs/config.json (fallback)');
this.recovered.push(`Created ${this.ccsDir}/config.json (fallback)`);
return true;
} catch (_fallbackErr) {
// Both writes failed - log but don't crash
@@ -104,7 +104,7 @@ class RecoveryManager {
// Create ~/.claude/ if missing
if (!fs.existsSync(this.claudeDir)) {
fs.mkdirSync(this.claudeDir, { recursive: true, mode: 0o755 });
this.recovered.push('Created ~/.claude/ directory');
this.recovered.push(`Created ${this.claudeDir} directory`);
}
// Create settings.json if missing
@@ -113,7 +113,7 @@ class RecoveryManager {
fs.writeFileSync(tmpPath, '{}\n', 'utf8');
fs.renameSync(tmpPath, claudeSettingsPath);
this.recovered.push('Created ~/.claude/settings.json');
this.recovered.push(`Created ${claudeSettingsPath}`);
return true;
}
@@ -129,7 +129,7 @@ class RecoveryManager {
// Create shared directory
if (!fs.existsSync(this.sharedDir)) {
fs.mkdirSync(this.sharedDir, { recursive: true, mode: 0o755 });
this.recovered.push('Created ~/.ccs/shared/');
this.recovered.push(`Created ${this.sharedDir}`);
created = true;
}
@@ -190,7 +190,7 @@ class RecoveryManager {
}
if (installed) {
this.recovered.push('Installed shell completions to ~/.ccs/completions/');
this.recovered.push(`Installed shell completions to ${this.completionsDir}`);
}
return installed;
@@ -238,7 +238,7 @@ class RecoveryManager {
this.recovered.forEach((msg) => console.log(` - ${msg}`));
// Show login hint if created Claude settings
if (this.recovered.some((msg) => msg.includes('~/.claude/settings.json'))) {
if (this.recovered.some((msg) => msg.includes('settings.json') && msg.includes('.claude'))) {
console.log('');
console.log(info('Next step: Login to Claude CLI'));
console.log(' Run: claude /login');
+15 -9
View File
@@ -30,16 +30,25 @@ export function getCcsHome(): string {
return process.env.CCS_HOME || os.homedir();
}
/**
* Resolve the CCS directory with source information.
* Single source of truth for precedence logic.
*/
function _resolveCcsDir(): { source: string; dir: string } {
if (_globalConfigDir) return { source: '--config-dir', dir: _globalConfigDir };
if (process.env.CCS_DIR) return { source: 'CCS_DIR', dir: path.resolve(process.env.CCS_DIR) };
if (process.env.CCS_HOME)
return { source: 'CCS_HOME', dir: path.join(path.resolve(process.env.CCS_HOME), '.ccs') };
return { source: 'default', dir: path.join(os.homedir(), '.ccs') };
}
/**
* Get the CCS directory path.
* Precedence: --config-dir flag > CCS_DIR env > CCS_HOME env (legacy, appends .ccs) > ~/.ccs default
* @returns Path to CCS config directory
*/
export function getCcsDir(): string {
if (_globalConfigDir) return _globalConfigDir;
if (process.env.CCS_DIR) return path.resolve(process.env.CCS_DIR);
if (process.env.CCS_HOME) return path.join(path.resolve(process.env.CCS_HOME), '.ccs');
return path.join(os.homedir(), '.ccs');
return _resolveCcsDir().dir;
}
/**
@@ -47,11 +56,8 @@ export function getCcsDir(): string {
* @returns [source_label, resolved_path] tuple
*/
export function getCcsDirSource(): [string, string] {
if (_globalConfigDir) return ['--config-dir', _globalConfigDir];
if (process.env.CCS_DIR) return ['CCS_DIR', path.resolve(process.env.CCS_DIR)];
if (process.env.CCS_HOME)
return ['CCS_HOME', path.join(path.resolve(process.env.CCS_HOME), '.ccs')];
return ['default', path.join(os.homedir(), '.ccs')];
const r = _resolveCcsDir();
return [r.source, r.dir];
}
/**
+6 -4
View File
@@ -38,10 +38,12 @@ function getSessionSecret(): string {
return process.env.CCS_SESSION_SECRET;
}
const secretPath = getSessionSecretPath();
// 2. Try to read persisted secret
try {
if (fs.existsSync(getSessionSecretPath())) {
const secret = fs.readFileSync(getSessionSecretPath(), 'utf-8').trim();
if (fs.existsSync(secretPath)) {
const secret = fs.readFileSync(secretPath, 'utf-8').trim();
if (secret.length >= 32) {
return secret;
}
@@ -53,11 +55,11 @@ function getSessionSecret(): string {
// 3. Generate and persist new random secret
const newSecret = crypto.randomBytes(32).toString('hex');
try {
const dir = path.dirname(getSessionSecretPath());
const dir = path.dirname(secretPath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(getSessionSecretPath(), newSecret, { mode: 0o600 });
fs.writeFileSync(secretPath, newSecret, { mode: 0o600 });
} catch (err) {
// Log warning - sessions won't persist across restarts
console.warn('[!] Failed to persist session secret:', (err as Error).message);
+3 -2
View File
@@ -42,8 +42,9 @@ const CACHE_VERSION = 3;
* Ensure ~/.ccs/cache directory exists
*/
function ensureCacheDir(): void {
if (!fs.existsSync(getCacheDir())) {
fs.mkdirSync(getCacheDir(), { recursive: true });
const dir = getCacheDir();
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
}