@politty/zod 0.1.2 → 0.2.1

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.
Files changed (59) hide show
  1. package/README.md +31 -0
  2. package/dist/{arg-registry-Be4GJADw.d.ts → arg-registry-BzQ-y-e0.d.ts} +2 -2
  3. package/dist/augment.d.ts +1 -1
  4. package/dist/augment.js +1 -1
  5. package/dist/cli-main-B4FX3h4J.js +1 -0
  6. package/dist/cli-main-CjYk6tAX.js +2 -0
  7. package/dist/cli-run-DN55A0JZ.js +1 -0
  8. package/dist/cli.js +1 -15
  9. package/dist/command-Mbdt0bmN.js +1 -0
  10. package/dist/compile-cache-VuUbFWIo.js +1 -0
  11. package/dist/compile-cache.js +1 -3
  12. package/dist/completion-D8DMrPqg.js +116 -0
  13. package/dist/completion-DUMcvXkT.js +1 -0
  14. package/dist/completion.d.ts +87 -7
  15. package/dist/completion.js +1 -4
  16. package/dist/docs.d.ts +1 -1
  17. package/dist/docs.js +84 -3063
  18. package/dist/dynamic-CMtee4tD.js +1 -0
  19. package/dist/dynamic-CrnlveHj.js +6 -0
  20. package/dist/field-meta-COGya7xp.js +1 -0
  21. package/dist/index.d.ts +3 -3
  22. package/dist/index.js +1 -27
  23. package/dist/log-collector-DICKib75.js +1 -0
  24. package/dist/logger-CJsyJ8sb.js +1 -0
  25. package/dist/prompt-CqmGq1_N.js +1 -0
  26. package/dist/prompt-clack.d.ts +1 -1
  27. package/dist/prompt-clack.js +1 -32
  28. package/dist/prompt-inquirer.d.ts +1 -1
  29. package/dist/prompt-inquirer.js +1 -47
  30. package/dist/prompt.d.ts +1 -1
  31. package/dist/prompt.js +1 -4
  32. package/dist/register-C1WbbeYH.js +1 -0
  33. package/dist/runner-BIdcntiz.js +29 -0
  34. package/dist/runner-Chaf_5lK.js +1 -0
  35. package/dist/schema-BiUP_KyV.js +1 -0
  36. package/dist/schema-extractor-DU0Vuhbd.js +1 -0
  37. package/dist/skill.d.ts +1 -1
  38. package/dist/skill.js +2 -1837
  39. package/dist/subcommand-router-D8GTMXYL.js +1 -0
  40. package/dist/{index-B4NylQOW.d.ts → with-completion-command-CmVdHFHX.d.ts} +3 -74
  41. package/dist/with-completion-command-DDohm8U6.js +3 -0
  42. package/package.json +3 -3
  43. package/dist/cli-main-CAa5KwCo.js +0 -314
  44. package/dist/cli-main-DIRMduj1.js +0 -3
  45. package/dist/cli-run-B--tZW_I.js +0 -8
  46. package/dist/command-k-4yAz4J.js +0 -42
  47. package/dist/compile-cache-BC65o7MH.js +0 -103
  48. package/dist/completion-Ot2qpNNw.js +0 -5616
  49. package/dist/field-meta-CO38jOvl.js +0 -148
  50. package/dist/log-collector-CoUkLVJB.js +0 -114
  51. package/dist/logger-i_bb-Jhc.js +0 -133
  52. package/dist/prompt-BjIZThsH.js +0 -169
  53. package/dist/register-DdcsbNwM.js +0 -440
  54. package/dist/runner-Bz3arOqh.js +0 -3051
  55. package/dist/runner-Cs-Mz9JN.js +0 -3
  56. package/dist/schema-extractor-CVNDs07l.js +0 -250
  57. package/dist/src-D4QGGgW1.js +0 -6
  58. package/dist/src-DhQVU2hG.js +0 -12
  59. package/dist/subcommand-router-Cskpofdk.js +0 -134
