diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4b1b39a5..ff1a0757 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -144,8 +144,8 @@ git checkout -b your-feature-name # Test locally with ./ccs # Run tests -./tests/edge-cases.sh # Unix -./tests/edge-cases.ps1 # Windows +bun run test # All tests +bun run test:native # Native Unix tests only ``` #### Testing npm Package Locally diff --git a/docs/system-architecture.md b/docs/system-architecture.md index bf586581..48ca93a3 100644 --- a/docs/system-architecture.md +++ b/docs/system-architecture.md @@ -1094,11 +1094,11 @@ graph TD ``` tests/ -├── shared/ -│ ├── unit/ # Unit tests for individual modules -│ └── fixtures/ # Test data and configurations +├── unit/ # Module unit tests (GLMT, delegation) +├── shared/ # Shared utilities and fixtures ├── npm/ # npm package-specific tests -└── edge-cases.sh # Comprehensive scenario testing +├── native/ # Native installation tests (bash/PowerShell) +└── integration/ # Integration tests ``` ### Test Coverage Strategy diff --git a/package.json b/package.json index ebbe79ee..adaefaad 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,6 @@ "test:unit": "mocha 'tests/**/unit/**/*.test.js' 'tests/unit/**/*.test.js' --timeout 5000", "test:npm": "mocha tests/npm/**/*.test.js --timeout 10000", "test:native": "bash tests/native/unix/edge-cases.sh", - "test:edge-cases": "bash tests/edge-cases.sh", "prepublishOnly": "npm run validate && node scripts/sync-version.js", "prepack": "npm run validate && node scripts/sync-version.js", "prepare": "node scripts/check-executables.js", diff --git a/src/ccs.ts.backup b/src/ccs.ts.backup deleted file mode 100644 index 7e1c9ccf..00000000 --- a/src/ccs.ts.backup +++ /dev/null @@ -1,958 +0,0 @@ -#!/usr/bin/env node -/** - * CCS (Claude Code Switch) - Entry Point - * - * Instant profile switching for Claude CLI. - * Supports multiple accounts, alternative models (GLM, Kimi), - * and cost-optimized delegation. - */ - -import { spawn, ChildProcess, spawnSync } from 'child_process'; -import * as path from 'path'; -import * as fs from 'fs'; -import * as os from 'os'; -import { colored } from './utils/helpers'; -import { detectClaudeCli } from './utils/claude-detector'; -import { getSettingsPath, getConfigPath } from './utils/config-manager'; -import { ErrorManager } from './utils/error-manager'; -import { handleHelpCommand } from './commands/help-command'; - -// Version (sync with package.json) -const CCS_VERSION = JSON.parse( - fs.readFileSync(path.join(__dirname, '../package.json'), 'utf8') -).version; - -// ========== Helper Functions ========== - -/** - * Escape arguments for shell execution (Windows compatibility) - */ -function escapeShellArg(arg: string): string { - return '"' + String(arg).replace(/"/g, '""') + '"'; -} - -/** - * Execute Claude CLI with unified spawn logic - */ -function execClaude( - claudeCli: string, - args: string[], - envVars: NodeJS.ProcessEnv | null = null -): void { - const isWindows = process.platform === 'win32'; - const needsShell = isWindows && /\.(cmd|bat|ps1)$/i.test(claudeCli); - - // Prepare environment (merge with process.env if envVars provided) - const env = envVars ? { ...process.env, ...envVars } : process.env; - - let child: ChildProcess; - if (needsShell) { - // When shell needed: concatenate into string to avoid DEP0190 warning - const cmdString = [claudeCli, ...args].map(escapeShellArg).join(' '); - child = spawn(cmdString, { - stdio: 'inherit', - windowsHide: true, - shell: true, - env, - }); - } else { - // When no shell needed: use array form (faster, no shell overhead) - child = spawn(claudeCli, args, { - stdio: 'inherit', - windowsHide: true, - env, - }); - } - - child.on('exit', (code, signal) => { - if (signal) process.kill(process.pid, signal as NodeJS.Signals); - else process.exit(code || 0); - }); - - child.on('error', () => { - ErrorManager.showClaudeNotFound(); - process.exit(1); - }); -} - -// ========== Command Handlers ========== - -/** - * Handle version command - */ -function handleVersionCommand(): void { - console.log(colored(`CCS (Claude Code Switch) v${CCS_VERSION}`, 'bold')); - console.log(''); - - console.log(colored('Installation:', 'cyan')); - const installLocation = process.argv[1] || '(not found)'; - console.log(` ${colored('Location:'.padEnd(17), 'cyan')} ${installLocation}`); - - const ccsDir = path.join(os.homedir(), '.ccs'); - console.log(` ${colored('CCS Directory:'.padEnd(17), 'cyan')} ${ccsDir}`); - - const configPath = getConfigPath(); - console.log(` ${colored('Config:'.padEnd(17), 'cyan')} ${configPath}`); - - const profilesJson = path.join(os.homedir(), '.ccs', 'profiles.json'); - console.log(` ${colored('Profiles:'.padEnd(17), 'cyan')} ${profilesJson}`); - - // Delegation status - const delegationSessionsPath = path.join(os.homedir(), '.ccs', '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`); - if (fs.existsSync(settingsPath)) { - try { - const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); - const apiKey = settings.env?.ANTHROPIC_AUTH_TOKEN; - if (apiKey && !apiKey.match(/YOUR_.*_API_KEY_HERE/) && !apiKey.match(/sk-test.*/)) { - readyProfiles.push(profile); - } - } catch (_error) { - // Invalid JSON, skip - } - } - } - - const hasValidApiKeys = readyProfiles.length > 0; - const delegationEnabled = delegationConfigured || hasValidApiKeys; - - if (delegationEnabled) { - console.log(` ${colored('Delegation:'.padEnd(17), 'cyan')} Enabled`); - } else { - console.log(` ${colored('Delegation:'.padEnd(17), 'cyan')} Not configured`); - } - - console.log(''); - - if (readyProfiles.length > 0) { - console.log(colored('Delegation Ready:', 'cyan')); - console.log( - ` ${colored('[OK]', 'yellow')} ${readyProfiles.join(', ')} profiles are ready for delegation` - ); - console.log(''); - } else if (delegationEnabled) { - console.log(colored('Delegation Ready:', 'cyan')); - console.log(` ${colored('[!]', 'yellow')} Delegation configured but no valid API keys found`); - console.log(''); - } - - console.log(`${colored('Documentation:', 'cyan')} https://github.com/kaitranntt/ccs`); - console.log(`${colored('License:', 'cyan')} MIT`); - console.log(''); - console.log(colored("Run 'ccs --help' for usage information", 'yellow')); - - process.exit(0); -} - -/** - * Handle help command - */ - -function handleInstallCommand(): void { - console.log(''); - console.log('Feature not available'); - console.log(''); - console.log('The --install flag is currently under development.'); - console.log('.claude/ integration testing is not complete.'); - console.log(''); - console.log('For updates: https://github.com/kaitranntt/ccs/issues'); - console.log(''); - process.exit(0); -} - -function handleUninstallCommand(): void { - console.log(''); - console.log('Feature not available'); - console.log(''); - console.log('The --uninstall flag is currently under development.'); - console.log('.claude/ integration testing is not complete.'); - console.log(''); - console.log('For updates: https://github.com/kaitranntt/ccs/issues'); - console.log(''); - process.exit(0); -} - -async function handleDoctorCommand(): Promise { - const DoctorModule = await import('./management/doctor'); - const Doctor = DoctorModule.default; - const doctor = new Doctor(); - - await doctor.runAllChecks(); - - // Exit with error code if unhealthy - process.exit(doctor.isHealthy() ? 0 : 1); -} - -async function handleSyncCommand(): Promise { - console.log(''); - console.log(colored('Syncing CCS Components...', 'cyan')); - console.log(''); - - // First, copy .claude/ directory from package to ~/.ccs/.claude/ - const { ClaudeDirInstaller } = await import('./utils/claude-dir-installer'); - const installer = new ClaudeDirInstaller(); - installer.install(); - - console.log(''); - - const cleanupResult = installer.cleanupDeprecated(); - if (cleanupResult.success && cleanupResult.cleanedFiles.length > 0) { - console.log(''); - } - - // Then, create symlinks from ~/.ccs/.claude/ to ~/.claude/ - const { ClaudeSymlinkManager } = await import('./utils/claude-symlink-manager'); - const manager = new ClaudeSymlinkManager(); - manager.install(false); - - console.log(''); - console.log(colored('[OK] Sync complete!', 'green')); - console.log(''); - - process.exit(0); -} - -/** - * Detect installation method - */ -function detectInstallationMethod(): 'npm' | 'direct' { - const scriptPath = process.argv[1]; - - // Method 1: Check if script is inside node_modules - if (scriptPath.includes('node_modules')) { - return 'npm'; - } - - // Method 2: Check if script is in npm global bin directory - const npmGlobalBinPatterns = [ - /\.npm\/global\/bin\//, - /\/\.nvm\/versions\/node\/[^/]+\/bin\//, - /\/usr\/local\/bin\//, - /\/usr\/bin\//, - ]; - - for (const pattern of npmGlobalBinPatterns) { - if (pattern.test(scriptPath)) { - try { - const binDir = path.dirname(scriptPath); - const nodeModulesDir = path.join(binDir, '..', 'lib', 'node_modules', '@kaitranntt', 'ccs'); - const globalModulesDir = path.join(binDir, '..', 'node_modules', '@kaitranntt', 'ccs'); - - if (fs.existsSync(nodeModulesDir) || fs.existsSync(globalModulesDir)) { - return 'npm'; - } - } catch (_err) { - // Continue checking other patterns - } - } - } - - // Method 3: Check if package.json exists in parent directory - const packageJsonPath = path.join(__dirname, '..', 'package.json'); - - if (fs.existsSync(packageJsonPath)) { - try { - const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); - if (pkg.name === '@kaitranntt/ccs') { - return 'npm'; - } - } catch (_err) { - // Ignore parse errors - } - } - - // Method 4: Check if script is a symlink pointing to node_modules - try { - const stats = fs.lstatSync(scriptPath); - if (stats.isSymbolicLink()) { - const targetPath = fs.readlinkSync(scriptPath); - if (targetPath.includes('node_modules') || targetPath.includes('@kaitranntt/ccs')) { - return 'npm'; - } - } - } catch (_err) { - // Continue to default - } - - return 'direct'; -} - -/** - * Detect which package manager was used for installation - */ -function detectPackageManager(): 'npm' | 'yarn' | 'pnpm' | 'bun' { - const scriptPath = process.argv[1]; - - // Check if script path contains package manager indicators - if (scriptPath.includes('.pnpm')) return 'pnpm'; - if (scriptPath.includes('yarn')) return 'yarn'; - if (scriptPath.includes('bun')) return 'bun'; - - // Check parent directories for lock files - const binDir = path.dirname(scriptPath); - - let checkDir = binDir; - for (let i = 0; i < 5; i++) { - if (fs.existsSync(path.join(checkDir, 'pnpm-lock.yaml'))) return 'pnpm'; - if (fs.existsSync(path.join(checkDir, 'yarn.lock'))) return 'yarn'; - if (fs.existsSync(path.join(checkDir, 'bun.lockb'))) return 'bun'; - checkDir = path.dirname(checkDir); - } - - // Check if package managers are available on the system - try { - const yarnResult = spawnSync('yarn', ['global', 'list', '--pattern', '@kaitranntt/ccs'], { - encoding: 'utf8', - shell: true, - timeout: 5000, - }); - if (yarnResult.status === 0 && yarnResult.stdout.includes('@kaitranntt/ccs')) { - return 'yarn'; - } - } catch (_err) { - // Continue to next check - } - - try { - const pnpmResult = spawnSync('pnpm', ['list', '-g', '--pattern', '@kaitranntt/ccs'], { - encoding: 'utf8', - shell: true, - timeout: 5000, - }); - if (pnpmResult.status === 0 && pnpmResult.stdout.includes('@kaitranntt/ccs')) { - return 'pnpm'; - } - } catch (_err) { - // Continue to next check - } - - try { - const bunResult = spawnSync('bun', ['pm', 'ls', '-g', '--pattern', '@kaitranntt/ccs'], { - encoding: 'utf8', - shell: true, - timeout: 5000, - }); - if (bunResult.status === 0 && bunResult.stdout.includes('@kaitranntt/ccs')) { - return 'bun'; - } - } catch (_err) { - // Continue to default - } - - return 'npm'; -} - -async function handleUpdateCommand(): Promise { - const { checkForUpdates } = await import('./utils/update-checker'); - - console.log(''); - console.log(colored('Checking for updates...', 'cyan')); - console.log(''); - - const installMethod = detectInstallationMethod(); - const isNpmInstall = installMethod === 'npm'; - - const updateResult = await checkForUpdates(CCS_VERSION, true, installMethod); - - if (updateResult.status === 'check_failed') { - console.log(colored(`[X] ${updateResult.message}`, 'red')); - console.log(''); - console.log(colored('[i] Possible causes:', 'yellow')); - console.log(' - Network connection issues'); - console.log(' - Firewall blocking requests'); - console.log(' - GitHub/npm API temporarily unavailable'); - console.log(''); - console.log('Try again later or update manually:'); - if (isNpmInstall) { - const packageManager = detectPackageManager(); - let manualCommand: string; - - switch (packageManager) { - case 'npm': - manualCommand = 'npm install -g @kaitranntt/ccs@latest'; - break; - case 'yarn': - manualCommand = 'yarn global add @kaitranntt/ccs@latest'; - break; - case 'pnpm': - manualCommand = 'pnpm add -g @kaitranntt/ccs@latest'; - break; - case 'bun': - manualCommand = 'bun add -g @kaitranntt/ccs@latest'; - break; - default: - manualCommand = 'npm install -g @kaitranntt/ccs@latest'; - } - - console.log(colored(` ${manualCommand}`, 'yellow')); - } else { - const isWindows = process.platform === 'win32'; - if (isWindows) { - console.log(colored(' irm ccs.kaitran.ca/install | iex', 'yellow')); - } else { - console.log(colored(' curl -fsSL ccs.kaitran.ca/install | bash', 'yellow')); - } - } - console.log(''); - process.exit(1); - } - - if (updateResult.status === 'no_update') { - let message = `You are already on the latest version (${CCS_VERSION})`; - - switch (updateResult.reason) { - case 'dismissed': - message = `Update dismissed. You are on version ${CCS_VERSION}`; - console.log(colored(`[i] ${message}`, 'yellow')); - break; - case 'cached': - message = `No updates available (cached result). You are on version ${CCS_VERSION}`; - console.log(colored(`[i] ${message}`, 'cyan')); - break; - default: - console.log(colored(`[OK] ${message}`, 'green')); - } - console.log(''); - process.exit(0); - } - - // Update available - console.log( - colored(`[i] Update available: ${updateResult.current} -> ${updateResult.latest}`, 'yellow') - ); - console.log(''); - - if (isNpmInstall) { - const packageManager = detectPackageManager(); - let updateCommand: string; - let updateArgs: string[]; - let cacheCommand: string | null; - let cacheArgs: string[] | null; - - switch (packageManager) { - case 'npm': - updateCommand = 'npm'; - updateArgs = ['install', '-g', '@kaitranntt/ccs@latest']; - cacheCommand = 'npm'; - cacheArgs = ['cache', 'clean', '--force']; - break; - case 'yarn': - updateCommand = 'yarn'; - updateArgs = ['global', 'add', '@kaitranntt/ccs@latest']; - cacheCommand = 'yarn'; - cacheArgs = ['cache', 'clean']; - break; - case 'pnpm': - updateCommand = 'pnpm'; - updateArgs = ['add', '-g', '@kaitranntt/ccs@latest']; - cacheCommand = 'pnpm'; - cacheArgs = ['store', 'prune']; - break; - case 'bun': - updateCommand = 'bun'; - updateArgs = ['add', '-g', '@kaitranntt/ccs@latest']; - cacheCommand = null; - cacheArgs = null; - break; - default: - updateCommand = 'npm'; - updateArgs = ['install', '-g', '@kaitranntt/ccs@latest']; - cacheCommand = 'npm'; - cacheArgs = ['cache', 'clean', '--force']; - } - - console.log(colored(`Updating via ${packageManager}...`, 'cyan')); - console.log(''); - - const performUpdate = (): void => { - const child = spawn(updateCommand, updateArgs, { - stdio: 'inherit', - }); - - child.on('exit', (code) => { - if (code === 0) { - console.log(''); - console.log(colored('[OK] Update successful!', 'green')); - console.log(''); - console.log(`Run ${colored('ccs --version', 'yellow')} to verify`); - console.log(''); - } else { - console.log(''); - console.log(colored('[X] Update failed', 'red')); - console.log(''); - console.log('Try manually:'); - console.log(colored(` ${updateCommand} ${updateArgs.join(' ')}`, 'yellow')); - console.log(''); - } - process.exit(code || 0); - }); - - child.on('error', () => { - console.log(''); - console.log(colored(`[X] Failed to run ${packageManager} update`, 'red')); - console.log(''); - console.log('Try manually:'); - console.log(colored(` ${updateCommand} ${updateArgs.join(' ')}`, 'yellow')); - console.log(''); - process.exit(1); - }); - }; - - if (cacheCommand && cacheArgs) { - console.log(colored('Clearing package cache...', 'cyan')); - const cacheChild = spawn(cacheCommand, cacheArgs, { - stdio: 'inherit', - }); - - cacheChild.on('exit', (code) => { - if (code !== 0) { - console.log(colored('[!] Cache clearing failed, proceeding anyway...', 'yellow')); - } - performUpdate(); - }); - - cacheChild.on('error', () => { - console.log(colored('[!] Cache clearing failed, proceeding anyway...', 'yellow')); - performUpdate(); - }); - } else { - performUpdate(); - } - } else { - // Direct installation - re-run installer - console.log(colored('Updating via installer...', 'cyan')); - console.log(''); - - const isWindows = process.platform === 'win32'; - let command: string; - let args: string[]; - - if (isWindows) { - command = 'powershell.exe'; - args = [ - '-NoProfile', - '-ExecutionPolicy', - 'Bypass', - '-Command', - 'irm ccs.kaitran.ca/install | iex', - ]; - } else { - command = '/bin/bash'; - args = ['-c', 'curl -fsSL ccs.kaitran.ca/install | bash']; - } - - const child = spawn(command, args, { - stdio: 'inherit', - }); - - child.on('exit', (code) => { - if (code === 0) { - console.log(''); - console.log(colored('[OK] Update successful!', 'green')); - console.log(''); - console.log(`Run ${colored('ccs --version', 'yellow')} to verify`); - console.log(''); - } else { - console.log(''); - console.log(colored('[X] Update failed', 'red')); - console.log(''); - console.log('Try manually:'); - if (isWindows) { - console.log(colored(' irm ccs.kaitran.ca/install | iex', 'yellow')); - } else { - console.log(colored(' curl -fsSL ccs.kaitran.ca/install | bash', 'yellow')); - } - console.log(''); - } - process.exit(code || 0); - }); - - child.on('error', () => { - console.log(''); - console.log(colored('[X] Failed to run installer', 'red')); - console.log(''); - console.log('Try manually:'); - if (isWindows) { - console.log(colored(' irm ccs.kaitran.ca/install | iex', 'yellow')); - } else { - console.log(colored(' curl -fsSL ccs.kaitran.ca/install | bash', 'yellow')); - } - console.log(''); - process.exit(1); - }); - } -} - -// ========== Profile Detection ========== - -interface DetectedProfile { - profile: string; - remainingArgs: string[]; -} - -/** - * Smart profile detection - */ -function detectProfile(args: string[]): DetectedProfile { - if (args.length === 0 || args[0].startsWith('-')) { - // No args or first arg is a flag → use default profile - return { profile: 'default', remainingArgs: args }; - } else { - // First arg doesn't start with '-' → treat as profile name - return { profile: args[0], remainingArgs: args.slice(1) }; - } -} - -// ========== GLMT Proxy Execution ========== - -/** - * Execute Claude CLI with embedded proxy (for GLMT profile) - */ -async function execClaudeWithProxy( - claudeCli: string, - profileName: string, - args: string[] -): Promise { - // 1. Read settings to get API key - const settingsPath = getSettingsPath(profileName); - const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); - const apiKey = settings.env.ANTHROPIC_AUTH_TOKEN; - - if (!apiKey || apiKey === 'YOUR_GLM_API_KEY_HERE') { - console.error('[X] GLMT profile requires Z.AI API key'); - console.error(' Edit ~/.ccs/glmt.settings.json and set ANTHROPIC_AUTH_TOKEN'); - process.exit(1); - } - - // Detect verbose flag - const verbose = args.includes('--verbose') || args.includes('-v'); - - // 2. Spawn embedded proxy with verbose flag - const proxyPath = path.join(__dirname, 'glmt', 'glmt-proxy.js'); - const proxyArgs = verbose ? ['--verbose'] : []; - // Use process.execPath for Windows compatibility (CVE-2024-27980) - const proxy = spawn(process.execPath, [proxyPath, ...proxyArgs], { - stdio: ['ignore', 'pipe', verbose ? 'pipe' : 'inherit'], - }); - - // 3. Wait for proxy ready signal (with timeout) - const { ProgressIndicator } = await import('./utils/progress-indicator'); - const spinner = new ProgressIndicator('Starting GLMT proxy'); - spinner.start(); - - let port: number; - try { - port = await new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - reject(new Error('Proxy startup timeout (5s)')); - }, 5000); - - proxy.stdout?.on('data', (data: Buffer) => { - const match = data.toString().match(/PROXY_READY:(\d+)/); - if (match) { - clearTimeout(timeout); - resolve(parseInt(match[1])); - } - }); - - proxy.on('error', (error) => { - clearTimeout(timeout); - reject(error); - }); - - proxy.on('exit', (code) => { - if (code !== 0 && code !== null) { - clearTimeout(timeout); - reject(new Error(`Proxy exited with code ${code}`)); - } - }); - }); - - spinner.succeed(`GLMT proxy ready on port ${port}`); - } catch (error) { - const err = error as Error; - spinner.fail('Failed to start GLMT proxy'); - console.error('[X] Error:', err.message); - console.error(''); - console.error('Possible causes:'); - console.error(' 1. Port conflict (unlikely with random port)'); - console.error(' 2. Node.js permission issue'); - console.error(' 3. Firewall blocking localhost'); - console.error(''); - 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(''); - proxy.kill(); - process.exit(1); - } - - // 4. Spawn Claude CLI with proxy URL - const envVars: NodeJS.ProcessEnv = { - ANTHROPIC_BASE_URL: `http://127.0.0.1:${port}`, - ANTHROPIC_AUTH_TOKEN: apiKey, - ANTHROPIC_MODEL: 'glm-4.6', - }; - - const isWindows = process.platform === 'win32'; - const needsShell = isWindows && /\.(cmd|bat|ps1)$/i.test(claudeCli); - const env = { ...process.env, ...envVars }; - - let claude: ChildProcess; - if (needsShell) { - const cmdString = [claudeCli, ...args].map(escapeShellArg).join(' '); - claude = spawn(cmdString, { - stdio: 'inherit', - windowsHide: true, - shell: true, - env, - }); - } else { - claude = spawn(claudeCli, args, { - stdio: 'inherit', - windowsHide: true, - env, - }); - } - - // 5. Cleanup: kill proxy when Claude exits - claude.on('exit', (code, signal) => { - proxy.kill('SIGTERM'); - if (signal) process.kill(process.pid, signal as NodeJS.Signals); - else process.exit(code || 0); - }); - - claude.on('error', (error) => { - console.error('[X] Claude CLI error:', error); - proxy.kill('SIGTERM'); - process.exit(1); - }); - - // Also handle parent process termination - process.once('SIGTERM', () => { - proxy.kill('SIGTERM'); - claude.kill('SIGTERM'); - }); - - process.once('SIGINT', () => { - proxy.kill('SIGTERM'); - claude.kill('SIGTERM'); - }); -} - -/** - * Handle shell completion installation - */ -async function handleShellCompletionCommand(args: string[]): Promise { - const { ShellCompletionInstaller } = await import('./utils/shell-completion'); - - console.log(colored('Shell Completion Installer', 'bold')); - console.log(''); - - // Parse flags - let targetShell: string | null = null; - if (args.includes('--bash')) targetShell = 'bash'; - else if (args.includes('--zsh')) targetShell = 'zsh'; - else if (args.includes('--fish')) targetShell = 'fish'; - else if (args.includes('--powershell')) targetShell = 'powershell'; - - try { - const installer = new ShellCompletionInstaller(); - const result = installer.install(targetShell as 'bash' | 'zsh' | 'fish' | 'powershell' | null); - - if (result.alreadyInstalled) { - console.log(colored('[OK] Shell completion already installed', 'green')); - console.log(''); - return; - } - - console.log(colored('[OK] Shell completion installed successfully!', 'green')); - console.log(''); - console.log(result.message); - console.log(''); - console.log(colored('To activate:', 'cyan')); - console.log(` ${result.reload}`); - console.log(''); - console.log(colored('Then test:', 'cyan')); - console.log(' ccs # See available profiles'); - console.log(' ccs auth # See auth subcommands'); - console.log(''); - } catch (error) { - const err = error as Error; - console.error(colored('[X] Error:', 'red'), err.message); - console.error(''); - console.error(colored('Usage:', 'yellow')); - console.error(' ccs --shell-completion # Auto-detect shell'); - console.error(' ccs --shell-completion --bash # Install for bash'); - console.error(' ccs --shell-completion --zsh # Install for zsh'); - console.error(' ccs --shell-completion --fish # Install for fish'); - console.error(' ccs --shell-completion --powershell # Install for PowerShell'); - console.error(''); - process.exit(1); - } -} - -// ========== Main Execution ========== - -interface ProfileError extends Error { - profileName?: string; - availableProfiles?: string; - suggestions?: string[]; -} - -async function main(): Promise { - const args = process.argv.slice(2); - - // Special case: version command (check BEFORE profile detection) - const firstArg = args[0]; - if (firstArg === 'version' || firstArg === '--version' || firstArg === '-v') { - handleVersionCommand(); - } - - // Special case: help command - if (firstArg === '--help' || firstArg === '-h' || firstArg === 'help') { - handleHelpCommand(); - return; - } - - // Special case: install command - if (firstArg === '--install') { - handleInstallCommand(); - return; - } - - // Special case: uninstall command - if (firstArg === '--uninstall') { - handleUninstallCommand(); - return; - } - - // Special case: shell completion installer - if (firstArg === '--shell-completion' || firstArg === '-sc') { - await handleShellCompletionCommand(args.slice(1)); - return; - } - - // Special case: doctor command - if (firstArg === 'doctor' || firstArg === '--doctor') { - await handleDoctorCommand(); - return; - } - - // Special case: sync command - if (firstArg === 'sync' || firstArg === '--sync') { - await handleSyncCommand(); - return; - } - - // Special case: update command - if (firstArg === 'update' || firstArg === '--update') { - await handleUpdateCommand(); - return; - } - - // Special case: auth command - if (firstArg === 'auth') { - const AuthCommandsModule = await import('./auth/auth-commands'); - const AuthCommands = AuthCommandsModule.default; - const authCommands = new AuthCommands(); - await authCommands.route(args.slice(1)); - return; - } - - // Special case: headless delegation (-p flag) - if (args.includes('-p') || args.includes('--prompt')) { - const { DelegationHandler } = await import('./delegation/delegation-handler'); - const handler = new DelegationHandler(); - await handler.route(args); - 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(); - } - - // Detect profile - const { profile, remainingArgs } = detectProfile(args); - - // Detect Claude CLI first (needed for all paths) - const claudeCli = detectClaudeCli(); - if (!claudeCli) { - ErrorManager.showClaudeNotFound(); - process.exit(1); - } - - // Use ProfileDetector to determine profile type - const ProfileDetectorModule = await import('./auth/profile-detector'); - const ProfileDetector = ProfileDetectorModule.default; - const InstanceManagerModule = await import('./management/instance-manager'); - const InstanceManager = InstanceManagerModule.default; - const ProfileRegistryModule = await import('./auth/profile-registry'); - const ProfileRegistry = ProfileRegistryModule.default; - - const detector = new ProfileDetector(); - - try { - const profileInfo = detector.detectProfileType(profile); - - if (profileInfo.type === 'settings') { - // Check if this is GLMT profile (requires proxy) - if (profileInfo.name === 'glmt') { - // GLMT FLOW: Settings-based with embedded proxy for thinking support - await execClaudeWithProxy(claudeCli, profileInfo.name, remainingArgs); - } else { - // EXISTING FLOW: Settings-based profile (glm, kimi) - // Use --settings flag (backward compatible) - const expandedSettingsPath = getSettingsPath(profileInfo.name); - execClaude(claudeCli, ['--settings', expandedSettingsPath, ...remainingArgs]); - } - } else if (profileInfo.type === 'account') { - // NEW FLOW: Account-based profile (work, personal) - // All platforms: Use instance isolation with CLAUDE_CONFIG_DIR - const registry = new ProfileRegistry(); - const instanceMgr = new InstanceManager(); - - // Ensure instance exists (lazy init if needed) - const instancePath = instanceMgr.ensureInstance(profileInfo.name); - - // Update last_used timestamp - registry.touchProfile(profileInfo.name); - - // Execute Claude with instance isolation - const envVars: NodeJS.ProcessEnv = { CLAUDE_CONFIG_DIR: instancePath }; - execClaude(claudeCli, remainingArgs, envVars); - } else { - // DEFAULT: No profile configured, use Claude's own defaults - execClaude(claudeCli, remainingArgs); - } - } catch (error) { - const err = error as ProfileError; - // Check if this is a profile not found error with suggestions - if (err.profileName && err.availableProfiles !== undefined) { - const allProfiles = err.availableProfiles.split('\n'); - ErrorManager.showProfileNotFound(err.profileName, allProfiles, err.suggestions); - } else { - console.error(`[X] ${err.message}`); - } - process.exit(1); - } -} - -// Run main -main().catch((error) => { - console.error('Fatal error:', error.message); - process.exit(1); -}); diff --git a/tests/README.md b/tests/README.md index 9047b988..3f0091ed 100644 --- a/tests/README.md +++ b/tests/README.md @@ -2,90 +2,62 @@ ## Organization -- `native/` - Native installation tests only (curl|bash, irm|iex) - - `unix/` - Unix/Linux/macOS native tests (37 tests) - - `windows/` - Windows PowerShell tests -- `npm/` - npm package tests only (39 tests) - - `postinstall.test.js` - Postinstall behavior tests (Section 10) - - `cli.test.js` - CLI argument parsing tests - - `cross-platform.test.js` - Cross-platform compatibility tests - - `special-commands.test.js` - npm package integration tests -- `shared/` - Shared utilities, fixtures, and unit tests - - `helpers.sh` - Bash test utilities and functions - - `test-data.js` - Test data for npm tests - - `fixtures/` - Test configuration files - - `unit/` - Unit tests for helper functions -- `unit/` - Module unit tests (GLMT, delegation) +``` +tests/ +├── unit/ # Module unit tests (Mocha) +│ ├── glmt/ # GLMT transformer tests +│ └── delegation/ # Delegation module tests +├── npm/ # npm package tests (Mocha) +├── native/ # Native installation tests (bash/PowerShell) +│ ├── unix/ # Unix/Linux/macOS tests +│ └── windows/ # Windows PowerShell tests +├── integration/ # Integration tests (manual execution) +└── shared/ # Shared utilities + ├── fixtures/ # Test configuration and environment + ├── unit/ # Helper function tests + ├── helpers.sh # Bash test utilities + └── test-data.js # Test data for npm tests +``` ## Running Tests ```bash -bun run test # All tests (177 total) -bun run test:unit # Unit tests only (138) -bun run test:npm # npm package tests (39) -bun run test:native # Native installation tests (37) -bun run test:edge-cases # Master orchestrator (backward compatible) +bun run test # All automated tests (unit + npm) +bun run test:unit # Unit tests only (Mocha) +bun run test:npm # npm package tests (Mocha) +bun run test:native # Native Unix tests (bash) ``` -## Test Structure +## Test Categories -### Native Tests (`native/`) -Test the traditional installation methods where CCS is installed via: -- Unix/Linux/macOS: `curl | bash` → tests `lib/ccs` -- Windows: `irm | iex` (PowerShell) → tests `lib/ccs.ps1` - -These tests use bash/PowerShell scripts and cover Sections 1-9 from the original edge-cases.sh. - -**Files**: -- `native/unix/edge-cases.sh` - 37 native Unix tests -- `native/windows/edge-cases.ps1` - Windows PowerShell tests -- `native/unix/install.sh` - Unix installation tests -- `native/windows/install.ps1` - Windows installation tests +### Unit Tests (`unit/`) +Module-level tests using Mocha framework: +- `unit/glmt/` - GLMT transformer, SSE parser, delta accumulator +- `unit/delegation/` - Permission mode, session manager, result formatter ### npm Tests (`npm/`) -Test the npm package installation where CCS is installed via: -- npm: `npm install -g @kaitranntt/ccs` → tests `bin/ccs.js` +npm package functionality tests using Mocha: +- `postinstall.test.js` - Postinstall behavior +- `cli.test.js` - CLI argument parsing +- `cross-platform.test.js` - Cross-platform compatibility +- `special-commands.test.js` - Integration tests -These tests use Node.js/mocha framework and include Section 10 (postinstall) plus CLI tests. +### Native Tests (`native/`) +Installation tests for curl|bash (Unix) and irm|iex (Windows): +- `native/unix/edge-cases.sh` - Unix edge case tests +- `native/windows/edge-cases.ps1` - Windows edge case tests -**Files**: -- `npm/postinstall.test.js` - 6 postinstall behavior tests (Section 10) -- `npm/cli.test.js` - 15 CLI argument parsing tests -- `npm/cross-platform.test.js` - 13 cross-platform compatibility tests -- `npm/special-commands.test.js` - 5 integration tests for npm package +### Integration Tests (`integration/`) +Manual execution tests for specific scenarios: +- `token-counting-test.js` - Token counting validation +- `z-ai-streaming-test.js` - Z.AI streaming +- `glmt-integration-test.sh` - GLMT integration +- `symlink-chain-test.sh` - Symlink chain handling +- `ux-integration-test.sh` - CLI UX integration -### Shared Resources (`shared/`) -Common test code, data, and helper functions shared across test suites to avoid duplication. +## Adding New Tests -**Files**: -- `shared/helpers.sh` - Bash test utilities and functions -- `shared/test-data.js` - Test data for npm tests -- `shared/fixtures/` - Test configuration files -- `shared/unit/` - Unit tests for helper functions -- `unit/glmt/` - GLMT transformer unit tests -- `unit/delegation/` - Delegation module unit tests - -## Test Counts - -| Test Type | Count | Location | -|-----------|-------|----------| -| Native Unix | 37 | `native/unix/` | -| npm Package | 39 | `npm/` | -| Unit Tests | 138 | `shared/unit/`, `unit/glmt/`, `unit/delegation/` | -| **Total** | **177** | **All suites** | - -## Backward Compatibility - -All existing commands still work: -- `bash tests/edge-cases.sh` - Master orchestrator (runs all tests) -- `npm test` - Now runs comprehensive test suite -- No breaking changes to existing workflows - -## Migration Notes - -This restructure solves the original problem where Section 10 (npm postinstall tests) was buried in the native `edge-cases.sh` file. Now: -- ✅ Clear separation: npm tests in `npm/`, native in `native/` -- ✅ Targeted execution: `npm run test:npm` vs `npm run test:native` -- ✅ Better organization: Obvious where to add new tests -- ✅ DRY principle: Shared utilities in `shared/` -- ✅ Increased coverage: From 41 to 177 tests \ No newline at end of file +- **Unit tests**: Add to `unit//` using Mocha + Node.js assert +- **npm tests**: Add to `npm/` using Mocha +- **Native tests**: Add to `native/unix/` or `native/windows/` +- **Integration tests**: Add to `integration/` diff --git a/tests/ux-integration-test.sh b/tests/integration/ux-integration-test.sh similarity index 100% rename from tests/ux-integration-test.sh rename to tests/integration/ux-integration-test.sh diff --git a/tests/npm/cli.test.js b/tests/npm/cli.test.js index acb31a57..00cb3dea 100644 --- a/tests/npm/cli.test.js +++ b/tests/npm/cli.test.js @@ -1,7 +1,7 @@ const assert = require('assert'); const { execSync } = require('child_process'); const path = require('path'); -const { createTestEnvironment } = require('../fixtures/test-environment'); +const { createTestEnvironment } = require('../shared/fixtures/test-environment'); describe('npm CLI', () => { const ccsPath = path.join(__dirname, '..', '..', 'dist', 'ccs.js'); diff --git a/tests/npm/postinstall.test.js b/tests/npm/postinstall.test.js index 438a0870..0032e117 100644 --- a/tests/npm/postinstall.test.js +++ b/tests/npm/postinstall.test.js @@ -1,7 +1,7 @@ const assert = require('assert'); const { execSync } = require('child_process'); const path = require('path'); -const { createTestEnvironment } = require('../fixtures/test-environment'); +const { createTestEnvironment } = require('../shared/fixtures/test-environment'); describe('npm postinstall', () => { let testEnv; diff --git a/tests/fixtures/test-environment.js b/tests/shared/fixtures/test-environment.js similarity index 100% rename from tests/fixtures/test-environment.js rename to tests/shared/fixtures/test-environment.js