@kidd-cli/core 0.2.0 → 0.4.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 (41) hide show
  1. package/README.md +2 -3
  2. package/dist/{config-Db_sjFU-.js → config-D8e5qxLp.js} +5 -17
  3. package/dist/config-D8e5qxLp.js.map +1 -0
  4. package/dist/{create-store-D-fQpCql.js → create-store-OHdkm_Yt.js} +3 -4
  5. package/dist/{create-store-D-fQpCql.js.map → create-store-OHdkm_Yt.js.map} +1 -1
  6. package/dist/index.d.ts +36 -3
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +297 -73
  9. package/dist/index.js.map +1 -1
  10. package/dist/lib/config.js +3 -4
  11. package/dist/lib/logger.d.ts +1 -1
  12. package/dist/lib/logger.js +1 -2
  13. package/dist/lib/logger.js.map +1 -1
  14. package/dist/lib/project.d.ts +1 -1
  15. package/dist/lib/project.d.ts.map +1 -1
  16. package/dist/lib/project.js +2 -3
  17. package/dist/lib/store.d.ts +1 -1
  18. package/dist/lib/store.js +3 -4
  19. package/dist/{logger-BkQQej8h.d.ts → logger-9j49T5da.d.ts} +1 -1
  20. package/dist/{logger-BkQQej8h.d.ts.map → logger-9j49T5da.d.ts.map} +1 -1
  21. package/dist/middleware/auth.d.ts +81 -41
  22. package/dist/middleware/auth.d.ts.map +1 -1
  23. package/dist/middleware/auth.js +287 -233
  24. package/dist/middleware/auth.js.map +1 -1
  25. package/dist/middleware/http.d.ts +1 -1
  26. package/dist/middleware/http.js +163 -4
  27. package/dist/middleware/http.js.map +1 -1
  28. package/dist/{middleware-BFBKNSPQ.js → middleware-BWnPSRWR.js} +2 -4
  29. package/dist/{middleware-BFBKNSPQ.js.map → middleware-BWnPSRWR.js.map} +1 -1
  30. package/dist/{project-DuXgjaa_.js → project-D0g84bZY.js} +4 -8
  31. package/dist/project-D0g84bZY.js.map +1 -0
  32. package/dist/{types-C0CYivzY.d.ts → types-D-BxshYM.d.ts} +1 -1
  33. package/dist/{types-C0CYivzY.d.ts.map → types-D-BxshYM.d.ts.map} +1 -1
  34. package/dist/{types-BaZ5WqVM.d.ts → types-U73X_oQ_.d.ts} +60 -10
  35. package/dist/types-U73X_oQ_.d.ts.map +1 -0
  36. package/package.json +7 -7
  37. package/dist/config-Db_sjFU-.js.map +0 -1
  38. package/dist/create-http-client-tZJWlWp1.js +0 -165
  39. package/dist/create-http-client-tZJWlWp1.js.map +0 -1
  40. package/dist/project-DuXgjaa_.js.map +0 -1
  41. package/dist/types-BaZ5WqVM.d.ts.map +0 -1
package/dist/index.js CHANGED
@@ -1,11 +1,12 @@
1
1
  import { createCliLogger } from "./lib/logger.js";
2
- import { n as DEFAULT_EXIT_CODE, t as createConfigClient } from "./config-Db_sjFU-.js";
3
- import { n as decorateContext, t as middleware } from "./middleware-BFBKNSPQ.js";
4
- import "./project-DuXgjaa_.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";
5
5
  import { basename, extname, join, resolve } from "node:path";
6
6
  import { loadConfig } from "@kidd-cli/config/loader";
7
- import { attemptAsync, err, isPlainObject, isString, ok } from "@kidd-cli/utils/fp";
7
+ import { P, attemptAsync, err, isPlainObject, isString, match, ok } from "@kidd-cli/utils/fp";
8
8
  import yargs from "yargs";
9
+ import { z } from "zod";
9
10
  import * as clack from "@clack/prompts";
10
11
  import { TAG, hasTag, withTag } from "@kidd-cli/utils/tag";
11
12
  import { jsonStringify } from "@kidd-cli/utils/json";
@@ -13,7 +14,6 @@ import { readdir } from "node:fs/promises";
13
14
  import { formatZodIssues } from "@kidd-cli/utils/validate";
