@get-bb/plugin-sdk 0.4.101 → 0.4.104

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,6 +1,802 @@
1
1
  // src/backend-contract.ts
2
2
  var PLUGIN_CLI_OUTPUT_MAX_BYTES = 1024 * 1024;
3
3
 
4
+ // src/cli-spec.ts
5
+ var PluginCliError = class extends Error {
6
+ code;
7
+ hint;
8
+ exitCode;
9
+ constructor(message, options) {
10
+ super(message);
11
+ this.name = "CliError";
12
+ this.code = options?.code ?? "command_failed";
13
+ this.hint = options?.hint;
14
+ this.exitCode = options?.exitCode ?? 1;
15
+ }
16
+ };
17
+ function cliCommand(command) {
18
+ return command;
19
+ }
20
+ var DURATION_UNIT_MS = {
21
+ ms: 1,
22
+ s: 1e3,
23
+ m: 6e4,
24
+ h: 36e5,
25
+ d: 864e5
26
+ };
27
+ var DURATION_UNIT_LABEL = {
28
+ ms: "milliseconds",
29
+ s: "seconds",
30
+ m: "minutes",
31
+ h: "hours",
32
+ d: "days"
33
+ };
34
+ var DURATION_PATTERN = /^(\d+(?:\.\d+)?)(ms|s|m|h|d)?$/;
35
+ var UsageError = class extends Error {
36
+ failure;
37
+ constructor(failure) {
38
+ super(failure.message);
39
+ this.name = "CliUsageError";
40
+ this.failure = failure;
41
+ }
42
+ };
43
+ function editDistance(left, right) {
44
+ const columns = right.length + 1;
45
+ let previous = Array.from({ length: columns }, (_unused, index) => index);
46
+ for (let row = 1; row <= left.length; row += 1) {
47
+ const current = [row];
48
+ for (let column = 1; column < columns; column += 1) {
49
+ const substitution = (previous[column - 1] ?? 0) + (left[row - 1] === right[column - 1] ? 0 : 1);
50
+ const deletion = (previous[column] ?? 0) + 1;
51
+ const insertion = (current[column - 1] ?? 0) + 1;
52
+ current.push(Math.min(substitution, deletion, insertion));
53
+ }
54
+ previous = current;
55
+ }
56
+ return previous[columns - 1] ?? Math.max(left.length, right.length);
57
+ }
58
+ function nearest(input, candidates) {
59
+ let best = null;
60
+ for (const candidate of candidates) {
61
+ const score = editDistance(input, candidate);
62
+ const related = score <= 2 || input.length >= 3 && candidate.startsWith(input) || candidate.length >= 3 && input.startsWith(candidate);
63
+ if (!related) continue;
64
+ if (best === null || score < best.score) best = { name: candidate, score };
65
+ }
66
+ return best?.name ?? null;
67
+ }
68
+ function isRequired(option) {
69
+ return option.type !== "boolean" && option.required === true;
70
+ }
71
+ function isRepeatable(option) {
72
+ if (option.type === "string" || option.type === "enum") {
73
+ return option.repeatable === true;
74
+ }
75
+ return false;
76
+ }
77
+ function defaultOf(option) {
78
+ return option.type === "boolean" ? void 0 : option.default;
79
+ }
80
+ function optionPlaceholder(option) {
81
+ if (option.placeholder !== void 0) return option.placeholder;
82
+ switch (option.type) {
83
+ case "boolean":
84
+ return "";
85
+ case "enum":
86
+ return option.values.join("|");
87
+ case "integer":
88
+ return `${option.min}-${option.max}`;
89
+ case "duration":
90
+ return "duration";
91
+ default:
92
+ return "value";
93
+ }
94
+ }
95
+ function optionSignature(name, option) {
96
+ const placeholder = optionPlaceholder(option);
97
+ return placeholder ? `--${name} <${placeholder}>` : `--${name}`;
98
+ }
99
+ function visibleOptions(command) {
100
+ return Object.entries(command.options ?? {}).filter(
101
+ ([, option]) => option.hidden !== true
102
+ );
103
+ }
104
+ function visibleCommands(spec) {
105
+ return Object.entries(spec.commands).filter(
106
+ ([, command]) => command.hidden !== true
107
+ );
108
+ }
109
+ function usageLine(cliName, path, command) {
110
+ const parts = [path ? `bb ${cliName} ${path}` : `bb ${cliName}`];
111
+ for (const positional of command.positionals ?? []) {
112
+ const token = positional.variadic ? `<${positional.name}...>` : `<${positional.name}>`;
113
+ parts.push(positional.required ? token : `[${token}]`);
114
+ }
115
+ for (const [name, option] of visibleOptions(command)) {
116
+ const signature = optionSignature(name, option);
117
+ if (isRequired(option)) parts.push(signature);
118
+ else if (isRepeatable(option)) parts.push(`[${signature}]...`);
119
+ else parts.push(`[${signature}]`);
120
+ }
121
+ if (command.passthrough === true) parts.push("-- <command...>");
122
+ return parts.join(" ");
123
+ }
124
+ function padColumns(rows) {
125
+ const width = rows.reduce(
126
+ (longest, [left]) => left.length <= 34 ? Math.max(longest, left.length) : longest,
127
+ 0
128
+ );
129
+ return rows.map(
130
+ ([left, right]) => ` ${left.padEnd(width)} ${right}`.trimEnd()
131
+ );
132
+ }
133
+ function optionHelpRow(name, option) {
134
+ const markers = [];
135
+ if (isRequired(option)) markers.push("required");
136
+ if (isRepeatable(option)) markers.push("repeatable");
137
+ const fallback = defaultOf(option);
138
+ if (fallback !== void 0) markers.push(`default: ${fallback}`);
139
+ if (option.stdin === true) markers.push(`or --${name}-stdin from the bb CLI`);
140
+ const suffix = markers.length > 0 ? ` (${markers.join(", ")})` : "";
141
+ return [optionSignature(name, option), `${option.description}${suffix}`];
142
+ }
143
+ function constraintRows(command) {
144
+ return (command.constraints ?? []).map((constraint) => {
145
+ if (constraint.kind === "requires") {
146
+ return [
147
+ `--${constraint.option}`,
148
+ `requires ${constraint.needs.map((need) => `--${need}`).join(", ")}`
149
+ ];
150
+ }
151
+ const label = constraint.kind === "exactly-one" ? "Exactly one of:" : constraint.kind === "at-most-one" ? "At most one of:" : "At least one of:";
152
+ return [label, constraint.options.map((name) => `--${name}`).join(", ")];
153
+ });
154
+ }
155
+ function optionsSection(command) {
156
+ const options = visibleOptions(command);
157
+ if (options.length === 0) return [];
158
+ const rows = options.map(
159
+ ([name, option]) => optionHelpRow(name, option)
160
+ );
161
+ rows.push(["--help, -h", "Show this help and exit"]);
162
+ return ["", "Options:", ...padColumns(rows)];
163
+ }
164
+ function positionalsSection(command) {
165
+ const positionals = command.positionals ?? [];
166
+ if (positionals.length === 0) return [];
167
+ return [
168
+ "",
169
+ "Arguments:",
170
+ ...padColumns(
171
+ positionals.map((positional) => [
172
+ positional.variadic ? `<${positional.name}...>` : `<${positional.name}>`,
173
+ `${positional.description}${positional.required === true ? " (required)" : ""}`
174
+ ])
175
+ )
176
+ ];
177
+ }
178
+ function rulesSection(command) {
179
+ const rows = constraintRows(command);
180
+ if (rows.length === 0) return [];
181
+ return ["", "Rules:", ...padColumns(rows)];
182
+ }
183
+ function commandRows(spec) {
184
+ return visibleCommands(spec).map(([path, command]) => [
185
+ `bb ${spec.name} ${path}`,
186
+ command.summary
187
+ ]);
188
+ }
189
+ function renderTopLevelHelp(spec) {
190
+ const lines = [`bb ${spec.name} \u2014 ${spec.summary}`];
191
+ if (spec.description !== void 0) lines.push("", spec.description);
192
+ lines.push("", "Usage:");
193
+ if (spec.root) lines.push(` ${usageLine(spec.name, "", spec.root)}`);
194
+ lines.push(` bb ${spec.name} <command> [options]`);
195
+ const rows = commandRows(spec);
196
+ if (rows.length > 0) lines.push("", "Commands:", ...padColumns(rows));
197
+ if (spec.root) {
198
+ lines.push(...positionalsSection(spec.root), ...optionsSection(spec.root));
199
+ }
200
+ lines.push(
201
+ "",
202
+ `Run \`bb ${spec.name} <command> --help\` for a command's arguments and options.`
203
+ );
204
+ return `${lines.join("\n")}
205
+ `;
206
+ }
207
+ function renderHelp(spec, resolved) {
208
+ if (resolved === null) return renderTopLevelHelp(spec);
209
+ const { path, command } = resolved;
210
+ const lines = [
211
+ `bb ${spec.name} ${path} \u2014 ${command.summary}`,
212
+ "",
213
+ "Usage:",
214
+ ` ${usageLine(spec.name, path, command)}`
215
+ ];
216
+ if (command.description !== void 0) lines.push("", command.description);
217
+ lines.push(
218
+ ...positionalsSection(command),
219
+ ...optionsSection(command),
220
+ ...rulesSection(command)
221
+ );
222
+ return `${lines.join("\n")}
223
+ `;
224
+ }
225
+ function commandGroupRows(spec, words) {
226
+ return visibleCommands(spec).filter(([path]) => {
227
+ const pathWords = path.split(" ");
228
+ return pathWords.length > words.length && words.every((word, index) => pathWords[index] === word);
229
+ }).map(([path, command]) => [`bb ${spec.name} ${path}`, command.summary]);
230
+ }
231
+ function renderGroupHelp(spec, words, rows) {
232
+ const group = `bb ${spec.name} ${words.join(" ")}`;
233
+ return `${[
234
+ `${group} \u2014 commands`,
235
+ "",
236
+ "Usage:",
237
+ ` ${group} <command> [options]`,
238
+ "",
239
+ "Commands:",
240
+ ...padColumns(rows),
241
+ "",
242
+ `Run \`${group} <command> --help\` for a command's arguments and options.`
243
+ ].join("\n")}
244
+ `;
245
+ }
246
+ function commandListBlock(spec) {
247
+ return ["Commands:", ...padColumns(commandRows(spec))].join("\n");
248
+ }
249
+ function leadingWords(argv) {
250
+ const words = [];
251
+ for (const token of argv) {
252
+ if (token.startsWith("-")) break;
253
+ words.push(token);
254
+ }
255
+ return words;
256
+ }
257
+ function resolveCommand(spec, words) {
258
+ const candidates = [];
259
+ for (const [path, command] of Object.entries(spec.commands)) {
260
+ candidates.push({ path, words: path.split(" ") });
261
+ for (const alias of command.aliases ?? []) {
262
+ candidates.push({ path, words: alias.split(" ") });
263
+ }
264
+ }
265
+ candidates.sort((left, right) => right.words.length - left.words.length);
266
+ for (const candidate of candidates) {
267
+ if (candidate.words.length > words.length) continue;
268
+ if (!candidate.words.every((word, index) => words[index] === word)) {
269
+ continue;
270
+ }
271
+ const command = spec.commands[candidate.path];
272
+ if (command) {
273
+ return {
274
+ path: candidate.path,
275
+ command,
276
+ words: candidate.words.length
277
+ };
278
+ }
279
+ }
280
+ return null;
281
+ }
282
+ function unknownCommandError(spec, words) {
283
+ const suggestions = /* @__PURE__ */ new Map();
284
+ for (const [path, command] of Object.entries(spec.commands)) {
285
+ for (const alias of command.aliases ?? []) suggestions.set(alias, path);
286
+ for (const near of command.suggestFor ?? []) suggestions.set(near, path);
287
+ }
288
+ const visiblePaths = visibleCommands(spec).map(([path]) => path.split(" "));
289
+ let shared = 0;
290
+ while (shared < words.length && visiblePaths.some(
291
+ (path) => path.length > shared + 1 && path.slice(0, shared + 1).every((word, index) => words[index] === word)
292
+ )) {
293
+ shared += 1;
294
+ }
295
+ const prefix = words.slice(0, shared);
296
+ const siblings = [
297
+ ...new Set(
298
+ visiblePaths.filter(
299
+ (path) => path.length > shared && prefix.every((word, index) => path[index] === word)
300
+ ).map((path) => path[shared] ?? "")
301
+ )
302
+ ];
303
+ const attemptedWord = words[shared];
304
+ if (attemptedWord === void 0) {
305
+ return new UsageError({
306
+ code: "missing_command",
307
+ message: `missing command after '${prefix.join(" ")}'`,
308
+ hint: `Expected one of: ${siblings.join(", ")}`,
309
+ detail: commandListBlock(spec),
310
+ exitCode: spec.usageErrorExitCode ?? 1
311
+ });
312
+ }
313
+ const attempted = [...prefix, attemptedWord].join(" ");
314
+ const nearestSibling = nearest(attemptedWord, siblings);
315
+ const suggestion = suggestions.get(attempted) ?? suggestions.get(attemptedWord) ?? (nearestSibling === null ? null : [...prefix, nearestSibling].join(" "));
316
+ return new UsageError({
317
+ code: "unknown_command",
318
+ message: `unknown command '${attempted}'`,
319
+ ...suggestion === null || suggestion === void 0 ? {} : { hint: `Did you mean ${suggestion}?` },
320
+ detail: commandListBlock(spec),
321
+ exitCode: spec.usageErrorExitCode ?? 1
322
+ });
323
+ }
324
+ function usageFailure(spec, path, command, failure) {
325
+ return new UsageError({
326
+ ...failure,
327
+ usage: `Usage:
328
+ ${usageLine(spec.name, path, command)}`,
329
+ exitCode: spec.usageErrorExitCode ?? 1
330
+ });
331
+ }
332
+ function namesDeclaredOption(token, byName, byShort) {
333
+ if (token === "--" || token === "--help" || token === "-h") return true;
334
+ if (token.startsWith("--")) {
335
+ const spelled = token.slice(2).split("=")[0] ?? "";
336
+ return byName.has(spelled) || spelled.endsWith("-stdin") && byName.has(spelled.slice(0, -"-stdin".length));
337
+ }
338
+ if (token.startsWith("-") && token.length > 1) {
339
+ return byShort.has(token.slice(1).split("=")[0] ?? "");
340
+ }
341
+ return false;
342
+ }
343
+ function tokenize(spec, path, command, argv) {
344
+ const options = command.options ?? {};
345
+ const byName = /* @__PURE__ */ new Map();
346
+ const byShort = /* @__PURE__ */ new Map();
347
+ for (const [name, option] of Object.entries(options)) {
348
+ byName.set(name, name);
349
+ for (const alias of option.aliases ?? []) byName.set(alias, name);
350
+ if (option.short !== void 0) {
351
+ byShort.set(normalizeShort(option.short), name);
352
+ }
353
+ }
354
+ const parsed = {
355
+ values: /* @__PURE__ */ new Map(),
356
+ flags: /* @__PURE__ */ new Set(),
357
+ positionals: [],
358
+ passthrough: []
359
+ };
360
+ const fail = (failure) => usageFailure(spec, path, command, failure);
361
+ for (let index = 0; index < argv.length; index += 1) {
362
+ const token = argv[index] ?? "";
363
+ if (token === "--") {
364
+ const rest = argv.slice(index + 1);
365
+ if (command.passthrough === true) parsed.passthrough.push(...rest);
366
+ else parsed.positionals.push(...rest);
367
+ break;
368
+ }
369
+ if (!token.startsWith("-") || token === "-" || NEGATIVE_NUMBER_PATTERN.test(token) && !byShort.has(token.slice(1))) {
370
+ parsed.positionals.push(token);
371
+ continue;
372
+ }
373
+ const long = token.startsWith("--");
374
+ const body = long ? token.slice(2) : token.slice(1);
375
+ const separator = body.indexOf("=");
376
+ const spelled = separator === -1 ? body : body.slice(0, separator);
377
+ const inline = separator === -1 ? void 0 : body.slice(separator + 1);
378
+ const name = long ? byName.get(spelled) : byShort.get(spelled);
379
+ const option = name === void 0 ? void 0 : options[name];
380
+ if (name === void 0 || option === void 0) {
381
+ const stdinTarget = spelled.endsWith("-stdin") ? byName.get(spelled.slice(0, -"-stdin".length)) : void 0;
382
+ if (stdinTarget !== void 0 && options[stdinTarget]?.stdin === true) {
383
+ throw fail({
384
+ code: "invalid_value",
385
+ message: `--${spelled} is read by the bb CLI, which rewrites it to --${stdinTarget} <value> before this command runs`,
386
+ hint: `Pass --${stdinTarget} <value> here, or run this through the bb CLI.`
387
+ });
388
+ }
389
+ const suggestion = nearest(
390
+ spelled,
391
+ visibleOptions(command).map(([optionName]) => optionName)
392
+ );
393
+ throw fail({
394
+ code: "unknown_option",
395
+ message: `unknown option '${long ? "--" : "-"}${spelled}'`,
396
+ ...suggestion === null ? {} : { hint: `Did you mean --${suggestion}?` }
397
+ });
398
+ }
399
+ if (option.type === "boolean") {
400
+ if (inline !== void 0 && inline !== "true" && inline !== "false") {
401
+ throw fail({
402
+ code: "invalid_value",
403
+ message: `invalid value '${inline}' for --${name}. Expected one of: true, false`
404
+ });
405
+ }
406
+ if (parsed.flags.has(name)) {
407
+ throw fail({
408
+ code: "unexpected_argument",
409
+ message: `--${name} was given more than once`
410
+ });
411
+ }
412
+ if (inline !== "false") parsed.flags.add(name);
413
+ continue;
414
+ }
415
+ const existing = parsed.values.get(name);
416
+ if (existing !== void 0 && !isRepeatable(option)) {
417
+ throw fail({
418
+ code: "unexpected_argument",
419
+ message: `--${name} was given more than once; it takes a single value`
420
+ });
421
+ }
422
+ if (inline !== void 0) {
423
+ parsed.values.set(name, [...existing ?? [], inline]);
424
+ continue;
425
+ }
426
+ const next = argv[index + 1];
427
+ if (next === void 0 || namesDeclaredOption(next, byName, byShort)) {
428
+ throw fail({
429
+ code: "invalid_value",
430
+ message: `--${name} requires a value`,
431
+ hint: `Write --${name}=<value> when the value is itself an option name.`
432
+ });
433
+ }
434
+ parsed.values.set(name, [...existing ?? [], next]);
435
+ index += 1;
436
+ }
437
+ return parsed;
438
+ }
439
+ function parseIntegerValue(name, option, raw) {
440
+ const expected = `Expected an integer between ${option.min} and ${option.max}`;
441
+ if (!/^-?\d+$/.test(raw)) {
442
+ throw new Error(`invalid value '${raw}' for --${name}. ${expected}`);
443
+ }
444
+ const value = Number(raw);
445
+ if (value < option.min || value > option.max) {
446
+ throw new Error(`invalid value '${raw}' for --${name}. ${expected}`);
447
+ }
448
+ return value;
449
+ }
450
+ function durationExpectation(option) {
451
+ const forms = (option.bareUnits ?? [option.defaultUnit]).map((unit) => {
452
+ const unitMs = DURATION_UNIT_MS[unit];
453
+ const low = option.min === void 0 ? null : Math.ceil(option.min / unitMs);
454
+ const high = option.max === void 0 ? null : Math.floor(option.max / unitMs);
455
+ const range = low !== null && high !== null ? ` (${low}-${high})` : "";
456
+ return `a bare number of ${DURATION_UNIT_LABEL[unit]}${range}`;
457
+ });
458
+ return `Expected a duration with a unit (1500ms, 90s, 5m, 2h) or ${forms.join(", or ")}`;
459
+ }
460
+ function parseDurationValue(name, option, raw) {
461
+ const invalid = () => new Error(
462
+ `invalid value '${raw}' for --${name}. ${durationExpectation(option)}`
463
+ );
464
+ const match = DURATION_PATTERN.exec(raw.trim().toLowerCase());
465
+ if (match === null) throw invalid();
466
+ const amount = Number.parseFloat(match[1] ?? "");
467
+ const unit = match[2];
468
+ const inRange = (value) => (option.min === void 0 || value >= option.min) && (option.max === void 0 || value <= option.max);
469
+ if (unit !== void 0) {
470
+ const value = Math.round(
471
+ amount * DURATION_UNIT_MS[unit]
472
+ );
473
+ if (!inRange(value)) throw invalid();
474
+ return value;
475
+ }
476
+ for (const candidate of option.bareUnits ?? [option.defaultUnit]) {
477
+ const value = Math.round(amount * DURATION_UNIT_MS[candidate]);
478
+ if (inRange(value)) return value;
479
+ }
480
+ throw invalid();
481
+ }
482
+ function parseOptionValue(name, option, raw) {
483
+ switch (option.type) {
484
+ case "enum": {
485
+ if (!option.values.includes(raw)) {
486
+ throw new Error(
487
+ `invalid value '${raw}' for --${name}. Expected one of: ${option.values.join(", ")}`
488
+ );
489
+ }
490
+ return raw;
491
+ }
492
+ case "integer":
493
+ return parseIntegerValue(name, option, raw);
494
+ case "duration":
495
+ return parseDurationValue(name, option, raw);
496
+ default:
497
+ return raw;
498
+ }
499
+ }
500
+ function splitValues(option, values) {
501
+ const separator = option.type === "string" || option.type === "enum" ? option.split : void 0;
502
+ if (separator === void 0) return [...values];
503
+ return values.flatMap(
504
+ (value) => value.split(separator).map((part) => part.trim()).filter((part) => part.length > 0)
505
+ );
506
+ }
507
+ function buildOptionValues(spec, path, command, parsed) {
508
+ const values = {};
509
+ const missing = [];
510
+ for (const [name, option] of Object.entries(command.options ?? {})) {
511
+ if (option.type === "boolean") {
512
+ values[name] = parsed.flags.has(name);
513
+ continue;
514
+ }
515
+ const raw = splitValues(option, parsed.values.get(name) ?? []);
516
+ if (raw.length === 0) {
517
+ if (option.required === true) missing.push(`--${name}`);
518
+ if (isRepeatable(option)) values[name] = [];
519
+ else values[name] = option.default;
520
+ continue;
521
+ }
522
+ try {
523
+ if (isRepeatable(option)) {
524
+ values[name] = raw.map(
525
+ (value) => String(parseOptionValue(name, option, value))
526
+ );
527
+ continue;
528
+ }
529
+ values[name] = parseOptionValue(name, option, raw[0] ?? "");
530
+ } catch (error) {
531
+ throw usageFailure(spec, path, command, {
532
+ code: "invalid_value",
533
+ message: error instanceof Error ? error.message : String(error)
534
+ });
535
+ }
536
+ }
537
+ return { values, missing };
538
+ }
539
+ function buildPositionalValues(spec, path, command, parsed) {
540
+ const values = {};
541
+ const missing = [];
542
+ const tokens = [...parsed.positionals];
543
+ for (const positional of command.positionals ?? []) {
544
+ if (positional.variadic === true) {
545
+ const rest = tokens.splice(0, tokens.length);
546
+ values[positional.name] = rest;
547
+ if (positional.required === true && rest.length === 0) {
548
+ missing.push(`<${positional.name}>`);
549
+ }
550
+ continue;
551
+ }
552
+ const token = tokens.shift();
553
+ values[positional.name] = token;
554
+ if (positional.required === true && token === void 0) {
555
+ missing.push(`<${positional.name}>`);
556
+ }
557
+ }
558
+ if (tokens.length > 0) {
559
+ throw usageFailure(spec, path, command, {
560
+ code: "unexpected_argument",
561
+ message: `unexpected argument '${tokens[0]}'`,
562
+ ...command.unexpectedPositionalHint === void 0 ? {} : { hint: command.unexpectedPositionalHint }
563
+ });
564
+ }
565
+ return { values, missing };
566
+ }
567
+ function isPresent(command, parsed, name) {
568
+ if (command.options?.[name]?.type === "boolean")
569
+ return parsed.flags.has(name);
570
+ return (parsed.values.get(name) ?? []).length > 0;
571
+ }
572
+ function checkConstraints(spec, path, command, parsed) {
573
+ for (const constraint of command.constraints ?? []) {
574
+ if (constraint.kind === "requires") {
575
+ if (!isPresent(command, parsed, constraint.option)) continue;
576
+ const absent = constraint.needs.filter(
577
+ (need) => !isPresent(command, parsed, need)
578
+ );
579
+ if (absent.length === 0) continue;
580
+ throw usageFailure(spec, path, command, {
581
+ code: "missing_required",
582
+ message: `--${constraint.option} requires ${absent.map((need) => `--${need}`).join(", ")}`
583
+ });
584
+ }
585
+ const given = constraint.options.filter(
586
+ (name) => isPresent(command, parsed, name)
587
+ );
588
+ const list = constraint.options.map((name) => `--${name}`).join(", ");
589
+ if (given.length > 1 && constraint.kind !== "at-least-one") {
590
+ throw usageFailure(spec, path, command, {
591
+ code: "unexpected_argument",
592
+ message: `${given.map((name) => `--${name}`).join(" and ")} cannot be combined`,
593
+ hint: `${constraint.kind === "exactly-one" ? "Pass exactly one of" : "Pass at most one of"}: ${list}`
594
+ });
595
+ }
596
+ if (given.length === 0 && constraint.kind !== "at-most-one") {
597
+ throw usageFailure(spec, path, command, {
598
+ code: "missing_required",
599
+ message: `missing required options: one of ${list}`
600
+ });
601
+ }
602
+ }
603
+ }
604
+ function renderErrorText(failure) {
605
+ const blocks = [
606
+ failure.hint === void 0 ? failure.message : `${failure.message} (${failure.hint})`
607
+ ];
608
+ if (failure.detail !== void 0) blocks.push(failure.detail);
609
+ if (failure.usage !== void 0) blocks.push(failure.usage);
610
+ return `${blocks.join("\n\n")}
611
+ `;
612
+ }
613
+ function renderErrorJson(failure) {
614
+ const hint = failure.hint ?? failure.usage?.replace(/^Usage:\n {2}/, "");
615
+ return `${JSON.stringify(
616
+ {
617
+ ok: false,
618
+ error: {
619
+ code: failure.code,
620
+ message: failure.message,
621
+ ...hint === void 0 ? {} : { hint }
622
+ }
623
+ },
624
+ null,
625
+ 2
626
+ )}
627
+ `;
628
+ }
629
+ function wantsJsonOutput(argv) {
630
+ const terminator = argv.indexOf("--");
631
+ const scanned = terminator === -1 ? argv : argv.slice(0, terminator);
632
+ return scanned.some((token) => token === "--json" || token === "--json=true");
633
+ }
634
+ var NEGATIVE_NUMBER_PATTERN = /^-\.?\d/;
635
+ function normalizeShort(short) {
636
+ return short.startsWith("-") ? short.slice(1) : short;
637
+ }
638
+ function assertValidSpec(spec) {
639
+ const entries = Object.entries(
640
+ spec.commands
641
+ );
642
+ if (spec.root !== void 0) entries.push(["", spec.root]);
643
+ for (const [path, command] of entries) {
644
+ const label = `bb ${[spec.name, path].filter((part) => part.length > 0).join(" ")}`;
645
+ const spellings = /* @__PURE__ */ new Map([
646
+ ["help", "the built-in --help"]
647
+ ]);
648
+ const shorts = /* @__PURE__ */ new Map([["h", "the built-in -h"]]);
649
+ for (const [name, option] of Object.entries(command.options ?? {})) {
650
+ for (const spelling of [name, ...option.aliases ?? []]) {
651
+ const owner = spellings.get(spelling);
652
+ if (owner !== void 0) {
653
+ throw new Error(
654
+ `${label}: --${spelling} is declared by both ${owner} and --${name}`
655
+ );
656
+ }
657
+ spellings.set(spelling, `--${name}`);
658
+ }
659
+ if (option.short !== void 0) {
660
+ const short = normalizeShort(option.short);
661
+ if (short.length !== 1) {
662
+ throw new Error(
663
+ `${label}: short form ${JSON.stringify(option.short)} of --${name} must be one character`
664
+ );
665
+ }
666
+ const owner = shorts.get(short);
667
+ if (owner !== void 0) {
668
+ throw new Error(
669
+ `${label}: -${short} is declared by both ${owner} and --${name}`
670
+ );
671
+ }
672
+ shorts.set(short, `--${name}`);
673
+ }
674
+ if ((option.type === "string" || option.type === "enum") && option.split !== void 0 && option.repeatable !== true) {
675
+ throw new Error(
676
+ `${label}: --${name} declares split without repeatable: true, which would drop every value after the first`
677
+ );
678
+ }
679
+ }
680
+ }
681
+ }
682
+ function failureResult(failure, wantsJson) {
683
+ const stderr = renderErrorText(failure);
684
+ if (!wantsJson) return { exitCode: failure.exitCode, stderr };
685
+ return {
686
+ exitCode: failure.exitCode,
687
+ stdout: renderErrorJson(failure),
688
+ stderr
689
+ };
690
+ }
691
+ function defineCli(spec) {
692
+ assertValidSpec(spec);
693
+ const commands = visibleCommands(spec).map(
694
+ ([path, command]) => ({
695
+ name: path.replaceAll(" ", "-"),
696
+ summary: command.summary,
697
+ usage: usageLine(spec.name, path, command)
698
+ })
699
+ );
700
+ return {
701
+ name: spec.name,
702
+ summary: spec.summary,
703
+ commands,
704
+ rendersHelp: true,
705
+ async run(argv, ctx) {
706
+ const wantsJson = wantsJsonOutput(argv);
707
+ const asked = argv[0] === "help" ? argv.slice(1) : argv;
708
+ const terminator = asked.indexOf("--");
709
+ const scanned = terminator === -1 ? asked : asked.slice(0, terminator);
710
+ const helpRequested = argv[0] === "help" || scanned.some((token) => token === "--help" || token === "-h");
711
+ try {
712
+ const words = leadingWords(scanned);
713
+ const resolved = resolveCommand(spec, words);
714
+ if (helpRequested) {
715
+ if (resolved === null && words.length > 0) {
716
+ const group = commandGroupRows(spec, words);
717
+ if (group.length === 0) throw unknownCommandError(spec, words);
718
+ return {
719
+ exitCode: 0,
720
+ stdout: renderGroupHelp(spec, words, group)
721
+ };
722
+ }
723
+ return { exitCode: 0, stdout: renderHelp(spec, resolved) };
724
+ }
725
+ if (resolved === null) {
726
+ const root = spec.root;
727
+ if (root === void 0 || words.length > 0 && !root.positionals) {
728
+ if (words.length > 0) {
729
+ throw unknownCommandError(spec, words);
730
+ }
731
+ const failure = {
732
+ code: "missing_command",
733
+ message: "missing command",
734
+ hint: `Run \`bb ${spec.name} --help\` for the command list.`,
735
+ exitCode: spec.usageErrorExitCode ?? 1
736
+ };
737
+ if (!wantsJson) {
738
+ return {
739
+ exitCode: failure.exitCode,
740
+ stdout: renderTopLevelHelp(spec)
741
+ };
742
+ }
743
+ return {
744
+ exitCode: failure.exitCode,
745
+ stdout: renderErrorJson(failure),
746
+ stderr: renderTopLevelHelp(spec)
747
+ };
748
+ }
749
+ }
750
+ const path = resolved?.path ?? "";
751
+ const command = resolved?.command ?? spec.root;
752
+ if (command === void 0) {
753
+ throw unknownCommandError(spec, words);
754
+ }
755
+ const tail = asked.slice(resolved?.words ?? 0);
756
+ const parsed = tokenize(spec, path, command, tail);
757
+ const options = buildOptionValues(spec, path, command, parsed);
758
+ const positionals = buildPositionalValues(spec, path, command, parsed);
759
+ const missing = [
760
+ ...positionals.missing.length > 0 ? [`missing required arguments: ${positionals.missing.join(", ")}`] : [],
761
+ ...options.missing.length > 0 ? [`missing required options: ${options.missing.join(", ")}`] : []
762
+ ];
763
+ if (missing.length > 0) {
764
+ throw usageFailure(spec, path, command, {
765
+ code: "missing_required",
766
+ message: missing.join("; ")
767
+ });
768
+ }
769
+ checkConstraints(spec, path, command, parsed);
770
+ return await command.run(
771
+ {
772
+ options: options.values,
773
+ positionals: positionals.values,
774
+ passthrough: parsed.passthrough,
775
+ help: renderHelp(spec, resolved)
776
+ },
777
+ ctx
778
+ );
779
+ } catch (error) {
780
+ if (error instanceof UsageError) {
781
+ return failureResult(error.failure, wantsJson);
782
+ }
783
+ if (error instanceof PluginCliError) {
784
+ return failureResult(
785
+ {
786
+ code: error.code,
787
+ message: error.message,
788
+ ...error.hint === void 0 ? {} : { hint: error.hint },
789
+ exitCode: error.exitCode
790
+ },
791
+ wantsJson
792
+ );
793
+ }
794
+ throw error;
795
+ }
796
+ }
797
+ };
798
+ }
799
+
4
800
  // src/host-contract.ts
5
801
  function experimental_defineHostEntry(args) {
6
802
  return {
@@ -18,6 +814,9 @@ function defineRpcContract(contract) {
18
814
  }
19
815
  export {
20
816
  PLUGIN_CLI_OUTPUT_MAX_BYTES,
817
+ PluginCliError,
818
+ cliCommand,
819
+ defineCli,
21
820
  defineRpcContract,
22
821
  experimental_defineHostEntry
23
822
  };