mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-13 14:15:56 +00:00
Merge pull request #508 from kaitranntt/kai/feat/507-config-dir-override
feat(config): add CCS_DIR env var and --config-dir flag
This commit is contained in:
22 files changed
+421
-109
No files matched your search
@@ -5,10 +5,12 @@
|
||||
|
||||
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');
|
||||
function getCacheFile() {
|
||||
return path.join(getCcsDir(), 'openrouter-models-cache.json');
|
||||
}
|
||||
const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
||||
|
||||
export interface OpenRouterModel {
|
||||
@@ -30,8 +32,9 @@ interface CacheData {
|
||||
/** Check if cached data is valid */
|
||||
function getCachedModels(): OpenRouterModel[] | null {
|
||||
try {
|
||||
if (!fs.existsSync(CACHE_FILE)) return null;
|
||||
const data = JSON.parse(fs.readFileSync(CACHE_FILE, 'utf8')) as CacheData;
|
||||
const cacheFile = getCacheFile();
|
||||
if (!fs.existsSync(cacheFile)) return null;
|
||||
const data = JSON.parse(fs.readFileSync(cacheFile, 'utf8')) as CacheData;
|
||||
if (Date.now() - data.fetchedAt > CACHE_TTL_MS) return null;
|
||||
return data.models;
|
||||
} catch {
|
||||
@@ -42,10 +45,11 @@ function getCachedModels(): OpenRouterModel[] | null {
|
||||
/** Save models to cache */
|
||||
function setCachedModels(models: OpenRouterModel[]): void {
|
||||
try {
|
||||
const dir = path.dirname(CACHE_FILE);
|
||||
const cacheFile = getCacheFile();
|
||||
const dir = path.dirname(cacheFile);
|
||||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
CACHE_FILE,
|
||||
cacheFile,
|
||||
JSON.stringify({
|
||||
models,
|
||||
fetchedAt: Date.now(),
|
||||
|
||||
+73
-5
@@ -2,7 +2,13 @@ import { spawn, ChildProcess } from 'child_process';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import { detectClaudeCli } from './utils/claude-detector';
|
||||
import { getSettingsPath, loadSettings } from './utils/config-manager';
|
||||
import {
|
||||
getSettingsPath,
|
||||
loadSettings,
|
||||
getCcsDir,
|
||||
setGlobalConfigDir,
|
||||
detectCloudSyncPath,
|
||||
} from './utils/config-manager';
|
||||
import { validateGlmKey, validateMiniMaxKey } from './utils/api-key-validator';
|
||||
import { ErrorManager } from './utils/error-manager';
|
||||
import { execClaudeWithCLIProxy, CLIProxyProvider } from './cliproxy';
|
||||
@@ -15,7 +21,7 @@ import {
|
||||
import { getGlobalEnvConfig } from './config/unified-config-loader';
|
||||
import { ensureProfileHooks as ensureImageAnalyzerHooks } from './utils/hooks/image-analyzer-profile-hook-injector';
|
||||
import { getImageAnalysisHookEnv } from './utils/hooks';
|
||||
import { fail, info } from './utils/ui';
|
||||
import { fail, info, warn } from './utils/ui';
|
||||
|
||||
// Import centralized error handling
|
||||
import { handleError, runCleanup } from './errors';
|
||||
@@ -80,7 +86,7 @@ async function execClaudeWithProxy(
|
||||
|
||||
if (!apiKey || apiKey === 'YOUR_GLM_API_KEY_HERE') {
|
||||
console.error(fail('GLMT profile requires Z.AI API key'));
|
||||
console.error(' Edit ~/.ccs/glmt.settings.json and set ANTHROPIC_AUTH_TOKEN');
|
||||
console.error(` Edit ${getCcsDir()}/glmt.settings.json and set ANTHROPIC_AUTH_TOKEN`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -148,7 +154,7 @@ async function execClaudeWithProxy(
|
||||
console.error('Workarounds:');
|
||||
console.error(' - Use non-thinking mode: ccs glm "prompt"');
|
||||
console.error(' - Enable verbose logging: ccs glmt --verbose "prompt"');
|
||||
console.error(' - Check proxy logs in ~/.ccs/logs/ (if debug enabled)');
|
||||
console.error(` - Check proxy logs in ${getCcsDir()}/logs/ (if debug enabled)`);
|
||||
console.error('');
|
||||
proxy.kill();
|
||||
process.exit(1);
|
||||
@@ -259,7 +265,6 @@ async function showCachedUpdateNotification(): Promise<boolean> {
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const args = process.argv.slice(2);
|
||||
const firstArg = args[0];
|
||||
|
||||
// Initialize UI colors early to ensure consistent colored output
|
||||
// Must happen before any status messages (ok, info, fail, etc.)
|
||||
@@ -268,6 +273,69 @@ async function main(): Promise<void> {
|
||||
await initUI();
|
||||
}
|
||||
|
||||
// Parse --config-dir flag (must happen before any config loading)
|
||||
const configDirIdx = args.findIndex((a) => a === '--config-dir' || a.startsWith('--config-dir='));
|
||||
if (configDirIdx !== -1) {
|
||||
const arg = args[configDirIdx];
|
||||
let configDirValue: string | undefined;
|
||||
let spliceCount = 1;
|
||||
|
||||
if (arg.startsWith('--config-dir=')) {
|
||||
configDirValue = arg.split('=').slice(1).join('=');
|
||||
} else {
|
||||
configDirValue = args[configDirIdx + 1];
|
||||
spliceCount = 2;
|
||||
}
|
||||
|
||||
if (!configDirValue || configDirValue.startsWith('-')) {
|
||||
console.error(fail('--config-dir requires a path argument'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
const stat = fs.statSync(configDirValue);
|
||||
if (!stat.isDirectory()) {
|
||||
console.error(fail(`Not a directory: ${configDirValue}`));
|
||||
process.exit(1);
|
||||
}
|
||||
} catch {
|
||||
console.error(fail(`Config directory not found: ${configDirValue}`));
|
||||
console.error(info('Create the directory first, then copy your config files into it.'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
setGlobalConfigDir(configDirValue);
|
||||
|
||||
// Security warning: cloud sync paths expose OAuth tokens
|
||||
const cloudService = detectCloudSyncPath(configDirValue);
|
||||
if (cloudService) {
|
||||
console.error(warn(`CCS directory is under ${cloudService}.`));
|
||||
console.error(' OAuth tokens in cliproxy/auth/ will be synced to cloud.');
|
||||
console.error(' Consider: CCS_DIR=/path/outside/cloud ccs ...');
|
||||
}
|
||||
|
||||
// Remove consumed args so they don't leak to Claude CLI
|
||||
args.splice(configDirIdx, spliceCount);
|
||||
} else if (process.env.CCS_DIR) {
|
||||
// Also warn for CCS_DIR env var pointing to cloud sync
|
||||
const cloudService = detectCloudSyncPath(process.env.CCS_DIR);
|
||||
if (cloudService) {
|
||||
console.error(warn(`CCS directory is under ${cloudService}.`));
|
||||
console.error(' OAuth tokens in cliproxy/auth/ will be synced to cloud.');
|
||||
console.error(' Consider: CCS_DIR=/path/outside/cloud ccs ...');
|
||||
}
|
||||
} else if (process.env.CCS_HOME) {
|
||||
// Also warn for CCS_HOME env var pointing to cloud sync
|
||||
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.');
|
||||
console.error(' Consider: CCS_DIR=/path/outside/cloud ccs ...');
|
||||
}
|
||||
}
|
||||
|
||||
const firstArg = args[0];
|
||||
|
||||
// Trigger update check early for ALL commands except version/help/update
|
||||
// Only if TTY and not CI to avoid noise in automated environments
|
||||
const skipUpdateCheck = [
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,6 +2,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';
|
||||
import { getCcsDir, getCcsDirSource } from '../utils/config-manager';
|
||||
|
||||
// Get version from package.json (same as version-command.ts)
|
||||
const VERSION = JSON.parse(
|
||||
@@ -110,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...]`);
|
||||
@@ -121,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)'],
|
||||
@@ -268,6 +273,7 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim();
|
||||
|
||||
// Flags
|
||||
printSubSection('Flags', [
|
||||
['--config-dir <path>', 'Use custom CCS config directory'],
|
||||
['-h, --help', 'Show this help message'],
|
||||
['-v, --version', 'Show version and installation info'],
|
||||
['-sc, --shell-completion', 'Install shell auto-completion'],
|
||||
@@ -275,10 +281,10 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim();
|
||||
|
||||
// Configuration
|
||||
printConfigSection('Configuration', [
|
||||
['Config File:', isUnifiedMode() ? '~/.ccs/config.yaml' : '~/.ccs/config.json'],
|
||||
['Profiles:', '~/.ccs/profiles.json'],
|
||||
['Instances:', '~/.ccs/instances/'],
|
||||
['Settings:', '~/.ccs/*.settings.json'],
|
||||
['Config File:', isUnifiedMode() ? `${dirDisplay}/config.yaml` : `${dirDisplay}/config.json`],
|
||||
['Profiles:', `${dirDisplay}/profiles.json`],
|
||||
['Instances:', `${dirDisplay}/instances/`],
|
||||
['Settings:', `${dirDisplay}/*.settings.json`],
|
||||
]);
|
||||
|
||||
// CLI Proxy management
|
||||
@@ -345,6 +351,13 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim();
|
||||
['', 'providers (agy, gemini, codex, kiro, ghcp).'],
|
||||
]);
|
||||
|
||||
// CCS Environment Variables
|
||||
printSubSection('Environment Variables', [
|
||||
['CCS_DIR', 'Override CCS config directory (default: ~/.ccs)'],
|
||||
['CCS_HOME', 'Override home directory (legacy, appends .ccs)'],
|
||||
['CCS_DEBUG', 'Enable debug logging'],
|
||||
]);
|
||||
|
||||
// CLI Proxy env vars
|
||||
printSubSection('CLI Proxy Environment Variables', [
|
||||
['CCS_PROXY_HOST', 'Remote proxy hostname'],
|
||||
@@ -358,17 +371,17 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim();
|
||||
|
||||
// CLI Proxy paths
|
||||
console.log(subheader('CLI Proxy:'));
|
||||
console.log(` Binary: ${color('~/.ccs/cliproxy/bin/cli-proxy-api-plus', 'path')}`);
|
||||
console.log(` Config: ${color('~/.ccs/cliproxy/config.yaml', 'path')}`);
|
||||
console.log(` Auth: ${color('~/.ccs/cliproxy/auth/', 'path')}`);
|
||||
console.log(` Binary: ${color(`${dirDisplay}/cliproxy/bin/cli-proxy-api-plus`, 'path')}`);
|
||||
console.log(` Config: ${color(`${dirDisplay}/cliproxy/config.yaml`, 'path')}`);
|
||||
console.log(` Auth: ${color(`${dirDisplay}/cliproxy/auth/`, 'path')}`);
|
||||
console.log(` ${dim('Port: 8317 (default)')}`);
|
||||
console.log('');
|
||||
|
||||
// Shared Data
|
||||
console.log(subheader('Shared Data:'));
|
||||
console.log(` Commands: ${color('~/.ccs/shared/commands/', 'path')}`);
|
||||
console.log(` Skills: ${color('~/.ccs/shared/skills/', 'path')}`);
|
||||
console.log(` Agents: ${color('~/.ccs/shared/agents/', 'path')}`);
|
||||
console.log(` Commands: ${color(`${dirDisplay}/shared/commands/`, 'path')}`);
|
||||
console.log(` Skills: ${color(`${dirDisplay}/shared/skills/`, 'path')}`);
|
||||
console.log(` Agents: ${color(`${dirDisplay}/shared/agents/`, 'path')}`);
|
||||
console.log(` ${dim('Note: Symlinked across all profiles')}`);
|
||||
console.log('');
|
||||
|
||||
|
||||
@@ -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'));
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ResultFormatter } from './result-formatter';
|
||||
import { DelegationValidator } from '../utils/delegation-validator';
|
||||
import { SettingsParser } from './settings-parser';
|
||||
import { fail, warn } from '../utils/ui';
|
||||
import { getCcsDir } from '../utils/config-manager';
|
||||
|
||||
/**
|
||||
* Parse and validate a string flag value
|
||||
@@ -330,7 +331,7 @@ export class DelegationHandler {
|
||||
console.error(` ${validation.error}`);
|
||||
console.error('');
|
||||
console.error(' Run: ccs doctor');
|
||||
console.error(` Or configure: ~/.ccs/${profile}.settings.json`);
|
||||
console.error(` Or configure: ${getCcsDir()}/${profile}.settings.json`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
import { getEnvironmentDiagnostics } from '../environment-diagnostics';
|
||||
import { ok, warn } from '../../utils/ui';
|
||||
import { getCcsDir, getCcsDirSource } from '../../utils/config-manager';
|
||||
import { HealthCheck, IHealthChecker, createSpinner } from './types';
|
||||
|
||||
const ora = createSpinner();
|
||||
@@ -54,6 +55,12 @@ export class EnvironmentChecker implements IHealthChecker {
|
||||
}
|
||||
console.log(` ${''.padEnd(24)} Browser: ${diag.browserReason}`);
|
||||
|
||||
// Show CCS directory and source
|
||||
const ccsDir = getCcsDir();
|
||||
const [dirSource] = getCcsDirSource();
|
||||
const sourceLabel = dirSource === 'default' ? '' : ` (via ${dirSource})`;
|
||||
console.log(` ${''.padEnd(24)} CCS Dir: ${ccsDir}${sourceLabel}`);
|
||||
|
||||
results.addCheck(
|
||||
'Environment',
|
||||
envStatus === 'OK' ? 'success' : 'warning',
|
||||
|
||||
@@ -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');
|
||||
@@ -48,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;
|
||||
@@ -67,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
|
||||
@@ -83,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
|
||||
@@ -92,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
|
||||
@@ -111,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
|
||||
@@ -120,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;
|
||||
}
|
||||
|
||||
@@ -136,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;
|
||||
}
|
||||
|
||||
@@ -197,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;
|
||||
@@ -245,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');
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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[] = [];
|
||||
|
||||
|
||||
@@ -10,6 +10,18 @@ import { isUnifiedMode, loadOrCreateUnifiedConfig } from '../config/unified-conf
|
||||
// const { ErrorManager } = require('./error-manager');
|
||||
// const RecoveryManager = require('./recovery-manager');
|
||||
|
||||
// Module-level state for --config-dir CLI flag override
|
||||
let _globalConfigDir: string | undefined;
|
||||
|
||||
/**
|
||||
* Set global config directory from --config-dir CLI flag.
|
||||
* Must be called before any config loading.
|
||||
* Pass undefined to clear (useful for tests).
|
||||
*/
|
||||
export function setGlobalConfigDir(dir: string | undefined): void {
|
||||
_globalConfigDir = dir ? path.resolve(dir) : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the CCS home directory (respects CCS_HOME env var for test isolation)
|
||||
* @returns Home directory path
|
||||
@@ -19,11 +31,62 @@ export function getCcsHome(): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the CCS directory path (~/.ccs)
|
||||
* @returns Path to .ccs directory
|
||||
* 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 {
|
||||
return path.join(getCcsHome(), '.ccs');
|
||||
return _resolveCcsDir().dir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get which source determined the CCS directory (for diagnostics).
|
||||
* @returns [source_label, resolved_path] tuple
|
||||
*/
|
||||
export function getCcsDirSource(): [string, string] {
|
||||
const r = _resolveCcsDir();
|
||||
return [r.source, r.dir];
|
||||
}
|
||||
|
||||
/**
|
||||
* Cloud sync folder patterns for security warning.
|
||||
*/
|
||||
const CLOUD_SYNC_PATTERNS = [
|
||||
'Dropbox',
|
||||
'OneDrive',
|
||||
'Google Drive',
|
||||
'iCloud Drive',
|
||||
'iCloudDrive',
|
||||
'pCloud',
|
||||
'MEGA',
|
||||
'Box Sync',
|
||||
];
|
||||
|
||||
/**
|
||||
* Check if a path is under a cloud-synced folder.
|
||||
* @returns Detected service name or null
|
||||
*/
|
||||
export function detectCloudSyncPath(dir: string): string | null {
|
||||
const normalized = dir.replace(/\\/g, '/').toLowerCase();
|
||||
const segments = normalized.split('/');
|
||||
for (const pattern of CLOUD_SYNC_PATTERNS) {
|
||||
const patternLower = pattern.toLowerCase();
|
||||
// Exact path segment match to avoid false positives (e.g., "megauser" != "MEGA")
|
||||
if (segments.some((s) => s === patternLower)) return pattern;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -39,7 +102,7 @@ export function getCcsHooksDir(): string {
|
||||
* @deprecated Use getActiveConfigPath() for mode-aware config path
|
||||
*/
|
||||
export function getConfigPath(): string {
|
||||
return process.env.CCS_CONFIG || path.join(getCcsHome(), '.ccs', 'config.json');
|
||||
return process.env.CCS_CONFIG || path.join(getCcsDir(), 'config.json');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ export async function runHealthChecks(): Promise<HealthReport> {
|
||||
const healthChecks: HealthCheck[] = [];
|
||||
healthChecks.push(checkPermissions(ccsDir));
|
||||
healthChecks.push(checkCcsSymlinks());
|
||||
healthChecks.push(checkSettingsSymlinks(homedir, ccsDir, claudeDir));
|
||||
healthChecks.push(checkSettingsSymlinks(ccsDir, claudeDir));
|
||||
groups.push({ id: 'system-health', name: 'System Health', icon: 'Shield', checks: healthChecks });
|
||||
|
||||
// Group 6: CLIProxy
|
||||
@@ -139,7 +139,7 @@ export function fixHealthIssue(checkId: string): { success: boolean; message: st
|
||||
switch (checkId) {
|
||||
case 'ccs-dir':
|
||||
fs.mkdirSync(ccsDir, { recursive: true });
|
||||
return { success: true, message: 'Created ~/.ccs directory' };
|
||||
return { success: true, message: `Created ${ccsDir} directory` };
|
||||
|
||||
case 'config-file': {
|
||||
// Use appropriate config based on unified mode
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -49,13 +49,9 @@ export function checkCcsSymlinks(): HealthCheck {
|
||||
/**
|
||||
* Check settings symlinks
|
||||
*/
|
||||
export function checkSettingsSymlinks(
|
||||
homedir: string,
|
||||
ccsDir: string,
|
||||
claudeDir: string
|
||||
): HealthCheck {
|
||||
export function checkSettingsSymlinks(ccsDir: string, claudeDir: string): HealthCheck {
|
||||
try {
|
||||
const sharedDir = `${homedir}/.ccs/shared`;
|
||||
const sharedDir = `${ccsDir}/shared`;
|
||||
const sharedSettings = `${sharedDir}/settings.json`;
|
||||
const claudeSettings = `${claudeDir}/settings.json`;
|
||||
|
||||
|
||||
@@ -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,9 @@ 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');
|
||||
function getSessionSecretPath() {
|
||||
return path.join(getCcsDir(), '.session-secret');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate or retrieve persistent session secret.
|
||||
@@ -36,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(SESSION_SECRET_PATH)) {
|
||||
const secret = fs.readFileSync(SESSION_SECRET_PATH, 'utf-8').trim();
|
||||
if (fs.existsSync(secretPath)) {
|
||||
const secret = fs.readFileSync(secretPath, 'utf-8').trim();
|
||||
if (secret.length >= 32) {
|
||||
return secret;
|
||||
}
|
||||
@@ -51,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(SESSION_SECRET_PATH);
|
||||
const dir = path.dirname(secretPath);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(SESSION_SECRET_PATH, 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);
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import {
|
||||
loadDailyUsageData,
|
||||
loadMonthlyUsageData,
|
||||
@@ -25,28 +24,32 @@ 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');
|
||||
function getCcsInstancesDir() {
|
||||
return path.join(getCcsDir(), 'instances');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of CCS instance paths that have usage data
|
||||
* Only returns instances with existing projects/ directory
|
||||
*/
|
||||
function getInstancePaths(): string[] {
|
||||
if (!fs.existsSync(CCS_INSTANCES_DIR)) {
|
||||
const instancesDir = getCcsInstancesDir();
|
||||
if (!fs.existsSync(instancesDir)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const entries = fs.readdirSync(CCS_INSTANCES_DIR, { withFileTypes: true });
|
||||
const entries = fs.readdirSync(instancesDir, { withFileTypes: true });
|
||||
return entries
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.map((entry) => path.join(CCS_INSTANCES_DIR, entry.name))
|
||||
.map((entry) => path.join(instancesDir, entry.name))
|
||||
.filter((instancePath) => {
|
||||
// Only include instances that have a projects directory
|
||||
const projectsPath = path.join(instancePath, 'projects');
|
||||
|
||||
@@ -10,14 +10,17 @@
|
||||
|
||||
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_FILE = path.join(CACHE_DIR, 'usage.json');
|
||||
function getCacheDir() {
|
||||
return path.join(getCcsDir(), 'cache');
|
||||
}
|
||||
function getCacheFile() {
|
||||
return path.join(getCacheDir(), '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)
|
||||
|
||||
@@ -39,8 +42,9 @@ const CACHE_VERSION = 3;
|
||||
* Ensure ~/.ccs/cache directory exists
|
||||
*/
|
||||
function ensureCacheDir(): void {
|
||||
if (!fs.existsSync(CACHE_DIR)) {
|
||||
fs.mkdirSync(CACHE_DIR, { recursive: true });
|
||||
const dir = getCacheDir();
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,11 +55,12 @@ function ensureCacheDir(): void {
|
||||
*/
|
||||
export function readDiskCache(): UsageDiskCache | null {
|
||||
try {
|
||||
if (!fs.existsSync(CACHE_FILE)) {
|
||||
const cacheFile = getCacheFile();
|
||||
if (!fs.existsSync(cacheFile)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = fs.readFileSync(CACHE_FILE, 'utf-8');
|
||||
const data = fs.readFileSync(cacheFile, 'utf-8');
|
||||
const cache: UsageDiskCache = JSON.parse(data);
|
||||
|
||||
// Version check - invalidate if schema changed
|
||||
@@ -113,9 +118,10 @@ export function writeDiskCache(
|
||||
};
|
||||
|
||||
// Write atomically using temp file + rename
|
||||
const tempFile = CACHE_FILE + '.tmp';
|
||||
const cacheFile = getCacheFile();
|
||||
const tempFile = cacheFile + '.tmp';
|
||||
fs.writeFileSync(tempFile, JSON.stringify(cache), 'utf-8');
|
||||
fs.renameSync(tempFile, CACHE_FILE);
|
||||
fs.renameSync(tempFile, cacheFile);
|
||||
|
||||
console.log(ok('Disk cache updated'));
|
||||
} catch (err) {
|
||||
@@ -145,8 +151,9 @@ export function getCacheAge(cache: UsageDiskCache | null): string {
|
||||
*/
|
||||
export function clearDiskCache(): void {
|
||||
try {
|
||||
if (fs.existsSync(CACHE_FILE)) {
|
||||
fs.unlinkSync(CACHE_FILE);
|
||||
const cacheFile = getCacheFile();
|
||||
if (fs.existsSync(cacheFile)) {
|
||||
fs.unlinkSync(cacheFile);
|
||||
console.log(ok('Disk cache cleared'));
|
||||
}
|
||||
} catch (err) {
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
/**
|
||||
* Config Directory Override Unit Tests
|
||||
*
|
||||
* Tests CCS_DIR env var and --config-dir flag precedence,
|
||||
* getCcsDirSource() diagnostic, and cloud sync path detection.
|
||||
*/
|
||||
|
||||
const assert = require('assert');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
|
||||
describe('Config Directory Override', function () {
|
||||
let originalCcsDir;
|
||||
let originalCcsHome;
|
||||
|
||||
beforeEach(function () {
|
||||
originalCcsDir = process.env.CCS_DIR;
|
||||
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 () {
|
||||
if (originalCcsDir !== undefined) process.env.CCS_DIR = originalCcsDir;
|
||||
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 () {
|
||||
it('should return ~/.ccs by default', function () {
|
||||
const { getCcsDir } = require('../../dist/utils/config-manager');
|
||||
const expected = path.join(os.homedir(), '.ccs');
|
||||
assert.strictEqual(getCcsDir(), expected);
|
||||
});
|
||||
|
||||
it('should use CCS_HOME with .ccs appended (legacy behavior)', function () {
|
||||
process.env.CCS_HOME = '/tmp/test-home';
|
||||
const { getCcsDir } = require('../../dist/utils/config-manager');
|
||||
assert.strictEqual(getCcsDir(), '/tmp/test-home/.ccs');
|
||||
});
|
||||
|
||||
it('should use CCS_DIR directly (no .ccs append)', function () {
|
||||
process.env.CCS_DIR = '/tmp/my-ccs-config';
|
||||
const { getCcsDir } = require('../../dist/utils/config-manager');
|
||||
assert.strictEqual(getCcsDir(), path.resolve('/tmp/my-ccs-config'));
|
||||
});
|
||||
|
||||
it('should give CCS_DIR precedence over CCS_HOME', function () {
|
||||
process.env.CCS_DIR = '/tmp/ccs-dir';
|
||||
process.env.CCS_HOME = '/tmp/ccs-home';
|
||||
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 () {
|
||||
it('should return "default" when no overrides set', function () {
|
||||
const { getCcsDirSource } = require('../../dist/utils/config-manager');
|
||||
const [source] = getCcsDirSource();
|
||||
assert.strictEqual(source, 'default');
|
||||
});
|
||||
|
||||
it('should return "CCS_DIR" when CCS_DIR is set', function () {
|
||||
process.env.CCS_DIR = '/tmp/test';
|
||||
const { getCcsDirSource } = require('../../dist/utils/config-manager');
|
||||
const [source] = getCcsDirSource();
|
||||
assert.strictEqual(source, 'CCS_DIR');
|
||||
});
|
||||
|
||||
it('should return "CCS_HOME" when only CCS_HOME is set', function () {
|
||||
process.env.CCS_HOME = '/tmp/home';
|
||||
const { getCcsDirSource } = require('../../dist/utils/config-manager');
|
||||
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 () {
|
||||
it('should detect Dropbox', function () {
|
||||
const { detectCloudSyncPath } = require('../../dist/utils/config-manager');
|
||||
assert.strictEqual(detectCloudSyncPath('/Users/kai/Dropbox/ccs'), 'Dropbox');
|
||||
});
|
||||
|
||||
it('should detect OneDrive on Windows paths', function () {
|
||||
const { detectCloudSyncPath } = require('../../dist/utils/config-manager');
|
||||
assert.strictEqual(detectCloudSyncPath('C:\\Users\\kai\\OneDrive\\ccs'), 'OneDrive');
|
||||
});
|
||||
|
||||
it('should detect iCloud Drive', function () {
|
||||
const { detectCloudSyncPath } = require('../../dist/utils/config-manager');
|
||||
assert.strictEqual(
|
||||
detectCloudSyncPath('/Users/kai/Library/Mobile Documents/iCloud Drive/ccs'),
|
||||
'iCloud Drive'
|
||||
);
|
||||
});
|
||||
|
||||
it('should detect Google Drive', function () {
|
||||
const { detectCloudSyncPath } = require('../../dist/utils/config-manager');
|
||||
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);
|
||||
});
|
||||
|
||||
it('should return null for default ~/.ccs path', function () {
|
||||
const { detectCloudSyncPath } = require('../../dist/utils/config-manager');
|
||||
assert.strictEqual(detectCloudSyncPath(path.join(os.homedir(), '.ccs')), null);
|
||||
});
|
||||
|
||||
it('should not false-positive on substrings (e.g., megauser, Dropbox-api)', function () {
|
||||
const { detectCloudSyncPath } = require('../../dist/utils/config-manager');
|
||||
assert.strictEqual(detectCloudSyncPath('/home/megauser/.ccs'), null);
|
||||
assert.strictEqual(detectCloudSyncPath('/home/kai/Dropbox-api-client/ccs'), null);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user