安装、运行与首次启动
本页说明 @deepseek-ai/dsh CLI 的安装后入口、profile 首次初始化、参数解析、配置检查和启动失败处理。重点是从 dsh 命令到 profile 运行时的真实路径;具体 profile 的业务插件、Web/SDK/ACP 协议细节以及插件兼容性策略属于相应的兄弟文档。
Purpose and Scope
本页覆盖以下首次启动所需的关键机制:
apps/cli/package.json声明的dsh可执行入口及其运行时依赖;apps/cli/src/bin.ts中runCli()对版本、环境、profile 启动、插件管理和配置 dump 的分派;apps/cli/src/args.ts中 launcher 参数与应用参数的边界、profile 初始化选项、patch 覆盖层和配置检查;- 中文 CLI README 中定义的内置 profile、默认 workspace、
$DSH_HOME和构建后运行方式。
本页不展开各 profile 内部 agent、Web UI、SDK JSON-RPC、ACP stdio 或具体插件的实现。要了解配置层的完整优先级和失败矩阵,应继续阅读 CLI 行为参考及 app-boot 相关文档。
Overview
dsh 是 Node 应用的统一启动器,而不是一个把所有应用逻辑都塞进自身的命令。安装包将 dsh 映射到构建产物 lib/bin.js;运行时先读取 launcher 自己拥有的选项,再将第一个未知 token 及其后的参数原样交给已启动 profile 的应用插件。
首次启动的核心概念是 profile:profile 位于 $DSH_HOME/profiles/<name>,由有序的 plugin-bundle patch 层、profile 自身的 cordis.patch.yml、home 级 patch 以及命令行 --patch 覆盖层组成。README 明确说明 web、headless、sdk、sdk-minimal 和 acp 在首次使用时会从随附模板自动初始化;自定义 profile 可以通过 --from-default-profile 从模板创建。
启动器还提供三类不启动应用的操作:--dump-config 输出组合后的配置树,--dump-default-config 只输出 bundle 层,--dump-config-schema 输出插件声明的 JSON Schema。这样可以在真正挂载 profile 之前检查配置结构,避免把应用参数误当成 launcher 参数。
Architecture
该结构体现了实际的边界:runCli() 负责生命周期级分派,parseDshArgs() 只解析 launcher 拥有的参数,profile 的应用参数不在 CLI 中解释。profile 启动时先调用 loadLayeredEnv('dsh'),再把 profile、模板、patch 和剩余参数传给 runProfile();只有 StartupError 会被转换为诊断并以状态码 1 退出,其他异常继续向外抛出。
安装产物与入口
apps/cli/package.json 将包声明为 ESM 模块,并把公开命令 dsh 指向 lib/bin.js。这意味着生产运行依赖已构建的 lib 文件,而不是直接执行 TypeScript 源文件。README 进一步要求在仓库根目录先执行 pnpm run build,随后用 pnpm dsh <args...> 执行 TypeScript 入口进行开发运行。
关键的安装声明如下:
1{
2 "name": "@deepseek-ai/dsh",
3 "type": "module",
4 "bin": {
5 "dsh": "lib/bin.js"
6 },
7 "files": [
8 "lib/*.js",
9 "lib/types/*.d.ts"
10 ]
11}Source: package.json
因此,首次启动前应区分两种场景:发布/安装后的 CLI 需要包中已有的构建产物;仓库开发者则应先完成根目录构建,再使用 workspace 命令运行。README 还指出生产运行需要已构建的包与前端产物。
首次启动与 profile 初始化
内置 profile
README 将以下模式定义为常用入口:
| 命令 | 首次启动行为 |
|---|---|
dsh web 或 dsh --profile web | 启动 Web profile;首次使用时从随附模板初始化。 |
dsh --profile headless "job" | 创建/使用 headless profile,执行持久化会话,打印最终答案后退出。 |
dsh --profile sdk | 通过 JSON-RPC stdio 为 SDK 客户端服务,直至关闭或断开。 |
dsh --profile sdk-minimal | 使用极简 agent 配置树为 SDK 客户端服务。 |
dsh --profile acp | 通过 ACP stdio 服务自动化客户端,直至断开。 |
运行目录会成为默认 workspace 根目录。自定义 profile 可使用 --from-default-profile <template>,而 dsh plugin --profile <name> ... 会在首次使用时初始化以 base 为基础的 profile。desktop 是 Electron 专用保留名称,普通 CLI 不允许启动或管理它。
对应的实际启动示例来自 README:
1dsh --profile web --port 8080
2dsh --profile tui --resume <id>
3dsh --profile headless "run the tests"
4dsh --profile web --help
5dsh --helpSource: README.zh.md
注意:--port、--resume 和 profile 应用自己的 --help 并不是 launcher 解析的选项;它们会在参数边界之后由注入的应用插件解释。
参数解析与命令边界
launcher 参数先行,应用参数透传
parseDshArgs() 使用 Commander 建立 launcher 命令,但显式启用 .allowUnknownOption()、.passThroughOptions() 和 .enablePositionalOptions()。解析器的设计是:launcher 只消费自己的选项;第一个无法识别的 token 开始,后续参数进入 args,并由 profile 应用自行解释。这避免了 CLI 为每个 profile 复制一套参数定义,也使同一个 launcher 能承载 Web、TUI、headless 和协议服务等不同应用。
1program
2 .name('dsh')
3 .version(version, '-V, --version', 'output the version number')
4 .usage('[--profile] <name> [options] [app-args...]\\n dsh plugin --profile <name> <pnpm-args...>')
5 .description('dsh: boot a DeepSeek Harness profile — an ordered stack of plugin-bundle patch layers under your own overrides.')
6 .allowUnknownOption()
7 .passThroughOptions()
8 .enablePositionalOptions()
9 .argument('[args...]', 'arguments for the booted profile\\'s app (see: dsh --profile <name> --help)')Source: args.ts
当 argv 的第一个 token 不是 flag、也不是 plugin 时,解析器会把它扩展为 --profile <name>。因此 dsh web 是 dsh --profile web 的简写。没有 profile 时,裸的 -h/--help仍显示 launcher 帮助;有 profile 时,--help会随应用参数透传。
三类 boot 选项
resolveBoot() 将启动相关选项归并为三种结果:
- 没有 dump flag:返回
mode: 'profile',包含 profile 名、模板名、patch 列表和应用参数; --dump-config或--dump-default-config:返回mode: 'dump-config';--dump-config-schema:返回mode: 'dump-config-schema'。
三个 dump flag 互斥;配置 dump 不允许应用参数。--dump-default-config 还禁止 --patch,因为它的语义是只显示 bundle 层,而不是显示用户覆盖后的树。
1const dumps = [options.dumpConfig, options.dumpDefaultConfig, options.dumpConfigSchema].filter(Boolean)
2if (dumps.length === 0) {
3 return { mode: 'profile', profile, fromDefaultProfile: options.fromDefaultProfile, patches, args }
4}
5if (dumps.length > 1) {
6 program.error('error: --dump-config, --dump-default-config, and --dump-config-schema are mutually exclusive')
7}
8if (args.length > 0) {
9 program.error(`error: config dumps take no app arguments, got ${args.map(argument => JSON.stringify(argument)).join(' ')}`)
10}Source: args.ts
profile、模板与 patch
launcher 的配置相关选项是:
| 选项 | 类型 | 默认值 | 行为 |
|---|---|---|---|
--profile <name> | string | 无;必需 | 选择 $DSH_HOME/profiles/<name> 下的 profile。 |
--from-default-profile <name> | string | 未设置 | 缺失 profile 首次启动时使用随附模板初始化自定义 profile。 |
--patch <path> | repeatable string | [] | 按命令行顺序在 profile 层之后追加 patch overlay。 |
--dump-config | boolean | false | 打印组合后的配置树并退出,不挂载应用。 |
--dump-default-config | boolean | false | 只打印 bundle 层;不能与 --patch 同用。 |
--dump-config-schema | boolean | false | 打印 profile entry 与 patch 的 JSON Schema,不输出配置值。 |
dsh plugin --profile <name> <pnpm-args...> | string[] | 无 | 将剩余参数原样转发给 profile 目录中的 pnpm。 |
--patch 使用 repeatable single-value collector,而不是 variadic 参数。这样 patch 不会吞掉后续应用参数;多个 patch 的顺序也能稳定保留。
配置树与首次启动流程
README 给出的配置层顺序从空根开始:dsh.profile.bundles 中每个 bundle 的 patch,随后是 profile 自身的 cordis.patch.yml、home 级 $DSH_HOME/cordis.patch.yml,最后是命令行 --patch 指定的覆盖层。bundle 会先从 dsh 安装目录解析,再从 profile 的 node_modules 解析;pnpm 可把树外插件安装到 profile 目录。
Source: README.zh.md
启动流程的关键顺序如下:
runCli()读取getDshRuntimeVersion(),并将process.argv.slice(2)交给parseDshArgs();- profile 模式下,加载
loadLayeredEnv('dsh'); - 将 profile、模板、patch 和应用参数传入
runProfile(); - profile boot 负责初始化缺失 profile、组合 patch 层并挂载运行时;
- dump 模式则跳过应用启动,直接调用相应 dump 函数;
- 若 profile boot 抛出
StartupError,CLI 生成诊断并以状态码1退出。
1export async function runCli(options: RunCliOptions = {}): Promise<void> {
2 const version = getDshRuntimeVersion()
3 const { manageDesktopProfile, ...profileOptions } = options
4 const invocation = parseDshArgs(process.argv.slice(2), version, manageDesktopProfile)
5
6 switch (invocation.mode) {
7 case 'profile': {
8 const { runProfile } = await import('./profile-boot.ts')
9 try {
10 await runProfile({
11 environment: loadLayeredEnv('dsh'),
12 profile: invocation.profile,
13 fromDefaultProfile: invocation.fromDefaultProfile,
14 patchFiles: invocation.patches,
15 args: invocation.args,
16 ...profileOptions,
17 })
18 } catch (error) {
19 if (!(error instanceof StartupError)) throw error
20 await reportStartupFailure(error, { home: resolveDshHome(), version, profile: invocation.profile })
21 process.exit(1)
22 }
23 break
24 }Source: bin.ts
Core Flow
这个顺序有两个重要设计意图。第一,环境加载在 runProfile() 前发生,因此 profile boot 获得的是经过 loadLayeredEnv('dsh') 处理的运行环境。第二,StartupError 被当作可诊断的启动失败处理,而未知异常不会被静默转换;这保留了编程错误或未预期故障的可见性。
插件管理与配置检查
插件管理
plugin 是独立的 invocation mode。命令要求 profile 名和至少一个剩余参数,并把这些参数原样交给 profile 目录中的 pnpm。README 将其用于添加、移除或查询 profile 插件;CLI 层不重新解释 pnpm 的具体参数。
1if (first === 'plugin') {
2 const plugin = program.command('plugin').description('manage a profile\\'s plugins by forwarding the remaining arguments to pnpm in the profile directory')
3 plugin
4 .requiredOption('--profile <name>', 'the profile whose plugins to manage (initialized on first use)', selectProfile)
5 .allowUnknownOption()
6 .argument('[args...]', 'pnpm arguments, forwarded verbatim (add <pkg>, remove <pkg>, why <pkg>, ...)')
7 .action((args: string[], options: { profile: string }) => {
8 if (options.profile === '') program.error('error: plugin needs pnpm arguments to forward (e.g. add <package>)')
9 resolved = { mode: 'plugin', profile: options.profile.toLowerCase() === 'desktop' ? 'desktop' : options.profile, args }
10 })
11}Source: args.ts
实际执行由 runCli() 延迟导入 plugin.ts 后交给 runPlugin();这使插件管理代码不必在普通 profile 启动路径中提前加载。
配置 dump
配置 dump 是 boot-free 检查路径:不会运行应用命令行 provider,也不会挂载 profile。它适合首次启动前确认 bundle、用户 patch 和 schema 是否符合预期。README 特别提醒,schema dump 打印的是插件声明的 schema,而不是配置值;检查不受信任插件前应先阅读其安全范围说明。
runCli() 的分派保持三种 dump 行为分离:
1case 'dump-config': {
2 const { runDumpConfig } = await import('./dump-config.ts')
3 runDumpConfig(
4 invocation.profile,
5 invocation.defaultOnly,
6 invocation.patches,
7 invocation.fromDefaultProfile,
8 )
9 break
10}
11case 'dump-config-schema': {
12 const { runDumpConfigSchema } = await import('./dump-config-schema.ts')
13 await runDumpConfigSchema(invocation.profile, invocation.patches, invocation.fromDefaultProfile)
14 break
15}Source: bin.ts
失败模式、边界条件与并发语义
参数和 profile 边界
- 未提供
--profile时,普通启动会报错;若此时参数包含-h或--help,则显示 launcher 帮助。 - profile 名为空、
--patch路径为空或--from-default-profile名为空时,解析阶段立即报错。 desktopprofile 由 Electron 独占管理;CLI 会拒绝其启动和普通插件管理,除非安装载体显式传入manageDesktopProfile。- 多次指定 profile 会触发
select a profile only once;这避免同一次启动出现不确定的 profile 选择。 - 配置 dump 与应用参数互斥;多个 dump flag 互斥;
--dump-default-config与--patch互斥。
这些检查发生在配置真正挂载前,目的不是只改善错误消息,而是防止生成一个与实际 boot 语义不同的“检查结果”。尤其是 dump 路径拒绝 app args,可以确保输出不会误导用户以为应用参数参与了配置决策。
启动失败
runCli() 只捕获 StartupError。捕获后它调用 reportStartupFailure(),传入 $DSH_HOME 解析结果、运行时版本和 profile 名,再调用 process.exit(1)。非 StartupError 会重新抛出,因此不会被伪装成普通 profile 配置错误。
并发与生命周期
本页读取到的 CLI 入口没有展示 profile 内部的锁、HMR 重载或 dispose 实现,因此不能在此推断具体并发算法。README 明确指出:profile 插件管理与插件管理器共享 profile 写锁;配置 HMR 使用统一串行重载;未启用 HMR 时修改在重启后生效。首次启动文档能确认的边界是:CLI 将一次解析结果交给单次 runProfile() 调用,并等待该 Promise 完成;更细的重载并发行为属于 app-boot/HMR 文档。
性能与运维注意事项
bin.ts对profile、plugin和两类 dump 使用动态 import,普通命令不会在分派前加载所有路径。- 生产运行依赖构建后的包和前端产物;缺少构建产物时不应把问题归因于 profile 配置。
- 启动目录是默认 workspace 根目录,因此从不同目录运行同一个 profile 可能得到不同的工作区上下文。
- 启动前可优先使用
--dump-default-config、--dump-config和--dump-config-schema缩小配置问题范围。 - README 说明安装和 profile 启动会依据声明的 DSH peer 范围检查运行时版本;不兼容插件需要用户明确确认精确版本豁免。具体豁免规则不在本页展开。
API Reference
runCli(options?: RunCliOptions): Promise<void>
runCli() 是公开 CLI 调度入口。它读取当前进程参数和运行时版本,解析 invocation,然后执行 profile、plugin 或配置检查路径。
参数:
options(RunCliOptions,可选):由安装载体提供的包管理器选项,以及可选的manageDesktopProfile权限。
返回值:
Promise<void>:在选定命令模式完成时 settle。profile 模式会等待runProfile();插件模式通过process.exit()结束进程。
错误行为:
StartupError:报告诊断并以状态码1退出。- 其他异常:重新抛出,不在 CLI 层吞掉。
parseDshArgs(argv: readonly string[], version: string, manageDesktopProfile?: boolean): DshInvocation
解析 launcher 自身参数并返回四种 invocation union 之一:profile、plugin、dump-config 或 dump-config-schema。帮助、版本和 Commander 错误在解析过程中处理。
参数:
argv:Node 入口之后的参数数组,通常来自process.argv.slice(2)。version:--version输出的运行时版本。manageDesktopProfile:是否允许安装载体管理保留的 Desktop profile,默认false。
返回值: DshInvocation,其中 profile 模式包含 profile、patches、fromDefaultProfile 和透传的 args。
Extension Points
扩展一个新的 profile 时,CLI 层通常不需要新增 launcher 分支:选择一个 profile 名或模板,令 profile manifest 声明 bundles,再通过 patch 层注入应用插件。只有当新能力需要新的 launcher-owned 语义(例如新的 boot-free inspection mode)时,才需要同时扩展 DshInvocation、resolveBoot()、Commander 选项和 runCli() 分派。
应用专属 flag 应保留在 profile 的插件中,而不要添加到 launcher。现有的未知参数透传和共享不可变命令行快照机制正是为了让不同 profile 拥有独立的参数命名空间。