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; interface InstallResult { success: boolean; alreadyInstalled?: boolean; message?: string; reload?: string; } /** * Shell Completion Installer * Auto-configures shell completion for bash, zsh, fish, PowerShell */ export class ShellCompletionInstaller { private homeDir: string; private ccsDir: string; private completionDir: string; private scriptsDir: string; constructor() { this.homeDir = os.homedir(); this.ccsDir = getCcsDir(); this.completionDir = path.join(this.ccsDir, 'completions'); this.scriptsDir = path.join(__dirname, '../../scripts/completion'); } /** * Detect current shell */ detectShell(): ShellType { const shell = process.env.SHELL || ''; if (shell.includes('bash')) return 'bash'; if (shell.includes('zsh')) return 'zsh'; if (shell.includes('fish')) return 'fish'; if (process.platform === 'win32') return 'powershell'; return null; } /** * Ensure completion files are in ~/.ccs/completions/ */ ensureCompletionFiles(): void { if (!fs.existsSync(this.completionDir)) { fs.mkdirSync(this.completionDir, { recursive: true }); } // Copy completion scripts const files = ['ccs.bash', 'ccs.zsh', 'ccs.fish', 'ccs.ps1']; files.forEach((file) => { const src = path.join(this.scriptsDir, file); const dest = path.join(this.completionDir, file); if (fs.existsSync(src)) { fs.copyFileSync(src, dest); } }); } /** * Safely create directory, checking for file conflicts */ private ensureDirectory(dirPath: string): void { if (fs.existsSync(dirPath)) { const stat = fs.statSync(dirPath); if (!stat.isDirectory()) { throw new Error( `Cannot create directory: ${dirPath} exists but is a file.\n` + `Please remove or rename this file and try again.` ); } // Directory exists, nothing to do return; } // Check parent directories recursively const parentDir = path.dirname(dirPath); if (parentDir !== dirPath) { this.ensureDirectory(parentDir); } // Create the directory fs.mkdirSync(dirPath); } /** * Install bash completion */ private installBash(force = false): InstallResult { const rcFile = path.join(this.homeDir, '.bashrc'); const completionPath = path.join(this.completionDir, 'ccs.bash'); if (!fs.existsSync(completionPath)) { throw new Error('Completion file not found. Please reinstall CCS.'); } const marker = '# CCS shell completion'; const sourceCmd = `source "${completionPath}"`; const block = `\n${marker}\n${sourceCmd}\n`; // Check if already installed if (fs.existsSync(rcFile)) { const content = fs.readFileSync(rcFile, 'utf8'); if (content.includes(marker)) { if (!force) { return { success: true, alreadyInstalled: true }; } // Force: completion files already updated by ensureCompletionFiles() return { success: true, message: `Updated completion files in ${this.completionDir}`, reload: 'source ~/.bashrc', }; } } // Append to .bashrc fs.appendFileSync(rcFile, block); return { success: true, message: `Added to ${rcFile}`, reload: 'source ~/.bashrc', }; } /** * Install zsh completion */ private installZsh(force = false): InstallResult { const rcFile = path.join(this.homeDir, '.zshrc'); const completionPath = path.join(this.completionDir, 'ccs.zsh'); const zshCompDir = path.join(this.homeDir, '.zsh', 'completion'); if (!fs.existsSync(completionPath)) { throw new Error('Completion file not found. Please reinstall CCS.'); } // Create zsh completion directory (with file conflict checking) this.ensureDirectory(zshCompDir); // Copy to zsh completion directory (always update for force) const destFile = path.join(zshCompDir, '_ccs'); fs.copyFileSync(completionPath, destFile); const marker = '# CCS shell completion'; const setupCmds = ['fpath=(~/.zsh/completion $fpath)', 'autoload -Uz compinit && compinit']; const block = `\n${marker}\n${setupCmds.join('\n')}\n`; // Check if already installed if (fs.existsSync(rcFile)) { const content = fs.readFileSync(rcFile, 'utf8'); if (content.includes(marker)) { if (!force) { return { success: true, alreadyInstalled: true }; } // Force: files already updated above return { success: true, message: `Updated ${destFile}`, reload: 'source ~/.zshrc', }; } } // Append to .zshrc fs.appendFileSync(rcFile, block); return { success: true, message: `Added to ${rcFile}`, reload: 'source ~/.zshrc', }; } /** * Install fish completion */ private installFish(force = false): InstallResult { const completionPath = path.join(this.completionDir, 'ccs.fish'); const fishCompDir = path.join(this.homeDir, '.config', 'fish', 'completions'); if (!fs.existsSync(completionPath)) { throw new Error('Completion file not found. Please reinstall CCS.'); } // Create fish completion directory (with file conflict checking) this.ensureDirectory(fishCompDir); // Copy to fish completion directory (fish auto-loads from here) const destFile = path.join(fishCompDir, 'ccs.fish'); // Check if already installed if (fs.existsSync(destFile) && !force) { return { success: true, alreadyInstalled: true }; } fs.copyFileSync(completionPath, destFile); return { success: true, message: `Installed to ${destFile}`, reload: 'Fish auto-loads completions (no reload needed)', }; } /** * Install PowerShell completion */ private installPowerShell(force = false): InstallResult { const profilePath = process.env.PROFILE || path.join(this.homeDir, 'Documents', 'PowerShell', 'Microsoft.PowerShell_profile.ps1'); const completionPath = path.join(this.completionDir, 'ccs.ps1'); if (!fs.existsSync(completionPath)) { throw new Error('Completion file not found. Please reinstall CCS.'); } const marker = '# CCS shell completion'; const sourceCmd = `. "${completionPath.replace(/\\/g, '\\\\')}"`; const block = `\n${marker}\n${sourceCmd}\n`; // Create profile directory if needed (with file conflict checking) const profileDir = path.dirname(profilePath); this.ensureDirectory(profileDir); // Check if already installed if (fs.existsSync(profilePath)) { const content = fs.readFileSync(profilePath, 'utf8'); if (content.includes(marker)) { if (!force) { return { success: true, alreadyInstalled: true }; } // Force: completion files already updated by ensureCompletionFiles() return { success: true, message: `Updated completion files in ${this.completionDir}`, reload: '. $PROFILE', }; } } // Append to PowerShell profile fs.appendFileSync(profilePath, block); return { success: true, message: `Added to ${profilePath}`, reload: '. $PROFILE', }; } /** * Install for detected or specified shell */ install(shell: ShellType = null, options: { force?: boolean } = {}): InstallResult { const targetShell = shell || this.detectShell(); const { force = false } = options; if (!targetShell) { throw new Error( 'Could not detect shell. Please specify: --bash, --zsh, --fish, or --powershell' ); } // Ensure completion files exist (always copy latest) this.ensureCompletionFiles(); // Install for target shell switch (targetShell) { case 'bash': return this.installBash(force); case 'zsh': return this.installZsh(force); case 'fish': return this.installFish(force); case 'powershell': return this.installPowerShell(force); default: throw new Error(`Unsupported shell: ${targetShell}`); } } }