@@ -1,3051 +0,0 @@
1
- import { a as getValidatorAdapter, i as toKebabCase, r as toCamelCase, t as getAllAliases } from "./field-meta-CO38jOvl.js";
2
- import { n as getExtractedFields, o as isInternalArgsSchema, s as validateInternalArgs, t as extractFields } from "./schema-extractor-CVNDs07l.js";
3
- import { n as enableCompileCache } from "./compile-cache-BC65o7MH.js";
4
- import { n as emptyLogs, r as mergeLogs, t as createLogCollector } from "./log-collector-CoUkLVJB.js";
5
- import { a as resolveSubcommandWithAlias, c as resolveSubCommandMeta, n as listSubCommands, o as isLazyCommand, r as resolveLazyCommand, t as listSubCommandNamesWithAliases } from "./subcommand-router-Cskpofdk.js";
6
- import { a as symbols, i as styles } from "./logger-i_bb-Jhc.js";
7
- import { stripVTControlCharacters } from "node:util";
8
-
9
- //#region ../core/src/executor/command-runner.ts
10
- /**
11
- * Execute a command lifecycle: setup → run → cleanup
12
- *
13
- * This is an internal function that executes the command's lifecycle hooks.
14
- * For running commands with argument parsing, use `runCommand` instead.
15
- *
16
- * @param command - The command to execute
17
- * @param args - Already validated arguments
18
- * @param options - Lifecycle options
19
- * @returns The result of command execution
20
- * @internal
21
- */
22
- async function executeLifecycle(command, args, _options = {}) {
23
- let error;
24
- let result;
25
- const collector = _options.captureLogs ?? false ? createLogCollector() : null;
26
- collector?.start();
27
- const setupContext = { args };
28
- const cleanupContext = {
29
- args,
30
- error
31
- };
32
- let signalHandler;
33
- if (_options.handleSignals) {
34
- signalHandler = async (_signal) => {
35
- if (signalHandler) {
36
- process.off("SIGINT", signalHandler);
37
- process.off("SIGTERM", signalHandler);
38
- }
39
- const signalError = /* @__PURE__ */ new Error("Process interrupted");
40
- cleanupContext.error = signalError;
41
- if (command.cleanup) try {
42
- await command.cleanup(cleanupContext);
43
- } catch (e) {
44
- console.error("Error during signal cleanup:", e);
45
- }
46
- if (_options.globalCleanup) try {
47
- await _options.globalCleanup({ error: signalError });
48
- } catch (e) {
49
- console.error("Error during global signal cleanup:", e);
50
- }
51
- collector?.stop();
52
- if (process.stdout.writableNeedDrain) await new Promise((resolve) => {
53
- const timeout = setTimeout(() => {
54
- process.stdout.off("drain", onDrain);
55
- resolve();
56
- }, 200);
57
- const onDrain = () => {
58
- clearTimeout(timeout);
59
- resolve();
60
- };
61
- process.stdout.once("drain", onDrain);
62
- });
63
- process.exit(1);
64
- };
65
- process.on("SIGINT", signalHandler);
66
- process.on("SIGTERM", signalHandler);
67
- }
68
- try {
69
- if (command.setup) await command.setup(setupContext);
70
- if (command.run) result = await command.run(args);
71
- } catch (e) {
72
- error = e instanceof Error ? e : new Error(String(e));
73
- } finally {
74
- if (signalHandler) {
75
- process.off("SIGINT", signalHandler);
76
- process.off("SIGTERM", signalHandler);
77
- }
78
- }
79
- if (command.cleanup) {
80
- cleanupContext.error = error;
81
- try {
82
- await command.cleanup(cleanupContext);
83
- } catch (cleanupError) {
84
- if (!error) error = cleanupError instanceof Error ? cleanupError : new Error(String(cleanupError));
85
- }
86
- }
87
- collector?.stop();
88
- const existingLogs = _options.existingLogs ?? emptyLogs();
89
- const collectedLogs = collector?.getLogs() ?? emptyLogs();
90
- const logs = mergeLogs(existingLogs, collectedLogs);
91
- if (error) return {
92
- success: false,
93
- error,
94
- exitCode: 1,
95
- logs
96
- };
97
- return {
98
- success: true,
99
- result,
100
- exitCode: 0,
101
- logs
102
- };
103
- }
104
-
105
- //#endregion
106
- //#region ../core/src/output/string-width.ts
107
- /**
108
- * Lightweight replacement for the `string-width` package.
109
- *
110
- * Computes the visual (terminal) width of a string by:
111
- * 1. Stripping ANSI escape codes (via Node's `stripVTControlCharacters`)
112
- * 2. Skipping zero-width characters (combining marks, control chars, etc.)
113
- * 3. Counting East Asian wide / fullwidth characters and most emoji as 2
114
- * 4. Counting everything else as 1
115
- *
116
- * This covers the cases the markdown renderer cares about (CJK text, emoji,
117
- * and already-styled ANSI strings) without pulling in an external dependency.
118
- */
119
- /**
120
- * Whether a code point has no visual width (combining marks, zero-width
121
- * spaces/joiners, variation selectors, control characters).
122
- */
123
- function isZeroWidth(cp) {
124
- return cp <= 31 || cp >= 127 && cp <= 159 || cp >= 768 && cp <= 879 || cp >= 6832 && cp <= 6911 || cp >= 7616 && cp <= 7679 || cp >= 8400 && cp <= 8447 || cp >= 65056 && cp <= 65071 || cp === 8203 || cp >= 8204 && cp <= 8207 || cp === 65279 || cp >= 65024 && cp <= 65039 || cp >= 917760 && cp <= 917999;
125
- }
126
- /**
127
- * Whether a code point is rendered at double (full) width in a terminal.
128
- * Based on Unicode East Asian Width (Wide/Fullwidth) plus common emoji ranges.
129
- */
130
- function isFullWidth(cp) {
131
- return cp >= 4352 && cp <= 4447 || cp >= 11904 && cp <= 12350 || cp >= 12353 && cp <= 13311 || cp >= 13312 && cp <= 19903 || cp >= 19968 && cp <= 40959 || cp >= 40960 && cp <= 42191 || cp >= 43360 && cp <= 43391 || cp >= 44032 && cp <= 55203 || cp >= 63744 && cp <= 64255 || cp >= 65040 && cp <= 65049 || cp >= 65072 && cp <= 65135 || cp >= 65280 && cp <= 65376 || cp >= 65504 && cp <= 65510 || cp >= 110592 && cp <= 110959 || cp >= 127488 && cp <= 127569 || cp >= 131072 && cp <= 262141 || cp >= 9728 && cp <= 10175 || cp >= 126976 && cp <= 129791;
132
- }
133
- /**
134
- * Visual width of a single grapheme cluster (a user-perceived character).
135
- *
136
- * Iterating by code point would over-count multi-code-point clusters such as
137
- * ZWJ emoji sequences (👨‍👩‍👧), regional-indicator flags (🇯🇵), and emoji with
138
- * skin-tone modifiers (👍🏽). These render as a single glyph, so the whole
139
- * cluster contributes width 0/1/2 once.
140
- */
141
- function graphemeWidth(grapheme) {
142
- let hasVisible = false;
143
- let hasWide = false;
144
- let hasEmojiVariation = false;
145
- for (const char of grapheme) {
146
- const cp = char.codePointAt(0);
147
- if (cp === 65039) {
148
- hasEmojiVariation = true;
149
- continue;
150
- }
151
- if (isZeroWidth(cp)) continue;
152
- hasVisible = true;
153
- if (isFullWidth(cp)) hasWide = true;
154
- }
155
- if (!hasVisible) return 0;
156
- return hasWide || hasEmojiVariation ? 2 : 1;
157
- }
158
- const segmenter = (() => {
159
- try {
160
- return new Intl.Segmenter("en", { granularity: "grapheme" });
161
- } catch {
162
- return;
163
- }
164
- })();
165
- /**
166
- * Compute the visual width of a string as rendered in a terminal.
167
- */
168
- function stringWidth(input) {
169
- if (input.length === 0) return 0;
170
- const str = input.includes("\x1B") || input.includes("›") ? stripVTControlCharacters(input) : input;
171
- let width = 0;
172
- if (segmenter) for (const { segment } of segmenter.segment(str)) width += graphemeWidth(segment);
173
- else for (const char of str) width += graphemeWidth(char);
174
- return width;
175
- }
176
-
177
- //#endregion
178
- //#region ../core/src/output/markdown-renderer.ts
179
- /**
180
- * Lightweight Markdown-to-terminal renderer.
181
- *
182
- * Supports a subset of Markdown tailored for CLI help notes:
183
- * - Inline: bold, italic, inline code, links
184
- * - Block: paragraphs, unordered/ordered lists, blockquotes, headings,
185
- * horizontal rules, fenced code blocks
186
- */
187
- /**
188
- * Apply inline Markdown formatting to a string.
189
- *
190
- * Processing order matters to avoid conflicts:
191
- * 1. Inline code (backticks) — content inside is literal, no further processing
192
- * 2. Bold (**text**)
193
- * 3. Italic (*text* or _text_)
194
- * 4. Links [text](url)
195
- */
196
- function renderInline(text) {
197
- const codeSpans = [];
198
- let result = text.replace(/`([^`]+)`/g, (_match, code) => {
199
- const index = codeSpans.length;
200
- codeSpans.push(styles.cyan(code));
201
- return `\x00CODE${index}\x00`;
202
- });
203
- result = result.replace(/\*\*(.+?)\*\*/g, (_match, content) => styles.bold(content));
204
- result = result.replace(/__(.+?)__/g, (_match, content) => styles.bold(content));
205
- result = result.replace(/\*(.+?)\*/g, (_match, content) => styles.italic(content));
206
- result = result.replace(/(?<!\w)_(.+?)_(?!\w)/g, (_match, content) => styles.italic(content));
207
- result = result.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, linkText, url) => `${styles.underline(linkText)} ${styles.dim(`(${url})`)}`);
208
- result = result.replace(/\x00CODE(\d+)\x00/g, (_match, index) => codeSpans[Number(index)]);
209
- return result;
210
- }
211
- /**
212
- * Render a Markdown string to styled terminal output.
213
- *
214
- * Block-level processing:
215
- * - Splits input into blocks separated by blank lines
216
- * - Detects headings, horizontal rules, blockquotes, lists, code blocks, and paragraphs
217
- * - Applies inline formatting within each block
218
- */
219
- function renderMarkdown(markdown) {
220
- return splitIntoBlocks(markdown.split("\n")).map(renderBlock).join("\n\n");
221
- }
222
- const HEADING_RE = /^(#{1,6})\s+(.+)$/;
223
- const HR_RE = /^(?:---+|\*\*\*+|___+)\s*$/;
224
- const BLOCKQUOTE_RE = /^>\s?(.*)$/;
225
- const UL_RE = /^-\s+(.+)$/;
226
- const OL_RE = /^(\d+)[.)]\s+(.+)$/;
227
- const FENCE_OPEN_RE = /^(`{3,}|~{3,})(\S*)\s*$/;
228
- const ALERT_RE = /^\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\s*$/;
229
- const TABLE_ROW_RE = /^\|(.+)\|$/;
230
- const TABLE_SEP_RE = /^\|(\s*:?-+:?\s*\|)+$/;
231
- /**
232
- * Split lines into logical blocks separated by blank lines.
233
- * Consecutive lines of the same block type are grouped together.
234
- */
235
- function splitIntoBlocks(lines) {
236
- const blocks = [];
237
- let i = 0;
238
- while (i < lines.length) {
239
- const line = lines[i];
240
- if (line.trim() === "") {
241
- i++;
242
- continue;
243
- }
244
- const fenceMatch = line.match(FENCE_OPEN_RE);
245
- if (fenceMatch) {
246
- const fence = fenceMatch[1];
247
- const lang = fenceMatch[2] ?? "";
248
- const codeLines = [];
249
- i++;
250
- while (i < lines.length) {
251
- if (lines[i].startsWith(fence.charAt(0).repeat(fence.length)) && lines[i].trim() === fence.charAt(0).repeat(Math.max(fence.length, lines[i].trim().length))) {
252
- i++;
253
- break;
254
- }
255
- codeLines.push(lines[i]);
256
- i++;
257
- }
258
- blocks.push({
259
- type: "code",
260
- lang,
261
- lines: codeLines
262
- });
263
- continue;
264
- }
265
- const headingMatch = line.match(HEADING_RE);
266
- if (headingMatch) {
267
- blocks.push({
268
- type: "heading",
269
- level: headingMatch[1].length,
270
- content: headingMatch[2]
271
- });
272
- i++;
273
- continue;
274
- }
275
- if (HR_RE.test(line)) {
276
- blocks.push({ type: "hr" });
277
- i++;
278
- continue;
279
- }
280
- if (BLOCKQUOTE_RE.test(line)) {
281
- const bqLines = [];
282
- while (i < lines.length) {
283
- const bqMatch = lines[i].match(BLOCKQUOTE_RE);
284
- if (bqMatch) {
285
- bqLines.push(bqMatch[1]);
286
- i++;
287
- } else break;
288
- }
289
- if (bqLines.length > 0) {
290
- const alertMatch = bqLines[0].match(ALERT_RE);
291
- if (alertMatch) {
292
- const contentLines = bqLines.slice(1).filter((l) => l !== "");
293
- blocks.push({
294
- type: "alert",
295
- alertType: alertMatch[1],
296
- lines: contentLines
297
- });
298
- continue;
299
- }
300
- }
301
- blocks.push({
302
- type: "blockquote",
303
- lines: bqLines
304
- });
305
- continue;
306
- }
307
- if (UL_RE.test(line)) {
308
- const items = [];
309
- while (i < lines.length) {
310
- const ulMatch = lines[i].match(UL_RE);
311
- if (ulMatch) {
312
- items.push(ulMatch[1]);
313
- i++;
314
- } else break;
315
- }
316
- blocks.push({
317
- type: "ul",
318
- items
319
- });
320
- continue;
321
- }
322
- const olMatch = line.match(OL_RE);
323
- if (olMatch) {
324
- const start = Number(olMatch[1]);
325
- const items = [];
326
- while (i < lines.length) {
327
- const match = lines[i].match(OL_RE);
328
- if (match) {
329
- items.push(match[2]);
330
- i++;
331
- } else break;
332
- }
333
- blocks.push({
334
- type: "ol",
335
- items,
336
- start
337
- });
338
- continue;
339
- }
340
- if (TABLE_ROW_RE.test(line) && i + 1 < lines.length && TABLE_SEP_RE.test(lines[i + 1])) {
341
- const headers = parseCells(line);
342
- const alignments = parseAlignments(lines[i + 1]);
343
- i += 2;
344
- const rows = [];
345
- while (i < lines.length && TABLE_ROW_RE.test(lines[i])) {
346
- rows.push(parseCells(lines[i]));
347
- i++;
348
- }
349
- blocks.push({
350
- type: "table",
351
- headers,
352
- alignments,
353
- rows
354
- });
355
- continue;
356
- }
357
- const paraLines = [];
358
- while (i < lines.length) {
359
- const l = lines[i];
360
- if (l.trim() === "" || HEADING_RE.test(l) || HR_RE.test(l) || BLOCKQUOTE_RE.test(l) || UL_RE.test(l) || OL_RE.test(l) || FENCE_OPEN_RE.test(l) || TABLE_ROW_RE.test(l) && i + 1 < lines.length && TABLE_SEP_RE.test(lines[i + 1])) break;
361
- paraLines.push(l);
362
- i++;
363
- }
364
- if (paraLines.length > 0) blocks.push({
365
- type: "paragraph",
366
- lines: paraLines
367
- });
368
- }
369
- return blocks;
370
- }
371
- /**
372
- * Parse cells from a table row: `| a | b | c |` → `["a", "b", "c"]`
373
- */
374
- function parseCells(row) {
375
- return row.slice(1, -1).split("|").map((cell) => cell.trim());
376
- }
377
- /**
378
- * Parse alignment from separator row: `|:---|:---:|---:|` → `["left", "center", "right"]`
379
- */
380
- function parseAlignments(sepRow) {
381
- return sepRow.slice(1, -1).split("|").map((cell) => {
382
- const trimmed = cell.trim();
383
- if (trimmed.startsWith(":") && trimmed.endsWith(":")) return "center";
384
- if (trimmed.endsWith(":")) return "right";
385
- return "left";
386
- });
387
- }
388
- /**
389
- * Pad a string to a given width with the specified alignment.
390
- */
391
- function alignText(text, width, alignment) {
392
- const visualWidth = stringWidth(text);
393
- const total = Math.max(0, width - visualWidth);
394
- if (alignment === "right") return " ".repeat(total) + text;
395
- if (alignment === "center") {
396
- const left = Math.floor(total / 2);
397
- return " ".repeat(left) + text + " ".repeat(total - left);
398
- }
399
- return text + " ".repeat(total);
400
- }
401
- /**
402
- * Style configuration for GitHub-style alert blocks.
403
- */
404
- const alertStyles = {
405
- NOTE: {
406
- icon: "ℹ",
407
- label: "Note",
408
- styleFn: styles.cyan
409
- },
410
- TIP: {
411
- icon: "💡",
412
- label: "Tip",
413
- styleFn: styles.green
414
- },
415
- IMPORTANT: {
416
- icon: "❗",
417
- label: "Important",
418
- styleFn: styles.magenta
419
- },
420
- WARNING: {
421
- icon: "⚠",
422
- label: "Warning",
423
- styleFn: styles.yellow
424
- },
425
- CAUTION: {
426
- icon: "🔴",
427
- label: "Caution",
428
- styleFn: styles.red
429
- }
430
- };
431
- /**
432
- * Render a single block to styled terminal output.
433
- */
434
- function renderBlock(block) {
435
- switch (block.type) {
436
- case "heading": return styles.green(styles.bold(renderInline(block.content)));
437
- case "hr": return styles.dim("─".repeat(40));
438
- case "blockquote": {
439
- const prefix = styles.dim("│ ");
440
- return block.lines.map((line) => `${prefix}${renderInline(line)}`).join("\n");
441
- }
442
- case "alert": {
443
- const { icon, label, styleFn } = alertStyles[block.alertType];
444
- const prefix = styleFn(styles.bold("│")) + " ";
445
- const header = `${prefix}${styleFn(icon)} ${styleFn(label)}`;
446
- if (block.lines.length === 0) return header;
447
- return `${header}\n${block.lines.map((line) => `${prefix}${renderInline(line)}`).join("\n")}`;
448
- }
449
- case "ul": return block.items.map((item) => `${styles.dim("•")} ${renderInline(item)}`).join("\n");
450
- case "ol": {
451
- const maxNum = block.start + block.items.length - 1;
452
- const width = String(maxNum).length;
453
- return block.items.map((item, i) => {
454
- const num = String(block.start + i).padStart(width, " ");
455
- return `${styles.dim(`${num}.`)} ${renderInline(item)}`;
456
- }).join("\n");
457
- }
458
- case "table": {
459
- const colCount = block.headers.length;
460
- const renderedHeaders = block.headers.map((h) => renderInline(h));
461
- const renderedRows = block.rows.map((row) => Array.from({ length: colCount }, (_, i) => renderInline(row[i] ?? "")));
462
- const colWidths = renderedHeaders.map((h, i) => {
463
- const headerWidth = stringWidth(h);
464
- const cellWidths = renderedRows.map((row) => stringWidth(row[i]));
465
- return Math.max(headerWidth, ...cellWidths);
466
- });
467
- const pipe = styles.dim("│");
468
- const topBorder = styles.dim(`┌─${colWidths.map((w) => "─".repeat(w)).join("─┬─")}─┐`);
469
- const midBorder = styles.dim(`├─${colWidths.map((w) => "─".repeat(w)).join("─┼─")}─┤`);
470
- const botBorder = styles.dim(`└─${colWidths.map((w) => "─".repeat(w)).join("─┴─")}─┘`);
471
- return [
472
- topBorder,
473
- `${pipe} ${renderedHeaders.map((h, i) => styles.bold(alignText(h, colWidths[i], block.alignments[i] ?? "left"))).join(` ${pipe} `)} ${pipe}`,
474
- midBorder,
475
- ...renderedRows.map((row) => {
476
- const cells = row.map((cell, i) => alignText(cell, colWidths[i], block.alignments[i] ?? "left"));
477
- return `${pipe} ${cells.join(` ${pipe} `)} ${pipe}`;
478
- }),
479
- botBorder
480
- ].join("\n");
481
- }
482
- case "code": return block.lines.map((line) => ` ${styles.yellow(line)}`).join("\n");
483
- case "paragraph": return renderInline(block.lines.join(" "));
484
- }
485
- }
486
-
487
- //#endregion
488
- //#region ../core/src/output/help-generator.ts
489
- /**
490
- * Default descriptions for built-in options
491
- */
492
- const defaultBuiltinDescriptions = {
493
- help: "Show help",
494
- helpAll: "Show help with all subcommand options",
495
- version: "Show version"
496
- };
497
- /**
498
- * Internal subcommands are reserved for framework internals and hidden from help output.
499
- */
500
- function isVisibleSubcommand(name) {
501
- return !name.startsWith("__");
502
- }
503
- function getVisibleSubcommandEntries(subCommands) {
504
- return Object.entries(subCommands).filter(([name]) => isVisibleSubcommand(name));
505
- }
506
- /**
507
- * Build full command name from context
508
- */
509
- function buildFullCommandName(command, context) {
510
- if (context?.rootName && context.commandPath && context.commandPath.length > 0) return context.commandPath.join(" ");
511
- return command.name ?? "command";
512
- }
513
- /**
514
- * Build usage command name (includes root name for subcommands)
515
- */
516
- function buildUsageCommandName(command, context) {
517
- if (context?.rootName && context.commandPath && context.commandPath.length > 0) return `${context.rootName} ${context.commandPath.join(" ")}`;
518
- return command.name ?? "command";
519
- }
520
- /**
521
- * Render the usage line for a command
522
- */
523
- function renderUsageLine(command, context) {
524
- const parts = [];
525
- const name = buildUsageCommandName(command, context);
526
- parts.push(styles.commandName(name));
527
- if (context?.globalExtracted?.fields.length) parts.push(styles.placeholder("[global options]"));
528
- const extracted = getExtractedFields(command);
529
- if (extracted) {
530
- const positionals = extracted.fields.filter((a) => a.positional);
531
- if (extracted.fields.filter((a) => !a.positional).length > 0) parts.push(styles.placeholder("[options]"));
532
- if (command.subCommands && getVisibleSubcommandEntries(command.subCommands).length > 0) {
533
- if (command.run) parts.push(styles.placeholder("[command]"));
534
- else parts.push(styles.option("<command>"));
535
- }
536
- for (const arg of positionals) if (arg.required) parts.push(styles.option(`<${arg.name}>`));
537
- else parts.push(styles.placeholder(`[${arg.name}]`));
538
- } else if (command.subCommands && getVisibleSubcommandEntries(command.subCommands).length > 0) {
539
- if (command.run) parts.push(styles.placeholder("[command]"));
540
- else parts.push(styles.option("<command>"));
541
- }
542
- return parts.join(" ");
543
- }
544
- /**
545
- * Render the options section
546
- */
547
- function renderOptions(command, descriptions = {}, context) {
548
- const lines = [];
549
- const desc = {
550
- help: descriptions.help ?? defaultBuiltinDescriptions.help,
551
- helpAll: descriptions.helpAll ?? defaultBuiltinDescriptions.helpAll,
552
- version: descriptions.version ?? defaultBuiltinDescriptions.version
553
- };
554
- const extracted = getExtractedFields(command);
555
- const hasUserDefinedh = extracted?.fields.some((f) => f.overrideBuiltinAlias === true && getAllAliases(f).includes("h")) ?? false;
556
- const hasUserDefinedH = extracted?.fields.some((f) => f.overrideBuiltinAlias === true && getAllAliases(f).includes("H")) ?? false;
557
- if (hasUserDefinedh) lines.push(formatOption(styles.option("--help"), desc.help));
558
- else lines.push(formatOption(`${styles.option("-h")}, ${styles.option("--help")}`, desc.help));
559
- if (hasUserDefinedH) lines.push(formatOption(styles.option("--help-all"), desc.helpAll));
560
- else lines.push(formatOption(`${styles.option("-H")}, ${styles.option("--help-all")}`, desc.helpAll));
561
- if (context?.rootVersion) lines.push(formatOption(styles.option("--version"), desc.version));
562
- if (!extracted) return lines.join("\n");
563
- if (extracted.schemaType === "discriminatedUnion" && extracted.discriminator) return renderDiscriminatedUnionOptions(extracted, command, lines);
564
- if (extracted.schemaType === "union" && extracted.unionOptions) return renderUnionOptions(extracted, command, lines);
565
- if (extracted.schemaType === "xor" && extracted.unionOptions) return renderUnionOptions(extracted, command, lines);
566
- const options = extracted.fields.filter((a) => !a.positional);
567
- for (const opt of options) {
568
- const flags = formatFlags(opt);
569
- let desc = opt.description ?? "";
570
- if (opt.defaultValue !== void 0) desc += ` ${styles.defaultValue(`(default: ${JSON.stringify(opt.defaultValue)})`)}`;
571
- if (opt.required) desc += ` ${styles.required("(required)")}`;
572
- const envInfo = formatEnvInfo(opt.env);
573
- if (envInfo) desc += ` ${envInfo}`;
574
- lines.push(formatOption(flags, desc));
575
- const negationLine = formatNegationLine(opt);
576
- if (negationLine) lines.push(negationLine);
577
- }
578
- return lines.join("\n");
579
- }
580
- /**
581
- * Render a separate line for the custom negation option when a
582
- * `negationDescription` is provided. When no description is given, the
583
- * negation is shown inline by `formatFlags`.
584
- */
585
- function formatNegationLine(opt, indent = 0, extraDescPadding = 0) {
586
- if (!opt.negationDisplay || !opt.negationDescription) return null;
587
- return formatOption(styles.option(`--${opt.negationDisplay}`), `${opt.negationDescription} ${styles.dim(`(↔ --${opt.cliName})`)}`, indent, extraDescPadding);
588
- }
589
- /**
590
- * Render options for discriminated union with variants
591
- */
592
- function renderDiscriminatedUnionOptions(extracted, _command, lines) {
593
- const discriminator = extracted.discriminator;
594
- const variants = extracted.variants ?? [];
595
- const discriminatorField = extracted.fields.find((f) => f.name === discriminator);
596
- if (discriminatorField) {
597
- const variantValues = variants.map((v) => v.discriminatorValue).join("|");
598
- const flags = `${styles.option(`--${discriminator}`)} ${styles.placeholder(`<${variantValues}>`)}`;
599
- const description = extracted.description ?? discriminatorField.description ?? "Action to perform";
600
- lines.push(formatOption(flags, description));
601
- }
602
- const commonFields = /* @__PURE__ */ new Set();
603
- const allFieldNames = /* @__PURE__ */ new Set();
604
- for (const variant of variants) for (const field of variant.fields) allFieldNames.add(field.name);
605
- for (const fieldName of allFieldNames) {
606
- if (fieldName === discriminator) continue;
607
- if (variants.every((v) => v.fields.some((f) => f.name === fieldName))) commonFields.add(fieldName);
608
- }
609
- for (const fieldName of commonFields) {
610
- const field = extracted.fields.find((f) => f.name === fieldName);
611
- if (field && !field.positional) {
612
- const flags = formatFlags(field);
613
- let desc = field.description ?? "";
614
- if (field.defaultValue !== void 0) desc += ` ${styles.defaultValue(`(default: ${JSON.stringify(field.defaultValue)})`)}`;
615
- const envInfo = formatEnvInfo(field.env);
616
- if (envInfo) desc += ` ${envInfo}`;
617
- lines.push(formatOption(flags, desc));
618
- const negationLine = formatNegationLine(field);
619
- if (negationLine) lines.push(negationLine);
620
- }
621
- }
622
- for (const variant of variants) {
623
- const variantFields = variant.fields.filter((f) => f.name !== discriminator && !commonFields.has(f.name) && !f.positional);
624
- if (variantFields.length > 0) {
625
- lines.push("");
626
- const variantLabel = variant.description ? `${styles.dim("When")} ${styles.option(discriminator)}=${styles.bold(variant.discriminatorValue)}: ${variant.description}` : `${styles.dim("When")} ${styles.option(discriminator)}=${styles.bold(variant.discriminatorValue)}:`;
627
- lines.push(variantLabel);
628
- for (const field of variantFields) {
629
- const flags = formatFlags(field);
630
- let desc = field.description ?? "";
631
- if (field.defaultValue !== void 0) desc += ` ${styles.defaultValue(`(default: ${JSON.stringify(field.defaultValue)})`)}`;
632
- if (field.required) desc += ` ${styles.required("(required)")}`;
633
- const envInfo = formatEnvInfo(field.env);
634
- if (envInfo) desc += ` ${envInfo}`;
635
- lines.push(formatOption(flags, desc, 1));
636
- const negationLine = formatNegationLine(field, 1);
637
- if (negationLine) lines.push(negationLine);
638
- }
639
- }
640
- }
641
- return lines.join("\n");
642
- }
643
- /**
644
- * Render options for union with multiple options
645
- */
646
- function renderUnionOptions(extracted, _command, lines) {
647
- const unionOptions = extracted.unionOptions ?? [];
648
- const commonFields = /* @__PURE__ */ new Set();
649
- const allFieldNames = /* @__PURE__ */ new Set();
650
- for (const option of unionOptions) for (const field of option.fields) allFieldNames.add(field.name);
651
- for (const fieldName of allFieldNames) if (unionOptions.every((o) => o.fields.some((f) => f.name === fieldName))) commonFields.add(fieldName);
652
- for (const fieldName of commonFields) {
653
- const field = extracted.fields.find((f) => f.name === fieldName);
654
- if (field && !field.positional) {
655
- const flags = formatFlags(field);
656
- let desc = field.description ?? "";
657
- if (field.defaultValue !== void 0) desc += ` ${styles.defaultValue(`(default: ${JSON.stringify(field.defaultValue)})`)}`;
658
- const envInfo = formatEnvInfo(field.env);
659
- if (envInfo) desc += ` ${envInfo}`;
660
- lines.push(formatOption(flags, desc));
661
- const negationLine = formatNegationLine(field);
662
- if (negationLine) lines.push(negationLine);
663
- }
664
- }
665
- for (let i = 0; i < unionOptions.length; i++) {
666
- const option = unionOptions[i];
667
- if (!option) continue;
668
- const uniqueFields = option.fields.filter((f) => !commonFields.has(f.name) && !f.positional);
669
- const label = option.description ?? `Variant ${i + 1}`;
670
- if (uniqueFields.length > 0) {
671
- lines.push("");
672
- lines.push(` ${styles.bold(`${label}:`)}`);
673
- for (const field of uniqueFields) {
674
- const flags = formatFlags(field);
675
- let desc = field.description ?? "";
676
- if (field.defaultValue !== void 0) desc += ` ${styles.defaultValue(`(default: ${JSON.stringify(field.defaultValue)})`)}`;
677
- if (field.required) desc += ` ${styles.required("(required)")}`;
678
- const envInfo = formatEnvInfo(field.env);
679
- if (envInfo) desc += ` ${envInfo}`;
680
- lines.push(formatOption(flags, desc, 1));
681
- const negationLine = formatNegationLine(field, 1);
682
- if (negationLine) lines.push(negationLine);
683
- }
684
- } else {
685
- lines.push("");
686
- lines.push(` ${styles.bold(`${label}:`)}`);
687
- lines.push(` ${styles.dim(styles.italic("no options"))}`);
688
- }
689
- }
690
- return lines.join("\n");
691
- }
692
- /**
693
- * Format option flags (-v, --verbose <VALUE>)
694
- * Uses cliName (kebab-case) for display
695
- */
696
- function formatFlags(opt) {
697
- const aliasParts = [];
698
- if (opt.alias) {
699
- for (const alias of opt.alias) if (alias.length === 1) aliasParts.push(styles.option(`-${alias}`));
700
- }
701
- let longFlag = styles.option(`--${opt.cliName}`);
702
- if (opt.type !== "boolean") {
703
- const placeholder = opt.placeholder ?? opt.cliName.toUpperCase();
704
- longFlag += ` ${styles.placeholder(`<${placeholder}>`)}`;
705
- }
706
- aliasParts.push(longFlag);
707
- if (opt.alias) {
708
- for (const alias of opt.alias) if (alias.length > 1) {
709
- let longAlias = styles.option(`--${alias}`);
710
- if (opt.type !== "boolean") {
711
- const placeholder = opt.placeholder ?? opt.cliName.toUpperCase();
712
- longAlias += ` ${styles.placeholder(`<${placeholder}>`)}`;
713
- }
714
- aliasParts.push(longAlias);
715
- }
716
- }
717
- const aliasStr = aliasParts.join(", ");
718
- if (opt.type === "boolean" && opt.negationDisplay && !opt.negationDescription) return `${aliasStr} / ${styles.option(`--${opt.negationDisplay}`)}`;
719
- return aliasStr;
720
- }
721
- /**
722
- * Format environment variable info for help display
723
- */
724
- function formatEnvInfo(env) {
725
- if (!env) return "";
726
- const envNames = Array.isArray(env) ? env : [env];
727
- return styles.dim(`[env: ${envNames.join(", ")}]`);
728
- }
729
- /**
730
- * Strip ANSI escape codes from a string to get visual length
731
- */
732
- function stripAnsi(str) {
733
- return str.replace(/\x1B\[[0-9;]*m/g, "");
734
- }
735
- /**
736
- * Pad a string that may contain ANSI codes to a visual width
737
- */
738
- function padEndVisual(str, width) {
739
- const visualLength = stripAnsi(str).length;
740
- const padding = Math.max(0, width - visualLength);
741
- return str + " ".repeat(padding);
742
- }
743
- /**
744
- * Re-indent the continuation lines of a multi-line description so they align
745
- * under the description column. A `\n` in a description is treated as a hard
746
- * line break; every line after the first is padded to `column` spaces.
747
- */
748
- function indentDescription(description, column) {
749
- if (!description.includes("\n")) return description;
750
- const pad = " ".repeat(column);
751
- return description.split("\n").join(`\n${pad}`);
752
- }
753
- /**
754
- * Format a single option line
755
- * If flags exceed the column width, description is moved to the next line
756
- *
757
- * Descriptions may contain `\n` line breaks; continuation lines are indented to
758
- * stay aligned under the description column.
759
- */
760
- function formatOption(flags, description, indent = 0, extraDescPadding = 0) {
761
- const flagWidth = 32;
762
- const indentStr = " ".repeat(indent);
763
- const visualFlagLength = stripAnsi(flags).length;
764
- const effectiveFlagWidth = flagWidth - indent * 2 + extraDescPadding;
765
- const descColumn = effectiveFlagWidth + 2 + indent * 2;
766
- const desc = indentDescription(description, descColumn);
767
- if (visualFlagLength >= effectiveFlagWidth) return `${indentStr} ${flags}\n${" ".repeat(descColumn)}${desc}`;
768
- return `${indentStr} ${padEndVisual(flags, effectiveFlagWidth)}${desc}`;
769
- }
770
- /**
771
- * Format a single option field as a help line
772
- */
773
- function formatFieldLine(opt, indent = 0, extraDescPadding = 0) {
774
- const flags = formatFlags(opt);
775
- let desc = opt.description ?? "";
776
- if (opt.defaultValue !== void 0) desc += ` ${styles.defaultValue(`(default: ${JSON.stringify(opt.defaultValue)})`)}`;
777
- if (opt.required) desc += ` ${styles.required("(required)")}`;
778
- const envInfo = formatEnvInfo(opt.env);
779
- if (envInfo) desc += ` ${envInfo}`;
780
- return formatOption(flags, desc, indent, extraDescPadding);
781
- }
782
- /**
783
- * Render global options section
784
- */
785
- function renderGlobalOptions(globalExtracted) {
786
- const lines = [];
787
- for (const opt of globalExtracted.fields) {
788
- if (opt.positional) continue;
789
- lines.push(formatFieldLine(opt));
790
- const negationLine = formatNegationLine(opt);
791
- if (negationLine) lines.push(negationLine);
792
- }
793
- return lines.join("\n");
794
- }
795
- /**
796
- * Render options for a subcommand (used by showSubcommandOptions)
797
- */
798
- function renderSubcommandOptionsCompact(command, indent) {
799
- const lines = [];
800
- const extracted = getExtractedFields(command);
801
- if (extracted) {
802
- const options = extracted.fields.filter((a) => !a.positional);
803
- for (const opt of options) {
804
- const flags = formatFlags(opt);
805
- let desc = opt.description ?? "";
806
- if (opt.defaultValue !== void 0) desc += ` ${styles.defaultValue(`(default: ${JSON.stringify(opt.defaultValue)})`)}`;
807
- const envInfo = formatEnvInfo(opt.env);
808
- if (envInfo) desc += ` ${envInfo}`;
809
- lines.push(formatOption(flags, desc, indent, 2));
810
- const negationLine = formatNegationLine(opt, indent, 2);
811
- if (negationLine) lines.push(negationLine);
812
- }
813
- }
814
- return lines;
815
- }
816
- /**
817
- * Render subcommands recursively with their options (flat style)
818
- */
819
- function renderSubcommandsWithOptions(subCommands, parentPath, baseIndent) {
820
- const lines = [];
821
- for (const [name, subCmd] of getVisibleSubcommandEntries(subCommands)) {
822
- const cmd = resolveSubCommandMeta(subCmd);
823
- const fullPath = parentPath ? `${parentPath} ${name}` : name;
824
- const desc = cmd?.description ?? "";
825
- const aliases = cmd?.aliases;
826
- const displayName = aliases && aliases.length > 0 ? `${fullPath}, ${aliases.join(", ")}` : fullPath;
827
- lines.push(formatOption(styles.command(displayName), desc, baseIndent));
828
- if (cmd) {
829
- const optionLines = renderSubcommandOptionsCompact(cmd, baseIndent + 1);
830
- lines.push(...optionLines);
831
- const visibleNestedSubCommands = cmd.subCommands ? Object.fromEntries(getVisibleSubcommandEntries(cmd.subCommands)) : void 0;
832
- if (visibleNestedSubCommands && Object.keys(visibleNestedSubCommands).length > 0) {
833
- const nestedLines = renderSubcommandsWithOptions(visibleNestedSubCommands, fullPath, baseIndent);
834
- lines.push(...nestedLines);
835
- }
836
- }
837
- }
838
- return lines;
839
- }
840
- /**
841
- * Generate help text for a command
842
- *
843
- * @param command - The command to generate help for
844
- * @param options - Help generation options
845
- * @returns Formatted help text
846
- */
847
- function generateHelp(command, options) {
848
- const sections = [];
849
- const context = options.context;
850
- const displayName = buildFullCommandName(command, context);
851
- if (displayName) {
852
- let header = styles.commandName(displayName);
853
- if (context?.rootName && context.commandPath && context.commandPath.length > 0) {
854
- if (context.rootVersion) header += ` ${styles.version(`(${context.rootName} v${context.rootVersion})`)}`;
855
- else header += ` ${styles.version(`(${context.rootName})`)}`;
856
- } else if (context?.rootVersion) header += ` ${styles.version(`v${context.rootVersion}`)}`;
857
- sections.push(header);
858
- }
859
- if (context?.aliasFor) sections.push(styles.dim(`Alias for ${styles.commandName(context.aliasFor)}`));
860
- if (command.description) sections.push(command.description);
861
- if (!context?.aliasFor && command.aliases && command.aliases.length > 0) sections.push(`${styles.sectionHeader("Aliases:")} ${command.aliases.map((a) => styles.command(a)).join(", ")}`);
862
- sections.push(`${styles.sectionHeader("Usage:")} ${renderUsageLine(command, context)}`);
863
- const optionsText = renderOptions(command, options.descriptions, context);
864
- if (optionsText) sections.push(`${styles.sectionHeader("Options:")}\n${optionsText}`);
865
- if (context?.globalExtracted?.fields.length) sections.push(`${styles.sectionHeader("Global Options:")}\n${renderGlobalOptions(context.globalExtracted)}`);
866
- if (options.showSubcommands !== false && command.subCommands && getVisibleSubcommandEntries(command.subCommands).length > 0) {
867
- const currentPath = context?.commandPath?.join(" ") ?? "";
868
- const visibleSubCommands = Object.fromEntries(getVisibleSubcommandEntries(command.subCommands));
869
- if (options.showSubcommandOptions) {
870
- const subLines = renderSubcommandsWithOptions(visibleSubCommands, currentPath, 0);
871
- sections.push(`${styles.sectionHeader("Commands:")}\n${subLines.join("\n")}`);
872
- } else {
873
- const subLines = [];
874
- for (const [name, subCmd] of Object.entries(visibleSubCommands)) {
875
- const cmd = resolveSubCommandMeta(subCmd);
876
- const desc = cmd?.description ?? "";
877
- const fullName = currentPath ? `${currentPath} ${name}` : name;
878
- const aliases = cmd?.aliases;
879
- const displayName = aliases && aliases.length > 0 ? `${fullName}, ${aliases.join(", ")}` : fullName;
880
- subLines.push(formatOption(styles.command(displayName), desc));
881
- }
882
- sections.push(`${styles.sectionHeader("Commands:")}\n${subLines.join("\n")}`);
883
- }
884
- }
885
- if (command.examples && command.examples.length > 0) {
886
- const exampleLines = renderExamplesForHelp(command.examples, context);
887
- sections.push(`${styles.sectionHeader("Examples:")}\n${exampleLines}`);
888
- }
889
- if (command.notes) {
890
- const indented = renderMarkdown(command.notes).split("\n").map((line) => line === "" ? "" : ` ${line}`).join("\n");
891
- sections.push(`${styles.sectionHeader("Notes:")}\n${indented}`);
892
- }
893
- return `\n${sections.join("\n\n")}\n`;
894
- }
895
- /**
896
- * Render examples for CLI help output
897
- */
898
- function renderExamplesForHelp(examples, context) {
899
- const lines = [];
900
- const cmdPrefix = context?.rootName ? `${context.rootName} ` : "";
901
- const cmdPath = context?.commandPath?.join(" ") ?? "";
902
- const fullPrefix = cmdPath ? `${cmdPrefix}${cmdPath} ` : cmdPrefix;
903
- for (const example of examples) {
904
- lines.push(` ${styles.dim(example.desc).split("\n").join("\n ")}`);
905
- lines.push(` ${styles.dim("$")} ${fullPrefix}${example.cmd}`);
906
- if (example.output) for (const line of example.output.split("\n")) lines.push(` ${line}`);
907
- lines.push("");
908
- }
909
- if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
910
- return lines.join("\n");
911
- }
912
-
913
- //#endregion
914
- //#region ../core/src/validator/validation-errors.ts
915
- /**
916
- * Error thrown when positional argument configuration is invalid
917
- */
918
- var PositionalConfigError = class extends Error {
919
- constructor(message) {
920
- super(message);
921
- this.name = "PositionalConfigError";
922
- }
923
- };
924
- /**
925
- * Error thrown when a reserved alias is used
926
- */
927
- var ReservedAliasError = class extends Error {
928
- constructor(message) {
929
- super(message);
930
- this.name = "ReservedAliasError";
931
- }
932
- };
933
- /**
934
- * Error thrown when duplicate field names are detected
935
- */
936
- var DuplicateFieldError = class extends Error {
937
- constructor(message) {
938
- super(message);
939
- this.name = "DuplicateFieldError";
940
- }
941
- };
942
- /**
943
- * Error thrown when duplicate aliases are detected
944
- */
945
- var DuplicateAliasError = class extends Error {
946
- constructor(message) {
947
- super(message);
948
- this.name = "DuplicateAliasError";
949
- }
950
- };
951
- /**
952
- * Error thrown when fields are case variants of each other (e.g. "my-option" and "myOption")
953
- */
954
- var CaseVariantCollisionError = class extends Error {
955
- constructor(message) {
956
- super(message);
957
- this.name = "CaseVariantCollisionError";
958
- }
959
- };
960
- /**
961
- * Error thrown when a custom boolean negation name collides with another
962
- * field's name, cliName, alias, or another field's negation (including
963
- * derived camelCase variants).
964
- */
965
- var DuplicateNegationError = class extends Error {
966
- constructor(message) {
967
- super(message);
968
- this.name = "DuplicateNegationError";
969
- }
970
- };
971
- /**
972
- * Error thrown when a field name collides with a reserved, framework-injected
973
- * key on the final args object (e.g. `$source`).
974
- */
975
- var ReservedFieldNameError = class extends Error {
976
- constructor(message) {
977
- super(message);
978
- this.name = "ReservedFieldNameError";
979
- }
980
- };
981
- /**
982
- * Error thrown when a global field and a same-named local field have
983
- * different definitions (per `extractFields()`'s type bucket, whether the
984
- * field is positional, and enum values). Only exactly-matching definitions
985
- * are allowed to share a name across global/local schemas — anything else
986
- * is rejected at validation time rather than risking a value from one
987
- * schema silently flowing into the other.
988
- */
989
- var FieldTypeConflictError = class extends Error {
990
- constructor(message) {
991
- super(message);
992
- this.name = "FieldTypeConflictError";
993
- }
994
- };
995
-
996
- //#endregion
997
- //#region ../core/src/validator/command-validator.ts
998
- /**
999
- * Check for duplicate field names
1000
- */
1001
- function checkDuplicateFields(extracted, commandPath) {
1002
- const errors = [];
1003
- const seenNames = /* @__PURE__ */ new Map();
1004
- for (const field of extracted.fields) {
1005
- if (seenNames.has(field.name)) errors.push({
1006
- commandPath,
1007
- type: "duplicate_field",
1008
- message: `Duplicate field name "${field.name}" detected.`,
1009
- field: field.name
1010
- });
1011
- seenNames.set(field.name, field.name);
1012
- }
1013
- return errors;
1014
- }
1015
- /**
1016
- * Check for case-variant collisions (e.g. "my-option" and "myOption" defined simultaneously)
1017
- */
1018
- function checkCaseVariantCollisions(extracted, commandPath) {
1019
- const errors = [];
1020
- const canonicalMap = /* @__PURE__ */ new Map();
1021
- for (const field of extracted.fields) {
1022
- const camel = toCamelCase(field.name);
1023
- const existing = canonicalMap.get(camel);
1024
- if (existing && existing !== field.name) errors.push({
1025
- commandPath,
1026
- type: "case_variant_collision",
1027
- message: `Fields "${existing}" and "${field.name}" are case variants of each other and would collide.`,
1028
- field: field.name
1029
- });
1030
- canonicalMap.set(camel, field.name);
1031
- }
1032
- return errors;
1033
- }
1034
- /**
1035
- * Check for duplicate aliases and alias-field name conflicts
1036
- */
1037
- function checkDuplicateAliases(extracted, commandPath) {
1038
- const errors = [];
1039
- const seenAliases = /* @__PURE__ */ new Map();
1040
- const fieldNames = new Set(extracted.fields.map((f) => f.name));
1041
- const cliNames = new Set(extracted.fields.map((f) => f.cliName));
1042
- const registerAlias = (alias, fieldName, isDerived) => {
1043
- if (fieldNames.has(alias) || cliNames.has(alias)) errors.push({
1044
- commandPath,
1045
- type: "duplicate_alias",
1046
- message: `Alias "${alias}" for field "${fieldName}" conflicts with existing field name or CLI name "${alias}".`,
1047
- field: fieldName
1048
- });
1049
- const existingField = seenAliases.get(alias);
1050
- if (existingField && existingField !== fieldName) {
1051
- const qualifier = isDerived ? " (derived camelCase variant)" : "";
1052
- errors.push({
1053
- commandPath,
1054
- type: "duplicate_alias",
1055
- message: `Duplicate alias "${alias}"${qualifier} detected. Both "${existingField}" and "${fieldName}" use the same alias.`,
1056
- field: fieldName
1057
- });
1058
- }
1059
- seenAliases.set(alias, fieldName);
1060
- };
1061
- for (const field of extracted.fields) {
1062
- const allAliases = getAllAliases(field);
1063
- if (allAliases.length === 0) continue;
1064
- for (const alias of allAliases) {
1065
- registerAlias(alias, field.name, false);
1066
- if (alias.length > 1 && alias.includes("-")) {
1067
- const camelVariant = toCamelCase(alias);
1068
- if (camelVariant !== alias && !fieldNames.has(camelVariant)) registerAlias(camelVariant, field.name, true);
1069
- }
1070
- }
1071
- }
1072
- return errors;
1073
- }
1074
- /**
1075
- * Check for collisions involving custom boolean `negation` names
1076
- */
1077
- function checkDuplicateNegations(extracted, commandPath) {
1078
- const errors = [];
1079
- const claimed = /* @__PURE__ */ new Map();
1080
- const claim = (name, fieldName, kind) => {
1081
- if (!claimed.has(name)) claimed.set(name, {
1082
- field: fieldName,
1083
- kind
1084
- });
1085
- };
1086
- for (const field of extracted.fields) {
1087
- claim(field.name, field.name, "field name");
1088
- if (field.name.includes("-")) {
1089
- const camelName = toCamelCase(field.name);
1090
- if (camelName !== field.name) claim(camelName, field.name, "field name");
1091
- }
1092
- if (field.cliName !== field.name) claim(field.cliName, field.name, "CLI name");
1093
- if (field.cliName.includes("-")) {
1094
- const camelCli = toCamelCase(field.cliName);
1095
- if (camelCli !== field.cliName) claim(camelCli, field.name, "CLI name");
1096
- }
1097
- for (const alias of getAllAliases(field)) {
1098
- claim(alias, field.name, "alias");
1099
- if (alias.length > 1 && alias.includes("-")) {
1100
- const camelVariant = toCamelCase(alias);
1101
- if (camelVariant !== alias) claim(camelVariant, field.name, "alias");
1102
- }
1103
- }
1104
- if (field.type === "boolean" && field.negation === true) {
1105
- const defaultKebab = `no-${field.cliName}`;
1106
- claim(defaultKebab, field.name, "default negation");
1107
- const camelBase = toCamelCase(field.cliName);
1108
- const defaultCamel = `no${camelBase[0]?.toUpperCase() ?? ""}${camelBase.slice(1)}`;
1109
- if (defaultCamel !== defaultKebab) claim(defaultCamel, field.name, "default negation");
1110
- }
1111
- }
1112
- const seenNegations = /* @__PURE__ */ new Map();
1113
- const register = (name, fieldName, isDerived) => {
1114
- const claim = claimed.get(name);
1115
- if (claim) {
1116
- const qualifier = isDerived ? " (derived camelCase variant)" : "";
1117
- const conflict = claim.field === fieldName ? `the same field's own ${claim.kind} "${name}"` : `${claim.kind} "${name}" of field "${claim.field}"`;
1118
- errors.push({
1119
- commandPath,
1120
- type: "duplicate_negation",
1121
- message: `Negation "${name}"${qualifier} for field "${fieldName}" conflicts with ${conflict}.`,
1122
- field: fieldName
1123
- });
1124
- }
1125
- const existing = seenNegations.get(name);
1126
- if (existing && existing !== fieldName) {
1127
- const qualifier = isDerived ? " (derived camelCase variant)" : "";
1128
- errors.push({
1129
- commandPath,
1130
- type: "duplicate_negation",
1131
- message: `Duplicate negation "${name}"${qualifier} detected. Both "${existing}" and "${fieldName}" use the same negation name.`,
1132
- field: fieldName
1133
- });
1134
- }
1135
- seenNegations.set(name, fieldName);
1136
- };
1137
- for (const field of extracted.fields) {
1138
- if (typeof field.negation !== "string") continue;
1139
- register(field.negation, field.name, false);
1140
- if (field.negation.includes("-")) {
1141
- const camelVariant = toCamelCase(field.negation);
1142
- if (camelVariant !== field.negation) register(camelVariant, field.name, true);
1143
- }
1144
- }
1145
- return errors;
1146
- }
1147
- /**
1148
- * Check positional argument configuration
1149
- */
1150
- function checkPositionalConfig(extracted, commandPath) {
1151
- const errors = [];
1152
- const positionalFields = extracted.fields.filter((f) => f.positional);
1153
- let foundArrayPositional = null;
1154
- let foundOptionalPositional = null;
1155
- for (const field of positionalFields) {
1156
- if (foundArrayPositional !== null) errors.push({
1157
- commandPath,
1158
- type: "positional_config",
1159
- message: `Positional argument "${field.name}" cannot follow array positional argument "${foundArrayPositional}".`,
1160
- field: field.name
1161
- });
1162
- if (field.type === "array" && foundOptionalPositional !== null) errors.push({
1163
- commandPath,
1164
- type: "positional_config",
1165
- message: `Array positional "${field.name}" cannot be used with optional positional "${foundOptionalPositional}" (ambiguous parsing).`,
1166
- field: field.name
1167
- });
1168
- if (foundOptionalPositional !== null && field.required) errors.push({
1169
- commandPath,
1170
- type: "positional_config",
1171
- message: `Required positional "${field.name}" cannot follow optional positional "${foundOptionalPositional}".`,
1172
- field: field.name
1173
- });
1174
- if (field.type === "array") foundArrayPositional = field.name;
1175
- if (!field.required) foundOptionalPositional = field.name;
1176
- }
1177
- return errors;
1178
- }
1179
- /**
1180
- * Check for reserved aliases used without override flag
1181
- */
1182
- function checkReservedAliases(extracted, commandPath) {
1183
- const errors = [];
1184
- for (const field of extracted.fields) {
1185
- if (field.overrideBuiltinAlias === true) continue;
1186
- for (const alias of getAllAliases(field)) if (alias === "h" || alias === "H") errors.push({
1187
- commandPath,
1188
- type: "reserved_alias",
1189
- message: `Alias "${alias}" is reserved for --${alias === "h" ? "help" : "help-all"}.`,
1190
- field: field.name
1191
- });
1192
- }
1193
- return errors;
1194
- }
1195
- /**
1196
- * Check for field names starting with `$`.
1197
- *
1198
- * The `$` prefix is reserved for framework-injected helpers on the final
1199
- * args object (e.g. `$source`). It is also impractical as a real CLI flag
1200
- * since an unquoted `$name` is expanded by the shell before it ever reaches
1201
- * the program, so this is rejected outright rather than merely discouraged.
1202
- *
1203
- * Aliases can't start with `$` (schema extraction already restricts alias
1204
- * characters to `[A-Za-z0-9-]`), and `cliName` is derived from `name` via
1205
- * `toKebabCase`, which never strips or moves a leading `$` — so checking
1206
- * `field.name` alone covers every way `$` could reach the final args object.
1207
- */
1208
- function checkReservedFieldNames(extracted, commandPath) {
1209
- const errors = [];
1210
- for (const field of extracted.fields) if (field.name.startsWith("$")) errors.push({
1211
- commandPath,
1212
- type: "reserved_field_name",
1213
- message: `Field "${field.name}" starts with "$", which is reserved for framework-injected helpers (e.g. $source).`,
1214
- field: field.name
1215
- });
1216
- return errors;
1217
- }
1218
- /**
1219
- * Validate that no duplicate field names exist
1220
- *
1221
- * @param extracted - Extracted fields from schema
1222
- * @throws {DuplicateFieldError} If duplicate field names are found
1223
- */
1224
- function validateDuplicateFields(extracted) {
1225
- const errors = checkDuplicateFields(extracted, []);
1226
- if (errors.length > 0) {
1227
- const field = errors[0]?.field ?? "unknown";
1228
- throw new DuplicateFieldError(`Duplicate field name "${field}" detected. Each field must have a unique name.`);
1229
- }
1230
- }
1231
- /**
1232
- * Validate that no duplicate aliases exist
1233
- *
1234
- * Also checks for conflicts between aliases and field names
1235
- *
1236
- * @param extracted - Extracted fields from schema
1237
- * @throws {DuplicateAliasError} If duplicate aliases are found or alias conflicts with field name
1238
- */
1239
- function validateDuplicateAliases(extracted) {
1240
- const errors = checkDuplicateAliases(extracted, []);
1241
- if (errors.length > 0) {
1242
- const err = errors[0];
1243
- throw new DuplicateAliasError(err.message);
1244
- }
1245
- }
1246
- /**
1247
- * Validate positional argument configuration
1248
- *
1249
- * Rules:
1250
- * - Array positional arguments must be the last positional
1251
- * - No positional arguments can follow an array positional
1252
- * - Required positional arguments cannot follow optional positional arguments
1253
- * - Array positional and optional positional cannot be used together (ambiguous parsing)
1254
- *
1255
- * @param extracted - Extracted fields from schema
1256
- * @throws {PositionalConfigError} If configuration is invalid
1257
- */
1258
- function validatePositionalConfig(extracted) {
1259
- const errors = checkPositionalConfig(extracted, []);
1260
- if (errors.length > 0) {
1261
- const err = errors[0];
1262
- throw new PositionalConfigError(err.message);
1263
- }
1264
- }
1265
- /**
1266
- * Validate that no reserved aliases are used without explicit override
1267
- *
1268
- * Reserved aliases:
1269
- * - 'h' is reserved for --help
1270
- * - 'H' is reserved for --help-all
1271
- *
1272
- * Users can override these by setting overrideBuiltinAlias: true
1273
- *
1274
- * @param extracted - Extracted fields from schema
1275
- * @param _hasSubCommands - Whether the command has subcommands (reserved for future use)
1276
- * @throws {ReservedAliasError} If a reserved alias is used without override flag
1277
- */
1278
- function validateReservedAliases(extracted, _hasSubCommands) {
1279
- const errors = checkReservedAliases(extracted, []);
1280
- if (errors.length > 0) {
1281
- const field = errors[0].field ?? "unknown";
1282
- const found = extracted.fields.find((f) => f.name === field);
1283
- const alias = (found ? getAllAliases(found) : []).find((a) => a === "h" || a === "H") ?? "h";
1284
- throw new ReservedAliasError(`Alias "${alias}" is reserved for --${alias === "h" ? "help" : "help-all"}. To override this, set { overrideBuiltinAlias: true } for "${field}" and keep the alias where it is currently defined (in alias or hiddenAlias).`);
1285
- }
1286
- }
1287
- /**
1288
- * Validate that no field name starts with `$`
1289
- *
1290
- * The `$` prefix is reserved for framework-injected helpers on the final
1291
- * args object (e.g. `$source`), and is unusable as a real CLI flag anyway
1292
- * since an unquoted `$name` gets shell-expanded before the program sees it.
1293
- *
1294
- * Checking `field.name` alone is sufficient: aliases can't start with `$`
1295
- * (schema extraction already restricts alias characters to `[A-Za-z0-9-]`),
1296
- * and `cliName` is derived from `name` via `toKebabCase`, which never strips
1297
- * or moves a leading `$`. See {@link checkReservedFieldNames}.
1298
- *
1299
- * @param extracted - Extracted fields from schema
1300
- * @throws {ReservedFieldNameError} If a field name starts with "$"
1301
- */
1302
- function validateReservedFieldNames(extracted) {
1303
- const errors = checkReservedFieldNames(extracted, []);
1304
- if (errors.length > 0) {
1305
- const err = errors[0];
1306
- throw new ReservedFieldNameError(err.message);
1307
- }
1308
- }
1309
- /**
1310
- * Validate that custom boolean negation names do not collide with anything
1311
- *
1312
- * @param extracted - Extracted fields from schema
1313
- * @throws {DuplicateNegationError} If a colliding negation is found
1314
- */
1315
- function validateDuplicateNegations(extracted) {
1316
- const errors = checkDuplicateNegations(extracted, []);
1317
- if (errors.length > 0) {
1318
- const err = errors[0];
1319
- throw new DuplicateNegationError(err.message);
1320
- }
1321
- }
1322
- /**
1323
- * Validate that no case-variant collisions exist
1324
- *
1325
- * @param extracted - Extracted fields from schema
1326
- * @throws {CaseVariantCollisionError} If case-variant collisions are found
1327
- */
1328
- function validateCaseVariantCollisions(extracted) {
1329
- const errors = checkCaseVariantCollisions(extracted, []);
1330
- if (errors.length > 0) {
1331
- const err = errors[0];
1332
- throw new CaseVariantCollisionError(err.message);
1333
- }
1334
- }
1335
- /**
1336
- * Check whether two same-named fields from different schemas (e.g. global
1337
- * args and command args) have identical definitions. Only the facts
1338
- * `extractFields()` already exposes are compared: the coarse type bucket,
1339
- * whether the field is positional, and, for enum-like fields, the exact
1340
- * set of allowed values. Anything `extractFields()` can't see (e.g. a
1341
- * `.refine()`) is intentionally not compared — this is a coarse, cheap
1342
- * equality check, not a full schema comparison.
1343
- */
1344
- function fieldsAreIdentical(a, b) {
1345
- if (a.type !== b.type) return false;
1346
- if (a.positional !== b.positional) return false;
1347
- const aEnum = a.enumValues;
1348
- const bEnum = b.enumValues;
1349
- if (!aEnum && !bEnum) return true;
1350
- if (!aEnum || !bEnum) return false;
1351
- const aSet = new Set(aEnum);
1352
- const bSet = new Set(bEnum);
1353
- if (aSet.size !== bSet.size) return false;
1354
- for (const value of aSet) if (!bSet.has(value)) return false;
1355
- return true;
1356
- }
1357
- /**
1358
- * Check for cross-schema collisions between two schemas (e.g., global args
1359
- * and command args): neither a case-variant collision (same canonical name,
1360
- * different spelling) nor a same-named field with a different definition
1361
- * (same spelling, but the two schemas don't agree on what values are valid).
1362
- */
1363
- function checkCrossSchemaCollisions(extractedA, extractedB, commandPath) {
1364
- const errors = [];
1365
- const canonicalMap = /* @__PURE__ */ new Map();
1366
- for (const field of extractedA.fields) canonicalMap.set(toCamelCase(field.name), field);
1367
- for (const field of extractedB.fields) {
1368
- const camel = toCamelCase(field.name);
1369
- const existing = canonicalMap.get(camel);
1370
- if (!existing) continue;
1371
- if (existing.name !== field.name) {
1372
- errors.push({
1373
- commandPath,
1374
- type: "case_variant_collision",
1375
- message: `Global field "${existing.name}" and command field "${field.name}" are case variants of each other and would collide.`,
1376
- field: field.name
1377
- });
1378
- continue;
1379
- }
1380
- if (!fieldsAreIdentical(existing, field)) errors.push({
1381
- commandPath,
1382
- type: "field_type_conflict",
1383
- message: `Global field "${existing.name}" and command field "${field.name}" share the same name but have different definitions.`,
1384
- field: field.name
1385
- });
1386
- }
1387
- return errors;
1388
- }
1389
- /**
1390
- * Validate that no cross-schema collisions exist between two schemas
1391
- * (e.g., global args and command args): neither a case-variant collision
1392
- * (same canonical name, different spelling) nor a same-named field with a
1393
- * different definition (same spelling, but the two schemas don't agree on
1394
- * what values are valid).
1395
- *
1396
- * @param extractedA - Extracted fields from first schema (e.g., global args)
1397
- * @param extractedB - Extracted fields from second schema (e.g., command args)
1398
- * @throws {CaseVariantCollisionError} If cross-schema case-variant collisions are found
1399
- * @throws {FieldTypeConflictError} If a same-named field has a different definition on each schema
1400
- */
1401
- function validateCrossSchemaCollisions(extractedA, extractedB) {
1402
- const err = checkCrossSchemaCollisions(extractedA, extractedB, [])[0];
1403
- if (!err) return;
1404
- if (err.type === "case_variant_collision") throw new CaseVariantCollisionError(err.message);
1405
- throw new FieldTypeConflictError(err.message);
1406
- }
1407
- /**
1408
- * Collect validation errors for a single command's schema (non-throwing)
1409
- */
1410
- function collectSchemaErrors(extracted, _hasSubCommands, commandPath) {
1411
- return [
1412
- ...checkDuplicateFields(extracted, commandPath),
1413
- ...checkCaseVariantCollisions(extracted, commandPath),
1414
- ...checkDuplicateAliases(extracted, commandPath),
1415
- ...checkDuplicateNegations(extracted, commandPath),
1416
- ...checkPositionalConfig(extracted, commandPath),
1417
- ...checkReservedAliases(extracted, commandPath),
1418
- ...checkReservedFieldNames(extracted, commandPath)
1419
- ];
1420
- }
1421
- /**
1422
- * Check for alias conflicts within subcommands
1423
- * - Aliases must not conflict with subcommand names
1424
- * - Aliases must not conflict with other aliases
1425
- */
1426
- function checkSubCommandAliasConflicts(command, commandPath) {
1427
- const errors = [];
1428
- if (!command.subCommands) return errors;
1429
- const nameToOwner = /* @__PURE__ */ new Map();
1430
- for (const [name] of Object.entries(command.subCommands)) nameToOwner.set(name, name);
1431
- for (const [name, subCmdValue] of Object.entries(command.subCommands)) {
1432
- const resolved = isLazyCommand(subCmdValue) ? subCmdValue.meta : typeof subCmdValue !== "function" ? subCmdValue : null;
1433
- if (!resolved?.aliases) continue;
1434
- const subCommandPath = [...commandPath, name];
1435
- for (const alias of resolved.aliases) {
1436
- if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(alias)) {
1437
- errors.push({
1438
- commandPath: subCommandPath,
1439
- type: "invalid_alias",
1440
- message: `Alias "${alias}" is invalid. Aliases must start with an alphanumeric character and contain only alphanumeric characters, hyphens, or underscores.`,
1441
- field: name
1442
- });
1443
- continue;
1444
- }
1445
- if (alias === name) {
1446
- errors.push({
1447
- commandPath: subCommandPath,
1448
- type: "duplicate_alias",
1449
- message: `Alias "${alias}" conflicts with its own name.`,
1450
- field: name
1451
- });
1452
- continue;
1453
- }
1454
- const existing = nameToOwner.get(alias);
1455
- if (existing) {
1456
- if (existing === name) errors.push({
1457
- commandPath: subCommandPath,
1458
- type: "duplicate_alias",
1459
- message: `Alias "${alias}" is duplicated within the alias list.`,
1460
- field: name
1461
- });
1462
- else errors.push({
1463
- commandPath: subCommandPath,
1464
- type: "duplicate_alias",
1465
- message: `Alias "${alias}" conflicts with existing subcommand or alias "${existing}".`,
1466
- field: name
1467
- });
1468
- } else nameToOwner.set(alias, name);
1469
- }
1470
- }
1471
- return errors;
1472
- }
1473
- /**
1474
- * Check that a subcommand's own `.name` matches the key it is registered
1475
- * under in its parent's `subCommands` record.
1476
- *
1477
- * Routing (`resolveSubcommand`/`resolveSubcommandWithAlias`), `commandPath`,
1478
- * the "Alias for X" help line, shell completion, and `run()`'s
1479
- * `args.$invocation` all key off the registration key — none of them read a
1480
- * subcommand's own `.name` once it is nested. If the two disagree, a direct
1481
- * key match is indistinguishable from an alias match by name alone, so
1482
- * `$invocation`/`aliasFor` would report the key as if it were the canonical
1483
- * name even though the command's own `.name` says otherwise.
1484
- *
1485
- * Takes the already-resolved subcommand's name rather than resolving it
1486
- * itself: `validateCommand`'s recursive loop already awaits
1487
- * `resolveLazyCommand` for every subcommand (including pure async
1488
- * subcommand functions) to validate its nested schema, so by the time this
1489
- * runs the full `.name` is available at no extra cost -- unlike
1490
- * `checkSubCommandAliasConflicts`, which must stay synchronous because CLI
1491
- * argv routing resolves aliases before deciding whether to load a subcommand
1492
- * at all.
1493
- */
1494
- function checkSubCommandKeyNameMismatch(name, resolvedName, commandPath) {
1495
- if (name === resolvedName) return [];
1496
- return [{
1497
- commandPath: [...commandPath, name],
1498
- type: "subcommand_key_name_mismatch",
1499
- message: `Subcommand registered as "${name}" but its own name is "${resolvedName}".`,
1500
- field: name
1501
- }];
1502
- }
1503
- /**
1504
- * Validate a command and all its subcommands recursively
1505
- *
1506
- * This function collects all validation errors without throwing,
1507
- * making it suitable for test assertions.
1508
- *
1509
- * @param command - The command to validate
1510
- * @param options - Validation options
1511
- * @returns Validation result with all errors collected
1512
- *
1513
- * @example
1514
- * ```ts
1515
- * const result = await validateCommand(myCommand);
1516
- * if (!result.valid) {
1517
- * console.error(result.errors);
1518
- * }
1519
- * ```
1520
- */
1521
- async function validateCommand(command, options = {}) {
1522
- const commandPath = options.commandPath ?? [command.name];
1523
- const errors = [];
1524
- const hasSubCommands = command.subCommands ? Object.keys(command.subCommands).length > 0 : false;
1525
- const globalExtracted = options.globalArgs ? extractFields(options.globalArgs) : void 0;
1526
- if (command.args) {
1527
- const extracted = extractFields(command.args);
1528
- errors.push(...collectSchemaErrors(extracted, hasSubCommands, commandPath));
1529
- if (globalExtracted) errors.push(...checkCrossSchemaCollisions(globalExtracted, extracted, commandPath));
1530
- }
1531
- errors.push(...checkSubCommandAliasConflicts(command, commandPath));
1532
- if (command.subCommands) for (const [name, subCmd] of Object.entries(command.subCommands)) {
1533
- const resolvedSubCmd = await resolveLazyCommand(subCmd);
1534
- errors.push(...checkSubCommandKeyNameMismatch(name, resolvedSubCmd.name, commandPath));
1535
- const subResult = await validateCommand(resolvedSubCmd, {
1536
- commandPath: [...commandPath, name],
1537
- ...options.globalArgs ? { globalArgs: options.globalArgs } : {}
1538
- });
1539
- if (!subResult.valid) errors.push(...subResult.errors);
1540
- }
1541
- if (errors.length === 0) return { valid: true };
1542
- return {
1543
- valid: false,
1544
- errors
1545
- };
1546
- }
1547
- /**
1548
- * Format command validation errors for display
1549
- *
1550
- * @param errors - Array of validation errors
1551
- * @returns Formatted error message
1552
- */
1553
- function formatCommandValidationErrors(errors) {
1554
- if (errors.length === 0) return "";
1555
- const lines = ["Command definition errors:"];
1556
- for (const error of errors) {
1557
- const path = error.commandPath.join(" > ");
1558
- lines.push(` - [${path}] ${error.message}`);
1559
- }
1560
- return lines.join("\n");
1561
- }
1562
-
1563
- //#endregion
1564
- //#region ../core/src/parser/long-option-resolver.ts
1565
- /** Return a long option's name without leading dashes or an inline value. */
1566
- function getLongOptionName(arg) {
1567
- const equalsIndex = arg.indexOf("=");
1568
- return equalsIndex >= 0 ? arg.slice(2, equalsIndex) : arg.slice(2);
1569
- }
1570
- function resolveLongOption(arg, lookup) {
1571
- const withoutDashes = getLongOptionName(arg);
1572
- if (!arg.includes("=")) {
1573
- const negatedField = lookup.negationMap.get(withoutDashes);
1574
- if (negatedField && lookup.booleanFlags.has(negatedField)) return {
1575
- resolvedName: negatedField,
1576
- withoutDashes,
1577
- isNegated: true,
1578
- isCustomNegation: true,
1579
- isSuppressedNegation: false
1580
- };
1581
- }
1582
- const hasEquals = arg.includes("=");
1583
- if (!hasEquals && withoutDashes.startsWith("no-")) {
1584
- const flagName = withoutDashes.slice(3);
1585
- if (flagName === flagName.toLowerCase()) {
1586
- const resolvedName = lookup.aliasMap.get(flagName) ?? flagName;
1587
- if (lookup.booleanFlags.has(resolvedName)) {
1588
- const asIsResolved = lookup.aliasMap.get(withoutDashes) ?? withoutDashes;
1589
- if (!lookup.definedNames.has(asIsResolved)) {
1590
- if (lookup.defaultNegationDisabledFields.has(resolvedName)) return {
1591
- resolvedName,
1592
- withoutDashes,
1593
- isNegated: false,
1594
- isCustomNegation: false,
1595
- isSuppressedNegation: true
1596
- };
1597
- return {
1598
- resolvedName,
1599
- withoutDashes,
1600
- isNegated: true,
1601
- isCustomNegation: false,
1602
- isSuppressedNegation: false
1603
- };
1604
- }
1605
- }
1606
- }
1607
- }
1608
- if (!hasEquals && withoutDashes.length > 2 && withoutDashes.startsWith("no") && /[A-Z]/.test(withoutDashes[2])) {
1609
- const camelFlagName = withoutDashes[2].toLowerCase() + withoutDashes.slice(3);
1610
- const resolvedName = lookup.aliasMap.get(camelFlagName) ?? camelFlagName;
1611
- if (lookup.booleanFlags.has(resolvedName)) {
1612
- const asIsResolved = lookup.aliasMap.get(withoutDashes) ?? withoutDashes;
1613
- if (!lookup.definedNames.has(asIsResolved)) {
1614
- if (lookup.defaultNegationDisabledFields.has(resolvedName)) return {
1615
- resolvedName,
1616
- withoutDashes,
1617
- isNegated: false,
1618
- isCustomNegation: false,
1619
- isSuppressedNegation: true
1620
- };
1621
- return {
1622
- resolvedName,
1623
- withoutDashes,
1624
- isNegated: true,
1625
- isCustomNegation: false,
1626
- isSuppressedNegation: false
1627
- };
1628
- }
1629
- }
1630
- }
1631
- return {
1632
- resolvedName: lookup.aliasMap.get(withoutDashes) ?? withoutDashes,
1633
- withoutDashes,
1634
- isNegated: false,
1635
- isCustomNegation: false,
1636
- isSuppressedNegation: false
1637
- };
1638
- }
1639
-
1640
- //#endregion
1641
- //#region ../core/src/parser/argv-parser.ts
1642
- /**
1643
- * Coerce a `--flag=value` string to a boolean for boolean-typed flags.
1644
- * Values other than "true"/"false" are passed through unchanged so
1645
- * downstream validation reports the invalid input instead of silently
1646
- * guessing.
1647
- */
1648
- function coerceBoolean(value) {
1649
- if (value === "true") return true;
1650
- if (value === "false") return false;
1651
- return value;
1652
- }
1653
- /**
1654
- * Matches tokens that look like a negative number (e.g. "-5", "-5.2", "-.5"),
1655
- * as opposed to another flag. Used to decide whether a dash-prefixed token
1656
- * should still be consumed as a value-taking option's value.
1657
- */
1658
- const NEGATIVE_NUMBER_PATTERN = /^-\.?\d/;
1659
- function looksLikeNegativeNumber(value) {
1660
- return NEGATIVE_NUMBER_PATTERN.test(value);
1661
- }
1662
- /**
1663
- * Parse argv into a flat record
1664
- *
1665
- * Supports:
1666
- * - Long options: --flag, --flag=value, --flag value
1667
- * - Short options: -f, -f=value, -f value
1668
- * - Combined short options: -abc (treated as -a -b -c if all are boolean)
1669
- * - Positional arguments
1670
- * - -- to stop parsing options
1671
- * - Boolean negation: --no-flag, --noFlag (requires `booleanFlags` and `negation: true`)
1672
- *
1673
- * **Note:** When using negation detection (`--noFlag` / `--no-flag`),
1674
- * supply `definedNames` so that options whose names happen to start with
1675
- * "no" (e.g. `noDryRun`) are not mistaken for negation of another flag.
1676
- * Without `definedNames`, all `--noX` forms matching a boolean flag will
1677
- * be treated as negation.
1678
- *
1679
- * @param argv - Command line arguments
1680
- * @param options - Parser options
1681
- * @returns Parsed arguments
1682
- */
1683
- function parseArgv(argv, options = {}) {
1684
- const { aliasMap = /* @__PURE__ */ new Map(), booleanFlags = /* @__PURE__ */ new Set(), arrayFlags = /* @__PURE__ */ new Set(), definedNames = /* @__PURE__ */ new Set(), negationMap = /* @__PURE__ */ new Map(), defaultNegationDisabledFields: configuredDefaultNegationDisabledFields } = options;
1685
- const longOptionLookup = {
1686
- aliasMap,
1687
- booleanFlags,
1688
- definedNames,
1689
- negationMap,
1690
- defaultNegationDisabledFields: configuredDefaultNegationDisabledFields ?? new Set(booleanFlags)
1691
- };
1692
- const result = {
1693
- options: {},
1694
- positionals: [],
1695
- rest: []
1696
- };
1697
- let i = 0;
1698
- let stopParsing = false;
1699
- const setOption = (name, value) => {
1700
- const resolvedName = aliasMap.get(name) ?? name;
1701
- if (arrayFlags.has(resolvedName)) {
1702
- const existing = result.options[resolvedName];
1703
- if (Array.isArray(existing)) existing.push(value);
1704
- else if (existing !== void 0) result.options[resolvedName] = [existing, value];
1705
- else result.options[resolvedName] = [value];
1706
- } else result.options[resolvedName] = value;
1707
- };
1708
- while (i < argv.length) {
1709
- const arg = argv[i];
1710
- if (stopParsing) {
1711
- result.rest.push(arg);
1712
- i++;
1713
- continue;
1714
- }
1715
- if (arg === "--") {
1716
- stopParsing = true;
1717
- i++;
1718
- continue;
1719
- }
1720
- if (arg.startsWith("--")) {
1721
- const withoutDashes = arg.slice(2);
1722
- const resolution = resolveLongOption(arg, longOptionLookup);
1723
- if (resolution.isSuppressedNegation) {
1724
- setOption(resolution.withoutDashes, true);
1725
- i++;
1726
- continue;
1727
- }
1728
- if (resolution.isNegated) {
1729
- setOption(resolution.resolvedName, false);
1730
- i++;
1731
- continue;
1732
- }
1733
- const eqIndex = withoutDashes.indexOf("=");
1734
- if (eqIndex !== -1) {
1735
- const name = withoutDashes.slice(0, eqIndex);
1736
- const value = withoutDashes.slice(eqIndex + 1);
1737
- const resolvedName = aliasMap.get(name) ?? name;
1738
- setOption(name, booleanFlags.has(resolvedName) ? coerceBoolean(value) : value);
1739
- i++;
1740
- } else {
1741
- const name = withoutDashes;
1742
- const resolvedName = aliasMap.get(name) ?? name;
1743
- if (booleanFlags.has(resolvedName)) {
1744
- setOption(name, true);
1745
- i++;
1746
- } else {
1747
- const nextArg = argv[i + 1];
1748
- if (nextArg !== void 0 && (!nextArg.startsWith("-") || looksLikeNegativeNumber(nextArg))) {
1749
- setOption(name, nextArg);
1750
- i += 2;
1751
- } else {
1752
- setOption(name, true);
1753
- i++;
1754
- }
1755
- }
1756
- }
1757
- continue;
1758
- }
1759
- if (arg.startsWith("-") && arg.length > 1 && !arg.startsWith("--")) {
1760
- const withoutDash = arg.slice(1);
1761
- const eqIndex = withoutDash.indexOf("=");
1762
- if (eqIndex !== -1) {
1763
- const name = withoutDash.slice(0, eqIndex);
1764
- const value = withoutDash.slice(eqIndex + 1);
1765
- const resolvedName = aliasMap.get(name) ?? name;
1766
- setOption(name, booleanFlags.has(resolvedName) ? coerceBoolean(value) : value);
1767
- i++;
1768
- } else if (withoutDash.length === 1) {
1769
- const name = withoutDash;
1770
- const resolvedName = aliasMap.get(name) ?? name;
1771
- if (booleanFlags.has(resolvedName)) {
1772
- setOption(name, true);
1773
- i++;
1774
- } else {
1775
- const nextArg = argv[i + 1];
1776
- if (nextArg !== void 0 && (!nextArg.startsWith("-") || looksLikeNegativeNumber(nextArg))) {
1777
- setOption(name, nextArg);
1778
- i += 2;
1779
- } else {
1780
- setOption(name, true);
1781
- i++;
1782
- }
1783
- }
1784
- } else {
1785
- for (const char of withoutDash) setOption(char, true);
1786
- i++;
1787
- }
1788
- continue;
1789
- }
1790
- result.positionals.push(arg);
1791
- i++;
1792
- }
1793
- return result;
1794
- }
1795
- /**
1796
- * Build parser options from extracted fields
1797
- */
1798
- function buildParserOptions(extracted) {
1799
- const aliasMap = /* @__PURE__ */ new Map();
1800
- const booleanFlags = /* @__PURE__ */ new Set();
1801
- const arrayFlags = /* @__PURE__ */ new Set();
1802
- const definedNames = /* @__PURE__ */ new Set();
1803
- const negationMap = /* @__PURE__ */ new Map();
1804
- const defaultNegationDisabledFields = /* @__PURE__ */ new Set();
1805
- for (const field of extracted.fields) definedNames.add(field.name);
1806
- for (const field of extracted.fields) {
1807
- if (field.cliName !== field.name) aliasMap.set(field.cliName, field.name);
1808
- for (const alias of getAllAliases(field)) {
1809
- aliasMap.set(alias, field.name);
1810
- if (alias.length > 1 && alias.includes("-")) {
1811
- const camelAlias = toCamelCase(alias);
1812
- if (camelAlias !== alias && !definedNames.has(camelAlias) && !aliasMap.has(camelAlias)) aliasMap.set(camelAlias, field.name);
1813
- }
1814
- }
1815
- const camelVariant = toCamelCase(field.name);
1816
- if (camelVariant !== field.name && !definedNames.has(camelVariant) && !aliasMap.has(camelVariant)) aliasMap.set(camelVariant, field.name);
1817
- if (field.type === "boolean") booleanFlags.add(field.name);
1818
- if (field.type === "array") arrayFlags.add(field.name);
1819
- if (field.type === "boolean" && field.negation !== true) {
1820
- defaultNegationDisabledFields.add(field.name);
1821
- if (typeof field.negation === "string") {
1822
- negationMap.set(field.negation, field.name);
1823
- if (field.negation.includes("-")) {
1824
- const camelNegation = toCamelCase(field.negation);
1825
- if (camelNegation !== field.negation) negationMap.set(camelNegation, field.name);
1826
- }
1827
- }
1828
- }
1829
- }
1830
- return {
1831
- aliasMap,
1832
- booleanFlags,
1833
- arrayFlags,
1834
- definedNames,
1835
- negationMap,
1836
- defaultNegationDisabledFields
1837
- };
1838
- }
1839
- /**
1840
- * Merge parsed argv with positional fields to create a flat record
1841
- */
1842
- function mergeWithPositionals(parsed, extracted) {
1843
- const result = { ...parsed.options };
1844
- const positionalFields = extracted.fields.filter((f) => f.positional);
1845
- const allPositionals = parsed.rest.length > 0 ? [...parsed.positionals, ...parsed.rest] : parsed.positionals;
1846
- let positionalIndex = 0;
1847
- for (const field of positionalFields) {
1848
- if (positionalIndex >= allPositionals.length) break;
1849
- if (field.type === "array") {
1850
- result[field.name] = allPositionals.slice(positionalIndex);
1851
- break;
1852
- } else {
1853
- result[field.name] = allPositionals[positionalIndex];
1854
- positionalIndex++;
1855
- }
1856
- }
1857
- return result;
1858
- }
1859
-
1860
- //#endregion
1861
- //#region ../core/src/parser/subcommand-scanner.ts
1862
- /**
1863
- * Build lookup tables from extracted global schema fields.
1864
- * Shared by scanForSubcommand, separateGlobalArgs, and findFirstPositional.
1865
- */
1866
- function buildGlobalFlagLookup(globalExtracted) {
1867
- const { aliasMap = /* @__PURE__ */ new Map(), booleanFlags = /* @__PURE__ */ new Set(), definedNames = /* @__PURE__ */ new Set(), negationMap = /* @__PURE__ */ new Map(), defaultNegationDisabledFields = /* @__PURE__ */ new Set() } = buildParserOptions(globalExtracted);
1868
- const shortAliases = /* @__PURE__ */ new Set();
1869
- for (const field of globalExtracted.fields) for (const alias of getAllAliases(field)) if (alias.length === 1) shortAliases.add(alias);
1870
- return {
1871
- aliasMap,
1872
- booleanFlags,
1873
- definedNames,
1874
- flagNames: new Set(globalExtracted.fields.map((f) => f.name)),
1875
- cliNames: new Set(globalExtracted.fields.map((f) => f.cliName)),
1876
- aliases: shortAliases,
1877
- negationMap,
1878
- defaultNegationDisabledFields
1879
- };
1880
- }
1881
- /**
1882
- * Resolve a long option (--flag, --flag=value, --no-flag, --custom-negation)
1883
- * against global flag lookup. Returns the resolved camelCase name and whether
1884
- * it is a known global flag.
1885
- *
1886
- * `isSuppressedNegation` is true when the token matches a disabled default
1887
- * `--no-X` form on the target field.
1888
- * The caller may use this to keep argv scanning past such tokens (so a
1889
- * trailing subcommand is still detected) even though they no longer negate.
1890
- */
1891
- function resolveGlobalLongOption(arg, lookup) {
1892
- const { resolvedName, withoutDashes, isNegated, isCustomNegation, isSuppressedNegation } = resolveLongOption(arg, lookup);
1893
- if (isSuppressedNegation) return {
1894
- resolvedName,
1895
- withoutDashes,
1896
- isNegated: false,
1897
- isGlobal: false,
1898
- isSuppressedNegation
1899
- };
1900
- const flagName = isNegated && !isCustomNegation ? withoutDashes.startsWith("no-") ? withoutDashes.slice(3) : withoutDashes[2].toLowerCase() + withoutDashes.slice(3) : withoutDashes;
1901
- return {
1902
- resolvedName,
1903
- withoutDashes,
1904
- isNegated,
1905
- isGlobal: lookup.flagNames.has(resolvedName) || lookup.cliNames.has(withoutDashes) || lookup.cliNames.has(flagName),
1906
- isSuppressedNegation: false
1907
- };
1908
- }
1909
- /**
1910
- * Check whether a non-boolean flag should consume the next argv token as its value.
1911
- * Returns true when the next token exists, is not a flag, and the current flag
1912
- * is not boolean / negated / using = syntax.
1913
- */
1914
- function shouldConsumeValue(arg, resolvedName, isNegated, nextArg, booleanFlags) {
1915
- return !arg.includes("=") && !booleanFlags.has(resolvedName) && !isNegated && nextArg !== void 0 && !nextArg.startsWith("-");
1916
- }
1917
- /**
1918
- * Collect a recognized global flag (and its value if applicable) into `dest`,
1919
- * returning how many argv positions were consumed (1 or 2).
1920
- */
1921
- function collectGlobalFlag(argv, i, resolvedName, isNegated, booleanFlags, dest) {
1922
- const arg = argv[i];
1923
- dest.push(arg);
1924
- if (shouldConsumeValue(arg, resolvedName, isNegated, argv[i + 1], booleanFlags)) {
1925
- dest.push(argv[i + 1]);
1926
- return 2;
1927
- }
1928
- return 1;
1929
- }
1930
- /**
1931
- * Scan argv to find the subcommand position, skipping over global flags.
1932
- *
1933
- * Walks argv and recognizes global flags (long, short, --no-*) so that
1934
- * `my-cli --verbose build --output dist` correctly identifies `build` as
1935
- * the subcommand (index 1) rather than treating `--verbose` as the subcommand.
1936
- *
1937
- * Limitation: flags appearing before the subcommand name are matched only
1938
- * against the global schema. If a flag is defined in both global and a
1939
- * subcommand's local schema, the pre-subcommand occurrence is always treated
1940
- * as global because the local schema is not available until the subcommand is
1941
- * identified (lazy-loaded commands make eager checking infeasible). Place
1942
- * colliding flags after the subcommand name so that `separateGlobalArgs` can
1943
- * apply local-precedence logic.
1944
- *
1945
- * @param argv - Command line arguments
1946
- * @param subCommandNames - Valid subcommand names
1947
- * @param globalExtracted - Extracted fields from global args schema
1948
- * @returns Scan result with subcommand position and token separation
1949
- */
1950
- function scanForSubcommand(argv, subCommandNames, globalExtracted) {
1951
- const lookup = buildGlobalFlagLookup(globalExtracted);
1952
- const subCommandNameSet = new Set(subCommandNames);
1953
- const globalTokensBefore = [];
1954
- const suppressedTokens = [];
1955
- let i = 0;
1956
- while (i < argv.length) {
1957
- const arg = argv[i];
1958
- if (arg === "--" || BUILTIN_FLAGS.has(arg)) break;
1959
- if (!arg.startsWith("-") && subCommandNameSet.has(arg)) return {
1960
- subCommandIndex: i,
1961
- globalTokensBefore,
1962
- tokensAfterSubcommand: argv.slice(i + 1),
1963
- suppressedTokens
1964
- };
1965
- if (arg.startsWith("--")) {
1966
- const { resolvedName, withoutDashes, isNegated, isGlobal, isSuppressedNegation } = resolveGlobalLongOption(arg, lookup);
1967
- if (isGlobal) {
1968
- i += collectGlobalFlag(argv, i, resolvedName, isNegated, lookup.booleanFlags, globalTokensBefore);
1969
- continue;
1970
- }
1971
- if (isSuppressedNegation) {
1972
- suppressedTokens.push(withoutDashes);
1973
- i++;
1974
- continue;
1975
- }
1976
- break;
1977
- }
1978
- if (arg.startsWith("-") && arg.length > 1) {
1979
- const withoutDash = arg.includes("=") ? arg.slice(1, arg.indexOf("=")) : arg.slice(1);
1980
- if (withoutDash.length === 1) {
1981
- const resolvedName = lookup.aliasMap.get(withoutDash) ?? withoutDash;
1982
- if (lookup.aliases.has(withoutDash) || lookup.flagNames.has(resolvedName)) {
1983
- i += collectGlobalFlag(argv, i, resolvedName, false, lookup.booleanFlags, globalTokensBefore);
1984
- continue;
1985
- }
1986
- }
1987
- break;
1988
- }
1989
- break;
1990
- }
1991
- return {
1992
- subCommandIndex: -1,
1993
- globalTokensBefore,
1994
- tokensAfterSubcommand: [],
1995
- suppressedTokens
1996
- };
1997
- }
1998
- const BUILTIN_FLAGS = /* @__PURE__ */ new Set([
1999
- "--help",
2000
- "-h",
2001
- "--help-all",
2002
- "-H",
2003
- "--version"
2004
- ]);
2005
- /**
2006
- * Find the index of the first positional argument in argv, properly skipping
2007
- * global flag values. Returns -1 when no positional is present.
2008
- *
2009
- * Mirrors `scanForSubcommand`'s conservative stop conditions: the scan stops
2010
- * (returning -1) on a `--` terminator, a builtin flag (`--help`/`--version`),
2011
- * an unknown long flag, or an unknown/combined short flag. Past such tokens we
2012
- * can't tell a flag *value* from a positional, so continuing would misclassify
2013
- * e.g. `--help plugin` or `--unknown value` and wrongly trip plugin dispatch.
2014
- *
2015
- * Without globalExtracted, no flag is global, so any leading flag halts the
2016
- * scan and a positional is only found when it precedes every flag.
2017
- */
2018
- function findFirstPositionalIndex(argv, globalExtracted, options = {}) {
2019
- const stopOnSuppressedNegation = options.stopOnSuppressedNegation === true || globalExtracted?.unknownKeysMode === "strict";
2020
- const lookup = globalExtracted ? buildGlobalFlagLookup(globalExtracted) : {
2021
- aliasMap: /* @__PURE__ */ new Map(),
2022
- booleanFlags: /* @__PURE__ */ new Set(),
2023
- definedNames: /* @__PURE__ */ new Set(),
2024
- flagNames: /* @__PURE__ */ new Set(),
2025
- cliNames: /* @__PURE__ */ new Set(),
2026
- aliases: /* @__PURE__ */ new Set(),
2027
- negationMap: /* @__PURE__ */ new Map(),
2028
- defaultNegationDisabledFields: /* @__PURE__ */ new Set()
2029
- };
2030
- for (let i = 0; i < argv.length; i++) {
2031
- const arg = argv[i];
2032
- if (!arg.startsWith("-")) return i;
2033
- if (arg === "--" || BUILTIN_FLAGS.has(arg)) return -1;
2034
- if (arg.startsWith("--")) {
2035
- const { resolvedName, isNegated, isGlobal, isSuppressedNegation } = resolveGlobalLongOption(arg, lookup);
2036
- if (isGlobal) {
2037
- if (shouldConsumeValue(arg, resolvedName, isNegated, argv[i + 1], lookup.booleanFlags)) i++;
2038
- continue;
2039
- }
2040
- if (isSuppressedNegation) {
2041
- if (stopOnSuppressedNegation) return -1;
2042
- continue;
2043
- }
2044
- return -1;
2045
- }
2046
- const withoutDash = arg.includes("=") ? arg.slice(1, arg.indexOf("=")) : arg.slice(1);
2047
- if (withoutDash.length === 1) {
2048
- const resolvedName = lookup.aliasMap.get(withoutDash) ?? withoutDash;
2049
- if (lookup.aliases.has(withoutDash) || lookup.flagNames.has(resolvedName)) {
2050
- if (shouldConsumeValue(arg, resolvedName, false, argv[i + 1], lookup.booleanFlags)) i++;
2051
- continue;
2052
- }
2053
- }
2054
- return -1;
2055
- }
2056
- return -1;
2057
- }
2058
- /**
2059
- * Find the first positional argument in argv, properly skipping global flag
2060
- * values. Thin wrapper over {@link findFirstPositionalIndex} — see that
2061
- * function for the scan and stop conditions. Returns `undefined` when none.
2062
- */
2063
- function findFirstPositional(argv, globalExtracted) {
2064
- const index = findFirstPositionalIndex(argv, globalExtracted);
2065
- return index >= 0 ? argv[index] : void 0;
2066
- }
2067
-
2068
- //#endregion
2069
- //#region ../core/src/parser/arg-parser.ts
2070
- /**
2071
- * Parse CLI arguments for a command
2072
- *
2073
- * @param argv - Command line arguments
2074
- * @param command - The command to parse for
2075
- * @param options - Parse options
2076
- * @returns Parse result
2077
- */
2078
- function parseArgs(argv, command, options = {}) {
2079
- const subCommandNameSet = listSubCommandNamesWithAliases(command);
2080
- const subCommandNames = [...subCommandNameSet];
2081
- const hasSubCommands = subCommandNames.length > 0;
2082
- if (hasSubCommands && argv.length > 0) {
2083
- if (options.globalExtracted) {
2084
- const scanResult = scanForSubcommand(argv, subCommandNames, options.globalExtracted);
2085
- if (scanResult.subCommandIndex >= 0) {
2086
- const rawGlobalArgs = parseGlobalArgs(scanResult.globalTokensBefore, options.globalExtracted);
2087
- return {
2088
- helpRequested: false,
2089
- helpAllRequested: false,
2090
- versionRequested: false,
2091
- subCommand: argv[scanResult.subCommandIndex],
2092
- remainingArgs: scanResult.tokensAfterSubcommand,
2093
- rawArgs: {},
2094
- positionals: [],
2095
- rest: [],
2096
- unknownFlags: scanResult.suppressedTokens,
2097
- unknownGlobalFlags: scanResult.suppressedTokens,
2098
- rawGlobalArgs
2099
- };
2100
- }
2101
- } else {
2102
- const firstArg = argv[0];
2103
- if (firstArg && !firstArg.startsWith("-") && subCommandNameSet.has(firstArg)) return {
2104
- helpRequested: false,
2105
- helpAllRequested: false,
2106
- versionRequested: false,
2107
- subCommand: firstArg,
2108
- remainingArgs: argv.slice(1),
2109
- rawArgs: {},
2110
- positionals: [],
2111
- rest: [],
2112
- unknownFlags: []
2113
- };
2114
- }
2115
- }
2116
- let extracted;
2117
- if (command.args) {
2118
- extracted = extractFields(command.args);
2119
- if (!options.skipValidation) {
2120
- validateDuplicateFields(extracted);
2121
- validateCaseVariantCollisions(extracted);
2122
- validateDuplicateAliases(extracted);
2123
- validateDuplicateNegations(extracted);
2124
- validatePositionalConfig(extracted);
2125
- validateReservedAliases(extracted, hasSubCommands);
2126
- validateReservedFieldNames(extracted);
2127
- if (options.globalExtracted) validateCrossSchemaCollisions(options.globalExtracted, extracted);
2128
- }
2129
- }
2130
- let commandArgv = argv;
2131
- let rawGlobalArgs;
2132
- let suppressedGlobalFlags = [];
2133
- if (options.globalExtracted) {
2134
- const { separated, globalParsed, suppressedTokens } = separateGlobalArgs(argv, options.globalExtracted, extracted);
2135
- commandArgv = separated;
2136
- rawGlobalArgs = globalParsed;
2137
- suppressedGlobalFlags = suppressedTokens;
2138
- }
2139
- const ddIdx = argv.indexOf("--");
2140
- const flagScanArgv = ddIdx >= 0 ? argv.slice(0, ddIdx) : argv;
2141
- const hasUserDefinedH = extracted?.fields.some((f) => f.overrideBuiltinAlias === true && getAllAliases(f).includes("H")) ?? false;
2142
- const hasUserDefinedh = extracted?.fields.some((f) => f.overrideBuiltinAlias === true && getAllAliases(f).includes("h")) ?? false;
2143
- const helpAllRequested = flagScanArgv.includes("--help-all") || !hasUserDefinedH && flagScanArgv.includes("-H");
2144
- const helpRequested = !helpAllRequested && (flagScanArgv.includes("--help") || !hasUserDefinedh && flagScanArgv.includes("-h"));
2145
- const versionRequested = flagScanArgv.includes("--version");
2146
- if (helpRequested || helpAllRequested || versionRequested) return {
2147
- helpRequested,
2148
- helpAllRequested,
2149
- versionRequested,
2150
- subCommand: void 0,
2151
- remainingArgs: [],
2152
- rawArgs: {},
2153
- positionals: [],
2154
- rest: [],
2155
- unknownFlags: [],
2156
- unknownGlobalFlags: suppressedGlobalFlags,
2157
- rawGlobalArgs
2158
- };
2159
- if (!extracted) {
2160
- const ddIdx = commandArgv.indexOf("--");
2161
- return {
2162
- helpRequested: false,
2163
- helpAllRequested: false,
2164
- versionRequested: false,
2165
- subCommand: void 0,
2166
- remainingArgs: [],
2167
- rawArgs: {},
2168
- positionals: ddIdx >= 0 ? commandArgv.slice(0, ddIdx) : commandArgv,
2169
- rest: ddIdx >= 0 ? commandArgv.slice(ddIdx + 1) : [],
2170
- unknownFlags: [],
2171
- unknownGlobalFlags: suppressedGlobalFlags,
2172
- rawGlobalArgs
2173
- };
2174
- }
2175
- const parserOptions = buildParserOptions(extracted);
2176
- const parsed = parseArgv(commandArgv, parserOptions);
2177
- const rawArgs = mergeWithPositionals(parsed, extracted);
2178
- const envFallbackFields = /* @__PURE__ */ new Set();
2179
- for (const field of extracted.fields) if (field.env && rawArgs[field.name] === void 0) {
2180
- const envNames = Array.isArray(field.env) ? field.env : [field.env];
2181
- for (const envName of envNames) {
2182
- const envValue = process.env[envName];
2183
- if (envValue !== void 0) {
2184
- rawArgs[field.name] = envValue;
2185
- envFallbackFields.add(field.name);
2186
- break;
2187
- }
2188
- }
2189
- }
2190
- const knownFlags = new Set(extracted.fields.map((f) => f.name));
2191
- const knownCliNames = new Set(extracted.fields.map((f) => f.cliName));
2192
- const knownAliases = /* @__PURE__ */ new Set();
2193
- for (const f of extracted.fields) for (const alias of getAllAliases(f)) knownAliases.add(alias);
2194
- if (options.globalExtracted) for (const f of options.globalExtracted.fields) {
2195
- knownFlags.add(f.name);
2196
- knownCliNames.add(f.cliName);
2197
- for (const alias of getAllAliases(f)) knownAliases.add(alias);
2198
- }
2199
- const unknownFlags = [];
2200
- for (const key of Object.keys(parsed.options)) if (!knownFlags.has(key) && !knownCliNames.has(key) && !knownAliases.has(key)) unknownFlags.push(key);
2201
- return {
2202
- helpRequested: false,
2203
- helpAllRequested: false,
2204
- versionRequested: false,
2205
- subCommand: void 0,
2206
- remainingArgs: [],
2207
- rawArgs,
2208
- positionals: parsed.positionals,
2209
- rest: parsed.rest,
2210
- unknownFlags,
2211
- unknownGlobalFlags: suppressedGlobalFlags,
2212
- extractedFields: extracted,
2213
- rawGlobalArgs,
2214
- envFallbackFields
2215
- };
2216
- }
2217
- /**
2218
- * Parse global args from a list of tokens (e.g., tokens before the subcommand).
2219
- * Env fallbacks are applied later in the runner on the accumulated global args.
2220
- */
2221
- function parseGlobalArgs(tokens, globalExtracted) {
2222
- if (tokens.length === 0) return {};
2223
- const parserOptions = buildParserOptions(globalExtracted);
2224
- const parsed = parseArgv(tokens, parserOptions);
2225
- return mergeWithPositionals(parsed, globalExtracted);
2226
- }
2227
- /**
2228
- * Separate global flags from command-local args in argv.
2229
- * Global flags mixed with command args (e.g., `build --verbose --output dist`)
2230
- * are extracted and returned separately.
2231
- * When a flag is defined in both global and local schemas, the local definition
2232
- * takes precedence (the flag stays in the command tokens).
2233
- *
2234
- * Note: Combined short flags (e.g., `-vq`) are not decomposed here; only
2235
- * single-character short options are recognized as global. The underlying
2236
- * `parseArgv` handles combined shorts for command-local parsing.
2237
- */
2238
- function separateGlobalArgs(argv, globalExtracted, localExtracted) {
2239
- const lookup = buildGlobalFlagLookup(globalExtracted);
2240
- const localFieldNames = new Set(localExtracted?.fields.map((f) => f.name) ?? []);
2241
- const localCliNames = new Set(localExtracted?.fields.map((f) => f.cliName) ?? []);
2242
- const localParserOptions = localExtracted ? buildParserOptions(localExtracted) : void 0;
2243
- const localAliasMapKeys = new Set(localParserOptions?.aliasMap?.keys() ?? []);
2244
- const localNegationMapKeys = new Set(localParserOptions?.negationMap?.keys() ?? []);
2245
- const localDefaultNegationKeys = /* @__PURE__ */ new Set();
2246
- for (const field of localExtracted?.fields ?? []) {
2247
- if (field.type !== "boolean" || field.negation !== true) continue;
2248
- for (const name of [field.cliName, ...getAllAliases(field)]) {
2249
- const kebab = `no-${name}`;
2250
- localDefaultNegationKeys.add(kebab);
2251
- localDefaultNegationKeys.add(toCamelCase(kebab));
2252
- }
2253
- }
2254
- const globalTokens = [];
2255
- const commandTokens = [];
2256
- const suppressedTokens = [];
2257
- for (let i = 0; i < argv.length; i++) {
2258
- const arg = argv[i];
2259
- if (arg === "--") {
2260
- commandTokens.push(...argv.slice(i));
2261
- break;
2262
- }
2263
- if (arg.startsWith("--")) {
2264
- const { resolvedName, withoutDashes, isNegated, isGlobal, isSuppressedNegation } = resolveGlobalLongOption(arg, lookup);
2265
- const flagName = resolvedName;
2266
- const isLocalCollision = localFieldNames.has(withoutDashes) || localFieldNames.has(flagName) || localCliNames.has(withoutDashes) || localCliNames.has(flagName) || localAliasMapKeys.has(withoutDashes) || localAliasMapKeys.has(flagName) || localNegationMapKeys.has(withoutDashes) || localNegationMapKeys.has(flagName) || localDefaultNegationKeys.has(withoutDashes);
2267
- if (isGlobal && !isLocalCollision) {
2268
- i += collectGlobalFlag(argv, i, resolvedName, isNegated, lookup.booleanFlags, globalTokens) - 1;
2269
- continue;
2270
- }
2271
- if (isSuppressedNegation && !isLocalCollision) {
2272
- suppressedTokens.push(withoutDashes);
2273
- continue;
2274
- }
2275
- } else if (arg.startsWith("-") && arg.length > 1) {
2276
- const withoutDash = arg.includes("=") ? arg.slice(1, arg.indexOf("=")) : arg.slice(1);
2277
- if (withoutDash.length === 1) {
2278
- const resolvedName = lookup.aliasMap.get(withoutDash) ?? withoutDash;
2279
- if ((lookup.aliases.has(withoutDash) || lookup.flagNames.has(resolvedName)) && !localAliasMapKeys.has(withoutDash)) {
2280
- i += collectGlobalFlag(argv, i, resolvedName, false, lookup.booleanFlags, globalTokens) - 1;
2281
- continue;
2282
- }
2283
- }
2284
- }
2285
- commandTokens.push(arg);
2286
- }
2287
- return {
2288
- separated: commandTokens,
2289
- globalParsed: parseGlobalArgs(globalTokens, globalExtracted),
2290
- suppressedTokens
2291
- };
2292
- }
2293
-
2294
- //#endregion
2295
- //#region ../core/src/validator/args-validator.ts
2296
- /**
2297
- * Args validation facade.
2298
- *
2299
- * Routes raw parsed args to the implementation that understands the
2300
- * command's schema: politty's internal validator-free descriptors, or the
2301
- * registered validator adapter for user schemas. Neutral error shapes live
2302
- * in `adapter/types.ts`; the schema-library-specific validation lives in
2303
- * the adapter package (e.g. `@politty/zod`).
2304
- */
2305
- /**
2306
- * Validate raw arguments against a schema
2307
- *
2308
- * @param rawArgs - Parsed but unvalidated arguments
2309
- * @param schema - Args schema (ZodObject, ZodDiscriminatedUnion, internal descriptor, etc.)
2310
- * @returns Validation result with typed data or errors
2311
- */
2312
- function validateArgs(rawArgs, schema) {
2313
- if (isInternalArgsSchema(schema)) return validateInternalArgs(rawArgs, schema);
2314
- return getValidatorAdapter().validate(rawArgs, schema);
2315
- }
2316
- /**
2317
- * Format validation errors for display
2318
- */
2319
- function formatValidationErrors(errors) {
2320
- return errors.map((e) => {
2321
- return `${e.path.length > 0 ? `${e.path.join(".")}: ` : ""}${e.message}`;
2322
- }).join("\n");
2323
- }
2324
-
2325
- //#endregion
2326
- //#region ../core/src/validator/error-formatter.ts
2327
- /**
2328
- * Calculate Levenshtein distance between two strings
2329
- */
2330
- function levenshteinDistance(a, b) {
2331
- const matrix = [];
2332
- for (let i = 0; i <= b.length; i++) matrix[i] = [i];
2333
- for (let j = 0; j <= a.length; j++) matrix[0][j] = j;
2334
- for (let i = 1; i <= b.length; i++) for (let j = 1; j <= a.length; j++) if (b.charAt(i - 1) === a.charAt(j - 1)) matrix[i][j] = matrix[i - 1][j - 1];
2335
- else matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1);
2336
- return matrix[b.length][a.length];
2337
- }
2338
- /**
2339
- * Find similar strings from a list
2340
- */
2341
- function findSimilar(target, candidates) {
2342
- const threshold = Math.max(2, Math.floor(target.length / 2));
2343
- return candidates.map((candidate) => ({
2344
- candidate,
2345
- distance: levenshteinDistance(target.toLowerCase(), candidate.toLowerCase())
2346
- })).filter(({ distance }) => distance <= threshold).sort((a, b) => a.distance - b.distance).map(({ candidate }) => candidate).slice(0, 3);
2347
- }
2348
- /**
2349
- * Format unknown flag warning with suggestions (for strip mode)
2350
- *
2351
- * @param flag - The unknown flag (e.g., "--verbos")
2352
- * @param knownFlags - List of known flag names
2353
- * @returns Formatted warning message with suggestions
2354
- */
2355
- function formatUnknownFlagWarning(flag, knownFlags) {
2356
- const similar = findSimilar(flag.replace(/^-{1,2}/, ""), knownFlags);
2357
- let message = `${styles.warning("Warning: Unknown option:")} ${styles.bold(flag)}`;
2358
- if (similar.length > 0) {
2359
- message += `\n\n${styles.info("Did you mean?")}`;
2360
- for (const suggestion of similar) message += `\n ${symbols.arrow} ${styles.option(`--${suggestion}`)}`;
2361
- }
2362
- return message;
2363
- }
2364
- /**
2365
- * Format runtime error
2366
- *
2367
- * @param error - The error that occurred
2368
- * @param debug - Whether to include stack trace
2369
- * @returns Formatted error message
2370
- */
2371
- function formatRuntimeError(error, debug) {
2372
- if (debug && error.stack) return `${styles.error("Error:")} ${error.message}\n\n${styles.dim(error.stack)}`;
2373
- return `${styles.error("Error:")} ${error.message}`;
2374
- }
2375
-
2376
- //#endregion
2377
- //#region ../core/src/core/case-proxy.ts
2378
- /**
2379
- * Wrap an args object with a Proxy that allows dual-case access.
2380
- *
2381
- * Given `{ "my-option": "value" }`, both `obj["my-option"]` and `obj.myOption`
2382
- * will return `"value"`.
2383
- *
2384
- * - `Object.keys()`, `JSON.stringify()`, and spread return only the original keys.
2385
- * - The `in` operator detects both case variants.
2386
- */
2387
- function createDualCaseProxy(obj) {
2388
- return new Proxy(obj, {
2389
- get(target, prop, receiver) {
2390
- if (typeof prop === "string") {
2391
- if (prop in target) return Reflect.get(target, prop, receiver);
2392
- const camel = toCamelCase(prop);
2393
- if (camel !== prop && camel in target) return Reflect.get(target, camel, receiver);
2394
- const kebab = toKebabCase(prop);
2395
- if (kebab !== prop && kebab in target) return Reflect.get(target, kebab, receiver);
2396
- }
2397
- return Reflect.get(target, prop, receiver);
2398
- },
2399
- has(target, prop) {
2400
- if (typeof prop === "string") {
2401
- if (prop in target) return true;
2402
- const camel = toCamelCase(prop);
2403
- if (camel !== prop && camel in target) return true;
2404
- const kebab = toKebabCase(prop);
2405
- if (kebab !== prop && kebab in target) return true;
2406
- }
2407
- return Reflect.has(target, prop);
2408
- }
2409
- });
2410
- }
2411
-
2412
- //#endregion
2413
- //#region ../core/src/core/effect-runner.ts
2414
- /**
2415
- * Execute all registered effect callbacks for validated args.
2416
- *
2417
- * Effects run sequentially in field-definition order.
2418
- * Only fires for fields that have an `effect` callback defined.
2419
- *
2420
- * @param validatedArgs - The validated (post-Zod) argument values
2421
- * @param extracted - The extracted fields from the schema
2422
- * @param globalArgs - The validated global args (passed to command arg effects)
2423
- */
2424
- async function runEffects(validatedArgs, extracted, globalArgs) {
2425
- for (const field of extracted.fields) {
2426
- if (!field.effect) continue;
2427
- await field.effect(validatedArgs[field.name], {
2428
- name: field.name,
2429
- args: validatedArgs,
2430
- ...globalArgs != null && { globalArgs }
2431
- });
2432
- }
2433
- }
2434
-
2435
- //#endregion
2436
- //#region ../core/src/core/runner.ts
2437
- /**
2438
- * Default logger using console
2439
- */
2440
- const defaultLogger = {
2441
- log: (message) => console.log(message),
2442
- error: (message) => console.error(message),
2443
- warn: (message) => console.warn(message)
2444
- };
2445
- /**
2446
- * Attach a non-enumerable `$source` helper to the final args object so it
2447
- * stays invisible to `Object.keys`/`JSON.stringify`/spread (those only ever
2448
- * see real field values) while still being reachable via property access
2449
- * (including through `createDualCaseProxy`).
2450
- */
2451
- function attachArgSource(target, sourceMap) {
2452
- Object.defineProperty(target, "$source", {
2453
- value: (name) => sourceMap.get(name) ?? sourceMap.get(toCamelCase(name)) ?? sourceMap.get(toKebabCase(name)) ?? "default",
2454
- enumerable: false
2455
- });
2456
- }
2457
- /**
2458
- * Attach a non-enumerable `$invocation` helper to the final args object,
2459
- * mirroring `attachArgSource`'s visibility (invisible to `Object.keys`/
2460
- * `JSON.stringify`/spread, reachable via property access through
2461
- * `createDualCaseProxy`).
2462
- */
2463
- function attachInvocation(target, invocation) {
2464
- Object.defineProperty(target, "$invocation", {
2465
- value: invocation,
2466
- enumerable: false
2467
- });
2468
- }
2469
- /**
2470
- * Resolve the `RunInvocation` metadata for the command about to run: the
2471
- * literal CLI token that reached it (canonical name or alias), and the
2472
- * canonical name if that token was an alias.
2473
- *
2474
- * `context.commandPath`'s last entry is the raw token a parent recorded
2475
- * when descending into this command (see the subcommand-descent branch
2476
- * below); it falls back to `command.name` for the root command or a
2477
- * directly-invoked command (no subcommand routing took place).
2478
- */
2479
- function resolveInvocation(command, context) {
2480
- const name = context.commandPath?.at(-1) ?? command.name;
2481
- return context.aliasFor ? {
2482
- name,
2483
- aliasFor: context.aliasFor
2484
- } : { name };
2485
- }
2486
- /**
2487
- * Run a command with the given arguments (programmatic/test usage)
2488
- *
2489
- * This function parses arguments, validates them, routes to subcommands,
2490
- * and executes the command. It does NOT call process.exit.
2491
- *
2492
- * @param command - The command to run
2493
- * @param argv - Command line arguments to parse
2494
- * @param options - Run options
2495
- * @returns The result of command execution
2496
- *
2497
- * @example
2498
- * ```ts
2499
- * import { defineCommand, runCommand } from "politty";
2500
- *
2501
- * const command = defineCommand({
2502
- * name: "my-cli",
2503
- * args: z.object({ name: z.string() }),
2504
- * run: ({ name }) => console.log(`Hello, ${name}!`),
2505
- * });
2506
- *
2507
- * // In tests
2508
- * const result = await runCommand(command, ["--name", "World"]);
2509
- * expect(result.exitCode).toBe(0);
2510
- * ```
2511
- */
2512
- async function runCommand(command, argv, options = {}) {
2513
- const globalExtracted = extractAndValidateGlobal(options);
2514
- const shouldCaptureLogs = options.captureLogs ?? false;
2515
- const globalCollector = shouldCaptureLogs ? createLogCollector() : null;
2516
- if (options.setup) {
2517
- globalCollector?.start();
2518
- try {
2519
- await options.setup({});
2520
- } catch (e) {
2521
- const error = e instanceof Error ? e : new Error(String(e));
2522
- if (options.cleanup) try {
2523
- await options.cleanup({ error });
2524
- } catch {}
2525
- globalCollector?.stop();
2526
- return {
2527
- success: false,
2528
- error,
2529
- exitCode: 1,
2530
- logs: globalCollector?.getLogs() ?? emptyLogs()
2531
- };
2532
- }
2533
- globalCollector?.stop();
2534
- }
2535
- const result = await runCommandInternal(command, argv, {
2536
- ...options,
2537
- handleSignals: false,
2538
- _globalExtracted: globalExtracted,
2539
- _globalCleanup: options.cleanup,
2540
- _existingLogs: globalCollector?.getLogs()
2541
- });
2542
- if (options.cleanup) {
2543
- const cleanupCollector = shouldCaptureLogs ? createLogCollector() : null;
2544
- cleanupCollector?.start();
2545
- const cleanupCtx = { error: !result.success ? result.error : void 0 };
2546
- try {
2547
- await options.cleanup(cleanupCtx);
2548
- } catch (e) {
2549
- if (result.success) {
2550
- const error = e instanceof Error ? e : new Error(String(e));
2551
- cleanupCollector?.stop();
2552
- return {
2553
- success: false,
2554
- error,
2555
- exitCode: 1,
2556
- logs: mergeLogs(result.logs, cleanupCollector?.getLogs() ?? emptyLogs())
2557
- };
2558
- }
2559
- }
2560
- cleanupCollector?.stop();
2561
- const cleanupLogs = cleanupCollector?.getLogs() ?? emptyLogs();
2562
- if (cleanupLogs.entries.length > 0) return {
2563
- ...result,
2564
- logs: mergeLogs(result.logs, cleanupLogs)
2565
- };
2566
- }
2567
- return result;
2568
- }
2569
- /**
2570
- * Hidden internal subcommands (e.g. `__refresh-completion`) are spawned
2571
- * by background hooks and must not run user-provided
2572
- * `setup`/`cleanup`/`prompt` or required `globalArgs`. Those exist for
2573
- * the foreground CLI run; replaying them in a detached child causes
2574
- * duplicate side effects, stuck prompts, and validation failures the
2575
- * user never opted into.
2576
- *
2577
- * We treat any registered subcommand whose name starts with `__` as
2578
- * internal. We use `findFirstPositional` (schema-aware) instead of the
2579
- * naive "first non-flag token" so an option *value* like
2580
- * `--name __refresh-completion` doesn't trip the bypass — that would
2581
- * silently skip lifecycle hooks for ordinary invocations.
2582
- */
2583
- function isInternalSubcommandInvocation(command, argv, globalExtracted) {
2584
- const firstPositional = findFirstPositional(argv, globalExtracted);
2585
- if (!firstPositional || !firstPositional.startsWith("__")) return false;
2586
- return Object.hasOwn(command.subCommands ?? {}, firstPositional);
2587
- }
2588
- /**
2589
- * Run a CLI command as the main entry point
2590
- *
2591
- * This function:
2592
- * - Uses process.argv for arguments
2593
- * - Handles SIGINT/SIGTERM signals
2594
- * - Calls process.exit with the appropriate exit code
2595
- * - Invokes `command.runMainHook` once before parsing if set, so plug-ins
2596
- * like `withCompletionCommand` can fire detached background work
2597
- * - Bypasses user `setup`/`cleanup`/`prompt` and required `globalArgs`
2598
- * for registered hidden subcommands whose name starts with `__`
2599
- * (e.g. `__refresh-completion`)
2600
- *
2601
- * @param command - The command to run
2602
- * @param options - Main options (version, debug)
2603
- *
2604
- * @example
2605
- * ```ts
2606
- * import { defineCommand, runMain } from "politty";
2607
- *
2608
- * const command = defineCommand({
2609
- * name: "my-cli",
2610
- * run: () => console.log("Hello!"),
2611
- * });
2612
- *
2613
- * runMain(command, { version: "1.0.0" });
2614
- * ```
2615
- */
2616
- async function runMain(command, options = {}) {
2617
- if (options.compileCache !== false) enableCompileCache(typeof options.compileCache === "string" ? { cacheDir: options.compileCache } : { programName: command.name });
2618
- if (command.runMainHook) try {
2619
- command.runMainHook(process.argv.slice(2));
2620
- } catch {}
2621
- const argv = process.argv.slice(2);
2622
- let globalExtractedForBypass;
2623
- if (options.globalArgs) try {
2624
- globalExtractedForBypass = extractFields(options.globalArgs);
2625
- } catch {}
2626
- let effectiveOptions = options;
2627
- if (isInternalSubcommandInvocation(command, argv, globalExtractedForBypass)) {
2628
- const { setup: _s, cleanup: _c, prompt: _p, globalArgs: _g, ...rest } = options;
2629
- effectiveOptions = rest;
2630
- }
2631
- const globalExtracted = extractAndValidateGlobal(effectiveOptions);
2632
- if (effectiveOptions.onUnknownSubcommand && !command.run && !isInternalSubcommandInvocation(command, argv, globalExtractedForBypass)) {
2633
- const knownSubCommands = listSubCommandNamesWithAliases(command);
2634
- if (knownSubCommands.size > 0) {
2635
- const positionalIndex = findFirstPositionalIndex(argv, globalExtracted, { stopOnSuppressedNegation: globalExtracted?.unknownKeysMode !== "passthrough" });
2636
- const name = positionalIndex >= 0 ? argv[positionalIndex] : void 0;
2637
- if (name && !knownSubCommands.has(name)) {
2638
- const forwardArgs = argv.slice(positionalIndex + 1);
2639
- const exitCode = await effectiveOptions.onUnknownSubcommand({
2640
- commandPath: [],
2641
- name,
2642
- args: forwardArgs,
2643
- precedingArgs: argv.slice(0, positionalIndex)
2644
- });
2645
- if (typeof exitCode === "number") {
2646
- await flushStandardStreams();
2647
- return process.exit(exitCode);
2648
- }
2649
- }
2650
- }
2651
- }
2652
- if (effectiveOptions.setup) try {
2653
- await effectiveOptions.setup({});
2654
- } catch (e) {
2655
- const error = e instanceof Error ? e : new Error(String(e));
2656
- if (effectiveOptions.cleanup) try {
2657
- await effectiveOptions.cleanup({ error });
2658
- } catch {}
2659
- process.exit(1);
2660
- }
2661
- const result = await runCommandInternal(command, argv, {
2662
- debug: effectiveOptions.debug,
2663
- captureLogs: effectiveOptions.captureLogs,
2664
- skipValidation: effectiveOptions.skipValidation,
2665
- handleSignals: true,
2666
- logger: effectiveOptions.logger,
2667
- globalArgs: effectiveOptions.globalArgs,
2668
- prompt: effectiveOptions.prompt,
2669
- onUnknownSubcommand: effectiveOptions.onUnknownSubcommand,
2670
- _globalExtracted: globalExtracted,
2671
- _globalCleanup: effectiveOptions.cleanup,
2672
- _context: {
2673
- commandPath: [],
2674
- rootName: command.name,
2675
- rootVersion: effectiveOptions.version,
2676
- globalExtracted
2677
- }
2678
- });
2679
- if ((effectiveOptions.displayErrors ?? true) && !result.success && result.error) (effectiveOptions.logger ?? defaultLogger).error(formatRuntimeError(result.error, effectiveOptions.debug ?? false));
2680
- if (effectiveOptions.cleanup) {
2681
- const cleanupCtx = { error: !result.success ? result.error : void 0 };
2682
- try {
2683
- await effectiveOptions.cleanup(cleanupCtx);
2684
- } catch {}
2685
- }
2686
- await flushStandardStreams();
2687
- process.exit(result.exitCode);
2688
- }
2689
- /**
2690
- * Flush stdout/stderr before exit to prevent truncated output when piped
2691
- * (pipe writes are buffered asynchronously, so exiting early loses data).
2692
- *
2693
- * We await a zero-byte write's callback rather than a `drain` event: `drain`
2694
- * only fires after a `write()` returned `false` (backpressure), so buffered
2695
- * writes that never tripped it would hang. The write callback is ordered after
2696
- * all pending writes, so it resolves once the buffer is flushed.
2697
- */
2698
- async function flushStandardStreams() {
2699
- await Promise.all([process.stdout, process.stderr].map((stream) => stream.writableLength > 0 ? new Promise((resolve) => stream.write("", () => resolve())) : Promise.resolve()));
2700
- }
2701
- /**
2702
- * Internal implementation of command running
2703
- */
2704
- async function runCommandInternal(command, argv, options = {}) {
2705
- const logger = options.logger ?? defaultLogger;
2706
- const context = options._context ?? {
2707
- commandPath: [],
2708
- rootName: command.name,
2709
- globalExtracted: options._globalExtracted
2710
- };
2711
- const collector = options.captureLogs ?? false ? createLogCollector() : null;
2712
- collector?.start();
2713
- const getCurrentLogs = () => {
2714
- const existingLogs = options._existingLogs ?? emptyLogs();
2715
- const collectedLogs = collector?.getLogs() ?? emptyLogs();
2716
- return mergeLogs(existingLogs, collectedLogs);
2717
- };
2718
- try {
2719
- const parseResult = parseArgs(argv, command, {
2720
- skipValidation: options.skipValidation,
2721
- globalExtracted: options._globalExtracted
2722
- });
2723
- const accumulatedGlobalArgs = {
2724
- ...options._parsedGlobalArgs,
2725
- ...parseResult.rawGlobalArgs
2726
- };
2727
- const nestedCommandPath = context.commandPath ?? [];
2728
- if (options.onUnknownSubcommand && !command.run && nestedCommandPath.length > 0) {
2729
- const knownSubCommands = listSubCommandNamesWithAliases(command);
2730
- if (knownSubCommands.size > 0) {
2731
- const positionalIndex = findFirstPositionalIndex(argv, options._globalExtracted, { stopOnSuppressedNegation: options._globalExtracted?.unknownKeysMode !== "passthrough" });
2732
- const name = positionalIndex >= 0 ? argv[positionalIndex] : void 0;
2733
- if (name && !knownSubCommands.has(name)) {
2734
- const forwardArgs = argv.slice(positionalIndex + 1);
2735
- const exitCode = await options.onUnknownSubcommand({
2736
- commandPath: nestedCommandPath,
2737
- name,
2738
- args: forwardArgs,
2739
- precedingArgs: [...options._precedingArgs ?? [], ...argv.slice(0, positionalIndex)]
2740
- });
2741
- if (typeof exitCode === "number") {
2742
- collector?.stop();
2743
- if (options.handleSignals) {
2744
- if (options._globalCleanup) try {
2745
- await options._globalCleanup({ error: void 0 });
2746
- } catch {}
2747
- await flushStandardStreams();
2748
- process.exit(exitCode);
2749
- }
2750
- return exitCode === 0 ? {
2751
- success: true,
2752
- result: void 0,
2753
- exitCode: 0,
2754
- logs: getCurrentLogs()
2755
- } : {
2756
- success: false,
2757
- error: /* @__PURE__ */ new Error(`Plugin "${[...nestedCommandPath, name].join(" ")}" exited with code ${exitCode}`),
2758
- exitCode,
2759
- logs: getCurrentLogs()
2760
- };
2761
- }
2762
- }
2763
- }
2764
- }
2765
- if (parseResult.helpRequested || parseResult.helpAllRequested) {
2766
- let hasUnknownSubcommand = false;
2767
- const subCmdNames = listSubCommands(command);
2768
- const allSubCmdNameSet = listSubCommandNamesWithAliases(command);
2769
- if (subCmdNames.length > 0) {
2770
- const potentialSubCmd = findFirstPositional(argv, context.globalExtracted);
2771
- if (potentialSubCmd && !allSubCmdNameSet.has(potentialSubCmd)) hasUnknownSubcommand = true;
2772
- }
2773
- const help = generateHelp(command, {
2774
- showSubcommands: options.showSubcommands ?? true,
2775
- showSubcommandOptions: parseResult.helpAllRequested || options.showSubcommandOptions,
2776
- context
2777
- });
2778
- logger.log(help);
2779
- collector?.stop();
2780
- if (hasUnknownSubcommand) {
2781
- const unknownCmd = findFirstPositional(argv, context.globalExtracted) ?? "";
2782
- const similar = findSimilar(unknownCmd, [...allSubCmdNameSet]);
2783
- const suggestion = similar.length > 0 ? ` Did you mean: ${similar.join(", ")}?` : "";
2784
- return {
2785
- success: false,
2786
- error: /* @__PURE__ */ new Error(`Unknown subcommand: ${unknownCmd}${suggestion ? `.${suggestion}` : ""}`),
2787
- exitCode: 1,
2788
- logs: getCurrentLogs()
2789
- };
2790
- }
2791
- return {
2792
- success: true,
2793
- result: void 0,
2794
- exitCode: 0,
2795
- logs: getCurrentLogs()
2796
- };
2797
- }
2798
- if (parseResult.unknownGlobalFlags && parseResult.unknownGlobalFlags.length > 0) {
2799
- const globalMode = context.globalExtracted?.unknownKeysMode ?? "strip";
2800
- if (globalMode === "strict") {
2801
- collector?.stop();
2802
- return {
2803
- success: false,
2804
- error: /* @__PURE__ */ new Error(`Unknown flags: ${parseResult.unknownGlobalFlags.join(", ")}`),
2805
- exitCode: 1,
2806
- logs: getCurrentLogs()
2807
- };
2808
- }
2809
- if (globalMode === "strip") {
2810
- const knownGlobalFlags = context.globalExtracted?.fields.map((f) => f.name) ?? [];
2811
- for (const flag of parseResult.unknownGlobalFlags) logger.error(formatUnknownFlagWarning(flag, knownGlobalFlags));
2812
- }
2813
- }
2814
- if (parseResult.versionRequested) {
2815
- const version = context.rootVersion;
2816
- if (version) logger.log(version);
2817
- collector?.stop();
2818
- return {
2819
- success: true,
2820
- result: void 0,
2821
- exitCode: 0,
2822
- logs: getCurrentLogs()
2823
- };
2824
- }
2825
- if (parseResult.subCommand) {
2826
- const resolved = await resolveSubcommandWithAlias(command, parseResult.subCommand);
2827
- if (resolved) {
2828
- const subContext = {
2829
- commandPath: [...context.commandPath ?? [], parseResult.subCommand],
2830
- rootName: context.rootName,
2831
- rootVersion: context.rootVersion,
2832
- globalExtracted: context.globalExtracted,
2833
- aliasFor: resolved.aliasFor
2834
- };
2835
- collector?.stop();
2836
- const suppressedNames = new Set(options._globalExtracted?.unknownKeysMode === "passthrough" ? [] : parseResult.unknownGlobalFlags ?? []);
2837
- const isSuppressedFlag = (token) => {
2838
- return token.startsWith("--") && suppressedNames.has(getLongOptionName(token));
2839
- };
2840
- const levelPrecedingArgs = argv.slice(0, argv.length - parseResult.remainingArgs.length - 1).filter((token) => !isSuppressedFlag(token));
2841
- return runCommandInternal(resolved.command, parseResult.remainingArgs, {
2842
- ...options,
2843
- _context: subContext,
2844
- _existingLogs: getCurrentLogs(),
2845
- _parsedGlobalArgs: accumulatedGlobalArgs,
2846
- _precedingArgs: [...options._precedingArgs ?? [], ...levelPrecedingArgs]
2847
- });
2848
- }
2849
- }
2850
- const positionalFields = parseResult.extractedFields?.fields.filter((f) => f.positional) ?? [];
2851
- const hasArrayPositional = positionalFields.some((f) => f.type === "array");
2852
- const allPositionals = [...parseResult.positionals, ...parseResult.rest];
2853
- const extraPositionals = hasArrayPositional ? [] : allPositionals.slice(positionalFields.length);
2854
- const unconsumedRegulars = hasArrayPositional ? [] : parseResult.positionals.slice(positionalFields.length);
2855
- if (listSubCommands(command).length > 0 && !parseResult.subCommand && !command.run && extraPositionals.length === 0) {
2856
- const help = generateHelp(command, {
2857
- showSubcommands: options.showSubcommands ?? true,
2858
- context
2859
- });
2860
- logger.log(help);
2861
- collector?.stop();
2862
- return {
2863
- success: true,
2864
- result: void 0,
2865
- exitCode: 0,
2866
- logs: getCurrentLogs()
2867
- };
2868
- }
2869
- if (parseResult.unknownFlags.length > 0) {
2870
- const unknownKeysMode = parseResult.extractedFields?.unknownKeysMode ?? "strip";
2871
- const knownFlags = parseResult.extractedFields?.fields.map((f) => f.name) ?? [];
2872
- if (unknownKeysMode === "strict") {
2873
- collector?.stop();
2874
- return {
2875
- success: false,
2876
- error: /* @__PURE__ */ new Error(`Unknown flags: ${parseResult.unknownFlags.join(", ")}`),
2877
- exitCode: 1,
2878
- logs: getCurrentLogs()
2879
- };
2880
- } else if (unknownKeysMode === "strip") for (const flag of parseResult.unknownFlags) logger.error(formatUnknownFlagWarning(flag, knownFlags));
2881
- }
2882
- if (extraPositionals.length > 0) {
2883
- const subCmdNames = listSubCommandNamesWithAliases(command);
2884
- if (subCmdNames.size > 0) {
2885
- const unknownCmd = unconsumedRegulars.find((t) => !t.startsWith("-") && !subCmdNames.has(t));
2886
- if (unknownCmd) {
2887
- const similar = findSimilar(unknownCmd, [...subCmdNames]);
2888
- const suggestion = similar.length > 0 ? ` Did you mean: ${similar.join(", ")}?` : "";
2889
- collector?.stop();
2890
- return {
2891
- success: false,
2892
- error: /* @__PURE__ */ new Error(`Unknown subcommand: ${unknownCmd}${suggestion ? `.${suggestion}` : ""}`),
2893
- exitCode: 1,
2894
- logs: getCurrentLogs()
2895
- };
2896
- }
2897
- }
2898
- const unknownKeysMode = parseResult.extractedFields?.unknownKeysMode ?? "strip";
2899
- if (unknownKeysMode === "strict") {
2900
- collector?.stop();
2901
- return {
2902
- success: false,
2903
- error: /* @__PURE__ */ new Error(`Unexpected positional argument${extraPositionals.length > 1 ? "s" : ""}: ${extraPositionals.join(", ")}`),
2904
- exitCode: 1,
2905
- logs: getCurrentLogs()
2906
- };
2907
- } else if (unknownKeysMode === "strip") for (const positional of extraPositionals) (logger.warn ?? logger.error)(`Warning: Unexpected positional argument: ${positional}`);
2908
- }
2909
- let validatedGlobalArgs = {};
2910
- const isCompletionInvocation = command.name === "__complete";
2911
- const cliProvidedGlobalFields = new Set(Object.keys(accumulatedGlobalArgs));
2912
- const envFallbackGlobalFields = /* @__PURE__ */ new Set();
2913
- if (options.globalArgs && options._globalExtracted && !isCompletionInvocation) {
2914
- for (const field of options._globalExtracted.fields) if (field.env && accumulatedGlobalArgs[field.name] === void 0) {
2915
- const envNames = Array.isArray(field.env) ? field.env : [field.env];
2916
- for (const envName of envNames) {
2917
- const envValue = process.env[envName];
2918
- if (envValue !== void 0) {
2919
- accumulatedGlobalArgs[field.name] = envValue;
2920
- envFallbackGlobalFields.add(field.name);
2921
- break;
2922
- }
2923
- }
2924
- }
2925
- if (options.prompt) {
2926
- const resolved = await options.prompt(accumulatedGlobalArgs, options._globalExtracted);
2927
- for (const [key, value] of Object.entries(resolved)) if (value !== void 0) accumulatedGlobalArgs[key] = value;
2928
- }
2929
- const globalValidation = validateArgs(accumulatedGlobalArgs, options.globalArgs);
2930
- if (!globalValidation.success) {
2931
- collector?.stop();
2932
- return {
2933
- success: false,
2934
- error: new Error(formatValidationErrors(globalValidation.errors)),
2935
- exitCode: 1,
2936
- logs: getCurrentLogs()
2937
- };
2938
- }
2939
- validatedGlobalArgs = globalValidation.data;
2940
- }
2941
- if (!command.args) {
2942
- const globalSourceMap = /* @__PURE__ */ new Map();
2943
- for (const name of cliProvidedGlobalFields) globalSourceMap.set(name, "cli");
2944
- for (const name of envFallbackGlobalFields) globalSourceMap.set(name, "env");
2945
- attachArgSource(validatedGlobalArgs, globalSourceMap);
2946
- attachInvocation(validatedGlobalArgs, resolveInvocation(command, context));
2947
- const proxiedGlobalArgs = createDualCaseProxy(validatedGlobalArgs);
2948
- if (options._globalExtracted && !isCompletionInvocation) await runEffects(proxiedGlobalArgs, options._globalExtracted, proxiedGlobalArgs);
2949
- collector?.stop();
2950
- return await executeLifecycle(command, proxiedGlobalArgs, {
2951
- handleSignals: options.handleSignals,
2952
- captureLogs: options.captureLogs,
2953
- existingLogs: getCurrentLogs(),
2954
- globalCleanup: options._globalCleanup
2955
- });
2956
- }
2957
- let argsToValidate = parseResult.rawArgs;
2958
- const promptResolvedFields = /* @__PURE__ */ new Set();
2959
- if (options.prompt && parseResult.extractedFields) {
2960
- const resolved = await options.prompt(argsToValidate, parseResult.extractedFields);
2961
- const nextArgsToValidate = { ...argsToValidate };
2962
- for (const [key, value] of Object.entries(resolved)) if (value !== void 0) {
2963
- nextArgsToValidate[key] = value;
2964
- promptResolvedFields.add(key);
2965
- }
2966
- argsToValidate = nextArgsToValidate;
2967
- }
2968
- const globalPrefilledFields = /* @__PURE__ */ new Set();
2969
- if (parseResult.extractedFields) {
2970
- for (const field of parseResult.extractedFields.fields) if (!(Object.hasOwn(parseResult.rawArgs, field.name) || promptResolvedFields.has(field.name)) && cliProvidedGlobalFields.has(field.name) && Object.hasOwn(validatedGlobalArgs, field.name)) {
2971
- argsToValidate = {
2972
- ...argsToValidate,
2973
- [field.name]: validatedGlobalArgs[field.name]
2974
- };
2975
- globalPrefilledFields.add(field.name);
2976
- }
2977
- }
2978
- const validationResult = validateArgs(argsToValidate, command.args);
2979
- if (!validationResult.success) {
2980
- collector?.stop();
2981
- return {
2982
- success: false,
2983
- error: new Error(formatValidationErrors(validationResult.errors)),
2984
- exitCode: 1,
2985
- logs: getCurrentLogs()
2986
- };
2987
- }
2988
- const proxiedCommandArgs = createDualCaseProxy(validationResult.data);
2989
- const proxiedGlobalArgs = createDualCaseProxy(validatedGlobalArgs);
2990
- if (options._globalExtracted && !isCompletionInvocation) await runEffects(proxiedGlobalArgs, options._globalExtracted, proxiedGlobalArgs);
2991
- if (parseResult.extractedFields && !isCompletionInvocation) await runEffects(proxiedCommandArgs, parseResult.extractedFields, proxiedGlobalArgs);
2992
- const mergedPlainArgs = {
2993
- ...proxiedGlobalArgs,
2994
- ...proxiedCommandArgs
2995
- };
2996
- const argSourceMap = /* @__PURE__ */ new Map();
2997
- for (const name of cliProvidedGlobalFields) argSourceMap.set(name, "cli");
2998
- for (const name of envFallbackGlobalFields) argSourceMap.set(name, "env");
2999
- const localEnvFallbackFields = parseResult.envFallbackFields ?? /* @__PURE__ */ new Set();
3000
- for (const field of parseResult.extractedFields?.fields ?? []) {
3001
- if (globalPrefilledFields.has(field.name)) {
3002
- argSourceMap.set(field.name, "cli");
3003
- continue;
3004
- }
3005
- const localHasCliOrEnvValue = Object.hasOwn(parseResult.rawArgs, field.name) || localEnvFallbackFields.has(field.name);
3006
- argSourceMap.set(field.name, localHasCliOrEnvValue ? localEnvFallbackFields.has(field.name) ? "env" : "cli" : "default");
3007
- }
3008
- attachArgSource(mergedPlainArgs, argSourceMap);
3009
- attachInvocation(mergedPlainArgs, resolveInvocation(command, context));
3010
- const mergedArgs = createDualCaseProxy(mergedPlainArgs);
3011
- collector?.stop();
3012
- return await executeLifecycle(command, mergedArgs, {
3013
- handleSignals: options.handleSignals,
3014
- captureLogs: options.captureLogs,
3015
- existingLogs: getCurrentLogs(),
3016
- globalCleanup: options._globalCleanup
3017
- });
3018
- } catch (error) {
3019
- const err = error instanceof Error ? error : new Error(String(error));
3020
- collector?.stop();
3021
- return {
3022
- success: false,
3023
- error: err,
3024
- exitCode: 1,
3025
- logs: getCurrentLogs()
3026
- };
3027
- }
3028
- }
3029
- /**
3030
- * Extract global fields from options.globalArgs and validate the schema upfront.
3031
- * Rejects positional fields since global options must be flags.
3032
- * Returns undefined when no globalArgs is provided.
3033
- */
3034
- function extractAndValidateGlobal(options) {
3035
- if (!options.globalArgs) return void 0;
3036
- const extracted = extractFields(options.globalArgs);
3037
- if (!options.skipValidation) {
3038
- validateDuplicateFields(extracted);
3039
- validateCaseVariantCollisions(extracted);
3040
- validateDuplicateAliases(extracted);
3041
- validateDuplicateNegations(extracted);
3042
- validateReservedAliases(extracted, true);
3043
- validateReservedFieldNames(extracted);
3044
- const positionalNames = extracted.fields.filter((f) => f.positional).map((f) => f.name);
3045
- if (positionalNames.length > 0) throw new Error(`Global options schema must not contain positional arguments. Found: ${positionalNames.join(", ")}`);
3046
- }
3047
- return extracted;
3048
- }
3049
-
3050
- //#endregion
3051
- export { ReservedFieldNameError as C, renderMarkdown as E, ReservedAliasError as S, renderInline as T, DuplicateAliasError as _, parseArgv as a, FieldTypeConflictError as b, validateCommand as c, validateDuplicateFields as d, validateDuplicateNegations as f, CaseVariantCollisionError as g, validateReservedFieldNames as h, formatValidationErrors as i, validateCrossSchemaCollisions as l, validateReservedAliases as m, runMain as n, formatCommandValidationErrors as o, validatePositionalConfig as p, createDualCaseProxy as r, validateCaseVariantCollisions as s, runCommand as t, validateDuplicateAliases as u, DuplicateFieldError as v, generateHelp as w, PositionalConfigError as x, DuplicateNegationError as y };