fix(config): migrate all hardcoded paths to getCcsDir() and improve validation

- Replace os.homedir() + '.ccs' with getCcsDir() across 14 instances in 13 files:
  version-command, model-config, openrouter-catalog, config-checks, disk-cache,
  aggregator, auth-middleware, shell-completion, shared-manager, recovery-manager,
  auto-repair, delegation-validator, claude-dir-installer, cliproxy executor
- Add isDirectory() validation for --config-dir argument in ccs.ts
- Make detectCloudSyncPath() case-insensitive for cloud provider matching
- Fix help-command.ts to use dynamic dirDisplay for all path references
- Add 5 new tests: setGlobalConfigDir precedence, relative path resolution,
  reset behavior, getCcsDirSource with --config-dir, case-insensitive detection
- Clean up unused os imports after migration
This commit is contained in:
Tam Nhu Tran
2026-02-11 11:15:08 +07:00
parent 7a0e6a4112
commit 60d6bbd027
18 changed files with 87 additions and 59 deletions
+2 -2
View File
@@ -5,10 +5,10 @@
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { getCcsDir } from '../../utils/config-manager';
const OPENROUTER_API_URL = 'https://openrouter.ai/api/v1/models';
const CACHE_FILE = path.join(os.homedir(), '.ccs', 'openrouter-models-cache.json');
const CACHE_FILE = path.join(getCcsDir(), 'openrouter-models-cache.json');
const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
export interface OpenRouterModel {
+2 -2
View File
@@ -286,8 +286,8 @@ async function main(): Promise<void> {
process.exit(1);
}
if (!fs.existsSync(configDirValue)) {
console.error(fail(`Config directory not found: ${configDirValue}`));
if (!fs.existsSync(configDirValue) || !fs.statSync(configDirValue).isDirectory()) {
console.error(fail(`Config directory not found or not a directory: ${configDirValue}`));
console.error(info('Create the directory first, then copy your config files into it.'));
process.exit(1);
}
+3 -1
View File
@@ -13,8 +13,10 @@
import { spawn, ChildProcess } from 'child_process';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { ProgressIndicator } from '../../utils/progress-indicator';
import { ok, fail, info, warn } from '../../utils/ui';
import { getCcsDir } from '../../utils/config-manager';
import { escapeShellArg } from '../../utils/shell-executor';
import { ensureCLIProxyBinary } from '../binary-manager';
import {
@@ -655,7 +657,7 @@ export async function execClaudeWithCLIProxy(
upstreamBaseUrl: postSanitizationBaseUrl,
verbose,
defaultEffort: 'medium',
traceFilePath: traceEnabled ? `${os.homedir()}/.ccs/codex-reasoning-proxy.log` : '',
traceFilePath: traceEnabled ? path.join(getCcsDir(), 'codex-reasoning-proxy.log') : '',
modelMap: {
defaultModel: initialEnvVars.ANTHROPIC_MODEL,
opusModel: initialEnvVars.ANTHROPIC_DEFAULT_OPUS_MODEL,
+4 -6
View File
@@ -7,15 +7,12 @@
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { InteractivePrompt } from '../utils/prompt';
import { getProviderCatalog, supportsModelConfig, ModelEntry } from './model-catalog';
import { getProviderSettingsPath, getClaudeEnvVars } from './config-generator';
import { CLIProxyProvider } from './types';
import { initUI, color, bold, dim, ok, info, header } from '../utils/ui';
/** CCS directory */
const CCS_DIR = path.join(os.homedir(), '.ccs');
import { getCcsDir } from '../utils/config-manager';
/**
* Check if provider has user settings configured
@@ -185,8 +182,9 @@ export async function configureProviderModel(
};
// Ensure CCS directory exists
if (!fs.existsSync(CCS_DIR)) {
fs.mkdirSync(CCS_DIR, { recursive: true });
const ccsDir = getCcsDir();
if (!fs.existsSync(ccsDir)) {
fs.mkdirSync(ccsDir, { recursive: true });
}
// Write settings file
+5 -3
View File
@@ -111,6 +111,10 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim();
);
console.log('');
// Resolve display path for dynamic sections
const [dirSource] = getCcsDirSource();
const dirDisplay = dirSource === 'default' ? '~/.ccs' : getCcsDir();
// Usage section
console.log(subheader('Usage:'));
console.log(` ${color('ccs', 'command')} [profile] [claude-args...]`);
@@ -122,7 +126,7 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim();
// ═══════════════════════════════════════════════════════════════════════════
printMajorSection(
'API Key Profiles',
['Configure in ~/.ccs/*.settings.json'],
[`Configure in ${dirDisplay}/*.settings.json`],
[
['ccs', 'Use default Claude account'],
['ccs glm', 'GLM 4.6 (API key required)'],
@@ -267,8 +271,6 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim();
]);
// Configuration
const [dirSource] = getCcsDirSource();
const dirDisplay = dirSource === 'default' ? '~/.ccs' : getCcsDir();
printConfigSection('Configuration', [
['Config File:', isUnifiedMode() ? `${dirDisplay}/config.yaml` : `${dirDisplay}/config.json`],
['Profiles:', `${dirDisplay}/profiles.json`],
+5 -6
View File
@@ -6,9 +6,8 @@
import * as path from 'path';
import * as fs from 'fs';
import * as os from 'os';
import { initUI, header, subheader, color, warn } from '../utils/ui';
import { getActiveConfigPath } from '../utils/config-manager';
import { getActiveConfigPath, getCcsDir } from '../utils/config-manager';
import { getVersion } from '../utils/version';
/**
@@ -23,24 +22,24 @@ export async function handleVersionCommand(): Promise<void> {
const installLocation = process.argv[1] || '(not found)';
console.log(` ${color('Location:'.padEnd(17), 'info')} ${installLocation}`);
const ccsDir = path.join(os.homedir(), '.ccs');
const ccsDir = getCcsDir();
console.log(` ${color('CCS Directory:'.padEnd(17), 'info')} ${ccsDir}`);
const configPath = getActiveConfigPath();
console.log(` ${color('Config:'.padEnd(17), 'info')} ${configPath}`);
const profilesJson = path.join(os.homedir(), '.ccs', 'profiles.json');
const profilesJson = path.join(ccsDir, 'profiles.json');
console.log(` ${color('Profiles:'.padEnd(17), 'info')} ${profilesJson}`);
// Delegation status
const delegationSessionsPath = path.join(os.homedir(), '.ccs', 'delegation-sessions.json');
const delegationSessionsPath = path.join(ccsDir, 'delegation-sessions.json');
const delegationConfigured = fs.existsSync(delegationSessionsPath);
const readyProfiles: string[] = [];
// Check for profiles with valid API keys
for (const profile of ['glm', 'kimi']) {
const settingsPath = path.join(os.homedir(), '.ccs', `${profile}.settings.json`);
const settingsPath = path.join(ccsDir, `${profile}.settings.json`);
if (fs.existsSync(settingsPath)) {
try {
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
+2 -9
View File
@@ -6,8 +6,8 @@
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { info } from '../utils/ui';
import { getCcsHome, getCcsDir } from '../utils/config-manager';
import { createEmptyUnifiedConfig, UNIFIED_CONFIG_VERSION } from '../config/unified-config-types';
import {
saveUnifiedConfig,
@@ -15,13 +15,6 @@ import {
loadUnifiedConfig,
} from '../config/unified-config-loader';
/**
* Get CCS home directory (respects CCS_HOME env for test isolation)
*/
function getCcsHome(): string {
return process.env.CCS_HOME || os.homedir();
}
/**
* Recovery Manager Class
*/
@@ -35,7 +28,7 @@ class RecoveryManager {
constructor() {
this.homedir = getCcsHome();
this.ccsDir = path.join(this.homedir, '.ccs');
this.ccsDir = getCcsDir();
this.claudeDir = path.join(this.homedir, '.claude');
this.sharedDir = path.join(this.ccsDir, 'shared');
this.completionsDir = path.join(this.ccsDir, 'completions');
+2 -4
View File
@@ -4,8 +4,8 @@
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { ok, warn, fail, info, header, color } from '../../utils/ui';
import { getCcsDir } from '../../utils/config-manager';
import {
CLIPROXY_DEFAULT_PORT,
configNeedsRegeneration,
@@ -28,8 +28,6 @@ const ora = createSpinner();
* 4. OAuth callback ports blocked by CLIProxy
*/
export async function runAutoRepair(): Promise<void> {
const homedir = os.homedir();
console.log('');
console.log(header('AUTO-FIX MODE'));
console.log('');
@@ -111,7 +109,7 @@ export async function runAutoRepair(): Promise<void> {
// Fix 4: Fix shared symlinks (settings.json broken by Claude CLI toggle thinking, etc.)
const symlinkSpinner = ora('Checking shared settings.json symlink').start();
const sharedSettings = path.join(homedir, '.ccs', 'shared', 'settings.json');
const sharedSettings = path.join(getCcsDir(), 'shared', 'settings.json');
try {
if (fs.existsSync(sharedSettings)) {
const stats = fs.lstatSync(sharedSettings);
+1 -1
View File
@@ -62,7 +62,7 @@ class SharedManager {
const resolvedTarget = path.resolve(path.dirname(target), targetLink);
// Check if target points back to our shared dir or link path
const sharedDir = path.join(this.homeDir, '.ccs', 'shared');
const sharedDir = path.join(getCcsDir(), 'shared');
if (resolvedTarget.startsWith(sharedDir) || resolvedTarget === linkPath) {
console.log(warn(`Circular symlink detected: ${target}${resolvedTarget}`));
return true;
+1 -6
View File
@@ -197,12 +197,7 @@ export class ClaudeDirInstaller {
cleanupDeprecated(silent = false): CleanupResult {
const deprecatedFile = path.join(this.ccsClaudeDir, 'agents', 'ccs-delegator.md');
const userSymlinkFile = path.join(this.homeDir, '.claude', 'agents', 'ccs-delegator.md');
const migrationMarker = path.join(
this.homeDir,
'.ccs',
'.migrations',
'v435-delegator-cleanup'
);
const migrationMarker = path.join(getCcsDir(), '.migrations', 'v435-delegator-cleanup');
const cleanedFiles: string[] = [];
+3 -3
View File
@@ -72,10 +72,10 @@ const CLOUD_SYNC_PATTERNS = [
* @returns Detected service name or null
*/
export function detectCloudSyncPath(dir: string): string | null {
const normalized = dir.replace(/\\/g, '/');
const normalized = dir.replace(/\\/g, '/').toLowerCase();
for (const pattern of CLOUD_SYNC_PATTERNS) {
if (normalized.includes(pattern)) {
return pattern;
if (normalized.includes(pattern.toLowerCase())) {
return pattern; // Return original casing for display
}
}
return null;
+3 -5
View File
@@ -2,9 +2,9 @@
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { Settings } from '../types';
import { ValidationResult } from '../types/utils';
import { getCcsDir } from './config-manager';
/**
* Extended validation result for delegation profiles
@@ -28,8 +28,7 @@ export class DelegationValidator {
* @returns Validation result { valid: boolean, error?: string, settingsPath?: string }
*/
static validate(profileName: string): DelegationValidationResult {
const homeDir = os.homedir();
const settingsPath = path.join(homeDir, '.ccs', `${profileName}.settings.json`);
const settingsPath = path.join(getCcsDir(), `${profileName}.settings.json`);
// Check if profile directory exists
if (!fs.existsSync(settingsPath)) {
@@ -144,8 +143,7 @@ export class DelegationValidator {
* @returns List of profile names ready for delegation
*/
static getReadyProfiles(): string[] {
const homeDir = os.homedir();
const ccsDir = path.join(homeDir, '.ccs');
const ccsDir = getCcsDir();
const configPath = path.join(ccsDir, 'config.yaml');
if (!fs.existsSync(ccsDir)) {
+2 -1
View File
@@ -1,6 +1,7 @@
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { getCcsDir } from './config-manager';
// import { execSync } from 'child_process'; // Currently unused
type ShellType = 'bash' | 'zsh' | 'fish' | 'powershell' | null;
@@ -24,7 +25,7 @@ export class ShellCompletionInstaller {
constructor() {
this.homeDir = os.homedir();
this.ccsDir = path.join(this.homeDir, '.ccs');
this.ccsDir = getCcsDir();
this.completionDir = path.join(this.ccsDir, 'completions');
this.scriptsDir = path.join(__dirname, '../../scripts/completion');
}
+2 -3
View File
@@ -6,9 +6,8 @@
*/
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { getConfigPath } from '../../utils/config-manager';
import { getConfigPath, getCcsDir } from '../../utils/config-manager';
import { isUnifiedMode, hasUnifiedConfig } from '../../config/unified-config-loader';
import type { HealthCheck } from './types';
@@ -18,7 +17,7 @@ import type { HealthCheck } from './types';
export function checkConfigFile(): HealthCheck {
// In unified mode, check config.yaml
if (isUnifiedMode() || hasUnifiedConfig()) {
const ccsDir = path.join(os.homedir(), '.ccs');
const ccsDir = getCcsDir();
const yamlPath = path.join(ccsDir, 'config.yaml');
if (!fs.existsSync(yamlPath)) {
+2 -2
View File
@@ -10,7 +10,7 @@ import { getDashboardAuthConfig } from '../../config/unified-config-loader';
import crypto from 'crypto';
import fs from 'fs';
import path from 'path';
import os from 'os';
import { getCcsDir } from '../../utils/config-manager';
// Extend Express Request with session
declare module 'express-session' {
@@ -24,7 +24,7 @@ declare module 'express-session' {
const PUBLIC_PATHS = ['/api/auth/login', '/api/auth/check', '/api/auth/setup', '/api/health'];
/** Path to persistent session secret file */
const SESSION_SECRET_PATH = path.join(os.homedir(), '.ccs', '.session-secret');
const SESSION_SECRET_PATH = path.join(getCcsDir(), '.session-secret');
/**
* Generate or retrieve persistent session secret.
+2 -2
View File
@@ -7,7 +7,6 @@
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import {
loadDailyUsageData,
loadMonthlyUsageData,
@@ -25,13 +24,14 @@ import {
getCacheAge,
} from './disk-cache';
import { ok, info, fail } from '../../utils/ui';
import { getCcsDir } from '../../utils/config-manager';
// ============================================================================
// Multi-Instance Support - Aggregate usage from CCS profiles
// ============================================================================
/** Path to CCS instances directory */
const CCS_INSTANCES_DIR = path.join(os.homedir(), '.ccs', 'instances');
const CCS_INSTANCES_DIR = path.join(getCcsDir(), 'instances');
/**
* Get list of CCS instance paths that have usage data
+2 -3
View File
@@ -10,13 +10,12 @@
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import type { DailyUsage, HourlyUsage, MonthlyUsage, SessionUsage } from './types';
import { ok, info, warn } from '../../utils/ui';
import { getCcsDir } from '../../utils/config-manager';
// Cache configuration
const CCS_DIR = path.join(os.homedir(), '.ccs');
const CACHE_DIR = path.join(CCS_DIR, 'cache');
const CACHE_DIR = path.join(getCcsDir(), 'cache');
const CACHE_FILE = path.join(CACHE_DIR, 'usage.json');
const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
const STALE_TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7 days (max age for stale data)
+44
View File
@@ -18,6 +18,9 @@ describe('Config Directory Override', function () {
originalCcsHome = process.env.CCS_HOME;
delete process.env.CCS_DIR;
delete process.env.CCS_HOME;
// Reset module-level state
const { setGlobalConfigDir } = require('../../dist/utils/config-manager');
setGlobalConfigDir(undefined);
});
afterEach(function () {
@@ -25,6 +28,9 @@ describe('Config Directory Override', function () {
else delete process.env.CCS_DIR;
if (originalCcsHome !== undefined) process.env.CCS_HOME = originalCcsHome;
else delete process.env.CCS_HOME;
// Reset module-level state
const { setGlobalConfigDir } = require('../../dist/utils/config-manager');
setGlobalConfigDir(undefined);
});
describe('getCcsDir() precedence', function () {
@@ -52,6 +58,29 @@ describe('Config Directory Override', function () {
const { getCcsDir } = require('../../dist/utils/config-manager');
assert.strictEqual(getCcsDir(), path.resolve('/tmp/ccs-dir'));
});
it('should give setGlobalConfigDir() highest precedence', function () {
process.env.CCS_DIR = '/tmp/env-dir';
process.env.CCS_HOME = '/tmp/env-home';
const { getCcsDir, setGlobalConfigDir } = require('../../dist/utils/config-manager');
setGlobalConfigDir('/tmp/flag-dir');
assert.strictEqual(getCcsDir(), path.resolve('/tmp/flag-dir'));
});
it('should resolve relative paths in setGlobalConfigDir()', function () {
const { getCcsDir, setGlobalConfigDir } = require('../../dist/utils/config-manager');
setGlobalConfigDir('relative/path');
assert.strictEqual(getCcsDir(), path.resolve('relative/path'));
});
it('should clear override when setGlobalConfigDir(undefined) is called', function () {
const { getCcsDir, setGlobalConfigDir } = require('../../dist/utils/config-manager');
setGlobalConfigDir('/tmp/override');
assert.strictEqual(getCcsDir(), path.resolve('/tmp/override'));
setGlobalConfigDir(undefined);
const expected = path.join(os.homedir(), '.ccs');
assert.strictEqual(getCcsDir(), expected);
});
});
describe('getCcsDirSource()', function () {
@@ -74,6 +103,14 @@ describe('Config Directory Override', function () {
const [source] = getCcsDirSource();
assert.strictEqual(source, 'CCS_HOME');
});
it('should return "--config-dir" when setGlobalConfigDir() is set', function () {
const { getCcsDirSource, setGlobalConfigDir } = require('../../dist/utils/config-manager');
setGlobalConfigDir('/tmp/flag-test');
const [source, dir] = getCcsDirSource();
assert.strictEqual(source, '--config-dir');
assert.strictEqual(dir, path.resolve('/tmp/flag-test'));
});
});
describe('detectCloudSyncPath()', function () {
@@ -100,6 +137,13 @@ describe('Config Directory Override', function () {
assert.strictEqual(detectCloudSyncPath('/Users/kai/Google Drive/ccs'), 'Google Drive');
});
it('should detect cloud paths case-insensitively', function () {
const { detectCloudSyncPath } = require('../../dist/utils/config-manager');
assert.strictEqual(detectCloudSyncPath('/Users/kai/dropbox/ccs'), 'Dropbox');
assert.strictEqual(detectCloudSyncPath('C:\\Users\\kai\\onedrive\\ccs'), 'OneDrive');
assert.strictEqual(detectCloudSyncPath('/Users/kai/google drive/ccs'), 'Google Drive');
});
it('should return null for regular paths', function () {
const { detectCloudSyncPath } = require('../../dist/utils/config-manager');
assert.strictEqual(detectCloudSyncPath('/home/kai/.ccs'), null);