@gunshi/bone 0.27.6 → 0.28.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/lib/index.d.ts CHANGED
@@ -1068,6 +1068,15 @@ interface CommandContext<G extends GunshiParamsConstraint = DefaultGunshiParams>
1068
1068
  * The command call mode is `entry` when the command is executed as an entry command, and `subCommand` when the command is executed as a sub-command.
1069
1069
  */
1070
1070
  callMode: CommandCallMode;
1071
+ /**
1072
+ * The path of nested sub-commands that were resolved to reach the current command.
1073
+ *
1074
+ * For example, if the user runs `git remote add`, `commandPath` would be `['remote', 'add']`.
1075
+ * For the entry command, this is an empty array.
1076
+ *
1077
+ * @since v0.28.0
1078
+ */
1079
+ commandPath: string[];
1071
1080
  /**
1072
1081
  * Whether to convert the camel-case style argument name to kebab-case.
1073
1082
  * This context value is set from {@linkcode Command.toKebab} option.
@@ -1209,6 +1218,15 @@ interface Command<G extends GunshiParamsConstraint = DefaultGunshiParams> {
1209
1218
  * @since v0.27.0
1210
1219
  */
1211
1220
  rendering?: RenderingOptions<G>;
1221
+ /**
1222
+ * Nested sub-commands for this command.
1223
+ *
1224
+ * Allows building command trees like `git remote add`.
1225
+ * Each key is the sub-command name, and the value is a command or lazy command.
1226
+ *
1227
+ * @since v0.28.0
1228
+ */
1229
+ subCommands?: Record<string, SubCommandable> | Map<string, SubCommandable>;
1212
1230
  }
1213
1231
  /**
1214
1232
  * Lazy command interface.
@@ -1285,6 +1303,13 @@ interface SubCommandable {
1285
1303
  * see {@link LazyCommand.commandName}
1286
1304
  */
1287
1305
  commandName?: string;
1306
+ /**
1307
+ * Nested sub-commands for this command.
1308
+ *
1309
+ * @see {@link Command.subCommands}
1310
+ * @since v0.28.0
1311
+ */
1312
+ subCommands?: Record<string, any> | Map<string, any>;
1288
1313
  /**
1289
1314
  * Index signature to allow additional properties
1290
1315
  */
package/lib/index.js CHANGED
@@ -555,7 +555,8 @@ async function resolveLazyCommand(cmd, name, needRunResolving = false) {
555
555
  args: cmd.args,
556
556
  examples: cmd.examples,
557
557
  internal: cmd.internal,
558
- entry: cmd.entry
558
+ entry: cmd.entry,
559
+ subCommands: cmd.subCommands
559
560
  };
560
561
  if ("resource" in cmd && cmd.resource) baseCommand.resource = cmd.resource;
561
562
  command = Object.assign(create(), baseCommand);
@@ -571,6 +572,7 @@ async function resolveLazyCommand(cmd, name, needRunResolving = false) {
571
572
  command.examples = loaded.examples;
572
573
  command.internal = loaded.internal;
573
574
  command.entry = loaded.entry;
575
+ command.subCommands = loaded.subCommands || cmd.subCommands;
574
576
  if ("resource" in loaded && loaded.resource) command.resource = loaded.resource;
575
577
  } else throw new TypeError(`Cannot resolve command: ${cmd.name || name}`);
576
578
  }
@@ -596,6 +598,24 @@ function log(...args) {
596
598
  console.log(...args);
597
599
  }
