@gunshi/definition 1.0.0-rc.4 → 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
@@ -2,7 +2,6 @@
2
2
 
3
3
  [![Version][npm-version-src]][npm-version-href]
4
4
  [![InstallSize][install-size-src]][install-size-src]
5
- [![JSR][jsr-src]][jsr-href]
6
5
 
7
6
  > utilities for gunshi command definition
8
7
 
@@ -10,6 +9,9 @@ This package exports the bellow APIs and types.
10
9
 
11
10
  - `define`: A function to define a command.
12
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`.
13
15
  - Some basic type definitions, such as `Command`, `LazyCommand`, etc.
14
16
 
15
17
  <!-- eslint-disable markdown/no-missing-label-refs -->
@@ -32,9 +34,6 @@ pnpm add @gunshi/definition
32
34
  ## yarn
33
35
  yarn add @gunshi/definition
34
36
 
35
- ## deno
36
- deno add jsr:@gunshi/definition
37
-
38
37
  ## bun
39
38
  bun add @gunshi/definition
40
39
  ```
@@ -84,6 +83,4 @@ About details, See the below official docs sections:
84
83
 
85
84
  [npm-version-src]: https://img.shields.io/npm/v/@gunshi/definition?style=flat
86
85
  [npm-version-href]: https://npmjs.com/package/@gunshi/definition@alpha
87
- [jsr-src]: https://jsr.io/badges/@gunshi/definition
88
- [jsr-href]: https://jsr.io/@gunshi/definition
89
86
  [install-size-src]: https://pkg-size.dev/badge/install/23122
package/lib/index.d.ts CHANGED
@@ -1,5 +1,13 @@
1
- //#region ../../node_modules/.pnpm/args-tokens@1.0.0/node_modules/args-tokens/lib/parser-DhESN1gR.d.ts
2
- //#region src/parser.d.ts
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
10
+ //#region ../../node_modules/.pnpm/args-tokens@1.0.2/node_modules/args-tokens/lib/parser-DvPxBZG9.d.ts
3
11
  /**
4
12
  * Entry point of argument parser.
5
13
  *
@@ -56,8 +64,7 @@ interface ArgToken {
56
64
  inlineValue?: boolean | undefined;
57
65
  }
58
66
  //#endregion
59
- //#region ../../node_modules/.pnpm/args-tokens@1.0.0/node_modules/args-tokens/lib/resolver.d.ts
60
- //#region src/resolver.d.ts
67
+ //#region ../../node_modules/.pnpm/args-tokens@1.0.2/node_modules/args-tokens/lib/resolver.d.ts
61
68
  /**
62
69
  * An argument schema definition for command-line argument parsing.
63
70
  *
@@ -419,9 +426,9 @@ interface ArgSchema {
419
426
  */
420
427
  toKebab?: true;
421
428
  /**
422
- * Names of other options that conflict with this option.
429
+ * Names of other arguments that conflict with this argument.
423
430
  *
424
- * When this option is used together with any of the conflicting options, the error is an
431
+ * When this argument is used together with any of the conflicting arguments, the error is an
425
432
  * `ArgResolveError` with type 'conflict' and the code `err:arg:conflict`
426
433
  * ({@link ArgsValidationErrorKeys}.conflict). Its `values` has the `displayName` and `name` of
427
434
  * the argument whose `conflicts` names the other one, and the `conflictDisplayName` and
@@ -431,9 +438,8 @@ interface ArgSchema {
431
438
  *
432
439
  * A positional argument can be on either side, such as `[file]` and `--stdin`. It conflicts when
433
440
  * it is given, and it is shown by its name, such as `'file'` (in kebab-case with `toKebab`), as
434
- * in its other errors. The message starts with the kind of the argument given as `name`:
435
- * `Positional argument 'file' conflicts with '--stdin'` or
436
- * `Optional argument '--stdin' conflicts with 'file'`.
441
+ * in its other errors. The message names the argument given as `name` first:
442
+ * `'file' conflicts with '--stdin'` or `'--stdin' conflicts with 'file'`.
437
443
  *
438
444
  * Conflicts only need to be defined on one side - if option A defines a conflict
439
445
  * with option B, the conflict is automatically detected when both are used,
@@ -472,7 +478,7 @@ interface ArgSchema {
472
478
  * }
473
479
  * }
474
480
  * // Usage: --summer --autumn reports an error
475
- * // Error: "Optional argument '--summer' conflicts with '--autumn'"
481
+ * // Error: "'--summer' conflicts with '--autumn'"
476
482
  * ```
477
483
  *
478
484
  * @example
@@ -512,7 +518,7 @@ interface ArgSchema {
512
518
  * toKebab: true // Accessible as --autumn-season
513
519
  * }
514
520
  * }
515
- * // Error: "Optional argument '--summer-season' conflicts with '--autumn-season'"
521
+ * // Error: "'--summer-season' conflicts with '--autumn-season'"
516
522
  * ```
517
523
  */
