completion-fast.ts
1 /** 2 * Lightweight manifest-based completion for the fast path. 3 * 4 * This module MUST NOT import registry, discovery, or any heavy module. 5 * It only reads pre-compiled cli-manifest.json files synchronously. 6 */ 7 8 import * as fs from 'node:fs'; 9 import { 10 BUILTIN_COMMANDS, 11 bashCompletionScript, 12 zshCompletionScript, 13 fishCompletionScript, 14 } from './completion-shared.js'; 15 16 interface ManifestCompletionEntry { 17 site: string; 18 name: string; 19 aliases?: string[]; 20 } 21 22 /** 23 * Returns true only if ALL manifest files exist and are readable. 24 * If any source lacks a manifest (e.g. user adapters without a compiled manifest), 25 * the fast path must not be used — otherwise those adapters would silently 26 * disappear from completion results. 27 */ 28 export function hasAllManifests(manifestPaths: string[]): boolean { 29 for (const p of manifestPaths) { 30 try { 31 fs.accessSync(p); 32 } catch { 33 return false; 34 } 35 } 36 return manifestPaths.length > 0; 37 } 38 39 /** 40 * Lightweight completion that reads directly from manifest JSON files, 41 * bypassing full CLI discovery and adapter loading. 42 */ 43 export function getCompletionsFromManifest(words: string[], cursor: number, manifestPaths: string[]): string[] { 44 const entries = loadManifestEntries(manifestPaths); 45 if (entries === null) { 46 return []; 47 } 48 49 if (cursor <= 1) { 50 const sites = new Set<string>(); 51 for (const entry of entries) { 52 sites.add(entry.site); 53 } 54 return [...BUILTIN_COMMANDS, ...sites].sort(); 55 } 56 57 const site = words[0]; 58 if (BUILTIN_COMMANDS.includes(site)) { 59 return []; 60 } 61 62 if (cursor === 2) { 63 const subcommands: string[] = []; 64 for (const entry of entries) { 65 if (entry.site === site) { 66 subcommands.push(entry.name); 67 if (entry.aliases?.length) subcommands.push(...entry.aliases); 68 } 69 } 70 return [...new Set(subcommands)].sort(); 71 } 72 73 return []; 74 } 75 76 // ── Shell script generators (re-exported from shared, no registry dependency) ─────── 77 78 const SHELL_SCRIPTS: Record<string, () => string> = { 79 bash: bashCompletionScript, 80 zsh: zshCompletionScript, 81 fish: fishCompletionScript, 82 }; 83 84 /** 85 * Print completion script for the given shell. Returns true if handled, false if unknown shell. 86 */ 87 export function printCompletionScriptFast(shell: string): boolean { 88 const gen = SHELL_SCRIPTS[shell]; 89 if (!gen) return false; 90 process.stdout.write(gen()); 91 return true; 92 } 93 94 function loadManifestEntries(manifestPaths: string[]): ManifestCompletionEntry[] | null { 95 const entries: ManifestCompletionEntry[] = []; 96 let found = false; 97 for (const manifestPath of manifestPaths) { 98 try { 99 const raw = fs.readFileSync(manifestPath, 'utf-8'); 100 const manifest = JSON.parse(raw) as ManifestCompletionEntry[]; 101 entries.push(...manifest); 102 found = true; 103 } catch { /* skip missing/unreadable */ } 104 } 105 return found ? entries : null; 106 }