Skip to content

Commit 1f49934

Browse files
committed
add bun handler
1 parent 578f633 commit 1f49934

File tree

1 file changed

+196
-1
lines changed

1 file changed

+196
-1
lines changed

bin/handlers/bun-handler.ts

Lines changed: 196 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,200 @@
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+
} from '../utils/shared.js';
12+
13+
const COMMANDS_SECTION_RE = /^Commands:\s*$/i;
14+
const FLAGS_SECTION_RE = /^Flags:\s*$/i;
15+
const SECTION_END_RE = /^(Examples|Full documentation|Learn more)/i;
16+
const COMMAND_VALIDATION_RE = /^[a-z][a-z0-9-]*$/;
17+
const BUN_OPTION_RE =
18+
/^\s*(?:-([a-zA-Z]),?\s*)?--([a-z][a-z0-9-]*)(?:=<[^>]+>)?\s+(.+)$/;
19+
const MAIN_COMMAND_RE = /^ ([a-z][a-z0-9-]*)\s+(.+)$/;
20+
const CONTINUATION_COMMAND_RE = /^\s{12,}([a-z][a-z0-9-]*)\s+(.+)$/;
21+
const EMPTY_LINE_FOLLOWED_BY_NON_COMMAND_RE = /^\s+[a-z]/;
22+
const DESCRIPTION_SPLIT_RE = /\s{2,}/;
23+
const CAPITAL_LETTER_START_RE = /^[A-Z]/;
24+
const LINE_SPLIT_RE = /\r?\n/;
25+
26+
function toLines(text: string): string[] {
27+
return stripAnsiEscapes(text).split(LINE_SPLIT_RE);
28+
}
29+
30+
function findSectionStart(lines: string[], header: RegExp): number {
31+
for (let i = 0; i < lines.length; i++) {
32+
if (header.test(lines[i].trim())) return i + 1;
33+
}
34+
return -1;
35+
}
36+
37+
const bunOptionHandlers: OptionHandlers = {
38+
...commonOptionHandlers,
39+
40+
silent(complete) {
41+
complete('true', 'Enable silent mode');
42+
complete('false', 'Disable silent mode');
43+
},
44+
45+
backend(complete) {
46+
complete('clonefile', 'Clone files (default, fastest)');
47+
complete('hardlink', 'Use hard links');
48+
complete('symlink', 'Use symbolic links');
49+
complete('copyfile', 'Copy files');
50+
},
51+
52+
linker(complete) {
53+
complete('isolated', 'Isolated linker strategy');
54+
complete('hoisted', 'Hoisted linker strategy');
55+
},
56+
57+
omit(complete) {
58+
complete('dev', 'Omit devDependencies');
59+
complete('optional', 'Omit optionalDependencies');
60+
complete('peer', 'Omit peerDependencies');
61+
},
62+
63+
shell(complete) {
64+
complete('bun', 'Use Bun shell');
65+
complete('system', 'Use system shell');
66+
},
67+
68+
'unhandled-rejections'(complete) {
69+
complete('strict', 'Strict unhandled rejection handling');
70+
complete('throw', 'Throw on unhandled rejections');
71+
complete('warn', 'Warn on unhandled rejections');
72+
complete('none', 'Ignore unhandled rejections');
73+
complete('warn-with-error-code', 'Warn with error code');
74+
},
75+
};
76+
77+
/** ---------- Commands ---------- */
78+
export function parseBunHelp(helpText: string): Record<string, string> {
79+
const lines = toLines(helpText);
80+
81+
const startIndex = findSectionStart(lines, COMMANDS_SECTION_RE);
82+
if (startIndex === -1) return {};
83+
84+
const commands: Record<string, string> = {};
85+
86+
for (let i = startIndex; i < lines.length; i++) {
87+
const line = lines[i];
88+
89+
// stop when we hit Flags section or empty line followed by non-command content
90+
if (
91+
FLAGS_SECTION_RE.test(line.trim()) ||
92+
(line.trim() === '' &&
93+
i + 1 < lines.length &&
94+
!lines[i + 1].match(EMPTY_LINE_FOLLOWED_BY_NON_COMMAND_RE))
95+
) {
96+
break;
97+
}
98+
99+
if (!line.trim()) continue;
100+
101+
// main command row
102+
const main = line.match(MAIN_COMMAND_RE);
103+
if (main) {
104+
const [, command, rest] = main;
105+
if (COMMAND_VALIDATION_RE.test(command)) {
106+
const parts = rest.split(DESCRIPTION_SPLIT_RE);
107+
let desc = parts[parts.length - 1];
108+
109+
if (desc && CAPITAL_LETTER_START_RE.test(desc)) {
110+
commands[command] = desc.trim();
111+
} else if (parts.length > 1) {
112+
for (const p of parts) {
113+
if (CAPITAL_LETTER_START_RE.test(p)) {
114+
commands[command] = p.trim();
115+
break;
116+
}
117+
}
118+
}
119+
}
120+
}
121+
122+
const cont = line.match(CONTINUATION_COMMAND_RE);
123+
if (cont) {
124+
const [, command, description] = cont;
125+
if (COMMAND_VALIDATION_RE.test(command)) {
126+
commands[command] = description.trim();
127+
}
128+
}
129+
}
130+
131+
return commands;
132+
}
133+
134+
export async function getBunCommandsFromMainHelp(): Promise<
135+
Record<string, string>
136+
> {
137+
const output = await safeExec('bun --help');
138+
return output ? parseBunHelp(output) : {};
139+
}
140+
141+
export function parseBunOptions(
142+
helpText: string,
143+
{ flagsOnly = true }: { flagsOnly?: boolean } = {}
144+
): ParsedOption[] {
145+
const lines = toLines(helpText);
146+
const out: ParsedOption[] = [];
147+
148+
const start = findSectionStart(lines, FLAGS_SECTION_RE);
149+
if (start === -1) return out;
150+
151+
for (let i = start; i < lines.length; i++) {
152+
const line = lines[i];
153+
if (SECTION_END_RE.test(line.trim())) break;
154+
155+
const m = line.match(BUN_OPTION_RE);
156+
if (!m) continue;
157+
158+
const [, short, long, desc] = m;
159+
const takesValue = line.includes('=<'); // bun shows value as --opt=<val>
160+
if (flagsOnly && takesValue) continue;
161+
162+
out.push({
163+
short: short || undefined,
164+
long,
165+
desc: desc.trim(),
166+
});
167+
}
168+
169+
return out;
170+
}
171+
172+
function loadBunOptionsSync(cmd: LazyCommand, command: string): void {
173+
const output = safeExecSync(`bun ${command} --help`);
174+
if (!output) return;
175+
176+
const options = parseBunOptions(output, { flagsOnly: false });
177+
178+
for (const { long, short, desc } of options) {
179+
const exists = cmd.optionsRaw?.get?.(long);
180+
if (exists) continue;
181+
182+
const handler = bunOptionHandlers[long];
183+
if (handler) cmd.option(long, desc, handler, short);
184+
else cmd.option(long, desc, short);
185+
}
186+
}
2187

3188
export async function setupBunCompletions(
4189
completion: PackageManagerCompletion
5-
): Promise<void> {}
190+
): Promise<void> {
191+
try {
192+
const commands = await getBunCommandsFromMainHelp();
193+
194+
for (const [command, description] of Object.entries(commands)) {
195+
const c = completion.command(command, description);
196+
setupCommandArguments(c, command, 'bun');
197+
setupLazyOptionLoading(c, command, 'bun', loadBunOptionsSync);
198+
}
199+
} catch {}
200+
}

0 commit comments

Comments
 (0)