14
15
  import { match as match$1 } from "ts-pattern";
15
16
  import { defineConfig } from "@kidd-cli/config";
16
-
17
17
  //#region src/context/error.ts
18
18
  /**
19
19
  * Create a ContextError with an exit code and optional error code.
@@ -63,7 +63,7 @@ function isContextError(error) {
63
63
  }
64
64
  function resolveExitCode(options) {
65
65
  if (options && options.exitCode !== void 0) return options.exitCode;
66
- return DEFAULT_EXIT_CODE;
66
+ return 1;
67
67
  }
68
68
  function resolveCode(options) {
69
69
  if (options && options.code !== void 0) return options.code;
@@ -75,7 +75,6 @@ function createContextErrorData(message, options) {
75
75
  message
76
76
  }, "ContextError");
77
77
  }
78
-
79
78
  //#endregion
80
79
  //#region src/context/output.ts
81
80
  /**
@@ -192,7 +191,6 @@ function writeTableToStream(stream, rows, keys) {
192
191
  ].join("\n");
193
192
  stream.write(`${content}\n`);
194
193
  }
195
-
196
194
  //#endregion
197
195
  //#region src/context/prompts.ts
198
196
  /**
@@ -234,12 +232,11 @@ function unwrapCancelSignal(result) {
234
232
  clack.cancel("Operation cancelled.");
235
233
  throw createContextError("Prompt cancelled by user", {
236
234
  code: "PROMPT_CANCELLED",
237
- exitCode: DEFAULT_EXIT_CODE
235
+ exitCode: 1
238
236
  });
239
237
  }
240
238
  return result;
241
239
  }
242
-
243
240
  //#endregion
244
241
  //#region src/context/store.ts
245
242
  /**
@@ -268,7 +265,6 @@ function createMemoryStore() {
268
265
  }
269
266
  };
270
267
  }
271
-
272
268
  //#endregion
273
269
  //#region src/context/create-context.ts
274
270
  /**
@@ -306,7 +302,6 @@ function createContext(options) {
306
302
  store: ctxStore
307
303
  };
308
304
  }
309
-
310
305
  //#endregion
311
306
  //#region src/autoloader.ts
312
307
  const VALID_EXTENSIONS = new Set([
@@ -426,7 +421,8 @@ async function importCommand(filePath) {
426
421
  */
427
422
  function isCommandExport(mod) {
428
423
  if (typeof mod !== "object" || mod === null) return false;
429
- const def = mod["default"];
424
+ if (!("default" in mod)) return false;
425
+ const def = mod.default;
430
426
  if (!isPlainObject(def)) return false;
431
427
  return hasTag(def, "Command");
432
428
  }
@@ -464,7 +460,46 @@ function isCommandDir(entry) {
464
460
  if (!entry.isDirectory()) return false;
465
461
  return !entry.name.startsWith("_") && !entry.name.startsWith(".");
466
462
  }
467
-
463
+ //#endregion
464
+ //#region src/command.ts
465
+ /**
466
+ * Check whether a value is a structured {@link CommandsConfig} object.
467
+ *
468
+ * Discriminates from a plain `CommandMap` by checking for the `order` (array)
469
+ * or `path` (string) keys — neither can appear on a valid `CommandMap` whose
470
+ * values are tagged `Command` objects.
471
+ *
472
+ * @param value - The value to test.
473
+ * @returns `true` when `value` is a `CommandsConfig`.
474
+ */
475
+ function isCommandsConfig(value) {
476
+ if (typeof value !== "object" || value === null || value instanceof Promise) return false;
477
+ return "order" in value && Array.isArray(value.order) || "path" in value && typeof value.path === "string";
478
+ }
479
+ /**
480
+ * Define a CLI command with typed args, config, and handler.
481
+ *
482
+ * The `const TMiddleware` generic preserves the middleware tuple as a literal type,
483
+ * enabling TypeScript to extract and intersect `Variables` from each middleware
484
+ * element onto the handler's `ctx` type.
485
+ *
486
+ * When `def.commands` is a structured {@link CommandsConfig}, the factory
487
+ * normalizes it into flat `commands` and `order` fields on the output
488
+ * `Command` object so downstream consumers never need to handle the grouped form.
489
+ *
490
+ * @param def - Command definition including description, args schema, middleware, and handler.
491
+ * @returns A resolved Command object for registration in the command map.
492
+ */
493
+ function command(def) {
494
+ return match(def.commands).when(isCommandsConfig, (cfg) => {
495
+ const { order, commands: innerCommands } = cfg;
496
+ return withTag({
497
+ ...def,
498
+ commands: innerCommands,
499
+ order
500
+ }, "Command");
501
+ }).otherwise(() => withTag({ ...def }, "Command"));
502
+ }
468
503
  //#endregion
