@kidd-cli/core 0.3.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.
package/README.md CHANGED
@@ -28,6 +28,7 @@ await cli({
28
28
  name: 'my-app',
29
29
  version: '1.0.0',
30
30
  commands: { greet },
31
+ help: { header: 'my-app - a friendly CLI' },
31
32
  })
32
33
  ```
33
34
 
@@ -45,9 +46,7 @@ cli({
45
46
  commands: { deploy, migrate },
46
47
  middleware: [requireAuth()],
47
48
  config: { schema: MyConfigSchema },
48
- credentials: {
49
- apiKey: { env: 'API_KEY', required: true },
50
- },
49
+ help: { header: 'my-app - deploy and migrate with ease' },
51
50
  })
52
51
  ```
53
52
 
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { a as CommandDef, c as Middleware, d as Context, i as Command, l as MiddlewareEnv, n as AutoloadOptions, o as CommandMap, r as CliOptions, s as InferVariables, t as ArgsDef, u as MiddlewareFn } from "./types-CTvrsrnD.js";
2
- import { defineConfig } from "@kidd-cli/config";
1
+ import { a as Command, c as CommandsConfig, d as MiddlewareEnv, f as MiddlewareFn, i as CliOptions, l as InferVariables, n as AutoloadOptions, o as CommandDef, p as Context, r as CliHelpOptions, s as CommandMap, t as ArgsDef, u as Middleware } from "./types-U73X_oQ_.js";
3
2
  import { z } from "zod";
3
+ import { defineConfig } from "@kidd-cli/config";
4
4
 
5
5
  //#region src/cli.d.ts
6
6
  /**
@@ -21,6 +21,10 @@ declare function cli<TSchema extends z.ZodType = z.ZodType>(options: CliOptions<
21
21
  * enabling TypeScript to extract and intersect `Variables` from each middleware
22
22
  * element onto the handler's `ctx` type.
23
23
  *
24
+ * When `def.commands` is a structured {@link CommandsConfig}, the factory
25
+ * normalizes it into flat `commands` and `order` fields on the output
26
+ * `Command` object so downstream consumers never need to handle the grouped form.
27
+ *
24
28
  * @param def - Command definition including description, args schema, middleware, and handler.
25
29
  * @returns A resolved Command object for registration in the command map.
26
30
  */
