@ariestools/cli-kit-yargs

中文文档
2.1.0发布于 2026/10/07LGPL-3.0-onlyTypeScriptES Module近 30 天下载 2655

Yargs adapter for reusable actor command-line applications

原始使用文档
文档目录3 节
  1. Repeated options
  2. Rejecting unknown commands
  3. License

@ariestools/cli-kit-yargs

Yargs adapter for reusable actor command-line applications.

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.

Help, version, and completion output still end the run the way Yargs' own process exit would. Routing through the host means Yargs no longer ends the process; it records the exit and keeps going. So once Yargs exits zero, runYargsApplication skips every middleware registered on the parser, including .check() callbacks. A root --help, -h, or bare help therefore never reaches an application's configuration middleware, and app bogus --help prints help rather than Unknown command: bogus. Root help also waits for an async $0 builder: Yargs prints that help only after the builder settles, which would otherwise be after the parse has finished. Unknown-command failures stop later middleware and the matched handler while retaining usage output and failure mapping; see Rejecting unknown commands.

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.

Root help uses Yargs' promise-returning getHelp() for its callback output path. A rejecting async $0 builder settles the run through the existing handler-failure boundary: no successful help output or command effects, one onFailure callback, and the original error passed to the mapper. Successful help still waits for the builder, and unused default builders remain lazy. Both middleware phases wait for pending help before deciding whether to run. Explicit string/default showHelp overloads retain native Yargs behavior.

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 the raw error to stderr, including its stack when present, with no usage dump. Production keeps full diagnostics so fatal handler failures remain diagnosable; only the noisy usage block is suppressed. The exit code and mapFailureToExitCode contract are unchanged; only what is printed per origin differs.

Repeated options

configureCliSurface applies one repeated-option policy to every consumer, and withRepeatableOptions(parser) applies the same policy to a parser assembled without it:

  • an option declared array: true / type: 'array' collects every occurrence (--tag a --tag b gives ['a', 'b']), and a variadic positional (serve [files..]) keeps every value;
  • an option declared nargs: n with n > 1 keeps its last n values;
  • every other declared option resolves last-wins under all of its spellings — aliases, camelCase and hyphenated keys, dotted keys — so --out a --out b hands the handler 'b' rather than an array.

The policy keeps yargs' default duplicate-arguments-array: true and restores last-wins in a before-validation middleware, so choices, .check(...), coerce, and the handler see only the surviving value: --mode bogus --mode safe passes choices: ['fast', 'safe'], and --mode safe --mode bogus fails it. Turning duplicate-arguments-array off is no substitute under yargs 18: it keeps only the last occurrence of an array option, truncates a variadic positional to its last value, and can leave an array option's camelCase key disagreeing with its hyphenated key.

import { runYargsApplication, withRepeatableOptions } from '@ariestools/cli-kit-yargs'

await runYargsApplication({
  host,
  configure: parser => withRepeatableOptions(parser.scriptName('app'))
    .command(serveCommand)
    .strictOptions()
    .help(),
})

Three constraints keep the policy intact:

  • Leave duplicate-arguments-array unset in any .parserConfiguration(...) call, including one in a configureCliSurface customize hook. That call replaces yargs' configuration wholesale, and an unset key keeps yargs' default true; withRepeatableOptions never calls it, so it cannot discard settings applied before it.
  • Apply withRepeatableOptions before declaring any option with coerce: yargs runs coerce functions as middleware in registration order, so one registered earlier receives the uncollapsed array.
  • Declare a variadic positional with .positional(...), which records it as an array option (.strictOptions() rejects an undeclared positional anyway). Naming it only through .coerce(...) or a non-array .option(...) declares a single-value option instead, whose values collapse to the last one.

Rejecting unknown commands

.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:

import { rejectUnknownCommands, runYargsApplication } from '@ariestools/cli-kit-yargs'

await runYargsApplication({
  host,
  mapFailureToExitCode: (_error, origin) => (origin === 'parse' ? 64 : undefined),
  configure: (parser) => {
    let configured = parser.scriptName('app')
    for (const command of commands) configured = configured.command(command)
    return rejectUnknownCommands(configured, commands).help().version(version)
  },
})

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.

Four 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.
  • 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>.
  • 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.
  • 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.

When used through runYargsApplication, an unknown-token check stops later middleware and the matched handler: neither the $0 handler for a bare stray nor the publish handler for app publish 0 executes. Usage output, parse-origin mapping, default exit 1, and an application-selected usage exit 64 remain intact. Middleware and builders that ran before the rejecting check retain their existing execution order. This guarantee belongs to the adapter; applications using a standalone Yargs parse callback must provide equivalent failure-exit behavior themselves. The flat accepted-name constraints above remain unchanged.

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:

import { createNodeProcessHostWithDotEnv } from '@ariestools/cli-kit-node'
import { environmentToYargsConfig, runYargsApplication } from '@ariestools/cli-kit-yargs'

const host = createNodeProcessHostWithDotEnv()