469
504
  //#region src/runtime/args/zod.ts
470
505
  /**
@@ -624,7 +659,6 @@ function getZodTypeOption(schema) {
624
659
  };
625
660
  return base;
626
661
  }
627
-
628
662
  //#endregion
629
663
  //#region src/runtime/args/parser.ts
630
664
  /**
@@ -671,7 +705,6 @@ function validateArgs(argsDef, parsedArgs) {
671
705
  if (!result.success) return err(/* @__PURE__ */ new Error(`Invalid arguments:\n ${formatZodIssues(result.error.issues).message}`));
672
706
  return ok(result.data);
673
707
  }
674
-
675
708
  //#endregion
676
709
  //#region src/runtime/args/register.ts
677
710
  /**
@@ -688,10 +721,7 @@ function registerCommandArgs(builder, args) {
688
721
  if (isZodSchema(args)) {
689
722
  const options = zodSchemaToYargsOptions(args);
690
723
  for (const [key, opt] of Object.entries(options)) builder.option(key, opt);
691
- } else {
692
- const argsDef = args;
693
- for (const [key, def] of Object.entries(argsDef)) builder.option(key, yargsArgDefToOption(def));
694
- }
724
+ } else for (const [key, def] of Object.entries(args)) builder.option(key, yargsArgDefToOption(def));
695
725
  }
696
726
  /**
697
727
  * Convert a yargs-native arg definition into a yargs option object.
@@ -710,7 +740,44 @@ function yargsArgDefToOption(def) {
710
740
  type: def.type
711
741
  };
712
742
  }
713
-
743
+ //#endregion
744
+ //#region src/runtime/sort-commands.ts
745
+ /**
746
+ * Validate that every name in the order array exists in the provided command names.
747
+ *
748
+ * @param params - The order array and available command names to validate against.
749
+ * @returns A Result tuple — `[null, void]` on success or `[Error, null]` on failure.
750
+ */
751
+ function validateCommandOrder(params) {
752
+ const { order, commandNames } = params;
753
+ const seen = /* @__PURE__ */ new Set();
754
+ const duplicates = order.filter((name) => {
755
+ if (seen.has(name)) return true;
756
+ seen.add(name);
757
+ return false;
758
+ });
759
+ if (duplicates.length > 0) return err(`Invalid command order: duplicate command(s) ${[...new Set(duplicates)].map((n) => `"${n}"`).join(", ")}`);
760
+ const nameSet = new Set(commandNames);
761
+ const invalid = order.filter((name) => !nameSet.has(name));
762
+ if (invalid.length > 0) return err(`Invalid command order: unknown command(s) ${invalid.map((n) => `"${n}"`).join(", ")}`);
763
+ return ok();
764
+ }
765
+ /**
766
+ * Sort command entries with ordered names first (in specified order),
767
+ * remaining names alphabetically.
768
+ *
769
+ * @param params - The command entries and optional order array.
770
+ * @returns Sorted array of `[name, Command]` entries.
771
+ */
772
+ function sortCommandEntries(params) {
773
+ const { entries, order } = params;
774
+ if (!order || order.length === 0) return [...entries].toSorted(([a], [b]) => a.localeCompare(b));
775
+ const entryMap = new Map(entries);
776
+ const ordered = order.filter((name) => entryMap.has(name)).map((name) => [name, entryMap.get(name)]);
777
+ const orderedSet = new Set(order);
778
+ const remaining = entries.filter(([name]) => !orderedSet.has(name)).toSorted(([a], [b]) => a.localeCompare(b));
779
+ return [...ordered, ...remaining];
780
+ }
714
781
  //#endregion
715
782
  //#region src/runtime/register.ts