518
524
  conflicts?: string | string[];
@@ -733,7 +739,7 @@ interface PluginContext<G extends GunshiParamsConstraint = DefaultGunshiParams>
733
739
  * @param name - Command name
734
740
  * @param command - Command definition
735
741
  */
736
- addCommand(name: string, command: Command<G> | LazyCommand<G>): void;
742
+ addCommand(name: string, command: SubCommandable): void;
737
743
  /**
738
744
  * Check if a command exists.
739
745
  *
@@ -774,7 +780,7 @@ interface PluginContext<G extends GunshiParamsConstraint = DefaultGunshiParams>
774
780
  *
775
781
  * @param decorator - A decorator function that wraps the command runner
776
782
  */
777
- 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;
778
784
  }
779
785
  //#endregion
780
786
  //#region ../gunshi/src/plugin/core.d.ts
@@ -1011,7 +1017,11 @@ interface CommandEnvironment<G extends GunshiParamsConstraint = DefaultGunshiPar
1011
1017
  * (they are still returned as strings) and to silence {@linkcode CommandContext.log}.
1012
1018
  *
1013
1019
  * Output that must still appear — machine-readable results for an agent, warnings for the
1014
- * 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.
1015
1025
  *
1016
1026
  * @default false
1017
1027
  * @see {@linkcode CliOptions.usageSilent}
@@ -1071,7 +1081,8 @@ interface CommandEnvironment<G extends GunshiParamsConstraint = DefaultGunshiPar
1071
1081
  */
1072
1082
  onBeforeCommand: ((ctx: Readonly<CommandContext<G>>) => Awaitable<void>) | undefined;
1073
1083
  /**
1074
- * 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.
1075
1086
  *
1076
1087
  * @see {@linkcode CliOptions.onAfterCommand}
1077
1088
  * @since v0.27.0
@@ -1136,10 +1147,12 @@ interface CliOptions<G extends GunshiParamsConstraint = DefaultGunshiParams> {
1136
1147
  * (they are still returned as strings) and to silence {@linkcode CommandContext.log}.
1137
1148
  *
1138
1149
  * Output that must still appear — machine-readable results for an agent, warnings for the
1139
- * 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.
1140
1152
  *
1141
- * The validation errors are not printed either, so `cli()` rejects with them, for the caller to
1142
- * 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.
1143
1156
  *
1144
1157
  * @default false
1145
1158
  */
