main
  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};