close

Main Chain A · Step 01

args.ts:解析与分发

apps/cli/src/args.ts(191 行)用 commander 把 argv 变成结构化的 DshInvocation。它的核心设计只有一条:launcher 只解析自己的 flags,第一个不认识的东西起全归应用

bin.ts:26 args.ts:112 parseDshArgs (下一步) profile-boot.ts → 02 loadProfile

示例本次示例:dsh web 的 argv 分解

示例轨迹 01-1 · dsh web 在 args.ts 里的实际路径
# 用户输入:argv = ['web']
# args.ts:156 注册了名为 web 的子命令(硬编码别名,等价 --profile web)
# 匹配 subcommand 'web' → action 回调(args.ts:166-169)
#   rejectParentOptions('web')        ← web 子命令不接受父级 --profile 等选项
#   resolved = resolveBoot(web, 'web', options, [])
#     options = { patch: undefined, dumpConfig: undefined, dumpDefaultConfig: undefined }
#     86 行:不是 dump → 返回 { mode: 'profile', profile: 'web', patches: [], args: [] }
#
# 回到 bin.ts:28-38:mode === 'profile' → 动态 import profile-boot.ts → runProfile(...)
来源:apps/cli/src/args.ts:156-169, 83-103(真实行号)
示例轨迹 01-2 · dsh web -h 的真实帮助输出(本机执行)
$ node --import tsx/esm apps/cli/src/bin.ts web --help
Usage: dsh --profile web [options]

Serve the DeepSeek Harness browser UI.

Options:
  --host <host>                  bind host
  --no-open                      do not open the Web UI in the default browser
  --port <port>                  listen port; pass 0 to let the OS pick a free one
  --trusted-host <authority...>  extra authority the /api browser-trust fence accepts
  -h, --help                     show this help

# 注意:这个帮助不是 launcher 打印的(126 行禁用了 launcher 的 -h),
# 而是 web 应用自己打印的——'--host/--port' 全是 launcher 不认识的应用 flags。
来源:本机执行的真实输出 · args.ts:126-129

下一站:02 loadProfile——现在 invocation 是 {mode:'profile', profile:'web', patches:[], args:[]}

§1判别联合:三种 invocation

apps/cli/src/args.ts三种调用的类型定义21-48
21interface ProfileInvocation {
22  mode: 'profile'
23  profile: string
24  patches: string[]
26  args: string[]   // 内部参数:原样交给 booted 插件树
27}
30interface DumpConfigInvocation {
31  mode: 'dump-config'
32  profile: string
34  defaultOnly: boolean   // true = 只打印 bundle 层(跳过用户层与 --patch)
35  patches: string[]
36}
39interface PluginInvocation {
40  mode: 'plugin'
41  profile: string
43  args: string[]   // pnpm 参数,原样转发
44}
48export type DshInvocation = ProfileInvocation | DumpConfigInvocation | PluginInvocation
21-27

ProfileInvocation:主模式。四个字段各司其职——profile 是名字,patches--patch overlay 列表(按 argv 顺序),args 是「内部参数」(launcher 不认识的所有东西,原样进插件树)。

30-36

DumpConfigInvocation:打印配置树。注意它没有 args 字段——dump 是 boot-free 的(见 §3 的守卫逻辑)。

39-44

PluginInvocation:把剩余参数转发给 pnpm。与主链无关,但它说明了 args 字段在两种模式里语义不同(应用参数 vs pnpm 参数)。

48

判别联合:mode 就是判别标签。这就是 00 页 bin.ts switch 的穷尽性来源。

§2parseDshArgs:commander 装配

