@gunshi/definition 1.0.0-rc.5 → 1.0.0-rc.6

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
@@ -9,6 +9,9 @@ This package exports the bellow APIs and types.
9
9
 
10
10
  - `define`: A function to define a command.
11
11
  - `lazy`: A function to lazily load a command.
12
+ - `CommandError` and `isCommandError`: An error for the failures that a command expects, printed without a stack trace, and its guard.
13
+ - `jsonArgs`: The argument that a command spreads into its `args` to support `--json`.
14
+ - `SubCommandable`: The type of a command to collect into the sub-commands, with or without `jsonArgs`.
12
15
  - Some basic type definitions, such as `Command`, `LazyCommand`, etc.
13
16
 
14
17
  <!-- eslint-disable markdown/no-missing-label-refs -->
package/lib/index.d.ts CHANGED
@@ -1,3 +1,12 @@
1
+ //#region ../gunshi/src/constants.d.ts
2
+ /**
3
+ * The key of the mark on the `json` argument of `jsonArgs`. A string key, not a symbol: `gunshi` and the other
4
+ * packages bundle their own copies of the types, and each copy of a `unique symbol` is a different type.
5
+ *
6
+ * @internal
7
+ */
8
+ declare const JSON_ARG = "~gunshi.json-arg";
9
+ //#endregion
1
10
  //#region ../../node_modules/.pnpm/args-tokens@1.0.2/node_modules/args-tokens/lib/parser-DvPxBZG9.d.ts
2
11
  /**
3
12
  * Entry point of argument parser.
@@ -730,7 +739,7 @@ interface PluginContext<G extends GunshiParamsConstraint = DefaultGunshiParams>
730
739
  * @param name - Command name
731
740
  * @param command - Command definition
732
741
  */
733
- addCommand(name: string, command: Command<G> | LazyCommand<G>): void;
742
+ addCommand(name: string, command: SubCommandable): void;
734
743
  /**
735
744
  * Check if a command exists.
736
745
  *
@@ -771,7 +780,7 @@ interface PluginContext<G extends GunshiParamsConstraint = DefaultGunshiParams>
771
780
  *
772
781
  * @param decorator - A decorator function that wraps the command runner
773
782
  */
774
- decorateCommand<L extends Record<string, unknown> = DefaultGunshiParams['extensions']>(decorator: (baseRunner: (ctx: Readonly<CommandContext<MergeGunshiExtensions<G, L>>>) => Awaitable<void | string>) => (ctx: Readonly<CommandContext<MergeGunshiExtensions<G, L>>>) => Awaitable<void | string>): void;
783
+ decorateCommand<L extends Record<string, unknown> = DefaultGunshiParams['extensions']>(decorator: (baseRunner: (ctx: Readonly<CommandContext<MergeGunshiExtensions<G, L>>>) => Awaitable<void | string | JsonResult>) => (ctx: Readonly<CommandContext<MergeGunshiExtensions<G, L>>>) => Awaitable<void | string | JsonResult>): void;
775
784
  }
776
785
  //#endregion
777
786
  //#region ../gunshi/src/plugin/core.d.ts
@@ -1008,7 +1017,11 @@ interface CommandEnvironment<G extends GunshiParamsConstraint = DefaultGunshiPar
1008
1017
  * (they are still returned as strings) and to silence {@linkcode CommandContext.log}.
1009
1018
  *
1010
1019
  * Output that must still appear — machine-readable results for an agent, warnings for the
1011
- * CLI author — should use `console.log` / `console.warn`, not `ctx.log`.
1020
+ * CLI author — should use `console.log` / `console.warn`, not `ctx.log`. A command that spreads
1021
+ * `jsonArgs` and is given `--json` prints its result, and its errors, as JSON even with it.
1022
+ *
1023
+ * The validation errors, and a `CommandError` that the command throws, are not printed either, so
1024
+ * `cli()` rejects with them, for the caller to report them.
1012
1025
  *
1013
1026
  * @default false
1014
1027
  * @see {@linkcode CliOptions.usageSilent}
@@ -1068,7 +1081,8 @@ interface CommandEnvironment<G extends GunshiParamsConstraint = DefaultGunshiPar
1068
1081
  */
1069
1082
  onBeforeCommand: ((ctx: Readonly<CommandContext<G>>) => Awaitable<void>) | undefined;
