@gunshi/plugin 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
  > plugin development kit for gunshi
8
7
 
@@ -26,6 +25,4 @@ About details, See [the plugin development guides](https://gunshi.dev/guide/plug
26
25
 
27
26
  [npm-version-src]: https://img.shields.io/npm/v/@gunshi/plugin?style=flat
28
27
  [npm-version-href]: https://npmjs.com/package/@gunshi/plugin@alpha
29
- [jsr-src]: https://jsr.io/badges/@gunshi/plugin
30
- [jsr-href]: https://jsr.io/@gunshi/plugin
31
28
  [install-size-src]: https://pkg-size.dev/badge/install/27672
package/lib/index.d.ts CHANGED
@@ -1,5 +1,4 @@
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 ../../node_modules/.pnpm/args-tokens@1.0.2/node_modules/args-tokens/lib/parser-DvPxBZG9.d.ts
3
2
  /**
4
3
  * Entry point of argument parser.
5
4
  *
@@ -56,8 +55,7 @@ interface ArgToken {
56
55
  inlineValue?: boolean | undefined;
57
56
  }
58
57
  //#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
58
+ //#region ../../node_modules/.pnpm/args-tokens@1.0.2/node_modules/args-tokens/lib/resolver.d.ts
61
59
  /**
62
60
  * An argument schema definition for command-line argument parsing.
63
61
  *
@@ -419,9 +417,9 @@ interface ArgSchema {
419
417
  */
420
418
  toKebab?: true;
421
419
  /**
422
- * Names of other options that conflict with this option.
420
+ * Names of other arguments that conflict with this argument.
423
421
  *
424
- * When this option is used together with any of the conflicting options, the error is an
422
+ * When this argument is used together with any of the conflicting arguments, the error is an
425
423
  * `ArgResolveError` with type 'conflict' and the code `err:arg:conflict`
426
424
  * ({@link ArgsValidationErrorKeys}.conflict). Its `values` has the `displayName` and `name` of
427
425
  * the argument whose `conflicts` names the other one, and the `conflictDisplayName` and
@@ -431,9 +429,8 @@ interface ArgSchema {
431
429
  *
432
430
  * A positional argument can be on either side, such as `[file]` and `--stdin`. It conflicts when
433
431
  * 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'`.
432
+ * in its other errors. The message names the argument given as `name` first:
433
+ * `'file' conflicts with '--stdin'` or `'--stdin' conflicts with 'file'`.
437
434
  *
438
435
  * Conflicts only need to be defined on one side - if option A defines a conflict
439
436
  * with option B, the conflict is automatically detected when both are used,
@@ -472,7 +469,7 @@ interface ArgSchema {
472
469
  * }
473
470
  * }
474
471
  * // Usage: --summer --autumn reports an error
475
- * // Error: "Optional argument '--summer' conflicts with '--autumn'"
472
+ * // Error: "'--summer' conflicts with '--autumn'"
476
473
  * ```
477
474
  *
478
475
  * @example
@@ -512,7 +509,7 @@ interface ArgSchema {
512
509
  * toKebab: true // Accessible as --autumn-season
513
510
  * }
514
511
  * }
515
- * // Error: "Optional argument '--summer-season' conflicts with '--autumn-season'"
512
+ * // Error: "'--summer-season' conflicts with '--autumn-season'"
516
513
  * ```
517
514
  */
518
515
  conflicts?: string | string[];
@@ -796,7 +793,7 @@ interface PluginContext<G extends GunshiParamsConstraint = DefaultGunshiParams>
796
793
  * @param name - Command name
797
794
  * @param command - Command definition
798
795
  */
799
- addCommand(name: string, command: Command<G> | LazyCommand<G>): void;
796
+ addCommand(name: string, command: SubCommandable): void;
800
797
  /**
801
798
  * Check if a command exists.
802
799
  *
@@ -837,7 +834,7 @@ interface PluginContext<G extends GunshiParamsConstraint = DefaultGunshiParams>
837
834
  *
838
835
  * @param decorator - A decorator function that wraps the command runner
839
836
  */
840
- 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;
837
+ 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;
841
838
  }
842
839
  //#endregion
843
840
  //#region ../gunshi/src/plugin/core.d.ts
@@ -1274,7 +1271,11 @@ interface CommandEnvironment<G extends GunshiParamsConstraint = DefaultGunshiPar
1274
1271
  * (they are still returned as strings) and to silence {@linkcode CommandContext.log}.
1275
1272
  *
1276
1273
  * Output that must still appear — machine-readable results for an agent, warnings for the
1277
- * CLI author — should use `console.log` / `console.warn`, not `ctx.log`.
1274
+ * CLI author — should use `console.log` / `console.warn`, not `ctx.log`. A command that spreads
1275
+ * `jsonArgs` and is given `--json` prints its result, and its errors, as JSON even with it.
1276
+ *
1277
+ * The validation errors, and a `CommandError` that the command throws, are not printed either, so
1278
+ * `cli()` rejects with them, for the caller to report them.
1278
1279
  *
1279
1280
  * @default false
1280
1281
  * @see {@linkcode CliOptions.usageSilent}
@@ -1334,7 +1335,8 @@ interface CommandEnvironment<G extends GunshiParamsConstraint = DefaultGunshiPar
1334
1335
  */
1335
1336
  onBeforeCommand: ((ctx: Readonly<CommandContext<G>>) => Awaitable<void>) | undefined;
