@ariestools/cli-kit-yargs 1.0.1 → 1.0.2
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 +25 -1
- package/dist/node/index.d.ts +1 -0
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.mjs +47 -3
- package/dist/node/index.mjs.map +3 -3
- package/dist/node/rejectUnknownCommands.d.ts +43 -0
- package/dist/node/rejectUnknownCommands.d.ts.map +1 -0
- package/dist/node/runYargsApplication.d.ts +9 -0
- package/dist/node/runYargsApplication.d.ts.map +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -4,7 +4,31 @@ Yargs adapter for reusable actor command-line applications.
|
|
|
4
4
|
|
|
5
5
|
The package keeps Yargs out of `@ariestools/cli-kit` while routing parser help, version, validation errors, asynchronous failures, and exits through the core `ProcessHost` boundary. This makes a configured CLI safe to exercise with a recording host instead of allowing Yargs to write to the ambient console or terminate the test process.
|
|
6
6
|
|
|
7
|
-
`runYargsApplication` accepts an optional `mapFailureToExitCode` mapper applied to parser and handler failures alike: the mapper receives the failure origin (`'parse'` for validation failures, `'handler'` for handler or middleware rejections), returning a number selects the exit code, `undefined` retains the default exit code one
|
|
7
|
+
`runYargsApplication` accepts an optional `mapFailureToExitCode` mapper applied to parser and handler failures alike: the mapper receives the failure origin (`'parse'` for validation failures, `'handler'` for handler or middleware rejections), returning a number selects the exit code, `undefined` retains the default exit code one. A mapper that throws, or returns a code outside the integer range 0–255, falls back to exit code one. Intentional `ProcessExitError` exits pass through untouched.
|
|
8
|
+
|
|
9
|
+
Failure output is scoped by that same origin. A **parse-origin** failure (a Yargs validation or usage error, including one raised by `rejectUnknownCommands`) still prints the usage/help block, because the caller mis-invoked the CLI. A **handler-origin** failure (a thrown command handler or middleware — for example a mapped configuration error that exits `78`) prints only a concise error message to stderr: no usage dump, and outside development (`host.isDevelopment === false`) no stack. In development the full error is surfaced. This keeps a bad `--flag` explanatory while a clean config error stays a one-liner. The exit code and `mapFailureToExitCode` contract are unchanged; only what is printed per origin differs.
|
|
10
|
+
|
|
11
|
+
## Rejecting unknown commands
|
|
12
|
+
|
|
13
|
+
`.strictCommands()` cannot flag an unknown command once a `$0` default command is registered — the default command consumes the stray token as a positional and Yargs accepts it silently. `rejectUnknownCommands(parser, commands)` closes that gap with a `.check()` that compares each leftover positional against the command names (and aliases) declared by `commands`, throwing a parse-origin `Unknown command: <token>` for anything unrecognized. A bare invocation, a registered command with its options, and the `help`/`$0` paths are left untouched. Apply it after every `.command(...)` registration and before `.help()`, passing the same array you registered:
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { rejectUnknownCommands, runYargsApplication } from '@ariestools/cli-kit-yargs'
|
|
17
|
+
|
|
18
|
+
await runYargsApplication({
|
|
19
|
+
host,
|
|
20
|
+
mapFailureToExitCode: (_error, origin) => (origin === 'parse' ? 64 : undefined),
|
|
21
|
+
configure: (parser) => {
|
|
22
|
+
let configured = parser.scriptName('app')
|
|
23
|
+
for (const command of commands) configured = configured.command(command)
|
|
24
|
+
return rejectUnknownCommands(configured, commands).help().version(version)
|
|
25
|
+
},
|
|
26
|
+
})
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Two constraints keep the check accurate. 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. 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>`.
|
|
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).
|
|
8
32
|
|
|
9
33
|
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:
|
|
10
34
|
|
package/dist/node/index.d.ts
CHANGED
package/dist/node/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,+BAA+B,CAAA;AAC7C,cAAc,0BAA0B,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,+BAA+B,CAAA;AAC7C,cAAc,4BAA4B,CAAA;AAC1C,cAAc,0BAA0B,CAAA"}
|
package/dist/node/index.mjs
CHANGED
|
@@ -34,6 +34,40 @@ function environmentToYargsConfig(environment, prefix) {
|
|
|
34
34
|
return config;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
// src/rejectUnknownCommands.ts
|
|
38
|
+
function firstToken(entry) {
|
|
39
|
+
const token = entry.trim().split(/\s+/, 1)[0];
|
|
40
|
+
return token !== void 0 && token.length > 0 ? token : void 0;
|
|
41
|
+
}
|
|
42
|
+
function toEntries(value) {
|
|
43
|
+
if (value === void 0) return [];
|
|
44
|
+
return typeof value === "string" ? [value] : value;
|
|
45
|
+
}
|
|
46
|
+
function knownCommandNames(commands) {
|
|
47
|
+
const names = /* @__PURE__ */ new Set();
|
|
48
|
+
for (const command of commands) {
|
|
49
|
+
for (const entry of toEntries(command.command)) {
|
|
50
|
+
const token = firstToken(entry);
|
|
51
|
+
if (token !== void 0) names.add(token);
|
|
52
|
+
}
|
|
53
|
+
for (const alias of toEntries(command.aliases)) {
|
|
54
|
+
const token = firstToken(alias);
|
|
55
|
+
if (token !== void 0) names.add(token);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return names;
|
|
59
|
+
}
|
|
60
|
+
function rejectUnknownCommands(parser, commands) {
|
|
61
|
+
const known = knownCommandNames(commands);
|
|
62
|
+
return parser.check((argv) => {
|
|
63
|
+
const unknown = argv._.find(
|
|
64
|
+
(token) => typeof token === "string" && token.length > 0 && !known.has(token)
|
|
65
|
+
);
|
|
66
|
+
if (unknown !== void 0) throw new Error(`Unknown command: ${unknown}`);
|
|
67
|
+
return true;
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
37
71
|
// src/runYargsApplication.ts
|
|
38
72
|
import {
|
|
39
73
|
exitProcess,
|
|
@@ -53,6 +87,17 @@ async function invokeFailureHandler(handler, error) {
|
|
|
53
87
|
function reportFailureHandlerError(host, result) {
|
|
54
88
|
if (result.hasError) host.io.error("Error handling application failure:", result.error);
|
|
55
89
|
}
|
|
90
|
+
function toConciseMessage(error) {
|
|
91
|
+
if (error instanceof Error) return error.message.length > 0 ? error.message : error.name;
|
|
92
|
+
return String(error);
|
|
93
|
+
}
|
|
94
|
+
function reportHandlerFailure(host, error) {
|
|
95
|
+
if (host.isDevelopment) {
|
|
96
|
+
host.io.error(error);
|
|
97
|
+
} else {
|
|
98
|
+
host.io.error(toConciseMessage(error));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
56
101
|
async function runYargsApplication({
|
|
57
102
|
configure,
|
|
58
103
|
host,
|
|
@@ -70,9 +115,7 @@ async function runYargsApplication({
|
|
|
70
115
|
} catch (error) {
|
|
71
116
|
if (error instanceof ProcessExitError) throw error;
|
|
72
117
|
const failureHandlerResult = await invokeFailureHandler(onFailure, error);
|
|
73
|
-
host
|
|
74
|
-
host.io.error();
|
|
75
|
-
host.io.error(error);
|
|
118
|
+
reportHandlerFailure(host, error);
|
|
76
119
|
reportFailureHandlerError(host, failureHandlerResult);
|
|
77
120
|
exitProcess(host, resolveFailureExitCode(mapFailureToExitCode, error, "handler"));
|
|
78
121
|
}
|
|
@@ -93,6 +136,7 @@ async function runYargsApplication({
|
|
|
93
136
|
}
|
|
94
137
|
export {
|
|
95
138
|
environmentToYargsConfig,
|
|
139
|
+
rejectUnknownCommands,
|
|
96
140
|
runYargsApplication
|
|
97
141
|
};
|
|
98
142
|
//# sourceMappingURL=index.mjs.map
|
package/dist/node/index.mjs.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../../src/environmentToYargsConfig.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 {\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 * 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 */\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 host.io.error(await parser.getHelp())\n host.io.error()\n host.io.error(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": ";AAMA,SAAS,4BAA4B,OAAuB;AAC1D,QAAM,cAAc,UAAU,MAAM,YAAY,KAAK,UAAU,MAAM,YAAY;AACjF,QAAM,aAAa,cAAc,QAAQ,MAAM,YAAY;AAC3D,MAAI,CAAC,WAAW,SAAS,GAAG,KAAK,CAAC,WAAW,SAAS,GAAG,EAAG,QAAO;AAEnE,MAAI,SAAS;AACb,MAAI,uBAAuB;AAC3B,QAAM,iBAAiB,MAAM,KAAK,UAAU;AAC5C,QAAM,QAAQ,iBAAiB,CAAC,EAAE,UAAU;AAC5C,WAAS,QAAQ,OAAO,QAAQ,WAAW,QAAQ,SAAS;AAC1D,QAAI,YAAY,WAAW,OAAO,KAAK;AACvC,QAAI,sBAAsB;AACxB,6BAAuB;AACvB,kBAAY,UAAU,YAAY;AAAA,IACpC;AACA,QAAI,UAAU,MAAM,cAAc,OAAO,cAAc,MAAM;AAC3D,6BAAuB;AAAA,IACzB,WAAW,cAAc,OAAO,cAAc,KAAK;AACjD,gBAAU;AAAA,IACZ;AAAA,EACF;AACA,SAAO;AACT;AAOO,SAAS,yBACd,aACA,QACoC;AACpC,QAAM,SAA6C,CAAC;AACpD,aAAW,CAAC,iBAAiB,gBAAgB,KAAK,OAAO,QAAQ,WAAW,GAAG;AAC7E,QAAI,CAAC,gBAAgB,WAAW,MAAM,EAAG;AACzC,UAAM,OAAO,gBAAgB,MAAM,IAAI,EAAE,IAAI,CAAC,KAAK,UAAU;AAC3D,aAAO,4BAA4B,UAAU,IAAI,IAAI,MAAM,OAAO,MAAM,IAAI,GAAG;AAAA,IACjF,CAAC;AACD,UAAM,YAAY,KAAK,KAAK,GAAG;AAC/B,QAAI,CAAC,OAAO,OAAO,QAAQ,SAAS,EAAG,QAAO,SAAS,IAAI;AAAA,EAC7D;AACA,SAAO;AACT;;;
|
|
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\nfunction toConciseMessage(error: unknown): string {\n if (error instanceof Error) return error.message.length > 0 ? error.message : error.name\n return String(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. In development the full error\n * (including its stack) is surfaced; otherwise only a concise message reaches\n * stderr, matching the established CLI policy of hiding internal detail outside\n * development.\n */\nfunction reportHandlerFailure(host: ProcessHost, error: unknown): void {\n if (host.isDevelopment) {\n host.io.error(error)\n } else {\n host.io.error(toConciseMessage(error))\n }\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 only a concise error message to stderr \u2014\n * no usage dump and, outside development, no stack \u2014 since the invocation was\n * well-formed. Exit codes and the {@link FailureExitCodeMapper} contract are\n * unchanged; only 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
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Argv, CommandModule } from 'yargs';
|
|
2
|
+
/**
|
|
3
|
+
* Rejects any leftover positional token that does not name a registered
|
|
4
|
+
* command, turning `app bogus` into a parse-origin failure (exit 64 via the
|
|
5
|
+
* usage path when a mapper selects it) while leaving a bare invocation, a
|
|
6
|
+
* registered command, and the `help`/`$0` paths untouched.
|
|
7
|
+
*
|
|
8
|
+
* A `$0` default command consumes stray tokens as its own positionals rather
|
|
9
|
+
* than letting `.strictCommands()` flag them — with a default command in place
|
|
10
|
+
* `.strictCommands()` silently accepts an unknown word — so this explicit,
|
|
11
|
+
* command-name-aware `.check()` is what enforces the command surface. Because a
|
|
12
|
+
* matched command name remains in `argv._`, the check compares each token
|
|
13
|
+
* against the names declared by `commands`.
|
|
14
|
+
*
|
|
15
|
+
* Two constraints keep the check accurate; both are the caller's to honor:
|
|
16
|
+
*
|
|
17
|
+
* - `commands` MUST equal the set registered on the parser via `.command(...)`.
|
|
18
|
+
* The accepted-name set is derived from this array, not read back from the
|
|
19
|
+
* parser, so any drift is silently wrong: a command registered on the parser
|
|
20
|
+
* but omitted here has valid invocations rejected as unknown, and one listed
|
|
21
|
+
* here but never registered lets an unhandled token through. Register and pass
|
|
22
|
+
* the same array (see the example below) so the two cannot diverge.
|
|
23
|
+
* - Every accepted positional MUST be declared in its command string as
|
|
24
|
+
* `<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>`.
|
|
28
|
+
*
|
|
29
|
+
* 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.
|
|
34
|
+
*
|
|
35
|
+
* Apply it after every `.command(...)` registration and before `.help()`:
|
|
36
|
+
*
|
|
37
|
+
* ```ts
|
|
38
|
+
* for (const command of commands) configured = configured.command(command)
|
|
39
|
+
* return rejectUnknownCommands(configured, commands).help().version(version)
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export declare function rejectUnknownCommands(parser: Argv, commands: readonly CommandModule[]): Argv;
|
|
43
|
+
//# sourceMappingURL=rejectUnknownCommands.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -20,6 +20,15 @@ export interface RunYargsApplicationOptions {
|
|
|
20
20
|
* Runs a configured Yargs app through the supplied process boundary.
|
|
21
21
|
* Yargs' parse callback prevents its default console and process.exit calls;
|
|
22
22
|
* the adapter then routes the equivalent output and exit through ProcessHost.
|
|
23
|
+
*
|
|
24
|
+
* Failure output is scoped by origin. A parse-origin failure (a Yargs
|
|
25
|
+
* validation or usage error, including one raised by {@link rejectUnknownCommands})
|
|
26
|
+
* still prints the usage/help block, because the caller mis-invoked the CLI.
|
|
27
|
+
* A handler-origin failure (a thrown command handler or middleware, such as a
|
|
28
|
+
* mapped configuration error) prints only a concise error message to stderr —
|
|
29
|
+
* no usage dump and, outside development, no stack — since the invocation was
|
|
30
|
+
* well-formed. Exit codes and the {@link FailureExitCodeMapper} contract are
|
|
31
|
+
* unchanged; only what is printed per origin differs.
|
|
23
32
|
*/
|
|
24
33
|
export declare function runYargsApplication({ configure, host, mapFailureToExitCode, onFailure, }: RunYargsApplicationOptions): Promise<void>;
|
|
25
34
|
//# sourceMappingURL=runYargsApplication.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runYargsApplication.d.ts","sourceRoot":"","sources":["../../src/runYargsApplication.ts"],"names":[],"mappings":"AAAA,OAAO,EACQ,KAAK,qBAAqB,EAAoB,KAAK,WAAW,EAC5E,MAAM,qBAAqB,CAAA;AAC5B,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,OAAO,CAAA;AAIjC,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,SAAS,EAAE,CAAC,MAAM,EAAE,IAAI,KAAK,IAAI,CAAA;IAC1C,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAA;IAC1B;;;;;;;;;OASG;IACH,QAAQ,CAAC,oBAAoB,CAAC,EAAE,qBAAqB,CAAA;IACrD,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;CAC9D;
|
|
1
|
+
{"version":3,"file":"runYargsApplication.d.ts","sourceRoot":"","sources":["../../src/runYargsApplication.ts"],"names":[],"mappings":"AAAA,OAAO,EACQ,KAAK,qBAAqB,EAAoB,KAAK,WAAW,EAC5E,MAAM,qBAAqB,CAAA;AAC5B,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,OAAO,CAAA;AAIjC,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,SAAS,EAAE,CAAC,MAAM,EAAE,IAAI,KAAK,IAAI,CAAA;IAC1C,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAA;IAC1B;;;;;;;;;OASG;IACH,QAAQ,CAAC,oBAAoB,CAAC,EAAE,qBAAqB,CAAA;IACrD,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;CAC9D;AAiDD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,mBAAmB,CAAC,EACxC,SAAS,EACT,IAAI,EACJ,oBAAoB,EACpB,SAAS,GACV,EAAE,0BAA0B,GAAG,OAAO,CAAC,IAAI,CAAC,CAmC5C"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ariestools/cli-kit-yargs",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Yargs adapter for reusable actor command-line applications",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ariestools",
|
|
@@ -46,11 +46,11 @@
|
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@types/yargs": "~17.0.35",
|
|
48
48
|
"yargs": "~18.0.0",
|
|
49
|
-
"@ariestools/cli-kit": "~1.0.
|
|
49
|
+
"@ariestools/cli-kit": "~1.0.2"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
|
-
"@ariestools/toolchain": "~8.7.
|
|
53
|
-
"@ariestools/tsconfig": "~8.7.
|
|
52
|
+
"@ariestools/toolchain": "~8.7.23",
|
|
53
|
+
"@ariestools/tsconfig": "~8.7.23",
|
|
54
54
|
"eslint": "~10.7.0",
|
|
55
55
|
"eslint-import-resolver-typescript": "~4.4.5",
|
|
56
56
|
"typescript": "~6.0.3",
|