@kidd-cli/core 0.3.0 → 0.5.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 (48) hide show
  1. package/README.md +23 -8
  2. package/dist/{config-D8e5qxLp.js → config-BiEi8RG2.js} +2 -2
  3. package/dist/{config-D8e5qxLp.js.map → config-BiEi8RG2.js.map} +1 -1
  4. package/dist/{create-store-OHdkm_Yt.js → create-store-CGeHrTcl.js} +2 -2
  5. package/dist/{create-store-OHdkm_Yt.js.map → create-store-CGeHrTcl.js.map} +1 -1
  6. package/dist/index.d.ts +8 -3
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +265 -95
  9. package/dist/index.js.map +1 -1
  10. package/dist/lib/config.js +2 -2
  11. package/dist/lib/format.d.ts +73 -0
  12. package/dist/lib/format.d.ts.map +1 -0
  13. package/dist/lib/format.js +20 -0
  14. package/dist/lib/format.js.map +1 -0
  15. package/dist/lib/logger.d.ts +1 -1
  16. package/dist/lib/logger.js +10 -0
  17. package/dist/lib/logger.js.map +1 -1
  18. package/dist/lib/project.d.ts +1 -1
  19. package/dist/lib/project.js +1 -1
  20. package/dist/lib/store.d.ts +1 -1
  21. package/dist/lib/store.js +2 -2
  22. package/dist/{logger-9j49T5da.d.ts → logger-Bm-LRSeQ.d.ts} +17 -1
  23. package/dist/logger-Bm-LRSeQ.d.ts.map +1 -0
  24. package/dist/middleware/auth.d.ts +15 -3
  25. package/dist/middleware/auth.d.ts.map +1 -1
  26. package/dist/middleware/auth.js +48 -9
  27. package/dist/middleware/auth.js.map +1 -1
  28. package/dist/middleware/http.d.ts +1 -1
  29. package/dist/middleware/http.js +1 -1
  30. package/dist/middleware/icons.d.ts +119 -0
  31. package/dist/middleware/icons.d.ts.map +1 -0
  32. package/dist/middleware/icons.js +824 -0
  33. package/dist/middleware/icons.js.map +1 -0
  34. package/dist/{middleware-BWnPSRWR.js → middleware-BewRXb2G.js} +1 -1
  35. package/dist/{middleware-BWnPSRWR.js.map → middleware-BewRXb2G.js.map} +1 -1
  36. package/dist/{project-D0g84bZY.js → project-CoWHMVc8.js} +1 -1
  37. package/dist/{project-D0g84bZY.js.map → project-CoWHMVc8.js.map} +1 -1
  38. package/dist/tally-ioa20iGw.js +220 -0
  39. package/dist/tally-ioa20iGw.js.map +1 -0
  40. package/dist/{types-D-BxshYM.d.ts → types-Boe_1EjY.d.ts} +1 -1
  41. package/dist/{types-D-BxshYM.d.ts.map → types-Boe_1EjY.d.ts.map} +1 -1
  42. package/dist/types-Cp8_uIil.d.ts +160 -0
  43. package/dist/types-Cp8_uIil.d.ts.map +1 -0
  44. package/dist/{types-CTvrsrnD.d.ts → types-s-yUj9Zj.d.ts} +104 -44
  45. package/dist/types-s-yUj9Zj.d.ts.map +1 -0
  46. package/package.json +12 -3
  47. package/dist/logger-9j49T5da.d.ts.map +0 -1
  48. package/dist/types-CTvrsrnD.d.ts.map +0 -1
package/dist/index.js CHANGED
@@ -1,17 +1,20 @@
1
+ import "./tally-ioa20iGw.js";
1
2
  import { createCliLogger } from "./lib/logger.js";
2
- import { n as decorateContext, t as middleware } from "./middleware-BWnPSRWR.js";
3
- import "./project-D0g84bZY.js";
4
- import { t as createConfigClient } from "./config-D8e5qxLp.js";
3
+ import { n as decorateContext, t as middleware } from "./middleware-BewRXb2G.js";
4
+ import "./project-CoWHMVc8.js";
5
+ import { t as createConfigClient } from "./config-BiEi8RG2.js";
5
6
  import { basename, extname, join, resolve } from "node:path";
6
7
  import { loadConfig } from "@kidd-cli/config/loader";
