@ariestools/cli-kit-yargs 1.0.4 → 1.0.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
@@ -26,9 +26,16 @@ await runYargsApplication({
26
26
  })
27
27
  ```
28
28
 
29
- Two constraints keep the check accurate. The `commands` array must equal the set registered on the parserthe accepted-name set is derived from it, not read back from the parser, so any drift silently rejects a valid command or accepts an unhandled one. And every accepted positional must be declared in its command string (`serve <file>`, `serve [file]`, `serve [files..]`): Yargs consumes declared positionals out of `argv._` before the check runs, but a command that reads ad-hoc positionals it never declared would leave them in `argv._` and see the first rejected as `Unknown command: <value>`.
29
+ Every non-empty leftover token is inspected regardless of its runtime type. Yargs types `argv._` as `(string | number)[]` and, with its default `parse-positional-numbers` enabled, coerces a bare numeric token to a JS number so `app 0`, `app 123`, and `app -5` are flagged as `Unknown command: 0` (and so on) just like a word. No `.parserConfiguration({ 'parse-positional-numbers': false })` workaround is needed to catch a numeric stray. The message echoes the coerced value rather than the raw text, so an exotic numeric literal is reported normalized — `1e3` reads `Unknown command: 1000`, `0x10` reads `16`, `1.50` reads `1.5` because Yargs has already coerced by the time a `.check()` runs. Rejection is correct in every case; only the echoed text differs. An empty token (`app ""`) is the one exclusion: it names no command and falls through to `$0`.
30
30
 
31
- For an unknown token Yargs runs the failing check first, then still invokes the `$0` default command's handler before surfacing the parse failure, so keep that handler side-effect-free (printing a usage block is the intended shape).
31
+ Four constraints keep the check accurate.
32
+
33
+ - The `commands` array must equal the set registered on the parser — the accepted-name set is derived from it, not read back from the parser, so any drift silently rejects a valid command or accepts an unhandled one.
34
+ - Every accepted positional must be declared in its command string (`serve <file>`, `serve [file]`, `serve [files..]`): Yargs consumes declared positionals out of `argv._` into named keys before the check runs — including numeric-valued ones, so `serve <port>` invoked as `serve 8080` is unaffected — but a command that reads ad-hoc positionals it never declared would leave them in `argv._` and see the first rejected as `Unknown command: <value>`.
35
+ - A subcommand registered inside a parent command's `builder` must also appear in `commands`. Yargs leaves both tokens in `argv._` (`db migrate` yields `['db', 'migrate']`), so a nested name missing from the array is rejected as `Unknown command: migrate`. Passing the nested modules alongside the top-level ones fixes that, at the cost of a flat accepted-name set: the nested name is then also accepted at the top level, where it falls through to `$0`.
36
+ - Tokens after a `--` separator are not passthrough unless the caller enables `.parserConfiguration({ 'populate--': true })`. Yargs leaves `populate--` off by default, which merges those tokens into `argv._` rather than `argv['--']`, so `app local -- raw` is rejected as `Unknown command: raw` (and `app local -- 5` as `Unknown command: 5`). Enabling `populate--` routes them to `argv['--']`, out of `argv._` and out of this check's reach.
37
+
38
+ For an unknown token Yargs runs the failing check first, then still invokes the matched command's handler before surfacing the parse failure — the `$0` default command for a bare stray token, and the named command for `app publish 0`. `parseAsync` awaits an async handler to completion, so its side effects commit before the flagged exit. Keep any handler reachable alongside a stray token side-effect-free (for `$0`, printing a usage block is the intended shape).
32
39
 
33
40
  Use `environmentToYargsConfig` when an application owns config-file loading and wants to replace Yargs' process-global `.env()` lookup with an explicitly supplied environment. The resulting flat dotted object preserves Yargs' environment key normalization and can be passed to `.config()` before parsing:
34
41
 
@@ -60,9 +60,7 @@ function knownCommandNames(commands) {
60
60
  function rejectUnknownCommands(parser, commands) {
61
61
  const known = knownCommandNames(commands);
62
62
  return parser.check((argv) => {
63
- const unknown = argv._.find(
64
- (token) => typeof token === "string" && token.length > 0 && !known.has(token)
65
- );
63
+ const unknown = argv._.map(String).find((token) => token.length > 0 && !known.has(token));
66
64
  if (unknown !== void 0) throw new Error(`Unknown command: ${unknown}`);
67
65
  return true;
68
66
  });
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/environmentToYargsConfig.ts", "../../src/rejectUnknownCommands.ts", "../../src/runYargsApplication.ts"],
4
- "sourcesContent": ["export type CliEnvironment = Readonly<Record<string, string | undefined>>\n\n/**\n * Matches the segment normalization used by yargs-parser for environment keys.\n * Keeping this local avoids coupling the adapter to an unexported parser helper.\n */\nfunction camelCaseEnvironmentSegment(value: string): string {\n const isCamelCase = value !== value.toLowerCase() && value !== value.toUpperCase()\n const normalized = isCamelCase ? value : value.toLowerCase()\n if (!normalized.includes('-') && !normalized.includes('_')) return normalized\n\n let result = ''\n let isNextCharacterUpper = false\n const leadingHyphens = /^-+/.exec(normalized)\n const start = leadingHyphens?.[0].length ?? 0\n for (let index = start; index < normalized.length; index++) {\n let character = normalized.charAt(index)\n if (isNextCharacterUpper) {\n isNextCharacterUpper = false\n character = character.toUpperCase()\n }\n if (index !== 0 && (character === '-' || character === '_')) {\n isNextCharacterUpper = true\n } else if (character !== '-' && character !== '_') {\n result += character\n }\n }\n return result\n}\n\n/**\n * Converts a prefixed environment into the flat dotted config shape accepted\n * by Yargs. Values remain strings so Yargs applies its normal option typing and\n * coercion after command-line arguments have taken precedence.\n */\nexport function environmentToYargsConfig(\n environment: CliEnvironment,\n prefix: string,\n): Record<string, string | undefined> {\n const config: Record<string, string | undefined> = {}\n for (const [environmentName, environmentValue] of Object.entries(environment)) {\n if (!environmentName.startsWith(prefix)) continue\n const keys = environmentName.split('__').map((key, index) => {\n return camelCaseEnvironmentSegment(index === 0 ? key.slice(prefix.length) : key)\n })\n const configKey = keys.join('.')\n if (!Object.hasOwn(config, configKey)) config[configKey] = environmentValue\n }\n return config\n}\n", "import type { Argv, CommandModule } from 'yargs'\n\nfunction firstToken(entry: string): string | undefined {\n const token = entry.trim().split(/\\s+/, 1)[0]\n return token !== undefined && token.length > 0 ? token : undefined\n}\n\nfunction toEntries(value: string | readonly string[] | undefined): readonly string[] {\n if (value === undefined) return []\n return typeof value === 'string' ? [value] : value\n}\n\n/**\n * Collects the accepted command names (and aliases) declared by a set of\n * command modules. A command declared as `'serve <file>'` contributes `serve`;\n * a `$0` default command contributes `$0`. Aliases are included so an invocation\n * that reaches a command through an alias is not mistaken for an unknown token.\n */\nfunction knownCommandNames(commands: readonly CommandModule[]): ReadonlySet<string> {\n const names = new Set<string>()\n for (const command of commands) {\n for (const entry of toEntries(command.command)) {\n const token = firstToken(entry)\n if (token !== undefined) names.add(token)\n }\n for (const alias of toEntries(command.aliases)) {\n const token = firstToken(alias)\n if (token !== undefined) names.add(token)\n }\n }\n return names\n}\n\n/**\n * Rejects any leftover positional token that does not name a registered\n * command, turning `app bogus` into a parse-origin failure (exit 64 via the\n * usage path when a mapper selects it) while leaving a bare invocation, a\n * registered command, and the `help`/`$0` paths untouched.\n *\n * A `$0` default command consumes stray tokens as its own positionals rather\n * than letting `.strictCommands()` flag them \u2014 with a default command in place\n * `.strictCommands()` silently accepts an unknown word \u2014 so this explicit,\n * command-name-aware `.check()` is what enforces the command surface. Because a\n * matched command name remains in `argv._`, the check compares each token\n * against the names declared by `commands`.\n *\n * Two constraints keep the check accurate; both are the caller's to honor:\n *\n * - `commands` MUST equal the set registered on the parser via `.command(...)`.\n * The accepted-name set is derived from this array, not read back from the\n * parser, so any drift is silently wrong: a command registered on the parser\n * but omitted here has valid invocations rejected as unknown, and one listed\n * here but never registered lets an unhandled token through. Register and pass\n * the same array (see the example below) so the two cannot diverge.\n * - Every accepted positional MUST be declared in its command string as\n * `<arg>`/`[arg]`/`[arg..]`. Yargs consumes declared positionals out of\n * `argv._` before the check runs, so they are safe; but a command that reads\n * ad-hoc or variadic positionals it never declared leaves them in `argv._`,\n * where this check rejects the first as `Unknown command: <value>`.\n *\n * Note: for an unknown token, Yargs runs the failing check first (recording the\n * parse-origin failure) but still invokes the `$0` default command's handler\n * before surfacing that failure. Keep the default command's handler\n * side-effect-free \u2014 printing a usage block is the intended shape \u2014 so the\n * flagged exit remains authoritative.\n *\n * Apply it after every `.command(...)` registration and before `.help()`:\n *\n * ```ts\n * for (const command of commands) configured = configured.command(command)\n * return rejectUnknownCommands(configured, commands).help().version(version)\n * ```\n */\nexport function rejectUnknownCommands(parser: Argv, commands: readonly CommandModule[]): Argv {\n const known = knownCommandNames(commands)\n return parser.check((argv) => {\n const unknown = argv._.find(\n (token): token is string => typeof token === 'string' && token.length > 0 && !known.has(token),\n )\n if (unknown !== undefined) throw new Error(`Unknown command: ${unknown}`)\n return true\n })\n}\n", "import {\n exitProcess, type FailureExitCodeMapper, ProcessExitError, type ProcessHost, resolveFailureExitCode,\n} from '@ariestools/cli-kit'\nimport type { Argv } from 'yargs'\nimport yargs from 'yargs'\nimport { hideBin } from 'yargs/helpers'\n\nexport interface RunYargsApplicationOptions {\n readonly configure: (parser: Argv) => Argv\n readonly host: ProcessHost\n /**\n * Optional failure policy applied to parser and handler failures alike.\n * The mapper receives the failure origin (`'parse'` for validation\n * failures, `'handler'` for handler or middleware rejections) so exit\n * codes such as `usage` versus `software` need no error sniffing.\n * Returning a number selects the exit code; `undefined`, a thrown mapper,\n * or a result outside the integer range 0\u2013255 retains the default exit\n * code one. Intentional `ProcessExitError` exits are never offered to\n * the mapper.\n */\n readonly mapFailureToExitCode?: FailureExitCodeMapper\n readonly onFailure?: (error: unknown) => Promise<void> | void\n}\n\ninterface CapturedParseOutput {\n error: Error | undefined\n output: string\n}\n\ninterface FailureHandlerResult {\n readonly error?: unknown\n readonly hasError: boolean\n}\n\nasync function invokeFailureHandler(\n handler: RunYargsApplicationOptions['onFailure'],\n error: unknown,\n): Promise<FailureHandlerResult> {\n try {\n await handler?.(error)\n return { hasError: false }\n } catch (failureHandlerError) {\n return { error: failureHandlerError, hasError: true }\n }\n}\n\nfunction reportFailureHandlerError(host: ProcessHost, result: FailureHandlerResult): void {\n if (result.hasError) host.io.error('Error handling application failure:', result.error)\n}\n\n/**\n * Reports a handler-origin failure without the parser usage/help block. A\n * usage dump is right for a parse/usage error but noise for a handler-origin\n * failure such as a mapped configuration error. The raw error (including its\n * stack when present) always reaches stderr so production root-cause diagnosis\n * is preserved; only the usage block is suppressed.\n */\nfunction reportHandlerFailure(host: ProcessHost, error: unknown): void {\n host.io.error(error)\n}\n\n/**\n * Runs a configured Yargs app through the supplied process boundary.\n * Yargs' parse callback prevents its default console and process.exit calls;\n * the adapter then routes the equivalent output and exit through ProcessHost.\n *\n * Failure output is scoped by origin. A parse-origin failure (a Yargs\n * validation or usage error, including one raised by {@link rejectUnknownCommands})\n * still prints the usage/help block, because the caller mis-invoked the CLI.\n * A handler-origin failure (a thrown command handler or middleware, such as a\n * mapped configuration error) prints the raw error to stderr \u2014 including its\n * stack when present \u2014 with no usage dump, since the invocation was well-formed.\n * Exit codes and the {@link FailureExitCodeMapper} contract are unchanged; only\n * what is printed per origin differs.\n */\nexport async function runYargsApplication({\n configure,\n host,\n mapFailureToExitCode,\n onFailure,\n}: RunYargsApplicationOptions): Promise<void> {\n const applicationArguments = hideBin([...host.argv])\n const parser = configure(yargs(applicationArguments))\n const captured: CapturedParseOutput = { error: undefined, output: '' }\n\n try {\n await parser.parseAsync(applicationArguments, {}, (error, _argv, output) => {\n // Yargs 18 reports `null` on success at runtime despite @types/yargs\n // declaring this callback value as `Error | undefined`.\n captured.error = error ?? undefined\n captured.output = output\n })\n } catch (error) {\n if (error instanceof ProcessExitError) throw error\n const failureHandlerResult = await invokeFailureHandler(onFailure, error)\n reportHandlerFailure(host, error)\n reportFailureHandlerError(host, failureHandlerResult)\n exitProcess(host, resolveFailureExitCode(mapFailureToExitCode, error, 'handler'))\n }\n\n if (captured.error !== undefined) {\n const failureHandlerResult = await invokeFailureHandler(onFailure, captured.error)\n if (captured.output.length > 0) {\n host.io.error(captured.output)\n } else {\n host.io.error(captured.error)\n }\n reportFailureHandlerError(host, failureHandlerResult)\n exitProcess(host, resolveFailureExitCode(mapFailureToExitCode, captured.error, 'parse'))\n }\n\n if (captured.output.length > 0) {\n host.io.log(captured.output)\n exitProcess(host, 0)\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["export type CliEnvironment = Readonly<Record<string, string | undefined>>\n\n/**\n * Matches the segment normalization used by yargs-parser for environment keys.\n * Keeping this local avoids coupling the adapter to an unexported parser helper.\n */\nfunction camelCaseEnvironmentSegment(value: string): string {\n const isCamelCase = value !== value.toLowerCase() && value !== value.toUpperCase()\n const normalized = isCamelCase ? value : value.toLowerCase()\n if (!normalized.includes('-') && !normalized.includes('_')) return normalized\n\n let result = ''\n let isNextCharacterUpper = false\n const leadingHyphens = /^-+/.exec(normalized)\n const start = leadingHyphens?.[0].length ?? 0\n for (let index = start; index < normalized.length; index++) {\n let character = normalized.charAt(index)\n if (isNextCharacterUpper) {\n isNextCharacterUpper = false\n character = character.toUpperCase()\n }\n if (index !== 0 && (character === '-' || character === '_')) {\n isNextCharacterUpper = true\n } else if (character !== '-' && character !== '_') {\n result += character\n }\n }\n return result\n}\n\n/**\n * Converts a prefixed environment into the flat dotted config shape accepted\n * by Yargs. Values remain strings so Yargs applies its normal option typing and\n * coercion after command-line arguments have taken precedence.\n */\nexport function environmentToYargsConfig(\n environment: CliEnvironment,\n prefix: string,\n): Record<string, string | undefined> {\n const config: Record<string, string | undefined> = {}\n for (const [environmentName, environmentValue] of Object.entries(environment)) {\n if (!environmentName.startsWith(prefix)) continue\n const keys = environmentName.split('__').map((key, index) => {\n return camelCaseEnvironmentSegment(index === 0 ? key.slice(prefix.length) : key)\n })\n const configKey = keys.join('.')\n if (!Object.hasOwn(config, configKey)) config[configKey] = environmentValue\n }\n return config\n}\n", "import type { Argv, CommandModule } from 'yargs'\n\nfunction firstToken(entry: string): string | undefined {\n const token = entry.trim().split(/\\s+/, 1)[0]\n return token !== undefined && token.length > 0 ? token : undefined\n}\n\nfunction toEntries(value: string | readonly string[] | undefined): readonly string[] {\n if (value === undefined) return []\n return typeof value === 'string' ? [value] : value\n}\n\n/**\n * Collects the accepted command names (and aliases) declared by a set of\n * command modules. A command declared as `'serve <file>'` contributes `serve`;\n * a `$0` default command contributes `$0`. Aliases are included so an invocation\n * that reaches a command through an alias is not mistaken for an unknown token.\n */\nfunction knownCommandNames(commands: readonly CommandModule[]): ReadonlySet<string> {\n const names = new Set<string>()\n for (const command of commands) {\n for (const entry of toEntries(command.command)) {\n const token = firstToken(entry)\n if (token !== undefined) names.add(token)\n }\n for (const alias of toEntries(command.aliases)) {\n const token = firstToken(alias)\n if (token !== undefined) names.add(token)\n }\n }\n return names\n}\n\n/**\n * Rejects any leftover positional token that does not name a registered\n * command, turning `app bogus` into a parse-origin failure (exit 64 via the\n * usage path when a mapper selects it) while leaving a bare invocation, a\n * registered command, and the `help`/`$0` paths untouched.\n *\n * A `$0` default command consumes stray tokens as its own positionals rather\n * than letting `.strictCommands()` flag them \u2014 with a default command in place\n * `.strictCommands()` silently accepts an unknown word \u2014 so this explicit,\n * command-name-aware `.check()` is what enforces the command surface. Because a\n * matched command name remains in `argv._`, the check compares each token\n * against the names declared by `commands`.\n *\n * Every non-empty leftover token is inspected regardless of its runtime type.\n * Yargs types `argv._` as `(string | number)[]` and, with its default\n * `parse-positional-numbers` enabled, coerces a bare numeric token to a JS\n * number \u2014 so `app 0`, `app 123`, and `app -5` are rejected as\n * `Unknown command: 0` (and so on) rather than slipping through. Consumers do\n * not need `.parserConfiguration({ 'parse-positional-numbers': false })` to get\n * a numeric stray flagged. Two details of that normalization:\n *\n * - The message echoes the coerced value rather than the raw text, so an exotic\n * numeric literal is reported in its normalized form: `1e3` reads\n * `Unknown command: 1000`, `0x10` reads `16`, and `1.50` reads `1.5`. The\n * token is rejected correctly in every case; only the echoed text differs.\n * Yargs has already coerced by the time a `.check()` runs, so the raw text is\n * not recoverable here.\n * - An empty token (`app \"\"`) is skipped, because it names no command and would\n * report as a blank `Unknown command: `. It falls through to `$0` as before.\n *\n * Four constraints keep the check accurate; all are the caller's to honor:\n *\n * - `commands` MUST equal the set registered on the parser via `.command(...)`.\n * The accepted-name set is derived from this array, not read back from the\n * parser, so any drift is silently wrong: a command registered on the parser\n * but omitted here has valid invocations rejected as unknown, and one listed\n * here but never registered lets an unhandled token through. Register and pass\n * the same array (see the example below) so the two cannot diverge.\n * - Every accepted positional MUST be declared in its command string as\n * `<arg>`/`[arg]`/`[arg..]`. Yargs consumes declared positionals out of\n * `argv._` into named keys before the check runs \u2014 including numeric-valued\n * ones, so `serve <port>` invoked as `serve 8080` is safe \u2014 but a command that\n * reads ad-hoc or variadic positionals it never declared leaves them in\n * `argv._`, where this check rejects the first as `Unknown command: <value>`.\n * - A subcommand registered inside a parent command's `builder` MUST also appear\n * in `commands`. Yargs leaves both tokens in `argv._` (`db migrate` yields\n * `['db', 'migrate']`), so a nested name absent from this array is rejected as\n * `Unknown command: migrate`. Passing the nested modules alongside the\n * top-level ones fixes that, at the cost of a flat accepted-name set: the\n * nested name is then also accepted at the top level, where it falls through\n * to `$0`.\n * - Tokens after a `--` separator are NOT passthrough unless the caller enables\n * `.parserConfiguration({ 'populate--': true })`. Yargs leaves `populate--`\n * off by default, which merges those tokens into `argv._` rather than\n * `argv['--']`, so `app local -- raw` is rejected as `Unknown command: raw`\n * (and `app local -- 5` as `Unknown command: 5`). Enabling `populate--` routes\n * them to `argv['--']`, out of `argv._` and out of this check's reach.\n *\n * Note: for an unknown token, Yargs runs the failing check first (recording the\n * parse-origin failure) but still invokes the matched command's handler before\n * surfacing that failure \u2014 the `$0` default command for a bare stray token, and\n * the named command for `app publish 0`. `parseAsync` awaits an async handler to\n * completion, so its side effects commit before the flagged exit. Keep handlers\n * that can be reached alongside a stray token side-effect-free \u2014 for `$0`,\n * printing a usage block is the intended shape \u2014 so the flagged exit remains\n * authoritative.\n *\n * Apply it after every `.command(...)` registration and before `.help()`:\n *\n * ```ts\n * for (const command of commands) configured = configured.command(command)\n * return rejectUnknownCommands(configured, commands).help().version(version)\n * ```\n */\nexport function rejectUnknownCommands(parser: Argv, commands: readonly CommandModule[]): Argv {\n const known = knownCommandNames(commands)\n return parser.check((argv) => {\n // `argv._` is `(string | number)[]`: with Yargs' default\n // `parse-positional-numbers` a bare numeric token is coerced to a JS number,\n // so every token is normalized to its string form before comparison.\n // Narrowing to `typeof token === 'string'` here would skip numeric strays\n // entirely and let the command silently proceed. The empty token is the one\n // exclusion: it names no command and would report as a blank message.\n const unknown = argv._.map(String).find(token => token.length > 0 && !known.has(token))\n if (unknown !== undefined) throw new Error(`Unknown command: ${unknown}`)\n return true\n })\n}\n", "import {\n exitProcess, type FailureExitCodeMapper, ProcessExitError, type ProcessHost, resolveFailureExitCode,\n} from '@ariestools/cli-kit'\nimport type { Argv } from 'yargs'\nimport yargs from 'yargs'\nimport { hideBin } from 'yargs/helpers'\n\nexport interface RunYargsApplicationOptions {\n readonly configure: (parser: Argv) => Argv\n readonly host: ProcessHost\n /**\n * Optional failure policy applied to parser and handler failures alike.\n * The mapper receives the failure origin (`'parse'` for validation\n * failures, `'handler'` for handler or middleware rejections) so exit\n * codes such as `usage` versus `software` need no error sniffing.\n * Returning a number selects the exit code; `undefined`, a thrown mapper,\n * or a result outside the integer range 0\u2013255 retains the default exit\n * code one. Intentional `ProcessExitError` exits are never offered to\n * the mapper.\n */\n readonly mapFailureToExitCode?: FailureExitCodeMapper\n readonly onFailure?: (error: unknown) => Promise<void> | void\n}\n\ninterface CapturedParseOutput {\n error: Error | undefined\n output: string\n}\n\ninterface FailureHandlerResult {\n readonly error?: unknown\n readonly hasError: boolean\n}\n\nasync function invokeFailureHandler(\n handler: RunYargsApplicationOptions['onFailure'],\n error: unknown,\n): Promise<FailureHandlerResult> {\n try {\n await handler?.(error)\n return { hasError: false }\n } catch (failureHandlerError) {\n return { error: failureHandlerError, hasError: true }\n }\n}\n\nfunction reportFailureHandlerError(host: ProcessHost, result: FailureHandlerResult): void {\n if (result.hasError) host.io.error('Error handling application failure:', result.error)\n}\n\n/**\n * Reports a handler-origin failure without the parser usage/help block. A\n * usage dump is right for a parse/usage error but noise for a handler-origin\n * failure such as a mapped configuration error. The raw error (including its\n * stack when present) always reaches stderr so production root-cause diagnosis\n * is preserved; only the usage block is suppressed.\n */\nfunction reportHandlerFailure(host: ProcessHost, error: unknown): void {\n host.io.error(error)\n}\n\n/**\n * Runs a configured Yargs app through the supplied process boundary.\n * Yargs' parse callback prevents its default console and process.exit calls;\n * the adapter then routes the equivalent output and exit through ProcessHost.\n *\n * Failure output is scoped by origin. A parse-origin failure (a Yargs\n * validation or usage error, including one raised by {@link rejectUnknownCommands})\n * still prints the usage/help block, because the caller mis-invoked the CLI.\n * A handler-origin failure (a thrown command handler or middleware, such as a\n * mapped configuration error) prints the raw error to stderr \u2014 including its\n * stack when present \u2014 with no usage dump, since the invocation was well-formed.\n * Exit codes and the {@link FailureExitCodeMapper} contract are unchanged; only\n * what is printed per origin differs.\n */\nexport async function runYargsApplication({\n configure,\n host,\n mapFailureToExitCode,\n onFailure,\n}: RunYargsApplicationOptions): Promise<void> {\n const applicationArguments = hideBin([...host.argv])\n const parser = configure(yargs(applicationArguments))\n const captured: CapturedParseOutput = { error: undefined, output: '' }\n\n try {\n await parser.parseAsync(applicationArguments, {}, (error, _argv, output) => {\n // Yargs 18 reports `null` on success at runtime despite @types/yargs\n // declaring this callback value as `Error | undefined`.\n captured.error = error ?? undefined\n captured.output = output\n })\n } catch (error) {\n if (error instanceof ProcessExitError) throw error\n const failureHandlerResult = await invokeFailureHandler(onFailure, error)\n reportHandlerFailure(host, error)\n reportFailureHandlerError(host, failureHandlerResult)\n exitProcess(host, resolveFailureExitCode(mapFailureToExitCode, error, 'handler'))\n }\n\n if (captured.error !== undefined) {\n const failureHandlerResult = await invokeFailureHandler(onFailure, captured.error)\n if (captured.output.length > 0) {\n host.io.error(captured.output)\n } else {\n host.io.error(captured.error)\n }\n reportFailureHandlerError(host, failureHandlerResult)\n exitProcess(host, resolveFailureExitCode(mapFailureToExitCode, captured.error, 'parse'))\n }\n\n if (captured.output.length > 0) {\n host.io.log(captured.output)\n exitProcess(host, 0)\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -12,7 +12,24 @@ import type { Argv, CommandModule } from 'yargs';
12
12
  * matched command name remains in `argv._`, the check compares each token
