flake-update-20260505
1/**
2 * Bash shell completion provider.
3 *
4 * Uses bash's native completion system by running a script that sets up
5 * COMP_* environment variables and calls the registered completion function.
6 *
7 * Philosophy: Only provide completions if the user has bash-completion available.
8 */
9
10import type { AutocompleteItem } from "@mariozechner/pi-tui";
11import { spawnSync } from "node:child_process";
12import * as fs from "node:fs";
13import * as path from "node:path";
14import { fileURLToPath } from "node:url";
15import type { CompletionResult, ShellCompletionProvider } from "./types.js";
16
17const __dirname = path.dirname(fileURLToPath(import.meta.url));
18const COMPLETE_SCRIPT = path.join(__dirname, "scripts", "bash-complete.bash");
19
20/**
21 * Check if bash-completion is available.
22 * We check for the presence of completion scripts in standard locations.
23 */
24let completionCheckCache: boolean | null = null;
25
26function userHasBashCompletions(bashPath: string): boolean {
27 if (completionCheckCache !== null) {
28 return completionCheckCache;
29 }
30
31 try {
32 // Check if bash-completion framework or git completion exists
33 const result = spawnSync(
34 bashPath,
35 [
36 "-c",
37 `
38 for f in /usr/share/bash-completion/bash_completion /etc/bash_completion /opt/homebrew/etc/bash_completion /opt/homebrew/share/bash-completion/bash_completion; do
39 [[ -f "$f" ]] && { echo yes; exit 0; }
40 done
41 # Also check for individual completion files
42 for f in /opt/homebrew/etc/bash_completion.d/git* /usr/share/bash-completion/completions/git; do
43 [[ -f "$f" ]] && { echo yes; exit 0; }
44 done
45 echo no
46 `,
47 ],
48 {
49 encoding: "utf-8",
50 timeout: 500,
51 }
52 );
53
54 completionCheckCache = result.stdout?.trim() === "yes";
55 return completionCheckCache;
56 } catch {
57 completionCheckCache = false;
58 return false;
59 }
60}
61
62/**
63 * Get completions using bash's native completion system.
64 */
65export function getBashCompletions(
66 commandLine: string,
67 cwd: string,
68 bashPath: string
69): CompletionResult | null {
70 // Check if bash completions are available
71 if (!userHasBashCompletions(bashPath)) {
72 return null;
73 }
74
75 // Check if completion script exists
76 if (!fs.existsSync(COMPLETE_SCRIPT)) {
77 return null;
78 }
79
80 // Extract prefix
81 const trimmed = commandLine.trimStart();
82 let prefix = "";
83 if (!trimmed.endsWith(" ")) {
84 const words = trimmed.split(/\s+/);
85 prefix = words[words.length - 1] || "";
86 }
87
88 try {
89 const result = spawnSync(bashPath, [COMPLETE_SCRIPT, commandLine, cwd], {
90 encoding: "utf-8",
91 timeout: 500,
92 maxBuffer: 1024 * 100,
93 cwd,
94 });
95
96 if (result.error || !result.stdout) {
97 return null;
98 }
99
100 const items: AutocompleteItem[] = result.stdout
101 .trim()
102 .split("\n")
103 .filter(Boolean)
104 .map((line) => {
105 // Remove trailing space that bash completion adds
106 const value = line.trimEnd();
107 return { value, label: value };
108 });
109
110 if (items.length === 0) {
111 return null;
112 }
113
114 return {
115 items: items.slice(0, 30),
116 prefix,
117 };
118 } catch {
119 return null;
120 }
121}
122
123export const bashCompletionProvider: ShellCompletionProvider = {
124 getCompletions: getBashCompletions,
125};