DSH / Atlas
2026-09-15implementedarchitecture

Guided plugin installation

引导式插件安装

The install dialog put a spec straight into `pnpm add` and showed pnpm's terminal as the whole story: a typo, an installed package, a missing path, and a registry outage all ended in the same red exit code, the person read pnpm's output to learn which, and nothing could be stopped once started. A run that failed, or that added a package without a bundle patch, left the dependency in the profile with nothing in the li

Source compatibility: Chinese title uses a compatible non-canonical header

English

Problem

The install dialog put a spec straight into pnpm add and showed pnpm's terminal as the whole story: a typo, an installed package, a missing path, and a registry outage all ended in the same red exit code, the person read pnpm's output to learn which, and nothing could be stopped once started. A run that failed, or that added a package without a bundle patch, left the dependency in the profile with nothing in the list to show or remove it. Enabling was a checkbox to tick before knowing what would be installed, and a finished install left the new package somewhere in the list.

Decision

The Host reads a spec before installing it. PluginManager.inspect sorts the spec — registry name, absolute path, git address, tarball — with parseInstallSpec, refuses what pnpm or the registry would refuse, and asks the registry through pnpm view or the directory through its package.json for the name, version, description, and bundle declaration. pnpm view runs in the profile directory so the registry and proxy settings match the install. A package without a bundle patch is refused here, before pnpm runs: the manager installs bundles only. The answer carries one of seven problems; the client renders each as a sentence under the field and keeps the spec editable. The dialog refuses a name the list already shows without asking the Host.

A failed or cancelled installation restores the profile files. installBundle snapshots package.json and pnpm-lock.yaml before pnpm runs and puts them back when pnpm fails, when the run is cancelled, or when the package pnpm added declares no bundle patch. This reverses, for installations only, the manager's decision to retain partial changes: an installation that did not produce a usable bundle must not leave a dependency the page cannot show. Removals keep that decision. Downloaded files can stay under node_modules and the pnpm store.

A failed run is classified where the facts are. classifyInstallFailure reads how the run ended and what pnpm printed — its ERR_PNPM_* codes and Node's errno names — into packageResult.kind. A run the manager terminated is classified timeout whatever exit status the signal left behind, so installation restores the files and removal reports failure instead of reading it as a success (run bounds). The client shows the kind as one line and folds pnpm's output behind the details. Parsing the log is confined to this one function with a fixture-driven test.

GitHub connection failures have a short budget before installation. The Host runs a read-only git ls-remote check with the profile's Git and proxy configuration and a default githubConnectionTimeoutMs of 5000 ms. Credential helpers and prompts are disabled for this check, so authentication cannot consume the connection budget. Only network failures and timeouts prevent pnpm from starting; pnpm still owns authentication, HTTPS-to-SSH fallback, ref resolution and the full installation. The budget does not limit downloads or builds. The existing failure kind and failedAt: 'spec-host' let the Client show a connection error or timeout and offer a domestic registry for a replacement package spec. Cancellation and manager disposal terminate the check and its descendants before settling.

Cancellation is the manager's, not the signal's. The dialog generates a request id for each run; the Host streams the run's output and phases under it, and cancelInstall answers only after pnpm exited and the files are back. The dialog waits for that answer before offering the spec again; an aborted RPC or a lost connection is not a confirmation. Only the check takes a trailing AbortSignal: going back or closing drops a registry lookup, whose settlement nothing waits for.

Enabling comes after the fact. The run installs with enabled: false; the finished screen offers Enable now for the bundle it added, and the dialog closes and the list scrolls to it. Nothing is enabled before the person has seen what was installed.

Outcomes of the moment are toasts. A change that waits for the next start, one a higher layer overrides, a cancelled run, and a refused action each toast and retire; nothing stays on the page.

Blocked install scripts are approved from the failed screen. When pnpm 11 leaves a dependency's scripts undecided, the failed run reports the pending names (the manager's approval), and the failed screen shows them with Allow these scripts and retry in place of plain retry; the store runs the same checked subject again with approvedBuilds, and the installed screen names what was allowed. pnpm-workspace.yaml is not among the restored files for this reason. Without pending names the failure falls back to the manual instruction.

Alternatives considered

Validate specs on the client. Rejected: the rules are pnpm's, the registry's, and the profile's, and the client cannot import the Host package that owns them.

Look the package up over HTTP instead of pnpm view. Rejected: the registry, proxy, and auth settings that decide whether the install can succeed live in pnpm's configuration, which pnpm view reads and a direct fetch would have to reimplement.