13
13
  * against the names declared by `commands`.
14
14
  *
15
- * Two constraints keep the check accurate; both are the caller's to honor:
15
+ * Every non-empty leftover token is inspected regardless of its runtime type.
16
+ * Yargs types `argv._` as `(string | number)[]` and, with its default
17
+ * `parse-positional-numbers` enabled, coerces a bare numeric token to a JS
18
+ * number — so `app 0`, `app 123`, and `app -5` are rejected as
19
+ * `Unknown command: 0` (and so on) rather than slipping through. Consumers do
20
+ * not need `.parserConfiguration({ 'parse-positional-numbers': false })` to get
21
+ * a numeric stray flagged. Two details of that normalization:
22
+ *
23
+ * - The message echoes the coerced value rather than the raw text, so an exotic
24
+ * numeric literal is reported in its normalized form: `1e3` reads
25
+ * `Unknown command: 1000`, `0x10` reads `16`, and `1.50` reads `1.5`. The
26
+ * token is rejected correctly in every case; only the echoed text differs.
27
+ * Yargs has already coerced by the time a `.check()` runs, so the raw text is
28
+ * not recoverable here.
29
+ * - An empty token (`app ""`) is skipped, because it names no command and would
30
+ * report as a blank `Unknown command: `. It falls through to `$0` as before.
31
+ *
32
+ * Four constraints keep the check accurate; all are the caller's to honor:
16
33
  *
