@silvery/commander 0.8.0 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,719 @@
1
+ import { Argument, Command as Command$1, CommanderError, Help, Help as Help$1, InvalidArgumentError, Option, Option as Option$1 } from "commander";
2
+ import { createStyle } from "@silvery/ansi";
3
+ import * as zod from "zod";
4
+ //#region src/colorize.ts
5
+ /**
6
+ * Commander.js help colorization using @silvery/ansi.
7
+ *
8
+ * Uses Commander's built-in style hooks (styleTitle, styleOptionText, etc.)
9
+ * rather than regex post-processing.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * import { Command } from "@silvery/commander"
14
+ * // Command auto-colorizes in its constructor — no manual call needed.
15
+ * // For plain Commander:
16
+ * import { colorizeHelp } from "@silvery/commander"
17
+ * colorizeHelp(program)
18
+ * ```
19
+ */
20
+ const autoStyle = createStyle();
21
+ const s = createStyle({ level: "basic" });
22
+ /**
23
+ * Check if color output should be enabled.
24
+ * Delegates to @silvery/ansi's auto-detection (NO_COLOR, FORCE_COLOR, TERM).
25
+ */
26
+ function shouldColorize() {
27
+ return autoStyle.level > 0;
28
+ }
29
+ /**
30
+ * Apply colorized help output to a Commander.js program and all its subcommands.
31
+ *
32
+ * Uses Commander's built-in `configureHelp()` style hooks rather than
33
+ * post-processing the formatted string.
34
+ *
35
+ * @param program - A Commander Command instance (or compatible object)
36
+ * @param options - Override default style functions for each element
37
+ */
38
+ function colorizeHelp(program, options) {
39
+ const cmds = options?.commands ?? ((t) => s.primary(t));
40
+ const flags = options?.flags ?? ((t) => s.secondary(t));
41
+ const desc = options?.description ?? ((t) => t);
42
+ const heading = options?.heading ?? ((t) => s.bold(t));
43
+ const brackets = options?.brackets ?? ((t) => s.accent(t));
44
+ program.configureHelp({
45
+ styleTitle: (str) => heading(str),
46
+ styleCommandText: (str) => cmds(str),
47
+ styleOptionText: (str) => flags(str),
48
+ styleSubcommandText: (str) => cmds(str),
49
+ styleArgumentText: (str) => brackets(str),
50
+ styleDescriptionText: (str) => desc(str),
51
+ styleCommandDescription: (str) => s.bold.primary(str)
52
+ });
53
+ program.configureOutput({
54
+ getOutHasColors: () => true,
55
+ getErrHasColors: () => true
56
+ });
57
+ for (const sub of program.commands) colorizeHelp(sub, options);
58
+ }
59
+ //#endregion
60
+ //#region src/tokenize.ts
61
+ /** Detect a shell prompt at the start of a line. Supports `$ `, `# `, `> `, `❯ `. */
62
+ const PROMPT_RE = /^([$#>❯]\s+)/;
63
+ /** Match one of: long/short flag, bracket arg, quoted string, bare token, whitespace. */
64
+ const TOKEN_RE = /(--?\S+)|(<[^>]+>|\[[^\]]+\])|('[^']*'|"[^"]*")|(\S+)|(\s+)/g;
65
+ /** Bare command-word pattern (letters/digits/dash, no dots or slashes). */
66
+ const BARE_WORD_RE = /^[a-z][\w-]*$/i;
67
+ /**
68
+ * Tokenize a single line of shell-like text.
69
+ *
70
+ * Classification (matches the original styleSectionTerm contract):
71
+ * - First non-whitespace bare word after the prompt is `program`.
72
+ * - The IMMEDIATELY-FOLLOWING bare word (the second bare word) is `subcommand`,
73
+ * IF it looks like a command word (lowercase letter start, no dots/slashes).
74
+ * - All subsequent bare words are `value`s (filenames, package names, etc.).
75
+ * - Flags (`--foo`, `-f`), bracket args (`<x>`, `[y]`), and quoted strings end
76
+ * the "command portion" — anything after them is a value.
77
+ *
78
+ * The "max 1 subcommand" rule keeps `$ npm install foo` correctly classified
79
+ * as program/subcommand/value (not program/subcommand/subcommand). For nested
80
+ * commands (`$ git remote add ...`), only the first subcommand level is styled.
81
+ *
82
+ * If the line has no shell prompt, the same classification still applies —
83
+ * the leading word is `program`, etc.
84
+ */
85
+ function tokenizeCmdline(line) {
86
+ const tokens = [];
87
+ const promptMatch = line.match(PROMPT_RE);
88
+ let body = line;
89
+ if (promptMatch) {
90
+ tokens.push({
91
+ kind: "prompt",
92
+ text: promptMatch[1]
93
+ });
94
+ body = line.slice(promptMatch[1].length);
95
+ }
96
+ const matches = body.match(TOKEN_RE) ?? [];
97
+ let commandWords = 0;
98
+ let doneWithCommand = false;
99
+ for (const token of matches) {
100
+ if (/^\s+$/.test(token)) {
101
+ tokens.push({
102
+ kind: "whitespace",
103
+ text: token
104
+ });
105
+ continue;
106
+ }
107
+ if (/^--?\S/.test(token)) {
108
+ doneWithCommand = true;
109
+ tokens.push({
110
+ kind: "flag",
111
+ text: token
112
+ });
113
+ continue;
114
+ }
115
+ if (/^[<[]/.test(token)) {
116
+ doneWithCommand = true;
117
+ tokens.push({
118
+ kind: "arg-bracket",
119
+ text: token
120
+ });
121
+ continue;
122
+ }
123
+ if (/^["']/.test(token)) {
124
+ doneWithCommand = true;
125
+ tokens.push({
126
+ kind: "quoted",
127
+ text: token
128
+ });
129
+ continue;
130
+ }
131
+ if (!doneWithCommand && commandWords === 0) {
132
+ commandWords++;
133
+ tokens.push({
134
+ kind: "program",
135
+ text: token
136
+ });
137
+ continue;
138
+ }
139
+ if (!doneWithCommand && commandWords === 1 && BARE_WORD_RE.test(token)) {
140
+ commandWords++;
141
+ tokens.push({
142
+ kind: "subcommand",
143
+ text: token
144
+ });
145
+ continue;
146
+ }
147
+ doneWithCommand = true;
148
+ tokens.push({
149
+ kind: "value",
150
+ text: token
151
+ });
152
+ }
153
+ return tokens;
154
+ }
155
+ /** Returns true if the line starts with a recognized shell prompt. */
156
+ function isShellLine(line) {
157
+ return PROMPT_RE.test(line);
158
+ }
159
+ //#endregion
160
+ //#region src/command.ts
161
+ /**
162
+ * Enhanced Commander Command with auto-colorized help, Standard Schema support,
163
+ * array-as-choices detection, and **type-safe option + argument inference**.
164
+ *
165
+ * Subclasses Commander's Command so `new Command("app")` just works --
166
+ * it's Commander with auto-colorized help, automatic Standard Schema /
167
+ * legacy Zod detection, and array choices in `.option()` and `.argument()`.
168
+ *
169
+ * Each `.option()` call narrows the return type so that `.action()`, `.opts()`,
170
+ * and `.actionMerged()` know exactly which options exist and what types they have.
171
+ * Each `.argument()` call appends to both a positional tuple (for `.action()`)
172
+ * and a named record (for `.actionMerged()`).
173
+ *
174
+ * `.action(fn)` is Commander-native: positional args, then options, then command.
175
+ * `.actionMerged(fn)` is an opt-in helper that merges args and options into a
176
+ * single named object — nicer for commands with many positional arguments.
177
+ *
178
+ * @example
179
+ * ```ts
180
+ * import { Command, port, csv } from "@silvery/commander"
181
+ *
182
+ * // Commander-native: positional args, then options
183
+ * new Command("deploy")
184
+ * .argument("<service>", "Service to deploy")
185
+ * .argument("[env]", "Environment", ["dev", "staging", "prod"])
186
+ * .option("-p, --port <n>", "Port", port)
187
+ * .option("--verbose", "Verbose output")
188
+ * .action((service, env, opts) => {
189
+ * service // string
190
+ * env // "dev" | "staging" | "prod" | undefined
191
+ * opts.port // number | undefined
192
+ * opts.verbose // boolean | undefined
193
+ * })
194
+ *
195
+ * // Merged form: flat named object with args and options merged
196
+ * new Command("deploy")
197
+ * .argument("<service>")
198
+ * .argument("[env]", ["dev", "staging", "prod"] as const)
199
+ * .option("-p, --port <n>", "Port", port)
200
+ * .actionMerged(({ service, env, port }) => { ... })
201
+ *
202
+ * program.parse()
203
+ * ```
204
+ */
205
+ /** Runtime check: is this value a Standard Schema v1 object? */
206
+ function isStandardSchema(value) {
207
+ return typeof value === "object" && value !== null && "~standard" in value;
208
+ }
209
+ /** Wrap a Standard Schema as a Commander parser function */
210
+ function standardSchemaParser(schema) {
211
+ return (value) => {
212
+ const result = schema["~standard"].validate(value);
213
+ if ("issues" in result) {
214
+ const msg = result.issues.map((i) => i.message).join(", ");
215
+ throw new Error(msg);
216
+ }
217
+ return result.value;
218
+ };
219
+ }
220
+ function isLegacyZodSchema(value) {
221
+ return typeof value === "object" && value !== null && typeof value.parse === "function" && "_def" in value && !("~standard" in value);
222
+ }
223
+ function legacyZodParser(schema) {
224
+ return (value) => {
225
+ try {
226
+ return schema.parse(value);
227
+ } catch (err) {
228
+ if (err?.issues) {
229
+ const messages = err.issues.map((i) => i.message).join(", ");
230
+ throw new Error(messages);
231
+ }
232
+ throw err;
233
+ }
234
+ };
235
+ }
236
+ /**
237
+ * Style a SINGLE-LINE section term using Commander's style hooks.
238
+ *
239
+ * Three modes:
240
+ * - Shell command line (`$ termless play demo.tape`): tokenize via tokenizeCmdline()
241
+ * and style each token by kind (dim prompt, primary program/subcommand,
242
+ * secondary flags, accent brackets, dim quoted strings, plain values)
243
+ * - Option-like (`-f, --flag`): entire term styled as option
244
+ * - Plain text: command words + argument brackets
245
+ *
246
+ * For multi-line terms, callers split on \n and call this per line — see
247
+ * _renderSections().
248
+ */
249
+ function styleSectionTerm(term, helper) {
250
+ if (isShellLine(term)) return styleCmdlineTokens(tokenizeCmdline(term), helper);
251
+ if (/^\s*-/.test(term)) return helper.styleOptionText(term);
252
+ const parts = term.split(/(<[^>]+>|\[[^\]]+\])/g);
253
+ if (parts.length === 1) return helper.styleCommandText(term);
254
+ return parts.map((part) => {
255
+ if (/^[<[]/.test(part)) return helper.styleArgumentText(part);
256
+ if (part) return helper.styleCommandText(part);
257
+ return part;
258
+ }).join("");
259
+ }
260
+ /**
261
+ * Apply style hooks to a stream of CmdlineTokens.
262
+ *
263
+ * Each token kind maps to one of Commander's style hooks (or a manual ANSI
264
+ * dim escape for prompts/quoted strings, since Commander's style hook set
265
+ * doesn't have a "dim" entry).
266
+ */
267
+ function styleCmdlineTokens(tokens, helper) {
268
+ const dim = (text) => shouldColorize() ? `\x1b[2m${text}\x1b[22m` : text;
269
+ return tokens.map((t) => {
270
+ switch (t.kind) {
271
+ case "prompt": return dim(t.text);
272
+ case "program":
273
+ case "subcommand": return helper.styleCommandText(t.text);
274
+ case "flag": return helper.styleOptionText(t.text);
275
+ case "arg-bracket": return helper.styleArgumentText(t.text);
276
+ case "quoted": return dim(t.text);
277
+ case "value":
278
+ case "whitespace": return t.text;
279
+ }
280
+ }).join("");
281
+ }
282
+ /**
283
+ * Compute the longest visual width of any line within a (possibly multi-line) term.
284
+ * Used for column alignment when terms span multiple lines.
285
+ */
286
+ function maxLineWidth(term) {
287
+ let max = 0;
288
+ for (const line of term.split("\n")) if (line.length > max) max = line.length;
289
+ return max;
290
+ }
291
+ /**
292
+ * Type-safe Commander Command with auto-colorized help, Standard Schema support,
293
+ * array-as-choices detection, and inferred option types.
294
+ *
295
+ * Use `.action()` for Commander-native positional form — `(arg1, arg2, opts, cmd) => ...`.
296
+ * Use `.actionMerged()` for a flat destructured form — `({ arg1, arg2, opt1 }, cmd) => ...`.
297
+ *
298
+ * @example
299
+ * ```ts
300
+ * new Command("deploy")
301
+ * .argument("<service>", "Service to deploy")
302
+ * .argument("[env]", "Environment", ["dev", "staging", "prod"] as const)
303
+ * .option("--verbose", "Verbose output")
304
+ * .action((service, env, opts) => { ... })
305
+ * // or: .actionMerged(({ service, env, verbose }) => { ... })
306
+ * ```
307
+ */
308
+ const Command = class _CommandBase extends Command$1 {
309
+ _helpSectionList = [];
310
+ _helpSectionsInstalled = false;
311
+ /** Argument names registered via typed .argument() — used by .action() to merge into opts. */
312
+ _typedArgNames = [];
313
+ constructor(name) {
314
+ super(name);
315
+ colorizeHelp(this);
316
+ this._capitalizeBuiltinDescriptions();
317
+ }
318
+ /**
319
+ * Add an option with smart third-argument detection.
320
+ *
321
+ * The third argument is detected in order:
322
+ * 1. **Array** -- treated as choices (Commander `.choices()`)
323
+ * 2. **Standard Schema v1** -- wrapped as a parser function
324
+ * 3. **Legacy Zod** (pre-3.24, has `_def` + `parse`) -- wrapped as a parser
325
+ * 4. **Function** -- passed through as Commander's parser function
326
+ * 5. **Anything else** -- passed through as a default value
327
+ */
328
+ option(flags, description, parseArgOrDefault, defaultValue) {
329
+ if (Array.isArray(parseArgOrDefault)) {
330
+ const opt = new Option$1(flags, description ?? "").choices(parseArgOrDefault);
331
+ this.addOption(opt);
332
+ return this;
333
+ }
334
+ if (isStandardSchema(parseArgOrDefault)) return super.option(flags, description ?? "", standardSchemaParser(parseArgOrDefault), defaultValue);
335
+ if (isLegacyZodSchema(parseArgOrDefault)) return super.option(flags, description ?? "", legacyZodParser(parseArgOrDefault), defaultValue);
336
+ if (typeof parseArgOrDefault === "function") return super.option(flags, description ?? "", parseArgOrDefault, defaultValue);
337
+ return super.option(flags, description ?? "", parseArgOrDefault);
338
+ }
339
+ /**
340
+ * Add an argument with smart third-argument detection.
341
+ *
342
+ * Same detection as `.option()`: array → choices, Standard Schema → parser,
343
+ * legacy Zod → parser, function → parser, anything else → default value.
344
+ * Tracks argument names for `.action()` merging.
345
+ */
346
+ argument(flags, description, parseArgOrDefault, defaultValue) {
347
+ const camelName = flags.replace(/[<\[\]>.]/g, "").trim().replace(/-([a-z])/g, (_, c) => c.toUpperCase());
348
+ this._typedArgNames.push(camelName);
349
+ if (Array.isArray(parseArgOrDefault)) {
350
+ const arg = this.createArgument(flags, description ?? "");
351
+ arg.choices(parseArgOrDefault);
352
+ this.addArgument(arg);
353
+ return this;
354
+ }
355
+ if (isStandardSchema(parseArgOrDefault)) return super.argument(flags, description ?? "", standardSchemaParser(parseArgOrDefault), defaultValue);
356
+ if (isLegacyZodSchema(parseArgOrDefault)) return super.argument(flags, description ?? "", legacyZodParser(parseArgOrDefault), defaultValue);
357
+ if (typeof parseArgOrDefault === "function") return super.argument(flags, description ?? "", parseArgOrDefault, defaultValue);
358
+ return super.argument(flags, description ?? "", parseArgOrDefault);
359
+ }
360
+ /**
361
+ * Register an action callback with a named-parameter object instead of positional args.
362
+ *
363
+ * The handler receives a single object containing all positional arguments
364
+ * (merged by name) and all options, plus the Command instance as a second arg.
365
+ * Argument names come from the typed `.argument()` calls, camelCased.
366
+ *
367
+ * This is a convenience wrapper over `.action()` — `.action()` itself remains
368
+ * Commander-native `(...args, opts, command) => ...`. Prefer `.action()` for
369
+ * single-arg commands (where positional is more ergonomic) and `.actionMerged()`
370
+ * for multi-arg commands (where a flat named object is nicer).
371
+ *
372
+ * @example
373
+ * ```ts
374
+ * new Command("deploy")
375
+ * .argument("<service>")
376
+ * .argument("[env]")
377
+ * .option("-p, --port <n>", port)
378
+ * .actionMerged(({ service, env, port }, cmd) => {
379
+ * // service, env, port are all typed
380
+ * // cmd is the Command instance
381
+ * })
382
+ * ```
383
+ */
384
+ actionMerged(fn) {
385
+ const argNames = this._typedArgNames;
386
+ return super.action((...args) => {
387
+ const cmd = args[args.length - 1];
388
+ const merged = { ...args[args.length - 2] ?? {} };
389
+ for (let i = 0; i < argNames.length; i++) merged[argNames[i]] = args[i];
390
+ return fn(merged, cmd);
391
+ });
392
+ }
393
+ addHelpSection(positionOrTitle, titleOrContent, content) {
394
+ let position;
395
+ let title;
396
+ let body;
397
+ if (content !== void 0) {
398
+ position = positionOrTitle;
399
+ title = titleOrContent;
400
+ body = content;
401
+ } else {
402
+ position = "after";
403
+ title = positionOrTitle;
404
+ body = titleOrContent;
405
+ }
406
+ this._helpSectionList.push({
407
+ position,
408
+ title,
409
+ content: body
410
+ });
411
+ this._installHelpSectionHooks();
412
+ return this;
413
+ }
414
+ createCommand(name) {
415
+ return new _CommandBase(name);
416
+ }
417
+ /**
418
+ * Override Commander's `command()` to mirror inline-arg names (from
419
+ * `command("deploy <service>")`) into our `_typedArgNames`. This keeps
420
+ * `.actionMerged()` working on subcommands declared with the inline form,
421
+ * since the merged dispatch reads from `_typedArgNames` to know which
422
+ * positional indices map to which key.
423
+ *
424
+ * Both forms now coexist:
425
+ * .command("deploy <service>") // inline — names from string
426
+ * .command("deploy").argument("<service>") // explicit — names from .argument()
427
+ * Both produce equivalent runtime + type behavior.
428
+ */
429
+ command(...args) {
430
+ const result = super.command.apply(this, args);
431
+ const sub = this.commands[this.commands.length - 1];
432
+ if (sub && Array.isArray(sub._args)) for (const arg of sub._args) {
433
+ const camel = arg.name().replace(/-([a-z])/g, (_, c) => c.toUpperCase());
434
+ if (!sub._typedArgNames.includes(camel)) sub._typedArgNames.push(camel);
435
+ }
436
+ return result;
437
+ }
438
+ /**
439
+ * Auto-detect capitalization from user-provided descriptions and match it
440
+ * for built-in options (-V/--version, -h/--help).
441
+ */
442
+ _capitalizeBuiltinDescriptions() {
443
+ const origHelp = this.helpInformation.bind(this);
444
+ this.helpInformation = () => {
445
+ const builtinFlags = new Set(["-V, --version", "-h, --help"]);
446
+ const userDescs = this.options.filter((opt) => !builtinFlags.has(opt.flags) && opt.description && /^[a-zA-Z]/.test(opt.description)).map((opt) => opt.description);
447
+ for (const cmd of this.commands) if (cmd.description() && /^[a-zA-Z]/.test(cmd.description())) userDescs.push(cmd.description());
448
+ if (userDescs.length > 0) {
449
+ if (userDescs.filter((d) => /^[A-Z]/.test(d)).length > userDescs.length / 2) {
450
+ for (const opt of this.options) if (builtinFlags.has(opt.flags) && opt.description && /^[a-z]/.test(opt.description)) opt.description = opt.description[0].toUpperCase() + opt.description.slice(1);
451
+ const helpOpt = this._helpOption;
452
+ if (helpOpt?.description && /^[a-z]/.test(helpOpt.description)) helpOpt.description = helpOpt.description[0].toUpperCase() + helpOpt.description.slice(1);
453
+ }
454
+ }
455
+ return origHelp();
456
+ };
457
+ }
458
+ /**
459
+ * Render sections for a given position using the help formatter.
460
+ *
461
+ * Handles multi-line term entries (terms containing `\n`) by emitting one
462
+ * row per line. The description column is **top-aligned**: it appears only
463
+ * on the first line of a multi-line term, with subsequent lines holding
464
+ * just the styled term and an empty description column.
465
+ */
466
+ _renderSections(position, helper, termWidth) {
467
+ const sections = this._helpSectionList.filter((s) => s.position === position);
468
+ if (sections.length === 0) return "";
469
+ const lines = [];
470
+ for (const section of sections) {
471
+ lines.push("");
472
+ lines.push(helper.styleTitle(section.title));
473
+ if (typeof section.content === "string") for (const line of section.content.split("\n")) lines.push(` ${line}`);
474
+ else for (const [term, desc] of section.content) {
475
+ const termLines = term.split("\n");
476
+ for (let i = 0; i < termLines.length; i++) {
477
+ const lineText = termLines[i];
478
+ const styledTerm = styleSectionTerm(lineText, helper);
479
+ const lineDesc = i === 0 ? helper.styleDescriptionText(desc) : "";
480
+ lines.push(helper.formatItem(styledTerm, termWidth, lineDesc, helper));
481
+ }
482
+ }
483
+ }
484
+ lines.push("");
485
+ return lines.join("\n");
486
+ }
487
+ /** Install hooks once — merges with existing configureHelp, adds addHelpText for before/afterAll. */
488
+ _installHelpSectionHooks() {
489
+ if (this._helpSectionsInstalled) return;
490
+ this._helpSectionsInstalled = true;
491
+ const self = this;
492
+ const existing = this._helpConfiguration ?? {};
493
+ const origPadWidth = existing.padWidth;
494
+ const origFormatHelp = existing.formatHelp;
495
+ const protoFormatHelp = Help$1.prototype.formatHelp;
496
+ this.configureHelp({
497
+ ...existing,
498
+ padWidth(cmd, helper) {
499
+ const base = origPadWidth ? origPadWidth(cmd, helper) : Math.max(helper.longestOptionTermLength(cmd, helper), helper.longestGlobalOptionTermLength?.(cmd, helper) ?? 0, helper.longestSubcommandTermLength(cmd, helper), helper.longestArgumentTermLength(cmd, helper));
500
+ if (cmd !== self) return base;
501
+ let sectionMax = 0;
502
+ for (const section of self._helpSectionList) if (typeof section.content !== "string") for (const [term] of section.content) {
503
+ const w = maxLineWidth(term);
504
+ if (w > sectionMax) sectionMax = w;
505
+ }
506
+ return Math.max(base, sectionMax);
507
+ },
508
+ formatHelp(cmd, helper) {
509
+ const baseHelp = origFormatHelp ? origFormatHelp(cmd, helper) : protoFormatHelp.call(helper, cmd, helper);
510
+ if (cmd !== self) return baseHelp;
511
+ const termWidth = helper.padWidth(cmd, helper);
512
+ const before = self._renderSections("before", helper, termWidth);
513
+ const after = self._renderSections("after", helper, termWidth);
514
+ return (before ? before + "\n" : "") + baseHelp + after;
515
+ }
516
+ });
517
+ this.addHelpText("beforeAll", () => {
518
+ const helper = this.createHelp();
519
+ const termWidth = helper.padWidth(this, helper);
520
+ return this._renderSections("beforeAll", helper, termWidth);
521
+ });
522
+ this.addHelpText("afterAll", () => {
523
+ const helper = this.createHelp();
524
+ const termWidth = helper.padWidth(this, helper);
525
+ return this._renderSections("afterAll", helper, termWidth);
526
+ });
527
+ }
528
+ };
529
+ //#endregion
530
+ //#region src/presets.ts
531
+ function createType(vendor, validate) {
532
+ const schema = {
533
+ "~standard": {
534
+ version: 1,
535
+ vendor,
536
+ validate: (value) => {
537
+ try {
538
+ return { value: validate(value) };
539
+ } catch (e) {
540
+ return { issues: [{ message: e.message }] };
541
+ }
542
+ }
543
+ },
544
+ parse(value) {
545
+ const result = schema["~standard"].validate(value);
546
+ if ("issues" in result) throw new Error(result.issues[0]?.message ?? "Validation failed");
547
+ return result.value;
548
+ },
549
+ safeParse(value) {
550
+ const result = schema["~standard"].validate(value);
551
+ if ("issues" in result) return {
552
+ success: false,
553
+ issues: [...result.issues]
554
+ };
555
+ return {
556
+ success: true,
557
+ value: result.value
558
+ };
559
+ }
560
+ };
561
+ return schema;
562
+ }
563
+ const VENDOR = "@silvery/commander";
564
+ /** Integer (coerced from string). */
565
+ const int = createType(VENDOR, (v) => {
566
+ const s = String(v).trim();
567
+ if (s === "") throw new Error(`Expected integer, got "${v}"`);
568
+ const n = Number(s);
569
+ if (!Number.isInteger(n)) throw new Error(`Expected integer, got "${v}"`);
570
+ return n;
571
+ });
572
+ /** Unsigned integer (>= 0, coerced from string). */
573
+ const uint = createType(VENDOR, (v) => {
574
+ const s = String(v).trim();
575
+ if (s === "") throw new Error(`Expected unsigned integer (>= 0), got "${v}"`);
576
+ const n = Number(s);
577
+ if (!Number.isInteger(n) || n < 0) throw new Error(`Expected unsigned integer (>= 0), got "${v}"`);
578
+ return n;
579
+ });
580
+ /** Float (coerced from string). */
581
+ const float = createType(VENDOR, (v) => {
582
+ const s = String(v).trim();
583
+ if (s === "" || s === "NaN") throw new Error(`Expected number, got "${v}"`);
584
+ const n = Number(s);
585
+ if (Number.isNaN(n)) throw new Error(`Expected number, got "${v}"`);
586
+ return n;
587
+ });
588
+ /** Port number (1-65535). */
589
+ const port = createType(VENDOR, (v) => {
590
+ const n = Number(v);
591
+ if (!Number.isInteger(n) || n < 1 || n > 65535) throw new Error(`Expected port (1-65535), got "${v}"`);
592
+ return n;
593
+ });
594
+ /** URL (validated via URL constructor). */
595
+ const url = createType(VENDOR, (v) => {
596
+ const s = String(v);
597
+ try {
598
+ new URL(s);
599
+ return s;
600
+ } catch {
601
+ throw new Error(`Expected valid URL, got "${v}"`);
602
+ }
603
+ });
604
+ /** File path (non-empty string). */
605
+ const path = createType(VENDOR, (v) => {
606
+ const s = String(v);
607
+ if (!s) throw new Error("Expected non-empty path");
608
+ return s;
609
+ });
610
+ /** Comma-separated values to string[]. */
611
+ const csv = createType(VENDOR, (v) => {
612
+ return String(v).split(",").map((s) => s.trim()).filter(Boolean);
613
+ });
614
+ /** JSON string to parsed value. */
615
+ const json = createType(VENDOR, (v) => {
616
+ try {
617
+ return JSON.parse(String(v));
618
+ } catch {
619
+ throw new Error(`Expected valid JSON, got "${v}"`);
620
+ }
621
+ });
622
+ /** Boolean string ("true"/"false"/"1"/"0"/"yes"/"no"). */
623
+ const bool = createType(VENDOR, (v) => {
624
+ const s = String(v).toLowerCase();
625
+ if ([
626
+ "true",
627
+ "1",
628
+ "yes",
629
+ "y"
630
+ ].includes(s)) return true;
631
+ if ([
632
+ "false",
633
+ "0",
634
+ "no",
635
+ "n"
636
+ ].includes(s)) return false;
637
+ throw new Error(`Expected boolean (true/false/yes/no/1/0), got "${v}"`);
638
+ });
639
+ /** Date string to Date object. */
640
+ const date = createType(VENDOR, (v) => {
641
+ const d = new Date(String(v));
642
+ if (isNaN(d.getTime())) throw new Error(`Expected valid date, got "${v}"`);
643
+ return d;
644
+ });
645
+ /** Email address (basic validation). */
646
+ const email = createType(VENDOR, (v) => {
647
+ const s = String(v);
648
+ if (!s.includes("@") || !s.includes(".")) throw new Error(`Expected email address, got "${v}"`);
649
+ return s;
650
+ });
651
+ /** Regex pattern string to RegExp. */
652
+ const regex = createType(VENDOR, (v) => {
653
+ try {
654
+ return new RegExp(String(v));
655
+ } catch {
656
+ throw new Error(`Expected valid regex, got "${v}"`);
657
+ }
658
+ });
659
+ /** Integer with min/max bounds (factory). */
660
+ function intRange(min, max) {
661
+ return createType(VENDOR, (v) => {
662
+ const n = Number(v);
663
+ if (!Number.isInteger(n) || n < min || n > max) throw new Error(`Expected integer ${min}-${max}, got "${v}"`);
664
+ return n;
665
+ });
666
+ }
667
+ //#endregion
668
+ //#region src/z.ts
669
+ /**
670
+ * Extended Zod object with CLI types.
671
+ *
672
+ * Spreads all of Zod's exports and adds CLI-specific schemas.
673
+ * Tree-shakeable -- only evaluated if user imports `z`.
674
+ *
675
+ * @example
676
+ * ```ts
677
+ * import { z, Command } from "@silvery/commander"
678
+ *
679
+ * new Command("deploy")
680
+ * .option("-p, --port <n>", "Port", z.port)
681
+ * .option("--tags <t>", "Tags", z.csv)
682
+ * .option("-e, --env <e>", "Env", ["dev", "staging", "prod"])
683
+ *
684
+ * program.parse()
685
+ * ```
686
+ */
687
+ const z = {
688
+ ...zod,
689
+ port: zod.coerce.number().int().min(1).max(65535),
690
+ int: zod.coerce.number().int(),
691
+ uint: zod.coerce.number().int().min(0),
692
+ float: zod.coerce.number(),
693
+ csv: zod.string().transform((v) => v.split(",").map((s) => s.trim()).filter(Boolean)),
694
+ url: zod.string().url(),
695
+ path: zod.string().min(1),
696
+ email: zod.string().email(),
697
+ date: zod.coerce.date(),
698
+ json: zod.string().transform((v) => JSON.parse(v)),
699
+ bool: zod.enum([
700
+ "true",
701
+ "false",
702
+ "1",
703
+ "0",
704
+ "yes",
705
+ "no",
706
+ "y",
707
+ "n"
708
+ ]).transform((v) => [
709
+ "true",
710
+ "1",
711
+ "yes",
712
+ "y"
713
+ ].includes(v)),
714
+ intRange: (min, max) => zod.coerce.number().int().min(min).max(max)
715
+ };
716
+ //#endregion
717
+ export { Argument, Command, CommanderError, Help, InvalidArgumentError, Option, bool, colorizeHelp, csv, date, email, float, int, intRange, json, path, port, regex, shouldColorize, uint, url, z };
718
+
719
+ //# sourceMappingURL=index.mjs.map