npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@kira905/dsh-butler-install

v0.3.1

Published

Thin installer for the DSH butler suite: one command installs the plugin + CLI bundle through the host's own commands, then tells you whether it actually took effect, and can roll the whole batch back. Also ships a local wizard page (--ui) that lives outs

Readme

dsh-butler-install

这是什么

上面那一整套的一键安装器:一条命令装 16 个包,装完逐项自检,装坏了整批回滚。

它解决什么

一套东西散成十几个包,最容易死在「装一半」:有的装上了、有的没有,报错还只说 ERR_PNPM_。这个安装器给三件东西 —— ① 清单驱动(16 项一次装齐,缺一项就算失败)② 整批回滚(不是只撤最后一个)③ 失败说人话(把 pnpm 的环境障碍翻成「安全软件拦了建链接」「store 不在同一块盘」)。

一条命令跑起来

npm i -g @kira905/dsh-butler-install
dsh-butler-install --profile web --dry-run    # 先看要装什么
dsh-butler-install --profile web              # 真装

想点着鼠标装也行:dsh-butler-install --ui 会起一个本地向导页(只绑 127.0.0.1、只读、不执行)。

先看它长什么样(真实输出)

dsh-butler-install —— 大管家全家桶薄壳安装器
用法:
  dsh-butler-install --profile <profile> [选项]
  --profile <name>        要操作的 profile(必填)。宿主会用它定位 <数据根>/profiles/<name>
  --manifest <path>       清单文件,默认用包内自带的 suite-manifest.json

另有一个自足的 Windows exe(双击出界面,自带 node 运行时,不用先装环境)。


大管家全家桶的薄壳安装器:一条命令按清单装齐插件与命令行工具,装完立刻告诉你成了没,红了能整批回滚。

卖点不是"一键",是"一键之后的可验证性"。 一个包"装完 rc=0 但其实没生效"是这套生态里真实存在过的坑(宿主只会吐一行 declares no dsh.bundle 警告,然后把它装成普通依赖)。本工具逐包核对包实体在不在、 版本落不落在期望区间、插件到底有没有进组合列表,任一条不过就判这一步失败。

30 秒上手

# ① 先看它会做什么(默认就是演练档:一条命令都不跑,零写入)
npx @kira905/dsh-butler-install --profile web

# ② 真装
npx @kira905/dsh-butler-install --profile web --apply

# ③ 只装一部分(组名 / 包名 / 别名都认)
npx @kira905/dsh-butler-install --profile web --apply --only 面板,看板

装完/以后任何时候的自检,是 dsh-butler-core 仓里那条装后自检:

node <dsh-butler-core 仓根>/selfcheck-replica.mjs --manifest suite-manifest.json

⚠️ 这条自检命令的现状:它目前在 dsh-butler-core 仓里,还没有接清单 (只跑它内置的那几项判据)。本仓的 suite-manifest.json 的字段是为它将来接清单准备的 —— 字段含义见下文「清单字段」一节。别把它当成"已经能按清单跑了"。

想要个界面?--ui:挂在 DSH 外面的本地向导页(能在页面上点着装)

觉得命令行清单不好对?加 --ui 起一个本地网页:每一行写清装没装 / 什么版本 / 期望区间, 而且能在页面上点着装 —— 点了才装,不点不装。

# 起向导页(起来的时候什么都不装;点了【确认,开始】才装)
npx @kira905/dsh-butler-install --ui

# 换端口(默认 45080;被占会自动往后挑一个并打印实际地址)
npx @kira905/dsh-butler-install --ui --port 45090