1336
1337
  /**
1337
- * Hook that runs after successful command execution
1338
+ * Hook that runs after successful command execution. It receives the result of the command only
1339
+ * when it is a string.
1338
1340
  *
1339
1341
  * @see {@linkcode CliOptions.onAfterCommand}
1340
1342
  * @since v0.27.0
@@ -1399,10 +1401,12 @@ interface CliOptions<G extends GunshiParamsConstraint = DefaultGunshiParams> {
1399
1401
  * (they are still returned as strings) and to silence {@linkcode CommandContext.log}.
1400
1402
  *
1401
1403
  * Output that must still appear — machine-readable results for an agent, warnings for the
1402
- * CLI author — should use `console.log` / `console.warn`, not `ctx.log`.
1404
+ * CLI author — should use `console.log` / `console.warn`, not `ctx.log`. A command that spreads
1405
+ * `jsonArgs` and is given `--json` prints its result, and its errors, as JSON even with it.
1403
1406
  *
1404
- * The validation errors are not printed either, so `cli()` rejects with them, for the caller to
1405
- * report them (see {@linkcode CliOptions.renderValidationErrors}).
1407
+ * The validation errors, and a `CommandError` that the command throws, are not printed either, so
1408
+ * `cli()` rejects with them, for the caller to report them (see
1409
+ * {@linkcode CliOptions.renderValidationErrors}). The exit code is left alone.
1406
1410
  *
1407
1411
  * @default false
1408
1412
  */