1070
1083
  /**
1071
- * Hook that runs after successful command execution
1084
+ * Hook that runs after successful command execution. It receives the result of the command only
1085
+ * when it is a string.
1072
1086
  *
1073
1087
  * @see {@linkcode CliOptions.onAfterCommand}
1074
1088
  * @since v0.27.0
@@ -1133,10 +1147,12 @@ interface CliOptions<G extends GunshiParamsConstraint = DefaultGunshiParams> {
1133
1147
  * (they are still returned as strings) and to silence {@linkcode CommandContext.log}.
1134
1148
  *
1135
1149
  * Output that must still appear — machine-readable results for an agent, warnings for the
1136
- * CLI author — should use `console.log` / `console.warn`, not `ctx.log`.
1150
+ * CLI author — should use `console.log` / `console.warn`, not `ctx.log`. A command that spreads
1151
+ * `jsonArgs` and is given `--json` prints its result, and its errors, as JSON even with it.
1137
1152
  *
1138
- * The validation errors are not printed either, so `cli()` rejects with them, for the caller to
1139
- * report them (see {@linkcode CliOptions.renderValidationErrors}).
1153
+ * The validation errors, and a `CommandError` that the command throws, are not printed either, so
1154
+ * `cli()` rejects with them, for the caller to report them (see
1155
+ * {@linkcode CliOptions.renderValidationErrors}). The exit code is left alone.
1140
1156
  *
1141
1157
  * @default false
1142
1158
  */