598
600
  /**
601
+ * Get the sub-commands of a command as a normalized Map.
602
+ *
603
+ * @param cmd - A command or lazy command
604
+ * @returns A Map of sub-commands, or undefined if the command has no sub-commands.
605
+ */
606
+ function getCommandSubCommands(cmd) {
607
+ const subCommands = isLazyCommand(cmd) ? cmd.subCommands : typeof cmd === "object" ? cmd.subCommands : void 0;
608
+ if (!subCommands) return;
609
+ if (subCommands instanceof Map) return subCommands.size > 0 ? subCommands : void 0;
610
+ if (typeof subCommands === "object") {
611
+ const entries = Object.entries(subCommands);
612
+ if (entries.length === 0) return;
613
+ const map = /* @__PURE__ */ new Map();
614
+ for (const [name, cmd] of entries) map.set(name, cmd);
615
+ return map;
616
+ }
617
+ }
618
+ /**
599
619
  * Deep freeze an object, making it immutable.
600
620
  *
601
621
  * @param obj - The object to freeze
@@ -635,7 +655,7 @@ function deepFreeze(obj, ignores = []) {
635
655
  * @param param - A {@link CommandContextParams | parameters} to create a command context.
636
656
  * @returns A {@link CommandContext | command context}, which is readonly.
637
657
  */