Stop the run by aborting the add RPC. Rejected: a dropped RPC does not say whether pnpm stopped or the manifest is back, so the dialog would offer the spec again over a run still writing to the profile. The manager's cancelInstall answers only after cleanup, and the dialog waits for it.

Keep listing dependencies that are not bundles. Rejected: the manager manages bundles, and a package it refuses to install cannot be listed or removed through it; the check refuses such a package before anything is written.

Consequences

inspect, cancelInstall, and the plugin-manager/changed, plugin-manager/install-log, and plugin-manager/install-state events join the manager's Remote; listBundles carries titles, rows, and overrides; ChangeResult gains cancelled, bundle, version, and packageResult.kind; the config gains pnpmCommand, inspectTimeoutMs and githubConnectionTimeoutMs. The dialog is four screens over one subject card.

Testing

packages/boot/plugin-manager/tests/install-spec.spec.ts pins the spec forms and the failure classifier's inputs; manager.spec.ts drives inspect against a stubbed registry lookup and a real directory, streams a run, stops one and checks the restored files, and checks the change events; operations.spec.ts covers the registry lookup. packages/client/ui-plugin-manager/tests cover the store's phases, Host-confirmed cancellation, post-install enabling, toasts, and the page's four screens; apps/web/tests/plugin-manager.e2e.ts refuses an installed name, a missing path, and a bad name through the real Host and switches a bundle and one of its rows live, and plugin-install-cancel.e2e.ts stops a real child from the dialog, checks the restored files, and installs on the second try, and plugin-install-approve.e2e.ts leaves a script undecided through the fake pnpm, allows it from the dialog, and installs on the retry.

github-connection.spec.ts uses real Git against owned loopback transports to cover immediate failure, timeout, cancellation and noninteractive authentication. manager.spec.ts also installs a local repository through real Git and pnpm for HTTPS, GitHub shorthand and SSH specs, isolating pnpm's HTTPS probe through an owned proxy and verifying its SSH fallback. plugin-install-github.e2e.ts checks the connection error and timeout dialogs, child cleanup, and switching to a mirror without retrying the old GitHub address.

中文

问题

安装对话框把 spec 直接交给 pnpm add,并把 pnpm 的终端当作全部说明:打错字、已装过的包、不存在的路径、注册表不可用,最后都是同一个红色退出码,人得去读 pnpm 输出才知道是哪一种,而且一旦开始就停不下来。失败的运行,或者装进来一个没有组合包 patch 的包,会把依赖留在 profile 里,列表上却没有任何东西能显示或移除它。启用是一个在知道要装什么之前就得勾的选项,装完的包散在列表某处。

决定

宿主先读 spec,再安装。 PluginManager.inspect 用 parseInstallSpec 把 spec 分成注册表名、绝对路径、git 地址、压缩包,拒绝 pnpm 或注册表不会接受的写法,再通过 pnpm view 问注册表、或读目录的 package.json,得到名字、版本、描述和组合包声明。pnpm view 在 profile 目录里运行,让注册表和代理设置与安装一致。没有组合包 patch 的包在这一步、在 pnpm 运行之前就被拒绝:管理器只安装组合包。答复带七种 problem 之一;客户端把每一种渲染成输入框下的一句话,spec 保留可改。列表里已有的名字由对话框直接拒绝,不问宿主。

失败或被取消的安装恢复 profile 文件。 installBundle 在 pnpm 运行前快照 package.json 与 pnpm-lock.yaml,在 pnpm 失败、运行被取消、或 pnpm 装入的包没有声明组合包 patch 时把它们放回去。这只针对安装反转了管理器的决定中"保留部分改动"的部分:没有产出可用组合包的安装,不能留下一个页面无法显示的依赖。删除仍沿用那个决定。已下载文件可能留在 node_modules 与 pnpm 缓存中。

失败在事实所在处分类。 classifyInstallFailure 依据运行的结束方式和 pnpm 的输出——它的 ERR_PNPM_* 码和 Node 的 errno 名——给出 packageResult.kind。被管理器终止的运行,不论信号留下什么退出状态都归类为 timeout,因此安装会还原文件,删除则报告失败而不会读成成功(运行上界)。客户端把 kind 显示成一句话,把 pnpm 输出折叠进详情。对日志的解析只存在于这一个函数,用夹具驱动的测试钉住。