@@ -1148,6 +1164,10 @@ interface CliOptions<G extends GunshiParamsConstraint = DefaultGunshiParams> {
1148
1164
  * arguments or installed global options are reported as validation errors
1149
1165
  * before the command runner is executed.
1150
1166
  *
1167
+ * `--json` is not reported, since gunshi knows what it means: given to a command that does not
1168
+ * spread `jsonArgs`, the global options plugin reports `json_not_supported` for it instead, with or
1169
+ * without `strict`. `--json=false` asks for no JSON, and is reported.
1170
+ *
1151
1171
  * @default false
1152
1172
  */
1153
1173
  strict?: boolean;
@@ -1167,6 +1187,9 @@ interface CliOptions<G extends GunshiParamsConstraint = DefaultGunshiParams> {
1167
1187
  * Set it to `null` to report the validation errors yourself: nothing is printed, and `cli()` rejects
1168
1188
  * with the `AggregateError`. A renderer that returns an empty message, and
1169
1189
  * {@linkcode CliOptions.usageSilent}, also leave them to the caller.
1190
+ *
1191
+ * With `--json`, the plugin prints the validation errors as one line of JSON, and does not call
1192
+ * the renderer. `null` and {@linkcode CliOptions.usageSilent} still leave them to the caller.
1170
1193
  */
1171
1194
  renderValidationErrors?: ((ctx: Readonly<CommandContext<G>>, error: AggregateError) => Promise<string>) | null;
1172
1195
  /**
@@ -1193,15 +1216,17 @@ interface CliOptions<G extends GunshiParamsConstraint = DefaultGunshiParams> {
1193
1216
  * Hook that runs after successful command execution
1194
1217
  *
1195
1218
  * @param ctx - The command context
1196
- * @param result - The command execution result
1219
+ * @param result - The result of the command, when it is a string. The result of a command that
1220
+ * prints JSON with `--json` is printed, not passed, so it is `undefined`
1197
1221
  * @since v0.27.0
1198
1222
  */
1199
1223
  onAfterCommand?: (ctx: Readonly<CommandContext<G>>, result: string | undefined) => Awaitable<void>;
1200
1224
  /**
1201
1225
  * Hook that runs when a command throws an error, or when the arguments fail validation
1202
1226
  *
1203
- * For the validation errors that the global options plugin has printed, the exit code is already `1`
1204
- * when the hook runs, so the hook can set another one, and `cli()` resolves after it. Any other
1227
+ * For the validation errors and a `CommandError` that the global options plugin has printed, the
1228
+ * exit code is already set when the hook runs (`1` for the validation errors, and the `exitCode` of
1229
+ * a `CommandError`), so the hook can set another one, and `cli()` resolves after it. Any other
1205
1230
  * error rejects `cli()` after the hook.
1206
1231
  *
1207
1232
  * @param ctx - The command context
@@ -1301,9 +1326,11 @@ interface CommandContext<G extends GunshiParamsConstraint = DefaultGunshiParams>
1301
1326
  */
1302
1327
  toKebab?: boolean;
1303
1328
  /**
1304
- * Output a message.
1329
+ * Output a message for people.
1305
1330
  *
1306
- * If {@linkcode CommandEnvironment.usageSilent} is true, the message is not output.
1331
+ * If {@linkcode CommandEnvironment.usageSilent} is true, the message is not output. Nor is it when a
1332
+ * command that spreads `jsonArgs` is asked for JSON with `--json`, so that stdout has the JSON only:
1333
+ * the hooks and the plugins get the same function.
1307
1334
  *
1308
1335
  * @param message - an output message, see {@linkcode console.log}
1309
1336
  * @param optionalParams - an optional parameters, see {@linkcode console.log}
@@ -1547,9 +1574,40 @@ type CommandExamplesFetcher<G extends GunshiParamsConstraint = DefaultGunshiPara
1547
1574
  * @typeParam G - A type extending {@linkcode GunshiParams} to specify the shape of command context.
1548
1575
  *
1549
1576
  * @param ctx - A {@link CommandContext | command context}
1550
- * @returns void or string (for CLI output)
1577
+ * @returns The result of a command that spreads `jsonArgs`, an object keyed by a noun, which is printed
1578
+ * as JSON with `--json`. For any other command, a string, which `cli()` returns, or nothing. See
1579
+ * {@linkcode CommandRunnerResult}.
1580
+ */
1581
+ type CommandRunner<G extends GunshiParamsConstraint = DefaultGunshiParams> = (ctx: Readonly<CommandContext<G>>) => CommandRunnerResult<G>;
1582
+ /**
1583
+ * The result of a command that supports `--json`: an object keyed by a noun, such as
1584
+ * `{ project }` or `{ projects }`, so that fields can be added later. The global options plugin prints
1585
+ * it with `JSON.stringify`, so convert what JSON cannot hold, such as a `Map` or a `BigInt`, yourself.
1586
+ */
1587
+ type JsonResult = Record<string, unknown>;
1588
+ /**
1589
+ * Whether the arguments have the `json` of `jsonArgs`.
1590
+ *
1591
+ * @typeParam A - The arguments of a command
1592
+ */
1593
+ type HasJsonArgs<A> = [A] extends [{
1594
+ readonly json: {
1595
+ readonly [JSON_ARG]: true;
1596
+ };
1597
+ }] ? true : false;
1598
+ /**
1599
+ * What the runner of a command returns: its result for a command that spreads `jsonArgs`, otherwise a string or
1600
+ * nothing.
1601
+ *
1602
+ * @typeParam G - A type extending {@linkcode GunshiParams}
1551
1603
  */
1552
- type CommandRunner<G extends GunshiParamsConstraint = DefaultGunshiParams> = (ctx: Readonly<CommandContext<G>>) => Awaitable<string | void>;
1604
+ type CommandRunnerResult<G extends GunshiParamsConstraint = DefaultGunshiParams> = IsAnyArgs<ExtractArgs<G>> extends true ? Awaitable<string | void | JsonResult> : HasJsonArgs<ExtractArgs<G>> extends true ? Awaitable<JsonResult> : Awaitable<string | void>;
1605
+ /**
1606
+ * Whether the arguments are `any`, as in `CommandRunner<any>`. Then a runner may return anything it could before.
1607
+ *
1608
+ * @internal
1609
+ */
1610
+ type IsAnyArgs<A> = 0 extends 1 & A ? true : false;
1553
1611
  /**
1554
1612
  * Command loader.
1555
1613
  *
@@ -1585,7 +1643,7 @@ interface CommandContextParams<G extends GunshiParams | {
1585
1643
  args: Args;
1586
1644
  } | {
1587
1645
  extensions: ExtendContext;
1588
- }, V extends ArgValues<ExtractArgs<G>>, C extends Command<G> | LazyCommand<G> = Command<G>, E extends Record<string, CommandContextExtension> = Record<string, CommandContextExtension>> {
1646
+ }, V extends ArgValues<ExtractArgs<G>>, C extends Command<G> | LazyCommand<G> | SubCommandable = Command<G>, E extends Record<string, CommandContextExtension> = Record<string, CommandContextExtension>> {
1589
1647
  /**
1590
1648
  * An arguments of target command
1591
1649
  */
@@ -1649,7 +1707,144 @@ interface CommandContextParams<G extends GunshiParams | {
1649
1707
  * @param param - A {@link CommandContextParams | parameters} to create a command context.
1650
1708
  * @returns A {@link CommandContext | command context}, which is readonly.
1651
1709
  */
1652
- declare function createCommandContext<G extends GunshiParamsConstraint = DefaultGunshiParams, V extends ArgValues<ExtractArgs<G>> = ArgValues<ExtractArgs<G>>, C extends Command<G> | LazyCommand<G> = Command<G>, E extends Record<string, CommandContextExtension> = {}>({ args, explicit, values, positionals, rest, argv, tokens, command, extensions, cliOptions, callMode, commandPath, omitted, validationError }: CommandContextParams<G, V, C, E>): Promise<CommandContextResult<G, E>>;
1710
+ declare function createCommandContext<G extends GunshiParamsConstraint = DefaultGunshiParams, V extends ArgValues<ExtractArgs<G>> = ArgValues<ExtractArgs<G>>, C extends Command<G> | LazyCommand<G> | SubCommandable = Command<G>, E extends Record<string, CommandContextExtension> = {}>({ args, explicit, values, positionals, rest, argv, tokens, command, extensions, cliOptions, callMode, commandPath, omitted, validationError }: CommandContextParams<G, V, C, E>): Promise<CommandContextResult<G, E>>;
1711
+ //#endregion
1712
+ //#region ../gunshi/src/error.d.ts
1713
+ /**
1714
+ * Options for {@link CommandError}. All of them are optional.
1715
+ */
1716
+ type CommandErrorOptions = {
1717
+ /**
1718
+ * The name of the failure, for programs to read, such as `not_found`. snake_case is recommended.
1719
+ *
1720
+ * It is not printed to the user. With `--json`, it is the `reason` of the JSON error, as it is.
1721
+ */
1722
+ code?: string;
1723
+ /**
1724
+ * The exit code of the process, an integer from 1 to 255. Defaults to 1, and any other value is
1725
+ * replaced with 1, since the command has failed.
1726
+ */
1727
+ exitCode?: number;
1728
+ /**
1729
+ * The error that caused the failure.
1730
+ *
1731
+ * Its message is printed as is, on a `Caused by:` line after the message of the
1732
+ * {@link CommandError}, and so is the message of its own cause. With `--json`, the messages are in
1733
+ * `data.causes` of the JSON error. Do not pass an error whose message may contain a secret, such as
1734
+ * a token in a URL: write the message of the {@link CommandError} yourself instead.
1735
+ */
1736
+ cause?: unknown;
1737
+ };
1738
+ /**
1739
+ * Error for a failure that a command expects, such as a project that does not exist.
1740
+ *
1741
+ * When a command throws it from `run`, or a plugin throws it from a command decorator, the global
1742
+ * options plugin prints its message to stderr, followed by a `Caused by:` line for each cause,
1743
+ * without a stack trace. It sets the exit code of the process to {@link CommandError.exitCode},
1744
+ * and `cli()` resolves with `undefined`. `onErrorCommand` is still called with the error, after the
1745
+ * exit code is set, so the hook can change it.
1746
+ *
1747
+ * It is not printed, and `cli()` rejects with it, in these cases:
1748
+ *
1749
+ * - `usageSilent` is set, for the caller to report it
1750
+ * - it is thrown from a hook, while a plugin extends the command context, or while a lazy command
1751
+ * loads, outside the command decorators
1752
+ * - the CLI runs without the global options plugin, as `gunshi/bone` does
1753
+ *
1754
+ * Any other error thrown from a command is not printed either, and `cli()` rejects with it.
1755
+ *
1756
+ * Each package of gunshi (`gunshi`, `@gunshi/plugin`, `@gunshi/definition`) bundles its own copy of
1757
+ * this class, so `instanceof` does not match an error from another copy. Use
1758
+ * {@link isCommandError} to tell it.
1759
+ *
1760
+ * @example
1761
+ * ```js
1762
+ * import { CommandError, define } from 'gunshi'
1763
+ *
1764
+ * export default define({
1765
+ * name: 'show',
1766
+ * args: { project: { type: 'positional', description: 'Project name' } },
1767
+ * run: async ctx => {
1768
+ * const project = await findProject(ctx.values.project)
1769
+ * if (!project) {
1770
+ * throw new CommandError(`Project ${ctx.values.project} not found`, { code: 'not_found' })
1771
+ * }
1772
+ * ctx.log(project.description)
1773
+ * }
1774
+ * })
1775
+ * ```
1776
+ */
1777
+ declare class CommandError extends Error {
1778
+ /**
1779
+ * The name of the failure, for programs to read. See {@link CommandErrorOptions.code}.
1780
+ */
1781
+ readonly code?: string;
1782
+ /**
1783
+ * The exit code of the process, an integer from 1 to 255.
1784
+ */
1785
+ readonly exitCode: number;
1786
+ /**
1787
+ * Create an error for a failure that a command expects.
1788
+ *
1789
+ * @param message - The message printed to the user. It may have more than one line.
1790
+ * @param options - The code, the exit code and the cause of the failure
1791
+ */
1792
+ constructor(message: string, options?: CommandErrorOptions);
1793
+ }
1794
+ /**
1795
+ * Check whether an error is a {@link CommandError}.
1796
+ *
1797
+ * An error from another copy of gunshi is recognized through the `Symbol.for('gunshi.CommandError')`
1798
+ * brand. It is not recognized by `error.name`, so an error of the app that is also named
1799
+ * `CommandError` is not taken for one.
1800
+ *
1801
+ * @param error - An unknown error
1802
+ * @returns `true` if the error is a {@link CommandError}
1803
+ */
1804
+ declare function isCommandError(error: unknown): error is CommandError;
1805
+ //#endregion
1806
+ //#region ../gunshi/src/json.d.ts
1807
+ /**
1808
+ * @author kazuya kawaguchi (a.k.a. kazupon)
1809
+ * @license MIT
1810
+ */
1811
+ /**
1812
+ * The argument that a command spreads into its `args` to support `--json`.
1813
+ *
1814
+ * A command that spreads it returns its result from `run`, as an object keyed by a noun, and prints
1815
+ * the output for people with `ctx.log`. With `--json`, the global options plugin silences `ctx.log`,
1816
+ * prints the result as one JSON document to stdout, and prints a failure as one line of JSON to
1817
+ * stderr. `--json` given to a command that does not spread it stops the command before it runs, with
1818
+ * `json_not_supported`.
1819
+ *
1820
+ * The `json` argument has a mark, by which gunshi tells it from a `json` argument of the command's
1821
+ * own, which gunshi does not touch. To give it a short name, spread it:
1822
+ * `{ ...jsonArgs.json, short: 'j' }`. A lazy command spreads it into its definition too.
1823
+ *
1824
+ * @example
1825
+ * ```js
1826
+ * import { define, jsonArgs } from 'gunshi'
1827
+ *
1828
+ * export default define({
1829
+ * name: 'list',
1830
+ * args: { ...jsonArgs },
1831
+ * run: async ctx => {
1832
+ * const projects = await fetchProjects()
1833
+ * for (const project of projects) {
1834
+ * ctx.log(project.name)
1835
+ * }
1836
+ * return { projects }
1837
+ * }
1838
+ * })
1839
+ * ```
1840
+ */
1841
+ declare const jsonArgs: {
1842
+ readonly json: {
1843
+ readonly type: "boolean";
1844
+ readonly description: "Output the result as JSON";
1845
+ readonly "~gunshi.json-arg": true;
1846
+ };
1847
+ };
1653
1848
  //#endregion
1654
1849
  //#region ../gunshi/src/definition.d.ts
1655
1850
  /**
@@ -1748,6 +1943,15 @@ type DefineWithTypesReturn<DefaultExtensions extends ExtendContext, DefaultArgs
1748
1943
  * @since v0.27.0
1749
1944
  */
1750
1945
  declare function defineWithTypes<G extends GunshiParamsConstraint>(): DefineWithTypesReturn<ExtractExtensions<G>, ExtractArgs<G>>;
1946
+ /**
1947
+ * A definition that does not spread `jsonArgs`. A definition that does is checked by the overload that ties the
1948
+ * runner to its arguments.
1949
+ *
1950
+ * @internal
1951
+ */
1952
+ type WithoutJsonArgs<D> = D extends {
1953
+ args: infer A;
1954
+ } ? HasJsonArgs<A> extends true ? never : unknown : unknown;
1751
1955
  /**
1752
1956
  * Define a {@link LazyCommand | lazy command}.
1753
1957
  *
@@ -1827,7 +2031,7 @@ declare function lazy<D extends {
1827
2031
  * including when the lazy command is used as the CLI entry command.
1828
2032
  * @returns A {@link LazyCommand | lazy command} that can be executed later
1829
2033
  */
1830
- declare function lazy<G extends GunshiParamsConstraint = DefaultGunshiParams, D extends Partial<Command<G>> = Partial<Command<G>>>(loader: CommandLoader<G>, definition?: D): LazyCommand<G, D>;
2034
+ declare function lazy<G extends GunshiParamsConstraint = DefaultGunshiParams, D extends Partial<Command<G>> = Partial<Command<G>>>(loader: CommandLoader<G>, definition?: D & WithoutJsonArgs<D>): LazyCommand<G, D>;
1831
2035
  /**
1832
2036
  * Return type for lazyWithTypes
1833
2037
  *
@@ -1872,4 +2076,4 @@ type LazyWithTypesReturn<FullG extends GunshiParamsConstraint> = <D extends Part
1872
2076
  */
1873
2077
  declare function lazyWithTypes<G extends GunshiParamsConstraint>(): LazyWithTypesReturn<NormalizeToGunshiParams<G>>;
1874
2078
  //#endregion
1875
- export { type ArgSchema, type ArgValues, type Args, type Command, type CommandContextParams, type CommandLoader, type CommandRunner, type DefaultGunshiParams, type ExtendContext, type GunshiParams, type LazyCommand, createCommandContext, define, defineWithTypes, lazy, lazyWithTypes };
2079
+ export { type ArgSchema, type ArgValues, type Args, type Command, type CommandContextParams, CommandError, type CommandErrorOptions, type CommandLoader, type CommandRunner, type CommandRunnerResult, type DefaultGunshiParams, type ExtendContext, type GunshiParams, type JsonResult, type LazyCommand, type SubCommandable, createCommandContext, define, defineWithTypes, isCommandError, jsonArgs, lazy, lazyWithTypes };
package/lib/index.js CHANGED
@@ -26,6 +26,13 @@ const CLI_OPTIONS_DEFAULT = {
26
26
  plugins: void 0,
27
27
  fallbackToEntry: false
28
28
  };
29
+ /**
30
+ * The key of the mark on the `json` argument of `jsonArgs`. A string key, not a symbol: `gunshi` and the other
31
+ * packages bundle their own copies of the types, and each copy of a `unique symbol` is a different type.
32
+ *
33
+ * @internal
34
+ */
35
+ const JSON_ARG = "~gunshi.json-arg";
29
36
  //#endregion
30
37
  //#region ../gunshi/src/utils.ts
31
38
  /**
@@ -33,6 +40,51 @@ const CLI_OPTIONS_DEFAULT = {
33
40
  * @license MIT
34
41
  */
35
42
  /**
43
+ * Check whether an argument schema is the `json` argument of `jsonArgs`, by its mark.
44
+ *
45
+ * @param schema - An argument schema
46
+ * @returns `true` if the schema has the mark of `jsonArgs`
47
+ */
48
+ function isJsonArg(schema) {
49
+ return typeof schema === "object" && schema !== null && schema["~gunshi.json-arg"] === true;
50
+ }
51
+ /**
52
+ * Resolve whether `--json` asks a command for JSON.
53
+ *
54
+ * A command that spreads `jsonArgs` is asked for JSON when the resolved value of its `json` is `true`:
55
+ * with `--json`, or with a short name that the command gives it, but not with `--json=false`, nor with
56
+ * `--json` after `--`. The raw tokens are read only when they are given, and only when there is no
57
+ * value to read: when the command has no `json` argument, and when the core could not resolve the
58
+ * command and left the values empty. A `json` argument without the mark belongs to the command, and
59
+ * `--json` is never taken for a request then.
60
+ *
61
+ * @param args - The arguments of the command, the global options included
62
+ * @param values - The resolved values of the arguments
63
+ * @param tokens - The raw tokens of the arguments, to find a `--json` that no argument declares
64
+ * @returns How the command is asked for JSON, or `undefined` when it is not
65
+ */
66
+ function resolveJsonRequest(args, values, tokens) {
67
+ const schema = Object.hasOwn(args, "json") ? args.json : void 0;
68
+ if (schema === void 0) return tokens !== void 0 && hasJsonOption(tokens) ? "undeclared" : void 0;
69
+ if (!isJsonArg(schema)) return;
70
+ if (values.json !== void 0) return values.json === true ? "declared" : void 0;
71
+ return tokens !== void 0 && hasJsonOption(tokens) ? "declared" : void 0;
72
+ }
73
+ /**
74
+ * Check whether the raw tokens have a `--json` that asks for JSON: `--json` or `--json=true`, before
75
+ * `--`. Without a declaration, a short name of `--json` is not known.
76
+ *
77
+ * @param tokens - The raw tokens of the arguments
78
+ * @returns `true` if a `--json` asks for JSON
79
+ */
80
+ function hasJsonOption(tokens) {
81
+ for (const token of tokens) {
82
+ if (token.kind === "option-terminator") return false;
83
+ if (token.kind === "option" && token.rawName === "--json" && (token.value === void 0 || token.value === "true")) return true;
84
+ }
85
+ return false;
86
+ }
87
+ /**
36
88
  * Check if the given command is a {@link LazyCommand}.
37
89
  *
38
90
  * @param cmd - A command to check
@@ -124,6 +176,14 @@ async function createCommandContext({ args = {}, explicit = {}, values = {}, pos
124
176
  /**
125
177
  * create the command context
126
178
  */
179
+ /**
180
+ * NOTE(kazupon): a command that spreads `jsonArgs` and is asked for JSON prints its result as JSON, so
181
+ * the human output of `ctx.log` is silenced, as it is with `usageSilent`. The hooks and the decorators
182
+ * get the same context, so their `ctx.log` is silenced too. Only the resolved value of the `json`
183
+ * argument counts here, not the raw tokens: a command that does not support `--json` keeps its output,
184
+ * and the global options plugin reports the `--json` given to it.
185
+ */
186
+ const silent = cliOptions.usageSilent || resolveJsonRequest(_args, values) === "declared";
127
187
  const core = Object.assign(create(), {
128
188
  name: getCommandName(command),
129
189
  description: command.description,
@@ -139,7 +199,7 @@ async function createCommandContext({ args = {}, explicit = {}, values = {}, pos
139
199
  _: argv,
140
200
  tokens,
141
201
  toKebab: command.toKebab,
142
- log: cliOptions.usageSilent ? NOOP : log,
202
+ log: silent ? NOOP : log,
143
203
  validationError
144
204
  });
145
205
  /**
@@ -165,6 +225,192 @@ function getCommandName(cmd) {
165
225
  else if (typeof cmd === "object") return cmd.name || "(anonymous)";
166
226
  else return ANONYMOUS_COMMAND_NAME;
167
227
  }
228
+ /**
229
+ * forked from `nodejs/node` (`pkgjs/parseargs`)
230
+ * repository url: https://github.com/nodejs/node (https://github.com/pkgjs/parseargs)
231
+ * code url: https://github.com/nodejs/node/blob/main/lib/internal/util/parse_args/parse_args.js
232
+ *
233
+ * @author kazuya kawaguchi (a.k.a. kazupon)
234
+ * @license MIT
235
+ */
236
+ "-".codePointAt(0);
237
+ "=".codePointAt(0);
238
+ //#endregion
239
+ //#region ../../node_modules/.pnpm/args-tokens@1.0.2/node_modules/args-tokens/lib/resolver-Cx5Pw6-S.js
240
+ /**
241
+ * @author kazuya kawaguchi (a.k.a. kazupon)
242
+ * @license MIT
243
+ */
244
+ /**
245
+ * @author kazuya kawaguchi (a.k.a. kazupon)
246
+ * @license MIT
247
+ */
248
+ //#endregion
249
+ //#region ../../node_modules/.pnpm/args-tokens@1.0.2/node_modules/args-tokens/lib/index.js
250
+ /**
251
+ * @author kazuya kawaguchi (a.k.a. kazupon)
252
+ * @license MIT
253
+ */
254
+ /**
255
+ * @author kazuya kawaguchi (a.k.a. kazupon)
256
+ * @license MIT
257
+ */
258
+ //#endregion
259
+ //#region ../gunshi/src/error.ts
260
+ /**
261
+ * @author kazuya kawaguchi (a.k.a. kazupon)
262
+ * @license MIT
263
+ */
264
+ const COMMAND_ERROR_BRAND = Symbol.for("gunshi.CommandError");
265
+ /**
266
+ * Error for a failure that a command expects, such as a project that does not exist.
267
+ *
268
+ * When a command throws it from `run`, or a plugin throws it from a command decorator, the global
269
+ * options plugin prints its message to stderr, followed by a `Caused by:` line for each cause,
270
+ * without a stack trace. It sets the exit code of the process to {@link CommandError.exitCode},
271
+ * and `cli()` resolves with `undefined`. `onErrorCommand` is still called with the error, after the
272
+ * exit code is set, so the hook can change it.
273
+ *
274
+ * It is not printed, and `cli()` rejects with it, in these cases:
275
+ *
276
+ * - `usageSilent` is set, for the caller to report it
277
+ * - it is thrown from a hook, while a plugin extends the command context, or while a lazy command
278
+ * loads, outside the command decorators
279
+ * - the CLI runs without the global options plugin, as `gunshi/bone` does
280
+ *
281
+ * Any other error thrown from a command is not printed either, and `cli()` rejects with it.
282
+ *
283
+ * Each package of gunshi (`gunshi`, `@gunshi/plugin`, `@gunshi/definition`) bundles its own copy of
284
+ * this class, so `instanceof` does not match an error from another copy. Use
285
+ * {@link isCommandError} to tell it.
286
+ *
287
+ * @example
288
+ * ```js
289
+ * import { CommandError, define } from 'gunshi'
290
+ *
291
+ * export default define({
292
+ * name: 'show',
293
+ * args: { project: { type: 'positional', description: 'Project name' } },
294
+ * run: async ctx => {
295
+ * const project = await findProject(ctx.values.project)
296
+ * if (!project) {
297
+ * throw new CommandError(`Project ${ctx.values.project} not found`, { code: 'not_found' })
298
+ * }
299
+ * ctx.log(project.description)
300
+ * }
301
+ * })
302
+ * ```
303
+ */
304
+ var CommandError = class extends Error {
305
+ /**
306
+ * The name of the failure, for programs to read. See {@link CommandErrorOptions.code}.
307
+ */
308
+ code;
309
+ /**
310
+ * The exit code of the process, an integer from 1 to 255.
311
+ */
312
+ exitCode;
313
+ /**
314
+ * Create an error for a failure that a command expects.
315
+ *
316
+ * @param message - The message printed to the user. It may have more than one line.
317
+ * @param options - The code, the exit code and the cause of the failure
318
+ */
319
+ constructor(message, options = {}) {
320
+ super(message, { cause: options.cause });
321
+ this.name = "CommandError";
322
+ this.code = options.code;
323
+ this.exitCode = isFailureExitCode(options.exitCode) ? options.exitCode : 1;
324
+ Object.defineProperty(this, COMMAND_ERROR_BRAND, {
325
+ value: true,
326
+ enumerable: false,
327
+ writable: false,
328
+ configurable: false
329
+ });
330
+ }
331
+ };
332
+ /**
333
+ * Check whether an error is a {@link CommandError}.
334
+ *
335
+ * An error from another copy of gunshi is recognized through the `Symbol.for('gunshi.CommandError')`
336
+ * brand. It is not recognized by `error.name`, so an error of the app that is also named
337
+ * `CommandError` is not taken for one.
338
+ *
339
+ * @param error - An unknown error
340
+ * @returns `true` if the error is a {@link CommandError}
341
+ */
342
+ function isCommandError(error) {
343
+ if (error instanceof CommandError) return true;
344
+ if (!isRecord(error) || !Object.hasOwn(error, COMMAND_ERROR_BRAND) || error[COMMAND_ERROR_BRAND] !== true) return false;
345
+ return hasCommandErrorShape(error);
346
+ }
347
+ function isRecord(value) {
348
+ return typeof value === "object" && value !== null;
349
+ }
350
+ /**
351
+ * Check whether `code` is an own or inherited property that is a string or `undefined`.
352
+ *
353
+ * Constructors always assign `code`, even when the option is omitted.
354
+ *
355
+ * @param error - An error from another copy
356
+ * @returns `true` if `code` has the expected type
357
+ */
358
+ function hasOptionalStringCode(error) {
359
+ return "code" in error && (error.code === void 0 || typeof error.code === "string");
360
+ }
361
+ function hasCommandErrorShape(error) {
362
+ return hasOptionalStringCode(error) && typeof error.message === "string" && isFailureExitCode(error.exitCode);
363
+ }
364
+ /**
365
+ * Check whether a value is an exit code of a failure: an integer from 1 to 255.
366
+ *
367
+ * @param value - The value to check
368
+ * @returns `true` if the value is an exit code of a failure
369
+ */
370
+ function isFailureExitCode(value) {
371
+ return Number.isInteger(value) && value >= 1 && value <= 255;
372
+ }
373
+ //#endregion
374
+ //#region ../gunshi/src/json.ts
375
+ /**
376
+ * @author kazuya kawaguchi (a.k.a. kazupon)
377
+ * @license MIT
378
+ */
379
+ /**
380
+ * The argument that a command spreads into its `args` to support `--json`.
381
+ *
382
+ * A command that spreads it returns its result from `run`, as an object keyed by a noun, and prints
383
+ * the output for people with `ctx.log`. With `--json`, the global options plugin silences `ctx.log`,
384
+ * prints the result as one JSON document to stdout, and prints a failure as one line of JSON to
385
+ * stderr. `--json` given to a command that does not spread it stops the command before it runs, with
386
+ * `json_not_supported`.
387
+ *
388
+ * The `json` argument has a mark, by which gunshi tells it from a `json` argument of the command's
389
+ * own, which gunshi does not touch. To give it a short name, spread it:
390
+ * `{ ...jsonArgs.json, short: 'j' }`. A lazy command spreads it into its definition too.
391
+ *
392
+ * @example
393
+ * ```js
394
+ * import { define, jsonArgs } from 'gunshi'
395
+ *
396
+ * export default define({
397
+ * name: 'list',
398
+ * args: { ...jsonArgs },
399
+ * run: async ctx => {
400
+ * const projects = await fetchProjects()
401
+ * for (const project of projects) {
402
+ * ctx.log(project.name)
403
+ * }
404
+ * return { projects }
405
+ * }
406
+ * })
407
+ * ```
408
+ */
409
+ const jsonArgs = { json: {
410
+ type: "boolean",
411
+ description: "Output the result as JSON",
412
+ [JSON_ARG]: true
413
+ } };
168
414
  //#endregion
169
415
  //#region ../gunshi/src/definition.ts
170
416
  /**
@@ -288,4 +534,4 @@ function lazyWithTypes() {
288
534
  * @license MIT
289
535
  */
290
536
  //#endregion
291
- export { createCommandContext, define, defineWithTypes, lazy, lazyWithTypes };
537
+ export { CommandError, createCommandContext, define, defineWithTypes, isCommandError, jsonArgs, lazy, lazyWithTypes };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gunshi/definition",
3
3
  "description": "utilities for gunshi command definition",
4
- "version": "1.0.0-rc.5",
4
+ "version": "1.0.0-rc.6",
5
5
  "author": {
6
6
  "name": "kazuya kawaguchi",
7
7
  "email": "kawakazu80@gmail.com"
@@ -52,7 +52,7 @@
52
52
  }
53
53
  },
54
54
  "devDependencies": {
55
- "gunshi": "1.0.0-rc.5",
55
+ "gunshi": "1.0.0-rc.6",
56
56
  "publint": "^0.3.20",
57
57
  "tsdown": "0.22.14"
58
58
  },