@costa92/dshenv
v0.13.0
Published
Environment-as-Code manager for DeepSeek Harness
Readme
dshenv
dshenv 是用于 DeepSeek Harness (DSH) 的声明式环境与插件管理工具(Environment-as-Code layer)。
它构建在 DSH 官方插件管理器协议与能力之上,通过声明式清单(manifest.yaml)和精确锁文件(lock.json)管理多 Profile 的插件、精确版本与配置补丁。
当前提供环境探测、Profile 盘点、捕获与接管、声明式插件管理、差异比对、健康诊断,以及范围受限的实际应用能力。apply 通过 DSH CLI 执行 install/update/remove,通过 Profile dsh.profile.bundles 执行 enable/disable,并通过 cordis.patch.yml 受管块执行 configure。调用 DSH CLI 前会用与 doctor 相同的版本矩阵做门禁。remove 只卸载 state.ownership 中且已从清单删除的插件。
核心特性
- 审阅后应用:
plan与apply --dry-run先展示影响;正式apply对install/update/remove调用 DSH CLI,并在执行后重新盘点确认收敛。 - 声明式漂移检测:自动计算实际安装态与目标清单差异(
plan/status)。 - 无损环境捕获与接管:将现有 DSH Profile 盘点为可审阅的候选清单(
capture),确认事实未过期后再建立所有权(adopt)。 - 多运行时与能力探测:无缝支持源码运行模式(
--harness-source)、环境变量(DSH_CLI)及全局 PATH 探测(doctor)。 - 结构化输出:所有命令的成功结果均支持
--json格式;带--json时错误以{"error":{"type","message","exitCode"}}写入 stderr,命令行参数解析错误(未知选项、缺参数)也一样。 - DSH 配置双向同步:你在 DSH 里改的设置(模型、语言、权限、技能目录等写进
cordis.patch.yml的条目)由dshenv pull收进清单,含本机绝对路径的条目放进本机 overlay;~/.dsh/skills下的 loose skill 收进envctl/skills。apply按清单写回,plan能分辨改动来自 DSH 还是清单。 - 组件脚手架:
dshenv new从模板生成 skill/agent/tool/mcp 组件包,可选直接登记进清单。 - 团队共享基线:
dshenv remote add订阅团队 Git 配置仓库,dshenv remote sync预览并显式接受固定 commit 的更新;远程文件与团队 lock 条目只读,本机定制写本地 overlay,其插件的 lock 条目照常由本机维护。
路径与解析优先级
Harness 主目录解析优先级:
- CLI 参数
--dsh-home <path> - 环境变量
DSH_HOME(为空或只有空白时视为未设置,与 DSH 一致) - 默认用户主目录
~/.dsh
dshenv 数据目录(清单、lock、state、overlay、声明的 skill、快照、trash、日志)解析优先级:
- CLI 参数
--envctl-dir <path> - 环境变量
DSHENV_HOME(为空或只有空白时视为未设置) - 默认
<Harness 主目录>/envctl
DSH 读写 Profile,读 <Harness 主目录>/skills 与全局 cordis.patch.yml,从不读这个目录,所以它可以放在 ~/.dsh 之外,不改 DSH。数据目录本身及其中任何顶层条目(文件或目录)不能是软链接,否则命令以退出码 3 拒绝。把已有的数据目录(包括软链接方式放在别处的)搬到新位置用 dshenv migrate --to <dir> --yes:先完整复制并核对,再在旧位置留下记着新位置的 dshenv.moved 并清空旧目录,之后在旧位置运行的命令以退出码 3 拒绝并给出新位置;软链接形式的旧目录保留链接和目标。清单、overlay、lock 与快照里指向旧目录内部的路径会改写为新位置,之后运行 plan 与 apply --yes 让 DSH 按新路径重装本地插件。细节见设计文档 4.3、4.4 节。搬完后设置 DSHENV_HOME=<dir>。迁移在当前解析出的数据目录上进行,所以要在设置 DSHENV_HOME 之前运行;设置了新位置但其中还没有清单、而默认位置有清单时,命令会在 stderr 提示迁移。
dshenv migrate --to ~/dshenv-data # 预览,退出码 2
dshenv migrate --to ~/dshenv-data --yes
export DSHENV_HOME=~/dshenv-data开头的 ~、~/、~\ 按 DSH 的规则展开为用户主目录(.env、Docker ENV 里的 ~ 不经 shell 展开);其余相对路径先按当前工作目录转为绝对路径。盘点读取 Profile 的 package.json(dsh.profile.bundles + dependencies),不把 node_modules 中的传递依赖当成插件,也不跟随 Profile 外的 symlink 读取包元数据。
DSH 运行时命令解析优先级:
- 命令行
--harness-source <path>(转换为pnpm --silent --dir <sourceDir> dsh,在源码目录中运行)。相对路径按当前目录解析,目录不存在时以退出码 3 报错,不会改用其他 DSH - 环境变量
DSH_CLI(支持 JSON 数组或字面执行文件名,绝不进入 shell) - 清单中的
environment.harness.sourceDir(同样转换为pnpm --silent --dir <sourceDir> dsh,在源码目录中运行),在本机不存在时继续往下找 - 系统
PATH中的dsh
跨机器同步 manifest.yaml 与 overlays/;state.json、overlay-selection.json 只属于本机。lock.json 由本机维护,但订阅团队 remote 后,团队 lock 中的条目归远程、随 sync 更新(见第 20 节)。
改清单的命令(install、update、plugins config set 等)按固定格式重写整个 manifest.yaml 或 overlay 文件:其中的注释会被去掉,键也会重新排序。需要留存的说明请写在别处(如仓库的 README)。
安装与快速上手
从 npm 安装(推荐)
npm install -g @costa92/dshenv
# 或
pnpm add -g @costa92/dshenv
dshenv --versionnpm 包已含构建好的 lib/,安装时不需要构建;固定版本用 @costa92/[email protected]。各版本见 Releases(附同一份 .tgz),变更见 CHANGELOG。
从 Git 地址安装
pnpm add -g --allow-build=@costa92/dshenv "git+https://github.com/costa92/dshenv.git#v0.13.0"# 后可换成其他 tag、commit 或 master(未发布的最新代码)。安装时 pnpm 会在克隆中执行 prepare 构建 lib/;pnpm 10 默认不运行依赖的构建脚本,所以必须带 --allow-build=@costa92/dshenv,否则安装后缺少 lib/ 无法运行。npm 从 Git 地址安装时会在准备阶段崩溃(npm 10.9 arborist 缺陷),请使用 pnpm。
本地链接安装
cd /path/to/dshenv
pnpm install
pnpm build
pnpm link --global验证安装:
dshenv --version升级 dshenv
dshenv self-update --check # 只查询:有新版本时退出码 2,已是最新时退出码 0
dshenv self-update # 升级到 npm 上的最新版本
dshenv self-update --to 0.2.0 # 升级或回退(降级)到指定的精确版本self-update 用 npm view --prefer-online 查询版本,再用安装 dshenv 的包管理器替换自身:全局 npm 安装执行 npm install -g @costa92/dshenv@<版本> --prefer-online,全局 pnpm 安装执行 pnpm add -g @costa92/dshenv@<版本>。安装过程的输出直接显示在终端(stderr),不设超时,因为中途打断可能留下装了一半的全局包。
- 不带
--to时只会升级:本机版本高于 npm 上的最新版(如预发布版或本地构建)时报告newer-installed,不做改动。回退必须用--to明确指定,输出会注明是降级。 - 本地链接或源码检出安装的 dshenv,以及用 pnpm 从 Git 地址安装的 dshenv,有版本要装时不会被替换为 npm 版本,而以退出码 3 给出升级方法(如
git pull && pnpm build)。已是最新或本机版本更新时,照常报告up-to-date/newer-installed,退出码 0。--check对这类安装仍会报告,method为null。 - 失败时只显示错误码(如
EACCES、ERR_PNPM_FETCH_401),完整原因见上方包管理器自己的输出;EACCES表示全局安装目录不可写。 - Windows 上 npm 会在
dshenv.cmd运行期间覆盖它,升级成功后命令行可能多出一行批处理报错,可以忽略,用dshenv --version确认版本。
卸载:npm uninstall -g @costa92/dshenv(pnpm 安装的用 pnpm remove -g @costa92/dshenv);~/.dsh/envctl 保留,不需要时手动删除。
也可以手动升级:npm install -g @costa92/dshenv@latest --prefer-online。刚发布的版本在本机 npm 缓存过期前可能报 notarget,加 --prefer-online 即可。升级前先看 CHANGELOG 中的「变更」,例如 0.2.0 起清单不再接受 npm 来源的 registry 字段,lock 中的 git commit 必须是十六进制 commit id。
完整流程、能力边界、agent-teams 示例和常见问题见 中文使用教程。
DSH 发布新版本时,用 make smoke-dsh DSH_VERSION=<版本> 验证兼容性,放宽版本门禁的步骤见 DSH 新版本兼容验证。
发布新版本的步骤见 发布流程。
命令参考
插件命令也可以写在 plugins 下(dshenv plugins install|update|remove|enable|disable|list|config|official),与 tools、web、source 等命令组写法一致;install、update、remove、enable、disable 同时保留在顶层。旧命令名 list、config、runtime、source pull 仍可使用,但不再出现在帮助里,分别对应 plugins list、plugins config、verify、source sync。
选择 Profile(-p)
-p, --profile <name> 是 DSH Profile 的名字,即 profiles/<name>/ 的目录名和清单 profiles: 下的键。所有命令用同一套规则:
作用于单个 Profile 的命令(
install、update、enable、disable、remove、purge、plugins config、tools、verify、web start、web stop):不写-p时取环境变量DSHENV_PROFILE;两者都没有时以退出码 3 报错并列出可选的 Profile(清单声明的与 DSH 已创建的)。verify、web start、web stop在清单只声明一个 Profile 时直接用它。按 Profile 过滤的命令(
plugins list、plugins official、plan、apply、status、pull、capture、overlay show、mark-restarted、web list):不写-p表示全部 Profile,DSHENV_PROFILE对它们不生效;-p写了清单没声明、DSH 也没创建的名字时以退出码 3 拒绝,不会把拼错的名字报成“已同步”。pull、capture的-p还要求 DSH 已创建该 Profile,overlay show的-p要求合并后的清单声明了它,否则报Profile not found一类错误(退出码 3),不给相近名字。source、new的-p表示把克隆或新建的包登记到该 Profile,不写就不登记。写入命令(
install、new -p、source clone -p)指定一个清单没声明、DSH 也没创建的 Profile 时,默认按拼写错误拒绝并给出相近的名字;确实要新建时加--new-profile。环境里还没有任何 Profile(清单与 DSH 都没有)时不做这项检查,DSH 的模板 Profile 名(acp、web等)也不例外。写入类命令用了
DSHENV_PROFILE时,会在 stderr 提示Using profile 'web' from DSHENV_PROFILE(--json时不提示);只读命令(plugins config get、tools list、verify、web start等)不提示。
export DSHENV_PROFILE=web # 之后 dshenv web start、dshenv verify 等可以省略 -p
dshenv disable agent-teams # 等同于 dshenv disable agent-teams -p web确认执行(--yes)
哪些命令要加 --yes,只看一条规则:
- 只改 envctl 声明(清单、overlay、lock)的命令直接写入:
install、update、remove、enable、disable、plugins config set|unset、tools enable|disable|config set|unset|reset、overlay create|use、new -p、source clone -p。它们不碰 DSH profile,改错了再改回来即可,DSH 要等apply --yes才变。 - 会改 DSH、批量接管或覆盖文件的命令要
--yes:apply、adopt、pull、rollback、gc、purge、migrate、remote add|sync|remove、source sync(它会移动 Git 检出并改写 lock,与remote sync一致)。不加--yes时只预览、什么都不写;有待执行的内容时退出码为 2,并提示加--yes重跑(remote add、remote sync写在 stdout,其余写在 stderr),所以 CI 里可以直接用不带--yes的命令检查漂移。这些命令都接受--dry-run:即使同时写了--yes也只预览(self-update的--dry-run同--check)。self-update只替换 dshenv 自己,不碰 DSH 与 envctl,所以不需要--yes,直接执行。 overlay use --none与--no-overlay不同:前者清除保存的 overlay 选择,之后的命令都不再用它;后者只让这一条命令不用 overlay。
remove 仍接受 -y(旧脚本兼容),但它只改清单,加不加都一样。
1. dshenv doctor
探测 DSH 运行时能力并检查环境就绪状态。
# 自动探测系统 DSH
dshenv doctor
# 指定 DSH 源码目录
dshenv doctor --harness-source "$HOME/code/dsh/deepseek-harness"
# 结构化 JSON 输出
dshenv doctor --jsonPaths 里的 Manager Dir 注明数据目录从哪里来:(from --envctl-dir)、(from DSHENV_HOME) 或 (default),--json 对应 paths.managerDirSource 的 flag、env、default。
doctor --json 在 runtime.capabilities 中逐项报告能力状态,并保留 runtime.discoverySupported、runtime.mutationsSupported 等兼容字段:
| 状态 | 含义 |
| :--- | :--- |
| available | 已知 DSH 版本支持该能力,且需要的本地证据已验证;仅表示该项能力可见。 |
| requires-live-service | 该能力依赖已连接并认证的 live manager service;当前阶段尚未建立其连接契约。 |
| disabled | 当前版本、探测证据或本工具的安全边界不允许使用该能力。 |
对已验证的 DSH 0.1.7-rc.2 源码,discovery 与 packageOperations 为 available,bundleSelection 与 entryToggle 为 requires-live-service,configurationValidation 与 environmentMutation 为 disabled。packageOperations 需要官方 operations export 的声明及目标文件均通过只读探测;只通过 DSH 命令探测、缺少可验证源码时,该项为 disabled。
runtime.mutationsSupported=false(doctor 文本输出为 Mutation Capability: Planned apply steps only)表示通用、完整的环境写能力仍未开放。它不代表 apply 命令不存在:当前 apply 通过 DSH CLI 执行 install/update/remove,通过 Profile dsh.profile.bundles 执行 enable/disable,并对 configure 写入 cordis.patch.yml 受管块。计划之外的通用环境变更仍不受支持。
2. dshenv init
在 dshenv 数据目录(默认 $DSH_HOME/envctl/,见路径与解析优先级)下初始化空的清单、锁文件与初始状态。
dshenv init
dshenv init --dsh-home /path/to/custom-dsh3. dshenv capture
无损盘点现有 DSH Profile 并生成待审阅的候选清单。
# 输出到控制台
dshenv capture
# 只捕获一个 Profile
dshenv capture --profile web
# 原子写入审阅文件(若目标文件已存在则拒绝覆盖)
dshenv capture --output my-dsh-backup.yaml4. dshenv adopt
接管来自 capture 生成的候选清单,建立明确的插件所有权记录。
dshenv adopt my-candidate.yaml # 预览会接管哪些插件,有待接管时退出码 2,不写文件
dshenv adopt my-candidate.yaml --yes # 校验候选事实一致性并接管所有权不带 --yes 时 adopt 只列出将接管的插件;带 --yes 才写入清单、锁文件和状态。
接管的 Profile 中写在 cordis.patch.yml 受管块之外的条目,adopt 会按 dshenv pull 的规则一并收进清单。
本地来源(link:、file:)的插件带有本机绝对路径,adopt 不写进 base,而是同 pull 一样放进本机 overlay(没有选中的 overlay 时新建并选中 local)并记录源码摘要,预览中标为 into an overlay;--no-overlay 且未选 overlay 时在写入前以退出码 3 拒绝。base 已声明的同名包仍在 base 中更新。
5. dshenv plan
比对期望清单与当前 Profile 实际安装状态,计算变更计划。
dshenv plan
dshenv plan -p web # 只看一个 Profileplan 只读文件,不调用 DSH。清单声明并启用了 DSH 0.2.1-alpha.1 退役的 bundle(@deepseek-ai/dsh-experimental-schedule-bundle,Web 组合自己挂载 Schedule)时,plan 在 Retired bundles 下警告并给出 dshenv remove <别名> -p <profile>(别名只在当前 overlay 里声明时加 --layer overlay):DSH 0.2.1 每次加载都会把它从 Profile 的 bundle 列表里删掉,它会一直显示为漂移。--json 新增 retiredBundles(overlay 声明的项带 layer: "overlay")。
6. dshenv apply
基于受管清单与锁文件,将期望状态安全收敛应用到 DSH 运行环境中(具备独占写锁、快照备份与操作日志审计)。
# 模拟执行(不修改磁盘或获取排他锁);有变更时退出码 2
dshenv apply --dry-run
# 执行变更并提交状态
dshenv apply --yes
# 只应用一个 Profile,与 plan -p web 看到的一致
dshenv apply -p web --yes-p 与 plan -p 一样只读取、只改动这个 Profile 的插件与 profile patch,其他 Profile 的所有权与重启记录原样保留;skill 在 $DSH_HOME/skills 下、不属于任何 Profile,照常一并应用。
不带 --yes 的 apply 与 --dry-run 相同:展示计划、有变更时退出码 2,并在 stderr 提示加 --yes 重跑。pull、rollback、gc、purge、adopt、migrate、remote add、remote remove、remote sync、source sync 同样如此:不带 --yes 只预览,有待执行的内容时退出码 2,没有时退出码 0。例外:要做的事无从做起时按错误处理,退出码 3,例如没有任何快照时的 rollback、没有订阅时的 remote remove。
当前执行计划中的 install/update/enable/disable/remove/configure。configure 只写入 Profile cordis.patch.yml 的受管块;停用的插件 DSH 不会加载,它的补丁块会被清掉(清单里保留,重新启用时写回)。没有所有权记录的实际插件只标为 unmanaged,不会卸载。
apply(含 --dry-run)会对有操作的 Profile 各运行一次 dsh --profile <p> --dump-config(超时 15 秒),读其中的 hmr 行判断 DSH 热加载是否开启,据此报告哪些改动无需重启:
| 操作 | 热加载开启 | 热加载关闭或无法判断 |
| --- | --- | --- |
| install、enable、disable、configure、remove | 无需重启(state 记为 healthy,remove 删除条目) | 需要重启(restart-required) |
| update(npm 版本、Git commit、本地源码变化) | 需要重启 | 需要重启 |
成功后在计划之后输出分组,某组为空时省略,没有需要重启的项时不输出 Then run 行:
No restart needed:
[web] enable @nanmicoder/dsh-agent-teams
Restart DSH to load:
[web] update shared-plugin (package updates are not hot-reloaded)
[cli] install tool-x (hot reload is off for profile cli)
Then run: dshenv mark-restarted全局补丁($DSH_HOME/cordis.patch.yml)有变化时,它对每个 Profile 都生效,所以对每个已创建的 Profile 都探测一次,热加载关闭的列为 [<profile>] configure global patches。
--dry-run 在每个计划操作后标注 (no restart) 或 (restart required: <原因>)。--json 结果新增 restart: { notRequired, required },每项为 { profile, package, kind, reason, detail? },reason 取 hmr-on、package-update、hmr-off、hmr-unknown,detail 只在 hmr-unknown 时出现,为探测失败的原因。Profile 尚未创建时不运行探测(--dump-config 会创建 Profile),按无法判断处理。
要写 Profile 补丁或全局补丁时,apply(含预览)先在一个临时的 DSH 主目录副本里放上要写入的补丁文件(主目录下的目录用软链接、其余文件不放入;Profile 目录里的目录用软链接、文件复制;真实的 DSH 主目录不变),对相关 Profile 运行 dsh --profile <p> --dump-config,读 DSH 在 stderr 里报告的被跳过的条目(patch: entry "<id>" not found,以及 name mismatch … skipping),再对当前文件运行一次作对比:
- 预览在
Patch entries DSH would skip, as no row has their id下列出全部匹配不到的条目,本次新增的标为(new: apply --yes stops on it);--json结果新增patchTargets: { unmatched, added, unchecked }。 apply --yes只在两种情况下以退出码 3 停下,什么都不改:Profile 条目的 id 现在在任何一层都匹配得到,写入后却匹配不到;全局条目的 id 在每个已有 Profile 里都没有对应的行(全局条目指向部分 Profile 才有的行属正常,只列出不阻断)。已经匹配不到的 id 照常放行,换到另一层也一样,例如 0.2 起移进 preset 的time-context。- 本次 apply 要装、启用、停用或卸载插件的 Profile 不检查:插件提供的行要等插件步骤之后才有,在
Patch ids not checked下注明;有这样的 Profile 时全局条目也不阻断。 - 检测不到的情况:
--dump-config不加载插件、不求值!!js,所以条件表达式的结果、插件启动失败都发现不了;被策略禁用的插件行仍算存在。找不到 DSH 时不检查;DSH 运行失败时在Patch ids not checked下说明原因,不阻断。
apply --yes --verify 在应用之后,对每个有操作的 Profile 像 verify 一样核对运行中的 dsh web(先看 DSHENV_DSH_URL,否则用 dshenv web start 启动的那个;有操作的 Profile 不止一个时不用 DSHENV_DSH_URL,因为无法判断它指向哪个 Profile,只核对各自 web start 启动的那个),并输出同样的核对结果。结果仍可能随热加载改变(loading、not-loaded、still-loaded)时每秒再问一次,最多等 --verify-timeout <秒>(默认 30);failed、missing 等立即报告。退出码与 verify 相同:都已加载为 0,超时仍在加载为 2,加载失败或核对出错为 5。没有 dsh web 在运行的 Profile 在 stderr 注明 Not verified,不影响退出码;不带 --yes 时拒绝 --verify(退出码 3)。--json 结果新增 verify,每项为 { profile, endpoint, results }、{ profile, skipped } 或 { profile, error }。
dshenv 改写 Profile package.json(启用、停用、卸载前移出 bundle)或 cordis.patch.yml(写入、清除受管块及回滚恢复)时持有 DSH 的 package.json.lock,被占用时最多等 30 秒。热加载开启时卸载插件会先移出 bundle、等待 3 秒让 DSH 卸下插件,再调用 dsh plugin remove;插件本来就不在 bundle 列表中时不等待。apply 失败回滚 cordis.patch.yml 时,如果 DSH 在 dshenv 写入之后又改过该文件,只把该插件的受管块恢复原样,DSH 的改动保留。
几点说明:
- 「无需重启」表示 DSH 会自动重新加载;dshenv 不确认插件是否真的加载成功(DSH 只在日志里记录重新加载失败)。
configure会列在分组里,但不写入 state,所以只含configure的 apply 之后运行dshenv mark-restarted可能显示清除了 0 个插件。- 判定为无需重启的
remove会连同该插件此前的restart-required条目一起删除(插件已经不在了)。 - 等 Profile 锁超时后 apply 会回滚,但回滚本身写 bundle 与
cordis.patch.yml时也可能要等这把锁;回滚未能完成时运行dshenv plan查看现状。DSH Web 安装插件时整个安装过程都持锁,可能超过 dshenv 的 30 秒等待,等安装结束后再重试。
7. dshenv rollback
新快照按实际内容保存单个软链接 skill 目录;恢复时在 envctl/skills 下创建独立目录副本,不改写原来的外部目标,skill 内部的软链接保留。旧快照若仅保存了链接,无法恢复当时未备份的目标内容。
从 envctl/backups/ 恢复最近一次(或指定 operation id 的)管理文件快照(rollback 自己也会先存一份 pre-rollback-* 快照,所以连续执行不带 id 的 rollback 会在两个状态之间来回切换;要再往前退须指定 id):manifest.yaml / lock.json / state.json、envctl/skills、remote.json,以及快照时保存的 overlay(团队 overlay、pull/sync 改写的本机 overlay、apply 时生效的 overlay)。不带 id 时跳过失败的 apply 留下的快照(它们已经自己恢复了 lock.json 与 state.json,恢复它们等于什么都不做),输出会写明跳过了哪些;apply 的快照保存的正是它所应用的清单与 overlay,所以回到某次成功的 apply 就是回到它应用的清单。不撤销已经发生的 DSH 包安装;由 apply 安装、仍装在 Profile 里的插件保留所有权记录,之后从清单删除时照常卸载。DSH_HOME/skills 里仍与 dshenv 上次同步时一致的 skill 同样保留基线,恢复后的清单不再声明它时,下一次 apply 把它移进 trash。恢复前会把当前这些文件另存为一份新快照(输出中给出其 id,可再 rollback 回去);快照里没有的文件会被删除。
dshenv rollback --dry-run
dshenv rollback --yes
dshenv rollback apply-abc123 --yes8. dshenv gc
删除 envctl/trash/ 中超过保留期的条目,以及 envctl/backups/ 中超过保留期的快照(始终保留最近 10 个,rollback 只能回到还在的快照)。默认 7 天。不会删除这两个目录之外的路径。
dshenv gc --dry-run
dshenv gc --yes
dshenv gc --older-than 3 --yes9. dshenv purge
把有所有权的受管 patch(以及 envctl/sources/<profile>/<package> 下的 clone)移入 envctl/trash/<operation-id>。不删除外部 Git 目录、Profile 根或凭据。apply 删除插件后所有权记录随之去掉,这时用包名执行 purge 仍可清理它留在 envctl/sources 下的 clone(清单不再声明该包时)。
dshenv purge agent-teams --profile web --dry-run
dshenv purge agent-teams --profile web --yes10. dshenv plugins list
以表格列出清单中的插件(PROFILE ALIAS PACKAGE VERSION ENABLED INSTALLED,有生效 overlay 时加 ORIGIN 列),以及 plan 标出的 unmanaged 包(别名显示为 (unmanaged))。非 npm 来源的 VERSION 列写来源类型;没有插件时提示如何添加。
dshenv plugins list
dshenv plugins list --profile web --json11. dshenv update
只改清单(以及已有 lock 条目)里的精确 npm 版本,不解析 latest。随后用 apply --yes 真正更新。
dshenv update agent-teams --profile web --to 0.1.2212. dshenv plugins config
读取或改清单中的插件配置。set 只写 manifest;apply 才会落到 cordis.patch.yml。
dshenv plugins config get agent-teams --profile web
dshenv plugins config get agent-teams taskPlanning --profile web # 只读一个字段
dshenv plugins config validate agent-teams --profile web
dshenv plugins config set agent-teams taskPlanning captain --profile web
dshenv plugins config unset agent-teams taskPlanning --profile web # 删掉一个键- 值按 JSON 解析(
3、true、{"a":1}),解析不了时当作字符串;要写字符串"3"就传'"3"'。 - DSH 用补丁的
config整体替换插件的配置,所以清单里还没有这个插件的补丁时,config set先把 DSH 当前为它组合出的全部配置(dsh --dump-config,含!!js表达式)抄进补丁,再改这个键,插件的默认值得以保留;之后的config set只改各自的键。只在没有 dshenv 补丁生效时这样做(含 overlay 给它的补丁,否则抄到的是那份补丁而不是默认值);抄来的配置含本机路径时写 base 以退出码 3 拒绝,改用--layer overlay。抄不了时(DSH 还没有这个插件、Profile 未创建、DSH 无法运行,或已有补丁生效)只写这个键,并在 stderr 说明原因。新补丁的 id 取 DSH 加载该插件的那一行(bundle 插件的行 id 可能与别名不同);键只写进enabled不为false的补丁,全都禁用时以退出码 3 拒绝。 - DSH 为该插件组合出了配置而其中没有这个顶层键时,
config set在 stderr 提示一行(附相近的键名)后照常写入:DSH 只组合出带默认值的键,插件文档里的键可能不在其中。--force不再提示。 - 读取或删除不存在的键时以退出码 3 报错;只认配置里自己的键,不会读到
toString这类继承来的属性。路径为空,或含__proto__、prototype、constructor时以退出码 3 拒绝。 config unset在该插件声明的所有 patch 里找这个键;删完后什么都不设的 patch 会一并删掉,不留下config: {}。
13. dshenv status
显示当前环境状态摘要与操作统计;给出插件别名或包名时只显示该插件。有待执行的变更时退出码 2,环境已同步时为 0,计划中有 blocked 操作时为 5;环境降级(degraded)或 DSH 不兼容(incompatible)时为 5。
- 能找到 DSH、且版本通过门禁时,对每个已创建的 Profile 运行一次
dsh --dump-config(DSH 每次都会重写 Profile 的cordis.yml,0.2.1 还会从package.json删掉退役 bundle),列出 DSH 加载时跳过的 bundle(Bundles DSH skips when it loads the profile)。这类 bundle 声明了、装上了,却没有运行,plan看不出来,所以status记为degraded。doctor在Profile bundles下给出同样的列表。 - 列出各 Profile
compatibility.json里的版本豁免(dsh plugin allow-version写入的包@版本 -> DSH 版本)。只列 DSH 认可的记录(精确的包名@版本映射到精确的 DSH 版本),其余记录和读不了的文件在 stderr 警告。豁免绑定精确的 DSH 版本,换机器或升级 DSH 后就失效,所以不进清单。 - 列出清单仍启用的退役 bundle(详见
plan)。 - 给出插件时,以上几项只列与它有关的。
--json新增skippedBundles、versionExemptions、retiredBundles。
dshenv status
dshenv status agent-teams -p web
dshenv status --json14. dshenv source clone
带 --profile 时克隆到 envctl/sources/<profile>/<package>(带作用域的包名里的 / 换成 _,如 @scope_plugin),并把 HEAD commit 写入 lock;包名取仓库 package.json 的 name(可用 --package 指定)。清单里这个别名已声明同一仓库的 commit 或 ref(如 install <url>#<sha> 写入的)时,克隆并锁定它而不是 HEAD,commit 不在仓库里时报错;给了 --ref 时改用该 ref 并写进清单。别名默认与 install 相同(仓库名去掉 dsh-plugin-、dsh- 前缀);别名已指向另一个包时拒绝(退出码 3)。本机仓库的路径(如 /src/plugin)记为 file:// 地址,pnpm 才会按 Git 仓库安装。带账号密码或 token 的 URL 会被拒绝,请改用 SSH 或 git credential helper。随后 apply --yes 才能安装。显式给出目标目录时仍可克隆到外部路径(purge 不会删除外部目录)。
dshenv source clone https://github.com/ex/plugin.git --profile web --as demo
dshenv source clone https://github.com/ex/plugin.git ./external-checkout
dshenv source show -p web --as demo # 受管 clone 的 Git 状态(是否有未提交改动、commit、分支)与源码摘要
dshenv source sync -p web --as demo # 预览:clone 与 lock 会移到哪个 commit,有变化时退出码 2
dshenv source sync -p web --as demo --yes # 快进受管 clone,并把新的 HEAD commit 写入 lock
dshenv source sync -p web --as demo --ref v1.2.0 --yessource sync 不带 --yes 时只 fetch 并预览(--dry-run 相同),不移动检出、不写 lock,与 remote sync 一致。旧名 source pull 仍可用,但会在 stderr 提醒:它把 clone 从上游 Git 拉新,方向与 dshenv pull(把 DSH 里的改动收回清单)相反。
source show / source sync 不带 --profile 时作用于给出的目录(不给时为当前目录),sync 只快进、不写 lock,--ref 指向更旧的 commit 时以退出码 3 拒绝;不给 --ref 时快进到当前分支对应的远端分支,处于 detached HEAD 时须给 --ref。带 --profile 时,Profile 里恰好有一个 Git 插件可省略 --as;envctl/sources 下的受管 clone 也可以用 --ref 退回到更旧的 commit(如 plan 提示的清单 commit),处于 detached HEAD 且不给 --ref 时跟随清单的 ref,没有时跟随远端默认分支;清单锁定的 commit 与新 commit 不一致时在 stderr 提示(plan 会显示 blocked)。sync 总是按清单中的 URL 写入完整的 lock 条目,之后 apply --yes 安装新 commit。base 与所有 overlay 都不再声明的别名,其 lock 条目在下次 apply 时删除,之后用同一别名重新加入的 Git 插件须重新锁定 commit。
不是 DSH bundle 的插件包(package.json 没有 dsh.bundle,例如 dsh-session-search)不能放进 bundle 列表,DSH 会跳过它。dshenv 在安装后检查包类型,这类插件改为在 cordis.patch.yml 里写一个受管的 insert 行挂载(# dshenv:begin ... plugin=@mount:<alias>),enable/disable 切换这一行,verify 按已加载的插件条目判断。
15. dshenv overlay
按机器/环境在 base 清单(envctl/manifest.yaml)之上叠加 envctl/overlays/<name>.yaml。
dshenv overlay create laptop # 新建空 overlay:envctl/overlays/laptop.yaml
dshenv overlay use laptop # 本机持久选择
dshenv overlay use --none # 清除选择
dshenv overlay list # 列出 overlay,标出当前生效项
dshenv overlay show --json # 合并结果与每个插件的出处
dshenv plan --overlay server # 单次命令临时指定
dshenv plan --no-overlay # 单次命令只用 baseoverlay 文件格式与清单相同的 profiles 结构,只写需要覆盖的字段;插件条目写 remove: true 删除 base 中的同名插件,Profile 级 patches 中写 { id, remove: true } 删除同 id 的条目;插件自己的 patches 只能按 id 覆盖或新增,不能删除:
apiVersion: dshenv-overlay/v1
profiles:
web:
plugins:
agent-teams:
enabled: false # 覆盖 base 中的字段
local-tool: # 只在本机存在的插件
package: "@me/local-tool"
source: { type: local-link, path: /home/me/src/local-tool }
old-plugin:
remove: true # 本机不装 base 中的这个插件选择优先级:--overlay 或 --no-overlay(两者同时使用时报错,退出码 3)> DSHENV_OVERLAY > 本机选择文件。选中的 overlay 不存在或无效时报错,不会退回只用 base。overlay 里对 base 已不再声明的插件写的 remove: true 或字段覆盖(不带 package)不起作用、也不报错,例如团队 sync 删掉了本机 overlay 停用或改了来源的插件;带 package 的条目仍按新增插件处理,必须写 source。有生效 overlay 时,改清单的命令(install、update、enable、disable、remove、plugins config set/unset、tools enable/disable/config/reset、source clone --profile、new -p)必须带 --layer base 或 --layer overlay;也可以设环境变量 DSHENV_LAYER 作为默认值,使用时会在 stderr 提示。没有生效 overlay 时 DSHENV_LAYER=base 不起作用,DSHENV_LAYER=overlay 则与 --layer overlay 一样以退出码 3 拒绝。adopt 不需要 --layer:共享的插件写 base,本地来源的插件进 overlay。
16. dshenv mark-restarted
apply 输出 Restart DSH to load: 分组时,其中插件的状态标为 restart-required(升级了已装插件,或该 Profile 的热加载关闭、无法判断)。热加载开启时的安装、启用、停用、配置与卸载当场生效,不需要本命令。重启 DSH 后运行本命令确认,清除该状态(已卸载插件的条目一并删除)。dshenv 无法自行判断 DSH 是否已重启。
dshenv mark-restarted
dshenv mark-restarted --profile web --json旧名 dshenv restarted 仍可使用。
verify:核对运行中的 DSH 是否已加载
apply 只能推断改动是否已被热加载。dshenv verify 登录运行中的 dsh web,读取 Plugin Manager 报告的真实加载状态,与清单对比:
export DSHENV_DSH_URL='http://127.0.0.1:3080/?token=...' # dsh web 启动时打印的地址
dshenv verify --profile web- 没有设置
DSHENV_DSH_URL时,使用dshenv web start为该 Profile 启动并仍在运行的 dsh web(见第 24 节)。 --timeout <秒>在结果仍是loading、not-loaded、still-loaded(DSH 可能还在热加载)时每秒再查一次,最多等这么久,与apply --verify-timeout相同;默认 0,只查一次。- 也可以用
dshenv verify --profile web --start临时启动一个(dsh --profile web --no-open --port 0),核对完即停止;它不读取DSHENV_DSH_URL,Profile 须已存在。 - 显式给出地址只能用环境变量
DSHENV_DSH_URL,没有对应的命令行参数。地址等同于登录凭据,dshenv 不会输出或记录其中的 token;默认只连本机,连其他主机需加--allow-remote,且地址必须是 https(明文 http 会把 token 暴露在网络上)。 - 只适用于
dsh web;headless、sdk、acp 运行不开 web 服务,无法核对(--start以退出码 3 报Profile <p> runs <应用包>, not dsh web)。 - 每个插件的结果:
loaded、unloaded(符合清单),loading(热加载进行中,也覆盖apply之后 DSH 还未热加载的瞬间;若持续为loading,说明热加载没有生效,需重启 DSH;带is waiting for services it injects时,是插件依赖的 service 还没有任何插件提供,检查是否漏装或停用了提供它的插件),unverifiable(包没有可核对的插件行),missing、failed、not-loaded、still-loaded(与清单不符)。 - 退出码:
0全部符合;2仍在加载,稍后重跑;5有不符项;1无法连接、登录失败或运行中的 DSH 不是该 profile;3用法错误。 - 不核对版本:DSH 只报告磁盘上的版本,看不出内存中加载的是哪个版本;升级插件后仍需重启 DSH。
17. 在 CI 中使用
仓库自身的 CI 见 .github/workflows/ci.yml(Node 22/24 上跑 typecheck、test、build,合入 master 必须通过)。对真实 DSH 的检查与之分层:e2e.yml 在 PR 与 master 上对已验证的 DSH 跑主链端到端测试(make e2e-dsh DSH_VERSION=<版本>),compat.yml 每天对已验证版本、latest 与 next 跑兼容冒烟(make smoke-dsh)。在你的配置仓库里校验清单与 overlay、在真实环境上做漂移门禁,可参考 docs/examples/github-actions/dshenv-check.yml,说明见 docs/使用教程.md 第 15 节。
18. dshenv new
从模板生成 DSH 组件包,可选直接登记进清单:
dshenv new skill code-review # skill bundle:skills/code-review/SKILL.md
dshenv new skill code-review --loose # 直接写入 $DSH_HOME/skills/code-review/SKILL.md
dshenv new agent reviewer # agent 预设(dsh-agent-preset + dsh-persona)
dshenv new tool echo-text -p web # 纯 JS tool 插件,并登记到 profile web
dshenv new tool echo-text --typescript # TypeScript 版本,需先 pnpm install && pnpm build
dshenv new mcp docs-server # MCP server 配置包- 名称必须是 kebab-case;目标目录非空时拒绝。
-p等同于随后执行dshenv install <目录> -p <profile>(支持--as、--layer);登记失败时删除生成的目录。不会自动apply。
19. 容器示例
docs/examples/container/ 提供构建 DSH Web 容器镜像的 Dockerfile、compose.yaml 与 cordis.patch.yml,镜像构建期执行 dshenv apply 装好清单声明的插件。安全要点:容器内监听 0.0.0.0 只是为了让 Docker 转发端口,宿主机端口必须只发布到 127.0.0.1(不要用 -P),否则会把 DSH Web 的 shell 执行能力暴露到局域网;回环发布挡不住同一 Docker 网络内的其他容器,它们能直接访问容器 IP 并通过 Host 校验,只剩启动 token 一道防线,因此应放在独立的自定义网络上(compose 的项目网络即可,但同项目新增的服务也能访问)。完整用法、构建参数与数据卷说明见 docs/examples/container/README.md。
20. dshenv remote
团队在一个 Git 配置仓库中维护 base 清单、lock 与团队 overlay(默认读取仓库内 envctl/),成员订阅后按固定 commit 显式接受更新:
dshenv remote add [email protected]:team/dsh-config.git # 预览:文件变化与接受后的 plan,退出码 2,不写文件
dshenv remote add [email protected]:team/dsh-config.git --expect <commit> --yes # 只接受预览过的那个 commit(预览末尾给出这条命令)
dshenv remote show # URL、分支、固定 commit、远程文件与 lock 条目及本地改动
dshenv remote sync # 拉取并预览更新,退出码 2;已是最新时退出码 0
dshenv remote sync --expect <commit> --yes # 只接受预览过的更新(只接受 fast-forward),之后自行 plan / apply
dshenv remote sync --ref v1.2.0 --yes # 移动到订阅分支上的某个 tag 或 commit
dshenv remote remove # 预览取消订阅,退出码 2
dshenv remote remove --yes # 取消订阅,文件保留为本地文件顶层的 dshenv sync 是 remote sync 的旧名,仍可使用。
- 只采用
<path>/manifest.yaml(必需)、<path>/lock.json、<path>/overlays/*.yaml与<path>/skills/<名字>/...(团队 skill,随apply装进DSH_HOME/skills);--path指定仓库内目录(.为仓库根),--branch指定分支(默认远程 HEAD 所指分支)。团队 manifest、团队 overlay 与团队 lock 都不能使用local-link/local-file源或指向本机的 Git 地址(file://、本地路径),团队 manifest 与团队 overlay 也不能设置environment.harness.sourceDir/environment.sourceRoot(本机路径无法跨机器共享,且 dshenv 会执行该目录下的 DSH),团队 manifest 与团队 overlay 的插件 patch 与 profile patch 也不能含 JavaScript 表达式(__jsExpr,dshenv 会把它写成 DSH 执行的!!js值),否则整个 commit 被拒绝;这些请写在本机 overlay 里。这些限制并不让团队配置变得无害:团队声明的插件会被安装并运行,插件配置(如dsh-mcp-client的command)与 profile patch 也能让 DSH 在成员机器上启动进程。只订阅可信的仓库,并在sync --yes前审阅预览。 manifest.yaml与团队 overlay 整文件归远程;lock.json按profile/alias条目归属:团队 lock 中的条目归远程,其余条目(本地 overlay 插件的 Git commit、本地源摘要)归本机,同步时只替换团队条目。本地 overlay 把团队 lock 已固定的插件改为local-link/local-file源时,apply以退出码 3 拒绝;应在本地 overlay 中对它写remove: true,再以新 alias 加入本地源插件。- 远程内容只读:写 base、写远程 overlay、改写团队 lock 条目的命令都以退出码 3 拒绝;本机定制写本地 overlay(
--layer overlay),source clone --profile等写本机条目的命令照常可用。 - 本地已有
manifest.yaml、同名 overlay,或本地 lock 已有团队 lock 同名条目时,remote add需要--replace(先快照再覆盖);本地改过远程文件或团队条目时sync拒绝,--discard-local-changes可覆盖。 - 团队仓库提供的 skill(
skills/<名字>/...)整个目录归团队所有:envctl/skills里已有不归团队的同名 skill 时,remote add与sync都拒绝(--replace也不行,以免两份混在一起),需先移走本地的那份。 - 每次接受都会先建快照,用
dshenv rollback <快照 id> --yes撤销(id 见接受时输出的snapshot ...;之后又apply过时,不带 id 的rollback --yes只会撤销那次 apply);接受后不会自动apply。接受更新即同意执行其中声明的插件。 - URL 不得内嵌凭据(认证交给 SSH 或 git credential helper);git 失败时退出码 1,并带出 git 的原始错误。
- 预览之后分支可能又有新提交,不带
--ref的--yes接受的是那时的最新 commit;预览末尾给出带--expect <预览的完整 commit>的命令:要接受的 commit 已不是它时以退出码 3 拒绝,接受的正是审阅过的内容。 --json时sync输出{status, from, to, files: {added, modified, removed}, lockEntries: {added, modified, removed}, plan},接受时另有operationId与snapshotId;lock 条目写作<profile>/<alias>,status为up-to-date、pending或accepted。
21. dshenv self-update
查询或升级 dshenv 自身,用法与安装方式的判断见升级 dshenv。--json 输出 {status, current, target, direction?, method?, command?}:status 为 up-to-date、newer-installed(本机版本高于最新版,不做改动)、available(仅 --check)或 updated;direction 为 upgrade 或 downgrade;method 为 npm、pnpm,无法自行替换时为 null。
22. dshenv pull
把 DSH 里改动的设置和装进 Profile 的插件收进清单,是 apply 的反方向。你在 DSH 界面里改模型、语言等设置时,DSH 会改写 Profile 的 cordis.patch.yml;plan 会在 Patch entries not in the manifest 下列出受管块之外的条目,或提示受管块在 DSH 里被改过。
dshenv pull # 预览,有变更时退出码 2(与 --dry-run 相同)
dshenv pull --yes # 收进清单,并把这些条目整理进 dshenv 的受管块
dshenv pull --yes --profile web # 只处理一个 Profile
dshenv pull --yes --prefer dsh # DSH 与清单都改过时,以 DSH 为准(--prefer manifest 以清单为准)
dshenv pull --yes --prefer skip # 跳过两边都改过的部分,其余照常收进清单;有跳过时退出码 6- 条目写进清单的
profiles.<profile>.patches,原样保留id、name、config、disabled、insert与!!js表达式(清单里记作{ __jsExpr: ... })。 - 含本机绝对路径(如技能目录)的条目写进当前 overlay;没有选中 overlay 时新建并选中
local。带--no-overlay时遇到这类条目会拒绝。订阅了团队 remote 时基础清单只读,全部条目写进本机 overlay。 - 自上次
apply以来 DSH 与清单都改过时拒绝执行,需用--prefer指定以哪一边为准。--prefer skip按补丁目标(一个 Profile 或全局层)和单个 skill 跳过,不是按单个条目:跳过的部分合成一条警告,基线不动,下次pull仍会报告。有跳过时退出码为 6(即使只是预览,也即使还有别的改动待收:跳过的部分要人来处理,所以 6 优先于 2,与plan有 blocked 操作时 5 优先于 2 相同;要知道有没有待收的改动看--json的changes),明文写进受管块而被挡下的目标也算跳过,--json的skipped: { patchTargets, skills }列出它们,适合 CI 定时同步。 - 条目里有新的明文密钥时不收。明文指凭据类的键名(以
apiKey、secret、secretKey、privateKey、accessKey、password、token、Authorization、credentials结尾,或以PAT结尾,如OPENAI_API_KEY、clientSecret、AWS_SECRET_ACCESS_KEY、GH_PAT;以Env结尾的如apiKeyEnv除外,maxTokens、max_token这类计数也不算)下的非空字符串,password类键下的数字,任何键下带密码或 token 的 URL(如postgres://u:pw@host/db),以及 MCP 客户端行(@deepseek-ai/dsh-mcp-client)headers里的全部值;它的env按键名判断。清单里已有的同一个值不算新:在 DSH 里改了它旁边的别的字段,照常收回。这样的条目留在cordis.patch.yml里,放到 dshenv 受管块之后(它可能覆盖块里条目插入的行),同一 Profile 的其他条目照常收进清单;警告给出目标 / 条目 / 键路径,不打印值。清单与 overlay 都会跨机器同步,所以明文也不会转存进 overlay。请改用apiKeyEnv: DEEPSEEK_API_KEY这类只写环境变量名的写法。明文写进了 dshenv 受管块(在 DSH 界面改了受管的条目)时,这个目标的补丁一条都不收,plan把它的写入标为 blocked,因为apply会覆盖掉这次改动;在 DSH 里改成*Env写法后再pull。 ${...}按原文收进清单:DSH 不做插值。- 全局补丁
$DSH_HOME/cordis.patch.yml:DSH 先应用 Profile 自己的补丁,再应用这个文件,所以它对每个 Profile 都生效,而且同 id 时覆盖 Profile 层。DSH 界面从不写它,条目都是手写的。dshenv 把它当作独立的一层管理:- 清单(以及 overlay)的顶层
patches:对应这个文件,写法与profiles.<profile>.patches相同;apply写进文件里 dshenv 的受管块,受管块之外的手写内容与注释保留;不再声明任何条目时文件写成[](空文件或只有注释的文件会让 DSH 启动失败)。 - 不带
--profile的pull收回其中手写的条目(输出记为[global]),含本机路径的进 overlay,规则与 Profile 层相同;手改了受管块时plan提示,pull --yes保留、apply --yes覆盖。 --profile只作用于单个 Profile,plan、apply、pull带它时都不碰全局文件。- Profile 层(含插件补丁)有与全局文件同 id 的条目,且全局那一条写了 Profile 条目也写的字段时,
plan(也包括plan -p、status、apply预览)在Profile patches the global cordis.patch.yml overrides下列出:DSH 按字段整体替换,这些字段实际用的是全局那一条。两层写的字段不同(如全局只写disabled、Profile 只写config)时互不覆盖,不算。被覆盖的配置要改清单的顶层patches,不要在 DSH 界面里改:config-editor 拒绝保存这些字段。 - 全局条目写了
disabled时,这个 id 无论 Profile 条目写什么都会列出,并单独注明:DSH 插件页通过 Profile 层的disabled启停插件,全局的值总是优先,在插件页启停不生效。--json的shadowedPatches项带disabled列出这些 id。 - 文件无法解析时,
apply以退出码 5 拒绝写入(原样保留,需手动修好),pull跳过它并给出警告,其余照常。 - 团队 remote 的清单与 overlay 同样不允许在顶层
patches里带!!js表达式。
- 清单(以及 overlay)的顶层
plan在Unmanaged plugins下列出的插件(装在 Profile 里、清单没有声明)也一并接管,描述方式与capture相同(别名、来源、版本;只靠insert加载、不在 bundles 里的记为enabled: false),并像adopt一样写入 lock 与所有权记录,之后plan不会要求重装;local-file插件除外:装进 Profile 的是当时的副本,无法证明与源目录一致,下一次apply会重装一次以记下源码 digest。local-link/local-file插件按含本机路径条目的规则写进 overlay(local-link同时记下源码 digest),其余写进基础清单;团队 remote 拥有基础清单时写进 overlay。adopt之后的那次pull只接管候选清单中的本地来源插件。- 写入前先建快照,
dshenv rollback <快照 id> --yes可撤销(id 见--json输出的snapshotId)。 $DSH_HOME/skills下的 loose skill 也一并处理:目录复制到envctl/skills/<名字>,DSH 里删掉的技能从清单里删除。apply反向复制,被覆盖或删除的 DSH 副本移进envctl/trash(gc清理);plan在Planned skill changes与Skills not in the manifest下列出技能。envctl/skills可以放进团队配置仓库,随remote/sync同步;团队拥有的技能在 DSH 里改动后pull会拒绝。Git 标记为可执行的文件同步后保持可执行;只改可执行位的提交也会同步,skill 的摘要同样计入可执行位,所以这类改动会一直到 DSH 里的副本(Windows 没有可执行位,不比较)。--json输出{dryRun, changes: [{profile, from, added, changed, removed, base, overlay, overlayName?}], skills?: {added, changed, removed}, plugins?: [{profile, alias, package, sourceType, enabled, layer, overlayName?}], warnings?, overlayCreated?, skipped?, operationId?, snapshotId?};warnings列出无法接管的插件(如 npm 版本不是确定版本)。
23. dshenv tools
按 DSH 架构图的分类(终端、文件、网络、代码、编排、交互、会话/技能/自省)列出、开关和配置 Profile 里的内置工具。工具状态读自 dsh --profile <p> --dump-config,所以需要 Profile 已存在、DSH 可以运行(--harness-source、DSH_CLI 或 PATH)。
dshenv tools list -p web # 默认预设的工具:+ 开启,- 关闭,~ 按条件关闭
dshenv tools list -p web --preset ptc # 看另一个 agent 预设;--all 列出每个位置
dshenv tools disable tool-web -p web # 关闭;enable 打开(id 见 tools list)
dshenv tools config get tool-web -p web # 查看配置;加 <路径> 看单个键
dshenv tools config set tool-web fetchMaxOutputChars 20000 -p web # 设置一个键
dshenv tools config unset tool-web fetchMaxOutputChars -p web # 删掉一个键
dshenv tools reset tool-web -p web # 删掉改动它的 patch,恢复 DSH 的默认
dshenv apply --yes # 写进 DSHtools config get|set|unset与plugins config写法相同;旧写法tools config <tool> [路径] [值]仍可使用。tools 写 base 时,若这条在清单 base 里还没有,而 DSH 当前组合出的配置已含 dshenv 补丁(如已 apply 的 overlay 写的),或含本机路径,以退出码 3 拒绝,改用--layer overlay。- 结果写进清单的
profiles.<profile>.patches,与pull管理的条目相同;overlay 激活时用--layer base|overlay选择写入层。 - 顶层工具行(如 headless Profile)只写一条小 patch:
{id, name, disabled}或config。DSH 的 patch 会整体替换一行的config,所以config设置时会把当前整份配置连同改动一起写入。 - web 等 Profile 的工具在 agent 预设(
preset-standard、preset-ptc等)里,按 id 的 patch 够不到预设内部,只能整份替换预设的config。dshenv 会把当前预设整份复制进清单再改目标工具(!!js条件原样保留),这个预设从此固定:DSH 升级对它的改动不再生效,plan会在Pinned agent presets下列出。删掉清单里那条预设 patch 并apply,即恢复跟随 DSH。 - 目标与
tools list显示的一致:先找--preset指定的预设(不指定时用 DSH 当前的默认预设),其中没有该工具时改 profile 级那一行;只有别的预设里有时报错,提示加--preset。 - overlay 已声明同一个 id 的条目时,写 base 会被它覆盖而不生效,因此会拒绝,请改用
--layer overlay。 - 只接受
tools list --all列出的 id;不在 Profile 组合里的工具(如默认未装的tool-lsp、tool-terminal)不能用enable打开,需先安装对应的包。 tools reset <tool>删掉清单里改动该工具的那条 patch:顶层工具只删它自己的,预设里的工具会删掉整个预设 patch(该预设内的所有工具改动一起撤销,预设恢复跟随 DSH)。overlay 里写时,base 声明的 patch 记为remove: true;已经记过的再 reset 一次什么也不改。
24. dshenv web
在后台启动、停止和查看 dsh web,不必占着一个终端:
dshenv web start -p web # 后台启动,打印浏览器地址;--port 3080 指定端口,默认随机空闲端口
dshenv web list # 列出 dshenv 启动的 dsh web:运行状态、pid、地址(不含 token);-p 只看一个 Profile
dshenv verify -p web # 没设 DSHENV_DSH_URL 时自动连这个 dsh web
dshenv web stop -p web # 停止它以及它启动的子进程(如 stdio MCP 服务),等全部退出后返回- 启动命令为
dsh --profile <P> --no-open --port <端口>,DSH CLI 的选择与其他命令相同(DSH_CLI、--harness-source、environment.harness.sourceDir、PATH)。DSH 的cordis.patch.yml(如$DSH_HOME/cordis.patch.yml)设置了webserver.port时,DSH 忽略--port,web start --port与verify --start的随机空闲端口都不起作用。dsh web 在独立的进程组中运行,dshenv 退出或关闭终端后继续运行。 - 地址(含登录 token)与 pid 记在
envctl/run/<profile>.json,输出写到envctl/run/<profile>.log,两者权限均为0600;不在快照、同步与团队仓库范围内。start打印地址,已在运行时再次start也会打印现有的地址;web list(旧名web status)从不打印 token。 - 已在运行时
start只报告现有的那个(--port与它的端口不同时以退出码 3 拒绝,先stop);它自己退出后,web list显示not running,再次start会启动新的。dsh web 自己退出但它启动的子进程还在时,web list显示not running (leftover processes),start先停掉这些子进程再启动,stop也会停掉它们。 - 记录里保存了 dsh web 的启动时间,
stop只停止 pid 与启动时间都对得上的进程,被系统复用的 pid 不会被误停;无法确认时(web list显示unknown)stop和start报错并保留记录,不做任何停止。SIGKILL 后仍未退出时stop以非零退出码报错并保留记录,可以再次执行。 - 同一 Profile 的
start、stop依次执行,两个start同时运行也只会启动一个;启动过程中按 Ctrl+C 会停止正在启动的 dsh web(verify --start在核对过程中被中断也一样),不会遗留进程。 - Profile 必须已存在(DSH 会自动创建不存在的 Profile);bundles 选了其他应用(
dsh-headless、dsh-acp-app、dsh-sdk-app)的 Profile 以退出码 3 报Profile <p> runs <应用包>, not dsh web,60 秒内没有打印地址也会停止并报错。 - Windows 上 dsh web 以 detached 方式启动,不附着在启动它的控制台上,关闭启动它的控制台窗口后继续运行;停止用
taskkill /T /F结束整棵进程树。
25. dshenv install / enable / disable / remove
只改清单,随后用 dshenv apply --yes 真正安装、启停或卸载:
dshenv install @nanmicoder/[email protected] -p web # npm 包,必须是精确版本
dshenv source clone https://github.com/ex/dsh-plugin-demo.git -p web # Git 来源:克隆并在 lock 中固定 commit(见第 14 节)
dshenv install ./my-plugin -p web # 本地目录(./、../、绝对路径或 file:),登记为 local-link
dshenv install in-box:@deepseek-ai/dsh-acp-app -p acp --new-profile # 随 DSH 发布的 bundle,不装依赖,只在 Profile 中选中;acp 还不存在,所以加 --new-profile
dshenv plugins official # 列出当前 DSH 自带的官方 bundle
dshenv install in-box:@deepseek-ai/dsh-experimental-agent-team-profile -p web # 启用其中一个
dshenv disable agent-teams -p web # enable 反之
dshenv remove agent-teams -p web@nanmicoder/[email protected] 只声明支持到 DSH 0.1.7-rc.2;DSH 0.2.0 版本族请用 0.1.22,0.2.1 上目前没有它的兼容版本,DSH 会拒绝安装(见 DSH 新版本兼容验证)。
install in-box:<包> 能从 DSH 的安装位置找到这个包时检查它是否 bundle(package.json 有 dsh.bundle),不是时以退出码 3 拒绝(DSH 会在每次启动时跳过它);找不到时不检查。
DSH 的官方 bundle 随 DSH 一起安装,默认关闭,在 Profile 的 dsh.profile.bundles 里选中才加载(DSH Web 插件页"官方"一组里的开关就是这个):
dshenv plugins official从 DSH 的安装位置读出这些 bundle(DSH 自带、不属于 Profile 模板的 bundle),并标出清单在哪些 Profile 里声明了它们;-p只看一个 Profile,--json输出dshVersion与bundles。列表随 DSH 版本变化:0.2.1-alpha.1 有 agent-team-profile、voice-input-bundle、auto-review、inspector-profile 四个,0.2.0-rc.2 用 schedule-bundle 代替 inspector-profile。找不到 DSH 的安装位置时以退出码 4 报错。- 用
install in-box:<包> -p <profile>声明,默认别名去掉dsh-experimental-前缀和-profile、-bundle后缀(如agent-team、voice-input、auto-review、inspector;capture也这样命名),apply只把它加进dsh.profile.bundles,不写dependencies、不跑 npm;版本总是跟随已安装的 DSH,所以不受第三方插件的 peer 范围限制。disable/remove把它从列表里去掉。 - Profile 还不存在、而名字是 DSH 的模板 Profile(
acp、headless、sdk、sdk-minimal、web)时,apply先运行一次dsh --profile <名字> --dump-config,让 DSH 按自己的模板创建它(预览在Profiles DSH creates from its own template first下列出,--json为createdProfiles),再写 bundle 与补丁;apply 失败回滚时删掉这个新建的 Profile。install声明时它还不存在,所以照常要加--new-profile(见选择 Profile)。其他名字 DSH 不会自己创建,仍为blocked,要先装一个 npm 插件进去。插件别名不能是@profile,也不能以@mount:开头(这是 dshenv 自己的补丁块名)。
install <git 地址>[#<commit|分支|tag>] 只在清单里声明 Git 来源(# 后是 commit 时记为 commit,否则记为 ref);Git 插件要在 lock.json 有固定的 commit 才能 apply,所以之后仍需 source clone --profile 或 source sync --profile 锁定,否则 plan 显示 blocked。
- 本地来源(
local-link、local-file)由apply记录源目录摘要,目录内容变了plan才会提示更新。package.json有files时只算 npm 会发布的文件(package.json、README、LICENSE、main与files列出的内容,支持通配与!排除),改文档、测试等不算更新;没有files时算整个目录(跳过node_modules、.git)。skill 总是算整个目录,不看files。目录里的软链接按它指向的路径计入,不读取指向的内容;skill 在envctl/skills与DSH_HOME/skills之间复制时软链接原样保留,整个 skill 目录本身是软链接时复制其内容。 - 本地来源的目录必须存在(也可写成
file://地址),且不能带#<ref>;要按 Git 仓库安装本机仓库,写git+file://<路径>#<commit>。 - 别名默认取自来源并去掉
dsh-plugin-、dsh-前缀:npm 与 in-box 来源取包名(去掉作用域),本地来源取目录名,Git 来源取仓库名;--as指定。本地来源的包名默认读其package.json的name(读不到时用目录名),Git 来源默认用仓库名,与实际包名不同时用--package指定(source clone --profile会读仓库的package.json);--package只对 Git 与本地来源有效。 - 同一别名重新
install同一个包只改来源,保留patches与启用状态;输出会说明从哪个版本(来源)改成了哪个。别名已经指向另一个包时拒绝(退出码 3),用--as换一个别名,或先remove原来的。 - npm 来源(
install与update --to)先用npm view核对:包在而版本不存在时以退出码 3 报错并给出最新版本。npm 看不到这个包(可能是需要凭据的私有包)、拒绝凭据,或查询不了(离线、超过约 5 秒)时只警告,照常写入清单。加--no-npm-check或设DSHENV_NPM_CHECK=off跳过核对;--json输出的npmCheck是verified、unverified、unreachable或skipped。包名不合法(例如以-开头)时直接拒绝,不会交给 npm。 enable、disable、remove、update、plugins config也接受包名;别名写错时报错会给出相近的别名。- 输出统一为「改了清单 + 下一步」,如
Added … to profile 'web' in the manifest. Next: dshenv plan, then dshenv apply --yes.。 remove从清单删除该条目;写 overlay 时,base 中已有的插件记为remove: true。apply只卸载有所有权记录的插件(dshenv 安装或adopt接管的),其他实际存在的插件标为unmanaged,不会卸载。remove只改清单,无需确认;旧脚本里的-y/--yes仍被接受。uninstall是remove的别名。- 有生效 overlay 时这四个命令都须带
--layer base|overlay,或设环境变量DSHENV_LAYER=base|overlay作为默认(见第 15 节)。
退出码规范
| 退出码 | 含义 |
| :--- | :--- |
| 0 | 成功 / 环境与清单完全同步(Clean) |
| 1 | 意外失败(如 Git、npm 或网络错误) |
| 2 | 存在有效变更计划(Drifted,plan/status/apply --dry-run);不带 --yes 的 apply、pull、rollback、gc、purge、adopt、migrate、remote add、remote remove、remote sync、source sync 预览有待执行的内容;verify 有插件仍在加载;self-update --check 有可安装的版本 |
| 3 | 用法错误(缺参数、未知选项或命令)或输入、清单格式校验失败(ValidationError);--json 时以 {"error": {...}} 输出 |
| 4 | DSH 运行时能力不支持或未找到(CapabilityError) |
| 5 | 环境降级或运行时响应异常(DegradedError);plan/status/apply 的计划中有 blocked 操作;verify 有与清单不符的插件 |
| 6 | pull --prefer skip 跳过了两边都改过的补丁目标或 skill;预览时也优先于 2,待收的改动看 --json 的 changes |
安全边界与约束
- 路径约束:清单中的本地链接和本地文件路径必须为绝对路径;仍应只使用可信源码目录和规范的 npm 包名。Profile 名(清单、overlay、lock 与
-p)只能含字母、数字、.、_、-,不能以-开头,也不能是.或..;Git 地址与 ref 不能以-开头,清单中的commit必须是 7-64 位十六进制 commit id。 - 凭据使用约束:不要把明文密钥写入清单、锁文件、patch 配置或源码
package.json。清单与 lock 中带账号密码或 token 的 git URL 会被 schema 拒绝,capture会跳过这类依赖并告警。补丁里的明文密钥(规则见pull):pull不收;config set、tools config set等写清单或 overlay 的命令拒绝写入新的明文(退出码 3);plan、status、doctor发现清单或 overlay 已有明文时在 stderr 警告;团队仓库的清单或 overlay 含明文时remote add/sync拒绝。doctor不回显DSH_CLI参数,但source show --json会输出源码包摘要,使用前应检查其中是否含敏感字段。 - 非受管保护:实际 Profile 中未写入
manifest.yaml的插件保持unmanaged,不会被自动删除(DSH 创建 Profile 时自带的 base/app bundle 不算unmanaged)。 - 锁与管理文件快照:所有写
manifest/overlay/lock/state的命令都先获取环境锁(最多等 5 秒)。apply执行前备份当时已经存在的管理文件;失败时只原子恢复它自己会写的lock.json与state.json(快照中不存在的会被删除),manifest.yaml、overlay 与envctl/skills保持原样,以免覆盖 apply 期间的手工修改;已成功安装的插件在恢复后仍记入所有权;恢复本身失败时错误信息会提示运行dshenv rollback <id> --yes。某一步失败时,错误信息写明失败的步骤(如[web] install cc (cc), step 2 of 2)、本次的 operation id、仍然留在 Profile 里的已装插件,以及回到上一次成功 apply 所用清单的命令dshenv rollback <上次的 id> --yes。apply 还会逆序撤销本工具对 Profiledsh.profile.bundles与cordis.patch.yml的改动;DSH CLI 已完成的包安装、更新或卸载不会撤销,已成功卸载的包也不会恢复其 bundle 与受管块。失败后应重新运行status与plan。
后续路线图
见 docs/roadmap.md。后续版本计划交付:
- 配置补丁之外的 live manager 写能力
- 细粒度 live manager service 双向通讯
