diff --git a/.agents/notes/implemented/process/2026-09-14-generated-cli-help-reference.i18n.yaml b/.agents/notes/implemented/process/2026-09-14-generated-cli-help-reference.i18n.yaml new file mode 100644 index 0000000000..b0b5d6295a --- /dev/null +++ b/.agents/notes/implemented/process/2026-09-14-generated-cli-help-reference.i18n.yaml @@ -0,0 +1,19 @@ +# Bilingual-pair consistency record for 2026-09-14-generated-cli-help-reference.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-09-14-generated-cli-help-reference.md +/agent-note-generate-the-cli-reference-from-command-help: + en: 618bafe3fd2afdb9 + zh: 433df2a8d0530c41 +/agent-note-generate-the-cli-reference-from-command-help/problem: + en: 2ad571458d5d8191 + zh: 4db6bddbad65bf5c +/agent-note-generate-the-cli-reference-from-command-help/decision: + en: d99a93133c460385 + zh: 0f710ecf6e7137f2 +/agent-note-generate-the-cli-reference-from-command-help/alternatives-considered: + en: df2f7f07b3e84418 + zh: 2dd87333d9d460c3 +/agent-note-generate-the-cli-reference-from-command-help/consequences: + en: aada6686406c892e + zh: 9a30b6b1c5ab6d55 diff --git a/.agents/notes/implemented/process/2026-09-14-generated-cli-help-reference.md b/.agents/notes/implemented/process/2026-09-14-generated-cli-help-reference.md new file mode 100644 index 0000000000..2a33722fe0 --- /dev/null +++ b/.agents/notes/implemented/process/2026-09-14-generated-cli-help-reference.md @@ -0,0 +1,25 @@ +# Agent Note: Generate the CLI reference from command help + +Status: implemented + +English | [中文](2026-09-14-generated-cli-help-reference.zh.md) + +## Problem + +Launcher flags, profile options, and nested commands have separate owners. A manually maintained command reference can omit subcommands or retain obsolete defaults. + +## Decision + +The CLI reference captures the supported `dsh` launcher's help, the plugin forwarder's help, and each shipped profile's help, then recursively discovers the subcommands listed in each page's `Commands:` section. Shipped profile names come from `PROFILE_TEMPLATES`. Every page retains its complete help text; identical alias help appears once with all invocation forms. + +Generation uses temporary Harness and Agents homes. Help exits before application work starts and requires no server or model. The plugin forwarder exposes its own help before pnpm arguments; flags after the pnpm command remain pnpm input. + +The generated bilingual pages retain CLI output in its original language. `verify-cli-help` compares the complete pages and pairing record and runs with the documentation checks. + +## Alternatives considered + +A second command inventory or help-specific runtime API would duplicate the parser's definitions. Reading actual help keeps the reference tied to the same descriptions, options, defaults, and examples users receive. + +## Consequences + +Command changes update the reference through the same generator. Reviewers can compare complete help without assembling separate command invocations. diff --git a/.agents/notes/implemented/process/2026-09-14-generated-cli-help-reference.zh.md b/.agents/notes/implemented/process/2026-09-14-generated-cli-help-reference.zh.md new file mode 100644 index 0000000000..beba0b1ce2 --- /dev/null +++ b/.agents/notes/implemented/process/2026-09-14-generated-cli-help-reference.zh.md @@ -0,0 +1,25 @@ +# Agent Note: 从命令帮助生成 CLI 参考 + +Status: implemented + +[English](2026-09-14-generated-cli-help-reference.md) | 中文 + +## Problem + +启动器选项、profile 选项和嵌套命令由不同部分负责。手工维护的命令参考可能遗漏子命令或保留过时的默认值。 + +## Decision + +CLI 参考采集受支持的 `dsh` 启动器帮助、插件转发器帮助和各随附 profile 的帮助,并递归发现每页 `Commands:` 段落列出的子命令。随附 profile 的名称来自 `PROFILE_TEMPLATES`。每页都保留完整的帮助文本;相同的别名帮助只展示一次,并列出所有调用形式。 + +生成过程使用临时 Harness home 和 Agents home。帮助在应用开始工作前退出,不需要服务器或模型。插件转发器在 pnpm 参数之前提供自己的帮助;pnpm 命令之后的选项仍作为 pnpm 输入。 + +生成的双语页面保留 CLI 输出的原始语言。`verify-cli-help` 比较完整页面和配对记录,并随文档检查一起运行。 + +## Alternatives considered + +另一份命令清单或专供帮助使用的运行时 API 会重复解析器的定义。读取实际帮助,可以让参考文档始终对应用户收到的说明、选项、默认值和示例。 + +## Consequences + +命令变化通过同一个生成器更新参考文档。审阅者可以直接比较完整帮助,无需自行拼接各个命令的调用结果。 diff --git a/apps/cli/reference/README.i18n.yaml b/apps/cli/reference/README.i18n.yaml index b5a62ccbed..3da6cf51e6 100644 --- a/apps/cli/reference/README.i18n.yaml +++ b/apps/cli/reference/README.i18n.yaml @@ -3,8 +3,8 @@ # After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write apps/cli/reference/README.md /dsh-cli-behavior-reference: - en: d25712a65d21ab45 - zh: b1c57b706bc64085 + en: 5a4f43756dae900e + zh: d58c479481cfa5fe /dsh-cli-behavior-reference/profile-boot: en: e3a159750f5f9ed3 zh: e02d9004fcc239f9 @@ -18,8 +18,8 @@ en: d6368bf97a02bd57 zh: 2ab7888a108961ce /dsh-cli-behavior-reference/plugin-management: - en: 9980ff1600fa9460 - zh: cc66ba45e27b5540 + en: 142404f01a747121 + zh: a6a5edbb0614bc62 /dsh-cli-behavior-reference/web-profile: en: 42f0b9349cf1574b zh: a3cc6e352e71badb diff --git a/apps/cli/reference/README.md b/apps/cli/reference/README.md index edc3dd4272..865f955037 100644 --- a/apps/cli/reference/README.md +++ b/apps/cli/reference/README.md @@ -4,6 +4,8 @@ English | [中文](README.zh.md) This reference defines the profile, plugin-management, and config-dump command modes. Argv is parsed once through [`src/args.ts`](../src/args.ts), and [`src/bin.ts`](../src/bin.ts) dynamically imports only the selected runner. +The [generated command help](../../../docs/cli-help.md) contains complete launcher, plugin-management, and shipped-profile help. Run `pnpm run gen-cli-help` to refresh it; `pnpm run verify-cli-help` checks it against the current CLI. + ## Profile boot `dsh ` abbreviates `dsh --profile ` and boots the profile at `$DSH_HOME/profiles/`. The shorthand name must immediately follow `dsh`; `plugin` remains the plugin-management command, so boot a profile with that name using `dsh --profile plugin`. The effective tree is composed over an empty root by applying, in order: each bundle patch named in the profile manifest's `dsh.profile.bundles` list, the profile's own `cordis.patch.yml`, the home-level `$DSH_HOME/cordis.patch.yml` (machine-local preferences shared by every profile, so it outranks the per-profile layer), and each `--patch ` overlay in argv order. Later layers win per row; a patch replaces the targeted row's complete `config` value rather than deep-merging keys, and may insert new rows. The final YAML composition controls whether `dsh-hmr` watches configuration; without HMR, changes require restart. A parse, schema, resolution, or plugin boot failure is reported and exits nonzero. SIGINT and SIGTERM dispose the mounted root before exit. @@ -80,6 +82,8 @@ New directories and files request modes `0700` and `0600` on POSIX. The CLI prin `dsh plugin --profile ` initializes the profile when missing (shipped template, or `@deepseek-ai/dsh-base` alone for other names), then forwards `` to `pnpm` with the profile directory as working directory — `add`, `remove`, `why`, `update`, and every other pnpm verb work unchanged; pnpm must be on PATH. Relative path specs (`.`, `../plugin`, and their `file:`/`link:` forms) are anchored to the invoking directory first, so `add .` from a plugin checkout installs that checkout, not the profile. After every successful run, `dsh.profile.bundles` is reconciled against the installed state: each dependency resolving to a package whose manifest declares `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }` joins the layer stack (so an `update` that gains the declaration activates it), a bundle-less dependency stays plain with a one-time warning, and a removed dependency leaves the stack. +`dsh plugin` reads its own options only before the forwarded arguments, so `--profile ` comes first. When the forwarded arguments begin with the pnpm command, everything after that command reaches pnpm verbatim, including `-h`, `--help`, and `--profile`. Otherwise `-h` or `--help` prints the forwarder's own help and exits 0. + The Codex and Claude Code subagent providers are separate optional Bundles. Add either package, both in one command, or remove either package independently: ```sh diff --git a/apps/cli/reference/README.zh.md b/apps/cli/reference/README.zh.md index e4c4be781c..9cfc5327a3 100644 --- a/apps/cli/reference/README.zh.md +++ b/apps/cli/reference/README.zh.md @@ -4,6 +4,8 @@ 本参考定义 profile 启动、插件管理和配置 dump 等命令模式。argv 由 [`src/args.ts`](../src/args.ts) 统一解析一次,[`src/bin.ts`](../src/bin.ts) 只会动态导入选中的运行器。 +[生成的命令帮助](../../../docs/cli-help.zh.md)包含启动器、插件管理和随附 profile 的完整帮助。运行 `pnpm run gen-cli-help` 刷新,运行 `pnpm run verify-cli-help` 检查它是否与当前 CLI 一致。 + ## Profile 启动 @@ -82,6 +84,8 @@ dsh --profile web --patch ./extra.yml --dump-config `dsh plugin --profile ` 在 profile 缺失时先初始化它(有随附模板的用模板,其他名称只装 `@deepseek-ai/dsh-base`),然后以 profile 目录为工作目录,把 `` 转发给 `pnpm`:`add`、`remove`、`why`、`update` 及其他所有 pnpm 子命令都照常可用;pnpm 必须在 PATH 上。相对路径 spec(`.`、`../plugin` 及其 `file:`/`link:` 形式)会先锚定到调用目录,因此在插件 checkout 中执行 `add .` 安装的是该 checkout,而不是 profile。每次成功运行后,系统都会根据当前安装状态更新 `dsh.profile.bundles`:如果某项依赖解析到的包在 manifest 中声明了 `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }`,该依赖就会加入配置层栈;如果某项依赖在 `update` 后获得该声明,也会随即激活。没有组合包声明的依赖仍作为普通依赖保留,并显示一次性警告;已移除的依赖则从配置层栈中删除。 +`dsh plugin` 只在被转发的参数之前读取自己的选项,因此 `--profile ` 必须写在最前面。被转发的参数以 pnpm 命令开头时,该命令之后的所有内容都会原样交给 pnpm,包括 `-h`、`--help` 和 `--profile`。其他情况下,`-h` 或 `--help` 会打印转发器自身的帮助并以 0 退出。 + Codex 与 Claude Code subagent 提供方是两个彼此独立的可选组合包。可以只添加一个包、在同一命令中添加两个包,或独立移除任一包: ```sh diff --git a/apps/cli/src/args.ts b/apps/cli/src/args.ts index 7adf1913a3..465208b778 100644 --- a/apps/cli/src/args.ts +++ b/apps/cli/src/args.ts @@ -187,6 +187,8 @@ export function parseDshArgs(argv: readonly string[], version: string, manageDes if (first === 'plugin') { const plugin = program.command('plugin').description('manage a profile\'s plugins by forwarding the remaining arguments to pnpm in the profile directory') plugin + .helpOption('-h, --help', 'show this help') + .passThroughOptions() .requiredOption('--profile ', 'the profile whose plugins to manage (initialized on first use)', selectProfile) .allowUnknownOption() .argument('[args...]', 'pnpm arguments, forwarded verbatim (add , remove , why , ...)') diff --git a/apps/cli/tests/args.spec.ts b/apps/cli/tests/args.spec.ts index 88138f2dec..e331fa4725 100644 --- a/apps/cli/tests/args.spec.ts +++ b/apps/cli/tests/args.spec.ts @@ -120,6 +120,21 @@ describe('parseDshArgs', () => { // Unknown pnpm flags forward verbatim. expect(parse(['plugin', '--profile', 'tui', 'add', '--save-dev', 'x'])) .toEqual({ mode: 'plugin', profile: 'tui', args: ['add', '--save-dev', 'x'] }) + // Options after the pnpm command belong to pnpm, including the forwarder's own names. + expect(parse(['plugin', '--profile', 'tui', 'add', '--help'])) + .toEqual({ mode: 'plugin', profile: 'tui', args: ['add', '--help'] }) + expect(parse(['plugin', '--profile', 'tui', 'add', '-h', '--profile', 'x'])) + .toEqual({ mode: 'plugin', profile: 'tui', args: ['add', '-h', '--profile', 'x'] }) + expect(exitCode(['plugin', 'add', 'x', '--profile', 'tui'])).toBe(1) + }) + + it.each([ + ['plugin', '--help'], ['plugin', '-h'], ['plugin', '--profile', 'tui', '--help'], + ['plugin', '--profile', 'tui', '--filter', 'x', 'add', '--help'], + ])('prints the plugin forwarder help for %j', (...argv: string[]) => { + const stdout = vi.spyOn(process.stdout, 'write').mockReturnValue(true) + expect(exitCode(argv)).toBe(0) + expect(stdout.mock.calls.map(([chunk]) => String(chunk)).join('')).toContain('Usage: dsh plugin [options] [args...]') }) it.each(['desktop', 'Desktop', 'DESKTOP'])('permits installed Desktop plugin commands for %s', (profile) => { diff --git a/docs/cli-help.i18n.yaml b/docs/cli-help.i18n.yaml new file mode 100644 index 0000000000..91bcf85e29 --- /dev/null +++ b/docs/cli-help.i18n.yaml @@ -0,0 +1,34 @@ +# Bilingual-pair consistency record for cli-help.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write docs/cli-help.md +/: + en: 24a3ad7efe7ed273 + zh: 24a3ad7efe7ed273 +/dsh-cli-help: + en: b7174eab3476579d + zh: 5ddab967320e3cff +/dsh-cli-help/contents: + en: 00308bcc6dc90afd + zh: 16f7442b76686a02 +/dsh-cli-help/dsh: + en: e0695abcf7d8b521 + zh: 712c23b3c8a115ca +/dsh-cli-help/dsh-plugin: + en: adad99c125d0ac51 + zh: f11024971184f737 +/dsh-cli-help/dsh-acp: + en: bcb0f84577ce5f03 + zh: 4382e617006849db +/dsh-cli-help/dsh-headless: + en: 878a1dd78017964d + zh: bb43d91b4beb9bda +/dsh-cli-help/dsh-sdk: + en: 701f239c5157dedb + zh: ee11be2932b05ce4 +/dsh-cli-help/dsh-sdk-minimal: + en: 8f0d7548d299ffd3 + zh: fa2262e70d6819a0 +/dsh-cli-help/dsh-web: + en: 5d073104e8954cc8 + zh: 91cd28f7c6ac96ac diff --git a/docs/cli-help.md b/docs/cli-help.md new file mode 100644 index 0000000000..2f2c495216 --- /dev/null +++ b/docs/cli-help.md @@ -0,0 +1,193 @@ + + +# dsh CLI help + +This reference contains complete help for the launcher, subcommands, and shipped profiles. Identical help appears once with every invocation listed. Help output retains its original language. + +Run `pnpm run gen-cli-help` to regenerate; run `pnpm run verify-cli-help` to check freshness. + +## Contents + +- [`dsh`](#dsh) +- [`dsh plugin`](#dsh-plugin) +- [`dsh acp`](#dsh-acp) +- [`dsh headless`](#dsh-headless) +- [`dsh sdk`](#dsh-sdk) +- [`dsh sdk-minimal`](#dsh-sdk-minimal) +- [`dsh web`](#dsh-web) + + +## `dsh` + +Invocations: `dsh --help` + +```text +Usage: dsh [--profile] [options] [app-args...] + dsh plugin --profile + +dsh: boot a DeepSeek Harness profile — an ordered stack of plugin-bundle patch +layers under your own overrides. + +Arguments: + args arguments for the booted profile's app (see: + dsh --profile --help) + +Options: + -V, --version output the version number + --profile the profile under $DSH_HOME/profiles to boot + --from-default-profile initialize a new custom profile from a shipped + profile template + --patch extra patch-list overlay applied after the + profile layer (repeatable) + --dump-config print the composed profile tree and exit + --dump-config-schema print JSON Schema for profile entries and + patches without mounting + --dump-default-config print the profile tree without its user layer + or --patch overlays and exit + +Examples: + dsh web boot the web profile (same as: dsh --profile web) + dsh rescue --from-default-profile web + create rescue from the shipped web template, then boot it + dsh headless "run the tests" answer one task, print the result, and exit + dsh tui --patch ./extra.yml boot a custom profile with one extra overlay + dsh tui --resume arguments after the launcher flags reach the app + dsh web --help the web app's own flags and help + dsh plugin --profile tui add install a plugin into the tui profile + +``` + + +## `dsh plugin` + +Invocations: `dsh plugin --help` + +```text +Usage: dsh plugin [options] [args...] + +manage a profile's plugins by forwarding the remaining arguments to pnpm in the +profile directory + +Arguments: + args pnpm arguments, forwarded verbatim (add , remove , + why , ...) + +Options: + --profile the profile whose plugins to manage (initialized on first + use) + -h, --help show this help +``` + + +## `dsh acp` + +Invocations: `dsh acp --help`, `dsh --profile acp --help` + +```text +Usage: dsh --profile acp [options] + +Serve automation clients over Agent Client Protocol stdio. + +Options: + -h, --help show this help + +Example: + dsh --profile acp serve ACP until the client disconnects + +``` + + +## `dsh headless` + +Invocations: `dsh headless --help`, `dsh --profile headless --help` + +```text +Usage: dsh --profile headless [options] [task...] + +Answer one task and exit; the answer goes to stdout and diagnostics to stderr. + +Arguments: + task the task text; multiple words are joined by spaces, and `-` + reads stdin + +Options: + --json write newline-delimited run events to stdout instead of the + final message + --session-id adopt the persisted Session with this id; an unknown id is + an error + -h, --help show this help + +Examples: + dsh --profile headless "run the tests" answer one task and exit + echo "run the tests" | dsh --profile headless read the task from stdin + dsh --profile headless --json "run the tests" emit machine-readable run events + dsh --profile headless --session-id session-… "continue" resume an existing Session + +``` + + +## `dsh sdk` + +Invocations: `dsh sdk --help`, `dsh --profile sdk --help` + +```text +Usage: dsh --profile sdk [options] + +Serve DeepSeek Harness SDK clients over stdio JSON-RPC. + +Options: + -h, --help show this help + +Example: + dsh --profile sdk serve one SDK runtime until its client disconnects + +``` + + +## `dsh sdk-minimal` + +Invocations: `dsh sdk-minimal --help`, `dsh --profile sdk-minimal --help` + +```text +Usage: dsh --profile sdk-minimal [options] + +Serve DeepSeek Harness SDK clients over stdio JSON-RPC. + +Options: + -h, --help show this help + +Example: + dsh --profile sdk-minimal serve one SDK runtime until its client disconnects + +``` + + +## `dsh web` + +Invocations: `dsh web --help`, `dsh --profile web --help` + +```text +Usage: dsh --profile web [options] + +Serve the DeepSeek Harness browser UI. + +Options: + --host bind host + --no-open do not open the Web UI in the default browser + --port listen port; pass 0 to let the OS pick a free + one + --public-url advertise this HTTP(S) root in the printed, + opened, web-surface, and DSH_WEB_URL forms; + grants no trust + --trusted-host extra authority the /api browser-trust fence + accepts (host or host:port; repeatable) + -h, --help show this help + +Examples: + dsh --profile web serve on the composed host and port + dsh --profile web --no-open serve without opening a browser + dsh --profile web --port 8080 serve on another port + dsh --profile web --public-url https://app.example/ui/ --trusted-host app.example + advertise a prefix-stripping HTTPS proxy entry and admit its authority + +``` diff --git a/docs/cli-help.zh.md b/docs/cli-help.zh.md new file mode 100644 index 0000000000..d4f5238c3b --- /dev/null +++ b/docs/cli-help.zh.md @@ -0,0 +1,195 @@ + + +# dsh 命令行帮助 + +[English](cli-help.md) | 中文 + +此参考收录启动器、子命令和随附 profile 的完整帮助。同一份帮助只展示一次,并列出所有调用方式。帮助输出保留原文。 + +运行 `pnpm run gen-cli-help` 重新生成;运行 `pnpm run verify-cli-help` 检查是否与当前命令一致。 + +## 目录 + +- [`dsh`](#dsh) +- [`dsh plugin`](#dsh-plugin) +- [`dsh acp`](#dsh-acp) +- [`dsh headless`](#dsh-headless) +- [`dsh sdk`](#dsh-sdk) +- [`dsh sdk-minimal`](#dsh-sdk-minimal) +- [`dsh web`](#dsh-web) + + +## `dsh` + +调用方式: `dsh --help` + +```text +Usage: dsh [--profile] [options] [app-args...] + dsh plugin --profile + +dsh: boot a DeepSeek Harness profile — an ordered stack of plugin-bundle patch +layers under your own overrides. + +Arguments: + args arguments for the booted profile's app (see: + dsh --profile --help) + +Options: + -V, --version output the version number + --profile the profile under $DSH_HOME/profiles to boot + --from-default-profile initialize a new custom profile from a shipped + profile template + --patch extra patch-list overlay applied after the + profile layer (repeatable) + --dump-config print the composed profile tree and exit + --dump-config-schema print JSON Schema for profile entries and + patches without mounting + --dump-default-config print the profile tree without its user layer + or --patch overlays and exit + +Examples: + dsh web boot the web profile (same as: dsh --profile web) + dsh rescue --from-default-profile web + create rescue from the shipped web template, then boot it + dsh headless "run the tests" answer one task, print the result, and exit + dsh tui --patch ./extra.yml boot a custom profile with one extra overlay + dsh tui --resume arguments after the launcher flags reach the app + dsh web --help the web app's own flags and help + dsh plugin --profile tui add install a plugin into the tui profile + +``` + + +## `dsh plugin` + +调用方式: `dsh plugin --help` + +```text +Usage: dsh plugin [options] [args...] + +manage a profile's plugins by forwarding the remaining arguments to pnpm in the +profile directory + +Arguments: + args pnpm arguments, forwarded verbatim (add , remove , + why , ...) + +Options: + --profile the profile whose plugins to manage (initialized on first + use) + -h, --help show this help +``` + + +## `dsh acp` + +调用方式: `dsh acp --help`, `dsh --profile acp --help` + +```text +Usage: dsh --profile acp [options] + +Serve automation clients over Agent Client Protocol stdio. + +Options: + -h, --help show this help + +Example: + dsh --profile acp serve ACP until the client disconnects + +``` + + +## `dsh headless` + +调用方式: `dsh headless --help`, `dsh --profile headless --help` + +```text +Usage: dsh --profile headless [options] [task...] + +Answer one task and exit; the answer goes to stdout and diagnostics to stderr. + +Arguments: + task the task text; multiple words are joined by spaces, and `-` + reads stdin + +Options: + --json write newline-delimited run events to stdout instead of the + final message + --session-id adopt the persisted Session with this id; an unknown id is + an error + -h, --help show this help + +Examples: + dsh --profile headless "run the tests" answer one task and exit + echo "run the tests" | dsh --profile headless read the task from stdin + dsh --profile headless --json "run the tests" emit machine-readable run events + dsh --profile headless --session-id session-… "continue" resume an existing Session + +``` + + +## `dsh sdk` + +调用方式: `dsh sdk --help`, `dsh --profile sdk --help` + +```text +Usage: dsh --profile sdk [options] + +Serve DeepSeek Harness SDK clients over stdio JSON-RPC. + +Options: + -h, --help show this help + +Example: + dsh --profile sdk serve one SDK runtime until its client disconnects + +``` + + +## `dsh sdk-minimal` + +调用方式: `dsh sdk-minimal --help`, `dsh --profile sdk-minimal --help` + +```text +Usage: dsh --profile sdk-minimal [options] + +Serve DeepSeek Harness SDK clients over stdio JSON-RPC. + +Options: + -h, --help show this help + +Example: + dsh --profile sdk-minimal serve one SDK runtime until its client disconnects + +``` + + +## `dsh web` + +调用方式: `dsh web --help`, `dsh --profile web --help` + +```text +Usage: dsh --profile web [options] + +Serve the DeepSeek Harness browser UI. + +Options: + --host bind host + --no-open do not open the Web UI in the default browser + --port listen port; pass 0 to let the OS pick a free + one + --public-url advertise this HTTP(S) root in the printed, + opened, web-surface, and DSH_WEB_URL forms; + grants no trust + --trusted-host extra authority the /api browser-trust fence + accepts (host or host:port; repeatable) + -h, --help show this help + +Examples: + dsh --profile web serve on the composed host and port + dsh --profile web --no-open serve without opening a browser + dsh --profile web --port 8080 serve on another port + dsh --profile web --public-url https://app.example/ui/ --trusted-host app.example + advertise a prefix-stripping HTTPS proxy entry and admit its authority + +``` diff --git a/docs/upgrade-guide/v0.2.1-alpha.1/plugin-option-placement/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-alpha.1/plugin-option-placement/guide.i18n.yaml new file mode 100644 index 0000000000..724c2237e7 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.1/plugin-option-placement/guide.i18n.yaml @@ -0,0 +1,16 @@ +# Bilingual-pair consistency record for guide.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write docs/upgrade-guide/v0.2.1-alpha.1/plugin-option-placement/guide.md +/: + en: 26325999c8fa35d8 + zh: 1cee6fc8d459053c +/dsh-plugin-option-placement: + en: 3d717a2b7e9087d3 + zh: e6004d015e6a4829 +/dsh-plugin-option-placement/change: + en: 27539de1a79a7eec + zh: 3860da746b0f24a5 +/dsh-plugin-option-placement/migration: + en: 543880bb55c786bc + zh: 0be3793b66b0c1e6 diff --git a/docs/upgrade-guide/v0.2.1-alpha.1/plugin-option-placement/guide.md b/docs/upgrade-guide/v0.2.1-alpha.1/plugin-option-placement/guide.md new file mode 100644 index 0000000000..058d910413 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.1/plugin-option-placement/guide.md @@ -0,0 +1,20 @@ +--- +kind: upgrade-guide +description: "dsh plugin reads --profile and its own help only before the forwarded pnpm arguments." +--- + +# `dsh plugin` option placement + +English | [中文](guide.zh.md) + +## Change + +`dsh plugin` reads its own options only before the forwarded pnpm arguments. Previously it accepted `--profile ` anywhere on the command line, had no help of its own, and forwarded `-h` and `--help` to pnpm from any position. + +When the forwarded arguments begin with the pnpm command, everything after that command now reaches pnpm verbatim, including `-h`, `--help`, and `--profile`. Otherwise `-h` or `--help` prints the `dsh plugin` help and exits 0. Scripts that place `--profile` after the pnpm command fail with `required option '--profile ' not specified`, and `dsh plugin --profile --help` no longer prints pnpm's help. + +## Migration + +1. Move `--profile ` before the pnpm command: replace `dsh plugin add --profile ` with `dsh plugin --profile add `. +2. Request pnpm's help for one command after that command, as in `dsh plugin --profile add --help`. For pnpm's general help, forward `help`: `dsh plugin --profile help`. +3. Confirm that `dsh plugin --help` prints `Usage: dsh plugin [options] [args...]` and that the commands from steps 1 and 2 reach pnpm. diff --git a/docs/upgrade-guide/v0.2.1-alpha.1/plugin-option-placement/guide.zh.md b/docs/upgrade-guide/v0.2.1-alpha.1/plugin-option-placement/guide.zh.md new file mode 100644 index 0000000000..d3924b2507 --- /dev/null +++ b/docs/upgrade-guide/v0.2.1-alpha.1/plugin-option-placement/guide.zh.md @@ -0,0 +1,20 @@ +--- +kind: upgrade-guide +description: "dsh plugin 只在被转发的 pnpm 参数之前读取 --profile 和自身的帮助选项。" +--- + +# `dsh plugin` 选项位置 + +[English](guide.md) | 中文 + +## 变更 + +`dsh plugin` 只在被转发的 pnpm 参数之前读取自己的选项。此前它接受出现在命令行任意位置的 `--profile `,没有自己的帮助,并且无论 `-h` 和 `--help` 位于何处都会转发给 pnpm。 + +被转发的参数以 pnpm 命令开头时,该命令之后的所有内容现在都会原样交给 pnpm,包括 `-h`、`--help` 和 `--profile`。其他情况下,`-h` 或 `--help` 会打印 `dsh plugin` 的帮助并以 0 退出。把 `--profile` 写在 pnpm 命令之后的脚本会因 `required option '--profile ' not specified` 而失败,`dsh plugin --profile --help` 也不再打印 pnpm 的帮助。 + +## 迁移 + +1. 把 `--profile ` 移到 pnpm 命令之前:将 `dsh plugin add --profile ` 改为 `dsh plugin --profile add `。 +2. 查看某个 pnpm 命令的帮助时,把帮助选项写在该命令之后,例如 `dsh plugin --profile add --help`。查看 pnpm 的总体帮助时转发 `help`:`dsh plugin --profile help`。 +3. 确认 `dsh plugin --help` 打印 `Usage: dsh plugin [options] [args...]`,并且第 1、2 步中的命令能够到达 pnpm。 diff --git a/package.json b/package.json index f4970a1730..febdd038f7 100644 --- a/package.json +++ b/package.json @@ -150,6 +150,8 @@ "verify-client-domain-graph": "tsx scripts/verify-client-domain-graph.ts", "gen-tsconfig-paths": "tsx scripts/gen-tsconfig-paths.ts", "verify-tsconfig-paths": "tsx scripts/gen-tsconfig-paths.ts --check", + "gen-cli-help": "tsx scripts/gen-cli-help.ts", + "verify-cli-help": "tsx scripts/gen-cli-help.ts --check", "gen-cordis-catalog": "tsx scripts/gen-cordis-catalog.ts", "verify-cordis-catalog": "tsx scripts/gen-cordis-catalog.ts --check", "gen-cordis-api": "tsx scripts/gen-cordis-api.ts", diff --git a/scripts/gen-cli-help.spec.ts b/scripts/gen-cli-help.spec.ts new file mode 100644 index 0000000000..27b76764c9 --- /dev/null +++ b/scripts/gen-cli-help.spec.ts @@ -0,0 +1,34 @@ +/** Command discovery and complete output preservation for the CLI reference. */ + +import { describe, expect, it } from 'vitest' +import { collectCliHelp, parseCliHelp } from './gen-cli-help.ts' + +describe('CLI help discovery', () => { + it('reads command rows while retaining wrapped descriptions and examples', () => { + const help = 'Usage: dsh\r\n\r\nCommands:\r\n web [options] Serve the browser\r\n with a wrapped description\r\n plugin Manage plugins\r\n\r\nExamples:\r\n dsh web\r\n' + const result = parseCliHelp(help) + expect(result.commands).toEqual(['web', 'plugin']) + expect(result.output).toBe(help.replaceAll('\r\n', '\n')) + }) + + it('includes plugin, both profile forms, and their nested commands when launcher help has no command list', async () => { + const visited: string[] = [] + const pages = await collectCliHelp(async (args) => { + visited.push(args.join(' ')) + if (args.length === 0) return 'Usage: dsh [--profile] \n' + if (args[0] === 'plugin') return 'Usage: dsh plugin --profile \n' + const selected = args[0] === '--profile' ? args.slice(1) : args + if (selected[1] === 'nested') return 'Usage: nested\n' + return `Usage: ${selected[0]} [command]\n\nCommands:\n nested Run the nested command\n` + }, ['web']) + expect(visited).toEqual(['', 'plugin', 'web', 'web nested', '--profile web']) + expect(pages.map(page => page.invocations)).toEqual([ + ['dsh'], ['dsh plugin'], ['dsh web', 'dsh --profile web'], + ['dsh web nested', 'dsh --profile web nested'], + ]) + }) + + it.each(['', ' \r\n\n'])('rejects blank help %j', (help) => { + expect(() => parseCliHelp(help)).toThrow('CLI help returned empty output') + }) +}) diff --git a/scripts/gen-cli-help.ts b/scripts/gen-cli-help.ts new file mode 100644 index 0000000000..7ddc5fa683 --- /dev/null +++ b/scripts/gen-cli-help.ts @@ -0,0 +1,187 @@ +/** Generate paired CLI references by invoking the shipped launcher and profile help. */ + +import { execFile } from 'node:child_process' +import { existsSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join, resolve } from 'node:path' +import { promisify } from 'node:util' +import { PROFILE_TEMPLATES } from '@deepseek-ai/dsh-app-boot' +import { + computeTranslationPairingRecord, + renderTranslationPairingRecord, + translationPairPaths, +} from './translation-pairing-record.ts' +import { parseTranslationPairingManifest, translationPairSourcePredicate } from './translation-pairing.ts' + +const root = resolve(import.meta.dirname, '..') +const paths = translationPairPaths('docs/cli-help.md') +const execFileAsync = promisify(execFile) + +/** Complete help text and the immediate subcommands it advertises. */ +export interface CliHelp { + /** Complete help text with LF line endings. */ + output: string + /** Immediate command names listed in the `Commands:` section, in their displayed order. */ + commands: string[] +} + +interface HelpPage { + help: CliHelp + invocations: [string, ...string[]] + children: Map +} + +/** + * Discover commands from actual text help while preserving every displayed line. + * @param stdout - Successful CLI help output. + * @returns Help text with LF line endings and the command names listed in its `Commands:` section. + * @throws When the output is empty. + */ +export function parseCliHelp(stdout: string): CliHelp { + const output = stdout.replaceAll('\r\n', '\n') + if (output.trim() === '') throw new Error('CLI help returned empty output') + const section = /^Commands:\n((?:[ \t][^\n]*\n|\n)*)/mu.exec(output)?.[1] ?? '' + const commands = [...section.matchAll(/^ {2}\S+/gmu)].map(match => match[0].slice(2)) + return { output, commands } +} + +function addAlias(page: HelpPage, invocation: string): void { + if (page.invocations.includes(invocation)) return + page.invocations.push(invocation) + for (const [name, child] of page.children) addAlias(child, `${invocation} ${name}`) +} + +/** + * Collect launcher, plugin, and profile help even when root help lists no commands. + * @param readHelp - run one help invocation and return its complete stdout. + * @param profiles - shipped profile names whose explicit and shorthand forms must be documented. + * @returns distinct help pages with every discovered invocation and nested command. + */ +export async function collectCliHelp( + readHelp: (args: readonly string[]) => Promise, profiles: readonly string[], +): Promise { + const pages = new Map() + const visit = async (args: string[]): Promise => { + const invocation = ['dsh', ...args].join(' ') + const help = parseCliHelp(await readHelp(args)) + const existing = pages.get(help.output) + if (existing !== undefined) { + addAlias(existing, invocation) + return existing + } + const page: HelpPage = { help, invocations: [invocation], children: new Map() } + pages.set(help.output, page) + for (const name of help.commands) page.children.set(name, await visit([...args, name])) + return page + } + await visit([]) + await visit(['plugin']) + for (const profile of [...profiles].sort()) { + await visit([profile]) + await visit(['--profile', profile]) + } + return [...pages.values()] +} + +async function collectHelp(): Promise { + const directory = mkdtempSync(join(tmpdir(), 'dsh-cli-help-')) + const environment = { + ...process.env, + DSH_HOME: join(directory, 'harness'), + DSH_AGENTS_HOME: join(directory, 'agents'), + DSH_TELEMETRY_DISABLED: '1', + NO_COLOR: '1', + } + for (const name of ['NODE_OPTIONS', 'FORCE_COLOR']) Reflect.deleteProperty(environment, name) + try { + return await collectCliHelp(async (args) => { + const { stdout, stderr } = await execFileAsync(process.execPath, [ + '--import', 'tsx/esm', 'apps/cli/src/bin.ts', ...args, '--help', + ], { + cwd: root, env: environment, encoding: 'utf8', timeout: 30_000, + killSignal: 'SIGKILL', maxBuffer: 2 * 1024 * 1024, + }) + if (stderr !== '') throw new Error(`${['dsh', ...args].join(' ')} --help wrote diagnostics: ${stderr}`) + return stdout + }, Object.keys(PROFILE_TEMPLATES)) + } finally { + rmSync(directory, { recursive: true, force: true }) + } +} + +function anchor(page: HelpPage): string { + return page.invocations[0].replaceAll(/[^a-zA-Z0-9]+/gu, '-').replace(/-$/u, '').toLowerCase() +} + +function renderHelp(pages: readonly HelpPage[], chinese: boolean): string { + const lines = [ + '', + '', + chinese ? '# dsh 命令行帮助' : '# dsh CLI help', + '', + ...(chinese ? ['[English](cli-help.md) | 中文', ''] : []), + chinese + ? '此参考收录启动器、子命令和随附 profile 的完整帮助。同一份帮助只展示一次,并列出所有调用方式。帮助输出保留原文。' + : 'This reference contains complete help for the launcher, subcommands, and shipped profiles. Identical help appears once with every invocation listed. Help output retains its original language.', + '', + chinese + ? '运行 `pnpm run gen-cli-help` 重新生成;运行 `pnpm run verify-cli-help` 检查是否与当前命令一致。' + : 'Run `pnpm run gen-cli-help` to regenerate; run `pnpm run verify-cli-help` to check freshness.', + '', + chinese ? '## 目录' : '## Contents', + '', + ...pages.map(page => `- [\`${page.invocations[0]}\`](#${anchor(page)})`), + '', + ] + for (const page of pages) { + lines.push( + ``, + `## \`${page.invocations[0]}\``, + '', + `${chinese ? '调用方式' : 'Invocations'}: ${page.invocations.map(invocation => `\`${invocation} --help\``).join(', ')}`, + '', + `\`\`\`text\n${page.help.output}${page.help.output.endsWith('\n') ? '' : '\n'}\`\`\``, + '', + ) + } + return lines.join('\n') +} + +/** Regenerate both CLI reference pages, or check their bytes with --check. @returns Nothing. */ +export async function main(): Promise { + const check = process.argv.includes('--check') + const pages = await collectHelp() + const source = renderHelp(pages, false) + const zh = renderHelp(pages, true) + const written = new Set([paths.source, paths.zh]) + const context = { + repoRoot: root, + isTranslationPairSource: translationPairSourcePredicate(parseTranslationPairingManifest( + readFileSync(resolve(root, 'scripts/translation-pairing.manifest.json'), 'utf8'), + )), + // Resolve links between the two pages as this run writes them, so a first run + // records the same hashes that verify-translation-pairing computes afterward. + repositoryFileExists: (path: string) => written.has(path) + || existsSync(resolve(root, path)) && statSync(resolve(root, path)).isFile(), + } + const outputs = new Map([ + [paths.source, source], + [paths.zh, zh], + [paths.meta, renderTranslationPairingRecord(paths, computeTranslationPairingRecord(paths, source, zh, context))], + ]) + const changed: string[] = [] + for (const [path, content] of outputs) { + const destination = resolve(root, path) + if (existsSync(destination) && readFileSync(destination, 'utf8') === content) continue + changed.push(path) + if (!check) writeFileSync(destination, content) + } + if (check && changed.length > 0) { + console.error(`gen-cli-help: stale — ${changed.join(', ')}. Run pnpm run gen-cli-help.`) + process.exitCode = 1 + } else { + console.log(`gen-cli-help: ${pages.length} help pages; ${check ? 'all outputs are current' : `${changed.length} files written`}.`) + } +} + +if (process.argv[1] !== undefined && import.meta.filename === resolve(process.argv[1])) await main() diff --git a/scripts/run-gates.spec.ts b/scripts/run-gates.spec.ts index 76bcafb3ed..5c8be197cf 100644 --- a/scripts/run-gates.spec.ts +++ b/scripts/run-gates.spec.ts @@ -284,6 +284,13 @@ describe('gate graph validation', () => { expect(ids).toContain('public-repository-links') }) + it('checks the complete CLI reference locally and in the static CI lane', () => { + for (const mode of ['doc-sync', 'ci-static'] as const) { + const gates = withPnpmEntrypoint(() => gatesForMode(mode)) + expect(gates.find(gate => gate.id === 'cli-help')).toBeDefined() + } + }) + it('keeps the concrete terminology policy in the documentation gate', () => { const ids = withPnpmEntrypoint(() => gatesForMode('doc-sync').map(subject => subject.id)) @@ -360,8 +367,8 @@ describe('gate graph validation', () => { it('schedules the longest documentation leaves before short checks', () => { const ids = withPnpmEntrypoint(() => gatesForMode('doc-sync').map(subject => subject.id)) - expect(ids.slice(0, 10)).toEqual([ - 'doc-typecheck', 'docs-site-build', 'doc-graphs', 'markdown-links', 'type-equivalence', + expect(ids.slice(0, 11)).toEqual([ + 'doc-typecheck', 'docs-site-build', 'doc-graphs', 'cli-help', 'markdown-links', 'type-equivalence', 'cordis-catalog', 'cordis-inspect-catalog', 'workflow-guest', 'mermaid', 'translation-pairing', ]) }) diff --git a/scripts/run-gates.ts b/scripts/run-gates.ts index f4af0f5a1b..cccda2030a 100644 --- a/scripts/run-gates.ts +++ b/scripts/run-gates.ts @@ -799,6 +799,7 @@ function docSyncLeafGates(options: { : [pnpmScript('doc-typecheck', options.docTypecheckScript ?? 'doc-typecheck', docTypecheckOptions)], pnpmScript('docs-site-build', options.docsBuildScript ?? 'docs:build', { label: 'documentation build' }), pnpmScript('doc-graphs', 'verify-doc-graphs', { label: 'doc graphs' }), + pnpmScript('cli-help', 'verify-cli-help', { label: 'CLI help reference' }), pnpmScript('markdown-links', 'verify-md-links', { label: 'markdown links', quick: true }), pnpmScript('type-equivalence', 'verify-type-equiv', { label: 'type equivalence', quick: true }), pnpmScript('cordis-catalog', 'verify-cordis-catalog', { label: 'cordis catalog' }), diff --git a/scripts/translation-pairing.spec.ts b/scripts/translation-pairing.spec.ts index 89ab41d09d..89d912fca3 100644 --- a/scripts/translation-pairing.spec.ts +++ b/scripts/translation-pairing.spec.ts @@ -133,6 +133,7 @@ describe('translation pairing manifest', () => { describe('translation pairing switchers', () => { it('exempts only paired generated English sources from reciprocal switchers', () => { + expect(requiresSourceLanguageSwitcher('docs/cli-help.md')).toBe(false) expect(requiresSourceLanguageSwitcher('docs/config-catalog.md')).toBe(false) expect(requiresSourceLanguageSwitcher('docs/cordis-api/context.md')).toBe(false) expect(requiresSourceLanguageSwitcher('docs/cordis-api/inherited.md')).toBe(false) diff --git a/scripts/translation-pairing.ts b/scripts/translation-pairing.ts index 17a6641628..04ea0e72a6 100644 --- a/scripts/translation-pairing.ts +++ b/scripts/translation-pairing.ts @@ -311,6 +311,7 @@ export function requiresSourceLanguageSwitcher(source: string): boolean { return ![ 'docs/agent-lifecycle.md', 'docs/capability-seams.md', + 'docs/cli-help.md', 'docs/config-catalog.md', 'docs/cordis-api/context.md', 'docs/cordis-api/events.md',