/ src / completion-fast.ts
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  }