716
783
  /**
@@ -726,22 +793,36 @@ function isCommand(value) {
726
793
  * Register all commands from a CommandMap on a yargs instance.
727
794
  *
728
795
  * Iterates over the command map, filters for valid Command objects,
729
- * and recursively registers each command (including subcommands) on
730
- * the provided yargs Argv instance.
796
+ * validates the order array, sorts entries, and recursively registers
797
+ * each command (including subcommands) on the provided yargs Argv instance.
731
798
  *
732
799
  * @param options - Registration options including the command map, yargs instance, and resolution ref.
733
800
  */
734
801
  function registerCommands(options) {
735
- const { instance, commands, resolved, parentPath } = options;
736
- const commandEntries = Object.entries(commands).filter(([, entry]) => isCommand(entry));
737
- for (const [name, entry] of commandEntries) registerResolvedCommand({
802
+ const { instance, commands, resolved, parentPath, order, errorRef } = options;
803
+ const commandEntries = Object.entries(commands).filter((pair) => isCommand(pair[1]));
804
+ if (order && order.length > 0) {
805
+ const [validationError] = validateCommandOrder({
806
+ commandNames: commandEntries.map(([name]) => name),
807
+ order
808
+ });
809
+ if (validationError && errorRef) {
810
+ errorRef.error = validationError;
811
+ return;
812
+ }
813
+ }
814
+ sortCommandEntries({
815
+ entries: commandEntries,
816
+ order
817
+ }).map(([name, entry]) => registerResolvedCommand({
738
818
  builder: instance,
739
819
  cmd: entry,
820
+ errorRef,
740
821
  instance,
741
822
  name,
742
823
  parentPath,
743
824
  resolved
744
- });
825
+ }));
745
826
  }
746
827
  /**
747
828
  * Register a single resolved command (and its subcommands) with yargs.
@@ -754,20 +835,34 @@ function registerCommands(options) {
754
835
  * @param options - Command registration context.
755
836
  */
756
837
  function registerResolvedCommand(options) {
757
- const { instance, name, cmd, resolved, parentPath } = options;
838
+ const { instance, name, cmd, resolved, parentPath, errorRef } = options;
758
839
  const description = cmd.description ?? "";
759
840
  instance.command(name, description, (builder) => {
760
841
  registerCommandArgs(builder, cmd.args);
761
842
  if (cmd.commands) {
762
- const subCommands = Object.entries(cmd.commands).filter(([, entry]) => isCommand(entry));
763
- for (const [subName, subEntry] of subCommands) registerResolvedCommand({
843
+ const subCommands = Object.entries(cmd.commands).filter((pair) => isCommand(pair[1]));
844
+ if (cmd.order && cmd.order.length > 0) {
845
+ const [validationError] = validateCommandOrder({
846
+ commandNames: subCommands.map(([n]) => n),
847
+ order: cmd.order
848
+ });
849
+ if (validationError && errorRef) {
850
+ errorRef.error = validationError;
851
+ return builder;
852
+ }
853
+ }
854
+ sortCommandEntries({
855
+ entries: subCommands,
856
+ order: cmd.order
857
+ }).map(([subName, subEntry]) => registerResolvedCommand({
764
858
  builder,
765
859
  cmd: subEntry,
860
+ errorRef,
766
861
  instance: builder,
767
862
  name: subName,
768
863
  parentPath: [...parentPath, name],
769
864
  resolved
770
- });
865
+ }));
771
866
  if (cmd.handler) builder.demandCommand(0);
772
867
  else builder.demandCommand(1, "You must specify a subcommand.");
773
868
  }
@@ -781,7 +876,6 @@ function registerResolvedCommand(options) {
781
876
  };
782
877
  });
783
878
  }
784
-
785
879
  //#endregion
786
880
  //#region src/runtime/runner.ts
