UNPKG

@fission-ai/openspec

Version:

AI-native system for spec-driven development

257 lines 10.7 kB
import ora from 'ora'; import { CompletionFactory } from '../core/completions/factory.js'; import { COMMAND_REGISTRY } from '../core/completions/command-registry.js'; import { detectShell } from '../utils/shell-detection.js'; import { CompletionProvider } from '../core/completions/completion-provider.js'; import { getArchivedChangeIds } from '../utils/item-discovery.js'; /** * Command for managing shell completions for OpenSpec CLI */ export class CompletionCommand { completionProvider; constructor() { this.completionProvider = new CompletionProvider(); } /** * Resolve shell parameter or exit with error * * @param shell - The shell parameter (may be undefined) * @param operationName - Name of the operation (for error messages) * @returns Resolved shell or null if should exit */ resolveShellOrExit(shell, operationName) { const normalizedShell = this.normalizeShell(shell); if (!normalizedShell) { const detectionResult = detectShell(); if (detectionResult.shell && CompletionFactory.isSupported(detectionResult.shell)) { return detectionResult.shell; } // Shell was detected but not supported if (detectionResult.detected && !detectionResult.shell) { console.error(`Error: Shell '${detectionResult.detected}' is not supported yet. Currently supported: ${CompletionFactory.getSupportedShells().join(', ')}`); process.exitCode = 1; return null; } // No shell specified and cannot auto-detect console.error('Error: Could not auto-detect shell. Please specify shell explicitly.'); console.error(`Usage: openspec completion ${operationName} [shell]`); console.error(`Currently supported: ${CompletionFactory.getSupportedShells().join(', ')}`); process.exitCode = 1; return null; } if (!CompletionFactory.isSupported(normalizedShell)) { console.error(`Error: Shell '${normalizedShell}' is not supported yet. Currently supported: ${CompletionFactory.getSupportedShells().join(', ')}`); process.exitCode = 1; return null; } return normalizedShell; } /** * Generate completion script and output to stdout * * @param options - Options for generation (shell type) */ async generate(options = {}) { const shell = this.resolveShellOrExit(options.shell, 'generate'); if (!shell) return; await this.generateForShell(shell); } /** * Install completion script to the appropriate location * * @param options - Options for installation (shell type, verbose output) */ async install(options = {}) { const shell = this.resolveShellOrExit(options.shell, 'install'); if (!shell) return; await this.installForShell(shell, options.verbose || false); } /** * Uninstall completion script from the installation location * * @param options - Options for uninstallation (shell type, yes flag) */ async uninstall(options = {}) { const shell = this.resolveShellOrExit(options.shell, 'uninstall'); if (!shell) return; await this.uninstallForShell(shell, options.yes || false); } /** * Generate completion script for a specific shell */ async generateForShell(shell) { const generator = CompletionFactory.createGenerator(shell); const script = generator.generate(COMMAND_REGISTRY); console.log(script); } /** * Install completion script for a specific shell */ async installForShell(shell, verbose) { const generator = CompletionFactory.createGenerator(shell); const installer = CompletionFactory.createInstaller(shell); const spinner = ora(`Installing ${shell} completion script...`).start(); try { // Generate the completion script const script = generator.generate(COMMAND_REGISTRY); // Install it const result = await installer.install(script); spinner.stop(); if (result.success) { console.log(`✓ ${result.message}`); if (verbose && result.installedPath) { console.log(` Installed to: ${result.installedPath}`); if (result.backupPath) { console.log(` Backup created: ${result.backupPath}`); } // Check if any shell config was updated const configWasUpdated = result.zshrcConfigured || result.bashrcConfigured || result.profileConfigured; if (configWasUpdated) { const configPaths = { zsh: '~/.zshrc', bash: '~/.bashrc', fish: '~/.config/fish/config.fish', powershell: '$PROFILE', }; const configPath = configPaths[shell] || 'config file'; console.log(` ${configPath} configured automatically`); } } // Display warnings if present if (result.warnings && result.warnings.length > 0) { console.log(''); for (const warning of result.warnings) { console.log(warning); } } // Print instructions (only shown if .zshrc wasn't auto-configured) if (result.instructions && result.instructions.length > 0) { console.log(''); for (const instruction of result.instructions) { console.log(instruction); } } else { // Check if any shell config was updated (InstallationResult has: zshrcConfigured, bashrcConfigured, profileConfigured) const configWasUpdated = result.zshrcConfigured || result.bashrcConfigured || result.profileConfigured; if (configWasUpdated) { console.log(''); // Shell-specific reload instructions const reloadCommands = { zsh: 'exec zsh', bash: 'exec bash', fish: 'exec fish', powershell: '. $PROFILE', }; const reloadCmd = reloadCommands[shell] || `restart your ${shell} shell`; console.log(`Restart your shell or run: ${reloadCmd}`); } } } else { console.error(`✗ ${result.message}`); process.exitCode = 1; } } catch (error) { spinner.stop(); console.error(`✗ Failed to install completion script: ${error instanceof Error ? error.message : String(error)}`); process.exitCode = 1; } } /** * Uninstall completion script for a specific shell */ async uninstallForShell(shell, skipConfirmation) { const installer = CompletionFactory.createInstaller(shell); // Prompt for confirmation unless --yes flag is provided if (!skipConfirmation) { const { confirm } = await import('@inquirer/prompts'); // Get shell-specific config file path const configPaths = { zsh: '~/.zshrc', bash: '~/.bashrc', fish: 'Fish configuration', // Fish doesn't modify profile, just removes script file powershell: '$PROFILE', }; const configPath = configPaths[shell] || `${shell} configuration`; const confirmed = await confirm({ message: `Remove OpenSpec configuration from ${configPath}?`, default: false, }); if (!confirmed) { console.log('Uninstall cancelled.'); return; } } const spinner = ora(`Uninstalling ${shell} completion script...`).start(); try { const result = await installer.uninstall(); spinner.stop(); if (result.success) { console.log(`✓ ${result.message}`); } else { console.error(`✗ ${result.message}`); process.exitCode = 1; } } catch (error) { spinner.stop(); console.error(`✗ Failed to uninstall completion script: ${error instanceof Error ? error.message : String(error)}`); process.exitCode = 1; } } /** * Output machine-readable completion data for shell consumption * Format: tab-separated "id\tdescription" per line * * @param options - Options specifying completion type */ async complete(options) { const type = options.type.toLowerCase(); try { switch (type) { case 'changes': { const changeIds = await this.completionProvider.getChangeIds(); for (const id of changeIds) { console.log(`${id}\tactive change`); } break; } case 'specs': { const specIds = await this.completionProvider.getSpecIds(); for (const id of specIds) { console.log(`${id}\tspecification`); } break; } case 'archived-changes': { const archivedIds = await getArchivedChangeIds(); for (const id of archivedIds) { console.log(`${id}\tarchived change`); } break; } default: // Invalid type - silently exit with no output for graceful shell completion failure process.exitCode = 1; break; } } catch { // Silently fail for graceful shell completion experience process.exitCode = 1; } } /** * Normalize shell parameter to lowercase */ normalizeShell(shell) { return shell?.toLowerCase(); } } //# sourceMappingURL=completion.js.map