`dsh` shipped two config trees that were 43 rows the same: apps/cli/cordis.yml composed web as 74 flat rows, while the TUI booted examples/tui-agent/cordis.yml whose single `@deepseek-ai/dsh-tui-demo` row mounted twelve plugins behind a twenty-key pass-through Config. Neither file was what its location claimed — apps/cli hardcoded the "example" as the product default and the "demo" bundle was the application — and every capability change had to be made twice. - apps/cli/base.cordis.yml holds the 43 shared rows; tui.cordis.yml and web.cordis.yml are patch lists stating only what differs per surface - overlays apply as SIBLING patch lists at one include level, because include patches never cross an include boundary. Precedence: base < surface < (--config | personal ~/.dsh/config.yaml) < launcher flag/profile patches - `--config` now applies an overlay INSTEAD OF the personal one, so a demo or test tree never inherits the user's route; new `--config-replace` boots a file as the entire tree (the old `--config` behaviour). Both survive /resume - vendor/include: index each `insert`ed row as it is added so a later patch can configure or disable it. Upstream built the id index once before the patch loop, leaving every surface-only row — the whole TUI front door — silently unpatchable from user config. Logged as local modification 8 - session identity moves to dsh-agent-loop's CONFIGURED_AGENT_IDENTITIES_KEY; dsh-tui's MAIN_SESSION_ID_KEY is deleted (only the bundle read it) - delete examples/tui-agent, examples/cordis-agent, packages/examples/tui-demo; TUI tests → apps/cli/tests, cordis e2e → packages/cordis/tool-cordis/tests, examples/code-mode survives as an overlay leaf - `dsh web` gains --config, threaded into AppCLIEntry as an extra overlay Three latent defects surfaced and are fixed here: the TUI captured the optional sessionQuery service once at construction and could permanently disable /resume when it won the mount race; the session-store root silently reverted to a project-local ./.sessions; --config-replace was dropped by the resume handoff. Verified by booting each tree through the real Loader (TUI 55 entries, web 75, zero unsettled) rather than reading YAML. All eight terminal snapshots replay byte-identically; 14/14 PTY smoke, 112/112 snapshots, 25/25 doc-sync, hygiene and lint clean.
8.9 KiB
@deepseek-ai/dsh
English | 中文
dsh 命令行入口遵循 apps/ 组装层:apps/* 是位于 packages/* 库之上的产品组装。直接运行 dsh 会启动交互式 TUI 编码 agent(智能体),dsh -p "task" 运行一个无头轮次,dsh meta 以本 harness checkout 为 workspace 启动同一个 TUI,dsh migrate 和 dsh upgrade 启动一个全新的引导式 TUI 会话并在首轮调用内置 skill,dsh list-sessions 列出此刻正在运行的会话,dsh web 则提供浏览器 UI。
Argv 只会通过 Commander 适配器(src/args.ts)解析一次:同一个程序的默认形式(无子命令)是 TUI/无头界面(--config、-p/--prompt、--resume),meta 子命令是以本 checkout 为 workspace 的同一个 TUI,migrate/upgrade 子命令是无选项的引导会话入口,list-sessions 子命令(别名 ps)列出存活会话,web 子命令则是浏览器 UI。src/bin.ts 按解析后的 mode 分支,仅动态导入该 mode 的模块。dsh --help 列出所有 mode,dsh web --help 渲染 Web 用法,dsh --version 打印此应用的版本;未知选项或拼错的 --resume 会明确报错(stderr,退出码 1),而不会被错路由。凡与默认界面不共享任何选项的子命令(migrate、upgrade、list-sessions、web)都会拒绝泄漏进来的 --config/-p/--resume,而不会照常运行并丢弃它。dsh web 的 --host/--port 是未验证的直通覆盖:dsh-host-webserver schema 是默认值(标志缺失时使用已交付的 cordis.yml 值)和有效性的唯一真源,并在启动时拒绝错误值。--trusted-host 为 /api 浏览器信任栅栏追加具名权威;全接口绑定还会自行推导本机的 LAN IP 字面量(src/app-cli-entry.ts),因此打印出的 LAN URL 无需任何标志即可使用。
TUI 界面:
- 启动已交付的默认配置(
apps/cli/base.cordis.yml),或由--config <path>指定的树(演示/测试用于启动其他示例树的逃生口),并通过dsh-app-boot完成启动; - 使用
dsh --resume <session-id>恢复已持久化会话。当 Node 宿主公开process.execve时,还会提供 TUI 的原地移交宿主:选择器预检并刷新当前会话后,宿主会释放应用,并以规范化的恢复调用替换进程;不支持进程替换的运行时会让会话继续运行并给出提示。会话身份与退出行由本 CLI 拥有,而非由配置指定:它创建或选定main会话 id,并把该 id 以及可复现本次调用的确切命令一起提供到启动上下文(MAIN_SESSION_ID_KEY与TUI_GOODBYE_MESSAGE_KEY)。任何cordis.yml键都无法移除恢复能力;缺失或无法读取的 id 会明确报错,而不会创建新会话; - 将 调用目录 视为 workspace:会话、相对路径和 workspace 指令都从 cwd 解析(
dsh meta是唯一例外,见下文); - 告知 agent 自身源码所在位置:启动后添加一个命名此 harness checkout 的提示词段。该路径从启动器的真实路径解析,因此在 PATH 符号链接和任意 cwd 下仍然有效,使自指的
cordis工具集可以读取并修改它; - 应用
~/.dsh中的个人覆盖(参见 app-boot 的个人配置):.env填补环境缺口(环境中已有的值 > 项目.env> 个人.env),config.yaml则修补已启动的树。
dsh meta 是以本 harness checkout 为 workspace 的同一个 TUI,因此开发 dsh 自身无需 cd。它在两层 .env 都加载之后才 chdir 到 checkout 根目录(从启动器的真实路径解析,与源码路径提示词段所指的根目录相同),因此环境优先级不变,而会话 cwd、./.sessions 持久化根目录与 HMR 监视根目录会一并移动。它只接受 --resume <id>;--config(会启动其他配置树)和 -p(非交互)都会明确报错。由于 meta 会话位于该 checkout 之下,此处的 --resume 只能看到其他 meta 会话;原地移交与打印的退出行都会以 dsh meta --resume <id> 复现该 mode,因此复制的命令在任何目录下都能恢复到正确的会话。
dsh migrate 与 dsh upgrade 是默认 TUI 界面之上的引导式全新会话入口:各自在调用目录中创建一个全新会话,并以一个内置 skill 播种其首轮(dsh-migrate 用于从其他编码 agent 迁移——opencode、pi、Claude Code、Codex;dsh-upgrade 用于升级本 checkout),效果等同于用户手动键入 /skill:<name>。启动器将 skill 名称提供到启动上下文(INITIAL_SKILL_KEY),TUI 在聊天就绪后自动调用它。两者都不接受任何选项——--config、-p、--resume 都会明确报错——且仅在首次启动时播种,因此之后 dsh --resume <id> 恢复该会话时是普通 TUI 会话,不会重复注入。
dsh list-sessions 列出此刻正在运行的会话:会话 id、pid、运行时长、工作区和标题,最新的在前。它是只读的,不启动任何 agent 树——它只挂载会话注册表,因此列表既快,也不会作为副作用启动模型工作。每个界面都通过 dsh-session-registry-live 把自己的会话发布到该注册表,进程已不存在的记录会在读取时被剪除,因此崩溃的会话无需清理便会消失。--json 以机器可读的数组形式输出同样的记录;空列表打印一行并以 0 退出。没有工作区过滤:列表始终是全部存活会话,无论它们运行在哪个目录下。只有顶层界面会出现——subagent 共用别的进程,或 spawn 出别的进程,因此被刻意排除在列表之外。
Web 和无头界面启动同一个共享组合(cordis.yml):两者都将调用目录视为默认项目和 Workspace 根目录,除非通过 --workspace-root <path> 覆盖,否则会在该根目录下创建具名 Workspace;它们会把适用的 AGENTS.md/CLAUDE.md 指令加载到每个 agent-loop 请求前缀中,渲染预算为 65,536 字节,并选用首条消息模型标题。无头界面唯一的差异是监听操作系统分配的端口(并行 dsh -p 运行绝不冲突;stderr 打印的 URL 会在浏览器中打开实时会话)。两者都需要先构建前端 dist 和客户端 bundle(pnpm run build && pnpm run build:web)。
已交付的 TUI 和 Web 组合会注册原生 DeepSeek 适配器,以及 pi-ai 的 OpenAI 和 Anthropic 提供方配置。凭据和端点覆盖来自启动分层环境中的提供方标准变量对:DEEPSEEK_API_KEY / DEEPSEEK_BASE_URL、OPENAI_API_KEY / OPENAI_BASE_URL 和 ANTHROPIC_API_KEY / ANTHROPIC_BASE_URL。
DSH_TOOLS_MODE 为整个 Web/无头进程选择工具呈现模式:可选值为 native(未设置时的 schema 默认值)、code(仅含 run_code 的 Code Mode 协议接口)或 both;任何其他值都会经由 dsh-tools 配置 schema 在启动时明确报错。它是一个临时 seam:Loader 组合是静态的,因此该设置作用于整个进程;待 Web UI 负责逐会话工具模式选择后便会移除。TUI 界面会忽略该变量(其配置树固定了自身模式)。
安装(开发机)
将从源码运行的启动器符号链接到 PATH 上;它通过自身真实路径解析 checkout,因此代码更改会在下次启动时生效,无需构建:
ln -sf "$(pwd)/bin/dsh" ~/.local/bin/dsh
源码启动会通过 tsx 的 ESM-only hook(node --import tsx/esm)运行 apps/cli/src/bin.ts,由它转换 TypeScript 并将根 tsconfig 的 paths 映射投射到模块解析中。不使用 Node 原生 TypeScript 模式:Node 26 移除了 --experimental-transform-types,而 strip-only 模式无法接受源码图依赖的语法(vendor 中的参数属性、装饰器、运行时 enum/namespace)。CJS hook 保持关闭,因为源码图是纯 ESM,而 CJS 解析器会增加约 0.4s 启动耗时。bin/dsh 将 TSX_TSCONFIG_PATH 固定到 checkout 的根 tsconfig,使解析与 cwd 无关;node-compat 门禁 dsh-source-launch-smoke 会在每条受支持的 Node 版本线上运行这一精确启动向量。tsx 应用 paths 映射时不检查依赖声明,声明完整性由静态门禁保障:TUI 配置通过 examples/package.json 解析裸插件,Web/无头 cordis.yml 通过本包的 dependencies 解析;verify-cordis-config 要求每个已配置的裸插件均已声明,同时允许存在无关依赖。
pnpm run dsh 从仓库根目录运行同一入口并直接转发参数,例如 pnpm run dsh -p "task"。构建形式(lib/bin.js,通过 pnpm run build)会在普通 Node 下启动同一配置。