await runYargsApplication({
  host,
  configure: parser => parser
    .option('port', { type: 'number' })
    .config(environmentToYargsConfig(host.environment, 'XL1', { allowedKeys: ['port'] }))
    .scriptName('xl1')
    .command(commands)
    .strictOptions()
    .help()
    .version(version),
})

Load dotenv files through @ariestools/cli-kit-node (loadDotEnvFile / createNodeProcessHostWithDotEnv) so values reach host.environment without mutating process.env. This conversion preserves command-line-over-environment precedence, but it is not a general substitute for Yargs-native config-file options: Yargs ranks config objects below config files, and treats a top-level extends key in a config object specially. Such applications should resolve those precedence and reserved-key rules before passing the object to .config().

With strict options, keep the environment allowlist explicit and named in its mapped form (port, rpcUrl). The example declares port globally, so its config is valid for every command. When commands accept different options, declare the option and call configureCommandEnvironment inside each selected leaf's builder. The helper requires an allowlist at this boundary; the standalone converter keeps its optional allowlist. A serve builder can allow port while a status builder allows format:

import { configureCommandEnvironment, configureCliSurface, runYargsApplication } from '@ariestools/cli-kit-yargs'

const commandEnvironment = { environment: host.environment, prefix: 'APP_' }
const commands = [
  {
    command: ['serve', 'run'],
    builder: parser => configureCommandEnvironment(
      parser.option('port', { alias: 'p', type: 'number' }),
      { ...commandEnvironment, allowedKeys: ['port'] },
    ),
    handler: argv => serve(argv.port),
  },
  {
    command: 'status',
    builder: parser => configureCommandEnvironment(
      parser.option('format', { choices: ['json', 'text'], type: 'string' }),
      { ...commandEnvironment, allowedKeys: ['format'] },
    ),
    handler: argv => status(argv.format),
  },
]

await runYargsApplication({
  host,
  configure: parser => configureCliSurface(parser, {
    commands, host, scriptName: 'app', usage: 'app <command>', version,
  }),
})

APP_PORT=8080 APP_FORMAT=json app serve receives only port; app status receives only format. app run -p 9090 overrides APP_PORT, and app serve --format json still fails strict option validation. Include shared global keys in each leaf's allowlist when that leaf accepts them. For nested commands, apply this helper inside the leaf builders rather than the parent's builder: parent config objects are inherited by every child.

Unrelated prefixed variables and other commands' keys are filtered. Keep mistyped CLI flags strict. Root-parser option introspection cannot derive this surface before lazy command builders run. Filtering removes unsupported environment values; applications using config-only fields need a separate config pipeline or explicitly declared options. The generic converter keeps its optional allowlist for consumers that intentionally accept all keys. Existing configureCliSurface.environmentConfig remains a root/global config path and does not infer or filter command-specific keys. Its caller must retain only globally declared keys. The helper does not change Yargs config-file precedence or its special interpretation of a retained top-level extends key.

Mapped dotted keys retain Yargs' own strict-validation rules. With its default dot notation, declaring only api.port still produces an api container that strict options can reject as unknown. A flat dotted option surface can use .parserConfiguration({ 'dot-notation': false }) and declare api.port explicitly. Keep arbitrary nested application configuration in the separate config pipeline rather than treating it as a union of CLI options.

License

LGPL-3.0-only

版本与发布记录

@ariestools/cli-kit-yargs 共发布 15 个版本,首次发布于 2026/07/22;过去 12 个月发布了 15 个版本。

版本发布日期距今
2.1.0latest2026/10/071 个月前
2.0.12026/09/251 个月前
2.0.02026/09/191 个月前
1.2.32026/08/112 个月前
1.2.22026/08/112 个月前
1.2.12026/08/092 个月前

关于 @ariestools/cli-kit-yargs 的常见问题

@ariestools/cli-kit-yargs 是什么?

Yargs adapter for reusable actor command-line applications

如何安装 @ariestools/cli-kit-yargs?

在项目目录运行 npm install @ariestools/cli-kit-yargs,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:yargs(^18.2)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@ariestools/cli-kit-yargs 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@ariestools/cli-kit-yargs 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@ariestools/cli-kit-yargs 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=26。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@ariestools/cli-kit-yargs 可以商用吗?

@ariestools/cli-kit-yargs 使用 LGPL-3.0-only 许可。弱 Copyleft:作为库被调用时通常不要求开源你自己的代码,但修改库本身需以 LGPL 开源;前端打包合并后的适用边界存在不同理解。以上为概括,不构成法律意见,请以许可原文为准。

@ariestools/cli-kit-yargs 的下载量有多少?

根据 npm 官方下载统计,@ariestools/cli-kit-yargs 在 2026/10/07 – 2026/11/05 这 30 天内被下载 2,655 次,日均约 89 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@ariestools/cli-kit-yargs 还在维护吗?

最新版本 2.1.0 发布于 2026/10/07(1 个月前);至今共发布 15 个版本,其中过去 12 个月发布了 15 个;首次发布于 2026/07/22。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。