@@ -113,5 +117,5 @@ declare function decorateContext<TKey extends string, TValue>(ctx: Context, key:
113
117
  */
114
118
  declare function middleware<TEnv extends MiddlewareEnv = MiddlewareEnv>(handler: MiddlewareFn<TEnv>): Middleware<TEnv>;
115
119
  //#endregion
116
- export { type Command, type Context, type MiddlewareEnv, autoload, cli, command, compose, decorateContext, defineConfig, middleware };
120
+ export { type CliHelpOptions, type Command, type CommandsConfig, type Context, type MiddlewareEnv, autoload, cli, command, compose, decorateContext, defineConfig, middleware };
117
121
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/cli.ts","../src/command.ts","../src/compose.ts","../src/autoloader.ts","../src/context/decorate.ts","../src/middleware.ts"],"mappings":";;;;;;;;AAyBA;;;;;iBAAsB,GAAA,iBAAoB,CAAA,CAAE,OAAA,GAAU,CAAA,CAAE,OAAA,CAAA,CACtD,OAAA,EAAS,UAAA,CAAW,OAAA,IACnB,OAAA;;;;;;;AAFH;;;;;;iBCLgB,OAAA,kBACG,OAAA,GAAU,OAAA,kBACX,MAAA,oBAA0B,MAAA,sDACP,UAAA,CAAW,aAAA,eACnC,UAAA,CAAW,aAAA,IAAA,CACtB,GAAA,EAAK,UAAA,CAAW,QAAA,EAAU,OAAA,EAAS,WAAA,IAAe,OAAA;;;;;;;ADApD;UEXU,WAAA,8BAAyC,UAAA,CAAW,aAAA;EAAA,SACnD,SAAA,EAAW,cAAA,CAAe,WAAA,oCACf,CAAA,GACd,MAAA,oBACA,CAAA;AAAA;;;;;;;;;;;;;;;;;;;iBAsBQ,OAAA,oCAA2C,UAAA,CAAW,aAAA,IAAA,CACpE,WAAA,EAAa,WAAA,GACZ,UAAA,CAAW,WAAA,CAAY,WAAA;;;;;;;AFjB1B;;iBGPsB,QAAA,CAAS,OAAA,GAAU,eAAA,GAAkB,OAAA,CAAQ,UAAA;;;;;;;AHOnE;;;;;;;;;;;;;;;;;;;;;;iBIGgB,eAAA,6BAAA,CACd,GAAA,EAAK,OAAA,EACL,GAAA,EAAK,IAAA,EACL,KAAA,EAAO,MAAA,GACN,OAAA;;;;;;;AJPH;;;;;;;;;;;;;iBKJgB,UAAA,cAAwB,aAAA,GAAgB,aAAA,CAAA,CACtD,OAAA,EAAS,YAAA,CAAa,IAAA,IACrB,UAAA,CAAW,IAAA"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/cli.ts","../src/command.ts","../src/compose.ts","../src/autoloader.ts","../src/context/decorate.ts","../src/middleware.ts"],"mappings":";;;;;;;;AA4BA;;;;;iBAAsB,GAAA,iBAAoB,CAAA,CAAE,OAAA,GAAU,CAAA,CAAE,OAAA,CAAA,CACtD,OAAA,EAAS,UAAA,CAAW,OAAA,IACnB,OAAA;;;;;;;;;;;;;;;;;iBCgBa,OAAA,kBACG,OAAA,GAAU,OAAA,kBACX,MAAA,oBAA0B,MAAA,sDACP,UAAA,CAAW,aAAA,eACnC,UAAA,CAAW,aAAA,IAAA,CACtB,GAAA,EAAK,UAAA,CAAW,QAAA,EAAU,OAAA,EAAS,WAAA,IAAe,OAAA;;;;;;;ADvBpD;UEdU,WAAA,8BAAyC,UAAA,CAAW,aAAA;EAAA,SACnD,SAAA,EAAW,cAAA,CAAe,WAAA,oCACf,CAAA,GACd,MAAA,oBACA,CAAA;AAAA;;;;;;;;;;;;;;;;;;;iBAsBQ,OAAA,oCAA2C,UAAA,CAAW,aAAA,IAAA,CACpE,WAAA,EAAa,WAAA,GACZ,UAAA,CAAW,WAAA,CAAY,WAAA;;;;;;;AFd1B;;iBGVsB,QAAA,CAAS,OAAA,GAAU,eAAA,GAAkB,OAAA,CAAQ,UAAA;;;;;;;AHUnE;;;;;;;;;;;;;;;;;;;;;;iBIAgB,eAAA,6BAAA,CACd,GAAA,EAAK,OAAA,EACL,GAAA,EAAK,IAAA,EACL,KAAA,EAAO,MAAA,GACN,OAAA;;;;;;;AJJH;;;;;;;;;;;;;iBKPgB,UAAA,cAAwB,aAAA,GAAgB,aAAA,CAAA,CACtD,OAAA,EAAS,YAAA,CAAa,IAAA,IACrB,UAAA,CAAW,IAAA"}
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@ import { basename, extname, join, resolve } from "node:path";
6
6
  import { loadConfig } from "@kidd-cli/config/loader";
7
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";
@@ -460,6 +461,46 @@ function isCommandDir(entry) {
460
461
  return !entry.name.startsWith("_") && !entry.name.startsWith(".");
461
462
  }
462
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
+ }
503
+ //#endregion
463
504
  //#region src/runtime/args/zod.ts
464
505
  /**
465
506
  * Type guard that checks whether a value is a zod object schema.
@@ -700,6 +741,44 @@ function yargsArgDefToOption(def) {
700
741
  };
701
742
  }
702
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
+ }
781
+ //#endregion
703
782
  //#region src/runtime/register.ts
704
783
  /**
705
784
  * Type guard that checks whether a value is a Command object.
@@ -714,22 +793,36 @@ function isCommand(value) {
714
793
  * Register all commands from a CommandMap on a yargs instance.
715
794
  *
716
795
  * 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.
796
+ * validates the order array, sorts entries, and recursively registers
797
+ * each command (including subcommands) on the provided yargs Argv instance.
719
798
  *
720
799
  * @param options - Registration options including the command map, yargs instance, and resolution ref.
721
800
  */
722
801
  function registerCommands(options) {
723
- const { instance, commands, resolved, parentPath } = options;
802
+ const { instance, commands, resolved, parentPath, order, errorRef } = options;
724
803
  const commandEntries = Object.entries(commands).filter((pair) => isCommand(pair[1]));
725
- for (const [name, entry] of commandEntries) registerResolvedCommand({
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({
726
818
  builder: instance,
727
819
  cmd: entry,
820
+ errorRef,
728
821
  instance,
729
822
  name,
730
823
  parentPath,
731
824
  resolved
732
- });
825
+ }));
733
826
  }
734
827
  /**
735
828
  * Register a single resolved command (and its subcommands) with yargs.
@@ -742,20 +835,34 @@ function registerCommands(options) {
742
835
  * @param options - Command registration context.
743
836
  */