787
881
  /**
@@ -832,7 +926,6 @@ async function runMiddlewareChain(middlewares, ctx, finalHandler) {
832
926
  }
833
927
  await executeChain(0);
834
928
  }
835
-
836
929
  //#endregion
837
930
  //#region src/runtime/runtime.ts
838
931
  /**
@@ -855,7 +948,7 @@ async function createRuntime(options) {
855
948
  args: validatedArgs,
856
949
  config,
857
950
  meta: {
858
- command: command.commandPath,
951
+ command: [...command.commandPath],
859
952
  name: options.name,
860
953
  version: options.version
861
954
  }
@@ -890,7 +983,6 @@ async function resolveConfig(configOptions, defaultName) {
890
983
  if (configError || !configResult) return {};
891
984
  return configResult.config;
892
985
  }
893
-
894
986
  //#endregion
895
987
  //#region src/cli.ts
896
988
  const ARGV_SLICE_START = 2;
@@ -905,31 +997,46 @@ const ARGV_SLICE_START = 2;
905
997
  async function cli(options) {
906
998
  const logger = createCliLogger();
907
999
  const [uncaughtError, result] = await attemptAsync(async () => {
908
- const program = yargs(process.argv.slice(ARGV_SLICE_START)).scriptName(options.name).version(options.version).strict().help().option("cwd", {
1000
+ const [versionError, version] = resolveVersion(options.version);
1001
+ if (versionError) return versionError;
1002
+ const program = yargs(process.argv.slice(ARGV_SLICE_START)).scriptName(options.name).version(version).strict().help().option("cwd", {
909
1003
  describe: "Set the working directory",
910
1004
  global: true,
911
1005
  type: "string"
912
1006
  });
913
1007
  if (options.description) program.usage(options.description);
1008
+ const footer = extractFooter(options.help);
1009
+ if (footer) program.epilogue(footer);
914
1010
  const resolved = { ref: void 0 };
915
- const commands = await resolveCommands(options.commands);
916
- if (commands) {
1011
+ const errorRef = { error: void 0 };
1012
+ const resolvedCmds = await resolveCommands(options.commands);
1013
+ if (resolvedCmds) {
917
1014
  registerCommands({
918
- commands,
1015
+ commands: resolvedCmds.commands,
1016
+ errorRef,
919
1017
  instance: program,
1018
+ order: resolvedCmds.order,
920
1019
  parentPath: [],
921
1020
  resolved
922
1021
  });
923
- program.demandCommand(1, "You must specify a command.");
1022
+ if (errorRef.error) return errorRef.error;
924
1023
  }
925
1024
  const argv = await program.parseAsync();
926
1025
  applyCwd(argv);
927
- if (!resolved.ref) return;
1026
+ if (!resolved.ref) {
1027
+ showNoCommandHelp({
1028
+ argv,
1029
+ commands: resolvedCmds,
1030
+ help: options.help,
1031
+ program
1032
+ });
1033
+ return;
1034
+ }
928
1035
  const [runtimeError, runtime] = await createRuntime({
929
1036
  config: options.config,
930
1037
  middleware: options.middleware,
931
1038
  name: options.name,
932
- version: options.version
1039
+ version
933
1040
  });
934
1041
  if (runtimeError) return runtimeError;
935
1042
  const [executeError] = await runtime.execute({
@@ -947,23 +1054,64 @@ async function cli(options) {
947
1054
  }
948
1055
  if (result) exitOnError(result, logger);
949
1056
  }
1057
+ const VERSION_ERROR = /* @__PURE__ */ new Error("No CLI version available. Either pass `version` to cli() or build with the kidd bundler.");
1058
+ const VersionSchema = z.string().trim().min(1);
1059
+ /**
1060
+ * Resolve the CLI version from an explicit value or the compile-time constant.
1061
+ *
1062
+ * Resolution order:
1063
+ * 1. Explicit version string passed to `cli()`
1064
+ * 2. `__KIDD_VERSION__` injected by the kidd bundler at build time
1065
+ *
1066
+ * Returns an error when neither source provides a non-empty version.
1067
+ *
1068
+ * @private
1069
+ * @param explicit - The version string from `CliOptions.version`, if provided.
1070
+ * @returns A Result tuple with the resolved version string or an Error.
1071
+ */
1072
+ function resolveVersion(explicit) {
1073
+ if (explicit !== void 0) {
1074
+ const parsed = VersionSchema.safeParse(explicit);
1075
+ if (parsed.success) return ok(parsed.data);
1076
+ return err(VERSION_ERROR);
1077
+ }
1078
+ if (typeof __KIDD_VERSION__ === "string") {
1079
+ const parsed = VersionSchema.safeParse(__KIDD_VERSION__);
1080
+ if (parsed.success) return ok(parsed.data);
1081
+ }
1082
+ return err(VERSION_ERROR);
1083
+ }
950
1084
  /**
951
- * Resolve the commands option to a CommandMap.
1085
+ * Resolve the commands option to a {@link ResolvedCommands}.
952
1086
  *
953
1087
  * Accepts a directory string (triggers autoload), a static CommandMap,
954
- * a Promise<CommandMap> (from autoload() called at the call site),
1088
+ * a Promise<CommandMap>, a structured {@link CommandsConfig},
955
1089
  * or undefined (loads `kidd.config.ts` and autoloads from its `commands` field,
956
1090
  * falling back to `'./commands'`).
957
1091
  *
958
1092
  * @private
959
1093
  * @param commands - The commands option from CliOptions.
960
- * @returns A CommandMap or undefined.
1094
+ * @returns Resolved commands with optional order, or undefined.
961
1095
  */
