@gunshi/plugin 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.
Files changed (3) hide show
  1. package/lib/index.d.ts +241 -18
  2. package/lib/index.js +219 -2
  3. package/package.json +2 -2
package/lib/index.d.ts CHANGED
@@ -793,7 +793,7 @@ interface PluginContext<G extends GunshiParamsConstraint = DefaultGunshiParams>
793
793
  * @param name - Command name
794
794
  * @param command - Command definition
795
795
  */
796
- addCommand(name: string, command: Command<G> | LazyCommand<G>): void;
796
+ addCommand(name: string, command: SubCommandable): void;
797
797
  /**
798
798
  * Check if a command exists.
799
799
  *
@@ -834,7 +834,7 @@ interface PluginContext<G extends GunshiParamsConstraint = DefaultGunshiParams>
834
834
  *
835
835
  * @param decorator - A decorator function that wraps the command runner
836
836
  */
837
- 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;
838
838
  }
839
839
  //#endregion
840
840
  //#region ../gunshi/src/plugin/core.d.ts
@@ -1271,7 +1271,11 @@ interface CommandEnvironment<G extends GunshiParamsConstraint = DefaultGunshiPar
1271
1271
  * (they are still returned as strings) and to silence {@linkcode CommandContext.log}.
1272
1272
  *
1273
1273
  * Output that must still appear — machine-readable results for an agent, warnings for the
1274
- * 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.
1275
1279
  *
1276
1280
  * @default false
1277
1281
  * @see {@linkcode CliOptions.usageSilent}
@@ -1331,7 +1335,8 @@ interface CommandEnvironment<G extends GunshiParamsConstraint = DefaultGunshiPar
1331
1335
  */
1332
1336
  onBeforeCommand: ((ctx: Readonly<CommandContext<G>>) => Awaitable<void>) | undefined;
1333
1337
  /**
1334
- * 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.
1335
1340
  *
1336
1341
  * @see {@linkcode CliOptions.onAfterCommand}
1337
1342
  * @since v0.27.0
@@ -1396,10 +1401,12 @@ interface CliOptions<G extends GunshiParamsConstraint = DefaultGunshiParams> {
1396
1401
  * (they are still returned as strings) and to silence {@linkcode CommandContext.log}.
1397
1402
  *
1398
1403
  * Output that must still appear — machine-readable results for an agent, warnings for the
1399
- * 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.
1400
1406
  *
1401
- * The validation errors are not printed either, so `cli()` rejects with them, for the caller to
1402
- * 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.
1403
1410
  *
1404
1411
  * @default false
1405
1412
  */