@@ -1151,6 +1164,10 @@ interface CliOptions<G extends GunshiParamsConstraint = DefaultGunshiParams> {
1151
1164
  * arguments or installed global options are reported as validation errors
1152
1165
  * before the command runner is executed.
1153
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
+ *
1154
1171
  * @default false
1155
1172
  */
1156
1173
  strict?: boolean;
@@ -1170,6 +1187,9 @@ interface CliOptions<G extends GunshiParamsConstraint = DefaultGunshiParams> {
1170
1187
  * Set it to `null` to report the validation errors yourself: nothing is printed, and `cli()` rejects
1171
1188
  * with the `AggregateError`. A renderer that returns an empty message, and
1172
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.
1173
1193
  */
1174
1194
  renderValidationErrors?: ((ctx: Readonly<CommandContext<G>>, error: AggregateError) => Promise<string>) | null;
1175
1195
  /**
@@ -1196,15 +1216,17 @@ interface CliOptions<G extends GunshiParamsConstraint = DefaultGunshiParams> {
1196
1216
  * Hook that runs after successful command execution
1197
1217
  *
1198
1218
  * @param ctx - The command context
1199
- * @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`
1200
1221
  * @since v0.27.0
1201
1222
  */
1202
1223
  onAfterCommand?: (ctx: Readonly<CommandContext<G>>, result: string | undefined) => Awaitable<void>;
1203
1224
  /**
1204
1225
  * Hook that runs when a command throws an error, or when the arguments fail validation
1205
1226
  *
1206
- * For the validation errors that the global options plugin has printed, the exit code is already `1`
1207
- * 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
1208
1230
  * error rejects `cli()` after the hook.
1209
1231
  *
1210
1232
  * @param ctx - The command context
@@ -1304,9 +1326,11 @@ interface CommandContext<G extends GunshiParamsConstraint = DefaultGunshiParams>
1304
1326
  */
1305
1327
  toKebab?: boolean;
1306
1328
  /**
1307
- * Output a message.
1329
+ * Output a message for people.
1308
1330
  *
1309
- * 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.
1310
1334
  *
1311
1335
  * @param message - an output message, see {@linkcode console.log}
1312
1336
  * @param optionalParams - an optional parameters, see {@linkcode console.log}
@@ -1550,9 +1574,40 @@ type CommandExamplesFetcher<G extends GunshiParamsConstraint = DefaultGunshiPara
1550
1574
  * @typeParam G - A type extending {@linkcode GunshiParams} to specify the shape of command context.
1551
1575
  *
1552
1576
  * @param ctx - A {@link CommandContext | command context}
1553
- * @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}
1554
1603
  */
1555
- 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;
1556
1611
  /**
1557
1612
  * Command loader.
1558
1613
  *
@@ -1588,7 +1643,7 @@ interface CommandContextParams<G extends GunshiParams | {
1588
1643
  args: Args;
1589
1644
  } | {
1590
1645
  extensions: ExtendContext;
1591
- }, 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>> {
1592
1647
  /**
1593
1648
  * An arguments of target command
1594
1649
  */
@@ -1652,7 +1707,144 @@ interface CommandContextParams<G extends GunshiParams | {
1652
1707
  * @param param - A {@link CommandContextParams | parameters} to create a command context.
1653
1708
  * @returns A {@link CommandContext | command context}, which is readonly.
1654
1709
  */
1655
- 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
+ };
1656
1848
  //#endregion
1657
1849
  //#region ../gunshi/src/definition.d.ts
1658
1850
  /**
@@ -1751,6 +1943,15 @@ type DefineWithTypesReturn<DefaultExtensions extends ExtendContext, DefaultArgs
1751
1943
  * @since v0.27.0
1752
1944
  */
1753
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;
1754
1955
  /**
1755
1956
  * Define a {@link LazyCommand | lazy command}.
1756
1957
  *
@@ -1830,7 +2031,7 @@ declare function lazy<D extends {
1830
2031
  * including when the lazy command is used as the CLI entry command.
1831
2032
  * @returns A {@link LazyCommand | lazy command} that can be executed later
1832
2033
  */
1833
- 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>;
1834
2035
  /**
1835
2036
  * Return type for lazyWithTypes
1836
2037
  *
@@ -1875,4 +2076,4 @@ type LazyWithTypesReturn<FullG extends GunshiParamsConstraint> = <D extends Part
1875
2076
  */
1876
2077
  declare function lazyWithTypes<G extends GunshiParamsConstraint>(): LazyWithTypesReturn<NormalizeToGunshiParams<G>>;
1877
2078
  //#endregion
1878
- 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.4",
4
+ "version": "1.0.0-rc.6",
5
5
  "author": {
6
6
  "name": "kazuya kawaguchi",
7
7
  "email": "kawakazu80@gmail.com"
@@ -52,16 +52,11 @@
52
52
  }
53
53
  },
54
54
  "devDependencies": {
55
- "deno": "^2.7.14",
56
- "gunshi": "1.0.0-rc.4",
57
- "jsr": "^0.14.3",
58
- "jsr-exports-lint": "^0.4.2",
55
+ "gunshi": "1.0.0-rc.6",
59
56
  "publint": "^0.3.20",
60
57
  "tsdown": "0.22.14"
61
58
  },
62
59
  "scripts": {
63
- "build": "tsdown",
64
- "lint:jsr": "jsr publish --dry-run --allow-dirty",
65
- "typecheck:deno": "deno check --import-map=../../importmap.json ./src"
60
+ "build": "tsdown"
66
61
  }
67
62
  }