962
1096
  async function resolveCommands(commands) {
963
- if (isString(commands)) return autoload({ dir: commands });
964
- if (commands instanceof Promise) return commands;
965
- if (isPlainObject(commands)) return commands;
966
- return resolveCommandsFromConfig();
1097
+ 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());
1098
+ }
1099
+ /**
1100
+ * Resolve a structured {@link CommandsConfig} into flat commands and order.
1101
+ *
1102
+ * When `path` is provided, autoloads from that directory. Otherwise uses the
1103
+ * inline `commands` map (resolved if it is a promise).
1104
+ *
1105
+ * @private
1106
+ * @param config - The structured commands configuration.
1107
+ * @returns Resolved commands with optional order.
1108
+ */
1109
+ async function resolveCommandsConfig(config) {
1110
+ const { order, path, commands: innerCommands } = config;
1111
+ return {
1112
+ commands: await match(innerCommands).when(() => isString(path), async () => autoload({ dir: path })).with(P.instanceOf(Promise), async (p) => p).when(isPlainObject, (cmds) => cmds).otherwise(() => ({})),
1113
+ order
1114
+ };
967
1115
  }
968
1116
  /**
969
1117
  * Load `kidd.config.ts` and autoload commands from its `commands` field.
@@ -977,8 +1125,8 @@ async function resolveCommands(commands) {
977
1125
  async function resolveCommandsFromConfig() {
978
1126
  const DEFAULT_COMMANDS_DIR = "./commands";
979
1127
  const [configError, configResult] = await loadConfig();
980
- if (configError || !configResult) return autoload({ dir: DEFAULT_COMMANDS_DIR });
981
- return autoload({ dir: configResult.config.commands ?? DEFAULT_COMMANDS_DIR });
1128
+ if (configError || !configResult) return { commands: await autoload({ dir: DEFAULT_COMMANDS_DIR }) };
1129
+ return { commands: await autoload({ dir: configResult.config.commands ?? DEFAULT_COMMANDS_DIR }) };
982
1130
  }
983
1131
  /**
984
1132
  * Change the process working directory when `--cwd` is provided.
@@ -993,6 +1141,47 @@ function applyCwd(argv) {
993
1141
  if (isString(argv.cwd)) process.chdir(resolve(argv.cwd));
994
1142
  }
995
1143
  /**
1144
+ * Show help output when no command was matched.
1145
+ *
1146
+ * Prints the header (if configured) above the yargs help text. Skipped when
1147
+ * `--help` was explicitly passed, since yargs already handles that case.
1148
+ *
1149
+ * @private
1150
+ * @param params - The argv, commands, help options, and yargs program instance.
1151
+ */
1152
+ function showNoCommandHelp({ argv, commands, help, program }) {
1153
+ if (!commands) return;
1154
+ if (argv.help) return;
1155
+ const header = extractHeader(help);
1156
+ if (header) {
1157
+ console.log(header);
1158
+ console.log();
1159
+ }
1160
+ program.showHelp("log");
1161
+ }
1162
+ /**
1163
+ * Extract the header string from help options.
1164
+ *
1165
+ * @private
1166
+ * @param help - The help options, possibly undefined.
1167
+ * @returns The header string or undefined.
1168
+ */
1169
+ function extractHeader(help) {
1170
+ if (!help) return;
1171
+ return help.header;
1172
+ }
1173
+ /**
1174
+ * Extract the footer string from help options.
1175
+ *
1176
+ * @private
1177
+ * @param help - The help options, possibly undefined.
1178
+ * @returns The footer string or undefined.
1179
+ */
1180
+ function extractFooter(help) {
1181
+ if (!help) return;
1182
+ return help.footer;
1183
+ }
1184
+ /**
996
1185
  * Handle a CLI error by logging the message and exiting with the appropriate code.
997
1186
  *
998
1187
  * ContextErrors carry a custom exit code; all other errors exit with code 1.
@@ -1002,34 +1191,69 @@ function applyCwd(argv) {
1002
1191
  * @param logger - Logger with an error method for output.
1003
1192
  */