7
8
  import { P, attemptAsync, err, isPlainObject, isString, match, ok } from "@kidd-cli/utils/fp";
8
9
  import yargs from "yargs";
10
+ import { z } from "zod";
9
11
  import * as clack from "@clack/prompts";
12
+ import pc from "picocolors";
13
+ import { match as match$1 } from "ts-pattern";
10
14
  import { TAG, hasTag, withTag } from "@kidd-cli/utils/tag";
11
15
  import { jsonStringify } from "@kidd-cli/utils/json";
12
16
  import { readdir } from "node:fs/promises";
13
17
  import { formatZodIssues } from "@kidd-cli/utils/validate";
14
- import { match as match$1 } from "ts-pattern";
15
18
  import { defineConfig } from "@kidd-cli/config";
16
19
  //#region src/context/error.ts
17
20
  /**
@@ -75,40 +78,26 @@ function createContextErrorData(message, options) {
75
78
  }, "ContextError");
76
79
  }
77
80
  //#endregion
78
- //#region src/context/output.ts
81
+ //#region src/context/format.ts
79
82
  /**
80
- * Create the structured output methods for a context.
83
+ * Create the pure string formatter methods for a context.
81
84
  *
82
85
  * @private
83
- * @param stream - The writable stream to write output to.
84
- * @returns An Output instance backed by the given stream.
86
+ * @returns A Format instance with json and table formatters.
85
87
  */
