dsh-dev-env
Verifieddsh-dev-env · v1.2.0 · GPL-3.0-or-later · Web UI
DSH plugin: inventory your Windows dev/test toolchain, spot the gaps, and fill them with one click.
Install
dsh plugin add dsh-dev-env Confirm the layer applied with dsh --profile default --dump-config — see the install guide.
Source
Tags
Creators
Readme
dsh-dev-env
一个 DSH 插件:把本机已装好的开发与测试工具链盘点出来,显示在 DSH 的 Web 界面里,标出缺口,并能一键补齐。
只支持 Windows。 插件要读 Windows 的卸载注册表、要驱动 winget,在 macOS 和 Linux 上跑不起来。
截图应当放在 docs/screenshot.png,需要维护者自己补上。
你能得到什么
侧栏面板
DSH Web 界面里的一个全局面板:sidebar.panellist 中 id 为 dev-env 的条目,对应 main 里同名的 key。
面板把目录分成 5 类,逐条显示探测到的版本和它所在的位置——探测不到时显示原因。缺口会被高亮,每条缺口带三个动作:
- 一键补齐 —— 执行选中的补齐方案,结果实时流进下面的任务列表;
- 下载页 —— 用默认浏览器打开厂商官方页面;
- 复制命令 —— 把完整的命令行复制到剪贴板。
每一行下面有一句话说明这个工具到底是什么,写给从没听说过它的人看;右侧的 ↗ 按钮则会先问你想怎么办:
让 DSH 讲解,或者打开这个工具的官网。走 DSH 这条路时,面板把问题发到 /dsh-dev-env/api/explain,
由它投进你最近活跃的那个会话——所以答案直接出现在对话里,Agent 还能顺带说明这个工具在这台机器上的状态。
百科链接已经去掉:百度百科覆盖不了开发工具链的一大半,把新手丢到一个空词条页比不给链接更糟。
这些说明放在 describe.js 里、按条目 id 索引;npm test 强制每个目录条目都必须有中英两条。
顶部的胶囊标签汇总已安装 / 缺失 / 核心必备缺失 / 总数以及扫描时间;下方的任务列表显示实时状态和安装输出的尾部。
样式只用主题 token(--dsw-alias-*),浅色和深色主题都能正常跟随。
安装过程会报进度。 每个任务都带阶段(下载中 / 校验中 / 安装中 / 完成)、安装器最后打印的那一行,
以及它打印出体积时的体积数字;进度条在能从输出里解析出百分比时是确定的,否则是不确定进度——
面板不会凭空编一个它并不知道的数字。已用时长每秒走字,正在安装的那条目录行会就地显示阶段,
并且整个安装期间按钮都是禁用的(不只是点击的那一瞬间)。有任务在跑时,任务列表的轮询从 2 秒压到 1 秒。
任务行保持一行:详情里展开下载与安装进度、当前活动行、退出码和日志尾部,删除(或清空已结束)
把任务移出列表——一次失败的安装不该永远占着版面。
面板标题栏有一个语言切换按钮,默认显示中文。按钮上写的始终是"切过去之后的语言",并用该语言书写
(中文界面显示 English,英文界面显示 中文),选择会按浏览器记住。界面文字、分类名和条目名都会跟着切换。
它刻意不跟随 DSH 的语言设置:两种词条都打包在插件里。
Agent 工具
| 工具 | 用途 | 参数 |
|---|---|---|
dev_env_inventory |
返回完整清单、缺口列表,以及补上每条缺口要执行的命令。 | refresh(布尔,绕过 60 秒缓存)、only(all | missing | installed)、category(language | ide | test | data | tool)、verbose(布尔,在文本结果里逐条列出全部) |
dev_env_install |
补齐一条缺口。它会改动本机环境,所以只在用户明确要求时调用。 | id(必填)、mode(auto | winget | npm | pip | page | command) |
dev_env_job |
查询补齐任务的状态、退出码与最近日志。 | jobId(省略则列出最近任务)、tailLines(默认 30) |
dev_env_install 的 mode: "command" 是空跑:只返回候选命令,不执行任何东西。
HTTP API
面板和工具一样,都是 host 半边的消费者:
| 方法 | 路径 | 输入 | 返回 |
|---|---|---|---|
GET |
/dsh-dev-env/api/inventory |
?refresh=1 绕过缓存 |
完整清单:items、categories、summary |
GET |
/dsh-dev-env/api/jobs |
– | 补齐任务列表,最新的在前 |
POST |
/dsh-dev-env/api/install |
{ "id": "gcc", "mode": "auto" } |
已启动的任务,以及该条目可用的方案 |
POST |
/dsh-dev-env/api/open |
{ "url": "https://example.com" } |
用默认浏览器打开该地址 |
POST |
/dsh-dev-env/api/jobs/delete |
{ "id": "44ee61f9" } 或 { "scope": "finished" } |
剩余任务;该任务仍在运行则返回 400 |
POST |
/dsh-dev-env/api/explain |
{ "id": "gcc", "lang": "zh" } |
通过 sessionController.prompt 把关于该工具的提问投进最近活跃的会话 |
条目 id 未知时返回 404;mode 找不到对应方案(或 url 不是 http(s))时返回 400;方法不对返回 405。
mode: "command" 只属于工具;HTTP 端点接受 auto 或某个具体方案的模式。explain 在当前 DSH 没有
sessionController 时返回 503,没有会话能接提问时返回 409——这样调用方就能区分「这个版本做不到」
和「能做到,但没地方放答案」。
运行要求
- Windows 10 或 11。已在 Windows 11 + DSH 0.2.0-rc.2 上验证。
- Node.js 18 或更新(见
package.json的engines)。 - 没有任何运行时依赖。插件有意不声明 peer dependency,这样 DSH 安装前的版本兼容性门禁永远不会拦住它。
winget:走 winget 的补齐需要它。面板把winget本身就当成一条目录项,而「下载页」兜底方案不依赖它。- Python 和/或 Node.js:只在个别条目需要
pip/npmGlobal探测或pip/npm补齐方案时才需要。
安装
两个渠道都已上线,任选一个 spec。
npm —— 零前置依赖。 不需要 git、不需要 Node,什么都不用先装:
dsh-dev-env
GitHub —— 跟随默认分支,但需要本机有 git:DSH 会先用 git ls-remote 校验仓库,pnpm 也走 git 拉取。
github:cszi666/dsh-dev-env
1.1.2 之前 npm 包名带 scope(
@cszi666/dsh-dev-env)。旧版本仍可用那个名字安装, 但新安装请用不带 scope 的这个。
在 DSH 桌面版里(不需要 CLI)
桌面版把 CLI 打包在 app.asar 内部、不会暴露到 PATH,而且 desktop profile 归 Electron 所有——
用 npm 装的 dsh CLI 会被拒绝管理它。所以在 DSH 内部安装:
直接吩咐 Agent。 在任意会话里粘贴:
安装 DSH 插件 dsh-dev-envAgent 会用
plugin_manager发起action: install_bundle、target填这个 spec,并应用到当前 profile。 它会请求授权,同意即可(plugin_manager需要danger-full-access或单次批准)。如果你的 Agent 预设没有 暴露这个工具,就用下面的面板。或者用面板。 设置 → 插件,粘贴同一个 spec。
装完如果侧栏没出现,刷新页面;如果面板出现了但它的 API 返回 404,重启一次 DSH(见踩坑)。
升级已安装的版本
直接覆盖安装不是升级。 DSH 会保留旧的 bundle 行,一旦新一代加载失败就会报:
dsh: warning: 1 entry did not activate dev-env (@cszi666/dsh-dev-env): failed to import
这读起来像整个应用坏了,而不是插件坏了。1.1.0 就是这么发的,所以下面的顺序不能反。
在会话里粘贴:
我已经装过旧版的 @cszi666/dsh-dev-env,现在要升级到 [email protected]。
请按顺序执行,不要跳步:
1. 先用 plugin_manager 的 list_bundles 告诉我当前装的**包名**和版本。
2. 用 remove_bundle 先卸载旧的那个。不要直接覆盖安装:DSH 会保留旧 bundle 行,
新一代加载失败时报 "1 entry did not activate dev-env: failed to import",
那看起来像整个应用坏了。
3. 用 install_bundle 安装 [email protected]——版本写死,便于核对。
4. 再用 list_bundles 确认装上的就是 [email protected],并报告 application 状态。
5. 如果有 restart-required 就告诉我,不要自己重启客户端。
1.1.2 改了包名,所以这不只是升版本,也是换名字:npm 包原本是 @cszi666/dsh-dev-env,现在是不带
scope 的 dsh-dev-env。旧名字对在它名下发布过的版本(到 1.1.1 为止)仍然有效,这也是为什么第 1 步
要问包名而不只是版本、第 4 步要同时核对两者。如果你的旧名是用本地 link: 装的,不要用第 2 步——
见 包名 里的 link 情形。
第 4 步不是走过场:"有这一行"不等于"插件激活了"。同样的流程手动做也可以—— 在设置 → 插件里卸载,再添加写死版本的 spec。
用 dsh CLI(npm 管理的 profile)
npm install -g @deepseek-ai/dsh # 还没有 CLI 才需要
dsh plugin --profile <profile> add dsh-dev-env
dsh plugin 会把参数原样转发给 profile 目录里的 pnpm,所以任何 pnpm spec 都成立:
@scope/name、github:owner/repo、https://github.com/owner/repo、.tgz 链接、版本范围都行。<profile>
填你在用的那个 profile——不能填 desktop,它归 Electron 应用所有。
npm 包现在是不带 scope 的 dsh-dev-env。1.0.0 到 1.1.1 期间它曾叫 @cszi666/dsh-dev-env,原因是
dsh-dev-env 这个名字当时还在 npm 的取消发布冷却期里;1.1.2 改回来了。旧名字对在它名下发布过的版本
仍然有效,所以已装的副本照常能用,但新安装请用上面那个不带 scope 的 spec。变的只有 npm spec——
GitHub spec 和仓库名从来没变过。
对 GitHub 仓库,DSH 会先用 git ls-remote 确认可达性,所以账号或仓库名打错会立刻失败,而不是装到一半才报错。
工作原理
目录(catalog)
catalog.js 是唯一的真源:82 个条目、5 个分类,每个条目同时声明「怎么探测」和「怎么补齐」。
| 分类 id | 条目数 | 覆盖范围 |
|---|---|---|
language |
23 | 语言与运行时:JDK、Python、Node.js、.NET、Go、Rust、GCC/Clang…… |
ide |
20 | IDE 与编辑器:JetBrains 全家桶、VS Code、Visual Studio、Eclipse、DevEco Studio…… |
test |
16 | 测试工具:JMeter、禅道、Postman、pytest、Selenium、Playwright、Appium…… |
data |
10 | 数据库、中间件与 CI:MySQL、PostgreSQL、MongoDB、Tomcat、Jenkins、Docker…… |
tool |
13 | 构建与命令行工具:Git、CMake、Ninja、Maven、Gradle、winget、Scoop…… |
标了 essential: true 的 11 个条目会单独计入「核心必备缺失」,这样缺一个编译器不会被一堆可选缺失淹没。
探测方式
probe.kind |
做什么 | 字段 |
|---|---|---|
argv |
执行命令,用 pattern 从 stdout + stderr 里抓版本号。 |
argv,可选 pattern |
versionFile |
读一个确定的文件,用 pattern 抓版本号。 |
path,可选 pattern、prefix、suffix |
pathList |
从一份有序的、做过环境变量展开和通配展开的候选列表里取第一个存在的。 | paths;可选 file、pattern、requires、prefix、suffix、version |
dir |
环境变量展开后的路径上,文件或目录存在即算命中。 | path |
pip |
执行 python -m pip show <module>,读 Version:。 |
module |
npmGlobal |
全局 npm 根目录(npm root -g)下的一个包目录。 |
name |
registry |
Windows 卸载注册表条目,按 DisplayName 做大小写不敏感匹配,版本取 DisplayVersion。 |
match |
npm test 会强制校验这个集合。当前目录用到了 argv、pathList、registry、pip 和 npmGlobal;
versionFile 和 dir 受支持,但目前没有条目在用。
对 pathList 来说:写了 file 时 pattern 作用在文件内容上,否则作用在命中路径的 basename 上;第一个捕获组就是版本号。
下面两个选项让匹配从"看名字"变成"验内容",这也是同一个文件夹里放着多个 JDK 时还能认准的原因:
// 有人把 JDK 8 解压进自己的目录——注册表里根本没有它,只能靠它自己的 release 文件认领。
// pattern 钉死主版本,requires 证明它是 JDK 而不是 JRE(JRE 的 release 也写着 1.8,但没有 bin\javac.exe)。
probe: { kind: 'pathList', file: 'release', pattern: 'JAVA_VERSION="(1\\.8[^"]*)"',
requires: ['bin\\javac.exe'], paths: ['%JAVA_HOME%', '%DRIVES%\\Java\\*', ...] }
挑最窄的那种:工具有可用的版本命令就用 argv;会自己注册的 GUI 安装包用 registry;压缩包和解压出来的 SDK 用
pathList;由包管理器装的用 pip / npmGlobal。
可移植路径
没有任何目录条目硬编码盘符或用户路径——写了 npm test 就会失败。路径只能走下面几种:
%任意环境变量%,大小写不敏感,%ProgramFiles(x86)%之类都行。变量没设的候选会被丢掉而不是瞎猜, 所以%CATALINA_HOME%没设时会如实报「未安装」,而不是跑去一个毫无意义的目录里找。%DRIVES%—— 伪变量,展开成系统盘加上任何实际存在的D:–H:盘。上限是刻意的:逐个字母探测在断开的网络映射上会卡好几秒。%DSH_DEV_ENV_ROOTS%—— 用户自定义的兜底入口,专门解决「工具被解压到自己建的某个文件夹里」这种情况。 它展开成DSH_DEV_ENV_ROOTS环境变量里列出的根目录(分号分隔,去掉结尾的分隔符,空项丢弃); 变量没设时贡献零个候选,也就是绝不瞎猜。这是在不改代码的前提下修掉误报的官方办法:# 把你解压到某处的 JDK/IDE/工具目录变得可被发现 setx DSH_DEV_ENV_ROOTS "D:\Java\tools;E:\sdk"这个值在每次扫描时读取,所以不需要重新构建;但它必须出现在 DSH host 进程的环境里, 而
setx只对之后启动的进程生效,所以执行完要重启一次 DSH。~表示用户主目录。单段的
*和?通配,一层目录一层目录地展开。展开刻意不做递归,所以任何一个目录条目都不可能把一次扫描变成全盘爬取。
补齐方案
条目的 install 声明会按优先级翻译成可执行方案:
install: {
winget: 'Git.Git', // winget install --id Git.Git -e --accept-* --disable-interactivity
npm: 'pnpm', // npm install -g pnpm
pip: 'pytest', // python -m pip install pytest
page: 'https://git-scm.com/download/win', // 打开官方下载页
note: '会显示在面板里对应行的下方。',
}
winget 优先,其次 npm,再次 pip;打开官方下载页是永远可用的兜底。mode: "auto" 取第一个非 page 的方案。
每个目录条目都必须至少声明一种方式,npm test 同样会校验这一点。
扫描行为
- 探测 8 路并发;单条命令 12 秒超时;整份清单缓存 60 秒。工具的
refresh: true或 HTTP 的?refresh=1会强制重扫。 - 卸载注册表每次扫描只读一遍(三个分支一起),之后所有
registry条目共用。 - 全局 npm 根目录每次扫描只问一次;
npm root -g答不出来时回落到常见的%APPDATA%/%ProgramFiles%位置。 - Windows 上很多工具是
.cmd/.bat垫片(npm、pnpm、gradle、tsc……)。Node 18.20+ 起不再直接执行它们, 所以插件先把真实路径解析出来再交给cmd.exe,并且整行原样传给cmd,保证内层引号不被转义坏。 - 进程启动时快照的
PATH是死的。补齐任务成功收尾后,插件会从注册表重读 Machine 和 User 的Path(扫描期间最多每 20 秒刷新一次),所以刚装好的工具不需要重启 DSH 就能被认出来。 - 缺失条目会带一个
debug对象——用了什么探测、实际执行了什么命令、退出码、输出片段——让误报可以被解释,而不是靠猜。
实测表现
在参考机器上(Windows 11、DSH 0.2.0-rc.2),用的是当时发布的 68 条目目录:
- 探测到 41 条已安装,全部带精确版本号。
- 探测到 27 条缺失,其中 2 条属于核心必备。
- 从面板发起的
winget install --id Git.Git跑到结束,退出码 0,日志完整流进任务列表。
后来目录扩到了 82 个条目,所以在新的机器上扫描得到的总数会更大。具体数字取决于装了什么。
踩坑
下面都是 DSH 本身的行为,这个插件一路踩过来了。贡献者大概率也会撞上。
host 侧代码只在启动时 import 一次,之后被缓存。 index.js、catalog.js、paths.js 是在 DSH 启动时加载的,
改动它们必须完整重启 DSH 才会生效——禁用再启用插件会重新执行 apply() 但不会重新 import 模块,改文件名、
卸载再重装也都不会。浏览器半边(client.js)不一样:它的 bundle 按文件 mtime 和 size 生成 revision,刷新页面就够了。
绝对不要在 apply 时用一次性的 ctx.get('webServer') 去取 webServer。 插件可能先于该服务激活,
于是这次调用拿到 undefined 而且永不重试:面板能渲染,但所有 /dsh-dev-env/api/* 都返回 404。
它在「插件安装进一个正在运行的 DSH」时完全正常,到下一次冷启动就坏,因此极难排查。正确做法是等它:
ctx.inject(['webServer'], (scope) => {
const dispose = scope.webServer.register({ kind: 'exact', path: '/x', handler })
return () => dispose()
})
winget 在受限 shell 里行为不同。 在 DSH 自己的 host 进程里它是可用的——参考机器上那次 Git 安装就是通过它完成的。
在被沙箱限制或非交互的 shell 里,这个 MSIX 别名可能直接以内部错误码立即失败。面板始终会显示退出码和日志,
而且「下载页」兜底方案不受影响。
安装器的输出不是按行组织的,把它当按行处理就会毁掉进度。 进度条是用裸回车 \r 反复重写同一行画出来的。
只按 \n 切分——也就是最直觉的写法——会把每一次重绘粘成一条极长的行,安装器报的百分比其实采到了,但已经没法读。
正确做法是按 \r 也切分、剥掉 ANSI 控制序列,并让连续的进度行互相覆盖而不是堆积。另外,
stdio 是管道的子进程不是终端,所以多数安装器会干脆隐藏动态进度条、只打印阶段行——这就是为什么
面板在解析不出百分比时显示不确定进度,也是为什么"它还活着"这件事由阶段和已用时长来承担。
已知限制
- 只支持 Windows。 没有 macOS 或 Linux 的探测实现。
- 一键补齐会改动本机环境,并可能弹出 UAC 提权确认。插件通过 UI 征求确认;Agent 只应在用户明确要求时调用
dev_env_install。 - GitHub 安装 spec 只用
git ls-remote确认了可达性;bundle 的内容是在取下来之后才校验的,不是之前。 - 目录覆盖的是常见工具链(82 个条目)。装在非常规位置的工具可能需要在
catalog.js里补一条候选路径,或者新增一个条目。 先试不改代码的办法:把那个目录写进DSH_DEV_ENV_ROOTS,它就会成为用到%DSH_DEV_ENV_ROOTS%的那些条目的候选 (JMeter、Tomcat 9/11、禅道、Jenkins)。 - 版本号来自工具自己打印的内容。如果某个程序只在卸载注册表里登记了 build 号(比如 JetBrains 系 IDE), 面板显示的就是那个 build 号。
开发
npm test # node scripts/check.mjs —— 校验目录、路径解析器和打包配置
npm run check # 同上
npm test 不需要任何依赖,也不会 spawn 任何进程。它校验目录结构、所有正则能否编译、有没有路径硬编码了盘符或用户目录,
对路径解析器做单元测试,把 package.json 和 bundle patch、client 入口、files[] 做交叉核对,
并且在 package.json 还留着 cszi666 发布占位符时拒绝通过。
全部通过时会打印 all checks passed (82 catalog entries)。
CI 会对每个 JavaScript 文件跑 node --check,然后跑 npm test,运行在 windows-latest 上,覆盖 Node 18、20、22。
新增目录条目的写法见 CONTRIBUTING.md。
许可证
GPL-3.0-or-later,详见 LICENSE。