@kidd-cli/core 0.1.0

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 (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +214 -0
  3. package/dist/config-BvGapuFJ.js +282 -0
  4. package/dist/config-BvGapuFJ.js.map +1 -0
  5. package/dist/create-store-BQUX0tAn.js +197 -0
  6. package/dist/create-store-BQUX0tAn.js.map +1 -0
  7. package/dist/index.d.ts +73 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +1034 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/lib/config.d.ts +64 -0
  12. package/dist/lib/config.d.ts.map +1 -0
  13. package/dist/lib/config.js +4 -0
  14. package/dist/lib/logger.d.ts +2 -0
  15. package/dist/lib/logger.js +55 -0
  16. package/dist/lib/logger.js.map +1 -0
  17. package/dist/lib/output.d.ts +62 -0
  18. package/dist/lib/output.d.ts.map +1 -0
  19. package/dist/lib/output.js +276 -0
  20. package/dist/lib/output.js.map +1 -0
  21. package/dist/lib/project.d.ts +59 -0
  22. package/dist/lib/project.d.ts.map +1 -0
  23. package/dist/lib/project.js +3 -0
  24. package/dist/lib/prompts.d.ts +24 -0
  25. package/dist/lib/prompts.d.ts.map +1 -0
  26. package/dist/lib/prompts.js +3 -0
  27. package/dist/lib/store.d.ts +56 -0
  28. package/dist/lib/store.d.ts.map +1 -0
  29. package/dist/lib/store.js +4 -0
  30. package/dist/logger-BkQQej8h.d.ts +76 -0
  31. package/dist/logger-BkQQej8h.d.ts.map +1 -0
  32. package/dist/middleware/auth.d.ts +22 -0
  33. package/dist/middleware/auth.d.ts.map +1 -0
  34. package/dist/middleware/auth.js +759 -0
  35. package/dist/middleware/auth.js.map +1 -0
  36. package/dist/middleware/http.d.ts +87 -0
  37. package/dist/middleware/http.d.ts.map +1 -0
  38. package/dist/middleware/http.js +255 -0
  39. package/dist/middleware/http.js.map +1 -0
  40. package/dist/middleware-D3psyhYo.js +54 -0
  41. package/dist/middleware-D3psyhYo.js.map +1 -0
  42. package/dist/project-NPtYX2ZX.js +181 -0
  43. package/dist/project-NPtYX2ZX.js.map +1 -0
  44. package/dist/prompts-lLfUSgd6.js +63 -0
  45. package/dist/prompts-lLfUSgd6.js.map +1 -0
  46. package/dist/types-CqKJhsYk.d.ts +135 -0
  47. package/dist/types-CqKJhsYk.d.ts.map +1 -0
  48. package/dist/types-Cz9h927W.d.ts +23 -0
  49. package/dist/types-Cz9h927W.d.ts.map +1 -0
  50. package/dist/types-DFtYg5uZ.d.ts +26 -0
  51. package/dist/types-DFtYg5uZ.d.ts.map +1 -0
  52. package/dist/types-kjpRau0U.d.ts +382 -0
  53. package/dist/types-kjpRau0U.d.ts.map +1 -0
  54. package/package.json +94 -0
package/dist/index.js ADDED
@@ -0,0 +1,1034 @@
1
+ import { createCliLogger } from "./lib/logger.js";
2
+ import { i as createSpinner, r as createPromptUtils } from "./prompts-lLfUSgd6.js";
3
+ import { n as DEFAULT_EXIT_CODE, t as createConfigClient } from "./config-BvGapuFJ.js";
4
+ import { n as decorateContext, t as middleware } from "./middleware-D3psyhYo.js";
5
+ import "./project-NPtYX2ZX.js";
6
+ import { basename, extname, join, resolve } from "node:path";
7
+ import { loadConfig } from "@kidd-cli/config/loader";
8
+ import { attemptAsync, err, isPlainObject, isString, ok } from "@kidd-cli/utils/fp";
9
+ import yargs from "yargs";
10
+ import { TAG, hasTag, withTag } from "@kidd-cli/utils/tag";
11
+ import { jsonStringify } from "@kidd-cli/utils/json";
12
+ import { readdir } from "node:fs/promises";
13
+ import { formatZodIssues } from "@kidd-cli/utils/validate";
14
+ import { match as match$1 } from "ts-pattern";
15
+ import { defineConfig } from "@kidd-cli/config";
16
+
17
+ //#region src/context/error.ts
18
+ /**
19
+ * Create a ContextError with an exit code and optional error code.
20
+ *
21
+ * Used to surface user-facing CLI errors with clean messages.
22
+ * The error carries a Symbol-based tag for reliable type-narrowing
23
+ * via {@link isContextError}.
24
+ *
25
+ * @param message - Human-readable error message.
26
+ * @param options - Optional error code and exit code overrides.
27
+ * @returns A ContextError instance.
28
+ */
29
+ function createContextError(message, options) {
30
+ const data = createContextErrorData(message, options);
31
+ const error = new Error(data.message);
32
+ error.name = "ContextError";
33
+ Object.defineProperty(error, TAG, {
34
+ enumerable: false,
35
+ value: "ContextError",
36
+ writable: false
37
+ });
38
+ Object.defineProperty(error, "code", {
39
+ enumerable: true,
40
+ value: data.code,
41
+ writable: false
42
+ });
43
+ Object.defineProperty(error, "exitCode", {
44
+ enumerable: true,
45
+ value: data.exitCode,
46
+ writable: false
47
+ });
48
+ return error;
49
+ }
50
+ /**
51
+ * Type guard that narrows an unknown value to {@link ContextError}.
52
+ *
53
+ * Checks that the value is an Error instance whose `[TAG]` property
54
+ * equals `'ContextError'`, which distinguishes CLI-layer errors from
55
+ * unexpected exceptions.
56
+ *
57
+ * @param error - The value to check.
58
+ * @returns `true` when the value is a ContextError.
59
+ */
60
+ function isContextError(error) {
61
+ if (error instanceof Error) return hasTag(error, "ContextError");
62
+ return false;
63
+ }
64
+ function resolveExitCode(options) {
65
+ if (options && options.exitCode !== void 0) return options.exitCode;
66
+ return DEFAULT_EXIT_CODE;
67
+ }
68
+ function resolveCode(options) {
69
+ if (options && options.code !== void 0) return options.code;
70
+ }
71
+ function createContextErrorData(message, options) {
72
+ return withTag({
73
+ code: resolveCode(options),
74
+ exitCode: resolveExitCode(options),
75
+ message
76
+ }, "ContextError");
77
+ }
78
+
79
+ //#endregion
80
+ //#region src/context/output.ts
81
+ /**
82
+ * Create the structured output methods for a context.
83
+ *
84
+ * @private
85
+ * @param stream - The writable stream to write output to.
86
+ * @returns An Output instance backed by the given stream.
87
+ */
88
+ function createContextOutput(stream) {
89
+ return {
90
+ markdown(content) {
91
+ stream.write(`${content}\n`);
92
+ },
93
+ raw(content) {
94
+ stream.write(content);
95
+ },
96
+ table(rows, options) {
97
+ if (options && options.json) {
98
+ const [, json] = jsonStringify(rows, { pretty: true });
99
+ stream.write(`${json}\n`);
100
+ return;
101
+ }
102
+ if (rows.length === 0) return;
103
+ const [firstRow] = rows;
104
+ if (!firstRow) return;
105
+ writeTableToStream(stream, rows, Object.keys(firstRow));
106
+ },
107
+ write(data, options) {
108
+ if (options && options.json || typeof data === "object" && data !== null) {
109
+ const [, json] = jsonStringify(data, { pretty: true });
110
+ stream.write(`${json}\n`);
111
+ } else stream.write(`${String(data)}\n`);
112
+ }
113
+ };
114
+ }
115
+ /**
116
+ * Format an unknown value as a string for table cell display.
117
+ *
118
+ * @private
119
+ * @param val - The value to format.
120
+ * @returns The stringified value, or empty string for undefined.
121
+ */
122
+ function formatStringValue(val) {
123
+ if (val === void 0) return "";
124
+ return String(val);
125
+ }
126
+ /**
127
+ * Create a padded header row string from column keys and widths.
128
+ *
129
+ * @private
130
+ * @param options - The keys and column widths.
131
+ * @returns A formatted header string.
132
+ */
133
+ function createTableHeader(options) {
134
+ const { keys, widths } = options;
135
+ return keys.map((key, idx) => {
136
+ const width = widths[idx];
137
+ if (width === void 0) return key;
138
+ return key.padEnd(width);
139
+ }).join(" ");
140
+ }
141
+ /**
142
+ * Create a padded row string from a data record, column keys, and widths.
143
+ *
144
+ * @private
145
+ * @param options - The row data, keys, and column widths.
146
+ * @returns A formatted row string.
147
+ */
148
+ function createTableRow(options) {
149
+ const { row, keys, widths } = options;
150
+ return keys.map((key, idx) => {
151
+ const width = widths[idx];
152
+ const val = formatStringValue(row[key]);
153
+ if (width === void 0) return val;
154
+ return val.padEnd(width);
155
+ }).join(" ");
156
+ }
157
+ /**
158
+ * Compute the maximum column width for each key across all rows.
159
+ *
160
+ * @private
161
+ * @param rows - The data rows.
162
+ * @param keys - The column keys.
163
+ * @returns An array of column widths.
164
+ */
165
+ function computeColumnWidths(rows, keys) {
166
+ return keys.map((key) => {
167
+ const values = rows.map((row) => formatStringValue(row[key]));
168
+ return Math.max(key.length, ...values.map((val) => val.length));
169
+ });
170
+ }
171
+ /**
172
+ * Write a formatted table (header, separator, rows) to a writable stream.
173
+ *
174
+ * @private
175
+ * @param stream - The writable stream.
176
+ * @param rows - The data rows.
177
+ * @param keys - The column keys.
178
+ */
179
+ function writeTableToStream(stream, rows, keys) {
180
+ const widths = computeColumnWidths(rows, keys);
181
+ const content = [
182
+ createTableHeader({
183
+ keys,
184
+ widths
185
+ }),
186
+ widths.map((width) => "-".repeat(width)).join(" "),
187
+ ...rows.map((row) => createTableRow({
188
+ keys,
189
+ row,
190
+ widths
191
+ }))
192
+ ].join("\n");
193
+ stream.write(`${content}\n`);
194
+ }
195
+
196
+ //#endregion
197
+ //#region src/context/prompts.ts
198
+ /**
199
+ * Create the interactive prompt methods for a context.
200
+ *
201
+ * @private
202
+ * @returns A Prompts instance backed by clack.
203
+ */
204
+ function createContextPrompts() {
205
+ const utils = createPromptUtils();
206
+ return {
207
+ async confirm(opts) {
208
+ return unwrapCancelSignal(utils, await utils.confirm(opts));
209
+ },
210
+ async multiselect(opts) {
211
+ return unwrapCancelSignal(utils, await utils.multiselect(opts));
212
+ },
213
+ async password(opts) {
214
+ return unwrapCancelSignal(utils, await utils.password(opts));
215
+ },
216
+ async select(opts) {
217
+ return unwrapCancelSignal(utils, await utils.select(opts));
218
+ },
219
+ async text(opts) {
220
+ return unwrapCancelSignal(utils, await utils.text(opts));
221
+ }
222
+ };
223
+ }
224
+ /**
225
+ * Unwrap a prompt result that may be a cancel symbol.
226
+ *
227
+ * If the user cancelled (Ctrl-C), throws a ContextError. Otherwise returns
228
+ * the typed result value.
229
+ *
230
+ * @private
231
+ * @param utils - The prompt utils instance (for isCancel and cancel).
232
+ * @param result - The raw prompt result (value or cancel symbol).
233
+ * @returns The unwrapped typed value.
234
+ */
235
+ function unwrapCancelSignal(utils, result) {
236
+ if (utils.isCancel(result)) {
237
+ utils.cancel("Operation cancelled.");
238
+ throw createContextError("Prompt cancelled by user", {
239
+ code: "PROMPT_CANCELLED",
240
+ exitCode: DEFAULT_EXIT_CODE
241
+ });
242
+ }
243
+ return result;
244
+ }
245
+
246
+ //#endregion
247
+ //#region src/context/store.ts
248
+ /**
249
+ * Create an in-memory key-value store.
250
+ *
251
+ * @private
252
+ * @returns A Store instance backed by a Map.
253
+ */
254
+ function createMemoryStore() {
255
+ const map = /* @__PURE__ */ new Map();
256
+ return {
257
+ clear() {
258
+ map.clear();
259
+ },
260
+ delete(key) {
261
+ return map.delete(key);
262
+ },
263
+ get(key) {
264
+ return map.get(key);
265
+ },
266
+ has(key) {
267
+ return map.has(key);
268
+ },
269
+ set(key, value) {
270
+ map.set(key, value);
271
+ }
272
+ };
273
+ }
274
+
275
+ //#endregion
276
+ //#region src/context/create-context.ts
277
+ /**
278
+ * Create the {@link Context} object threaded through middleware and command handlers.
279
+ *
280
+ * Assembles logger, spinner, output, store, prompts, and meta from
281
+ * the provided options into a single immutable context. Each sub-system is
282
+ * constructed via its own factory so this function remains a lean orchestrator.
283
+ *
284
+ * @param options - Args, config, and meta for the current invocation.
285
+ * @returns A fully constructed Context.
286
+ */
287
+ function createContext(options) {
288
+ const ctxLogger = options.logger ?? createCliLogger();
289
+ const ctxSpinner = createSpinner();
290
+ const ctxOutput = createContextOutput(options.output ?? process.stdout);
291
+ const ctxStore = createMemoryStore();
292
+ const ctxPrompts = createContextPrompts();
293
+ const ctxMeta = {
294
+ command: options.meta.command,
295
+ name: options.meta.name,
296
+ version: options.meta.version
297
+ };
298
+ return {
299
+ args: options.args,
300
+ config: options.config,
301
+ fail(message, failOptions) {
302
+ throw createContextError(message, failOptions);
303
+ },
304
+ logger: ctxLogger,
305
+ meta: ctxMeta,
306
+ output: ctxOutput,
307
+ prompts: ctxPrompts,
308
+ spinner: ctxSpinner,
309
+ store: ctxStore
310
+ };
311
+ }
312
+
313
+ //#endregion
314
+ //#region src/autoloader.ts
315
+ const VALID_EXTENSIONS = new Set([
316
+ ".ts",
317
+ ".js",
318
+ ".mjs"
319
+ ]);
320
+ const INDEX_NAME = "index";
321
+ /**
322
+ * Scan a directory for command files and produce a CommandMap.
323
+ *
324
+ * @param options - Autoload configuration (directory override, etc.).
325
+ * @returns A promise resolving to a CommandMap built from the directory tree.
326
+ */
327
+ async function autoload(options) {
328
+ const dir = resolveDir(options);
329
+ const entries = await readdir(dir, { withFileTypes: true });
330
+ const fileEntries = entries.filter(isCommandFile);
331
+ const dirEntries = entries.filter(isCommandDir);
332
+ const fileResults = await Promise.all(fileEntries.map(async (entry) => {
333
+ const cmd = await importCommand(join(dir, entry.name));
334
+ if (!cmd) return;
335
+ return [deriveCommandName(entry), cmd];
336
+ }));
337
+ const dirResults = await Promise.all(dirEntries.map((entry) => buildDirCommand(join(dir, entry.name))));
338
+ const validPairs = [...fileResults, ...dirResults].filter((pair) => pair !== void 0);
339
+ return Object.fromEntries(validPairs);
340
+ }
341
+ /**
342
+ * Resolve the target directory from autoload options.
343
+ *
344
+ * @private
345
+ * @param options - Optional autoload configuration.
346
+ * @returns The resolved absolute directory path.
347
+ */
348
+ function resolveDir(options) {
349
+ if (options && isString(options.dir)) return resolve(options.dir);
350
+ return resolve("./commands");
351
+ }
352
+ /**
353
+ * Scan a subdirectory and assemble it as a parent command with subcommands.
354
+ *
355
+ * If the directory contains an `index.ts`/`index.js`, that becomes the parent
356
+ * handler. Otherwise a handler-less group command is created that demands a
357
+ * subcommand.
358
+ *
359
+ * @private
360
+ * @param dir - Absolute path to the subdirectory.
361
+ * @returns A tuple of [name, Command] or undefined if the directory is empty.
362
+ */
363
+ async function buildDirCommand(dir) {
364
+ const name = basename(dir);
365
+ const dirEntries = await readdir(dir, { withFileTypes: true });
366
+ const subCommands = await buildSubCommands(dir, dirEntries);
367
+ const indexFile = findIndexInEntries(dirEntries);
368
+ if (indexFile) {
369
+ const parentCommand = await importCommand(join(dir, indexFile.name));
370
+ if (parentCommand) return [name, withTag({
371
+ ...parentCommand,
372
+ commands: subCommands
373
+ }, "Command")];
374
+ }
375
+ if (Object.keys(subCommands).length === 0) return;
376
+ return [name, withTag({ commands: subCommands }, "Command")];
377
+ }
378
+ /**
379
+ * Build subcommands from already-read directory entries, avoiding a redundant readdir call.
380
+ *
381
+ * @private
382
+ * @param dir - Absolute path to the directory.
383
+ * @param entries - Pre-read directory entries.
384
+ * @returns A CommandMap built from the entries.
385
+ */
386
+ async function buildSubCommands(dir, entries) {
387
+ const fileEntries = entries.filter(isCommandFile);
388
+ const dirEntries = entries.filter(isCommandDir);
389
+ const fileResults = await Promise.all(fileEntries.map(async (entry) => {
390
+ const cmd = await importCommand(join(dir, entry.name));
391
+ if (!cmd) return;
392
+ return [deriveCommandName(entry), cmd];
393
+ }));
394
+ const dirResults = await Promise.all(dirEntries.map((entry) => buildDirCommand(join(dir, entry.name))));
395
+ const validPairs = [...fileResults, ...dirResults].filter((pair) => pair !== void 0);
396
+ return Object.fromEntries(validPairs);
397
+ }
398
+ /**
399
+ * Find the index file (index.ts or index.js) in pre-read directory entries.
400
+ *
401
+ * @private
402
+ * @param entries - Pre-read directory entries.
403
+ * @returns The index file's Dirent or undefined.
404
+ */
405
+ function findIndexInEntries(entries) {
406
+ return entries.find((entry) => entry.isFile() && VALID_EXTENSIONS.has(extname(entry.name)) && basename(entry.name, extname(entry.name)) === INDEX_NAME);
407
+ }
408
+ /**
409
+ * Dynamically import a file and validate that its default export is a Command.
410
+ *
411
+ * @private
412
+ * @param filePath - Absolute path to the file to import.
413
+ * @returns The Command if valid, or undefined.
414
+ */
415
+ async function importCommand(filePath) {
416
+ const mod = await import(filePath);
417
+ if (isCommandExport(mod)) return mod.default;
418
+ }
419
+ /**
420
+ * Check whether a module's default export is a Command object.
421
+ *
422
+ * ES module namespace objects have a null prototype, so isPlainObject
423
+ * rejects them. We only need to verify the namespace is a non-null
424
+ * object with a default export that is a plain Command object.
425
+ *
426
+ * @private
427
+ * @param mod - The imported module to inspect.
428
+ * @returns True when the module has a Command as its default export.
429
+ */
430
+ function isCommandExport(mod) {
431
+ if (typeof mod !== "object" || mod === null) return false;
432
+ const def = mod["default"];
433
+ if (!isPlainObject(def)) return false;
434
+ return hasTag(def, "Command");
435
+ }
436
+ /**
437
+ * Derive a command name from a directory entry by stripping its extension.
438
+ *
439
+ * @private
440
+ * @param entry - The directory entry to derive the name from.
441
+ * @returns The file name without its extension.
442
+ */
443
+ function deriveCommandName(entry) {
444
+ return basename(entry.name, extname(entry.name));
445
+ }
446
+ /**
447
+ * Predicate: entry is a command file (.ts/.js, not index, not _/. prefixed).
448
+ *
449
+ * @private
450
+ * @param entry - The directory entry to check.
451
+ * @returns True when the entry is a valid command file.
452
+ */
453
+ function isCommandFile(entry) {
454
+ if (!entry.isFile()) return false;
455
+ if (entry.name.startsWith("_") || entry.name.startsWith(".")) return false;
456
+ if (!VALID_EXTENSIONS.has(extname(entry.name))) return false;
457
+ return deriveCommandName(entry) !== INDEX_NAME;
458
+ }
459
+ /**
460
+ * Predicate: entry is a scannable command directory (not _/. prefixed).
461
+ *
462
+ * @private
463
+ * @param entry - The directory entry to check.
464
+ * @returns True when the entry is a valid command directory.
465
+ */
466
+ function isCommandDir(entry) {
467
+ if (!entry.isDirectory()) return false;
468
+ return !entry.name.startsWith("_") && !entry.name.startsWith(".");
469
+ }
470
+
471
+ //#endregion
472
+ //#region src/runtime/args/zod.ts
473
+ /**
474
+ * Type guard that checks whether a value is a zod object schema.
475
+ *
476
+ * @param args - The value to check.
477
+ * @returns True when args is a ZodObject.
478
+ */
479
+ function isZodSchema(args) {
480
+ return typeof args === "object" && args !== null && "_def" in args && typeof args._def === "object" && args._def !== null && args._def.type === "object";
481
+ }
482
+ /**
483
+ * Convert an entire zod object schema into a record of yargs options.
484
+ *
485
+ * @param schema - The zod object schema.
486
+ * @returns A record mapping field names to yargs option definitions.
487
+ */
488
+ function zodSchemaToYargsOptions(schema) {
489
+ const shape = schema.shape;
490
+ return Object.fromEntries(Object.entries(shape).map(([key, fieldSchema]) => [key, getZodTypeOption(fieldSchema)]));
491
+ }
492
+ /**
493
+ * Extract a default value from a zod definition, falling back to the provided value.
494
+ *
495
+ * @private
496
+ * @param def - The zod definition to inspect.
497
+ * @param fallback - Value to return when no default is defined.
498
+ * @returns The resolved default value.
499
+ */
500
+ function resolveDefaultValue(def, fallback) {
501
+ if (def.defaultValue !== void 0) return def.defaultValue;
502
+ return fallback;
503
+ }
504
+ /**
505
+ * Unwrap a ZodOptional type, recursing into the inner type.
506
+ *
507
+ * @private
508
+ * @param options - The unwrap options containing def, current type, and default value.
509
+ * @returns Unwrapped type information.
510
+ */
511
+ function unwrapOptional(options) {
512
+ const { def, current, defaultValue } = options;
513
+ if (def.innerType) return unwrapZodTypeRecursive({
514
+ current: def.innerType,
515
+ defaultValue,
516
+ isOptional: true
517
+ });
518
+ return {
519
+ defaultValue,
520
+ inner: current,
521
+ isOptional: true
522
+ };
523
+ }
524
+ /**
525
+ * Unwrap a ZodDefault type, resolving its default value and recursing.
526
+ *
527
+ * @private
528
+ * @param options - The unwrap options containing def, current type, and default value.
529
+ * @returns Unwrapped type information with the resolved default.
530
+ */
531
+ function unwrapDefault(options) {
532
+ const { def, current, defaultValue } = options;
533
+ const newDefault = resolveDefaultValue(def, defaultValue);
534
+ if (def.innerType) return unwrapZodTypeRecursive({
535
+ current: def.innerType,
536
+ defaultValue: newDefault,
537
+ isOptional: true
538
+ });
539
+ return {
540
+ defaultValue: newDefault,
541
+ inner: current,
542
+ isOptional: true
543
+ };
544
+ }
545
+ /**
546
+ * Recursively unwrap optional and default wrappers from a zod type.
547
+ *
548
+ * @private
549
+ * @param options - The recursive unwrap options containing current type, optionality flag, and default value.
550
+ * @returns The fully unwrapped type information.
551
+ */
552
+ function unwrapZodTypeRecursive(options) {
553
+ const { current, isOptional, defaultValue } = options;
554
+ const def = current._def;
555
+ if (def.type === "optional") return unwrapOptional({
556
+ current,
557
+ def,
558
+ defaultValue
559
+ });
560
+ if (def.type === "default") return unwrapDefault({
561
+ current,
562
+ def,
563
+ defaultValue
564
+ });
565
+ return {
566
+ defaultValue,
567
+ inner: current,
568
+ isOptional
569
+ };
570
+ }
571
+ /**
572
+ * Unwrap a zod schema to extract its base type, optionality, and default value.
573
+ *
574
+ * @private
575
+ * @param schema - The zod type to unwrap.
576
+ * @returns The unwrapped type information.
577
+ */
578
+ function unwrapZodType(schema) {
579
+ return unwrapZodTypeRecursive({
580
+ current: schema,
581
+ defaultValue: void 0,
582
+ isOptional: false
583
+ });
584
+ }
585
+ /**
586
+ * Map a zod type name to a yargs option type string.
587
+ *
588
+ * @private
589
+ * @param typeName - The zod type name (e.g. 'string', 'number').
590
+ * @returns The corresponding yargs type.
591
+ */
592
+ function resolveZodYargsType(typeName) {
593
+ return match$1(typeName).with("string", () => "string").with("number", () => "number").with("boolean", () => "boolean").with("array", () => "array").otherwise(() => "string");
594
+ }
595
+ /**
596
+ * Build a base yargs option from a zod schema's description and default.
597
+ *
598
+ * @private
599
+ * @param inner - The unwrapped zod schema instance.
600
+ * @param defaultValue - The resolved default value.
601
+ * @returns A partial yargs option object.
602
+ */
603
+ function buildBaseOption(inner, defaultValue) {
604
+ const base = {};
605
+ const { description } = inner;
606
+ if (description) base.describe = description;
607
+ if (defaultValue !== void 0) base.default = defaultValue;
608
+ return base;
609
+ }
610
+ /**
611
+ * Convert a single zod field schema into a complete yargs option definition.
612
+ *
613
+ * @private
614
+ * @param schema - A single zod field type.
615
+ * @returns A complete yargs option object.
616
+ */
617
+ function getZodTypeOption(schema) {
618
+ const { inner, isOptional, defaultValue } = unwrapZodType(schema);
619
+ const innerDef = inner._def;
620
+ const base = {
621
+ ...buildBaseOption(inner, defaultValue),
622
+ type: resolveZodYargsType(innerDef.type)
623
+ };
624
+ if (!isOptional) return {
625
+ ...base,
626
+ demandOption: true
627
+ };
628
+ return base;
629
+ }
630
+
631
+ //#endregion
632
+ //#region src/runtime/args/parser.ts
633
+ /**
634
+ * Create an args parser that cleans and validates raw parsed arguments.
635
+ *
636
+ * Captures the argument definition in a closure and returns an ArgsParser
637
+ * whose `parse` method strips yargs-internal keys and validates against
638
+ * a zod schema when one is defined.
639
+ *
640
+ * @param argsDef - The argument definition from the command.
641
+ * @returns An ArgsParser with a parse method.
642
+ */
643
+ function createArgsParser(argsDef) {
644
+ return { parse(rawArgs) {
645
+ return validateArgs(argsDef, cleanParsedArgs(rawArgs));
646
+ } };
647
+ }
648
+ /**
649
+ * Strip yargs-internal keys (`_`, `$0`) and camelCase-duplicated hyphenated keys
650
+ * from a parsed argv record, returning only user-defined arguments.
651
+ *
652
+ * @private
653
+ * @param argv - Raw parsed argv from yargs.
654
+ * @returns A cleaned record containing only user-defined arguments.
655
+ */
656
+ function cleanParsedArgs(argv) {
657
+ return Object.fromEntries(Object.entries(argv).filter(([key]) => key !== "_" && key !== "$0" && !key.includes("-")));
658
+ }
659
+ /**
660
+ * Validate parsed arguments against a zod schema when one is defined.
661
+ *
662
+ * If the command uses yargs-native args (no zod schema), the parsed args are
663
+ * returned as-is. When a zod schema is present, validation is performed and
664
+ * a Result error is returned on failure.
665
+ *
666
+ * @private
667
+ * @param argsDef - The argument definition from the command.
668
+ * @param parsedArgs - The cleaned parsed arguments.
669
+ * @returns A Result containing validated arguments (zod-parsed when applicable).
670
+ */
671
+ function validateArgs(argsDef, parsedArgs) {
672
+ if (!argsDef || !isZodSchema(argsDef)) return ok(parsedArgs);
673
+ const result = argsDef.safeParse(parsedArgs);
674
+ if (!result.success) return err(/* @__PURE__ */ new Error(`Invalid arguments:\n ${formatZodIssues(result.error.issues).message}`));
675
+ return ok(result.data);
676
+ }
677
+
678
+ //#endregion
679
+ //#region src/runtime/args/register.ts
680
+ /**
681
+ * Register argument definitions on a yargs builder.
682
+ *
683
+ * Accepts either a zod object schema or a record of yargs-native arg definitions
684
+ * and wires them as yargs options on the given builder instance.
685
+ *
686
+ * @param builder - The yargs Argv instance to register options on.
687
+ * @param args - Argument definitions from a Command.
688
+ */
689
+ function registerCommandArgs(builder, args) {
690
+ if (!args) return;
691
+ if (isZodSchema(args)) {
692
+ const options = zodSchemaToYargsOptions(args);
693
+ for (const [key, opt] of Object.entries(options)) builder.option(key, opt);
694
+ } else {
695
+ const argsDef = args;
696
+ for (const [key, def] of Object.entries(argsDef)) builder.option(key, yargsArgDefToOption(def));
697
+ }
698
+ }
699
+ /**
700
+ * Convert a yargs-native arg definition into a yargs option object.
701
+ *
702
+ * @private
703
+ * @param def - The yargs arg definition.
704
+ * @returns A yargs option object.
705
+ */
706
+ function yargsArgDefToOption(def) {
707
+ return {
708
+ alias: def.alias,
709
+ choices: def.choices,
710
+ default: def.default,
711
+ demandOption: def.required ?? false,
712
+ describe: def.description,
713
+ type: def.type
714
+ };
715
+ }
716
+
717
+ //#endregion
718
+ //#region src/runtime/register.ts
719
+ /**
720
+ * Type guard that checks whether a value is a Command object.
721
+ *
722
+ * @param value - The value to test.
723
+ * @returns True when the value has `[TAG] === 'Command'`.
724
+ */
725
+ function isCommand(value) {
726
+ return hasTag(value, "Command");
727
+ }
728
+ /**
729
+ * Register all commands from a CommandMap on a yargs instance.
730
+ *
731
+ * Iterates over the command map, filters for valid Command objects,
732
+ * and recursively registers each command (including subcommands) on
733
+ * the provided yargs Argv instance.
734
+ *
735
+ * @param options - Registration options including the command map, yargs instance, and resolution ref.
736
+ */
737
+ function registerCommands(options) {
738
+ const { instance, commands, resolved, parentPath } = options;
739
+ const commandEntries = Object.entries(commands).filter(([, entry]) => isCommand(entry));
740
+ for (const [name, entry] of commandEntries) registerResolvedCommand({
741
+ builder: instance,
742
+ cmd: entry,
743
+ instance,
744
+ name,
745
+ parentPath,
746
+ resolved
747
+ });
748
+ }
749
+ /**
750
+ * Register a single resolved command (and its subcommands) with yargs.
751
+ *
752
+ * Sets up the yargs command handler, wires argument definitions, and
753
+ * recursively registers any nested subcommands. On match, stores the
754
+ * resolved handler and command path in the shared ref.
755
+ *
756
+ * @private
757
+ * @param options - Command registration context.
758
+ */
759
+ function registerResolvedCommand(options) {
760
+ const { instance, name, cmd, resolved, parentPath } = options;
761
+ const description = cmd.description ?? "";
762
+ instance.command(name, description, (builder) => {
763
+ registerCommandArgs(builder, cmd.args);
764
+ if (cmd.commands) {
765
+ const subCommands = Object.entries(cmd.commands).filter(([, entry]) => isCommand(entry));
766
+ for (const [subName, subEntry] of subCommands) registerResolvedCommand({
767
+ builder,
768
+ cmd: subEntry,
769
+ instance: builder,
770
+ name: subName,
771
+ parentPath: [...parentPath, name],
772
+ resolved
773
+ });
774
+ if (cmd.handler) builder.demandCommand(0);
775
+ else builder.demandCommand(1, "You must specify a subcommand.");
776
+ }
777
+ return builder;
778
+ }, () => {
779
+ resolved.ref = {
780
+ args: cmd.args,
781
+ commandPath: [...parentPath, name],
782
+ handler: cmd.handler,
783
+ middleware: cmd.middleware ?? []
784
+ };
785
+ });
786
+ }
787
+
788
+ //#endregion
789
+ //#region src/runtime/runner.ts
790
+ /**
791
+ * Create a runner that executes root and command middleware chains.
792
+ *
793
+ * Root middleware wraps the command middleware chain, which in turn wraps
794
+ * the command handler — producing a nested onion lifecycle:
795
+ *
796
+ * ```
797
+ * root middleware start →
798
+ * command middleware start →
799
+ * handler
800
+ * command middleware end
801
+ * root middleware end
802
+ * ```
803
+ *
804
+ * @param rootMiddleware - Root-level middleware from `cli({ middleware })`.
805
+ * @returns A Runner with an execute method.
806
+ */
807
+ function createRunner(rootMiddleware) {
808
+ return { async execute({ ctx, handler, middleware }) {
809
+ const commandHandler = async (innerCtx) => {
810
+ await runMiddlewareChain(middleware, innerCtx, handler);
811
+ };
812
+ await runMiddlewareChain(rootMiddleware, ctx, commandHandler);
813
+ } };
814
+ }
815
+ /**
816
+ * Execute a middleware chain followed by a final handler.
817
+ *
818
+ * Runs each middleware in order, passing `ctx` and a `next` callback.
819
+ * When all middleware have called `next()`, the final handler is invoked.
820
+ * A middleware can short-circuit by not calling `next()`.
821
+ *
822
+ * @private
823
+ * @param middlewares - Ordered array of middleware to execute.
824
+ * @param ctx - The context object threaded through middleware and handler.
825
+ * @param finalHandler - The command handler to invoke after all middleware.
826
+ */
827
+ async function runMiddlewareChain(middlewares, ctx, finalHandler) {
828
+ async function executeChain(index) {
829
+ if (index >= middlewares.length) {
830
+ await finalHandler(ctx);
831
+ return;
832
+ }
833
+ const mw = middlewares[index];
834
+ if (mw) await mw.handler(ctx, () => executeChain(index + 1));
835
+ }
836
+ await executeChain(0);
837
+ }
838
+
839
+ //#endregion
840
+ //#region src/runtime/runtime.ts
841
+ /**
842
+ * Create a runtime that orchestrates config loading and middleware execution.
843
+ *
844
+ * Loads config up front, then captures it in a closure alongside a runner.
845
+ * The returned `runtime.execute` method handles arg parsing, context creation,
846
+ * and middleware chain execution for each command invocation.
847
+ *
848
+ * @param options - Runtime configuration including name, version, config, and middleware.
849
+ * @returns An AsyncResult containing the runtime or an error.
850
+ */
851
+ async function createRuntime(options) {
852
+ const config = await resolveConfig(options.config, options.name);
853
+ const runner = createRunner(options.middleware ?? []);
854
+ return ok({ async execute(command) {
855
+ const [argsError, validatedArgs] = createArgsParser(command.args).parse(command.rawArgs);
856
+ if (argsError) return err(argsError);
857
+ const ctx = createContext({
858
+ args: validatedArgs,
859
+ config,
860
+ meta: {
861
+ command: command.commandPath,
862
+ name: options.name,
863
+ version: options.version
864
+ }
865
+ });
866
+ const finalHandler = command.handler ?? (async () => {});
867
+ const [execError] = await attemptAsync(() => runner.execute({
868
+ ctx,
869
+ handler: finalHandler,
870
+ middleware: command.middleware
871
+ }));
872
+ if (execError) return err(execError);
873
+ return ok();
874
+ } });
875
+ }
876
+ /**
877
+ * Load and validate a config file via the config client.
878
+ *
879
+ * Returns the validated config record or an empty object when no config
880
+ * options are provided or when loading fails.
881
+ *
882
+ * @private
883
+ * @param configOptions - Config loading options with schema and optional name override.
884
+ * @param defaultName - Fallback config file name derived from the CLI name.
885
+ * @returns The loaded config record or an empty object.
886
+ */
887
+ async function resolveConfig(configOptions, defaultName) {
888
+ if (!configOptions || !configOptions.schema) return {};
889
+ const [configError, configResult] = await createConfigClient({
890
+ name: configOptions.name ?? defaultName,
891
+ schema: configOptions.schema
892
+ }).load();
893
+ if (configError || !configResult) return {};
894
+ return configResult.config;
895
+ }
896
+
897
+ //#endregion
898
+ //#region src/cli.ts
899
+ const ARGV_SLICE_START = 2;
900
+ /**
901
+ * Bootstrap and run the CLI application.
902
+ *
903
+ * Parses argv, resolves the matched command, loads config, runs the
904
+ * middleware chain, and invokes the command handler.
905
+ *
906
+ * @param options - CLI configuration including name, version, commands, and middleware.
907
+ */
908
+ async function cli(options) {
909
+ const logger = createCliLogger();
910
+ const [uncaughtError, result] = await attemptAsync(async () => {
911
+ const program = yargs(process.argv.slice(ARGV_SLICE_START)).scriptName(options.name).version(options.version).strict().help().option("cwd", {
912
+ describe: "Set the working directory",
913
+ global: true,
914
+ type: "string"
915
+ });
916
+ if (options.description) program.usage(options.description);
917
+ const resolved = { ref: void 0 };
918
+ const commands = await resolveCommands(options.commands);
919
+ if (commands) {
920
+ registerCommands({
921
+ commands,
922
+ instance: program,
923
+ parentPath: [],
924
+ resolved
925
+ });
926
+ program.demandCommand(1, "You must specify a command.");
927
+ }
928
+ const argv = await program.parseAsync();
929
+ applyCwd(argv);
930
+ if (!resolved.ref) return;
931
+ const [runtimeError, runtime] = await createRuntime({
932
+ config: options.config,
933
+ middleware: options.middleware,
934
+ name: options.name,
935
+ version: options.version
936
+ });
937
+ if (runtimeError) return runtimeError;
938
+ const [executeError] = await runtime.execute({
939
+ args: resolved.ref.args,
940
+ commandPath: resolved.ref.commandPath,
941
+ handler: resolved.ref.handler,
942
+ middleware: resolved.ref.middleware,
943
+ rawArgs: argv
944
+ });
945
+ return executeError;
946
+ });
947
+ if (uncaughtError) {
948
+ exitOnError(uncaughtError, logger);
949
+ return;
950
+ }
951
+ if (result) exitOnError(result, logger);
952
+ }
953
+ /**
954
+ * Resolve the commands option to a CommandMap.
955
+ *
956
+ * Accepts a directory string (triggers autoload), a static CommandMap,
957
+ * a Promise<CommandMap> (from autoload() called at the call site),
958
+ * or undefined (loads `kidd.config.ts` and autoloads from its `commands` field,
959
+ * falling back to `'./commands'`).
960
+ *
961
+ * @private
962
+ * @param commands - The commands option from CliOptions.
963
+ * @returns A CommandMap or undefined.
964
+ */
965
+ async function resolveCommands(commands) {
966
+ if (isString(commands)) return autoload({ dir: commands });
967
+ if (commands instanceof Promise) return commands;
968
+ if (isPlainObject(commands)) return commands;
969
+ return resolveCommandsFromConfig();
970
+ }
971
+ /**
972
+ * Load `kidd.config.ts` and autoload commands from its `commands` field.
973
+ *
974
+ * Falls back to `'./commands'` when the config file is missing, fails to load,
975
+ * or does not specify a `commands` field.
976
+ *
977
+ * @private
978
+ * @returns A CommandMap autoloaded from the configured commands directory.
979
+ */
980
+ async function resolveCommandsFromConfig() {
981
+ const DEFAULT_COMMANDS_DIR = "./commands";
982
+ const [configError, configResult] = await loadConfig();
983
+ if (configError || !configResult) return autoload({ dir: DEFAULT_COMMANDS_DIR });
984
+ return autoload({ dir: configResult.config.commands ?? DEFAULT_COMMANDS_DIR });
985
+ }
986
+ /**
987
+ * Change the process working directory when `--cwd` is provided.
988
+ *
989
+ * Resolves the value to an absolute path and calls `process.chdir()` so
990
+ * that all downstream `process.cwd()` calls reflect the override.
991
+ *
992
+ * @private
993
+ * @param argv - The parsed argv record from yargs.
994
+ */
995
+ function applyCwd(argv) {
996
+ if (isString(argv.cwd)) process.chdir(resolve(argv.cwd));
997
+ }
998
+ /**
999
+ * Handle a CLI error by logging the message and exiting with the appropriate code.
1000
+ *
1001
+ * ContextErrors carry a custom exit code; all other errors exit with code 1.
1002
+ *
1003
+ * @private
1004
+ * @param error - The caught error value.
1005
+ * @param logger - Logger with an error method for output.
1006
+ */
1007
+ function exitOnError(error, logger) {
1008
+ if (isContextError(error)) {
1009
+ logger.error(error.message);
1010
+ process.exit(error.exitCode);
1011
+ } else if (error instanceof Error) {
1012
+ logger.error(error.message);
1013
+ process.exit(DEFAULT_EXIT_CODE);
1014
+ } else {
1015
+ logger.error(String(error));
1016
+ process.exit(DEFAULT_EXIT_CODE);
1017
+ }
1018
+ }
1019
+
1020
+ //#endregion
1021
+ //#region src/command.ts
1022
+ /**
1023
+ * Define a CLI command with typed args, config, and handler.
1024
+ *
1025
+ * @param def - Command definition including description, args schema, and handler.
1026
+ * @returns A resolved Command object for registration in the command map.
1027
+ */
1028
+ function command(def) {
1029
+ return withTag({ ...def }, "Command");
1030
+ }
1031
+
1032
+ //#endregion
1033
+ export { autoload, cli, command, decorateContext, defineConfig, middleware };
1034
+ //# sourceMappingURL=index.js.map