打开它打印的地址(形如 http://127.0.0.1:45080/)就能看到清单表。页面上的动作:

| 按钮 | 干什么 | 不干什么 | |---|---|---| | 开始安装 | 算一遍"将对哪个 profile 做哪几件事",把 16 项、profile 名、回滚点位置逐条念给你看 | 那一步一条命令都不跑、一个文件都不写 | | 确认,开始 | 真装。逐项显示 待装 / 安装中 / 成功 / 跳过 / 失败 | 不替你重启 DSH(装完要你自己重启,见下) | | 取消 | 杀掉当前那条命令的进程树 | 不是干净撤回:已经装好的项会留着;页面会明说"已取消,装了一半" | | 卸载这次装的(推荐) | 把这一轮装上去的真的卸掉:插件从 profile 摘掉、全局命令行包从全局前缀删掉 ⇒ 重查会真的变回"未装" | 不覆盖任何文件,所以你随后在侧边栏另装的插件不受影响 | | 只恢复配置文件到装前 | 只把这些声明层还到开跑前那一份 | node_modules 实体与全局 CLI 包还在,所以重开仍显示"已装";期间有别的程序改过这些文件就拒动 | | 复制命令 | 把这一行放进剪贴板 —— 与 --dry-run 打印的是同一行(同一个函数产出) | 不替你执行 | | 重查 | 重跑一遍探测、把最新读数写回同一张表 | 不改任何状态、不安装、不写文件 |

🔴 装完第 ⑥ 步:要重启 DSH,插件才会出现在侧边栏。 宿主只在启动时读一次组合列表, 不重启侧边栏不会多出任何东西 —— 这是最容易让人以为"装了没用、被骗了"的地方。 顺手那行「复制命令」给的是单项原语命令;要整批自己跑,用页面上那条 带 --apply 的整批命令(不带 --apply 的那条只是演练,跑完什么都不装)。

为什么挂在 DSH 外面,而不是做成插件侧边栏页

这是设计前提,不是偷懒:

| | 插件侧边栏页(最初的想法) | 宿主外的本地页面(现在这样) | |---|---|---| | 要装插件 / 重启 DSH 才能打开这个向导页 | 要(dsh.bundle + cordis.patch.yml + 重启) | 不要 | | DSH 升级会不会崩 | 会(往页面里注入 client DOM,宿主一改就可能失效) | 不会(它根本不进宿主) | | 装 DSH 之前能不能用 | 不能 | 能 —— 还没装宿主时,向导正好可以告诉你"你得先装 DSH" | | 成本 | 约 8h | 约 4h | | 代价 | 写入口的风险面大 | 要监听一个本地端口(所以只许 127.0.0.1) |

安全边界(写死的,不是可选项)

  • 🔴 只绑 127.0.0.1:同一局域网的别的机器连不上。没有 0.0.0.0 开关,也不读环境变量改它。
  • 🔴 写入口只有四条,全是 POST:/install/preview(算一遍,不装)、/install/start(真装,必须带 confirm:true)、 /install/cancel、/install/rollback(撤回的两个动作)。除这四条之外的任何写方法一律 405,未知路径 404; Allow 响应头按路径给(写路径 POST、读路径 GET),不再是一刀切的 Allow: GET。
  • 🔴 每个写入口都要过两道闸:安装令牌(进程级,启动时生成一次、印在控制台、也内嵌在页面 meta 里; 只从页面读、绝不拼进 URL —— 拼进 URL 就落进历史记录与代理日志)+ 同源校验 (Origin 的 host:port 逐字等于 server.address() 的 address:port;端口是动态的,--port 0 也照样对)。 Sec-Fetch-Site 存在才校验:Chrome 76+ / Safari 16.4+ 才有这个头,双击入口打开的是系统默认浏览器 (可能是 360 兼容模式),硬要求它等于给所有按钮静默 403。
  • 🔴 这条要如实说:它防不住本机、同一个 Windows 用户下的其他程序 —— 那些程序读得到这个页面、 也调得到这些接口。它是一个"给本机同一个人用的安装器",不是多用户隔离的服务,别当成后者。
  • 双击两次 .cmd 不会起两个服务:新入口先探 127.0.0.1:45080,认(GET /state 里的 service 标记) 是自家服务就把浏览器指过去 ⇒ 两个标签看同一个服务,天然单实例。
  • 端口被占:自动往后挑一个(最多 20 个),都不行就让系统分配,并打印实际地址。

「按顺序装」是什么意思

页面会给你建议顺序 + 为什么这么建议,但请看清它的含义:

组件之间没有代码依赖,谁先装谁后装都能装上。 真正有先后的只有三格: Node(≥22)→ dsh 主包 → profile。下面的"顺序"是给人看的排查路径,不是成功的前提。 看到"必须按 1→2→3 才能成功"这种话,那是假的仪式。

不想碰命令?下载 exe 版(给完全不会命令行的人)

上面那套都要敲命令。连 PowerShell 都不想开的话,下载一个 exe 就行:

下载地址:https://pan.baidu.com/s/1CxK4g-DAwuGoFVKLp4GqCA?pwd=9jir(提取码 9jir;文件名 dsh-butler-install-0.3.0-win-x64.exe,约 89 MB · 93,148,160 B)

用法就三步:

  1. 下载 → 双击。
  2. 会弹出一个黑窗口,它先体检(dsh 在不在、npm 在不在),然后自动打开浏览器 —— 浏览器里那个页面就是上面说的安装向导,点两下就装完了。
  3. 装完直接关掉黑窗口(关窗口 = 关掉向导页;装到一半别关)。

🔴 第一次运行会被 Windows 拦一下(这里必须说清楚)

没有买代码签名证书(约 ¥1000+/年,暂时不买),所以 Windows SmartScreen 会弹:

Windows 已保护你的电脑

十个人里有九个以为中病毒就关掉了。正确做法:

  1. 点弹窗里的 「更多信息」;
  2. 再点 「仍要运行」。

(这拦的是"没有数字签名的程序",不是"这个程序有毒"。看不放心就别装,用上面的 npx 命令版。)

这个 exe 的前提

  • 这台电脑上已经装好了 DSH(dsh --version 能打出版本号)。exe 不负责装 DSH、也不装 Node; 缺了它会在黑窗口里用大字告诉你缺什么、怎么装,然后停住等你按任意键(不会一闪就没)。
  • 只有 Windows x64 版。自带 node 运行时所以 89 MB —— 好处是你机器上 node 不在 PATH 也能起界面。

exe 里到底跑了什么

它就是同一个安装向导:界面 HTML 内嵌在 exe 里、清单 suite-manifest.json 也内嵌在 exe 里, 所以把 exe 单独拷到空目录也照样能起界面(不读旁边任何文件)。命令行的行为也一模一样 —— 页面上 「复制命令」给你的,就是 CLI 真会跑的那一行。

自己造一个(仓里只留"怎么造",exe 不进仓)

npm install                 # esbuild + postject(devDependencies)
npm run build:exe           # → dist/dsh-butler-install-<版本>-win-x64.exe

实现是三步(scripts/build-exe.mjs,改路线前先看每步的注释):esbuild 打单文件 CJS → node --experimental-sea-config 生成 blob → 复制本机 node.exe + postject 注入。 dist/ 在 .gitignore 里:exe 走网盘分发,进 git 只会把仓撑爆。

这一版不改 npm 包的行为:dsh-butler-install、--ui、--apply、退出码、参数一个都没动。 会敲命令的人继续用上面那些;exe 只是同一套东西的"双击壳"。

它只做两件事,且只用两个原语

| 原语 | 用来装什么 | |---|---| | dsh plugin --profile <p> add <包>@<期望区间> / remove <包> | 插件类(要进 profile 的组合列表) | | npm i -g <包>@<期望区间> / npm rm -g <包> | 命令行类(不参与挂载) |

明确不做(这是设计红线,不是懒):

  • 不写 dsh.profile.bundles —— 那是宿主的活;本工具只调 dsh plugin add,让宿主自己登记。
  • 不写 cordis.patch.yml —— 需要手工 insert 的包,本工具只告诉你"它要手工挂",不代劳。
  • 不碰 node_modules —— 不自己下载、不解包、不建链接。代码里没有第二条装包路径。
  • 不替你重启 DSH,不改别的仓,不帮你决定要不要装。 ⚠️ 但插件装完你确实要自己重启 DSH,不然侧边栏不会出现它们 —— 这不是本工具偷懒,是宿主只在启动时读组合列表。

删掉本工具,一切照旧:它没有留下任何自己的运行时状态(只有回滚快照,见下)。

为什么安装命令要带 <包>@<期望区间>

因为注册表的 latest 标签和"最高版本号"不是一回事。实测踩过:某个包的 latest 指着一个旧版本,而更新的版本确实已发布、却没被 latest 指着 —— 不带区间装上来的是 旧的那一版,于是"期望 ^0.1.1、实体 0.1.0",装完看着成功、实际装错了版本。

带上区间之后,解析器只能落在期望区间里:落不进去就报错停下(而不是装个别的版本还报成功)。

退出码

| 码 | 含义 | |---|---| | 0 | 全绿(含"本来就已满足、这次全跳过") | | 2 | 用法 / 清单 / 前置条件错误 —— 什么都没改 | | 3 | 有步骤失败,但已回滚干净 | | 4 | 有步骤失败,且回滚不干净(要人看:报告里会指出哪份文件对不上、快照在哪) |

失败时那段英文是什么意思(人话归因)

失败时原始报错仍然原样打印(一个字都不改),人话归因追加在它后面:

  ❌ 安装命令退出码 1:…(原始 stderr 首行,原样)
    💡 这不是包坏了:你的安全软件(360 / EDR)拦了 pnpm 往 store 里建符号链接。
       …(为什么)
       怎么办:① 在安全软件里给 pnpm / node 放行(或暂时退出)  ② 换个不会被拦的位置做数据根(--home 指到别处)  ③ 直接重试一次(这类拦截是间歇的)

认得出的环境障碍(实现见 lib/explain-failure.mjs:纯函数,不跑命令、不读盘、不发网络):

| 归因 | 原始报错里长什么样 | 一句话 | |---|---|---| | blocked-symlink | EPERM … symlink(同一行)或栈里有 registerProject | 安全软件(360 / EDR)拦了 pnpm 建符号链接 —— 不是包坏了;放行 / 换位置 / 重试 | | store-mismatch | ERR_PNPM_UNEXPECTED_STORE | 这个 profile 的 node_modules 是用另一处 pnpm store 装的,两边对不上;用回原来那处 / 在那个 profile 重装 / 换干净数据根 | | unknown-pnpm | 任何 ERR_PNPM_<码> | 确定是 pnpm 自己报的错,但这个码还没收录 ⇒ 把原码带走,去搜 / 贴给维护者 |

认不出来就一个字都不加(不猜)。归因只做展示:不参与判据,也不改退出码(0/2/3/4 口径不变)。 --ui 页面与 exe 走同一条路:页面进度里另有一条摘要行(💡 …|怎么办:…),失败项的详情里也带这一句。

退出码本身也写人话:Windows 上子进程被信号/异常结束时退出码是无符号的, 会看到 退出码 4294963248(= -4048,0xFFFFF030) —— 原始数值仍在最前面,括号里是等价写法。

回滚:两级,别只做一级

失败时默认只退失败那一步(前面已装好的几步保留,不用全重来):

| 参数 | 干什么 | |---|---| | --rollback / --rollback=step(默认) | 回到失败那一步之前:先卸掉这一步装进去的包,再回放那一步之前拍的快照 | | --rollback=batch | 回到本轮开跑之前:逐包卸掉本轮装过的,再回放开跑前的快照 | | --uninstall | 不是回滚:把选中的包从 profile / 全局前缀卸掉("我不想装这套了") |

一句话分清:回滚看的是"这一轮做了什么",卸载看的是"现在装着什么"。

快照拍的是声明层文件:package.json、pnpm-lock.yaml、cordis.patch.yml, 外加包管理器自己会写的 pnpm-workspace.yaml(实测会出现,不拍就漏一份)。开跑前一份, 之后每成功一步再拍一份 —— 这样"退到上一步之前"才有落点。

🔴 诚实边界:node_modules 里的实体文件不在快照内。remove 能摘掉依赖链接, 但清不干净包管理器的缓存与虚拟store,所以回滚后可能还留着实体文件 —— 报告会明写这一句, 不会假装"回滚 = 什么都没变"。

清单(suite-manifest.json)= 单一真相

安装器读它;将来 dsh-butler-core 的装后自检也读同一份。任何一方想加判据,只能改这份清单 —— 两处真相必然漂移。

字段:

| 字段 | 必填 | 说明 | |---|---|---| | name | ✅ | 包名(@scope/name 或裸名) | | versionRange | ✅ | 期望区间,写法只接受 ^x.y.z / ~x.y.z / x.y.z。不支持 >=1.2、1.x、|| 组合 —— 与其两处语义各猜一半,不如让写法收敛 | | type | ✅ | plugin(要进组合列表)/ cli(不参与挂载) | | group | ✅ | 组名,--only 按组选 | | mount | 插件可选 | auto(默认,装完应自动挂)/ manual(按设计不进组合列表,挂载由它自带的部署命令完成)/ none | | probe | ✅ | 见下「探针」 | | aliases | 可选 | --only 用的别名(如 面板、看板),不许与包名重复 | | note | 可选 | 一句人话:这个包是什么、有什么坑 | | excluded[] | 顶层可选 | 不可安装项:不是包的目录(如历史聚合仓)。演练档会跳过它并打印原因 |

期望版本一律写区间,不钉精确版本:宿主落进 profile 的本来就是区间, 钉精确版本只会造出"清单与实体永远不一致"的假红。

探针(probe.kind)

| 值 | 干什么 | 什么时候能用 | |---|---|---| | existence(默认) | 只看文件在不在:插件看 node_modules/<包>/package.json,命令行看 .bin 或全局前缀下的命令文件。一条命令都不执行 | 任何命令都能用 | | help | 先静态核该命令源码里确有 --help 分支,再跑 <命令> --help | 仅当这条命令确实实现了 --help。核不到就拒绝执行并判失败 | | cmd | 跑清单指定的一个只读命令 | 清单里必须写 readonlyBecause:凭什么认定它只读 |

🔴 为什么 help 要加硬闸:这批命令行工具里有相当一部分没有参数解析,未知参数被忽略后 直接执行真动作(导出快照、归档、改状态)。拿 --help 当"只看一眼"是错的。 所以 help 探针在执行之前先读源码,找不到 --help 分支就拒跑 —— 宁可判红,不赌它是只读的。

要取证"哪些命令真的实现了 --help",用随包附带的只读扫描器(一个命令都不跑):

npx -p @kira905/dsh-butler-install dsh-butler-install-probe-scan <某仓根或父目录>

只有它标 ✅ 的命令,才允许在清单里写 probe.kind: "help"。

常用参数

| 参数 | 说明 | |---|---| | --profile <name> | 必填。宿主会定位 <数据根>/profiles/<name> | | --apply | 真装(不加就是演练档) | | --only / --skip | 逗号分隔,认组名 / 包名 / 包名末段 / 别名 | | --upgrade | 已满足的项也重跑一遍安装("装过但坏了"的情形) | | --rollback[=step\|batch] | 失败后怎么回滚,默认 step | | --uninstall | 卸掉选中的包(不是回滚) | | --npm-prefix <dir> | 全局包落点(默认取 npm 自己的全局前缀)。隔离演练时用它:装到临时目录,不碰你真正的全局包 | | --home <dir> | DSH 数据根(默认 $DSH_HOME,再默认 ~/.dsh) | | --timeout / --budget | 单步超时(默认 120s)/ 整批墙钟上限(默认 300s,超了就停下并回滚本批) | | --snapshot-dir <dir> | 快照目录,默认 <数据根>/butler-install/snapshots/<时间戳> | | --json | 只输出一份 JSON 汇总(人读的输出改走 stderr) | | --ui | 起一个本地向导页,能在页面上点着装(点了才装、不点不装):只绑 127.0.0.1、写入口四条都得令牌 + 同源。装完要重启 DSH 才在侧边栏出现。见上文。用它时 --profile 可省(默认 web) | | --port <n> | 向导页端口(只在 --ui 时有用),默认 45080;被占用会自动换一个并打印实际地址 |

它在 Windows 上怎么跑子进程

dsh / npm 在这台机器上是 .cmd 壳:不带 shell 的 spawn 直接 ENOENT, 显式写 .cmd 又会 EINVAL。所以本工具在 Windows 上过 shell 执行,并且参数做了最小必要引号化 (只有含空格等特殊字符的参数才加引号)——打印出来的命令行与真正执行的命令行由同一个函数生成, 报告里不会出现"说要跑 A、实际跑了 B"。

已知限制(别当 bug)

  • 回滚不覆盖 node_modules 里的实体文件与包管理器缓存(见上文诚实边界)。
  • 用 --upgrade 重装一个之前就装着的包、然后失败回滚时,声明层会回到快照值, 但实体层可能变成"声明在、实体更新过"的中间态 —— 再跑一次安装即可对齐。
  • mount: "manual" 的包(按设计不进组合列表、靠自带部署命令挂载)本工具只装不挂, 挂载判据记"跳过"。
  • 不判"运行时会不会崩":--dump-config 只证明组合树里有这一行。真要验运行时,得自己起一次服务看日志。
  • 不替你决定"要不要装某一件",也不做"装完自动起服务"。
  • 🔴 向导页能装,但它防不住本机同用户的其它程序(见上文安全边界)。要更严的隔离,就别在本机跑第三方程序。
  • 向导页的取消不是干净撤回:已装好的项留着,当前那一项可能只装了一半 —— 页面会直说,并给撤回入口。
  • 向导页的**「只恢复配置文件到装前」只还声明层**:node_modules 实体与全局 CLI 包还在;期间有别的程序改过那几份文件就拒动。
  • 向导页不落自己的状态:进度只在内存里。关掉窗口 = 这一批断了;重开页面按"现在装着什么"现跑给真实读数 (这比读一份可能过时的状态文件更准)。

夹具

npm test

夹具全部落在自己的临时目录里(假 dsh、假 npm、假 profile、假全局前缀), 不读也不写真实数据根。覆盖:清单校验(每条错误都带文件名 + 行号)· 版本区间三态 · 探针三分支(含"标了 help 但该命令没实现 ⇒ 拒执行且不产生任何执行")· 组合树三条计数判据 (各自独立可触发)· 快照回放与核对 · 演练档零副作用(目录指纹必须一模一样)· 退出码 0/3/4 · 步级与整批回滚的差别 · 卸载 · 向导页(监听地址只许 127.0.0.1 / 写入口只有那四条、其余 405 / 令牌与同源两道闸 / 两步确认 / 进度轮询 / 撤回两条路与前置核对 / 页面数据与 --dry-run 同源 / 每请求零 spawn / 端口冲突自动换 / 不给 --ui 时输出逐字节不变)· 以及变异测试(4 条):把监听地址改成 0.0.0.0、去掉令牌校验、把 POST 白名单放开到任意路径、 去掉同源校验 —— 每一条都必须让夹具变红在指定判据上(改坏了还绿 ⇒ 这条夹具是假的)。

清单漂移判据(test/range-drift.mjs)

清单是单一真相,但真相会烂:写清单那天按当天的 latest 填区间,之后某个包跨了 0.x 的小版本 (0 阶段里这就是大版本),caret 的上界就把新版挡在门外 —— 照清单装一遍拿到旧一套,而且装后自检 照样绿(它读同一份清单)。所以这条判据对着清单外面的世界核,而不是自己核自己:

node test/range-drift.mjs                                    # npm view,真网络
node test/range-drift.mjs --offline test/fixtures/npm-latest-snapshot.json   # 读快照,零网络
node test/range-drift.mjs --json --manifest <别的清单>

退出码:0 全部接受 · 3 有漂移(照清单装会拿到旧版)· 4 取数失败/无法判定(绝不当通过)· 2 用法错或清单读不进来。test/run-all.mjs 里那一格走仓内快照、零网络;快照的 capturedAt 是这份数据"什么时候是真的"的凭据,刷新清单区间时记得一起刷。

许可

MIT