Skip to content

Commit ee4a0eb

Browse files
committed
big update
1 parent 4239c73 commit ee4a0eb

File tree

4 files changed

+507
-262
lines changed

4 files changed

+507
-262
lines changed

bin/handlers/bun-handler.ts

Lines changed: 237 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,241 @@
11
import type { PackageManagerCompletion } from '../package-manager-completion.js';
2+
import { stripAnsiEscapes, type ParsedOption } from '../utils/text-utils.js';
3+
import {
4+
LazyCommand,
5+
OptionHandlers,
6+
commonOptionHandlers,
7+
setupLazyOptionLoading,
8+
setupCommandArguments,
9+
safeExec,
10+
safeExecSync,
11+
// createLogLevelHandler,
12+
} from '../utils/package-manager-base.js';
13+
14+
// regex patterns to avoid recompilation in loops
15+
const COMMANDS_SECTION_RE = /^Commands:\s*$/i;
16+
const FLAGS_SECTION_RE = /^Flags:\s*$/i;
17+
const SECTION_END_RE = /^(Examples|Full documentation|Learn more)/i;
18+
const COMMAND_VALIDATION_RE = /^[a-z][a-z0-9-]*$/;
19+
const BUN_OPTION_RE =
20+
/^\s*(?:-([a-zA-Z]),?\s*)?--([a-z][a-z0-9-]*)(?:=<[^>]+>)?\s+(.+)$/;
21+
// const NON_INDENTED_LINE_RE = /^\s/;
22+
23+
// bun-specific completion handlers
24+
const bunOptionHandlers: OptionHandlers = {
25+
// Use common handlers
26+
...commonOptionHandlers,
27+
28+
// bun doesn't have traditional log levels, but has verbose/silent
29+
silent: function (complete) {
30+
complete('true', 'Enable silent mode');
31+
complete('false', 'Disable silent mode');
32+
},
33+
34+
backend: function (complete) {
35+
// From bun help: "clonefile" (default), "hardlink", "symlink", "copyfile"
36+
complete('clonefile', 'Clone files (default, fastest)');
37+
complete('hardlink', 'Use hard links');
38+
complete('symlink', 'Use symbolic links');
39+
complete('copyfile', 'Copy files');
40+
},
41+
42+
linker: function (complete) {
43+
// From bun help: "isolated" or "hoisted"
44+
complete('isolated', 'Isolated linker strategy');
45+
complete('hoisted', 'Hoisted linker strategy');
46+
},
47+
48+
omit: function (complete) {
49+
// From bun help: 'dev', 'optional', or 'peer'
50+
complete('dev', 'Omit devDependencies');
51+
complete('optional', 'Omit optionalDependencies');
52+
complete('peer', 'Omit peerDependencies');
53+
},
54+
55+
shell: function (complete) {
56+
// From bun help: 'bun' or 'system'
57+
complete('bun', 'Use Bun shell');
58+
complete('system', 'Use system shell');
59+
},
60+
61+
'unhandled-rejections': function (complete) {
62+
// From bun help: "strict", "throw", "warn", "none", or "warn-with-error-code"
63+
complete('strict', 'Strict unhandled rejection handling');
64+
complete('throw', 'Throw on unhandled rejections');
65+
complete('warn', 'Warn on unhandled rejections');
66+
complete('none', 'Ignore unhandled rejections');
67+
complete('warn-with-error-code', 'Warn with error code');
68+
},
69+
};
70+
71+
// Parse bun help text to extract commands and their descriptions
72+
export function parseBunHelp(helpText: string): Record<string, string> {
73+
const helpLines = stripAnsiEscapes(helpText).split(/\r?\n/);
74+
75+
// Find "Commands:" section
76+
let startIndex = -1;
77+
for (let i = 0; i < helpLines.length; i++) {
78+
if (COMMANDS_SECTION_RE.test(helpLines[i].trim())) {
79+
startIndex = i + 1;
80+
break;
81+
}
82+
}
83+
84+
if (startIndex === -1) return {};
85+
86+
const commands: Record<string, string> = {};
87+
88+
// Parse bun's unique command format
89+
for (let i = startIndex; i < helpLines.length; i++) {
90+
const line = helpLines[i];
91+
92+
// Stop when we hit Flags section or empty line followed by non-command content
93+
if (
94+
FLAGS_SECTION_RE.test(line.trim()) ||
95+
(line.trim() === '' &&
96+
i + 1 < helpLines.length &&
97+
!helpLines[i + 1].match(/^\s+[a-z]/))
98+
)
99+
break;
100+
101+
// Skip empty lines
102+
if (line.trim() === '') continue;
103+
104+
// Handle different bun command formats:
105+
// Format 1: " run ./my-script.ts Execute a file with Bun"
106+
// Format 2: " lint Run a package.json script" (continuation)
107+
// Format 3: " install Install dependencies for a package.json (bun i)"
108+
109+
// Try to match command at start of line (2 spaces)
110+
const mainCommandMatch = line.match(/^ ([a-z][a-z0-9-]*)\s+(.+)$/);
111+
if (mainCommandMatch) {
112+
const [, command, rest] = mainCommandMatch;
113+
if (COMMAND_VALIDATION_RE.test(command)) {
114+
// Extract description - find the last part that looks like a description
115+
// Split by multiple spaces and take the last part that contains letters
116+
const parts = rest.split(/\s{2,}/);
117+
let description = parts[parts.length - 1];
118+
119+
// If the last part starts with a capital letter, it's likely the description
120+
if (description && /^[A-Z]/.test(description)) {
121+
commands[command] = description.trim();
122+
} else if (parts.length > 1) {
123+
// Otherwise, look for the first part that starts with a capital
124+
for (const part of parts) {
125+
if (/^[A-Z]/.test(part)) {
126+
commands[command] = part.trim();
127+
break;
128+
}
129+
}
130+
}
131+
}
132+
}
133+
134+
// Handle continuation lines (12+ spaces)
135+
const continuationMatch = line.match(/^\s{12,}([a-z][a-z0-9-]*)\s+(.+)$/);
136+
if (continuationMatch) {
137+
const [, command, description] = continuationMatch;
138+
if (COMMAND_VALIDATION_RE.test(command)) {
139+
commands[command] = description.trim();
140+
}
141+
}
142+
}
143+
144+
return commands;
145+
}
146+
147+
// Get bun commands from the main help output
148+
export async function getBunCommandsFromMainHelp(): Promise<
149+
Record<string, string>
150+
> {
151+
const output = await safeExec('bun --help');
152+
return output ? parseBunHelp(output) : {};
153+
}
154+
155+
// Parse bun options from help text
156+
export function parseBunOptions(
157+
helpText: string,
158+
{ flagsOnly = true }: { flagsOnly?: boolean } = {}
159+
): ParsedOption[] {
160+
const helpLines = stripAnsiEscapes(helpText).split(/\r?\n/);
161+
const optionsOut: ParsedOption[] = [];
162+
163+
// Find the Flags: section
164+
let optionsStartIndex = -1;
165+
for (let i = 0; i < helpLines.length; i++) {
166+
if (FLAGS_SECTION_RE.test(helpLines[i].trim())) {
167+
optionsStartIndex = i + 1;
168+
break;
169+
}
170+
}
171+
172+
if (optionsStartIndex === -1) return [];
173+
174+
// Parse bun's flag format
175+
for (let i = optionsStartIndex; i < helpLines.length; i++) {
176+
const line = helpLines[i];
177+
178+
// Stop at examples or other sections
179+
if (SECTION_END_RE.test(line.trim())) break;
180+
181+
// Parse option lines like: " -c, --config=<val> Specify path to config file"
182+
const optionMatch = line.match(BUN_OPTION_RE);
183+
184+
if (optionMatch) {
185+
const [, short, long, desc] = optionMatch;
186+
187+
// Check if this option takes a value (has =<val>)
188+
const takesValue = line.includes('=<');
189+
190+
if (flagsOnly && takesValue) continue;
191+
192+
optionsOut.push({
193+
short: short || undefined,
194+
long,
195+
desc: desc.trim(),
196+
});
197+
}
198+
}
199+
200+
return optionsOut;
201+
}
202+
203+
// Load dynamic options synchronously when requested
204+
function loadBunOptionsSync(cmd: LazyCommand, command: string): void {
205+
const output = safeExecSync(`bun ${command} --help`);
206+
if (!output) return;
207+
208+
const allOptions = parseBunOptions(output, { flagsOnly: false });
209+
210+
for (const { long, short, desc } of allOptions) {
211+
const alreadyDefined = cmd.optionsRaw?.get?.(long);
212+
if (!alreadyDefined) {
213+
const handler = bunOptionHandlers[long];
214+
if (handler) {
215+
cmd.option(long, desc, handler, short);
216+
} else {
217+
cmd.option(long, desc, short);
218+
}
219+
}
220+
}
221+
}
2222

3223
export async function setupBunCompletions(
4224
completion: PackageManagerCompletion
5-
): Promise<void> {}
225+
): Promise<void> {
226+
try {
227+
const commandsWithDescriptions = await getBunCommandsFromMainHelp();
228+
229+
for (const [command, description] of Object.entries(
230+
commandsWithDescriptions
231+
)) {
232+
const cmd = completion.command(command, description);
233+
234+
// Setup common argument patterns
235+
setupCommandArguments(cmd, command, 'bun');
236+
237+
// Setup lazy option loading
238+
setupLazyOptionLoading(cmd, command, 'bun', loadBunOptionsSync);
239+
}
240+
} catch (_err) {}
241+
}

0 commit comments

Comments
 (0)