Repository Wiki
deepseek-ai/deepseek-harness

安装、运行与首次启动

本页说明 @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

Loading diagram...

该结构体现了实际的边界:runCli() 负责生命周期级分派,parseDshArgs() 只解析 launcher 拥有的参数,profile 的应用参数不在 CLI 中解释。profile 启动时先调用 loadLayeredEnv('dsh'),再把 profile、模板、patch 和剩余参数传给 runProfile();只有 StartupError 会被转换为诊断并以状态码 1 退出,其他异常继续向外抛出。

Source: bin.ts Source: args.ts

安装产物与入口

apps/cli/package.json 将包声明为 ESM 模块,并把公开命令 dsh 指向 lib/bin.js。这意味着生产运行依赖已构建的 lib 文件,而不是直接执行 TypeScript 源文件。README 进一步要求在仓库根目录先执行 pnpm run build,随后用 pnpm dsh <args...> 执行 TypeScript 入口进行开发运行。

关键的安装声明如下:

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

sh
1dsh --profile web --port 8080 2dsh --profile tui --resume <id> 3dsh --profile headless "run the tests" 4dsh --profile web --help 5dsh --help

Source: 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 和协议服务等不同应用。

typescript
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() 将启动相关选项归并为三种结果:

  1. 没有 dump flag:返回 mode: 'profile',包含 profile 名、模板名、patch 列表和应用参数;
  2. --dump-config 或 --dump-default-config:返回 mode: 'dump-config';
  3. --dump-config-schema:返回 mode: 'dump-config-schema'。

三个 dump flag 互斥;配置 dump 不允许应用参数。--dump-default-config 还禁止 --patch,因为它的语义是只显示 bundle 层,而不是显示用户覆盖后的树。

typescript
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-configbooleanfalse打印组合后的配置树并退出,不挂载应用。
--dump-default-configbooleanfalse只打印 bundle 层;不能与 --patch 同用。
--dump-config-schemabooleanfalse打印 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 目录。

Loading diagram...

Source: README.zh.md

启动流程的关键顺序如下:

  1. runCli() 读取 getDshRuntimeVersion(),并将 process.argv.slice(2) 交给 parseDshArgs();
  2. profile 模式下,加载 loadLayeredEnv('dsh');
  3. 将 profile、模板、patch 和应用参数传入 runProfile();
  4. profile boot 负责初始化缺失 profile、组合 patch 层并挂载运行时;
  5. dump 模式则跳过应用启动,直接调用相应 dump 函数;
  6. 若 profile boot 抛出 StartupError,CLI 生成诊断并以状态码 1 退出。
typescript
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

Loading diagram...

Source: bin.ts Source: args.ts

这个顺序有两个重要设计意图。第一,环境加载在 runProfile() 前发生,因此 profile boot 获得的是经过 loadLayeredEnv('dsh') 处理的运行环境。第二,StartupError 被当作可诊断的启动失败处理,而未知异常不会被静默转换;这保留了编程错误或未预期故障的可见性。

插件管理与配置检查

插件管理

plugin 是独立的 invocation mode。命令要求 profile 名和至少一个剩余参数,并把这些参数原样交给 profile 目录中的 pnpm。README 将其用于添加、移除或查询 profile 插件;CLI 层不重新解释 pnpm 的具体参数。

typescript
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 行为分离:

typescript
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 名为空时,解析阶段立即报错。
  • desktop profile 由 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 拥有独立的参数命名空间。

Sources

(4 files)