17
34
  * - `commands` MUST equal the set registered on the parser via `.command(...)`.
18
35
  * The accepted-name set is derived from this array, not read back from the
@@ -22,15 +39,32 @@ import type { Argv, CommandModule } from 'yargs';
22
39
  * the same array (see the example below) so the two cannot diverge.
23
40
  * - Every accepted positional MUST be declared in its command string as
24
41
  * `<arg>`/`[arg]`/`[arg..]`. Yargs consumes declared positionals out of
25
- * `argv._` before the check runs, so they are safe; but a command that reads
26
- * ad-hoc or variadic positionals it never declared leaves them in `argv._`,
27
- * where this check rejects the first as `Unknown command: <value>`.
42
+ * `argv._` into named keys before the check runs including numeric-valued
43
+ * ones, so `serve <port>` invoked as `serve 8080` is safe — but a command that
44
+ * reads ad-hoc or variadic positionals it never declared leaves them in
45
+ * `argv._`, where this check rejects the first as `Unknown command: <value>`.
46
+ * - A subcommand registered inside a parent command's `builder` MUST also appear
47
+ * in `commands`. Yargs leaves both tokens in `argv._` (`db migrate` yields
48
+ * `['db', 'migrate']`), so a nested name absent from this array is rejected as
49
+ * `Unknown command: migrate`. Passing the nested modules alongside the
50
+ * top-level ones fixes that, at the cost of a flat accepted-name set: the
51
+ * nested name is then also accepted at the top level, where it falls through
52
+ * to `$0`.
53
+ * - Tokens after a `--` separator are NOT passthrough unless the caller enables
54
+ * `.parserConfiguration({ 'populate--': true })`. Yargs leaves `populate--`
55
+ * off by default, which merges those tokens into `argv._` rather than
56
+ * `argv['--']`, so `app local -- raw` is rejected as `Unknown command: raw`
57
+ * (and `app local -- 5` as `Unknown command: 5`). Enabling `populate--` routes
58
+ * them to `argv['--']`, out of `argv._` and out of this check's reach.
28
59
  *