@@ -1411,6 +1418,10 @@ interface CliOptions<G extends GunshiParamsConstraint = DefaultGunshiParams> {
1411
1418
  * arguments or installed global options are reported as validation errors
1412
1419
  * before the command runner is executed.
1413
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
+ *
1414
1425
  * @default false
1415
1426
  */
1416
1427
  strict?: boolean;
@@ -1430,6 +1441,9 @@ interface CliOptions<G extends GunshiParamsConstraint = DefaultGunshiParams> {
1430
1441
  * Set it to `null` to report the validation errors yourself: nothing is printed, and `cli()` rejects
1431
1442
  * with the `AggregateError`. A renderer that returns an empty message, and
1432
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.
1433
1447
  */
1434
1448
  renderValidationErrors?: ((ctx: Readonly<CommandContext<G>>, error: AggregateError) => Promise<string>) | null;
1435
1449
  /**
@@ -1456,15 +1470,17 @@ interface CliOptions<G extends GunshiParamsConstraint = DefaultGunshiParams> {
1456
1470
  * Hook that runs after successful command execution
1457
1471
  *
1458
1472
  * @param ctx - The command context
1459
- * @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`
1460
1475
  * @since v0.27.0
1461
1476
  */
1462
1477
  onAfterCommand?: (ctx: Readonly<CommandContext<G>>, result: string | undefined) => Awaitable<void>;
1463
1478
  /**
1464
1479
  * Hook that runs when a command throws an error, or when the arguments fail validation
1465
1480
  *
1466
- * For the validation errors that the global options plugin has printed, the exit code is already `1`
1467
- * 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
1468
1484
  * error rejects `cli()` after the hook.
1469
1485
  *
1470
1486
  * @param ctx - The command context
@@ -1564,9 +1580,11 @@ interface CommandContext<G extends GunshiParamsConstraint = DefaultGunshiParams>
1564
1580
  */
1565
1581
  toKebab?: boolean;
1566
1582
  /**
1567
- * Output a message.
1583
+ * Output a message for people.
1568
1584
  *
1569
- * 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.
1570
1588
  *
1571
1589
  * @param message - an output message, see {@linkcode console.log}
1572
1590
  * @param optionalParams - an optional parameters, see {@linkcode console.log}
@@ -1810,14 +1828,49 @@ type CommandExamplesFetcher<G extends GunshiParamsConstraint = DefaultGunshiPara
1810
1828
  * @typeParam G - A type extending {@linkcode GunshiParams} to specify the shape of command context.
1811
1829
  *
1812
1830
  * @param ctx - A {@link CommandContext | command context}
1813
- * @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}
1814
1857
  */
1815
- 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;
1816
1865
  /**
1817
1866
  * Command decorator.
1818
1867
  *
1819
1868
  * A function that wraps a command runner to add or modify its behavior.
1820
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
+ *
1821
1874
  * @typeParam G - A type extending {@linkcode GunshiParams} to specify the shape of command context.
1822
1875
  *
1823
1876
  * @param baseRunner - The base command runner to decorate
@@ -1825,7 +1878,7 @@ type CommandRunner<G extends GunshiParamsConstraint = DefaultGunshiParams> = (ct
1825
1878
  *
1826
1879
  * @since v0.27.0
1827
1880
  */
1828
- 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>;
1829
1882
  /**
1830
1883
  * Renderer decorator type.
1831
1884
  *
@@ -1859,6 +1912,13 @@ type ValidationErrorsDecorator<G extends GunshiParamsConstraint = DefaultGunshiP
1859
1912
  //#region ../gunshi/src/constants.d.ts
1860
1913
  declare const ANONYMOUS_COMMAND_NAME = "(anonymous)";
1861
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";
1862
1922
  //#endregion
1863
1923
  //#region ../gunshi/src/context.d.ts
1864
1924
  /**
@@ -1883,7 +1943,7 @@ interface CommandContextParams<G extends GunshiParams | {
1883
1943
  args: Args;
1884
1944
  } | {
1885
1945
  extensions: ExtendContext;
1886
- }, 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>> {
1887
1947
  /**
1888
1948
  * An arguments of target command
1889
1949
  */
@@ -1947,7 +2007,7 @@ interface CommandContextParams<G extends GunshiParams | {
1947
2007
  * @param param - A {@link CommandContextParams | parameters} to create a command context.
1948
2008
  * @returns A {@link CommandContext | command context}, which is readonly.
1949
2009
  */
1950
- 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>>;
1951
2011
  //#endregion
1952
2012
  //#region ../gunshi/src/error.d.ts
1953
2013
  /**
@@ -1974,6 +2034,18 @@ declare const CommandResolutionErrorKeys: {
1974
2034
  };
1975
2035
  /** Command resolution error code. */
1976
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];
1977
2049
  /**
1978
2050
  * Options for {@link CommandNotFoundError}.
1979
2051
  */
@@ -2016,6 +2088,31 @@ type CommandResolutionErrorOptions = {
2016
2088
  /** Underlying cause. */
2017
2089
  cause?: unknown;
2018
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
+ };
2019
2116
  /**
2020
2117
  * Error raised when a command cannot be resolved.
2021
2118
  *
@@ -2057,6 +2154,62 @@ declare class CommandResolutionError extends Error {
2057
2154
  */
2058
2155
  constructor(message: string, options?: CommandResolutionErrorOptions);
2059
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
+ }
2060
2213
  /**
2061
2214
  * Check whether an error is a {@link CommandNotFoundError}.
2062
2215
  *
@@ -2076,6 +2229,76 @@ declare function isCommandNotFoundError(error: unknown): error is CommandNotFoun
2076
2229
  * @returns `true` when the error has the command-resolution brand and shape
2077
2230
  */
2078
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;
2079
2302
  /**
2080
2303
  * Check whether an error is an {@link ArgsValidationError}.
2081
2304
  *
@@ -2103,4 +2326,4 @@ declare function isArgsValidationError(error: unknown): error is ArgsValidationE
2103
2326
  */
2104
2327
  declare function hasPriorityValidationError(error: AggregateError | undefined): boolean;
2105
2328
  //#endregion
2106
- 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
  /**
@@ -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.5",
4
+ "version": "1.0.0-rc.6",
5
5
  "author": {
6
6
  "name": "kazuya kawaguchi",
7
7
  "email": "kawakazu80@gmail.com"
@@ -51,7 +51,7 @@
51
51
  }
52
52
  },
53
53
  "devDependencies": {
54
- "gunshi": "1.0.0-rc.5",
54
+ "gunshi": "1.0.0-rc.6",
55
55
  "publint": "^0.3.20",
56
56
  "tsdown": "0.22.14"
57
57
  },