@@ -1414,6 +1418,10 @@ interface CliOptions<G extends GunshiParamsConstraint = DefaultGunshiParams> {
1414
1418
  * arguments or installed global options are reported as validation errors
1415
1419
  * before the command runner is executed.
1416
1420
  *
1421
+ * `--json` is not reported, since gunshi knows what it means: given to a command that does not
1422
+ * spread `jsonArgs`, the global options plugin reports `json_not_supported` for it instead, with or
1423
+ * without `strict`. `--json=false` asks for no JSON, and is reported.
1424
+ *
1417
1425
  * @default false
1418
1426
  */
1419
1427
  strict?: boolean;
@@ -1433,6 +1441,9 @@ interface CliOptions<G extends GunshiParamsConstraint = DefaultGunshiParams> {
1433
1441
  * Set it to `null` to report the validation errors yourself: nothing is printed, and `cli()` rejects
1434
1442
  * with the `AggregateError`. A renderer that returns an empty message, and
1435
1443
  * {@linkcode CliOptions.usageSilent}, also leave them to the caller.
1444
+ *
1445
+ * With `--json`, the plugin prints the validation errors as one line of JSON, and does not call
1446
+ * the renderer. `null` and {@linkcode CliOptions.usageSilent} still leave them to the caller.
1436
1447
  */
1437
1448
  renderValidationErrors?: ((ctx: Readonly<CommandContext<G>>, error: AggregateError) => Promise<string>) | null;
1438
1449
  /**
@@ -1459,15 +1470,17 @@ interface CliOptions<G extends GunshiParamsConstraint = DefaultGunshiParams> {
1459
1470
  * Hook that runs after successful command execution
1460
1471
  *
1461
1472
  * @param ctx - The command context
1462
- * @param result - The command execution result
1473
+ * @param result - The result of the command, when it is a string. The result of a command that
1474
+ * prints JSON with `--json` is printed, not passed, so it is `undefined`
1463
1475
  * @since v0.27.0
1464
1476
  */
1465
1477
  onAfterCommand?: (ctx: Readonly<CommandContext<G>>, result: string | undefined) => Awaitable<void>;
1466
1478
  /**
1467
1479
  * Hook that runs when a command throws an error, or when the arguments fail validation
1468
1480
  *
1469
- * For the validation errors that the global options plugin has printed, the exit code is already `1`
1470
- * when the hook runs, so the hook can set another one, and `cli()` resolves after it. Any other
1481
+ * For the validation errors and a `CommandError` that the global options plugin has printed, the
1482
+ * exit code is already set when the hook runs (`1` for the validation errors, and the `exitCode` of
1483
+ * a `CommandError`), so the hook can set another one, and `cli()` resolves after it. Any other
1471
1484
  * error rejects `cli()` after the hook.
1472
1485
  *
1473
1486
  * @param ctx - The command context
@@ -1567,9 +1580,11 @@ interface CommandContext<G extends GunshiParamsConstraint = DefaultGunshiParams>
1567
1580
  */
1568
1581
  toKebab?: boolean;
1569
1582
  /**
1570
- * Output a message.
1583
+ * Output a message for people.
1571
1584
  *
1572
- * If {@linkcode CommandEnvironment.usageSilent} is true, the message is not output.
1585
+ * If {@linkcode CommandEnvironment.usageSilent} is true, the message is not output. Nor is it when a
1586
+ * command that spreads `jsonArgs` is asked for JSON with `--json`, so that stdout has the JSON only:
1587
+ * the hooks and the plugins get the same function.
1573
1588
  *
1574
1589
  * @param message - an output message, see {@linkcode console.log}
1575
1590
  * @param optionalParams - an optional parameters, see {@linkcode console.log}
@@ -1813,14 +1828,49 @@ type CommandExamplesFetcher<G extends GunshiParamsConstraint = DefaultGunshiPara
1813
1828
  * @typeParam G - A type extending {@linkcode GunshiParams} to specify the shape of command context.
1814
1829
  *
1815
1830
  * @param ctx - A {@link CommandContext | command context}
1816
- * @returns void or string (for CLI output)
1831
+ * @returns The result of a command that spreads `jsonArgs`, an object keyed by a noun, which is printed
1832
+ * as JSON with `--json`. For any other command, a string, which `cli()` returns, or nothing. See
1833
+ * {@linkcode CommandRunnerResult}.
1834
+ */
1835
+ type CommandRunner<G extends GunshiParamsConstraint = DefaultGunshiParams> = (ctx: Readonly<CommandContext<G>>) => CommandRunnerResult<G>;
1836
+ /**
1837
+ * The result of a command that supports `--json`: an object keyed by a noun, such as
1838
+ * `{ project }` or `{ projects }`, so that fields can be added later. The global options plugin prints
1839
+ * it with `JSON.stringify`, so convert what JSON cannot hold, such as a `Map` or a `BigInt`, yourself.
1840
+ */
1841
+ type JsonResult = Record<string, unknown>;
1842
+ /**
1843
+ * Whether the arguments have the `json` of `jsonArgs`.
1844
+ *
1845
+ * @typeParam A - The arguments of a command
1846
+ */
1847
+ type HasJsonArgs<A> = [A] extends [{
1848
+ readonly json: {
1849
+ readonly [JSON_ARG]: true;
1850
+ };
1851
+ }] ? true : false;
1852
+ /**
1853
+ * What the runner of a command returns: its result for a command that spreads `jsonArgs`, otherwise a string or
1854
+ * nothing.
1855
+ *
1856
+ * @typeParam G - A type extending {@linkcode GunshiParams}
1817
1857
  */
1818
- type CommandRunner<G extends GunshiParamsConstraint = DefaultGunshiParams> = (ctx: Readonly<CommandContext<G>>) => Awaitable<string | void>;
1858
+ 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>;
1859
+ /**
1860
+ * Whether the arguments are `any`, as in `CommandRunner<any>`. Then a runner may return anything it could before.
1861
+ *
1862
+ * @internal
1863
+ */
1864
+ type IsAnyArgs<A> = 0 extends 1 & A ? true : false;
1819
1865
  /**
1820
1866
  * Command decorator.
1821
1867
  *
1822
1868
  * A function that wraps a command runner to add or modify its behavior.
1823
1869
  *
1870
+ * The decorated runner returns what the base runner returns, as it is: the result of a command that
1871
+ * spreads `jsonArgs` must reach the global options plugin, which prints it as JSON with `--json`. A
1872
+ * decorator prints with `ctx.log`, which is silenced then.
1873
+ *
1824
1874
  * @typeParam G - A type extending {@linkcode GunshiParams} to specify the shape of command context.
1825
1875
  *
1826
1876
  * @param baseRunner - The base command runner to decorate
@@ -1828,7 +1878,7 @@ type CommandRunner<G extends GunshiParamsConstraint = DefaultGunshiParams> = (ct
1828
1878
  *
1829
1879
  * @since v0.27.0
1830
1880
  */
1831
- type CommandDecorator<G extends GunshiParamsConstraint = DefaultGunshiParams> = (baseRunner: (ctx: Readonly<CommandContext<G>>) => Awaitable<string | void>) => (ctx: Readonly<CommandContext<G>>) => Awaitable<string | void>;
1881
+ type CommandDecorator<G extends GunshiParamsConstraint = DefaultGunshiParams> = (baseRunner: (ctx: Readonly<CommandContext<G>>) => Awaitable<string | void | JsonResult>) => (ctx: Readonly<CommandContext<G>>) => Awaitable<string | void | JsonResult>;
1832
1882
  /**
1833
1883
  * Renderer decorator type.
1834
1884
  *
@@ -1862,6 +1912,13 @@ type ValidationErrorsDecorator<G extends GunshiParamsConstraint = DefaultGunshiP
1862
1912
  //#region ../gunshi/src/constants.d.ts
1863
1913
  declare const ANONYMOUS_COMMAND_NAME = "(anonymous)";
1864
1914
  declare const CLI_OPTIONS_DEFAULT: CliOptions<DefaultGunshiParams>;
1915
+ /**
1916
+ * The key of the mark on the `json` argument of `jsonArgs`. A string key, not a symbol: `gunshi` and the other
1917
+ * packages bundle their own copies of the types, and each copy of a `unique symbol` is a different type.
1918
+ *
1919
+ * @internal
1920
+ */
1921
+ declare const JSON_ARG = "~gunshi.json-arg";
1865
1922
  //#endregion
1866
1923
  //#region ../gunshi/src/context.d.ts
1867
1924
  /**
@@ -1886,7 +1943,7 @@ interface CommandContextParams<G extends GunshiParams | {
1886
1943
  args: Args;
1887
1944
  } | {
1888
1945
  extensions: ExtendContext;
1889
- }, V extends ArgValues<ExtractArgs<G>>, C extends Command<G> | LazyCommand<G> = Command<G>, E extends Record<string, CommandContextExtension> = Record<string, CommandContextExtension>> {
1946
+ }, V extends ArgValues<ExtractArgs<G>>, C extends Command<G> | LazyCommand<G> | SubCommandable = Command<G>, E extends Record<string, CommandContextExtension> = Record<string, CommandContextExtension>> {
1890
1947
  /**
1891
1948
  * An arguments of target command
1892
1949
  */
@@ -1950,7 +2007,7 @@ interface CommandContextParams<G extends GunshiParams | {
1950
2007
  * @param param - A {@link CommandContextParams | parameters} to create a command context.
1951
2008
  * @returns A {@link CommandContext | command context}, which is readonly.
1952
2009
  */
1953
- 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>>;
2010
+ 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>>;
1954
2011
  //#endregion
1955
2012
  //#region ../gunshi/src/error.d.ts
1956
2013
  /**
@@ -1977,6 +2034,18 @@ declare const CommandResolutionErrorKeys: {
1977
2034
  };
1978
2035
  /** Command resolution error code. */
1979
2036
  type CommandResolutionErrorCode = (typeof CommandResolutionErrorKeys)[keyof typeof CommandResolutionErrorKeys];
2037
+ /**
2038
+ * The resource keys of the errors of the JSON output, which are the codes of {@link JsonOutputError}.
2039
+ */
2040
+ declare const JsonOutputErrorKeys: {
2041
+ readonly notSupported: "err:json:not-supported";
2042
+ readonly invalidResult: "err:json:invalid-result";
2043
+ readonly unserializableResult: "err:json:unserializable-result";
2044
+ };
2045
+ /**
2046
+ * The code of a {@link JsonOutputError}.
2047
+ */
2048
+ type JsonOutputErrorCode = (typeof JsonOutputErrorKeys)[keyof typeof JsonOutputErrorKeys];
1980
2049
  /**
1981
2050
  * Options for {@link CommandNotFoundError}.
1982
2051
  */
@@ -2019,6 +2088,31 @@ type CommandResolutionErrorOptions = {
2019
2088
  /** Underlying cause. */
2020
2089
  cause?: unknown;
2021
2090
  };
2091
+ /**
2092
+ * Options for {@link CommandError}. All of them are optional.
2093
+ */
2094
+ type CommandErrorOptions = {
2095
+ /**
2096
+ * The name of the failure, for programs to read, such as `not_found`. snake_case is recommended.
2097
+ *
2098
+ * It is not printed to the user. With `--json`, it is the `reason` of the JSON error, as it is.
2099
+ */
2100
+ code?: string;
2101
+ /**
2102
+ * The exit code of the process, an integer from 1 to 255. Defaults to 1, and any other value is
2103
+ * replaced with 1, since the command has failed.
2104
+ */
2105
+ exitCode?: number;
2106
+ /**
2107
+ * The error that caused the failure.
2108
+ *
2109
+ * Its message is printed as is, on a `Caused by:` line after the message of the
2110
+ * {@link CommandError}, and so is the message of its own cause. With `--json`, the messages are in
2111
+ * `data.causes` of the JSON error. Do not pass an error whose message may contain a secret, such as
2112
+ * a token in a URL: write the message of the {@link CommandError} yourself instead.
2113
+ */
2114
+ cause?: unknown;
2115
+ };
2022
2116
  /**
2023
2117
  * Error raised when a command cannot be resolved.
2024
2118
  *
@@ -2060,6 +2154,62 @@ declare class CommandResolutionError extends Error {
2060
2154
  */
2061
2155
  constructor(message: string, options?: CommandResolutionErrorOptions);
2062
2156
  }
2157
+ /**
2158
+ * Error for a failure that a command expects, such as a project that does not exist.
2159
+ *
2160
+ * When a command throws it from `run`, or a plugin throws it from a command decorator, the global
2161
+ * options plugin prints its message to stderr, followed by a `Caused by:` line for each cause,
2162
+ * without a stack trace. It sets the exit code of the process to {@link CommandError.exitCode},
2163
+ * and `cli()` resolves with `undefined`. `onErrorCommand` is still called with the error, after the
2164
+ * exit code is set, so the hook can change it.
2165
+ *
2166
+ * It is not printed, and `cli()` rejects with it, in these cases:
2167
+ *
2168
+ * - `usageSilent` is set, for the caller to report it
2169
+ * - it is thrown from a hook, while a plugin extends the command context, or while a lazy command
2170
+ * loads, outside the command decorators
2171
+ * - the CLI runs without the global options plugin, as `gunshi/bone` does
2172
+ *
2173
+ * Any other error thrown from a command is not printed either, and `cli()` rejects with it.
2174
+ *
2175
+ * Each package of gunshi (`gunshi`, `@gunshi/plugin`, `@gunshi/definition`) bundles its own copy of
2176
+ * this class, so `instanceof` does not match an error from another copy. Use
2177
+ * {@link isCommandError} to tell it.
2178
+ *
2179
+ * @example
2180
+ * ```js
2181
+ * import { CommandError, define } from 'gunshi'
2182
+ *
2183
+ * export default define({
2184
+ * name: 'show',
2185
+ * args: { project: { type: 'positional', description: 'Project name' } },
2186
+ * run: async ctx => {
2187
+ * const project = await findProject(ctx.values.project)
2188
+ * if (!project) {
2189
+ * throw new CommandError(`Project ${ctx.values.project} not found`, { code: 'not_found' })
2190
+ * }
2191
+ * ctx.log(project.description)
2192
+ * }
2193
+ * })
2194
+ * ```
2195
+ */
2196
+ declare class CommandError extends Error {
2197
+ /**
2198
+ * The name of the failure, for programs to read. See {@link CommandErrorOptions.code}.
2199
+ */
2200
+ readonly code?: string;
2201
+ /**
2202
+ * The exit code of the process, an integer from 1 to 255.
2203
+ */
2204
+ readonly exitCode: number;
2205
+ /**
2206
+ * Create an error for a failure that a command expects.
2207
+ *
2208
+ * @param message - The message printed to the user. It may have more than one line.
2209
+ * @param options - The code, the exit code and the cause of the failure
2210
+ */
2211
+ constructor(message: string, options?: CommandErrorOptions);
2212
+ }
2063
2213
  /**
2064
2214
  * Check whether an error is a {@link CommandNotFoundError}.
2065
2215
  *
@@ -2079,6 +2229,76 @@ declare function isCommandNotFoundError(error: unknown): error is CommandNotFoun
2079
2229
  * @returns `true` when the error has the command-resolution brand and shape
2080
2230
  */
2081
2231
  declare function isCommandResolutionError(error: unknown): error is CommandResolutionError;
2232
+ /**
2233
+ * Check whether an error is a {@link CommandError}.
2234
+ *
2235
+ * An error from another copy of gunshi is recognized through the `Symbol.for('gunshi.CommandError')`
2236
+ * brand. It is not recognized by `error.name`, so an error of the app that is also named
2237
+ * `CommandError` is not taken for one.
2238
+ *
2239
+ * @param error - An unknown error
2240
+ * @returns `true` if the error is a {@link CommandError}
2241
+ */
2242
+ declare function isCommandError(error: unknown): error is CommandError;
2243
+ /**
2244
+ * Options for {@link JsonOutputError}.
2245
+ */
2246
+ type JsonOutputErrorOptions = {
2247
+ /**
2248
+ * The code of the error, which is also the resource key of its message.
2249
+ */
2250
+ code: JsonOutputErrorCode;
2251
+ /**
2252
+ * Values used when localizing the message.
2253
+ */
2254
+ values?: Record<string, unknown>;
2255
+ /**
2256
+ * Underlying cause, such as the error of `JSON.stringify`.
2257
+ */
2258
+ cause?: unknown;
2259
+ };
2260
+ /**
2261
+ * Error raised by the global options plugin for the JSON output, when a command is given `--json`.
2262
+ *
2263
+ * - `err:json:not-supported`: the command does not spread `jsonArgs`. It is raised before the command
2264
+ * runs, and thrown in an `AggregateError`, as the argument errors are.
2265
+ * - `err:json:invalid-result`: the command finished, but did not return an object to output as JSON
2266
+ * - `err:json:unserializable-result`: the command finished, but its result cannot be converted to
2267
+ * JSON, such as an object with a `BigInt`
2268
+ *
2269
+ * The plugin prints it as one line of JSON to stderr, and `cli()` resolves with `undefined`. For
2270
+ * `err:json:not-supported`, with `renderValidationErrors: null` or `usageSilent`, nothing is printed
2271
+ * and `cli()` rejects with the `AggregateError`, as it does for the argument errors.
2272
+ *
2273
+ * Use {@link isJsonOutputError} to tell it, since each package bundles its own copy of this class.
2274
+ */
2275
+ declare class JsonOutputError extends Error {
2276
+ /**
2277
+ * The code of the error, which is also the resource key of its message.
2278
+ */
2279
+ readonly code: JsonOutputErrorCode;
2280
+ /**
2281
+ * Values used when localizing the message.
2282
+ */
2283
+ readonly values: Record<string, unknown>;
2284
+ /**
2285
+ * Create an error of the JSON output.
2286
+ *
2287
+ * @param message - Fallback error message
2288
+ * @param options - The code of the error, and the values of its message
2289
+ */
2290
+ constructor(message: string, options: JsonOutputErrorOptions);
2291
+ }
2292
+ /**
2293
+ * Check whether an error is a {@link JsonOutputError}.
2294
+ *
2295
+ * An error from another copy of gunshi is recognized through the
2296
+ * `Symbol.for('gunshi.JsonOutputError')` brand, not by `error.name`.
2297
+ *
2298
+ * @param error - An unknown error
2299
+ * @returns `true` if the error is a {@link JsonOutputError}
2300
+ */
2301
+ declare function isJsonOutputError(error: unknown): error is JsonOutputError;
2082
2302
  /**
2083
2303
  * Check whether an error is an {@link ArgsValidationError}.
2084
2304
  *
@@ -2106,4 +2326,4 @@ declare function isArgsValidationError(error: unknown): error is ArgsValidationE
2106
2326
  */
2107
2327
  declare function hasPriorityValidationError(error: AggregateError | undefined): boolean;
2108
2328
  //#endregion
2109
- export { ANONYMOUS_COMMAND_NAME, type ArgSchema, type ArgToken, type ArgValues, type Args, ArgsValidationError, type ArgsValidationErrorCode, ArgsValidationErrorKeys, type Awaitable, CLI_OPTIONS_DEFAULT, type Command, type CommandContext, type CommandContextCore, type CommandContextExtension, type CommandContextParams, type CommandDecorator, type CommandExamplesFetcher, CommandNotFoundError, type CommandNotFoundErrorCode, CommandNotFoundErrorKeys, type CommandNotFoundErrorOptions, CommandResolutionError, type CommandResolutionErrorCode, CommandResolutionErrorKeys, type CommandResolutionErrorOptions, type CommandRunner, type DefaultGunshiParams, type ExtendContext, type ExtractArgs, type ExtractExtensions, type GunshiParams, type GunshiParamsConstraint, type LazyCommand, type MergeGunshiExtensions, type NormalizeToGunshiParams, type OnPluginExtension, type Plugin, type PluginContext, type PluginDependency, type PluginExtension, type PluginFunction, type PluginOptions, type PluginWithExtension, type PluginWithoutExtension, type Prettify, type RendererDecorator, type ValidationErrorsDecorator, createCommandContext, hasPriorityValidationError, isArgsValidationError, isCommandNotFoundError, isCommandResolutionError, plugin };
2329
+ export { ANONYMOUS_COMMAND_NAME, type ArgSchema, type ArgToken, type ArgValues, type Args, ArgsValidationError, type ArgsValidationErrorCode, ArgsValidationErrorKeys, type Awaitable, CLI_OPTIONS_DEFAULT, type Command, type CommandContext, type CommandContextCore, type CommandContextExtension, type CommandContextParams, type CommandDecorator, CommandError, type CommandErrorOptions, type CommandExamplesFetcher, CommandNotFoundError, type CommandNotFoundErrorCode, CommandNotFoundErrorKeys, type CommandNotFoundErrorOptions, CommandResolutionError, type CommandResolutionErrorCode, CommandResolutionErrorKeys, type CommandResolutionErrorOptions, type CommandRunner, type CommandRunnerResult, type DefaultGunshiParams, type ExtendContext, type ExtractArgs, type ExtractExtensions, type GunshiParams, type GunshiParamsConstraint, JsonOutputError, type JsonOutputErrorCode, JsonOutputErrorKeys, type JsonOutputErrorOptions, type JsonResult, type LazyCommand, type MergeGunshiExtensions, type NormalizeToGunshiParams, type OnPluginExtension, type Plugin, type PluginContext, type PluginDependency, type PluginExtension, type PluginFunction, type PluginOptions, type PluginWithExtension, type PluginWithoutExtension, type Prettify, type RendererDecorator, type SubCommandable, type ValidationErrorsDecorator, createCommandContext, hasPriorityValidationError, isArgsValidationError, isCommandError, isCommandNotFoundError, isCommandResolutionError, isJsonOutputError, plugin };
package/lib/index.js CHANGED
@@ -33,6 +33,51 @@ const CLI_OPTIONS_DEFAULT = {
33
33
  * @license MIT
34
34
  */
35
35
  /**
36
+ * Check whether an argument schema is the `json` argument of `jsonArgs`, by its mark.
37
+ *
38
+ * @param schema - An argument schema
39
+ * @returns `true` if the schema has the mark of `jsonArgs`
40
+ */
41
+ function isJsonArg(schema) {
42
+ return typeof schema === "object" && schema !== null && schema["~gunshi.json-arg"] === true;
43
+ }
44
+ /**
45
+ * Resolve whether `--json` asks a command for JSON.
46
+ *
47
+ * A command that spreads `jsonArgs` is asked for JSON when the resolved value of its `json` is `true`:
48
+ * with `--json`, or with a short name that the command gives it, but not with `--json=false`, nor with
49
+ * `--json` after `--`. The raw tokens are read only when they are given, and only when there is no
50
+ * value to read: when the command has no `json` argument, and when the core could not resolve the
51
+ * command and left the values empty. A `json` argument without the mark belongs to the command, and
52
+ * `--json` is never taken for a request then.
53
+ *
54
+ * @param args - The arguments of the command, the global options included
55
+ * @param values - The resolved values of the arguments
56
+ * @param tokens - The raw tokens of the arguments, to find a `--json` that no argument declares
57
+ * @returns How the command is asked for JSON, or `undefined` when it is not
58
+ */
59
+ function resolveJsonRequest(args, values, tokens) {
60
+ const schema = Object.hasOwn(args, "json") ? args.json : void 0;
61
+ if (schema === void 0) return tokens !== void 0 && hasJsonOption(tokens) ? "undeclared" : void 0;
62
+ if (!isJsonArg(schema)) return;
63
+ if (values.json !== void 0) return values.json === true ? "declared" : void 0;
64
+ return tokens !== void 0 && hasJsonOption(tokens) ? "declared" : void 0;
65
+ }
66
+ /**
67
+ * Check whether the raw tokens have a `--json` that asks for JSON: `--json` or `--json=true`, before
68
+ * `--`. Without a declaration, a short name of `--json` is not known.
69
+ *
70
+ * @param tokens - The raw tokens of the arguments
71
+ * @returns `true` if a `--json` asks for JSON
72
+ */
73
+ function hasJsonOption(tokens) {
74
+ for (const token of tokens) {
75
+ if (token.kind === "option-terminator") return false;
76
+ if (token.kind === "option" && token.rawName === "--json" && (token.value === void 0 || token.value === "true")) return true;
77
+ }
78
+ return false;
79
+ }
80
+ /**
36
81
  * Check if the given command is a {@link LazyCommand}.
37
82
  *
38
83
  * @param cmd - A command to check
@@ -124,6 +169,14 @@ async function createCommandContext({ args = {}, explicit = {}, values = {}, pos
124
169
  /**
125
170
  * create the command context
126
171
  */
172
+ /**
173
+ * NOTE(kazupon): a command that spreads `jsonArgs` and is asked for JSON prints its result as JSON, so
174
+ * the human output of `ctx.log` is silenced, as it is with `usageSilent`. The hooks and the decorators
175
+ * get the same context, so their `ctx.log` is silenced too. Only the resolved value of the `json`
176
+ * argument counts here, not the raw tokens: a command that does not support `--json` keeps its output,
177
+ * and the global options plugin reports the `--json` given to it.
178
+ */
179
+ const silent = cliOptions.usageSilent || resolveJsonRequest(_args, values) === "declared";
127
180
  const core = Object.assign(create(), {
128
181
  name: getCommandName(command),
129
182
  description: command.description,
@@ -139,7 +192,7 @@ async function createCommandContext({ args = {}, explicit = {}, values = {}, pos
139
192
  _: argv,
140
193
  tokens,
141
194
  toKebab: command.toKebab,
142
- log: cliOptions.usageSilent ? NOOP : log,
195
+ log: silent ? NOOP : log,
143
196
  validationError
144
197
  });
145
198
  /**
@@ -176,7 +229,7 @@ function getCommandName(cmd) {
176
229
  "-".codePointAt(0);
177
230
  "=".codePointAt(0);
178
231
  //#endregion
179
- //#region ../../node_modules/.pnpm/args-tokens@1.0.0/node_modules/args-tokens/lib/resolver-CAlgxe_R.js
232
+ //#region ../../node_modules/.pnpm/args-tokens@1.0.2/node_modules/args-tokens/lib/resolver-Cx5Pw6-S.js
180
233
  /**
181
234
  * @author kazuya kawaguchi (a.k.a. kazupon)
182
235
  * @license MIT
@@ -285,7 +338,7 @@ function isArgsValidationError$1(error) {
285
338
  return typeof values === "object" && values !== null;
286
339
  }
287
340
  //#endregion
288
- //#region ../../node_modules/.pnpm/args-tokens@1.0.0/node_modules/args-tokens/lib/index.js
341
+ //#region ../../node_modules/.pnpm/args-tokens@1.0.2/node_modules/args-tokens/lib/index.js
289
342
  /**
290
343
  * @author kazuya kawaguchi (a.k.a. kazupon)
291
344
  * @license MIT
@@ -317,6 +370,14 @@ const CommandResolutionErrorKeys = {
317
370
  lazySchemaMismatch: "err:cmd:lazy-schema-mismatch"
318
371
  };
319
372
  /**
373
+ * The resource keys of the errors of the JSON output, which are the codes of {@link JsonOutputError}.
374
+ */
375
+ const JsonOutputErrorKeys = {
376
+ notSupported: "err:json:not-supported",
377
+ invalidResult: "err:json:invalid-result",
378
+ unserializableResult: "err:json:unserializable-result"
379
+ };
380
+ /**
320
381
  * Brand that marks {@link CommandNotFoundError} instances.
321
382
  *
322
383
  * The brand is looked up in the global symbol registry with `Symbol.for`, so it stays
@@ -326,6 +387,8 @@ const CommandResolutionErrorKeys = {
326
387
  */
327
388
  const COMMAND_NOT_FOUND_ERROR_BRAND = Symbol.for("gunshi.CommandNotFoundError");
328
389
  const COMMAND_RESOLUTION_ERROR_BRAND = Symbol.for("gunshi.CommandResolutionError");
390
+ const COMMAND_ERROR_BRAND = Symbol.for("gunshi.CommandError");
391
+ const JSON_OUTPUT_ERROR_BRAND = Symbol.for("gunshi.JsonOutputError");
329
392
  /**
330
393
  * Error raised when a command cannot be resolved.
331
394
  *
@@ -395,6 +458,73 @@ var CommandResolutionError = class extends Error {
395
458
  }
396
459
  };
397
460
  /**
461
+ * Error for a failure that a command expects, such as a project that does not exist.
462
+ *
463
+ * When a command throws it from `run`, or a plugin throws it from a command decorator, the global
464
+ * options plugin prints its message to stderr, followed by a `Caused by:` line for each cause,
465
+ * without a stack trace. It sets the exit code of the process to {@link CommandError.exitCode},
466
+ * and `cli()` resolves with `undefined`. `onErrorCommand` is still called with the error, after the
467
+ * exit code is set, so the hook can change it.
468
+ *
469
+ * It is not printed, and `cli()` rejects with it, in these cases:
470
+ *
471
+ * - `usageSilent` is set, for the caller to report it
472
+ * - it is thrown from a hook, while a plugin extends the command context, or while a lazy command
473
+ * loads, outside the command decorators
474
+ * - the CLI runs without the global options plugin, as `gunshi/bone` does
475
+ *
476
+ * Any other error thrown from a command is not printed either, and `cli()` rejects with it.
477
+ *
478
+ * Each package of gunshi (`gunshi`, `@gunshi/plugin`, `@gunshi/definition`) bundles its own copy of
479
+ * this class, so `instanceof` does not match an error from another copy. Use
480
+ * {@link isCommandError} to tell it.
481
+ *
482
+ * @example
483
+ * ```js
484
+ * import { CommandError, define } from 'gunshi'
485
+ *
486
+ * export default define({
487
+ * name: 'show',
488
+ * args: { project: { type: 'positional', description: 'Project name' } },
489
+ * run: async ctx => {
490
+ * const project = await findProject(ctx.values.project)
491
+ * if (!project) {
492
+ * throw new CommandError(`Project ${ctx.values.project} not found`, { code: 'not_found' })
493
+ * }
494
+ * ctx.log(project.description)
495
+ * }
496
+ * })
497
+ * ```
498
+ */
499
+ var CommandError = class extends Error {
500
+ /**
501
+ * The name of the failure, for programs to read. See {@link CommandErrorOptions.code}.
502
+ */
503
+ code;
504
+ /**
505
+ * The exit code of the process, an integer from 1 to 255.
506
+ */
507
+ exitCode;
508
+ /**
509
+ * Create an error for a failure that a command expects.
510
+ *
511
+ * @param message - The message printed to the user. It may have more than one line.
512
+ * @param options - The code, the exit code and the cause of the failure
513
+ */
514
+ constructor(message, options = {}) {
515
+ super(message, { cause: options.cause });
516
+ this.name = "CommandError";
517
+ this.code = options.code;
518
+ this.exitCode = isFailureExitCode(options.exitCode) ? options.exitCode : 1;
519
+ Object.defineProperty(this, COMMAND_ERROR_BRAND, {
520
+ value: true,
521
+ enumerable: false,
522
+ writable: false,
523
+ configurable: false
524
+ });
525
+ }
526
+ };
527
+ /**
398
528
  * Check whether an error is a {@link CommandNotFoundError}.
399
529
  *
400
530
  * `instanceof` alone is not enough: `@gunshi/plugin` is bundled with its own copy of this
@@ -423,6 +553,78 @@ function isCommandResolutionError(error) {
423
553
  return Object.hasOwn(error, COMMAND_RESOLUTION_ERROR_BRAND) && error[COMMAND_RESOLUTION_ERROR_BRAND] === true;
424
554
  }
425
555
  /**
556
+ * Check whether an error is a {@link CommandError}.
557
+ *
558
+ * An error from another copy of gunshi is recognized through the `Symbol.for('gunshi.CommandError')`
559
+ * brand. It is not recognized by `error.name`, so an error of the app that is also named
560
+ * `CommandError` is not taken for one.
561
+ *
562
+ * @param error - An unknown error
563
+ * @returns `true` if the error is a {@link CommandError}
564
+ */
565
+ function isCommandError(error) {
566
+ if (error instanceof CommandError) return true;
567
+ if (!isRecord(error) || !Object.hasOwn(error, COMMAND_ERROR_BRAND) || error[COMMAND_ERROR_BRAND] !== true) return false;
568
+ return hasCommandErrorShape(error);
569
+ }
570
+ /**
571
+ * Error raised by the global options plugin for the JSON output, when a command is given `--json`.
572
+ *
573
+ * - `err:json:not-supported`: the command does not spread `jsonArgs`. It is raised before the command
574
+ * runs, and thrown in an `AggregateError`, as the argument errors are.
575
+ * - `err:json:invalid-result`: the command finished, but did not return an object to output as JSON
576
+ * - `err:json:unserializable-result`: the command finished, but its result cannot be converted to
577
+ * JSON, such as an object with a `BigInt`
578
+ *
579
+ * The plugin prints it as one line of JSON to stderr, and `cli()` resolves with `undefined`. For
580
+ * `err:json:not-supported`, with `renderValidationErrors: null` or `usageSilent`, nothing is printed
581
+ * and `cli()` rejects with the `AggregateError`, as it does for the argument errors.
582
+ *
583
+ * Use {@link isJsonOutputError} to tell it, since each package bundles its own copy of this class.
584
+ */
585
+ var JsonOutputError = class extends Error {
586
+ /**
587
+ * The code of the error, which is also the resource key of its message.
588
+ */
589
+ code;
590
+ /**
591
+ * Values used when localizing the message.
592
+ */
593
+ values;
594
+ /**
595
+ * Create an error of the JSON output.
596
+ *
597
+ * @param message - Fallback error message
598
+ * @param options - The code of the error, and the values of its message
599
+ */
600
+ constructor(message, options) {
601
+ super(message, { cause: options.cause });
602
+ this.name = "JsonOutputError";
603
+ this.code = options.code;
604
+ this.values = options.values || {};
605
+ Object.defineProperty(this, JSON_OUTPUT_ERROR_BRAND, {
606
+ value: true,
607
+ enumerable: false,
608
+ writable: false,
609
+ configurable: false
610
+ });
611
+ }
612
+ };
613
+ /**
614
+ * Check whether an error is a {@link JsonOutputError}.
615
+ *
616
+ * An error from another copy of gunshi is recognized through the
617
+ * `Symbol.for('gunshi.JsonOutputError')` brand, not by `error.name`.
618
+ *
619
+ * @param error - An unknown error
620
+ * @returns `true` if the error is a {@link JsonOutputError}
621
+ */
622
+ function isJsonOutputError(error) {
623
+ if (error instanceof JsonOutputError) return true;
624
+ if (!isRecord(error) || !Object.hasOwn(error, JSON_OUTPUT_ERROR_BRAND) || error[JSON_OUTPUT_ERROR_BRAND] !== true) return false;
625
+ return hasJsonOutputErrorShape(error);
626
+ }
627
+ /**
426
628
  * Check whether an error is an {@link ArgsValidationError}.
427
629
  *
428
630
  * Prefer this over the `args-tokens` guard of the same name. Both recognize errors from another
@@ -472,6 +674,21 @@ function hasCommandResolutionErrorShape(error) {
472
674
  function hasArgsValidationErrorShape(error) {
473
675
  return hasOptionalStringCode(error) && isRecord(error.values);
474
676
  }
677
+ function hasCommandErrorShape(error) {
678
+ return hasOptionalStringCode(error) && typeof error.message === "string" && isFailureExitCode(error.exitCode);
679
+ }
680
+ function hasJsonOutputErrorShape(error) {
681
+ return Object.values(JsonOutputErrorKeys).includes(error.code) && isRecord(error.values);
682
+ }
683
+ /**
684
+ * Check whether a value is an exit code of a failure: an integer from 1 to 255.
685
+ *
686
+ * @param value - The value to check
687
+ * @returns `true` if the value is an exit code of a failure
688
+ */
689
+ function isFailureExitCode(value) {
690
+ return Number.isInteger(value) && value >= 1 && value <= 255;
691
+ }
475
692
  /**
476
693
  * Check whether validation errors should be handled before version, help, or command execution.
477
694
  *
@@ -546,4 +763,4 @@ function plugin(options = {}) {
546
763
  * @license MIT
547
764
  */
548
765
  //#endregion
549
- export { ANONYMOUS_COMMAND_NAME, ArgsValidationError, ArgsValidationErrorKeys, CLI_OPTIONS_DEFAULT, CommandNotFoundError, CommandNotFoundErrorKeys, CommandResolutionError, CommandResolutionErrorKeys, createCommandContext, hasPriorityValidationError, isArgsValidationError, isCommandNotFoundError, isCommandResolutionError, plugin };
766
+ export { ANONYMOUS_COMMAND_NAME, ArgsValidationError, ArgsValidationErrorKeys, CLI_OPTIONS_DEFAULT, CommandError, CommandNotFoundError, CommandNotFoundErrorKeys, CommandResolutionError, CommandResolutionErrorKeys, JsonOutputError, JsonOutputErrorKeys, createCommandContext, hasPriorityValidationError, isArgsValidationError, isCommandError, isCommandNotFoundError, isCommandResolutionError, isJsonOutputError, plugin };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gunshi/plugin",
3
3
  "description": "plugin development kit for gunshi",
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"
@@ -51,16 +51,11 @@
51
51
  }
52
52
  },
53
53
  "devDependencies": {
54
- "deno": "^2.7.14",
55
- "gunshi": "1.0.0-rc.4",
56
- "jsr": "^0.14.3",
57
- "jsr-exports-lint": "^0.4.2",
54
+ "gunshi": "1.0.0-rc.6",
58
55
  "publint": "^0.3.20",
59
56
  "tsdown": "0.22.14"
60
57
  },
61
58
  "scripts": {
62
- "build": "tsdown",
63
- "lint:jsr": "jsr publish --dry-run --allow-dirty",
64
- "typecheck:deno": "deno check --import-map=../../importmap.json ./src"
59
+ "build": "tsdown"
65
60
  }
66
61
  }