apps/cli/src/args.tsparseDshArgs 主体112-145
112export function parseDshArgs(argv, version): DshInvocation {
116  const program: Command = new Command()
117  program
118    .name('dsh')
119    .version(version, '-V, --version', 'output the version number')
121    .addHelpText('after', HELP_EXAMPLES)
122    .exitOverride()          // 不让 commander 直接 process.exit,改抛 CommanderError
126    .helpOption(false)       // launcher 自己禁用 -h:它属于应用
127    .allowUnknownOption()
128    .passThroughOptions()
129    .enablePositionalOptions()
130    .argument('[args...]', 'arguments for the booted profile\'s app ...')
131    .option('--profile <name>', ...)
132    .option('--patch <path>', ..., collect)
133    .option('--dump-config', ...)
134    .option('--dump-default-config', ...)
135    .action((args, options) => {
138      if (options.profile === undefined) {
139        if (args.some(argument => argument === '-h' || argument === '--help')) program.help()
140        program.error('error: --profile <name> is required')
141      }
142      const profile = options.profile
143      if (profile === '') program.error('error: --profile needs a name')
144      resolved = resolveBoot(program, profile, options, args)
145    })
119

版本号从 bin.ts 的 readVersion() 传进来——launcher 自己不读 package.json,依赖注入。

122

exitOverride():把 commander 的「打印错误并 process.exit」改成「抛 CommanderError」。这样 183-187 行的 catch 能统一处理退出码(见 §3)。

126-129

四连是关键设计:禁用 launcher 的 -h(它属于应用)、允许未知选项、透传选项、启用位置参数——合起来实现「launcher 的 flags 在第一个不认识的 token 处结束,之后全归应用」。于是 dsh --profile web -h 打印的是 web 应用的帮助,不是 launcher 的。

131-134

launcher 自己的四个 flag。注意 --patchcollect(61 行)——可重复单值收集器,绝不 variadic:注释明确说 variadic 的 --patch 会吞掉内部参数。

135-145

action 回调:无 profile 时的兜底——若 args 里带 -h 则打印 launcher 帮助(因为没人接收它了),否则报「--profile required」。空字符串 profile 也要报错。最终委托 resolveBoot 组装结果。

§3resolveBoot:dump 的守卫

apps/cli/src/args.tsresolveBoot + parse 收尾83-103, 183-190
83function resolveBoot(program, profile, options, args): DshInvocation {
84  const patches = options.patch ?? []
85  if (patches.includes('')) program.error('error: --patch needs a path')
86  if (options.dumpConfig !== true && options.dumpDefaultConfig !== true) {
87    return { mode: 'profile', profile, patches, args }
88  }
89  if (options.dumpConfig === true && options.dumpDefaultConfig === true) {
90    program.error('error: --dump-config and --dump-default-config are mutually exclusive')
91  }
95  if (args.length > 0) {
96    program.error(`error: config dumps take no app arguments, got ${args...}`)
97  }
98  const defaultOnly = options.dumpDefaultConfig === true
99  if (defaultOnly && patches.length > 0) {
100    program.error('error: --dump-default-config prints the bundle layers and takes no --patch')
101  }
102  return { mode: 'dump-config', profile, defaultOnly, patches }
103}
183  try {
184    program.parse(argv, { from: 'user' })
185  } catch (error) {
186    return process.exit(error instanceof CommanderError ? error.exitCode : 1)
187  }
189  if (resolved === undefined) throw new Error('dsh: no invocation resolved')
190  return resolved
86-88

不是 dump 就直接返回 profile 调用——主路径在第一个 if 就返回,dump 的检查是守卫。

92-97

dump 是 boot-free 的,所以不允许带应用参数——注释解释得透彻:dump 不跑应用的命令行 provider,无法显示那些 flags 会决定什么;打印一棵与真实 boot 不同的树会误导人。所以直接报错。

99-101

--dump-default-config 只打印 bundle 层(无用户层、无 --patch),所以带 --patch 也报错。

183-187

配合 122 行的 exitOverride():commander 的错误到这里统一 process.exit。帮助、版本、解析错误都在这个 catch 里退出——所以 bin.ts 的 switch 只会收到合法的 invocation。

189

防御性断言:action 一定会 resolve(要么 Commander 抛错)。这行是类型系统的兜底——注释标了 v8 ignore next,实际不可达。

§4边界设计:flags 归属

这一页最值得记住的设计,是文件头 JSDoc 里那句:

The launcher parses only what it owns — which profile to boot, which extra patch overlays to apply, and the config dumps — and hands everything after its own flags to the booted tree verbatim.

launcher 只拥有四件事(profile 名、--patch、两个 dump 开关),其余一切——包括 --resume-h——都归「被启动的应用」自己解析。这是 CLI 分层的干净边界:launcher 是协议的,应用是具体的