744
837
  function registerResolvedCommand(options) {
745
- const { instance, name, cmd, resolved, parentPath } = options;
838
+ const { instance, name, cmd, resolved, parentPath, errorRef } = options;
746
839
  const description = cmd.description ?? "";
747
840
  instance.command(name, description, (builder) => {
748
841
  registerCommandArgs(builder, cmd.args);
749
842
  if (cmd.commands) {
750
843
  const subCommands = Object.entries(cmd.commands).filter((pair) => isCommand(pair[1]));
751
- for (const [subName, subEntry] of subCommands) registerResolvedCommand({
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({
752
858
  builder,
753
859
  cmd: subEntry,
860
+ errorRef,
754
861
  instance: builder,
755
862
  name: subName,
756
863
  parentPath: [...parentPath, name],
757
864
  resolved
758
- });
865
+ }));
759
866
  if (cmd.handler) builder.demandCommand(0);
760
867
  else builder.demandCommand(1, "You must specify a subcommand.");
761
868
  }
@@ -890,31 +997,46 @@ const ARGV_SLICE_START = 2;
890
997
  async function cli(options) {
891
998
  const logger = createCliLogger();
892
999
  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", {
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", {
894
1003
  describe: "Set the working directory",
895
1004
  global: true,
896
1005
  type: "string"
897
1006
  });
898
1007
  if (options.description) program.usage(options.description);
1008
+ const footer = extractFooter(options.help);
1009
+ if (footer) program.epilogue(footer);
899
1010
  const resolved = { ref: void 0 };
900
- const commands = await resolveCommands(options.commands);
901
- if (commands) {
1011
+ const errorRef = { error: void 0 };
1012
+ const resolvedCmds = await resolveCommands(options.commands);
1013
+ if (resolvedCmds) {
902
1014
  registerCommands({
903
- commands,
1015
+ commands: resolvedCmds.commands,
1016
+ errorRef,
904
1017
  instance: program,
1018
+ order: resolvedCmds.order,
905
1019
  parentPath: [],
906
1020
  resolved
907
1021
  });
908
- program.demandCommand(1, "You must specify a command.");
1022
+ if (errorRef.error) return errorRef.error;
909
1023
  }
910
1024
  const argv = await program.parseAsync();
911
1025
  applyCwd(argv);
912
- 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
+ }
913
1035
  const [runtimeError, runtime] = await createRuntime({
914
1036
  config: options.config,
915
1037
  middleware: options.middleware,
916
1038
  name: options.name,
917
- version: options.version
1039
+ version
918
1040
  });
919
1041
  if (runtimeError) return runtimeError;
920
1042
  const [executeError] = await runtime.execute({
@@ -932,23 +1054,64 @@ async function cli(options) {
932
1054
  }
933
1055
  if (result) exitOnError(result, logger);
934
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
+ }
935
1084
  /**
936
- * Resolve the commands option to a CommandMap.
1085
+ * Resolve the commands option to a {@link ResolvedCommands}.
937
1086
  *
938
1087
  * Accepts a directory string (triggers autoload), a static CommandMap,
939
- * a Promise<CommandMap> (from autoload() called at the call site),
1088
+ * a Promise<CommandMap>, a structured {@link CommandsConfig},
940
1089
  * or undefined (loads `kidd.config.ts` and autoloads from its `commands` field,
941
1090
  * falling back to `'./commands'`).
942
1091
  *
943
1092
  * @private
944
1093
  * @param commands - The commands option from CliOptions.
945
- * @returns A CommandMap or undefined.
1094
+ * @returns Resolved commands with optional order, or undefined.
946
1095
  */
947
1096
  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();
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
+ };
952
1115
  }
953
1116
  /**
954
1117
  * Load `kidd.config.ts` and autoload commands from its `commands` field.
@@ -962,8 +1125,8 @@ async function resolveCommands(commands) {
962
1125
  async function resolveCommandsFromConfig() {
963
1126
  const DEFAULT_COMMANDS_DIR = "./commands";
964
1127
  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 });
1128
+ if (configError || !configResult) return { commands: await autoload({ dir: DEFAULT_COMMANDS_DIR }) };
1129
+ return { commands: await autoload({ dir: configResult.config.commands ?? DEFAULT_COMMANDS_DIR }) };
967
1130
  }
968
1131
  /**
969
1132
  * Change the process working directory when `--cwd` is provided.
@@ -978,6 +1141,47 @@ function applyCwd(argv) {
978
1141
  if (isString(argv.cwd)) process.chdir(resolve(argv.cwd));
979
1142
  }
980
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
+ /**
981
1185
  * Handle a CLI error by logging the message and exiting with the appropriate code.
982
1186
  *
983
1187
  * ContextErrors carry a custom exit code; all other errors exit with code 1.
@@ -1001,21 +1205,6 @@ function exitOnError(error, logger) {
1001
1205
  process.exit(info.exitCode);
1002
1206
  }
1003
1207
  //#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
1208
  //#region src/compose.ts
1020
1209
  /**
1021
1210
  * Middleware combinator that merges multiple middleware into one.