GitHub 连接失败在安装前有单独的短时限。 宿主通过只读的 git ls-remote 检查连接,沿用 profile 的 Git 与代理配置,githubConnectionTimeoutMs 默认为 5000 毫秒。这次检查禁用凭据助手和认证提示,避免认证过程占用连接预算。只有网络失败与超时会阻止 pnpm 启动;认证、HTTPS 到 SSH 的回退、ref 解析及完整安装仍由 pnpm 负责。时限不约束下载或构建。已有的失败类型与 failedAt: 'spec-host' 让客户端显示连接失败或超时,并提供国内镜像供用户输入另一个包名。取消安装或销毁管理器会先终止检查及其子进程,再结束操作。

取消属于管理器,不属于信号。 对话框为每次运行生成一个 request id;宿主在它之下流式转发运行的输出与阶段,cancelInstall 只在 pnpm 退出且文件恢复后才答复。对话框等到这个答复才把 spec 重新交回;中止的 RPC 或断开的连接都不算确认。只有检查末尾接受 AbortSignal:返回编辑或关闭会丢掉一次注册表查询,没有人等它的结果。

启用在事后。 运行以 enabled: false 安装;完成画面为它新增的组合包提供立即启用,对话框关闭,列表滚动到它。在人看到装了什么之前,不会启用任何东西。

当下的结果是 toast。 要等下次启动的变更、被更高层覆盖的变更、被取消的运行、被拒绝的操作,各弹一条 toast 后自行消失;页面上不留任何东西。

被拦下的安装脚本在失败界面批准。 pnpm 11 把依赖脚本留作未决时,失败的运行报告待决定的包名(管理器的授权),失败界面列出它们并以允许这些脚本并重试取代普通重试;store 带着 approvedBuilds 用同一个已检查的 subject 再跑一次,安装完成界面说明允许了哪些脚本。正因如此,pnpm-workspace.yaml 不在恢复的文件之列。没有待决定的名字时,失败退回到手动放行的提示。

考虑过的替代方案

在客户端校验 spec。 否决:规则属于 pnpm、注册表和 profile,客户端无法导入拥有这些规则的宿主包。

用 HTTP 直接查注册表而不是 pnpm view。 否决:决定安装能否成功的注册表、代理与认证设置都在 pnpm 的配置里,pnpm view 读得到,直接 fetch 得重新实现一遍。

通过中止 add RPC 来停止运行。 否决:断掉的 RPC 说不清 pnpm 是否停了、manifest 是否已恢复,对话框会在一次仍在写 profile 的运行之上把 spec 重新交回。管理器的 cancelInstall 只在清理完成后答复,对话框等它。

继续列出不是组合包的依赖。 否决:管理器管理的是组合包,它拒绝安装的包无法经由它列出或移除;检查在写入任何东西之前就拒绝这样的包。

后果

inspect、cancelInstall 以及 plugin-manager/changed、plugin-manager/install-log、plugin-manager/install-state 事件加入管理器的 Remote;listBundles 携带标题、行与覆盖项;ChangeResult 增加 cancelled、bundle、version 与 packageResult.kind;配置增加 pnpmCommand、inspectTimeoutMs 与 githubConnectionTimeoutMs。对话框是围绕同一张主题卡的四个画面。

测试

packages/boot/plugin-manager/tests/install-spec.spec.ts 钉住 spec 形式与失败分类器的输入;manager.spec.ts 用桩住的注册表查询和真实目录驱动 inspect,流式转发一次运行,停下一次运行并检查恢复后的文件,并检查变更事件;operations.spec.ts 覆盖注册表查询。packages/client/ui-plugin-manager/tests 覆盖 store 的阶段、经宿主确认的取消、装后启用、toast 与页面的四个画面;apps/web/tests/plugin-manager.e2e.ts 经真实宿主拒绝已装名字、不存在的路径和坏名字,并实时切换一个组合包及其中一行,plugin-install-cancel.e2e.ts 从对话框停下一个真实子进程、检查恢复后的文件,并在第二次尝试时装成,plugin-install-approve.e2e.ts 用假 pnpm 把脚本留作未决、从对话框允许后在重试中装上。

github-connection.spec.ts 用真实 Git 和测试自有的本机网络端点覆盖立即失败、超时、取消与非交互认证。manager.spec.ts 还通过真实 Git 与 pnpm 安装本地仓库,覆盖 HTTPS、GitHub 简写和 SSH 地址,用自有代理隔离 pnpm 的 HTTPS 请求并验证其 SSH 回退。plugin-install-github.e2e.ts 检查连接失败及超时弹窗、子进程清理,以及切换镜像时不重试原 GitHub 地址。