638
- async function createCommandContext({ args = {}, explicit = {}, values = {}, positionals = [], rest = [], argv = [], tokens = [], command = {}, extensions = {}, cliOptions = {}, callMode = "entry", omitted = false, validationError = void 0 }) {
658
+ async function createCommandContext({ args = {}, explicit = {}, values = {}, positionals = [], rest = [], argv = [], tokens = [], command = {}, extensions = {}, cliOptions = {}, callMode = "entry", commandPath = [], omitted = false, validationError = void 0 }) {
639
659
  /**
640
660
  * normailize the options schema and values, to avoid prototype pollution
641
661
  */
@@ -664,6 +684,7 @@ async function createCommandContext({ args = {}, explicit = {}, values = {}, pos
664
684
  description: command.description,
665
685
  omitted,
666
686
  callMode,
687
+ commandPath,
667
688
  env,
668
689
  args: _args,
669
690
  explicit,
@@ -881,16 +902,18 @@ async function cliCore(argv, entry, options, plugins) {
881
902
  const resolvedPlugins = await applyPlugins(pluginContext, [...plugins, ...options.plugins || []]);
882
903
  const cliOptions = normalizeCliOptions(options, decorators, pluginContext);
883
904
  const tokens = parseArgs(argv);
884
- const subCommand = getSubCommand(tokens);
885
- const { commandName: name, command, callMode } = resolveCommand(subCommand, entry, cliOptions);
905
+ const resolved = resolveCommandTree(tokens, entry, cliOptions);
906
+ const { commandName: name, command, callMode, commandPath, depth, levelSubCommands } = resolved;
886
907
  if (!command) throw new Error(`Command not found: ${name || ""}`);
887
908
  const args = resolveArguments(pluginContext, getCommandArgs(command));
909
+ const skipPositional = depth > 0 ? depth - 1 : -1;
888
910
  const { explicit, values, positionals, rest, error } = resolveArgs(args, tokens, {
889
911
  shortGrouping: true,
890
912
  toKebab: command.toKebab,
891
- skipPositional: callMode === "subCommand" && cliOptions.subCommands.size > 0 ? 0 : -1
913
+ skipPositional
892
914
  });
893
- const omitted = !subCommand;
915
+ const omitted = resolved.omitted;
916
+ if (levelSubCommands) cliOptions.subCommands = levelSubCommands;
894
917
  const resolvedCommand = isLazyCommand(command) ? await resolveLazyCommand(command, name, true) : command;
895
918
  return await executeCommand(resolvedCommand, await createCommandContext({
896
919
  args,
@@ -902,6 +925,7 @@ async function cliCore(argv, entry, options, plugins) {
902
925
  tokens,
903
926
  omitted,
904
927
  callMode,
928
+ commandPath,
905
929
  command: resolvedCommand,
906
930
  extensions: getPluginExtensions(resolvedPlugins),
907
931
  validationError: error,
@@ -938,9 +962,12 @@ function createInitialSubCommands(options, entryCmd) {
938
962
  const subCommands = new Map(options.subCommands instanceof Map ? options.subCommands : []);
939
963
  if (!(options.subCommands instanceof Map) && isObject(options.subCommands)) for (const [name, cmd] of Object.entries(options.subCommands)) subCommands.set(name, cmd);
940
964
  if (hasSubCommands) {
941
- if (isLazyCommand(entryCmd) || typeof entryCmd === "object") {
942
- entryCmd.entry = true;
943
- subCommands.set(resolveEntryName(entryCmd), entryCmd);
965
+ if (isLazyCommand(entryCmd)) {
966
+ const entryCopy = Object.assign((...args) => entryCmd(...args), entryCmd, { entry: true });
967
+ subCommands.set(resolveEntryName(entryCopy), entryCopy);
968
+ } else if (typeof entryCmd === "object") {
969
+ const entryCopy = Object.assign(create(), entryCmd, { entry: true });
970
+ subCommands.set(resolveEntryName(entryCopy), entryCopy);
944
971
  } else if (typeof entryCmd === "function") {
945
972
  const name = entryCmd.name || ANONYMOUS_COMMAND_NAME;
946
973
  subCommands.set(name, {
@@ -960,48 +987,102 @@ function normalizeCliOptions(options, decorators, pluginContext) {
960
987
  if (resolvedOptions.renderValidationErrors === void 0) resolvedOptions.renderValidationErrors = decorators.getValidationErrorsRenderer();
961
988
  return resolvedOptions;
962
989
  }
963
- function getSubCommand(tokens) {
964
- const firstToken = tokens[0];
965
- return firstToken && firstToken.kind === "positional" && firstToken.index === 0 && firstToken.value ? firstToken.value : "";
990
+ function getPositionalTokens(tokens) {
991
+ return tokens.filter((t) => t.kind === "positional").map((t) => t.value).filter((v) => !!v);
966
992
  }
967
- const CANNOT_RESOLVE_COMMAND = { callMode: "unexpected" };
968
- function resolveCommand(sub, entry, options) {
969
- const omitted = !sub;
970
- function doResolveCommand() {
993
+ function resolveCommandTree(tokens, entry, options) {
994
+ const positionals = getPositionalTokens(tokens);
995
+ function resolveAsEntry() {
971
996
  if (typeof entry === "function") if ("commandName" in entry && entry.commandName) return {
972
997
  commandName: entry.commandName,
973
998
  command: entry,
974
- callMode: "entry"
999
+ callMode: "entry",
1000
+ commandPath: [],
1001
+ depth: 0,
1002
+ omitted: options.subCommands.size > 0 && !positionals[0],
1003
+ levelSubCommands: options.subCommands.size > 0 ? options.subCommands : void 0
975
1004
  };
976
1005
  else return {
977
1006
  command: {
978
1007
  run: entry,
979
1008
  entry: true
980
1009
  },
981
- callMode: "entry"
1010
+ callMode: "entry",
1011
+ commandPath: [],
1012
+ depth: 0,
1013
+ omitted: options.subCommands.size > 0 && !positionals[0],
1014
+ levelSubCommands: options.subCommands.size > 0 ? options.subCommands : void 0
982
1015
  };
983
1016
  else if (typeof entry === "object") return {
984
1017
  commandName: resolveEntryName(entry),
985
1018
  command: entry,
986
- callMode: "entry"
1019
+ callMode: "entry",
1020
+ commandPath: [],
1021
+ depth: 0,
1022
+ omitted: options.subCommands.size > 0 && !positionals[0],
1023
+ levelSubCommands: options.subCommands.size > 0 ? options.subCommands : void 0
987
1024
  };
988
- else return CANNOT_RESOLVE_COMMAND;
989
- }
990
- if (omitted || options.subCommands?.size === 0) return doResolveCommand();
991
- const cmd = options.subCommands?.get(sub);
992
- if (cmd == null) {
993
- if (options.fallbackToEntry) return doResolveCommand();
994
- return {
995
- commandName: sub,
996
- callMode: "unexpected"
1025
+ else return {
1026
+ callMode: "unexpected",
1027
+ commandPath: [],
1028
+ depth: 0,
1029
+ omitted: false,
1030
+ levelSubCommands: void 0
997
1031
  };
998
1032
  }
999
- if (isLazyCommand(cmd) && cmd.commandName == null) cmd.commandName = sub;
1000
- else if (typeof cmd === "object" && cmd.name == null) cmd.name = sub;
1033
+ if (positionals.length === 0 || options.subCommands.size === 0) return resolveAsEntry();
1034
+ let currentSubCommands = options.subCommands;
1035
+ let resolvedCommand;
1036
+ let resolvedName;
1037
+ const commandPath = [];
1038
+ let depth = 0;
1039
+ for (let i = 0; i < positionals.length; i++) {
1040
+ const token = positionals[i];
1041
+ const cmd = currentSubCommands.get(token);
1042
+ if (cmd == null) {
1043
+ if (depth === 0) {
1044
+ if (options.fallbackToEntry) return resolveAsEntry();
1045
+ return {
1046
+ commandName: token,
1047
+ callMode: "unexpected",
1048
+ commandPath: [],
1049
+ depth: 0,
1050
+ omitted: false,
1051
+ levelSubCommands: void 0
1052
+ };
1053
+ }
1054
+ break;
1055
+ }
1056
+ let resolved = cmd;
1057
+ if (typeof cmd === "function" && cmd.commandName == null) resolved = Object.assign((...args) => cmd(...args), cmd, { commandName: token });
1058
+ else if (typeof cmd === "object" && cmd.name == null) resolved = Object.assign(create(), cmd, { name: token });
1059
+ resolvedCommand = resolved;
1060
+ resolvedName = token;
1061
+ commandPath.push(token);
1062
+ depth++;
1063
+ const nestedSubCommands = getCommandSubCommands(cmd);
1064
+ if (nestedSubCommands && nestedSubCommands.size > 0) currentSubCommands = nestedSubCommands;
1065
+ else break;
1066
+ }
1067
+ if (!resolvedCommand) return resolveAsEntry();
1068
+ const resolvedSubCommands = getCommandSubCommands(resolvedCommand);
1069
+ const omitted = resolvedSubCommands != null && resolvedSubCommands.size > 0;
1070
+ let levelSubCommands;
1071
+ if (omitted && resolvedSubCommands) {
1072
+ levelSubCommands = new Map(resolvedSubCommands);
1073
+ let entryCopy;
1074
+ if (typeof resolvedCommand === "function") entryCopy = Object.assign((...args) => resolvedCommand(...args), resolvedCommand, { entry: true });
1075
+ else entryCopy = Object.assign(create(), resolvedCommand, { entry: true });
1076
+ levelSubCommands.set(resolvedName || resolveEntryName(entryCopy), entryCopy);
1077
+ }
1001
1078
  return {
1002
- commandName: sub,
1003
- command: cmd,
1004
- callMode: "subCommand"
1079
+ commandName: resolvedName,
1080
+ command: resolvedCommand,
1081
+ callMode: depth > 0 ? "subCommand" : "entry",
1082
+ commandPath,
1083
+ depth,
1084
+ omitted,
1085
+ levelSubCommands
1005
1086
  };
1006
1087
  }
1007
1088
  function resolveEntryName(entry) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gunshi/bone",
3
3
  "description": "gunshi minimum",
4
- "version": "0.27.6",
4
+ "version": "0.28.0",
5
5
  "author": {
6
6
  "name": "kazuya kawaguchi",
7
7
  "email": "kawakazu80@gmail.com"
@@ -56,10 +56,10 @@
56
56
  "jsr-exports-lint": "^0.4.1",
57
57
  "publint": "^0.3.16",
58
58
  "tsdown": "0.15.12",
59
- "@gunshi/definition": "0.27.6",
60
- "@gunshi/plugin-global": "0.27.6",
61
- "@gunshi/plugin-renderer": "0.27.6",
62
- "gunshi": "0.27.6"
59
+ "@gunshi/definition": "0.28.0",
60
+ "@gunshi/plugin-global": "0.28.0",
61
+ "@gunshi/plugin-renderer": "0.28.0",
62
+ "gunshi": "0.28.0"
63
63
  },
64
64
  "scripts": {
65
65
  "build": "tsdown",