Skip to content

Commit 26ed777

Browse files
committed
clean bun handler
1 parent 43e3277 commit 26ed777

File tree

1 file changed

+72
-95
lines changed

1 file changed

+72
-95
lines changed

bin/handlers/bun-handler.ts

Lines changed: 72 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import {
88
setupCommandArguments,
99
safeExec,
1010
safeExecSync,
11-
// createLogLevelHandler,
1211
} from '../utils/shared.js';
1312

1413
const COMMANDS_SECTION_RE = /^Commands:\s*$/i;
@@ -22,39 +21,51 @@ const CONTINUATION_COMMAND_RE = /^\s{12,}([a-z][a-z0-9-]*)\s+(.+)$/;
2221
const EMPTY_LINE_FOLLOWED_BY_NON_COMMAND_RE = /^\s+[a-z]/;
2322
const DESCRIPTION_SPLIT_RE = /\s{2,}/;
2423
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+
}
2536

2637
const bunOptionHandlers: OptionHandlers = {
2738
...commonOptionHandlers,
2839

29-
silent: function (complete) {
40+
silent(complete) {
3041
complete('true', 'Enable silent mode');
3142
complete('false', 'Disable silent mode');
3243
},
3344

34-
backend: function (complete) {
45+
backend(complete) {
3546
complete('clonefile', 'Clone files (default, fastest)');
3647
complete('hardlink', 'Use hard links');
3748
complete('symlink', 'Use symbolic links');
3849
complete('copyfile', 'Copy files');
3950
},
4051

41-
linker: function (complete) {
52+
linker(complete) {
4253
complete('isolated', 'Isolated linker strategy');
4354
complete('hoisted', 'Hoisted linker strategy');
4455
},
4556

46-
omit: function (complete) {
57+
omit(complete) {
4758
complete('dev', 'Omit devDependencies');
4859
complete('optional', 'Omit optionalDependencies');
4960
complete('peer', 'Omit peerDependencies');
5061
},
5162

52-
shell: function (complete) {
63+
shell(complete) {
5364
complete('bun', 'Use Bun shell');
5465
complete('system', 'Use system shell');
5566
},
5667

57-
'unhandled-rejections': function (complete) {
68+
'unhandled-rejections'(complete) {
5869
complete('strict', 'Strict unhandled rejection handling');
5970
complete('throw', 'Throw on unhandled rejections');
6071
complete('warn', 'Warn on unhandled rejections');
@@ -63,61 +74,54 @@ const bunOptionHandlers: OptionHandlers = {
6374
},
6475
};
6576

77+
/** ---------- Commands ---------- */
6678
export function parseBunHelp(helpText: string): Record<string, string> {
67-
const helpLines = stripAnsiEscapes(helpText).split(/\r?\n/);
68-
69-
let startIndex = -1;
70-
for (let i = 0; i < helpLines.length; i++) {
71-
if (COMMANDS_SECTION_RE.test(helpLines[i].trim())) {
72-
startIndex = i + 1;
73-
break;
74-
}
75-
}
79+
const lines = toLines(helpText);
7680

81+
const startIndex = findSectionStart(lines, COMMANDS_SECTION_RE);
7782
if (startIndex === -1) return {};
7883

7984
const commands: Record<string, string> = {};
8085

81-
// parse bun's unique command format
82-
for (let i = startIndex; i < helpLines.length; i++) {
83-
const line = helpLines[i];
86+
for (let i = startIndex; i < lines.length; i++) {
87+
const line = lines[i];
8488

8589
// stop when we hit Flags section or empty line followed by non-command content
8690
if (
8791
FLAGS_SECTION_RE.test(line.trim()) ||
8892
(line.trim() === '' &&
89-
i + 1 < helpLines.length &&
90-
!helpLines[i + 1].match(EMPTY_LINE_FOLLOWED_BY_NON_COMMAND_RE))
91-
)
93+
i + 1 < lines.length &&
94+
!lines[i + 1].match(EMPTY_LINE_FOLLOWED_BY_NON_COMMAND_RE))
95+
) {
9296
break;
97+
}
9398

94-
// Skip empty lines
95-
if (line.trim() === '') continue;
99+
if (!line.trim()) continue;
96100

97-
const mainCommandMatch = line.match(MAIN_COMMAND_RE);
98-
if (mainCommandMatch) {
99-
const [, command, rest] = mainCommandMatch;
101+
// main command row
102+
const main = line.match(MAIN_COMMAND_RE);
103+
if (main) {
104+
const [, command, rest] = main;
100105
if (COMMAND_VALIDATION_RE.test(command)) {
101106
const parts = rest.split(DESCRIPTION_SPLIT_RE);
102-
let description = parts[parts.length - 1];
107+
let desc = parts[parts.length - 1];
103108

104-
// If the last part starts with capital letter, it's likely the description
105-
if (description && CAPITAL_LETTER_START_RE.test(description)) {
106-
commands[command] = description.trim();
109+
if (desc && CAPITAL_LETTER_START_RE.test(desc)) {
110+
commands[command] = desc.trim();
107111
} else if (parts.length > 1) {
108-
for (const part of parts) {
109-
if (CAPITAL_LETTER_START_RE.test(part)) {
110-
commands[command] = part.trim();
112+
for (const p of parts) {
113+
if (CAPITAL_LETTER_START_RE.test(p)) {
114+
commands[command] = p.trim();
111115
break;
112116
}
113117
}
114118
}
115119
}
116120
}
117121

118-
const continuationMatch = line.match(CONTINUATION_COMMAND_RE);
119-
if (continuationMatch) {
120-
const [, command, description] = continuationMatch;
122+
const cont = line.match(CONTINUATION_COMMAND_RE);
123+
if (cont) {
124+
const [, command, description] = cont;
121125
if (COMMAND_VALIDATION_RE.test(command)) {
122126
commands[command] = description.trim();
123127
}
@@ -134,90 +138,63 @@ export async function getBunCommandsFromMainHelp(): Promise<
134138
return output ? parseBunHelp(output) : {};
135139
}
136140

137-
// Parse bun options from help text
138141
export function parseBunOptions(
139142
helpText: string,
140143
{ flagsOnly = true }: { flagsOnly?: boolean } = {}
141144
): ParsedOption[] {
142-
const helpLines = stripAnsiEscapes(helpText).split(/\r?\n/);
143-
const optionsOut: ParsedOption[] = [];
144-
145-
// Find the Flags: section
146-
let optionsStartIndex = -1;
147-
for (let i = 0; i < helpLines.length; i++) {
148-
if (FLAGS_SECTION_RE.test(helpLines[i].trim())) {
149-
optionsStartIndex = i + 1;
150-
break;
151-
}
152-
}
153-
154-
if (optionsStartIndex === -1) return [];
145+
const lines = toLines(helpText);
146+
const out: ParsedOption[] = [];
155147

156-
// Parse bun's flag format
157-
for (let i = optionsStartIndex; i < helpLines.length; i++) {
158-
const line = helpLines[i];
148+
const start = findSectionStart(lines, FLAGS_SECTION_RE);
149+
if (start === -1) return out;
159150

160-
// Stop at examples or other sections
151+
for (let i = start; i < lines.length; i++) {
152+
const line = lines[i];
161153
if (SECTION_END_RE.test(line.trim())) break;
162154

163-
// Parse option lines like: " -c, --config=<val> Specify path to config file"
164-
const optionMatch = line.match(BUN_OPTION_RE);
155+
const m = line.match(BUN_OPTION_RE);
156+
if (!m) continue;
165157

166-
if (optionMatch) {
167-
const [, short, long, desc] = optionMatch;
158+
const [, short, long, desc] = m;
159+
const takesValue = line.includes('=<'); // bun shows value as --opt=<val>
160+
if (flagsOnly && takesValue) continue;
168161

169-
// Check if this option takes a value (has =<val>)
170-
const takesValue = line.includes('=<');
171-
172-
if (flagsOnly && takesValue) continue;
173-
174-
optionsOut.push({
175-
short: short || undefined,
176-
long,
177-
desc: desc.trim(),
178-
});
179-
}
162+
out.push({
163+
short: short || undefined,
164+
long,
165+
desc: desc.trim(),
166+
});
180167
}
181168

182-
return optionsOut;
169+
return out;
183170
}
184171

185-
// load dynamic options synchronously when requested
186172
function loadBunOptionsSync(cmd: LazyCommand, command: string): void {
187173
const output = safeExecSync(`bun ${command} --help`);
188174
if (!output) return;
189175

190-
const allOptions = parseBunOptions(output, { flagsOnly: false });
176+
const options = parseBunOptions(output, { flagsOnly: false });
191177

192-
for (const { long, short, desc } of allOptions) {
193-
const alreadyDefined = cmd.optionsRaw?.get?.(long);
194-
if (!alreadyDefined) {
195-
const handler = bunOptionHandlers[long];
196-
if (handler) {
197-
cmd.option(long, desc, handler, short);
198-
} else {
199-
cmd.option(long, desc, short);
200-
}
201-
}
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);
202185
}
203186
}
204187

205188
export async function setupBunCompletions(
206189
completion: PackageManagerCompletion
207190
): Promise<void> {
208191
try {
209-
const commandsWithDescriptions = await getBunCommandsFromMainHelp();
210-
211-
for (const [command, description] of Object.entries(
212-
commandsWithDescriptions
213-
)) {
214-
const cmd = completion.command(command, description);
215-
216-
// Setup common argument patterns
217-
setupCommandArguments(cmd, command, 'bun');
192+
const commands = await getBunCommandsFromMainHelp();
218193

219-
// Setup lazy option loading
220-
setupLazyOptionLoading(cmd, command, 'bun', loadBunOptionsSync);
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);
221198
}
222-
} catch (_err) {}
199+
} catch {}
223200
}

0 commit comments

Comments
 (0)