mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-10-26 16:55:08 +00:00
fix(cli): bound CLI help alias discovery and document plugin option placement
`collectCliHelp` stops propagating an alias when it reaches a page already on the current path, so a `help [command]` row that prints its parent's help no longer recurses until the stack overflows. `serve|s` command rows are split into separately collected names. The CLI reference and upgrade guide describe the forwarded-argument boundary that Commander applies, including a leading pnpm option, and args.spec.ts pins that `--profile` placed after a leading pnpm option is forwarded. Generated pages use the repository's two-line generated header, and docs/AGENTS.md lists the CLI help among the generated references. Refs #4114
This commit is contained in:
1 parent
75935af1d0
commit
231d5a311e
14 files changed
+71
-38
No files matched your search
@@ -18,8 +18,8 @@
|
||||
en: d6368bf97a02bd57
|
||||
zh: 2ab7888a108961ce
|
||||
/dsh-cli-behavior-reference/plugin-management:
|
||||
en: 142404f01a747121
|
||||
zh: a6a5edbb0614bc62
|
||||
en: 8d2cfff9bab7327f
|
||||
zh: 21515ab6e6798504
|
||||
/dsh-cli-behavior-reference/web-profile:
|
||||
en: 42f0b9349cf1574b
|
||||
zh: a3cc6e352e71badb
|
||||
|
||||
@@ -82,7 +82,7 @@ New directories and files request modes `0700` and `0600` on POSIX. The CLI prin
|
||||
|
||||
`dsh plugin --profile <name> <args...>` initializes the profile when missing (shipped template, or `@deepseek-ai/dsh-base` alone for other names), then forwards `<args...>` 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 <name>` 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.
|
||||
`dsh plugin` reads `--profile` only before the forwarded arguments, which start at the first token it does not recognize, so `--profile <name>` comes first. When the forwarded arguments begin with the pnpm command, everything after that command reaches pnpm verbatim, including `-h` and `--help`. When they begin with a pnpm option such as `--filter <selector>` or `-r`, a later `-h` or `--help` still prints the forwarder's help instead of pnpm's, so put the pnpm command before them to reach pnpm's help. `-h` or `--help` before any forwarded argument 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:
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ dsh --profile web --patch ./extra.yml --dump-config
|
||||
|
||||
`dsh plugin --profile <name> <args...>` 在 profile 缺失时先初始化它(有随附模板的用模板,其他名称只装 `@deepseek-ai/dsh-base`),然后以 profile 目录为工作目录,把 `<args...>` 转发给 `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 <name>` 必须写在最前面。被转发的参数以 pnpm 命令开头时,该命令之后的所有内容都会原样交给 pnpm,包括 `-h`、`--help` 和 `--profile`。其他情况下,`-h` 或 `--help` 会打印转发器自身的帮助并以 0 退出。
|
||||
`dsh plugin` 只在被转发的参数之前读取 `--profile`;被转发的参数从它无法识别的第一个 token 开始,因此 `--profile <name>` 必须写在最前面。被转发的参数以 pnpm 命令开头时,该命令之后的所有内容都会原样交给 pnpm,包括 `-h` 和 `--help`。被转发的参数以 `--filter <selector>` 或 `-r` 等 pnpm 选项开头时,后面的 `-h` 或 `--help` 仍会打印转发器的帮助而不是 pnpm 的帮助,因此要查看 pnpm 的帮助,需把 pnpm 命令写在它们之前。在任何被转发的参数之前出现的 `-h` 或 `--help` 会打印转发器自身的帮助并以 0 退出。
|
||||
|
||||
Codex 与 Claude Code subagent 提供方是两个彼此独立的可选组合包。可以只添加一个包、在同一命令中添加两个包,或独立移除任一包:
|
||||
|
||||
|
||||
@@ -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
|
||||
// Commander copies the launcher's disabled help option into subcommands. The
|
||||
// forwarder restores its own help and stops reading options at forwarded pnpm input.
|
||||
.helpOption('-h, --help', 'show this help')
|
||||
.passThroughOptions()
|
||||
.requiredOption('--profile <name>', 'the profile whose plugins to manage (initialized on first use)', selectProfile)
|
||||
|
||||
@@ -125,7 +125,11 @@ describe('parseDshArgs', () => {
|
||||
.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'] })
|
||||
// A leading pnpm option starts the forwarded arguments before the pnpm command.
|
||||
expect(parse(['plugin', '--profile', 'tui', '--filter', 'x', '--profile', 'y', 'add', 'z']))
|
||||
.toEqual({ mode: 'plugin', profile: 'tui', args: ['--filter', 'x', '--profile', 'y', 'add', 'z'] })
|
||||
expect(exitCode(['plugin', 'add', 'x', '--profile', 'tui'])).toBe(1)
|
||||
expect(exitCode(['plugin', '--filter', 'x', 'add', '--profile', 'tui'])).toBe(1)
|
||||
})
|
||||
|
||||
it.each([
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ Each fact has one home: the tier whose job it is; elsewhere, link there.
|
||||
| [user/](user/index.md) | Product-facing guides published by the documentation website | Generated reference tables, contributor procedures, decision history |
|
||||
| Package README | The per-package contract: config, semantics, limitations, extension points, and [Model Experience](cookbook/adding-a-package.md#4-write-the-package-readme) | JSDoc restatement, generated-catalog restatement (event/tool tables), other packages' concerns |
|
||||
| [development.md](development.md) | Contributor setup, daily workflow, and a summary of CI; a bilingual pair under the [i18n contract](i18n/README.md) | Runtime/version rationale (→ Agent Notes), check-by-check lists that drift from `package.json` scripts |
|
||||
| Generated reference: the per-page `cordis-surface` regions in [subsystems/](subsystems/README.md), the [Cordis core API + inherited tier](cordis-api/context.md), [tool-catalog](tool-catalog.md), [config-catalog](config-catalog.md), [persistence-catalog](persistence-catalog.md), [module-graph.md](module-graph.md) | Exhaustive English sources regenerated from source and freshness-gated; reviewed Chinese counterparts follow the [pairing workflow](i18n/README.md#scope-and-exclusions) | Hand edits to generated English sources or regions; Chinese counterparts update through pairing only |
|
||||
| Generated reference: the per-page `cordis-surface` regions in [subsystems/](subsystems/README.md), the [Cordis core API + inherited tier](cordis-api/context.md), [tool-catalog](tool-catalog.md), [config-catalog](config-catalog.md), [persistence-catalog](persistence-catalog.md), [module-graph.md](module-graph.md), [cli-help](cli-help.md) | Exhaustive English sources regenerated from source and freshness-gated; reviewed Chinese counterparts follow the [pairing workflow](i18n/README.md#scope-and-exclusions) | Hand edits to generated English sources or regions; Chinese counterparts update through pairing only |
|
||||
| Skills (`.agents/skills/`) | Reusable workflows and specialized decision standards | Product and runtime contracts (→ docs or source) |
|
||||
|
||||
Placement: bugs → postmortems; rationale → Agent Notes; procedures → cookbooks; type definitions → subsystems; package contracts → READMEs; standing orders → root `AGENTS.md` with a rationale link.
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
# 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
|
||||
en: d11ca88bdc409249
|
||||
zh: d11ca88bdc409249
|
||||
/dsh-cli-help:
|
||||
en: b7174eab3476579d
|
||||
zh: 5ddab967320e3cff
|
||||
|
||||
+2
-1
@@ -1,4 +1,5 @@
|
||||
<!-- Generated by scripts/gen-cli-help.ts; run pnpm run gen-cli-help to regenerate. -->
|
||||
<!-- Generated by scripts/gen-cli-help.ts — do not edit by hand.
|
||||
Run `pnpm run gen-cli-help` to regenerate. -->
|
||||
|
||||
# dsh CLI help
|
||||
|
||||
|
||||
+2
-1
@@ -1,4 +1,5 @@
|
||||
<!-- Generated by scripts/gen-cli-help.ts; run pnpm run gen-cli-help to regenerate. -->
|
||||
<!-- Generated by scripts/gen-cli-help.ts — do not edit by hand.
|
||||
Run `pnpm run gen-cli-help` to regenerate. -->
|
||||
|
||||
# dsh 命令行帮助
|
||||
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
# 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
|
||||
en: 3f9b48b2230ec6e4
|
||||
zh: c1aad0e9121d46a4
|
||||
/dsh-plugin-option-placement:
|
||||
en: 3d717a2b7e9087d3
|
||||
zh: e6004d015e6a4829
|
||||
/dsh-plugin-option-placement/change:
|
||||
en: 27539de1a79a7eec
|
||||
zh: 3860da746b0f24a5
|
||||
en: 15fe8cf74db0ba53
|
||||
zh: c6ea1056aa7a6f77
|
||||
/dsh-plugin-option-placement/migration:
|
||||
en: 543880bb55c786bc
|
||||
zh: 0be3793b66b0c1e6
|
||||
en: b7239597ebc4d970
|
||||
zh: fa45f851cf1f3af1
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
kind: upgrade-guide
|
||||
description: "dsh plugin reads --profile and its own help only before the forwarded pnpm arguments."
|
||||
description: "dsh plugin reads --profile only before the forwarded pnpm arguments and prints its own help."
|
||||
---
|
||||
|
||||
# `dsh plugin` option placement
|
||||
@@ -9,12 +9,12 @@ English | [中文](guide.zh.md)
|
||||
|
||||
## Change
|
||||
|
||||
`dsh plugin` reads its own options only before the forwarded pnpm arguments. Previously it accepted `--profile <name>` anywhere on the command line, had no help of its own, and forwarded `-h` and `--help` to pnpm from any position.
|
||||
`dsh plugin` reads `--profile` only before the forwarded pnpm arguments, which start at the first token it does not recognize, and it has its own help. Previously it accepted `--profile <name>` 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 <name>' not specified`, and `dsh plugin --profile <name> --help` no longer prints pnpm's help.
|
||||
When the forwarded arguments begin with the pnpm command, everything after that command now reaches pnpm verbatim, including `-h`, `--help`, and `--profile`. In any other position, including after a leading pnpm option such as `--filter <selector>`, `-h` or `--help` prints the `dsh plugin` help and exits 0. Scripts that place `--profile` after the pnpm command or after a leading pnpm option fail with `required option '--profile <name>' not specified`, and `dsh plugin --profile <name> --help` no longer prints pnpm's help.
|
||||
|
||||
## Migration
|
||||
|
||||
1. Move `--profile <name>` before the pnpm command: replace `dsh plugin add <package> --profile <name>` with `dsh plugin --profile <name> add <package>`.
|
||||
2. Request pnpm's help for one command after that command, as in `dsh plugin --profile <name> add --help`. For pnpm's general help, forward `help`: `dsh plugin --profile <name> help`.
|
||||
1. Move `--profile <name>` before every pnpm argument: replace `dsh plugin add <package> --profile <name>` with `dsh plugin --profile <name> add <package>`.
|
||||
2. Request pnpm's help for one command after that command, with any pnpm options after it too, as in `dsh plugin --profile <name> add --help`. For pnpm's general help, forward `help`: `dsh plugin --profile <name> help`.
|
||||
3. Confirm that `dsh plugin --help` prints `Usage: dsh plugin [options] [args...]` and that the commands from steps 1 and 2 reach pnpm.
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
kind: upgrade-guide
|
||||
description: "dsh plugin 只在被转发的 pnpm 参数之前读取 --profile 和自身的帮助选项。"
|
||||
description: "dsh plugin 只在被转发的 pnpm 参数之前读取 --profile,并提供自己的帮助。"
|
||||
---
|
||||
|
||||
# `dsh plugin` 选项位置
|
||||
@@ -9,12 +9,12 @@ description: "dsh plugin 只在被转发的 pnpm 参数之前读取 --profile
|
||||
|
||||
## 变更
|
||||
|
||||
`dsh plugin` 只在被转发的 pnpm 参数之前读取自己的选项。此前它接受出现在命令行任意位置的 `--profile <name>`,没有自己的帮助,并且无论 `-h` 和 `--help` 位于何处都会转发给 pnpm。
|
||||
`dsh plugin` 只在被转发的 pnpm 参数之前读取 `--profile`,被转发的参数从它无法识别的第一个 token 开始;它还提供自己的帮助。此前它接受出现在命令行任意位置的 `--profile <name>`,没有自己的帮助,并且无论 `-h` 和 `--help` 位于何处都会转发给 pnpm。
|
||||
|
||||
被转发的参数以 pnpm 命令开头时,该命令之后的所有内容现在都会原样交给 pnpm,包括 `-h`、`--help` 和 `--profile`。其他情况下,`-h` 或 `--help` 会打印 `dsh plugin` 的帮助并以 0 退出。把 `--profile` 写在 pnpm 命令之后的脚本会因 `required option '--profile <name>' not specified` 而失败,`dsh plugin --profile <name> --help` 也不再打印 pnpm 的帮助。
|
||||
被转发的参数以 pnpm 命令开头时,该命令之后的所有内容现在都会原样交给 pnpm,包括 `-h`、`--help` 和 `--profile`。在其他任何位置,包括位于 `--filter <selector>` 等开头的 pnpm 选项之后,`-h` 或 `--help` 都会打印 `dsh plugin` 的帮助并以 0 退出。把 `--profile` 写在 pnpm 命令或开头的 pnpm 选项之后的脚本会因 `required option '--profile <name>' not specified` 而失败,`dsh plugin --profile <name> --help` 也不再打印 pnpm 的帮助。
|
||||
|
||||
## 迁移
|
||||
|
||||
1. 把 `--profile <name>` 移到 pnpm 命令之前:将 `dsh plugin add <package> --profile <name>` 改为 `dsh plugin --profile <name> add <package>`。
|
||||
2. 查看某个 pnpm 命令的帮助时,把帮助选项写在该命令之后,例如 `dsh plugin --profile <name> add --help`。查看 pnpm 的总体帮助时转发 `help`:`dsh plugin --profile <name> help`。
|
||||
1. 把 `--profile <name>` 移到所有 pnpm 参数之前:将 `dsh plugin add <package> --profile <name>` 改为 `dsh plugin --profile <name> add <package>`。
|
||||
2. 查看某个 pnpm 命令的帮助时,把帮助选项和其他 pnpm 选项都写在该命令之后,例如 `dsh plugin --profile <name> add --help`。查看 pnpm 的总体帮助时转发 `help`:`dsh plugin --profile <name> help`。
|
||||
3. 确认 `dsh plugin --help` 打印 `Usage: dsh plugin [options] [args...]`,并且第 1、2 步中的命令能够到达 pnpm。
|
||||
@@ -28,6 +28,23 @@ describe('CLI help discovery', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('visits each command alias and lists a help command that repeats its parent once', async () => {
|
||||
const visited: string[] = []
|
||||
const parent = 'Usage: web [command]\n\nCommands:\n serve|s [options] Serve the UI\n help [command] display help for command\n'
|
||||
const pages = await collectCliHelp(async (args) => {
|
||||
visited.push(args.join(' '))
|
||||
if (args.length === 0) return 'Usage: dsh\n'
|
||||
if (args[0] === 'plugin') return 'Usage: dsh plugin\n'
|
||||
const selected = args[0] === '--profile' ? args.slice(1) : args
|
||||
return selected[1] === 'serve' || selected[1] === 's' ? 'Usage: web serve\n' : parent
|
||||
}, ['web'])
|
||||
expect(visited).toEqual(['', 'plugin', 'web', 'web serve', 'web s', 'web help', '--profile web'])
|
||||
expect(pages.map(page => page.invocations)).toEqual([
|
||||
['dsh'], ['dsh plugin'], ['dsh web', 'dsh web help', 'dsh --profile web', 'dsh --profile web help'],
|
||||
['dsh web serve', 'dsh web s', 'dsh --profile web serve', 'dsh --profile web s'],
|
||||
])
|
||||
})
|
||||
|
||||
it.each(['', ' \r\n\n'])('rejects blank help %j', (help) => {
|
||||
expect(() => parseCliHelp(help)).toThrow('CLI help returned empty output')
|
||||
})
|
||||
|
||||
+21
-13
@@ -21,7 +21,7 @@ const execFileAsync = promisify(execFile)
|
||||
export interface CliHelp {
|
||||
/** Complete help text with LF line endings. */
|
||||
output: string
|
||||
/** Immediate command names listed in the `Commands:` section, in their displayed order. */
|
||||
/** Immediate command names and aliases listed in the `Commands:` section, in their displayed order. */
|
||||
commands: string[]
|
||||
}
|
||||
|
||||
@@ -34,21 +34,26 @@ interface HelpPage {
|
||||
/**
|
||||
* 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.
|
||||
* @returns Help text with LF line endings and the command names and aliases (`serve|s`) 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))
|
||||
const commands = [...section.matchAll(/^ {2}\S+/gmu)].flatMap(match => match[0].slice(2).split('|'))
|
||||
return { output, commands }
|
||||
}
|
||||
|
||||
function addAlias(page: HelpPage, invocation: string): void {
|
||||
// A `help [command]` row prints its parent's help, so a page can list itself as a child.
|
||||
// An invocation that reaches a page already on the current path joins that page without
|
||||
// extending its children, which keeps alias propagation finite.
|
||||
function addAlias(page: HelpPage, invocation: string, path: ReadonlySet<HelpPage>): void {
|
||||
if (page.invocations.includes(invocation)) return
|
||||
page.invocations.push(invocation)
|
||||
for (const [name, child] of page.children) addAlias(child, `${invocation} ${name}`)
|
||||
if (path.has(page)) return
|
||||
const childPath = new Set([...path, page])
|
||||
for (const [name, child] of page.children) addAlias(child, `${invocation} ${name}`, childPath)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -61,24 +66,26 @@ export async function collectCliHelp(
|
||||
readHelp: (args: readonly string[]) => Promise<string>, profiles: readonly string[],
|
||||
): Promise<HelpPage[]> {
|
||||
const pages = new Map<string, HelpPage>()
|
||||
const visit = async (args: string[]): Promise<HelpPage> => {
|
||||
const visit = async (args: string[], path: ReadonlySet<HelpPage>): Promise<HelpPage> => {
|
||||
const invocation = ['dsh', ...args].join(' ')
|
||||
const help = parseCliHelp(await readHelp(args))
|
||||
const existing = pages.get(help.output)
|
||||
if (existing !== undefined) {
|
||||
addAlias(existing, invocation)
|
||||
addAlias(existing, invocation, path)
|
||||
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]))
|
||||
const childPath = new Set([...path, page])
|
||||
for (const name of help.commands) page.children.set(name, await visit([...args, name], childPath))
|
||||
return page
|
||||
}
|
||||
await visit([])
|
||||
await visit(['plugin'])
|
||||
const top = new Set<HelpPage>()
|
||||
await visit([], top)
|
||||
await visit(['plugin'], top)
|
||||
for (const profile of [...profiles].sort()) {
|
||||
await visit([profile])
|
||||
await visit(['--profile', profile])
|
||||
await visit([profile], top)
|
||||
await visit(['--profile', profile], top)
|
||||
}
|
||||
return [...pages.values()]
|
||||
}
|
||||
@@ -115,7 +122,8 @@ function anchor(page: HelpPage): string {
|
||||
|
||||
function renderHelp(pages: readonly HelpPage[], chinese: boolean): string {
|
||||
const lines = [
|
||||
'<!-- Generated by scripts/gen-cli-help.ts; run pnpm run gen-cli-help to regenerate. -->',
|
||||
'<!-- Generated by scripts/gen-cli-help.ts — do not edit by hand.',
|
||||
' Run `pnpm run gen-cli-help` to regenerate. -->',
|
||||
'',
|
||||
chinese ? '# dsh 命令行帮助' : '# dsh CLI help',
|
||||
'',
|
||||
|
||||
Reference in new issue
Block a user