86
- function createContextOutput(stream) {
87
- return {
88
- markdown(content) {
89
- stream.write(`${content}\n`);
90
- },
91
- raw(content) {
92
- stream.write(content);
88
+ function createContextFormat() {
89
+ return Object.freeze({
90
+ json(data) {
91
+ const [, json] = jsonStringify(data, { pretty: true });
92
+ return `${json}\n`;
93
93
  },
94
- table(rows, options) {
95
- if (options && options.json) {
96
- const [, json] = jsonStringify(rows, { pretty: true });
97
- stream.write(`${json}\n`);
98
- return;
99
- }
100
- if (rows.length === 0) return;
94
+ table(rows) {
95
+ if (rows.length === 0) return "";
101
96
  const [firstRow] = rows;
102
- if (!firstRow) return;
103
- writeTableToStream(stream, rows, Object.keys(firstRow));
104
- },
105
- write(data, options) {
106
- if (options && options.json || typeof data === "object" && data !== null) {
107
- const [, json] = jsonStringify(data, { pretty: true });
108
- stream.write(`${json}\n`);
109
- } else stream.write(`${String(data)}\n`);
97
+ if (!firstRow) return "";
98
+ return formatTable(rows, Object.keys(firstRow));
110
99
  }
111
- };
100
+ });
112
101
  }
113
102
  /**
114
103
  * Format an unknown value as a string for table cell display.
@@ -167,16 +156,16 @@ function computeColumnWidths(rows, keys) {
167
156
  });
168
157
  }
169
158
  /**
170
- * Write a formatted table (header, separator, rows) to a writable stream.
159
+ * Format a table (header, separator, rows) as a string.
171
160
  *
172
161
  * @private
173
- * @param stream - The writable stream.
174
162
  * @param rows - The data rows.
175
163
  * @param keys - The column keys.
164
+ * @returns The formatted table string.
176
165
  */
177
- function writeTableToStream(stream, rows, keys) {
166
+ function formatTable(rows, keys) {
178
167
  const widths = computeColumnWidths(rows, keys);
179
- const content = [
168
+ return `${[
180
169
  createTableHeader({
181
170
  keys,
182
171
  widths
@@ -187,8 +176,7 @@ function writeTableToStream(stream, rows, keys) {
187
176
  row,
188
177
  widths
189
178
  }))
190
- ].join("\n");
191
- stream.write(`${content}\n`);
179
+ ].join("\n")}\n`;
192
180
  }
193
181
  //#endregion
194
182
  //#region src/context/prompts.ts
@@ -269,7 +257,7 @@ function createMemoryStore() {
269
257
  /**
270
258
  * Create the {@link Context} object threaded through middleware and command handlers.
271
259
  *
272
- * Assembles logger, spinner, output, store, prompts, and meta from
260
+ * Assembles logger, spinner, format, store, prompts, and meta from
273
261
  * the provided options into a single immutable context. Each sub-system is
274
262
  * constructed via its own factory so this function remains a lean orchestrator.
275
263
  *
@@ -279,7 +267,7 @@ function createMemoryStore() {
279
267
  function createContext(options) {
280
268
  const ctxLogger = options.logger ?? createCliLogger();
281
269
  const ctxSpinner = clack.spinner();
282
- const ctxOutput = createContextOutput(options.output ?? process.stdout);
270
+ const ctxFormat = createContextFormat();
283
271
  const ctxStore = createMemoryStore();
284
272
  const ctxPrompts = createContextPrompts();
285
273
  const ctxMeta = {
@@ -289,13 +277,14 @@ function createContext(options) {
289
277
  };
290
278
  return {
291
279
  args: options.args,
280
+ colors: Object.freeze({ ...pc }),
292
281
  config: options.config,
293
282
  fail(message, failOptions) {
294
283
  throw createContextError(message, failOptions);
295
284
  },
285
+ format: ctxFormat,
296
286
  logger: ctxLogger,
297
287
  meta: ctxMeta,
298
- output: ctxOutput,
299
288
  prompts: ctxPrompts,
300
289
  spinner: ctxSpinner,
301
290
  store: ctxStore
@@ -317,17 +306,7 @@ const INDEX_NAME = "index";
317
306
  */
318
307
  async function autoload(options) {
319
308
  const dir = resolveDir(options);
320
- const entries = await readdir(dir, { withFileTypes: true });
321
- const fileEntries = entries.filter(isCommandFile);
322
- const dirEntries = entries.filter(isCommandDir);
323
- const fileResults = await Promise.all(fileEntries.map(async (entry) => {
324
- const cmd = await importCommand(join(dir, entry.name));
325
- if (!cmd) return;
326
- return [deriveCommandName(entry), cmd];
327
- }));
328
- const dirResults = await Promise.all(dirEntries.map((entry) => buildDirCommand(join(dir, entry.name))));
329
- const validPairs = [...fileResults, ...dirResults].filter((pair) => pair !== void 0);
330
- return Object.fromEntries(validPairs);
309
+ return buildCommandMapFromEntries(dir, await readdir(dir, { withFileTypes: true }));
331
310
  }
332
311
  /**
333
312
  * Resolve the target directory from autoload options.
@@ -354,7 +333,7 @@ function resolveDir(options) {
354
333
  async function buildDirCommand(dir) {
355
334
  const name = basename(dir);
356
335
  const dirEntries = await readdir(dir, { withFileTypes: true });
357
- const subCommands = await buildSubCommands(dir, dirEntries);
336
+ const subCommands = await buildCommandMapFromEntries(dir, dirEntries);
358
337
  const indexFile = findIndexInEntries(dirEntries);
359
338
  if (indexFile) {
360
339
  const parentCommand = await importCommand(join(dir, indexFile.name));
@@ -367,14 +346,17 @@ async function buildDirCommand(dir) {
367
346
  return [name, withTag({ commands: subCommands }, "Command")];
368
347
  }
369
348
  /**
370
- * Build subcommands from already-read directory entries, avoiding a redundant readdir call.
349
+ * Build a CommandMap from pre-read directory entries.
350
+ *
351
+ * Shared by both `autoload` and `buildDirCommand` to avoid duplicating
352
+ * the file/dir fan-out and result-filtering logic.
371
353
  *
372
354
  * @private
373
- * @param dir - Absolute path to the directory.
374
- * @param entries - Pre-read directory entries.
355
+ * @param dir - Absolute path to the directory the entries belong to.
356
+ * @param entries - Pre-read directory entries for that directory.
375
357
  * @returns A CommandMap built from the entries.
376
358
  */
377
- async function buildSubCommands(dir, entries) {
359
+ async function buildCommandMapFromEntries(dir, entries) {
378
360
  const fileEntries = entries.filter(isCommandFile);
379
361
  const dirEntries = entries.filter(isCommandDir);
380
362
  const fileResults = await Promise.all(fileEntries.map(async (entry) => {
@@ -460,6 +442,46 @@ function isCommandDir(entry) {
460
442
  return !entry.name.startsWith("_") && !entry.name.startsWith(".");
461
443
  }
462
444
  //#endregion
445
+ //#region src/command.ts
446
+ /**
447
+ * Check whether a value is a structured {@link CommandsConfig} object.
448
+ *
449
+ * Discriminates from a plain `CommandMap` by checking for the `order` (array)
450
+ * or `path` (string) keys — neither can appear on a valid `CommandMap` whose
451
+ * values are tagged `Command` objects.
452
+ *
453
+ * @param value - The value to test.
454
+ * @returns `true` when `value` is a `CommandsConfig`.
455
+ */
456
+ function isCommandsConfig(value) {
457
+ if (typeof value !== "object" || value === null || value instanceof Promise) return false;
458
+ return "order" in value && Array.isArray(value.order) || "path" in value && typeof value.path === "string";
459
+ }
460
+ /**
461
+ * Define a CLI command with typed args, config, and handler.
462
+ *
463
+ * The `const TMiddleware` generic preserves the middleware tuple as a literal type,
464
+ * enabling TypeScript to extract and intersect `Variables` from each middleware
465
+ * element onto the handler's `ctx` type.
466
+ *
467
+ * When `def.commands` is a structured {@link CommandsConfig}, the factory
468
+ * normalizes it into flat `commands` and `order` fields on the output
469
+ * `Command` object so downstream consumers never need to handle the grouped form.
470
+ *
471
+ * @param def - Command definition including description, args schema, middleware, and handler.
472
+ * @returns A resolved Command object for registration in the command map.
473
+ */
474
+ function command(def) {
475
+ return match(def.commands).when(isCommandsConfig, (cfg) => {
476
+ const { order, commands: innerCommands } = cfg;
477
+ return withTag({
478
+ ...def,
479
+ commands: innerCommands,
480
+ order
481
+ }, "Command");
482
+ }).otherwise(() => withTag({ ...def }, "Command"));
483
+ }
484
+ //#endregion
463
485
  //#region src/runtime/args/zod.ts
464
486
  /**
465
487
  * Type guard that checks whether a value is a zod object schema.
@@ -700,6 +722,44 @@ function yargsArgDefToOption(def) {
700
722
  };
701
723
  }
702
724
  //#endregion
725
+ //#region src/runtime/sort-commands.ts
726
+ /**
727
+ * Validate that every name in the order array exists in the provided command names.
728
+ *
729
+ * @param params - The order array and available command names to validate against.
730
+ * @returns A Result tuple — `[null, void]` on success or `[Error, null]` on failure.
731
+ */
732
+ function validateCommandOrder(params) {
733
+ const { order, commandNames } = params;
734
+ const seen = /* @__PURE__ */ new Set();
735
+ const duplicates = order.filter((name) => {
736
+ if (seen.has(name)) return true;
737
+ seen.add(name);
738
+ return false;
739
+ });
740
+ if (duplicates.length > 0) return err(`Invalid command order: duplicate command(s) ${[...new Set(duplicates)].map((n) => `"${n}"`).join(", ")}`);
741
+ const nameSet = new Set(commandNames);
742
+ const invalid = order.filter((name) => !nameSet.has(name));
743
+ if (invalid.length > 0) return err(`Invalid command order: unknown command(s) ${invalid.map((n) => `"${n}"`).join(", ")}`);
744
+ return ok();
745
+ }
746
+ /**
747
+ * Sort command entries with ordered names first (in specified order),
748
+ * remaining names alphabetically.
749
+ *
750
+ * @param params - The command entries and optional order array.
751
+ * @returns Sorted array of `[name, Command]` entries.
752
+ */
753
+ function sortCommandEntries(params) {
754
+ const { entries, order } = params;
755
+ if (!order || order.length === 0) return [...entries].toSorted(([a], [b]) => a.localeCompare(b));
756
+ const entryMap = new Map(entries);
757
+ const ordered = order.filter((name) => entryMap.has(name)).map((name) => [name, entryMap.get(name)]);
758
+ const orderedSet = new Set(order);
759
+ const remaining = entries.filter(([name]) => !orderedSet.has(name)).toSorted(([a], [b]) => a.localeCompare(b));
760
+ return [...ordered, ...remaining];
761
+ }
762
+ //#endregion
703
763
  //#region src/runtime/register.ts
704
764
  /**
705
765
  * Type guard that checks whether a value is a Command object.
@@ -714,22 +774,36 @@ function isCommand(value) {
714
774
  * Register all commands from a CommandMap on a yargs instance.
715
775
  *
716
776
  * Iterates over the command map, filters for valid Command objects,
717
- * and recursively registers each command (including subcommands) on
718
- * the provided yargs Argv instance.
777
+ * validates the order array, sorts entries, and recursively registers
778
+ * each command (including subcommands) on the provided yargs Argv instance.
719
779
  *
720
780
  * @param options - Registration options including the command map, yargs instance, and resolution ref.
721
781
  */
722
782
  function registerCommands(options) {
723
- const { instance, commands, resolved, parentPath } = options;
783
+ const { instance, commands, resolved, parentPath, order, errorRef } = options;
724
784
  const commandEntries = Object.entries(commands).filter((pair) => isCommand(pair[1]));
725
- for (const [name, entry] of commandEntries) registerResolvedCommand({
785
+ if (order && order.length > 0) {
786
+ const [validationError] = validateCommandOrder({
787
+ commandNames: commandEntries.map(([name]) => name),
788
+ order
789
+ });
790
+ if (validationError && errorRef) {
791
+ errorRef.error = validationError;
792
+ return;
793
+ }
794
+ }
795
+ sortCommandEntries({
796
+ entries: commandEntries,
797
+ order
798
+ }).map(([name, entry]) => registerResolvedCommand({
726
799
  builder: instance,
727
800
  cmd: entry,
801
+ errorRef,
728
802
  instance,
729
803
  name,
730
804
  parentPath,
731
805
  resolved
732
- });
806
+ }));
733
807
  }
734
808
  /**
735
809
  * Register a single resolved command (and its subcommands) with yargs.
@@ -742,20 +816,34 @@ function registerCommands(options) {
742
816
  * @param options - Command registration context.
743
817
  */
744
818
  function registerResolvedCommand(options) {
745
- const { instance, name, cmd, resolved, parentPath } = options;
819
+ const { instance, name, cmd, resolved, parentPath, errorRef } = options;
746
820
  const description = cmd.description ?? "";
747
821
  instance.command(name, description, (builder) => {
748
822
  registerCommandArgs(builder, cmd.args);
749
823
  if (cmd.commands) {
750
824
  const subCommands = Object.entries(cmd.commands).filter((pair) => isCommand(pair[1]));
751
- for (const [subName, subEntry] of subCommands) registerResolvedCommand({
825
+ if (cmd.order && cmd.order.length > 0) {
826
+ const [validationError] = validateCommandOrder({
827
+ commandNames: subCommands.map(([n]) => n),
828
+ order: cmd.order
829
+ });
830
+ if (validationError && errorRef) {
831
+ errorRef.error = validationError;
832
+ return builder;
833
+ }
834
+ }
835
+ sortCommandEntries({
836
+ entries: subCommands,
837
+ order: cmd.order
838
+ }).map(([subName, subEntry]) => registerResolvedCommand({
752
839
  builder,
753
840
  cmd: subEntry,
841
+ errorRef,
754
842
  instance: builder,
755
843
  name: subName,
756
844
  parentPath: [...parentPath, name],
757
845
  resolved
758
- });
846
+ }));
759
847
  if (cmd.handler) builder.demandCommand(0);
760
848
  else builder.demandCommand(1, "You must specify a subcommand.");
761
849
  }
@@ -890,31 +978,46 @@ const ARGV_SLICE_START = 2;
890
978
  async function cli(options) {
891
979
  const logger = createCliLogger();
892
980
  const [uncaughtError, result] = await attemptAsync(async () => {
893
- const program = yargs(process.argv.slice(ARGV_SLICE_START)).scriptName(options.name).version(options.version).strict().help().option("cwd", {
981
+ const [versionError, version] = resolveVersion(options.version);
982
+ if (versionError) return versionError;
983
+ const program = yargs(process.argv.slice(ARGV_SLICE_START)).scriptName(options.name).version(version).strict().help().option("cwd", {
894
984
  describe: "Set the working directory",
895
985
  global: true,
896
986
  type: "string"
897
987
  });
898
988
  if (options.description) program.usage(options.description);
989
+ const footer = extractFooter(options.help);
990
+ if (footer) program.epilogue(footer);
899
991
  const resolved = { ref: void 0 };
900
- const commands = await resolveCommands(options.commands);
901
- if (commands) {
992
+ const errorRef = { error: void 0 };
993
+ const resolvedCmds = await resolveCommands(options.commands);
994
+ if (resolvedCmds) {
902
995
  registerCommands({
903
- commands,
996
+ commands: resolvedCmds.commands,
997
+ errorRef,
904
998
  instance: program,
999
+ order: resolvedCmds.order,
905
1000
  parentPath: [],
906
1001
  resolved
907
1002
  });
908
- program.demandCommand(1, "You must specify a command.");
1003
+ if (errorRef.error) return errorRef.error;
909
1004
  }
910
1005
  const argv = await program.parseAsync();
911
1006
  applyCwd(argv);
912
- if (!resolved.ref) return;
1007
+ if (!resolved.ref) {
1008
+ showNoCommandHelp({
1009
+ argv,
1010
+ commands: resolvedCmds,
1011
+ help: options.help,
1012
+ program
1013
+ });
1014
+ return;
1015
+ }
913
1016
  const [runtimeError, runtime] = await createRuntime({
914
1017
  config: options.config,
915
1018
  middleware: options.middleware,
916
1019
  name: options.name,
917
- version: options.version
1020
+ version
918
1021
  });
919
1022
  if (runtimeError) return runtimeError;
920
1023
  const [executeError] = await runtime.execute({
@@ -932,23 +1035,64 @@ async function cli(options) {
932
1035
  }
933
1036
  if (result) exitOnError(result, logger);
934
1037
  }
1038
+ const VERSION_ERROR = /* @__PURE__ */ new Error("No CLI version available. Either pass `version` to cli() or build with the kidd bundler.");
1039
+ const VersionSchema = z.string().trim().min(1);
1040
+ /**
1041
+ * Resolve the CLI version from an explicit value or the compile-time constant.
1042
+ *
1043
+ * Resolution order:
1044
+ * 1. Explicit version string passed to `cli()`
1045
+ * 2. `__KIDD_VERSION__` injected by the kidd bundler at build time
1046
+ *
1047
+ * Returns an error when neither source provides a non-empty version.
1048
+ *
1049
+ * @private
1050
+ * @param explicit - The version string from `CliOptions.version`, if provided.
1051
+ * @returns A Result tuple with the resolved version string or an Error.
1052
+ */
1053
+ function resolveVersion(explicit) {
1054
+ if (explicit !== void 0) {
1055
+ const parsed = VersionSchema.safeParse(explicit);
1056
+ if (parsed.success) return ok(parsed.data);
1057
+ return err(VERSION_ERROR);
1058
+ }
1059
+ if (typeof __KIDD_VERSION__ === "string") {
1060
+ const parsed = VersionSchema.safeParse(__KIDD_VERSION__);
1061
+ if (parsed.success) return ok(parsed.data);
1062
+ }
1063
+ return err(VERSION_ERROR);
1064
+ }
935
1065
  /**
936
- * Resolve the commands option to a CommandMap.
1066
+ * Resolve the commands option to a {@link ResolvedCommands}.
937
1067
  *
938
1068
  * Accepts a directory string (triggers autoload), a static CommandMap,
939
- * a Promise<CommandMap> (from autoload() called at the call site),
1069
+ * a Promise<CommandMap>, a structured {@link CommandsConfig},
940
1070
  * or undefined (loads `kidd.config.ts` and autoloads from its `commands` field,
941
1071
  * falling back to `'./commands'`).
942
1072
  *
943
1073
  * @private
944
1074
  * @param commands - The commands option from CliOptions.
945
- * @returns A CommandMap or undefined.
1075
+ * @returns Resolved commands with optional order, or undefined.
946
1076
  */
947
1077
  async function resolveCommands(commands) {
948
- if (isString(commands)) return autoload({ dir: commands });
949
- if (commands instanceof Promise) return commands;
950
- if (isPlainObject(commands)) return commands;
951
- return resolveCommandsFromConfig();
1078
+ return match(commands).when(isString, async (dir) => ({ commands: await autoload({ dir }) })).with(P.instanceOf(Promise), async (p) => ({ commands: await p })).when(isCommandsConfig, (cfg) => resolveCommandsConfig(cfg)).when(isPlainObject, (cmds) => ({ commands: cmds })).otherwise(() => resolveCommandsFromConfig());
1079
+ }
1080
+ /**
1081
+ * Resolve a structured {@link CommandsConfig} into flat commands and order.
1082
+ *
1083
+ * When `path` is provided, autoloads from that directory. Otherwise uses the
1084
+ * inline `commands` map (resolved if it is a promise).
1085
+ *
1086
+ * @private
1087
+ * @param config - The structured commands configuration.
1088
+ * @returns Resolved commands with optional order.
1089
+ */
1090
+ async function resolveCommandsConfig(config) {
1091
+ const { order, path, commands: innerCommands } = config;
1092
+ return {
1093
+ commands: await match(innerCommands).when(() => isString(path), async () => autoload({ dir: path })).with(P.instanceOf(Promise), async (p) => p).when(isPlainObject, (cmds) => cmds).otherwise(() => ({})),
1094
+ order
1095
+ };
952
1096
  }
953
1097
  /**
954
1098
  * Load `kidd.config.ts` and autoload commands from its `commands` field.
@@ -962,8 +1106,8 @@ async function resolveCommands(commands) {
962
1106
  async function resolveCommandsFromConfig() {
963
1107
  const DEFAULT_COMMANDS_DIR = "./commands";
964
1108
  const [configError, configResult] = await loadConfig();
965
- if (configError || !configResult) return autoload({ dir: DEFAULT_COMMANDS_DIR });
966
- return autoload({ dir: configResult.config.commands ?? DEFAULT_COMMANDS_DIR });
1109
+ if (configError || !configResult) return { commands: await autoload({ dir: DEFAULT_COMMANDS_DIR }) };
1110
+ return { commands: await autoload({ dir: configResult.config.commands ?? DEFAULT_COMMANDS_DIR }) };
967
1111
  }
968
1112
  /**
969
1113
  * Change the process working directory when `--cwd` is provided.
@@ -978,6 +1122,47 @@ function applyCwd(argv) {
978
1122
  if (isString(argv.cwd)) process.chdir(resolve(argv.cwd));
979
1123
  }
980
1124
  /**
1125
+ * Show help output when no command was matched.
1126
+ *
1127
+ * Prints the header (if configured) above the yargs help text. Skipped when
1128
+ * `--help` was explicitly passed, since yargs already handles that case.
1129
+ *
1130
+ * @private
1131
+ * @param params - The argv, commands, help options, and yargs program instance.
1132
+ */
1133
+ function showNoCommandHelp({ argv, commands, help, program }) {
1134
+ if (!commands) return;
1135
+ if (argv.help) return;
1136
+ const header = extractHeader(help);
1137
+ if (header) {
1138
+ console.log(header);
1139
+ console.log();
1140
+ }
1141
+ program.showHelp("log");
1142
+ }
1143
+ /**
1144
+ * Extract the header string from help options.
1145
+ *
1146
+ * @private
1147
+ * @param help - The help options, possibly undefined.
1148
+ * @returns The header string or undefined.
1149
+ */
1150
+ function extractHeader(help) {
1151
+ if (!help) return;
1152
+ return help.header;
1153
+ }
1154
+ /**
1155
+ * Extract the footer string from help options.
1156
+ *
1157
+ * @private
1158
+ * @param help - The help options, possibly undefined.
1159
+ * @returns The footer string or undefined.
1160
+ */
1161
+ function extractFooter(help) {
1162
+ if (!help) return;
1163
+ return help.footer;
1164
+ }
1165
+ /**
981
1166
  * Handle a CLI error by logging the message and exiting with the appropriate code.
982
1167
  *
983
1168
  * ContextErrors carry a custom exit code; all other errors exit with code 1.
@@ -1001,21 +1186,6 @@ function exitOnError(error, logger) {
1001
1186
  process.exit(info.exitCode);
1002
1187
  }
1003
1188
  //#endregion
1004
- //#region src/command.ts
1005
- /**
1006
- * Define a CLI command with typed args, config, and handler.
1007
- *
1008
- * The `const TMiddleware` generic preserves the middleware tuple as a literal type,
1009
- * enabling TypeScript to extract and intersect `Variables` from each middleware
1010
- * element onto the handler's `ctx` type.
1011
- *
1012
- * @param def - Command definition including description, args schema, middleware, and handler.
1013
- * @returns A resolved Command object for registration in the command map.
1014
- */
1015
- function command(def) {
1016
- return withTag({ ...def }, "Command");
1017
- }
1018
- //#endregion
1019
1189
  //#region src/compose.ts
1020
1190
  /**
1021
1191
  * Middleware combinator that merges multiple middleware into one.