29
60
  * Note: for an unknown token, Yargs runs the failing check first (recording the
30
- * parse-origin failure) but still invokes the `$0` default command's handler
31
- * before surfacing that failure. Keep the default command's handler
32
- * side-effect-free printing a usage block is the intended shape so the
33
- * flagged exit remains authoritative.
61
+ * parse-origin failure) but still invokes the matched command's handler before
62
+ * surfacing that failure the `$0` default command for a bare stray token, and
63
+ * the named command for `app publish 0`. `parseAsync` awaits an async handler to
64
+ * completion, so its side effects commit before the flagged exit. Keep handlers
65
+ * that can be reached alongside a stray token side-effect-free — for `$0`,
66
+ * printing a usage block is the intended shape — so the flagged exit remains
67
+ * authoritative.
34
68
  *
35
69
  * Apply it after every `.command(...)` registration and before `.help()`:
36
70
  *
@@ -1 +1 @@
1
- {"version":3,"file":"rejectUnknownCommands.d.ts","sourceRoot":"","sources":["../../src/rejectUnknownCommands.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,aAAa,EAAE,MAAM,OAAO,CAAA;AAiChD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,aAAa,EAAE,GAAG,IAAI,CAS5F"}
1
+ {"version":3,"file":"rejectUnknownCommands.d.ts","sourceRoot":"","sources":["../../src/rejectUnknownCommands.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,aAAa,EAAE,MAAM,OAAO,CAAA;AAiChD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyEG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,aAAa,EAAE,GAAG,IAAI,CAa5F"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ariestools/cli-kit-yargs",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "Yargs adapter for reusable actor command-line applications",
5
5
  "keywords": [
6
6
  "ariestools",
@@ -45,13 +45,14 @@
45
45
  ],
46
46
  "dependencies": {
47
47
  "@types/yargs": "~17.0.35",
48
- "yargs": "~18.0.0",
49
- "@ariestools/cli-kit": "~1.0.4"
48
+ "yargs": "~18.1.0",
49
+ "@ariestools/cli-kit": "~1.0.6"
50
50
  },
51
51
  "devDependencies": {
52
- "@ariestools/toolchain": "~8.7.24",
53
- "@ariestools/tsconfig": "~8.7.24",
54
- "eslint": "~10.7.0",
52
+ "@ariestools/actor-model": "~1.1.0",
53
+ "@ariestools/toolchain": "~8.7.26",
54
+ "@ariestools/tsconfig": "~8.7.26",
55
+ "eslint": "~10.8.0",
55
56
  "eslint-import-resolver-typescript": "~4.4.5",
56
57
  "typescript": "~6.0.3",
57
58
  "vite": "~8.1.5",