1004
1193
  function exitOnError(error, logger) {
1005
- if (isContextError(error)) {
1006
- logger.error(error.message);
1007
- process.exit(error.exitCode);
1008
- } else if (error instanceof Error) {
1009
- logger.error(error.message);
1010
- process.exit(DEFAULT_EXIT_CODE);
1011
- } else {
1012
- logger.error(String(error));
1013
- process.exit(DEFAULT_EXIT_CODE);
1014
- }
1194
+ const info = match(error).when(isContextError, (e) => ({
1195
+ exitCode: e.exitCode,
1196
+ message: e.message
1197
+ })).with(P.instanceOf(Error), (e) => ({
1198
+ exitCode: 1,
1199
+ message: e.message
1200
+ })).otherwise((e) => ({
1201
+ exitCode: 1,
1202
+ message: String(e)
1203
+ }));
1204
+ logger.error(info.message);
1205
+ process.exit(info.exitCode);
1015
1206
  }
1016
-
1017
1207
  //#endregion
1018
- //#region src/command.ts
1208
+ //#region src/compose.ts
1019
1209
  /**
1020
- * Define a CLI command with typed args, config, and handler.
1210
+ * Middleware combinator that merges multiple middleware into one.
1021
1211
  *
1022
- * The `const TMiddleware` generic preserves the middleware tuple as a literal type,
1023
- * enabling TypeScript to extract and intersect `Variables` from each middleware
1024
- * element onto the handler's `ctx` type.
1212
+ * @module
1213
+ */
1214
+ /**
1215
+ * Compose multiple middleware into a single middleware.
1025
1216
  *
1026
- * @param def - Command definition including description, args schema, middleware, and handler.
1027
- * @returns A resolved Command object for registration in the command map.
1217
+ * Executes each middleware in order, threading `next()` through the chain.
1218
+ * The final `next()` call from the last composed middleware continues to
1219
+ * the downstream middleware or command handler.
1220
+ *
1221
+ * The returned middleware's type merges all `Variables` from the input tuple,
1222
+ * so downstream handlers see the combined context.
1223
+ *
1224
+ * @param middlewares - An ordered tuple of middleware to compose.
1225
+ * @returns A single Middleware whose Variables is the intersection of all input Variables.
1226
+ *
1227
+ * @example
1228
+ * ```ts
1229
+ * const combined = compose([auth({ strategies: [auth.env()] }), auth.require()])
1230
+ * ```
1028
1231
  */
1029
- function command(def) {
1030
- return withTag({ ...def }, "Command");
1232
+ function compose(middlewares) {
1233
+ return middleware((ctx, next) => executeChain(middlewares, 0, ctx, next));
1234
+ }
1235
+ /**
1236
+ * Recursively execute middleware in order, calling next() after the last one.
1237
+ *
1238
+ * @private
1239
+ * @param middlewares - The middleware array.
1240
+ * @param index - Current position in the array.
1241
+ * @param ctx - The context object.
1242
+ * @param next - The downstream next function.
1243
+ */
1244
+ async function executeChain(middlewares, index, ctx, next) {
1245
+ if (index >= middlewares.length) {
1246
+ await next();
1247
+ return;
1248
+ }
1249
+ const mw = middlewares[index];
1250
+ if (mw === void 0) {
1251
+ await next();
1252
+ return;
1253
+ }
1254
+ await mw.handler(ctx, () => executeChain(middlewares, index + 1, ctx, next));
1031
1255
  }
1032
-
1033
1256
  //#endregion
1034
- export { autoload, cli, command, decorateContext, defineConfig, middleware };
1257
+ export { autoload, cli, command, compose, decorateContext, defineConfig, middleware };
1258
+
1035
1259
  //# sourceMappingURL=index.js.map