From d2bed0238eccd801440131cab369920e97665a7b Mon Sep 17 00:00:00 2001 From: turtle1999 Date: Thu, 5 Nov 2026 00:32:19 +0800 Subject: [PATCH 1/4] feat(plugins): update Git-installed plugins from Discover Discover offers Update for a plugin installed from a pinned Git commit when the plugin index lists a newer commit of the same repository directory. The Plugins page update removes the installed copy, installs the target with an expected-package check that undoes a wrong install with its modules, and reinstalls the removed commit when the target fails. A network failure reaching the target's host stops the update before anything is removed. --- ...26-10-14-registry-bundle-updates.i18n.yaml | 36 +-- .../2026-10-14-registry-bundle-updates.md | 35 ++- .../2026-10-14-registry-bundle-updates.zh.md | 35 ++- docs/event-producer-consumer.md | 12 +- docs/event-producer-consumer.zh.md | 12 +- docs/subsystems/boot.i18n.yaml | 8 +- docs/subsystems/boot.md | 28 ++- docs/subsystems/boot.zh.md | 28 ++- .../plugin-update-method/guide.i18n.yaml | 16 ++ .../v0.2.2-rc.1/plugin-update-method/guide.md | 22 ++ .../plugin-update-method/guide.zh.md | 22 ++ packages/boot/plugin-manager/README.i18n.yaml | 8 +- packages/boot/plugin-manager/README.md | 9 +- packages/boot/plugin-manager/README.zh.md | 9 +- .../boot/plugin-manager/src/diagnostic.ts | 1 + .../plugin-manager/src/github-connection.ts | 31 ++- packages/boot/plugin-manager/src/index.ts | 180 +++++++++++---- .../boot/plugin-manager/src/install-spec.ts | 38 ++- .../boot/plugin-manager/src/operations.ts | 42 +++- packages/boot/plugin-manager/src/tools.ts | 3 +- packages/boot/plugin-manager/src/types.ts | 28 ++- .../tests/bundle-detail.spec.ts | 2 +- .../tests/github-connection.spec.ts | 26 ++- .../plugin-manager/tests/install-spec.spec.ts | 31 ++- .../boot/plugin-manager/tests/manager.spec.ts | 218 +++++++++++++++++- .../plugin-manager/tests/operations.spec.ts | 96 ++++++++ .../boot/plugin-manager/tests/tools.spec.ts | 2 +- packages/boot/plugin-market/README.i18n.yaml | 8 +- packages/boot/plugin-market/README.md | 4 +- packages/boot/plugin-market/README.zh.md | 4 +- packages/boot/plugin-market/src/index.ts | 26 ++- packages/boot/plugin-market/src/types.ts | 6 + .../boot/plugin-market/tests/market.spec.ts | 31 ++- .../client/ui-plugin-manager/README.i18n.yaml | 14 +- packages/client/ui-plugin-manager/README.md | 10 +- .../client/ui-plugin-manager/README.zh.md | 10 +- .../src/client/PluginManagerPage.tsx | 5 +- .../ui-plugin-manager/src/client/index.ts | 2 +- .../ui-plugin-manager/src/client/locales.ts | 24 +- .../src/client/manager-store.ts | 64 +++-- .../src/client/presentation.ts | 1 + .../src/client/slot-contract.ts | 23 +- .../ui-plugin-manager/src/client/versions.tsx | 37 ++- .../tests/components.client.spec.tsx | 46 +++- .../tests/manager-store.client.spec.ts | 53 ++++- .../client/ui-plugin-market/README.i18n.yaml | 20 +- packages/client/ui-plugin-market/README.md | 12 +- packages/client/ui-plugin-market/README.zh.md | 12 +- .../src/client/DiscoverSection.tsx | 96 +++++--- .../ui-plugin-market/src/client/locales.ts | 16 ++ .../tests/discover.client.spec.tsx | 56 ++++- .../tests/presentation.client.spec.ts | 2 +- .../creation-recommendations/tests/fixture.ts | 2 +- .../plugin-recommendations/tests/fixture.ts | 2 +- .../src/client/slot-catalog.ts | 24 +- .../extensions/tool-cordis/src/api-catalog.ts | 16 +- 56 files changed, 1261 insertions(+), 343 deletions(-) create mode 100644 docs/upgrade-guide/v0.2.2-rc.1/plugin-update-method/guide.i18n.yaml create mode 100644 docs/upgrade-guide/v0.2.2-rc.1/plugin-update-method/guide.md create mode 100644 docs/upgrade-guide/v0.2.2-rc.1/plugin-update-method/guide.zh.md diff --git a/.agents/notes/implemented/architecture/2026-10-14-registry-bundle-updates.i18n.yaml b/.agents/notes/implemented/architecture/2026-10-14-registry-bundle-updates.i18n.yaml index 22e7100c04..1d72bf34af 100644 --- a/.agents/notes/implemented/architecture/2026-10-14-registry-bundle-updates.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-10-14-registry-bundle-updates.i18n.yaml @@ -2,21 +2,21 @@ # section, a hash of its English and Chinese blocks outside code blocks and generated regions. # After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-14-registry-bundle-updates.md -/agent-note-registry-bundle-updates: - en: 9b55699b0d5b3c7b - zh: 729a564af144048d -/agent-note-registry-bundle-updates/problem: - en: 47524f94e08192b5 - zh: 335104b6fda38f03 -/agent-note-registry-bundle-updates/decision: - en: 9201e6cc6cad6b8d - zh: be2332897037c43a -/agent-note-registry-bundle-updates/alternatives-considered: - en: 299b8e211b9c21bf - zh: 72bd106e96b42605 -/agent-note-registry-bundle-updates/consequences: - en: 57c30a10a233c3e0 - zh: 87cd51e2df8696bd -/agent-note-registry-bundle-updates/verification: - en: ab3442415bff92a6 - zh: 89d477c7d14f6098 +/agent-note-registry-and-git-bundle-updates: + en: 970e28efc1c454c5 + zh: 397ea3ace1d5b172 +/agent-note-registry-and-git-bundle-updates/problem: + en: b1cf4a8c1135321c + zh: e60ab2a2d876fc72 +/agent-note-registry-and-git-bundle-updates/decision: + en: 29e891007e8efe08 + zh: ac038ad0c2e17618 +/agent-note-registry-and-git-bundle-updates/alternatives-considered: + en: ba19c772e06f1f7d + zh: 65d18f2d89ac3adc +/agent-note-registry-and-git-bundle-updates/consequences: + en: aa716f92ddbdd547 + zh: 01c29b7f21db33b4 +/agent-note-registry-and-git-bundle-updates/verification: + en: b0ac6f7faff0f5d5 + zh: 5bbc3d5c16d4f2f1 diff --git a/.agents/notes/implemented/architecture/2026-10-14-registry-bundle-updates.md b/.agents/notes/implemented/architecture/2026-10-14-registry-bundle-updates.md index e17647a7db..7257cbf149 100644 --- a/.agents/notes/implemented/architecture/2026-10-14-registry-bundle-updates.md +++ b/.agents/notes/implemented/architecture/2026-10-14-registry-bundle-updates.md @@ -1,4 +1,4 @@ -# Agent Note: Registry bundle updates +# Agent Note: Registry and Git bundle updates Status: implemented @@ -6,15 +6,17 @@ English | [中文](2026-10-14-registry-bundle-updates.zh.md) ## Problem -Third-party bundles installed from a registry had no update path. The plugin page offered **Update** only for Official bundles, whose target is the running DSH version, so a person learned of a newer third-party release outside DSH and replaced it by uninstalling and installing again. With HMR an install over an installed package is refused, and a manual removal followed by a failed installation leaves the plugin uninstalled. +Third-party bundles installed from a registry had no update path. The plugin page offered **Update** only for Official bundles, whose target is the running DSH version, so a person learned of a newer third-party release outside DSH and replaced it by uninstalling and installing again. With HMR an install over an installed package is refused, and a manual removal followed by a failed installation leaves the plugin uninstalled. A bundle installed from Git had no update path either: a Git repository publishes no version list, and installing a newer commit from a plugin index over the installed copy resolves the package to another directory, which HMR cannot replace live, so the change waited for a restart and nothing reinstalled the old commit when it failed. ## Decision **Versions are read through `pnpm view` in the profile directory.** `listVersions` runs `pnpm view versions time dist-tags` over the same registry plan as `inspect`, so the `.npmrc` chain, proxy, and authentication that the installation uses also decide what the list shows, and a registry that cannot be reached moves the lookup to the next one. pnpm prints a range lookup as its highest match only, so each listed version takes its own `pnpm view @ name version engines peerDependencies deprecated dsh` on the registry that answered. `versionListLimit` bounds how many newer versions are read and `versionLookupConcurrency` bounds the subprocesses in flight. Compatibility reuses `evaluatePluginCompatibility` with the profile's exemptions, so the list and the installation judge peers the same way. -**The Host owns the update and its recovery.** `updateBundle` views the target first and refuses a missing version, one that declares no bundle, or one whose `engines.dsh` or DSH peers reject the running version before anything changes. With HMR it then calls `removeBundle` and `installBundle(name@version, { saveExact, registry, enabled })`; when that installation fails, is cancelled, or installs but cannot start, it removes a target that stayed and installs the removed version again from the registry it came from, else over the configured plan. The reinstallation carries no request id, so a cancellation of the update cannot stop it. Without HMR it installs over the running version, which `installBundle` allows there and reports as `restart-required`. `plugin-manager/update-state` announces each step for the dialog's progress list. +**The Host owns the update and its recovery.** For a registry copy, `updateBundle` views the target first and refuses a missing version, one that declares no bundle, or one whose `engines.dsh` or DSH peers reject the running version before anything changes. With HMR it then calls `removeBundle` and `installBundle(name@version, { saveExact, registry, enabled, expected })`, where `expected` makes the installation undo itself, modules included, unless it installs `name` at `version`; when that installation fails, is cancelled, or installs but cannot start, it removes a target that stayed and installs the removed version again from the registry it came from, else over the configured plan. The reinstallation carries no request id, so a cancellation of the update cannot stop it. Without HMR it installs over the running version, which `installBundle` allows there and reports as `restart-required`. `plugin-manager/update-state` announces each step for the dialog's progress list. -**Eligibility is a listed fact.** `BundleInfo.updatable` marks a third-party profile dependency saved as a registry spec under its own name and not as an `npm:` alias, with a semver version and no plugin management; the page shows the Versions section only for those, and both methods refuse every other bundle. The `plugin_manager` tool, which has no update action, omits the field. +**Eligibility is a listed fact.** `BundleInfo.update` is `registry` for a third-party profile dependency saved as a registry spec under its own name and not as an `npm:` alias, and `git` for one saved as a Git spec pinned to a full commit, whose reinstallation brings back the removed version; either needs a semver version and no plugin management. The page shows the Versions section only for `registry` bundles, `listVersions` refuses every other bundle, and `updateBundle` refuses every bundle without `update`. The `plugin_manager` tool, which has no update action, omits the field. + +**A Git copy updates to a version a plugin index lists, from the repository directory it came from.** A Git repository has no version list, while a plugin index pins each Git version to a commit and a package directory. The `pluginMarket` Host service marks a version with `update` when it is newer than a `git` copy and pins a commit of the same repository and `path:` directory; Discover offers **Update** for it and hands the package, version, and spec to the Plugins page, whose update dialog calls `updateBundle(name, version, { spec })`. The Host requires such a spec for a `git` copy and refuses one for a `registry` copy; `pinnedGitOrigin`, which both services import from `@deepseek-ai/dsh-plugin-manager`, compares origins regardless of the form pnpm saved, because a profile saves the SSH form of an HTTPS repository it cannot read and the `github:` shortcut of a GitHub URL. pnpm reads a Git package only while installing it, so before the removal the Host checks only that the network reaches the target's repository host; an unreachable host would fail the target and the reinstallation alike. A check Git cannot run, or one refused for credentials, is left to pnpm, which installs a public hosted commit as a tarball without Git. The rest is checked during the installation: compatibility as for every installation, and `expected` undoes a target that installs another package or version, or no bundle, before its bundle loads, failing with `unexpected-package` or `not-bundle`. The removed copy is reinstalled from the spec the profile saved, and Discover never installs over an installed copy. **Updates only move forward.** The page lists versions newer than the installed one and `updateBundle` refuses any other, so an update never asks a plugin to read data a newer version wrote. @@ -28,13 +30,26 @@ Third-party bundles installed from a registry had no update path. The plugin pag **List versions while reading bundles.** `listBundles` is offline; adding registry lookups would make opening the plugin page a network operation with one subprocess per version. +**Read Git versions from repository tags in the Host.** A tag names no package directory in a monorepo and says nothing about the version an index vetted; the plugin index already pins commit, directory, and version. + +**Install the newer commit over the installed copy.** It needs no removal, but with HMR the package resolves to another directory, so the live replacement is refused until a restart, and a failed installation reinstalls nothing. + +**Check the package name before removing the installed copy.** pnpm reads a Git spec's manifest only by fetching it, and a second fetch before the installation would double the download; checking the installed package inside the run keeps one fetch, and the reinstallation recovers a wrong target. + +**Remove a target that installed another package after the installation.** Its bundle would already have loaded and run, and when the other package was an existing dependency, `pnpm add` would have replaced it, so removing it would uninstall a plugin the person kept. Undoing the run with its lockfile restores that dependency too. + +**Accept a target from another repository.** Index content is unsigned, and the update dialog carries no trust notice of its own, so any configured index could replace a plugin's code with code from a source the person never chose; a plugin that moved to another repository is uninstalled and installed again, with the install confirmation. + ## Consequences - Checking a package costs one `pnpm view` plus one per listed version, each bounded by `inspectTimeoutMs`. -- A target saved by an update, and a reinstallation after a failed one, are exact versions even when the profile had saved a range; with HMR the bundle joins the end of the selection. +- A registry target saved by an update, and a reinstallation after a failed one, are exact versions even when the profile had saved a range; with HMR the bundle joins the end of the selection. - Removal and installation take the profile lock separately, so another operation can run between them; an installation that rejects, for example on a lock timeout, still triggers the reinstallation. - A version whose dependencies need a new build-script approval fails to install and is reinstalled at the previous version; its scripts are approved through an ordinary installation. - A reinstallation that also fails leaves the package uninstalled, and the dialog says so. +- A Git target whose commit is missing, that declares no bundle, rejects the running DSH, or installs another package or version is found only after the installed copy was removed; the reinstallation recovers it at the cost of a stop and a second installation. +- A Git copy saved with credentials in its URL is reinstalled from the saved spec without them, so a private repository needs a Git credential helper. +- A Git copy saved with a branch, a tag, or a range, or one whose plugin moved to another repository, updates only by uninstalling and installing again. ## Verification @@ -43,5 +58,11 @@ Third-party bundles installed from a registry had no update path. The plugin pag | Listing, registry fallback, refusals, compatibility, prereleases, deprecation, and the version limit | `packages/boot/plugin-manager/tests/manager.spec.ts` | | Update through removal and installation, reinstallation after a failed, cancelled, or rejected target, a failed reinstallation, refusal before any change, removal failure, and the no-HMR path | `packages/boot/plugin-manager/tests/manager.spec.ts` | | `pnpm view` answer parsing | `packages/boot/plugin-manager/tests/versions.spec.ts` | -| Version checks, update state, and step progress in the store | `packages/client/ui-plugin-manager/tests/manager-store.client.spec.ts` | -| Versions section, toolbar check, and every update outcome | `packages/client/ui-plugin-manager/tests/components.client.spec.tsx` | +| Git eligibility, update through a commit of the same directory, reinstallation from the saved spec, a target that installs another package or version, the no-HMR path with a target without a bundle, an unreachable repository host, and spec refusals | `packages/boot/plugin-manager/tests/manager.spec.ts` | +| Git origin across the forms pnpm saves | `packages/boot/plugin-manager/tests/install-spec.spec.ts` | +| A run undone with its modules when it installs another package, another version, or no bundle where one is expected | `packages/boot/plugin-manager/tests/operations.spec.ts` | +| The connection check of a Git repository on any host | `packages/boot/plugin-manager/tests/github-connection.spec.ts` | +| Git versions of the installed copy's directory marked as updates in an index listing | `packages/boot/plugin-market/tests/market.spec.ts` | +| Version checks, update state, and step progress in the store, including a Git update another tab names | `packages/client/ui-plugin-manager/tests/manager-store.client.spec.ts` | +| Versions section, toolbar check, every update outcome, and a Git target's repository in the dialog | `packages/client/ui-plugin-manager/tests/components.client.spec.tsx` | +| Discover's **Update** on cards and version rows, and no installation over an installed copy | `packages/client/ui-plugin-market/tests/discover.client.spec.tsx` | diff --git a/.agents/notes/implemented/architecture/2026-10-14-registry-bundle-updates.zh.md b/.agents/notes/implemented/architecture/2026-10-14-registry-bundle-updates.zh.md index b9a680ed3b..b308d27e39 100644 --- a/.agents/notes/implemented/architecture/2026-10-14-registry-bundle-updates.zh.md +++ b/.agents/notes/implemented/architecture/2026-10-14-registry-bundle-updates.zh.md @@ -1,4 +1,4 @@ -# Agent Note: 注册表组合包更新 +# Agent Note: 注册表与 Git 组合包更新 Status: implemented @@ -6,15 +6,17 @@ Status: implemented ## 问题 -从注册表安装的第三方组合包没有更新途径。插件页只为官方组合包提供 **更新**,其目标是当前运行的 DSH 版本,因此用户只能在 DSH 之外得知第三方新版本,并通过卸载再安装来替换。有 HMR 时,安装到已安装的包之上会被拒绝;手动移除后安装失败,插件就处于未安装状态。 +从注册表安装的第三方组合包没有更新途径。插件页只为官方组合包提供 **更新**,其目标是当前运行的 DSH 版本,因此用户只能在 DSH 之外得知第三方新版本,并通过卸载再安装来替换。有 HMR 时,安装到已安装的包之上会被拒绝;手动移除后安装失败,插件就处于未安装状态。从 Git 安装的组合包同样没有更新途径:Git 仓库不发布版本列表;从插件索引把较新的提交安装到已安装副本之上时,包会解析到另一个目录,HMR 无法在运行中替换,因此改动要等到重启,安装失败时也没有任何机制重新安装旧提交。 ## 决策 **版本通过在 profile 目录中运行 `pnpm view` 读取。** `listVersions` 按与 `inspect` 相同的注册表计划运行 `pnpm view versions time dist-tags`,因此安装所用的 `.npmrc` 链、代理与认证同样决定列表显示什么,连不上的注册表会使查询转向下一个。pnpm 对范围查询只打印最高的匹配版本,因此每个列出的版本在作答的注册表上单独运行一次 `pnpm view @ name version engines peerDependencies deprecated dsh`。`versionListLimit` 限制读取的新版本个数,`versionLookupConcurrency` 限制同时运行的子进程个数。兼容性复用 `evaluatePluginCompatibility` 与 profile 的豁免,因此列表与安装以同样方式判断 peer。 -**更新及其恢复由 Host 负责。** `updateBundle` 先查询目标版本,在任何改动之前拒绝不存在的版本、未声明组合包的版本,以及 `engines.dsh` 或 DSH peer 拒绝当前运行版本的版本。有 HMR 时,它随后调用 `removeBundle` 与 `installBundle(name@version, { saveExact, registry, enabled })`;该安装失败、被取消,或已安装但无法启动时,先移除仍留下的目标版本,再从被移除版本原来的注册表重新安装它,没有记录时按配置的计划安装。重新安装不带请求 id,因此取消更新无法中止它。没有 HMR 时,它直接安装在运行中的版本之上,`installBundle` 在这种情况下允许这样做并报告 `restart-required`。`plugin-manager/update-state` 通告每一步,供对话框的进度列表使用。 +**更新及其恢复由 Host 负责。** 对注册表副本,`updateBundle` 先查询目标版本,在任何改动之前拒绝不存在的版本、未声明组合包的版本,以及 `engines.dsh` 或 DSH peer 拒绝当前运行版本的版本。有 HMR 时,它随后调用 `removeBundle` 与 `installBundle(name@version, { saveExact, registry, enabled, expected })`,其中 `expected` 让安装在装入的不是 `version` 版本的 `name` 时连同模块一起撤销自身;该安装失败、被取消,或已安装但无法启动时,先移除仍留下的目标版本,再从被移除版本原来的注册表重新安装它,没有记录时按配置的计划安装。重新安装不带请求 id,因此取消更新无法中止它。没有 HMR 时,它直接安装在运行中的版本之上,`installBundle` 在这种情况下允许这样做并报告 `restart-required`。`plugin-manager/update-state` 通告每一步,供对话框的进度列表使用。 -**更新资格是列出的事实。** `BundleInfo.updatable` 标记以自身包名、按注册表 spec 保存(不是 `npm:` 别名)、带 semver 版本且不承载插件管理的第三方 profile 依赖;页面只为这些组合包显示版本区块,两个方法都拒绝其他组合包。没有更新操作的 `plugin_manager` 工具省略该字段。 +**更新资格是列出的事实。** 以自身包名、按注册表 spec 保存(不是 `npm:` 别名)的第三方 profile 依赖,其 `BundleInfo.update` 为 `registry`;按固定到完整提交的 Git spec 保存的为 `git`,其重新安装能恢复被移除的版本;两者都要求 semver 版本且不承载插件管理。页面只为 `registry` 组合包显示版本区块,`listVersions` 拒绝其他所有组合包,`updateBundle` 拒绝所有没有 `update` 的组合包。没有更新操作的 `plugin_manager` 工具省略该字段。 + +**Git 副本从其来源的仓库目录更新到插件索引列出的版本。** Git 仓库没有版本列表,而插件索引把每个 Git 版本固定到一个提交和一个包目录。版本比 `git` 副本新、且固定到同一仓库与 `path:` 目录中的一个提交时,`pluginMarket` Host 服务为其标上 `update`;「发现」为其提供 **更新**,并把包、版本和 spec 交给插件页,由插件页的更新对话框调用 `updateBundle(name, version, { spec })`。Host 要求 `git` 副本提供这样的 spec,并拒绝为 `registry` 副本提供的 spec;两个服务都从 `@deepseek-ai/dsh-plugin-manager` 导入 `pinnedGitOrigin`,它比较仓库来源时不考虑 pnpm 保存的形式,因为 profile 会为它无法读取的 HTTPS 仓库保存 SSH 形式,并为 GitHub URL 保存 `github:` 简写。pnpm 只在安装时读取 Git 包,因此 Host 在移除之前只检查网络能否到达目标仓库所在的主机;主机不可达时,目标的安装与重新安装同样会失败。Git 无法运行的检查,或因需要凭据而被拒绝的检查,交给 pnpm 处理,pnpm 无需 Git 即可将公开托管仓库中的提交作为 tarball 安装。其余检查在安装过程中进行:兼容性的检查与每次安装相同,`expected` 在组合包加载前撤销安装了另一个包或版本、或不含组合包的目标,并以 `unexpected-package` 或 `not-bundle` 失败。被移除的副本从 profile 保存的 spec 重新安装,「发现」从不在已安装副本之上安装。 **更新只向前。** 页面只列出比已安装版本新的版本,`updateBundle` 拒绝其他版本,因此更新不会让插件读取更新版本写入的数据。 @@ -28,13 +30,26 @@ Status: implemented **在读取组合包时列出版本。** `listBundles` 不联网;加入注册表查询会让打开插件页变成网络操作,且每个版本一个子进程。 +**由 Host 从仓库 tag 读取 Git 版本。** 在 monorepo 中,tag 不指明包目录,也不说明索引审核过哪个版本;插件索引已经固定了提交、目录与版本。 + +**把较新的提交安装到已安装副本之上。** 这样无需移除,但有 HMR 时包会解析到另一个目录,因此运行中替换会被拒绝,直到重启;安装失败时也不会重新安装任何版本。 + +**在移除已安装副本之前检查包名。** pnpm 只有拉取 Git spec 才能读取其 manifest(元数据清单),在安装前再拉取一次会使下载量翻倍;在本次运行中检查安装的包只需拉取一次,目标不对时由重新安装恢复。 + +**在安装之后移除安装了另一个包的目标。** 那时其组合包已经加载并运行;若另一个包是已有依赖,`pnpm add` 已将其替换,移除它会卸载用户保留的插件。连同锁文件撤销本次运行,也会恢复该依赖。 + +**接受来自其他仓库的目标。** 索引内容没有签名,更新对话框也不带自己的信任提示,因此任何已配置的索引都能用来自用户从未选择的来源的代码替换插件的代码;迁移到另一个仓库的插件需要先卸载再安装,并经过安装确认。 + ## 后果 - 检查一个包需要一次 `pnpm view`,再加上每个列出版本一次,每次都受 `inspectTimeoutMs` 限制。 -- 更新保存的目标版本,以及失败后重新安装的版本,都是确切版本,即使 profile 原先保存的是范围;有 HMR 时,组合包排到选中列表末尾。 +- 更新保存的注册表目标版本,以及失败后重新安装的版本,都是确切版本,即使 profile 原先保存的是范围;有 HMR 时,组合包排到选中列表末尾。 - 移除与安装分别获取 profile 锁,其他操作可能在两者之间运行;安装被拒绝(例如锁等待超时)时,同样会触发重新安装。 - 依赖需要新的构建脚本批准的版本会安装失败,并重新安装为原版本;其脚本通过普通安装批准。 - 重新安装也失败时,包处于未安装状态,对话框会如实说明。 +- 提交不存在、未声明组合包、拒绝当前运行 DSH,或安装了另一个包或版本的 Git 目标,只有在已安装副本被移除之后才会被发现;重新安装可以恢复,代价是插件停止一次并多安装一次。 +- URL 中带凭据保存的 Git 副本,从已保存的 spec 重新安装时不带这些凭据,因此私有仓库需要 Git 凭据助手。 +- 以分支、标签或范围保存的 Git 副本,或其插件已迁移到另一个仓库的 Git 副本,只能通过先卸载再安装来更新。 ## 验证 @@ -43,5 +58,11 @@ Status: implemented | 列表、注册表回退、拒绝、兼容性、预发布、弃用与版本上限 | `packages/boot/plugin-manager/tests/manager.spec.ts` | | 经移除与安装的更新、目标失败、取消或被拒绝后的重新安装、重新安装失败、改动前的拒绝、移除失败与无 HMR 路径 | `packages/boot/plugin-manager/tests/manager.spec.ts` | | `pnpm view` 答复解析 | `packages/boot/plugin-manager/tests/versions.spec.ts` | -| store 中的版本检查、更新状态与步骤进度 | `packages/client/ui-plugin-manager/tests/manager-store.client.spec.ts` | -| 版本区块、工具栏检查与每种更新结果 | `packages/client/ui-plugin-manager/tests/components.client.spec.tsx` | +| Git 更新资格、经同一目录中某个提交的更新、从已保存 spec 重新安装、安装另一个包或版本的目标、目标不含组合包时的无 HMR 路径、不可达的仓库主机,以及对 spec 的拒绝 | `packages/boot/plugin-manager/tests/manager.spec.ts` | +| pnpm 保存的各种形式下的 Git 来源 | `packages/boot/plugin-manager/tests/install-spec.spec.ts` | +| 安装了另一个包、另一个版本,或缺少预期的组合包时,连同模块一起撤销的运行 | `packages/boot/plugin-manager/tests/operations.spec.ts` | +| 对任意主机上 Git 仓库的连接检查 | `packages/boot/plugin-manager/tests/github-connection.spec.ts` | +| 索引列表中被标为更新的、已安装副本所在目录的 Git 版本 | `packages/boot/plugin-market/tests/market.spec.ts` | +| store 中的版本检查、更新状态与步骤进度,包括另一个标签页指定的 Git 更新 | `packages/client/ui-plugin-manager/tests/manager-store.client.spec.ts` | +| 版本区块、工具栏检查、每种更新结果,以及对话框中 Git 目标的仓库 | `packages/client/ui-plugin-manager/tests/components.client.spec.tsx` | +| 「发现」卡片和版本行上的 **更新**,以及不在已安装副本之上安装 | `packages/client/ui-plugin-market/tests/discover.client.spec.tsx` | diff --git a/docs/event-producer-consumer.md b/docs/event-producer-consumer.md index 94560967d2..99ba79208d 100644 --- a/docs/event-producer-consumer.md +++ b/docs/event-producer-consumer.md @@ -62,12 +62,12 @@ Application listeners are outside this package matrix. Desktop Host subscribes t | `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:76`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`llm-replay`](../packages/test-support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) | | `mcp-manager/status` | `parallel` | [`packages/mcp/mcp-manager/src/types.ts:46`](../packages/mcp/mcp-manager/src/types.ts) | [`mcp-manager`](../packages/mcp/mcp-manager) (`parallel`) | - | | `permission-presets/catalog-changed` | `emit` | [`packages/interaction/permission-presets/src/types.ts:48`](../packages/interaction/permission-presets/src/types.ts) | [`permission-presets`](../packages/interaction/permission-presets) (`events.dispatch`) | `remotes` | -| `plugin-manager/changed` | `emit` | [`packages/boot/plugin-manager/src/types.ts:432`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` | -| `plugin-manager/install-log` | `emit` | [`packages/boot/plugin-manager/src/types.ts:438`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` | -| `plugin-manager/install-state` | `emit` | [`packages/boot/plugin-manager/src/types.ts:445`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` | -| `plugin-manager/operation` | `parallel` | [`packages/boot/plugin-manager/src/types.ts:424`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`parallel`) | - | -| `plugin-manager/update-state` | `emit` | [`packages/boot/plugin-manager/src/types.ts:452`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` | -| `plugin-market/changed` | `emit` | [`packages/boot/plugin-market/src/types.ts:168`](../packages/boot/plugin-market/src/types.ts) | [`plugin-market`](../packages/boot/plugin-market) (`emit`) | `remotes` | +| `plugin-manager/changed` | `emit` | [`packages/boot/plugin-manager/src/types.ts:452`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` | +| `plugin-manager/install-log` | `emit` | [`packages/boot/plugin-manager/src/types.ts:458`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` | +| `plugin-manager/install-state` | `emit` | [`packages/boot/plugin-manager/src/types.ts:465`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` | +| `plugin-manager/operation` | `parallel` | [`packages/boot/plugin-manager/src/types.ts:444`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`parallel`) | - | +| `plugin-manager/update-state` | `emit` | [`packages/boot/plugin-manager/src/types.ts:472`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` | +| `plugin-market/changed` | `emit` | [`packages/boot/plugin-market/src/types.ts:174`](../packages/boot/plugin-market/src/types.ts) | [`plugin-market`](../packages/boot/plugin-market) (`emit`) | `remotes` | | `schedule/changed` | `emit` | [`packages/schedule/schedule/src/types.ts:634`](../packages/schedule/schedule/src/types.ts) | [`schedule`](../packages/schedule/schedule) (`emit`) | `remotes` | | `schedule/resolve-agent` | `waterfall` | [`packages/schedule/schedule/src/index.ts:71`](../packages/schedule/schedule/src/index.ts) | [`schedule`](../packages/schedule/schedule) (`waterfall`) | `server` | | `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:44`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - | diff --git a/docs/event-producer-consumer.zh.md b/docs/event-producer-consumer.zh.md index 911fcf3a70..54938cd97b 100644 --- a/docs/event-producer-consumer.zh.md +++ b/docs/event-producer-consumer.zh.md @@ -64,12 +64,12 @@ | `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:76`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`llm-replay`](../packages/test-support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) | | `mcp-manager/status` | `parallel` | [`packages/mcp/mcp-manager/src/types.ts:46`](../packages/mcp/mcp-manager/src/types.ts) | [`mcp-manager`](../packages/mcp/mcp-manager) (`parallel`) | - | | `permission-presets/catalog-changed` | `emit` | [`packages/interaction/permission-presets/src/types.ts:48`](../packages/interaction/permission-presets/src/types.ts) | [`permission-presets`](../packages/interaction/permission-presets) (`events.dispatch`) | `remotes` | -| `plugin-manager/changed` | `emit` | [`packages/boot/plugin-manager/src/types.ts:432`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` | -| `plugin-manager/install-log` | `emit` | [`packages/boot/plugin-manager/src/types.ts:438`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` | -| `plugin-manager/install-state` | `emit` | [`packages/boot/plugin-manager/src/types.ts:445`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` | -| `plugin-manager/operation` | `parallel` | [`packages/boot/plugin-manager/src/types.ts:424`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`parallel`) | - | -| `plugin-manager/update-state` | `emit` | [`packages/boot/plugin-manager/src/types.ts:452`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` | -| `plugin-market/changed` | `emit` | [`packages/boot/plugin-market/src/types.ts:168`](../packages/boot/plugin-market/src/types.ts) | [`plugin-market`](../packages/boot/plugin-market) (`emit`) | `remotes` | +| `plugin-manager/changed` | `emit` | [`packages/boot/plugin-manager/src/types.ts:452`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` | +| `plugin-manager/install-log` | `emit` | [`packages/boot/plugin-manager/src/types.ts:458`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` | +| `plugin-manager/install-state` | `emit` | [`packages/boot/plugin-manager/src/types.ts:465`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` | +| `plugin-manager/operation` | `parallel` | [`packages/boot/plugin-manager/src/types.ts:444`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`parallel`) | - | +| `plugin-manager/update-state` | `emit` | [`packages/boot/plugin-manager/src/types.ts:472`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` | +| `plugin-market/changed` | `emit` | [`packages/boot/plugin-market/src/types.ts:174`](../packages/boot/plugin-market/src/types.ts) | [`plugin-market`](../packages/boot/plugin-market) (`emit`) | `remotes` | | `schedule/changed` | `emit` | [`packages/schedule/schedule/src/types.ts:634`](../packages/schedule/schedule/src/types.ts) | [`schedule`](../packages/schedule/schedule) (`emit`) | `remotes` | | `schedule/resolve-agent` | `waterfall` | [`packages/schedule/schedule/src/index.ts:71`](../packages/schedule/schedule/src/index.ts) | [`schedule`](../packages/schedule/schedule) (`waterfall`) | `server` | | `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:44`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - | diff --git a/docs/subsystems/boot.i18n.yaml b/docs/subsystems/boot.i18n.yaml index a1b1b47591..e4107281a9 100644 --- a/docs/subsystems/boot.i18n.yaml +++ b/docs/subsystems/boot.i18n.yaml @@ -6,11 +6,11 @@ en: 962b4398c9052959 zh: 14471c340a9f6e60 /profile-management/management-records: - en: 2cf6a1f62dcc1de6 - zh: b6d519b60f6f5c79 + en: a99f861a2d7089da + zh: 0decb374afa4014f /profile-management/market-records: - en: 3dfc62d837d61b9b - zh: ec223be315f3dc62 + en: a36e220bc3995386 + zh: 9b88d8606519368b /profile-management/profile-reconciliation: en: 42a093314af774d4 zh: 7c1784a3af02b31c diff --git a/docs/subsystems/boot.md b/docs/subsystems/boot.md index 48b9b179e0..6780e92c43 100644 --- a/docs/subsystems/boot.md +++ b/docs/subsystems/boot.md @@ -10,9 +10,9 @@ The [boot package group](../../packages/boot/README.md) owns launcher-provided p `PluginInfo` carries module identity, effective enablement, fiber phase and optional display `meta`, plus a unique `patchId` or a `readOnlyReason`. -`BundleInfo.official` identifies project-maintained shipped optional bundles and on-demand catalog entries independently of installation. `availability` identifies a readable installation-provided or profile package, or `missing`; on-demand catalog entries exclude undeclared transitive copies. `installed` records the profile dependency declaration. An on-demand `installTarget` carries the host-derived exact spec and version. `BundleInfo` also carries the package name, optional installed version, selected enablement, removal availability, update eligibility (`updatable`), optional resolution or unsupported-platform error, and, for a bundle the profile's own dependency supplies and the installation does not, `source`: that dependency as a spec `pnpm add` accepts. Its optional `meta` and each `BundleRowInfo.meta` contain display text or a metadata diagnostic; Clients select a language at render time. +`BundleInfo.official` identifies project-maintained shipped optional bundles and on-demand catalog entries independently of installation. `availability` identifies a readable installation-provided or profile package, or `missing`; on-demand catalog entries exclude undeclared transitive copies. `installed` records the profile dependency declaration. An on-demand `installTarget` carries the host-derived exact spec and version. `BundleInfo` also carries the package name, optional installed version, selected enablement, removal availability, update method (`update`: `registry` or `git`, absent when the bundle cannot be updated), optional resolution or unsupported-platform error, and, for a bundle the profile's own dependency supplies and the installation does not, `source`: that dependency as a spec `pnpm add` accepts. Its optional `meta` and each `BundleRowInfo.meta` contain display text or a metadata diagnostic; Clients select a language at render time. -`InstallBundleOptions.saveExact` passes `--save-exact` to the shared package installer. `InstallBundleOptions.enabled` defaults to true. False installs without selecting the bundle layer. `approvedBuilds` grants persistent script permission to the supplied pending package names before installation. `registry` names the registry asked first; absent, the configured one. +`InstallBundleOptions.saveExact` passes `--save-exact` to the shared package installer. `InstallBundleOptions.enabled` defaults to true. False installs without selecting the bundle layer. `approvedBuilds` grants persistent script permission to the supplied pending package names before installation. `registry` names the registry asked first; absent, the configured one. `expected` names the package and manifest version the spec must install; a run that installs anything else is undone with its profile files and modules and fails with `unexpected-package`, one whose package declares no bundle fails with `not-bundle`, and one whose bundle patches cannot load fails as an operation error. `BundleDetail` is a profile bundle's long description: `{ status: 'found' }` with its `origin` (`locale` or `readme`), its `path` inside the package, and its `markdown`, or `{ status: 'none' }`; [`readBundleDetail`](../../packages/boot/plugin-manager/README.md#bundle-detail-documents) documents the order and the image route. `NpmFileSource` names a public npm file CDN, `jsdelivr`, `unpkg`, or `npmmirror`; a read's `npmFileSources` lists the ones to ask first. @@ -26,7 +26,7 @@ The [boot package group](../../packages/boot/README.md) owns launcher-provided p `MarketIndexSummary` carries one configured index URL with the `title` and `description` of its last successful read and, when the last read failed, its `error`. -`MarketQuery` selects one page of an index: fuzzy `text`, a `category`, `labels` that must all be present, a `MarketSort`, `offset`, `limit` up to 100, `refresh` to fetch again instead of using a cached document, and `fetchedAt` to continue the pages of one document. `MarketSearchResult` carries the index URL, `title`, optional `description`, market `metadata`, the document's `fetchedAt`, the page's `entries`, the `total` number of matches, and the `nextOffset`. Each `MarketListingEntry` adds `defaultVersion`, the version installed by default, and `installation`, the profile's status for the package name; each of its `versions` adds `install`, the exact spec and optional first registry for `installBundle`, and `compatibility`, the evaluation of its `engines.dsh` including a version exemption. +`MarketQuery` selects one page of an index: fuzzy `text`, a `category`, `labels` that must all be present, a `MarketSort`, `offset`, `limit` up to 100, `refresh` to fetch again instead of using a cached document, and `fetchedAt` to continue the pages of one document. `MarketSearchResult` carries the index URL, `title`, optional `description`, market `metadata`, the document's `fetchedAt`, the page's `entries`, the `total` number of matches, and the `nextOffset`. Each `MarketListingEntry` adds `defaultVersion`, the version installed by default, and `installation`, the profile's status for the package name; each of its `versions` adds `install`, the exact spec and optional first registry for `installBundle`, `compatibility`, the evaluation of its `engines.dsh` including a version exemption, and `update`, whether the version can replace an installed Git copy through `updateBundle`: it is newer and pins a commit of the repository directory that copy came from. `PluginDetail` is a market entry's long description: `{ status: 'found' }` with its `origin` (`locale`, `readme`, or `repository`), its `markdown`, and a `document` id whose images `GET /api/plugin-market/detail-file` serves, or `{ status: 'none' }`. The [plugin market documents the order](../../packages/boot/plugin-market/README.md#plugin-detail-documents) its sources are read in. @@ -267,15 +267,21 @@ Manage profile files and apply their declared reload lifecycle. @Remote async listVersions(name: string, options?: ListVersionsOptions, signal?: AbortSignal): Promise /** - * Replace an installed registry bundle with a newer published version. The target is checked on the registry first: - * it must exist, declare a bundle, and accept the running DSH version, or nothing changes. With HMR the installed - * version is removed, so its plugins stop, and the target is installed with the same selection; when that - * installation fails or is cancelled, the removed version is installed again from the registry it came from. - * Without HMR the target is installed over the running version and the result reports the restart. + * Replace an installed bundle with a newer version. A registry copy updates to `name@version`, checked on the registry + * first: it must exist, declare a bundle, and accept the running DSH version, or nothing changes. A Git copy updates + * to the Git spec `options.spec` names, which must pin a full commit of the repository directory the copy came from; + * with HMR, a network failure or timeout reaching its repository stops the update before anything changes. pnpm reads + * a Git package only while installing it, so its bundle and compatibility are checked then. Either target + * installation is undone, profile files and modules included, before its bundle loads unless it installs `name` at + * `version`. With HMR the installed version is removed, so its plugins stop, and the target is installed with the same + * selection; when that installation fails or is cancelled, the removed version is installed again: a registry copy + * from the registry it came from, a Git copy from its saved spec. Without HMR the target is installed over the running + * version and the result reports the restart. * @param name Installed profile dependency name. - * @param version Exact published version newer than the installed one. - * @param options The registry asked first, as for `listVersions`, and the request id naming the progress and the - * target installation's logs and cancellation. + * @param version Exact version newer than the installed one. + * @param options The registry asked first, as for `listVersions`; the Git spec of `version`, required for and accepted + * only by a bundle `listBundles` lists with `update: 'git'`; and the request id naming the progress and the target + * installation's logs and cancellation. * @returns The versions, the last step that ran with its result, and the reinstallation when one ran. */ @Remote async updateBundle(name: string, version: string, options?: UpdateBundleOptions): Promise diff --git a/docs/subsystems/boot.zh.md b/docs/subsystems/boot.zh.md index d80c05fb53..915b6e8112 100644 --- a/docs/subsystems/boot.zh.md +++ b/docs/subsystems/boot.zh.md @@ -10,9 +10,9 @@ `PluginInfo` 包含模块标识、实际启停状态、fiber 阶段和可选的展示 `meta`,以及唯一的 `patchId` 或 `readOnlyReason`。 -`BundleInfo.official` 标识项目维护的随附可选组合包和按需目录项,独立于安装状态。`availability` 表示可读取的安装随附包、profile 包或 `missing`;按需目录项不计入未声明的传递依赖副本。`installed` 记录 profile 的依赖声明。按需条目的 `installTarget` 包含 Host 生成的精确 spec 和版本。`BundleInfo` 还包含包名、可选的安装版本、组合层选择状态、删除可用性、更新资格(`updatable`)、可选的解析错误或平台不支持错误;由 profile 自身依赖提供、且安装不提供的组合包还带有 `source`,即该依赖在 `pnpm add` 中可用的 spec。它的可选 `meta` 与各行的 `BundleRowInfo.meta` 包含展示文本或元信息诊断;Client 在渲染时选择语言。 +`BundleInfo.official` 标识项目维护的随附可选组合包和按需目录项,独立于安装状态。`availability` 表示可读取的安装随附包、profile 包或 `missing`;按需目录项不计入未声明的传递依赖副本。`installed` 记录 profile 的依赖声明。按需条目的 `installTarget` 包含 Host 生成的精确 spec 和版本。`BundleInfo` 还包含包名、可选的安装版本、组合层选择状态、删除可用性、更新方式(`update`:`registry` 或 `git`,组合包无法更新时不存在)、可选的解析错误或平台不支持错误;由 profile 自身依赖提供、且安装不提供的组合包还带有 `source`,即该依赖在 `pnpm add` 中可用的 spec。它的可选 `meta` 与各行的 `BundleRowInfo.meta` 包含展示文本或元信息诊断;Client 在渲染时选择语言。 -`InstallBundleOptions.saveExact` 向共享包安装器传入 `--save-exact`。`InstallBundleOptions.enabled` 默认为 true,false 表示安装但不选择组合包层。`approvedBuilds` 在安装前向指定的待审批包名授予持久脚本权限。`registry` 指定首先询问的注册表;缺省为配置的那个。 +`InstallBundleOptions.saveExact` 向共享包安装器传入 `--save-exact`。`InstallBundleOptions.enabled` 默认为 true,false 表示安装但不选择组合包层。`approvedBuilds` 在安装前向指定的待审批包名授予持久脚本权限。`registry` 指定首先询问的注册表;缺省为配置的那个。`expected` 指定该 spec 必须安装的包与清单版本;安装了其他任何内容的运行会连同其 profile 文件与模块一起被撤销,并以 `unexpected-package` 失败,所安装的包未声明组合包的运行以 `not-bundle` 失败,组合包 patch 无法加载的运行则以操作错误失败。 `BundleDetail` 是 profile 中组合包的详细介绍:`{ status: 'found' }` 携带其 `origin`(`locale` 或 `readme`)、在包内的 `path` 和 `markdown`;否则为 `{ status: 'none' }`。[`readBundleDetail`](../../packages/boot/plugin-manager/README.zh.md#bundle-detail-documents) 说明读取顺序和图片路由。`NpmFileSource` 指定一个公共 npm 文件 CDN:`jsdelivr`、`unpkg` 或 `npmmirror`;一次读取的 `npmFileSources` 列出先请求的 CDN。 @@ -26,7 +26,7 @@ `MarketIndexSummary` 携带一个已配置的索引 URL,以及最近一次成功读取得到的 `title` 和 `description`;最近一次读取失败时,还携带其 `error`。 -`MarketQuery` 选择索引的一页:模糊匹配的 `text`、一个 `category`、必须全部具备的 `labels`、一个 `MarketSort`、`offset`、不超过 100 的 `limit`、表示重新获取而不使用缓存文档的 `refresh`,以及用于继续读取同一文档后续页的 `fetchedAt`。`MarketSearchResult` 携带索引 URL、`title`、可选的 `description`、市场 `metadata`、该文档的 `fetchedAt`、本页的 `entries`、匹配总数 `total` 和 `nextOffset`。每个 `MarketListingEntry` 额外携带 `defaultVersion`,即默认安装的版本,以及 `installation`,即该包名在当前 profile 中的状态;其 `versions` 中的每个版本额外携带 `install`,即交给 `installBundle` 的精确 spec 和可选的首选 registry,以及 `compatibility`,即包含版本豁免在内的 `engines.dsh` 评估结果。 +`MarketQuery` 选择索引的一页:模糊匹配的 `text`、一个 `category`、必须全部具备的 `labels`、一个 `MarketSort`、`offset`、不超过 100 的 `limit`、表示重新获取而不使用缓存文档的 `refresh`,以及用于继续读取同一文档后续页的 `fetchedAt`。`MarketSearchResult` 携带索引 URL、`title`、可选的 `description`、市场 `metadata`、该文档的 `fetchedAt`、本页的 `entries`、匹配总数 `total` 和 `nextOffset`。每个 `MarketListingEntry` 额外携带 `defaultVersion`,即默认安装的版本,以及 `installation`,即该包名在当前 profile 中的状态;其 `versions` 中的每个版本额外携带 `install`,即交给 `installBundle` 的精确 spec 和可选的首选 registry,`compatibility`,即包含版本豁免在内的 `engines.dsh` 评估结果,以及 `update`,即该版本能否通过 `updateBundle` 替换已安装的 Git 副本:它比该副本新,且固定到该副本来源的仓库目录中的一个提交。 `PluginDetail` 是市场条目的详细介绍:`{ status: 'found' }` 携带其 `origin`(`locale`、`readme` 或 `repository`)、`markdown`,以及一个 `document` id,该文档中的图片由 `GET /api/plugin-market/detail-file` 提供;否则为 `{ status: 'none' }`。读取其来源的顺序见[插件市场的说明](../../packages/boot/plugin-market/README.zh.md#plugin-detail-documents)。 @@ -267,15 +267,21 @@ Manage profile files and apply their declared reload lifecycle. @Remote async listVersions(name: string, options?: ListVersionsOptions, signal?: AbortSignal): Promise /** - * Replace an installed registry bundle with a newer published version. The target is checked on the registry first: - * it must exist, declare a bundle, and accept the running DSH version, or nothing changes. With HMR the installed - * version is removed, so its plugins stop, and the target is installed with the same selection; when that - * installation fails or is cancelled, the removed version is installed again from the registry it came from. - * Without HMR the target is installed over the running version and the result reports the restart. + * Replace an installed bundle with a newer version. A registry copy updates to `name@version`, checked on the registry + * first: it must exist, declare a bundle, and accept the running DSH version, or nothing changes. A Git copy updates + * to the Git spec `options.spec` names, which must pin a full commit of the repository directory the copy came from; + * with HMR, a network failure or timeout reaching its repository stops the update before anything changes. pnpm reads + * a Git package only while installing it, so its bundle and compatibility are checked then. Either target + * installation is undone, profile files and modules included, before its bundle loads unless it installs `name` at + * `version`. With HMR the installed version is removed, so its plugins stop, and the target is installed with the same + * selection; when that installation fails or is cancelled, the removed version is installed again: a registry copy + * from the registry it came from, a Git copy from its saved spec. Without HMR the target is installed over the running + * version and the result reports the restart. * @param name Installed profile dependency name. - * @param version Exact published version newer than the installed one. - * @param options The registry asked first, as for `listVersions`, and the request id naming the progress and the - * target installation's logs and cancellation. + * @param version Exact version newer than the installed one. + * @param options The registry asked first, as for `listVersions`; the Git spec of `version`, required for and accepted + * only by a bundle `listBundles` lists with `update: 'git'`; and the request id naming the progress and the target + * installation's logs and cancellation. * @returns The versions, the last step that ran with its result, and the reinstallation when one ran. */ @Remote async updateBundle(name: string, version: string, options?: UpdateBundleOptions): Promise diff --git a/docs/upgrade-guide/v0.2.2-rc.1/plugin-update-method/guide.i18n.yaml b/docs/upgrade-guide/v0.2.2-rc.1/plugin-update-method/guide.i18n.yaml new file mode 100644 index 0000000000..928d1d95aa --- /dev/null +++ b/docs/upgrade-guide/v0.2.2-rc.1/plugin-update-method/guide.i18n.yaml @@ -0,0 +1,16 @@ +# Bilingual-pair consistency record for guide.md (docs/i18n/README.md): per heading +# section, a hash of its English and Chinese blocks outside code blocks and generated regions. +# After editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write docs/upgrade-guide/v0.2.2-rc.1/plugin-update-method/guide.md +/: + en: 24d651741581bb5e + zh: 670f4299c6fcb8b4 +/bundleinfo-update-replaces-bundleinfo-updatable: + en: 3ccbfa4a29f90906 + zh: 3ff0b0d86a50c92f +/bundleinfo-update-replaces-bundleinfo-updatable/change: + en: 23ec2f7f6b1e6197 + zh: 33d096a4901d2de7 +/bundleinfo-update-replaces-bundleinfo-updatable/migration: + en: 782c9ca1a60d80a1 + zh: 6528822070e970ee diff --git a/docs/upgrade-guide/v0.2.2-rc.1/plugin-update-method/guide.md b/docs/upgrade-guide/v0.2.2-rc.1/plugin-update-method/guide.md new file mode 100644 index 0000000000..621e3da6eb --- /dev/null +++ b/docs/upgrade-guide/v0.2.2-rc.1/plugin-update-method/guide.md @@ -0,0 +1,22 @@ +--- +kind: upgrade-guide +description: "BundleInfo.updatable is replaced by BundleInfo.update, and ManagementError gains the unexpected-package code." +--- + +# `BundleInfo.update` replaces `BundleInfo.updatable` + +English | [中文](guide.zh.md) + +## Change + +`pluginManager.listBundles()` no longer returns `BundleInfo.updatable: boolean`. It returns the optional `BundleInfo.update`, which is `'registry'` for a bundle whose newer versions `listVersions` reads, `'git'` for a bundle installed from a Git spec pinned to a full commit, and absent for a bundle `updateBundle` cannot replace. A bundle that `updatable: true` marked before is now `update: 'registry'`. `updateBundle` also accepts a `'git'` bundle when `options.spec` names a newer commit of the same repository directory. + +`ManagementError['code']` gains `unexpected-package`: an installation run with `InstallBundleOptions.expected`, which `updateBundle` always passes, fails with it when the spec installs another package or version. + +This affects code that reads `listBundles()` through the Host API, `@deepseek-ai/dsh-plugin-manager/types`, or the remote client in `@deepseek-ai/dsh-api-remotes/client`, and code that switches exhaustively over `ManagementError['code']`. The `plugin_manager` tool never returned the field and is unchanged. + +## Migration + +1. Replace each read of `updatable`. To decide whether `listVersions` can list a bundle's versions, test `bundle.update === 'registry'`; to decide whether `updateBundle` can replace it at all, test `bundle.update !== undefined`. +2. If code switches exhaustively over `ManagementError['code']`, handle `unexpected-package`. +3. Run your type check; it succeeds when no `updatable` read and no unhandled `ManagementError` code remains. diff --git a/docs/upgrade-guide/v0.2.2-rc.1/plugin-update-method/guide.zh.md b/docs/upgrade-guide/v0.2.2-rc.1/plugin-update-method/guide.zh.md new file mode 100644 index 0000000000..dbac4fe064 --- /dev/null +++ b/docs/upgrade-guide/v0.2.2-rc.1/plugin-update-method/guide.zh.md @@ -0,0 +1,22 @@ +--- +kind: upgrade-guide +description: "BundleInfo.updatable 由 BundleInfo.update 取代,ManagementError 新增 unexpected-package 错误码。" +--- + +# `BundleInfo.update` 取代 `BundleInfo.updatable` + +[English](guide.md) | 中文 + +## 变更 + +`pluginManager.listBundles()` 不再返回 `BundleInfo.updatable: boolean`,改为返回可选的 `BundleInfo.update`:`listVersions` 能读取其较新版本的组合包为 `'registry'`,从固定到完整提交的 Git spec 安装的组合包为 `'git'`,`updateBundle` 无法替换的组合包不含此字段。此前标记为 `updatable: true` 的组合包现在为 `update: 'registry'`。当 `options.spec` 指向同一仓库目录的较新提交时,`updateBundle` 也接受 `'git'` 组合包。 + +`ManagementError['code']` 新增 `unexpected-package`:带 `InstallBundleOptions.expected` 运行的安装(`updateBundle` 总会传入该选项)在 spec 安装了其他包或其他版本时以此错误码失败。 + +受影响的代码包括:通过 Host API、`@deepseek-ai/dsh-plugin-manager/types` 或 `@deepseek-ai/dsh-api-remotes/client` 中的远端客户端读取 `listBundles()` 的代码,以及对 `ManagementError['code']` 做穷尽 switch 的代码。`plugin_manager` 工具从未返回该字段,保持不变。 + +## 迁移 + +1. 替换每一处对 `updatable` 的读取。要判断 `listVersions` 能否列出组合包的版本,检查 `bundle.update === 'registry'`;要判断 `updateBundle` 是否能以任何方式替换它,检查 `bundle.update !== undefined`。 +2. 如果代码对 `ManagementError['code']` 做穷尽 switch,需要处理 `unexpected-package`。 +3. 运行类型检查;当不再有对 `updatable` 的读取,也没有未处理的 `ManagementError` 错误码时,类型检查通过。 diff --git a/packages/boot/plugin-manager/README.i18n.yaml b/packages/boot/plugin-manager/README.i18n.yaml index 2b95a68b06..5a187dc4ac 100644 --- a/packages/boot/plugin-manager/README.i18n.yaml +++ b/packages/boot/plugin-manager/README.i18n.yaml @@ -15,8 +15,8 @@ en: 87679c51aa3693f5 zh: 11684a48a5806afb /deepseek-ai-dsh-plugin-manager/use-this-package: - en: ae0905aa47cc971d - zh: b22fc6e831fac136 + en: c4a9acdb98de091d + zh: 417d8bafe75157ec /deepseek-ai-dsh-plugin-manager/use-this-package/version-compatibility-and-exemptions: en: 483285658e267dd5 zh: 846bec2daac59a96 @@ -48,8 +48,8 @@ en: c29f60be7f3edc1a zh: d9b9388b96ef95cc /deepseek-ai-dsh-plugin-manager/known-limitations-and-deferred-work: - en: d26075545fe1e990 - zh: 068c07d642843c94 + en: 89306b478cb75b3b + zh: f2f50c88114fec99 /deepseek-ai-dsh-plugin-manager/known-limitations-and-deferred-work/failure-behavior: en: 3fde425e82bbd0f4 zh: 531e2365e7d67480 diff --git a/packages/boot/plugin-manager/README.md b/packages/boot/plugin-manager/README.md index a06788d42a..cccb096c15 100644 --- a/packages/boot/plugin-manager/README.md +++ b/packages/boot/plugin-manager/README.md @@ -53,9 +53,9 @@ Published installations offer `package@`. Source Web and de Official on-demand installs call the same `installBundle` operation as other bundles. Registry targets use `saveExact: true`; development targets retain the ordinary `link:` dependency. An explicit update removes the installed package and installs the target with the current `enabled` selection; without HMR, a bundle that runs since startup cannot be removed, so the installation goes over it and reports `restart-required`. A failed or cancelled installation after the removal leaves the package uninstalled, and the reinstalled bundle joins the end of the selection. Turning Off only deselects the layer, and Remove uses the existing removal checks. Installation failure restores the profile manifest and lockfile; activation failure after installation can retain the saved dependency and selection. The existing peer-compatibility check still applies, without an additional Official-only version admission rule. -`listVersions(name, options)` reads the published versions newer than an installed bundle that `listBundles` marks `updatable`: a third-party profile dependency saved as a registry spec under its own name, not an `npm:` alias, whose manifest declares a semver version, and that carries no plugin management. One `pnpm view versions time dist-tags` asks the registry plan from `options.registry`, else the registry this process last installed the package from, else the configured one. The newest `versionListLimit` versions above the installed one are then read from the registry that answered, one `pnpm view @` each and at most `versionLookupConcurrency` at a time, because pnpm prints only the highest match of a range. The list orders them newest first with their dist-tags, publication time, prerelease flag, deprecation message, whether they declare a bundle, and an `incompatible` record when their `engines.dsh` or DSH peers reject the running version without an exemption; `omitted` counts the newer versions left out, and `attempts` names every registry asked with what each failed with. A refusal is `not-updatable`, `not-found`, `network`, or `unknown`; one version lookup that fails refuses the whole list. Official bundles follow the DSH version and are not listed. +`listBundles` sets `update` to how `updateBundle` replaces a bundle: `registry` for a third-party profile dependency saved as a registry spec under its own name, not an `npm:` alias, and `git` for one saved as a Git spec pinned to a full 40-character commit; either needs a manifest semver version and no plugin management. A Git spec saved with a branch, a tag, or a range is not updatable, because reinstalling it after a failed update would not bring back the removed version. `listVersions(name, options)` reads the published versions newer than a `registry` bundle. One `pnpm view versions time dist-tags` asks the registry plan from `options.registry`, else the registry this process last installed the package from, else the configured one. The newest `versionListLimit` versions above the installed one are then read from the registry that answered, one `pnpm view @` each and at most `versionLookupConcurrency` at a time, because pnpm prints only the highest match of a range. The list orders them newest first with their dist-tags, publication time, prerelease flag, deprecation message, whether they declare a bundle, and an `incompatible` record when their `engines.dsh` or DSH peers reject the running version without an exemption; `omitted` counts the newer versions left out, and `attempts` names every registry asked with what each failed with. A refusal is `not-updatable`, `not-found`, `network`, or `unknown`; one version lookup that fails refuses the whole list. Official bundles follow the DSH version and are not listed, and a `git` bundle is refused as `not-updatable`, since a Git repository publishes no version list; a plugin index lists its newer versions. -`updateBundle(name, version, options)` replaces an updatable bundle with an exact newer version; an older version is refused. The target is first viewed over the same plan as `listVersions`, and a version that is missing, declares no bundle, or whose `engines.dsh` or DSH peers reject the running DSH stops the update at `step: 'check'` with the profile unchanged. With HMR the update removes the installed version, so its plugins stop, then installs `name@version` with `saveExact` and the current selection from the registry that answered. When that installation fails, is cancelled, or installs but cannot start, the target is removed if it stayed and the removed version is installed again from the registry it was installed from in this process, else over the configured plan as its removal asks, and reported as `restore`; a reinstallation that also fails leaves the package uninstalled. A removal or installation that rejects, for example on a lock timeout, is reported as that step's failure. Without HMR the target is installed over the running version and the result reports `restart-required`. `plugin-manager/update-state` announces `checking`, `removing`, `installing`, and `restoring` under the caller's `requestId`; the target's installation streams its logs and accepts `cancelInstall` under the same id, while the reinstallation carries none, so a cancellation cannot stop it. +`updateBundle(name, version, options)` replaces a bundle `listBundles` lists with `update` by an exact newer version; an older version is refused. A `registry` bundle updates to `name@version`, and `options.spec` is refused. Its target is first viewed over the same plan as `listVersions`, and a version that is missing, declares no bundle, or whose `engines.dsh` or DSH peers reject the running DSH stops the update at `step: 'check'` with the profile unchanged. A `git` bundle updates only to the Git spec `options.spec` names, which must pin a full commit of the repository and `path:` directory the installed copy pins; the comparison ignores the form pnpm saved, so the SSH form saved for an HTTPS repository it could not read, and the `github:`, `gitlab:`, or `bitbucket:` shortcut saved for a known host, match their HTTPS form. pnpm reads a Git package only while installing it, so with HMR only the repository's host is checked first: when `git ls-remote` fails on a network error or times out within `githubConnectionTimeoutMs`, the update stops at `step: 'check'` with `failedAt: 'spec-host'` and the profile unchanged, since an unreachable host would fail the target and the reinstallation alike; a check Git cannot run, or one the repository refuses without credentials, is left to pnpm. The installation checks the bundle declaration and compatibility. Either target installation runs with `expected: { name, version }`, so a target that installs another package or another version is undone before its bundle loads and fails with `unexpected-package`, and one without a bundle declaration is undone the same way and fails with `not-bundle`. With HMR the update removes the installed version, so its plugins stop, then installs the target with the current selection: `name@version` with `saveExact` from the registry that answered, or the Git spec as written. When that installation fails, is cancelled, or installs but cannot start, the target is removed if it stayed and the removed version is installed again, and reported as `restore`: a registry copy from the registry it was installed from in this process, else over the configured plan as its removal asks, and a Git copy from the spec the profile saved; a reinstallation that also fails leaves the package uninstalled. A removal or installation that rejects, for example on a lock timeout, is reported as that step's failure. Without HMR the target is installed over the running version and the result reports `restart-required`. `plugin-manager/update-state` announces `checking`, `removing`, `installing`, and `restoring` under the caller's `requestId`; the target's installation streams its logs and accepts `cancelInstall` under the same id, while the reinstallation carries none, so a cancellation cannot stop it. `installBundle` checks GitHub repositories with `git ls-remote` before starting pnpm, using the profile directory and the installer's Git and proxy configuration. `githubConnectionTimeoutMs` defaults to 5000 ms and limits only this check, not package download or builds. The check disables credential helpers and prompts; only network failures and timeouts stop installation, reporting `failedAt: 'spec-host'` with the existing failure kind and diagnostic log. A stopped installation streams that failure under its `requestId` as the installation's only `plugin-manager/install-log` run, named by the `git ls-remote` command line with any URL user information removed; a timed-out check reports no exit code. The answer's `packageResult` stays a plain result, without the command line. Authentication, repository lookup and other failures are left to pnpm, including its HTTPS-to-SSH fallback. Cancellation and manager disposal stop the check and its descendants. Registry packages, paths, tarballs and other Git hosts skip this check. A reachable repository can still fail during download or bundle validation. @@ -65,7 +65,7 @@ Registries are asked in turn. The plan starts at `options.registry`, else the co The browser-safe `@deepseek-ai/dsh-plugin-manager/registry` entry exports `OFFICIAL_NPM_REGISTRY` and `NPMMIRROR_REGISTRY` for consumers that identify those public registries. -`installBundle` accepts a caller-generated `requestId`, under which `plugin-manager/install-log` streams each pnpm run's output and `plugin-manager/install-state` announces `installing`, `cancelling`, and `applying`; a pnpm run that fails without printing streams the diagnostic its log carries, so the details panel is not left empty. `installing` is announced once per registry asked, with the `attempt`'s registry, position, and the plan's length. The install asks the same plan as the lookup, from `options.registry`, running `pnpm add` with `--registry` and restoring the profile files between attempts; it moves on for the failures another registry can change and stops at any other, and at a failure whose error line names the host a git or tarball spec is fetched from, which no registry stands in for; `failedAt` says which of the two the last failed run could not reach. `cancelInstall(requestId)` stops the run and answers `cancelled` only after the Git check or pnpm exited and the files are back, `too-late` once the bundle is being applied, and `not-running` for any other id; the install call then reports `application: 'cancelled'`. A run that fails, is cancelled, or adds a package without a bundle patch restores `package.json` and `pnpm-lock.yaml` as they were; `packageResult.kind` classifies the last run from its exit and output, `registries` names every registry asked, `bundle` names the package a finished run added, and `version` its manifest version, which can differ from the version `inspect` read, for example when pnpm's `minimumReleaseAge` holds back a recent release. `listBundles` carries each bundle's one-liner (the package `description`), the rows its patch declares with their live entries, and the built-in rows it overrides; it lists the profile's own bundles, the bundles the installation supplies, and a selected name without a bundle patch as a `not-bundle` problem, while an unselected plain dependency is left out. A bundle the launcher's `OPTIONAL_BUNDLES` names is `optional`: shipped switched off for the person to turn on, never removable, and selected by no shipped template ([rationale](../../../.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.md)). Every completed operation emits `plugin-manager/changed`; a patch generation applied outside the manager, by HMR's watcher after a CLI or hand edit, announces nothing, so the page learns of it on its next read. +`installBundle` accepts a caller-generated `requestId`, under which `plugin-manager/install-log` streams each pnpm run's output and `plugin-manager/install-state` announces `installing`, `cancelling`, and `applying`; a pnpm run that fails without printing streams the diagnostic its log carries, so the details panel is not left empty. `installing` is announced once per registry asked, with the `attempt`'s registry, position, and the plan's length. The install asks the same plan as the lookup, from `options.registry`, running `pnpm add` with `--registry` and restoring the profile files between attempts; it moves on for the failures another registry can change and stops at any other, and at a failure whose error line names the host a git or tarball spec is fetched from, which no registry stands in for; `failedAt` says which of the two the last failed run could not reach. `cancelInstall(requestId)` stops the run and answers `cancelled` only after the Git check or pnpm exited and the files are back, `too-late` once the bundle is being applied, and `not-running` for any other id; the install call then reports `application: 'cancelled'`. A run that fails, is cancelled, or adds a package without a bundle patch restores `package.json` and `pnpm-lock.yaml` as they were; `packageResult.kind` classifies the last run from its exit and output, `registries` names every registry asked, `bundle` names the package a finished run added, and `version` its manifest version, which can differ from the version `inspect` read, for example when pnpm's `minimumReleaseAge` holds back a recent release. `options.expected` names the package and manifest version the spec must install: a run that leaves that package missing or at another version, or that adds or changes another direct dependency, is undone like a compatibility rejection, with `package.json`, `pnpm-lock.yaml`, and `node_modules` restored, before any bundle loads, and fails with `unexpected-package`; one whose package declares no bundle is undone the same way and fails with `not-bundle`, and one whose bundle patches cannot load fails as an operation error. `packageResult.unmet` names that code, and the output names what the run installed. `listBundles` carries each bundle's one-liner (the package `description`), the rows its patch declares with their live entries, and the built-in rows it overrides; it lists the profile's own bundles, the bundles the installation supplies, and a selected name without a bundle patch as a `not-bundle` problem, while an unselected plain dependency is left out. A bundle the launcher's `OPTIONAL_BUNDLES` names is `optional`: shipped switched off for the person to turn on, never removable, and selected by no shipped template ([rationale](../../../.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.md)). Every completed operation emits `plugin-manager/changed`; a patch generation applied outside the manager, by HMR's watcher after a CLI or hand edit, announces nothing, so the page learns of it on its next read. `waitForInstall(requestId)` lets a client recover a lost response by waiting for the active installation, including its non-cancellable application phase. It returns the same outcome as the original call, or `null` if the request is not active. Completed results are not retained; `null` establishes neither success nor cancellation. @@ -169,8 +169,9 @@ Tool results append to the transcript. Enabling or disabling other tools can cha - Web approves the entire displayed pending group; it has no per-package selection. - An approved git key stays in `pnpm-workspace.yaml` after the plugin moves to a newer commit; nothing removes stale approvals. - pnpm verifies every lockfile tarball URL against its default registry when a command changes the lockfile. After a package was installed from another registry, installing a different package with the default registry, or removing that package after a restart, can fail with `ERR_PNPM_TARBALL_URL_MISMATCH`. -- An update saves the target as an exact version, and a reinstallation after a failed update saves the previous version exactly, even when the profile had saved a range. With HMR the reinstalled bundle joins the end of the selection, which can change which bundle's configuration takes precedence. +- A registry update saves the target as an exact version, and a reinstallation after a failed update saves the previous version exactly, even when the profile had saved a range. With HMR the reinstalled bundle joins the end of the selection, which can change which bundle's configuration takes precedence. - An update runs its removal and its installation as two profile operations; another operation can run between them. +- Only the reachability of a Git target's host is checked before the installed copy is removed, and only through Git: a host that only pnpm's own proxy settings reach is refused as unreachable. A target whose commit is missing, that needs credentials pnpm lacks, declares no bundle, rejects the running DSH, or installs another package or version costs a stop and a reinstallation. The reinstallation uses the saved spec, from which the user information of an http(s) URL was removed, so a private repository needs a Git credential helper or the SSH form. A copy the SSH fallback saved for an HTTPS URL with a port loses the port, so its origin no longer matches the index's HTTPS spec and its updates are refused. - The Git transport probe keeps the parent console on Windows, so an SSH host-key or passphrase prompt can appear in the launching terminal until `gitTransportTimeoutMs`. On POSIX a host missing from `known_hosts` fails the SSH probe, so installation stays on HTTPS until the host key is accepted once, for example with `ssh -T git@`. A credential-helper dialog or ssh-agent confirmation, such as a Touch ID or `ssh-add -c` dialog, that takes longer than the deadline counts as a failed probe, and an HTTPS probe that times out lets a reachable SSH form be saved to the profile. A private repository waits up to twice `gitTransportTimeoutMs` before pnpm starts. The SSH form always uses the `git` user and SSH's default port; other SSH endpoints need a `url..insteadOf` rule. When both probes fail, the diagnostic is pnpm's HTTPS failure; the SSH probe's reason is not reported. - A newer version whose dependencies need new build-script approval fails to update and is reinstalled at the previous version; approve its scripts through an ordinary installation after removing the package. - Package replacements require restarting the process without HMR. A package relinked outside Plugin Manager while it is still installed waits for a restart. diff --git a/packages/boot/plugin-manager/README.zh.md b/packages/boot/plugin-manager/README.zh.md index db1c756fae..cfc0c99f3a 100644 --- a/packages/boot/plugin-manager/README.zh.md +++ b/packages/boot/plugin-manager/README.zh.md @@ -53,9 +53,9 @@ kind: "package-reference" 官方按需安装调用和其他组合包相同的 `installBundle` 操作。注册表目标使用 `saveExact: true`;开发目标保留普通 `link:` 依赖。显式更新先删除已安装的包,再以当前 `enabled` 选择状态安装目标;没有 HMR 时,自启动起运行的组合包无法删除,因此安装直接覆盖它并报告 `restart-required`。删除之后安装失败或被取消时,该包保持未安装,重新安装的组合包排在选择列表末尾。关闭仅取消选择配置层,移除使用原有移除检查。安装失败恢复 profile 清单和锁文件;安装成功后激活失败可能保留已保存的依赖和选择。原有 peer 兼容性检查继续生效,不增加仅针对官方条目的版本准入规则。 -`listVersions(name, options)` 读取 `listBundles` 标为 `updatable` 的已安装组合包的新版本:以自身包名、按注册表 spec 保存(不是 `npm:` 别名)的第三方 profile 依赖,清单声明 semver 版本,且不承载插件管理。一次 `pnpm view versions time dist-tags` 按注册表计划询问,计划从 `options.registry` 开始,否则从本进程上次安装该包所用的注册表开始,否则从配置的注册表开始。随后从作答的注册表逐个读取比已安装版本新的最新 `versionListLimit` 个版本,每个版本一次 `pnpm view @`,同时最多 `versionLookupConcurrency` 个,因为 pnpm 对范围只打印最高的匹配版本。列表按从新到旧排列,带有各版本的 dist-tag、发布时间、预发布标记、弃用说明、是否声明组合包,以及在其 `engines.dsh` 或 DSH peer 拒绝当前运行版本且没有豁免时的 `incompatible` 记录;`omitted` 统计未列出的新版本数,`attempts` 列出问过的每个注册表及其失败原因。拒绝为 `not-updatable`、`not-found`、`network` 或 `unknown`;任一版本查询失败都会拒绝整个列表。官方组合包跟随 DSH 版本,不会列出。 +`listBundles` 用 `update` 标明 `updateBundle` 如何替换组合包:以自身包名、按注册表 spec 保存(不是 `npm:` 别名)的第三方 profile 依赖为 `registry`,按固定到完整 40 位提交的 Git spec 保存的为 `git`;两者都要求清单声明 semver 版本,且不承载插件管理。以分支、标签或范围保存的 Git spec 不可更新,因为更新失败后重新安装它无法恢复被移除的版本。`listVersions(name, options)` 读取 `registry` 组合包较新的已发布版本。一次 `pnpm view versions time dist-tags` 按注册表计划询问,计划从 `options.registry` 开始,否则从本进程上次安装该包所用的注册表开始,否则从配置的注册表开始。随后从作答的注册表逐个读取比已安装版本新的最新 `versionListLimit` 个版本,每个版本一次 `pnpm view @`,同时最多 `versionLookupConcurrency` 个,因为 pnpm 对范围只打印最高的匹配版本。列表按从新到旧排列,带有各版本的 dist-tag、发布时间、预发布标记、弃用说明、是否声明组合包,以及在其 `engines.dsh` 或 DSH peer 拒绝当前运行版本且没有豁免时的 `incompatible` 记录;`omitted` 统计未列出的新版本数,`attempts` 列出问过的每个注册表及其失败原因。拒绝为 `not-updatable`、`not-found`、`network` 或 `unknown`;任一版本查询失败都会拒绝整个列表。官方组合包跟随 DSH 版本,不会列出;`git` 组合包以 `not-updatable` 拒绝,因为 Git 仓库不发布版本列表,其新版本由插件索引列出。 -`updateBundle(name, version, options)` 把可更新的组合包替换为一个更新的确切版本;更旧的版本会被拒绝。目标版本先按与 `listVersions` 相同的计划查询,版本不存在、未声明组合包,或其 `engines.dsh` 或 DSH peer 拒绝当前 DSH 时,更新停在 `step: 'check'`,profile 保持不变。有 HMR 时,更新先移除已安装版本,使其插件停止,再以 `saveExact` 和当前的选中状态从作答的注册表安装 `name@version`。该安装失败、被取消,或已安装但无法启动时,先移除仍留下的目标版本,再从本进程安装被移除版本所用的注册表重新安装它,没有记录时按配置的计划安装,与移除的做法一致,结果以 `restore` 报告;重新安装也失败时,包处于未安装状态。移除或安装被拒绝(例如锁等待超时)时,报告为该步骤失败。没有 HMR 时,目标版本直接安装在运行中的版本之上,结果报告 `restart-required`。`plugin-manager/update-state` 以调用方的 `requestId` 通告 `checking`、`removing`、`installing` 与 `restoring`;目标版本的安装以同一 id 流式输出日志并接受 `cancelInstall`,重新安装不带 id,因此取消无法中止它。 +`updateBundle(name, version, options)` 把 `listBundles` 标有 `update` 的组合包替换为一个更新的确切版本;更旧的版本会被拒绝。`registry` 组合包更新到 `name@version`,`options.spec` 会被拒绝。其目标版本先按与 `listVersions` 相同的计划查询,版本不存在、未声明组合包,或其 `engines.dsh` 或 DSH peer 拒绝当前 DSH 时,更新停在 `step: 'check'`,profile 保持不变。`git` 组合包只更新到 `options.spec` 指定的 Git spec,该 spec 必须固定到一个完整提交,其仓库与 `path:` 目录须与已安装副本固定的相同;比较忽略 pnpm 保存的形式,因此为其无法读取的 HTTPS 仓库保存的 SSH 形式,以及为已知主机保存的 `github:`、`gitlab:` 或 `bitbucket:` 简写,都与其 HTTPS 形式相匹配。pnpm 只在安装时读取 Git 包,因此有 HMR 时预先只检查仓库所在的主机:`git ls-remote` 因网络错误失败或超出 `githubConnectionTimeoutMs` 时限时,更新停在 `step: 'check'`,标记 `failedAt: 'spec-host'`,profile 保持不变,因为主机不可达时,目标的安装与重新安装同样会失败;Git 无法运行的检查,或仓库因未提供凭据而拒绝的检查,交给 pnpm 处理。安装过程检查组合包声明与兼容性。两种目标的安装都带 `expected: { name, version }` 运行,因此安装了另一个包或另一个版本的目标会在其组合包加载前被撤销,并以 `unexpected-package` 失败;没有组合包声明的目标以同样方式被撤销,并以 `not-bundle` 失败。有 HMR 时,更新先移除已安装版本,使其插件停止,再以当前的选中状态安装目标:从作答的注册表以 `saveExact` 安装 `name@version`,或按原样安装 Git spec。该安装失败、被取消,或已安装但无法启动时,先移除仍留下的目标版本,再重新安装被移除的版本,结果以 `restore` 报告:注册表副本从本进程安装它所用的注册表重新安装,没有记录时按配置的计划安装,与移除的做法一致;Git 副本从 profile 保存的 spec 重新安装;重新安装也失败时,包处于未安装状态。移除或安装被拒绝(例如锁等待超时)时,报告为该步骤失败。没有 HMR 时,目标版本直接安装在运行中的版本之上,结果报告 `restart-required`。`plugin-manager/update-state` 以调用方的 `requestId` 通告 `checking`、`removing`、`installing` 与 `restoring`;目标版本的安装以同一 id 流式输出日志并接受 `cancelInstall`,重新安装不带 id,因此取消无法中止它。 `installBundle` 在启动 pnpm 前通过 `git ls-remote` 检查 GitHub 仓库,使用 profile 目录及安装器的 Git 与代理配置。`githubConnectionTimeoutMs` 默认为 5000 毫秒,只限制这次检查,不限制包下载或构建。检查禁用凭据助手和认证提示;只有网络失败与超时会停止安装,通过现有失败类型与诊断日志返回,并标记 `failedAt: 'spec-host'`。被停止的安装会以调用方的 `requestId` 把该失败转发为这次安装唯一的 `plugin-manager/install-log` 运行,其命令行是去掉 URL 用户信息的 `git ls-remote`;因超时结束的检查不报告退出码。应答中的 `packageResult` 仍是普通结果,不含该命令行。认证、仓库查找及其他失败继续交给 pnpm,包括其 HTTPS 到 SSH 的回退。取消安装或销毁管理器会停止检查及其子进程。注册表包、本地路径、压缩包和其他 Git 主机跳过此检查。仓库可达后,下载或组合包验证仍可能失败。 @@ -65,7 +65,7 @@ HTTP(S) Git URL 指向 pnpm 没有 HTTPS 到 SSH 回退的主机(即 `hosted-g 可在浏览器使用的 `@deepseek-ai/dsh-plugin-manager/registry` 入口导出 `OFFICIAL_NPM_REGISTRY` 和 `NPMMIRROR_REGISTRY`,供消费方识别这两个公共注册表。 -`installBundle` 接受调用方生成的 `requestId`,`plugin-manager/install-log` 在其下流式转发每次 pnpm 运行的输出,`plugin-manager/install-state` 通告 `installing`、`cancelling` 与 `applying`;未打印任何输出就失败的 pnpm 运行转发其日志中的诊断,详情面板因而不会为空。每问一个注册表通告一次 `installing`,`attempt` 带上这次的注册表、序号和计划长度。安装沿用查询的计划,从 `options.registry` 开始,以 `--registry` 运行 `pnpm add`,两次尝试之间恢复 profile 文件;换一个注册表能改变的失败才转问下一个,其他失败即停,错误行点名 git 或 tarball spec 自身拉取主机的失败也停,因为没有注册表能替代那台主机;`failedAt` 说明最后一次失败的运行连不上的是二者中的哪一个。`cancelInstall(requestId)` 停止运行,只在 Git 检查或 pnpm 退出且文件恢复后答复 `cancelled`,组合包已在应用时答复 `too-late`,其他 id 答复 `not-running`;安装调用随后报告 `application: 'cancelled'`。失败、被取消或装入了没有组合包 patch 的包的运行,会把 `package.json` 与 `pnpm-lock.yaml` 恢复原样;`packageResult.kind` 按退出方式与输出对最后一次运行分类,`registries` 列出问过的每个注册表,`bundle` 给出完成的运行新增的包,`version` 给出它的清单版本,它可能与 `inspect` 读到的版本不同,例如 pnpm 的 `minimumReleaseAge` 暂缓了最近的发布。`listBundles` 携带每个组合包的一句话简介(包的 `description`)、其 patch 声明的行及其存活条目,以及它覆盖的内置行;它列出 profile 自己的组合包、安装提供的组合包,以及被选中却没有组合包 patch 的名字(作为 `not-bundle` 问题),未选中的普通依赖不列出。启动器的 `OPTIONAL_BUNDLES` 点名的组合包是 `optional`:随安装提供、默认关闭、由用户开启,永不可卸载,也不被任何随附模板选中([理由](../../../.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.zh.md))。每个完成的操作都会发出 `plugin-manager/changed`;在管理器之外应用的一代 patch(HMR 监视到 CLI 或手工编辑后)不发通知,页面要到下一次读取才知道。 +`installBundle` 接受调用方生成的 `requestId`,`plugin-manager/install-log` 在其下流式转发每次 pnpm 运行的输出,`plugin-manager/install-state` 通告 `installing`、`cancelling` 与 `applying`;未打印任何输出就失败的 pnpm 运行转发其日志中的诊断,详情面板因而不会为空。每问一个注册表通告一次 `installing`,`attempt` 带上这次的注册表、序号和计划长度。安装沿用查询的计划,从 `options.registry` 开始,以 `--registry` 运行 `pnpm add`,两次尝试之间恢复 profile 文件;换一个注册表能改变的失败才转问下一个,其他失败即停,错误行点名 git 或 tarball spec 自身拉取主机的失败也停,因为没有注册表能替代那台主机;`failedAt` 说明最后一次失败的运行连不上的是二者中的哪一个。`cancelInstall(requestId)` 停止运行,只在 Git 检查或 pnpm 退出且文件恢复后答复 `cancelled`,组合包已在应用时答复 `too-late`,其他 id 答复 `not-running`;安装调用随后报告 `application: 'cancelled'`。失败、被取消或装入了没有组合包 patch 的包的运行,会把 `package.json` 与 `pnpm-lock.yaml` 恢复原样;`packageResult.kind` 按退出方式与输出对最后一次运行分类,`registries` 列出问过的每个注册表,`bundle` 给出完成的运行新增的包,`version` 给出它的清单版本,它可能与 `inspect` 读到的版本不同,例如 pnpm 的 `minimumReleaseAge` 暂缓了最近的发布。`options.expected` 指定该 spec 必须安装的包与清单版本:使该包缺失或处于其他版本、或新增或改动了其他直接依赖的运行,会像兼容性拒绝一样被撤销,在任何组合包加载前恢复 `package.json`、`pnpm-lock.yaml` 与 `node_modules`,并以 `unexpected-package` 失败;所安装的包未声明组合包的运行以同样方式被撤销,并以 `not-bundle` 失败,组合包 patch 无法加载的运行则以操作错误失败。`packageResult.unmet` 给出该错误码,输出给出该运行安装的内容。`listBundles` 携带每个组合包的一句话简介(包的 `description`)、其 patch 声明的行及其存活条目,以及它覆盖的内置行;它列出 profile 自己的组合包、安装提供的组合包,以及被选中却没有组合包 patch 的名字(作为 `not-bundle` 问题),未选中的普通依赖不列出。启动器的 `OPTIONAL_BUNDLES` 点名的组合包是 `optional`:随安装提供、默认关闭、由用户开启,永不可卸载,也不被任何随附模板选中([理由](../../../.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.zh.md))。每个完成的操作都会发出 `plugin-manager/changed`;在管理器之外应用的一代 patch(HMR 监视到 CLI 或手工编辑后)不发通知,页面要到下一次读取才知道。 `waitForInstall(requestId)` 让客户端在响应丢失后等待活动安装的结果,包括不可取消的应用阶段。它返回与原调用相同的结果,请求不在活动中时返回 `null`。已完成的结果不保留;`null` 不表示成功或已取消。 @@ -169,8 +169,9 @@ service 运行若在 `idleTimeoutMs` 内没有任何捕获输出即被终止, - Web 一次批准显示出来的整组待决定包,没有逐包选择。 - 插件移到更新的提交后,已批准的 git 键仍留在 `pnpm-workspace.yaml` 中;没有任何机制移除过期的批准。 - 命令改动锁文件时,pnpm 按默认注册表校验锁文件中的每个 tarball URL。某个包从其他注册表安装后,使用默认注册表安装另一个包,或在重启后删除该包,可能以 `ERR_PNPM_TARBALL_URL_MISMATCH` 失败。 -- 更新把目标保存为确切版本,失败后的重新安装也把原版本保存为确切版本,即使 profile 原先保存的是范围。有 HMR 时,重新安装的组合包排到选中列表末尾,这可能改变哪个组合包的配置优先。 +- 注册表更新把目标保存为确切版本,失败后的重新安装也把原版本保存为确切版本,即使 profile 原先保存的是范围。有 HMR 时,重新安装的组合包排到选中列表末尾,这可能改变哪个组合包的配置优先。 - 更新把移除与安装作为两次 profile 操作运行,其他操作可能在两者之间运行。 +- 在移除已安装副本之前,只检查 Git 目标所在主机的可达性,且只通过 Git 检查:只有经 pnpm 自身的代理设置才能到达的主机会被视为不可达而拒绝。对于提交不存在、需要 pnpm 不具备的凭据、未声明组合包、拒绝当前 DSH 或安装了其他包或版本的目标,代价是插件停止一次并重新安装一次。重新安装使用保存的 spec,其中 http(s) URL 的 user information 已被去掉,因此私有仓库需要 Git 凭据助手或 SSH 形式。对于带端口的 HTTPS URL,SSH 回退保存的副本会丢失端口,因此其仓库来源不再与插件索引中的 HTTPS spec 相匹配,其更新会被拒绝。 - 在 Windows 上,Git 传输探测沿用父进程的控制台,SSH 的主机密钥或密码提示可能出现在启动终端中,直到 `gitTransportTimeoutMs` 到期。在 POSIX 上,`known_hosts` 中没有的主机会让 SSH 探测失败,因此在接受一次主机密钥(例如执行 `ssh -T git@`)之前一直走 HTTPS。凭据助手对话框或 ssh-agent 确认(例如 Touch ID 或 `ssh-add -c` 对话框)超过时限时算作探测失败;HTTPS 探测超时时,可达的 SSH 形式会写入 profile。私有仓库在 pnpm 启动前最多等待两倍 `gitTransportTimeoutMs`。SSH 形式固定使用 `git` 用户和 SSH 默认端口,其他 SSH 端点需要 `url..insteadOf` 规则。两种探测都失败时,诊断是 pnpm 的 HTTPS 失败,不报告 SSH 探测失败的原因。 - 依赖需要新的构建脚本批准的新版本会更新失败,并重新安装为原版本;移除该包后通过普通安装批准其脚本。 - 没有 HMR 时,替换包需要重启进程。在 Plugin Manager 之外重新链接仍处于安装状态的包,需要等待重启。 diff --git a/packages/boot/plugin-manager/src/diagnostic.ts b/packages/boot/plugin-manager/src/diagnostic.ts index aa79fe5527..1250e8d5cb 100644 --- a/packages/boot/plugin-manager/src/diagnostic.ts +++ b/packages/boot/plugin-manager/src/diagnostic.ts @@ -57,6 +57,7 @@ const operation: z.ZodType = z.discriminatedUnion('phase', [ 'stale-approval': 'stale-approval', 'incompatible-version': 'incompatible-version', 'unsupported-platform': 'unsupported-platform', + 'unexpected-package': 'unexpected-package', 'operation-error': 'operation-error', } satisfies { [K in ManagementError['code']]: K }).optional(), restartReason: z.enum({ diff --git a/packages/boot/plugin-manager/src/github-connection.ts b/packages/boot/plugin-manager/src/github-connection.ts index e2d349c734..9a2b87788a 100644 --- a/packages/boot/plugin-manager/src/github-connection.ts +++ b/packages/boot/plugin-manager/src/github-connection.ts @@ -1,4 +1,4 @@ -/** Bounded GitHub repository checks using the installer's Git configuration and environment. */ +/** Bounded Git repository connection checks using the installer's Git configuration and environment. */ import { mkdir, mkdtemp, open } from 'node:fs/promises' import { join } from 'node:path' import { execa } from 'execa' @@ -19,7 +19,7 @@ export interface GithubConnectionFailure extends PackageResult { readonly argv: readonly string[] } -/** Deadline, diagnostics and cancellation for one read-only GitHub check. */ +/** Deadline, diagnostics and cancellation for one read-only Git repository check. */ export interface GithubConnectionOptions { timeoutMs: number outputBytes: number @@ -30,13 +30,10 @@ export interface GithubConnectionOptions { /** * Check a GitHub repository before pnpm starts, without downloading or building its package. - * Git reads the profile's Git and proxy configuration without invoking credential helpers or prompting. - * Timeout and cancellation terminate its descendants too; pnpm owns authentication and transport fallback. * @param spec The parsed installation address; npm packages, local paths and other hosts are not checked. * @param dir The profile directory where installation runs. * @param options The connection deadline, output bound and operation cancellation. - * @returns A failed check carrying the command line it ran (without URL user information) and its bounded output, with a - * complete log at logPath, or undefined for a reachable repository or an unhandled spec. + * @returns As {@link checkGitConnection}, or undefined for a spec that does not name a GitHub repository. */ export async function checkGithubConnection( spec: ParsedInstallSpec, dir: string, options: GithubConnectionOptions, @@ -44,9 +41,25 @@ export async function checkGithubConnection( if (spec.kind !== 'git') return undefined const host = spec.host.toLowerCase().replace(/:.*$/, '') if (host !== 'github.com' && !host.endsWith('.github.com')) return undefined + return checkGitConnection(spec, dir, options) +} + +/** + * Check a Git repository with `git ls-remote`, without downloading or building its package. A host shorthand is read + * over HTTPS; any other spec over the URL it names. Git reads the profile's Git and proxy configuration without + * invoking credential helpers or prompting, so a repository that needs credentials fails as `unknown`, not as a + * connection failure. Timeout and cancellation terminate its descendants too. + * @param spec The parsed address of a Git repository. + * @param dir The profile directory where installation runs. + * @param options The connection deadline, output bound and operation cancellation. + * @returns A failed check carrying the command line it ran (without URL user information), its bounded output and its + * failure kind, with a complete log at logPath, or undefined for a reachable repository. + */ +export async function checkGitConnection( + spec: ParsedInstallSpec & { readonly kind: 'git' }, dir: string, options: GithubConnectionOptions, +): Promise { const repository = spec.spec.replace(/#.*$/s, '') - .replace(/^github:/i, 'https://github.com/') - .replace(/^gist:/i, 'https://gist.github.com/') + .replace(/^(?:github|gitlab|bitbucket|gist):/i, `https://${spec.host}/`) .replace(/^git\+/i, '') const logRoot = join(dir, '.plugin-manager', 'logs') await mkdir(logRoot, { recursive: true, mode: 0o700 }) @@ -70,7 +83,7 @@ export async function checkGithubConnection( }) if (!result.failed) return undefined if (result.timedOut) await log.write(`dsh: connection to ${spec.host} timed out after ${String(options.timeoutMs)}ms\n`) - if ((await log.stat()).size === 0) await log.write(result.shortMessage ?? 'GitHub connection check failed') + if ((await log.stat()).size === 0) await log.write(result.shortMessage ?? 'Git connection check failed') const { size } = await log.stat() const bytes = Buffer.alloc(Math.min(size, options.outputBytes)) await log.read(bytes, 0, bytes.length, size - bytes.length) diff --git a/packages/boot/plugin-manager/src/index.ts b/packages/boot/plugin-manager/src/index.ts index b1d696ad74..86a0f57ca1 100644 --- a/packages/boot/plugin-manager/src/index.ts +++ b/packages/boot/plugin-manager/src/index.ts @@ -31,12 +31,12 @@ import type { ProfileContext, ProfileManifest } from '@deepseek-ai/dsh-app-boot' import { bundleManifest, readProfileRegistry, registryArguments, runProfilePnpm, saveManifest, viewProfilePackage } from './operations.ts' import { classifyInstallFailure } from './install-failure.ts' import { officialBundleInstallTarget } from './official-install-target.ts' -import { dependencySpec, InvalidInstallSpecError, parseInstallSpec, type ParsedInstallSpec } from './install-spec.ts' +import { dependencySpec, InvalidInstallSpecError, parseInstallSpec, pinnedGitOrigin, type ParsedInstallSpec } from './install-spec.ts' import { attributeFailure, normalizeRegistry, NPMMIRROR_REGISTRY, registryPlan } from './registry.ts' import { flatten, writePluginEnabled } from './patch.ts' import { incompatiblePlugin, ManagementFailure } from './failure.ts' import { approveBuilds, recordPendingBuilds } from './build-approval.ts' -import { checkGithubConnection, type GithubConnectionFailure } from './github-connection.ts' +import { checkGitConnection, checkGithubConnection, type GithubConnectionFailure } from './github-connection.ts' import { chooseGitTransport, sshGitSpec } from './git-transport.ts' import { newerVersions, VERSION_FIELDS, VERSION_LIST_FIELDS, versionFacts } from './versions.ts' import type { @@ -48,7 +48,7 @@ import type { } from './types.ts' export type * from './types.ts' export { classifyInstallFailure, type InstallFailureFacts } from './install-failure.ts' -export { InvalidInstallSpecError, parseInstallSpec, type ParsedInstallSpec } from './install-spec.ts' +export { InvalidInstallSpecError, parseInstallSpec, pinnedGitOrigin, type ParsedInstallSpec } from './install-spec.ts' /** The pnpm executable, registries, and limits for diagnostics, lookups and connection checks. */ export interface Config { @@ -161,18 +161,26 @@ function refused(problem: Extract[' return { status: 'refused', problem, reason } } -/** An installed registry bundle an update can replace, its installed version, and its parsed registry spec. */ -interface UpdateTarget { - bundle: B - installed: string - parsed: Extract +/** + * An installed bundle an update can replace and its installed version: a registry copy with its parsed registry spec, + * or a Git copy with the saved spec that installs it again and the repository directory it pins. + */ +type UpdateTarget = + | { kind: 'registry'; bundle: B; installed: string; parsed: Extract } + | { kind: 'git'; bundle: B; installed: string; source: string; origin: string } + +/** What an update installs: the target spec, the registry asked first when one is named, and whether the save is exact. */ +interface UpdatePlan { + spec: string + registry: Registry | undefined + saveExact: boolean } /** * Why an update cannot replace a listed bundle, or what it replaces. * @param bundle The listed bundle, or undefined for a name the inventory does not list. * @param name The package name, for the refusal. - * @returns The bundle, its installed version, and its registry spec; or the refusal's diagnostic. + * @returns The bundle, its installed version, and how it was installed; or the refusal's diagnostic. */ function updateRefusal>( bundle: B | undefined, name: string, @@ -184,12 +192,15 @@ function updateRefusal { - return Promise.resolve(this.readBundles().map(bundle => ({ ...bundle, updatable: typeof updateRefusal(bundle, bundle.name) !== 'string' }))) + return Promise.resolve(this.readBundles().map((bundle) => { + const target = updateRefusal(bundle, bundle.name) + return typeof target === 'string' ? bundle : { ...bundle, update: target.kind } + })) } /** The bundle inventory before update eligibility, which is derived from each listed bundle. */ - private readBundles(): Omit[] { + private readBundles(): Omit[] { const manifest = readProfileManifest('dsh', this.profile.dir) const exemptions = readProfileVersionExemptions(this.profile.dir) const selected = bundleSelection(manifest) @@ -500,7 +514,7 @@ export class PluginManager extends TypertRemoteService { const installation = JSON.parse(readFileSync(this.profile.installAnchor, 'utf8')) as InstallationManifest const catalog = new Map(OFFICIAL_ON_DEMAND_CATALOG.map(entry => [entry.packageName, entry])) const names = [...new Set([...selected, ...dependencies, ...Object.keys(installation.dependencies ?? {}), ...catalog.keys()])] - const bundles: Omit[] = [] + const bundles: Omit[] = [] const runtimeVersion = getDshRuntimeVersion() for (const name of names) { const installed = dependencies.includes(name) @@ -662,6 +676,7 @@ export class PluginManager extends TypertRemoteService { async listVersions(name: string, options?: ListVersionsOptions, signal?: AbortSignal): Promise { const target = updateRefusal((await this.listBundles()).find(bundle => bundle.name === name), name) if (typeof target === 'string') return { status: 'refused', problem: 'not-updatable', reason: target, attempts: [] } + if (target.kind === 'git') return { status: 'refused', problem: 'not-updatable', reason: `${name} is installed from Git, which lists no versions`, attempts: [] } const plan = registryPlan(this.registryFor(name, options?.registry), await this.registries()) const listing = await this.askRegistries(name, VERSION_LIST_FIELDS, plan, target.parsed, signal) if (listing.status === 'refused') return listing @@ -698,15 +713,21 @@ export class PluginManager extends TypertRemoteService { } /** - * Replace an installed registry bundle with a newer published version. The target is checked on the registry first: - * it must exist, declare a bundle, and accept the running DSH version, or nothing changes. With HMR the installed - * version is removed, so its plugins stop, and the target is installed with the same selection; when that - * installation fails or is cancelled, the removed version is installed again from the registry it came from. - * Without HMR the target is installed over the running version and the result reports the restart. + * Replace an installed bundle with a newer version. A registry copy updates to `name@version`, checked on the registry + * first: it must exist, declare a bundle, and accept the running DSH version, or nothing changes. A Git copy updates + * to the Git spec `options.spec` names, which must pin a full commit of the repository directory the copy came from; + * with HMR, a network failure or timeout reaching its repository stops the update before anything changes. pnpm reads + * a Git package only while installing it, so its bundle and compatibility are checked then. Either target + * installation is undone, profile files and modules included, before its bundle loads unless it installs `name` at + * `version`. With HMR the installed version is removed, so its plugins stop, and the target is installed with the same + * selection; when that installation fails or is cancelled, the removed version is installed again: a registry copy + * from the registry it came from, a Git copy from its saved spec. Without HMR the target is installed over the running + * version and the result reports the restart. * @param name Installed profile dependency name. - * @param version Exact published version newer than the installed one. - * @param options The registry asked first, as for `listVersions`, and the request id naming the progress and the - * target installation's logs and cancellation. + * @param version Exact version newer than the installed one. + * @param options The registry asked first, as for `listVersions`; the Git spec of `version`, required for and accepted + * only by a bundle `listBundles` lists with `update: 'git'`; and the request id naming the progress and the target + * installation's logs and cancellation. * @returns The versions, the last step that ran with its result, and the reinstallation when one ran. */ @Remote @@ -727,29 +748,34 @@ export class PluginManager extends TypertRemoteService { if (semver.valid(version) === null || !semver.gt(version, from)) { return refusedBefore({ code: 'invalid-spec', diagnostic: `${version} is not a version newer than the installed ${from}` }) } - const spec = `${name}@${version}` - const plan = registryPlan(this.registryFor(name, options?.registry), await this.registries()) - const checked = await this.askRegistries(spec, VERSION_FIELDS, plan, { kind: 'registry', spec, name }) - if (checked.status === 'refused') return refusedBefore({ code: 'operation-error', diagnostic: checked.reason }) - const facts = versionFacts(checked.answer, name, version) - if (!facts.bundle) return refusedBefore({ code: 'not-bundle' }) - let compatibility - try { compatibility = evaluatePluginCompatibility(facts.manifest, readProfileVersionExemptions(this.profile.dir)) } - catch (error) { return refusedBefore({ code: 'operation-error', diagnostic: messageOf(error) }) } - if (compatibility !== undefined && !compatibility.exempted) { - return refusedBefore({ code: 'incompatible-version', incompatible: [incompatiblePlugin(compatibility)] }) - } + const planned = target.kind === 'git' + ? this.planGitUpdate(name, target.origin, options) + : await this.checkRegistryUpdate(name, version, options) + if ('code' in planned) return refusedBefore(planned) + const { spec, registry, saveExact } = planned const { enabled } = bundle // A lock timeout or disposal rejects instead of reporting; it reads as the failed step it is. const settled = (stage: ChangeResult['stage'], target: string, operation: Promise): Promise => operation.catch((error: unknown) => ({ changed: false, application: 'failed', stage, target, error: managementError(error) })) const installTarget = (): Promise => settled('install', spec, this.installBundle(spec, { - saveExact: true, registry: checked.registry, enabled, ...requestId === undefined ? {} : { requestId }, + saveExact, enabled, expected: { name, version }, + ...registry === undefined ? {} : { registry }, ...requestId === undefined ? {} : { requestId }, })) if (this.ownerContext.get('hmr') === undefined) { announce('installing') return { from, to: version, step: 'install', result: await installTarget() } } + // A Git host that cannot be reached would fail the target and the reinstallation alike, so nothing is removed. + const unreachable = target.kind === 'git' ? await this.gitHostUnreachable(spec) : undefined + if (unreachable !== undefined) { + return { + from, to: version, step: 'check', + result: { + changed: false, application: 'failed', stage: 'install', target: name, failedAt: 'spec-host', packageResult: unreachable, + error: { code: 'operation-error', diagnostic: unreachable.output }, + }, + } + } // The removed version is installed again where it came from, else over the configured plan, as its removal asks. const previousRegistry = this.installedFrom.get(name) announce('removing') @@ -762,13 +788,80 @@ export class PluginManager extends TypertRemoteService { // A target that installed but failed to start is still a dependency; it goes before the removed version returns. if (result.stage === 'enable') await settled('remove', name, this.removeBundle(name)) // The reinstallation carries no request id, so cancelling the update cannot leave the package removed. - const previous = `${name}@${from}` + const previous = target.kind === 'git' ? target.source : `${name}@${from}` const restore = await settled('install', previous, this.installBundle(previous, { - saveExact: true, enabled, ...previousRegistry === undefined ? {} : { registry: previousRegistry }, + saveExact, enabled, ...previousRegistry === undefined ? {} : { registry: previousRegistry }, })) return { from, to: version, step: 'install', result, restore } } + /** + * The target of a Git copy's update: the caller's Git spec, pinned to a full commit of the copy's repository directory. + * @param name The package name. + * @param origin The repository directory the installed copy pins. + * @param options The caller's spec and registry. + * @returns The spec and the registry its dependencies are asked from first, or why the update is refused. + */ + private planGitUpdate(name: string, origin: string, options: UpdateBundleOptions | undefined): UpdatePlan | ManagementError { + const spec = options?.spec + const pinned = spec === undefined ? undefined : pinnedGitOrigin(spec) + if (spec === undefined || pinned === undefined) { + return { code: 'invalid-spec', diagnostic: `${name} is installed from Git and updates only to a Git spec pinned to a full commit` } + } + if (pinned !== origin) { + return { code: 'invalid-spec', diagnostic: `${spec} is not in the repository directory ${name} is installed from, ${origin}` } + } + return { spec, registry: this.registryFor(name, options?.registry), saveExact: false } + } + + /** + * Check that the network reaches the repository of a Git update's target. Only a network failure or a timeout + * counts: a check Git cannot run, or one the repository refuses without credentials, leaves the answer to pnpm. + * @param spec The target's pinned Git spec. + * @returns The failed check, or undefined when the host answered or the check could not tell. + */ + private async gitHostUnreachable(spec: string): Promise { + const parsed = parseInstallSpec(spec) + /* v8 ignore next -- planGitUpdate accepts only a pinned Git spec */ + if (parsed.kind !== 'git') return undefined + const env = this.profile.packageManager?.env === undefined ? {} : { env: this.profile.packageManager.env } + const check = checkGitConnection(parsed, this.profile.dir, { + timeoutMs: this.githubConnectionTimeoutMs, outputBytes: this.outputBytes, signal: this.abort.signal, ...env, + }) + this.packageOperations.add(check) + let failure: GithubConnectionFailure | undefined + try { failure = await check } finally { this.packageOperations.delete(check) } + this.abort.signal.throwIfAborted() + if (failure?.kind !== 'network' && failure?.kind !== 'timeout') return undefined + // The answer keeps a plain PackageResult; the check's command line stays out of the wire fields. + const { argv: _argv, ...packageResult } = failure + return packageResult + } + + /** + * Check a registry copy's update target on the registry plan before anything changes. + * @param name The package name. + * @param version The target version. + * @param options The caller's registry, as for `listVersions`; a spec is refused. + * @returns The exact spec and the registry that answered for it, or why the target cannot replace the bundle. + */ + private async checkRegistryUpdate( + name: string, version: string, options: UpdateBundleOptions | undefined, + ): Promise { + const spec = `${name}@${version}` + if (options?.spec !== undefined) return { code: 'invalid-spec', diagnostic: `${name} is installed from a registry and updates only to ${spec}` } + const plan = registryPlan(this.registryFor(name, options?.registry), await this.registries()) + const checked = await this.askRegistries(spec, VERSION_FIELDS, plan, { kind: 'registry', spec, name }) + if (checked.status === 'refused') return { code: 'operation-error', diagnostic: checked.reason } + const facts = versionFacts(checked.answer, name, version) + if (!facts.bundle) return { code: 'not-bundle' } + let compatibility + try { compatibility = evaluatePluginCompatibility(facts.manifest, readProfileVersionExemptions(this.profile.dir)) } + catch (error) { return { code: 'operation-error', diagnostic: messageOf(error) } } + if (compatibility !== undefined && !compatibility.exempted) return { code: 'incompatible-version', incompatible: [incompatiblePlugin(compatibility)] } + return { spec, registry: checked.registry, saveExact: true } + } + /** * The registry a version listing or an update asks first. * @param name The package name. @@ -938,11 +1031,16 @@ export class PluginManager extends TypertRemoteService { result.registries.push(registry) used = registry announce('installing', { registry, index: index + 1, total: plan.length }) - run = await this.runPnpm(['add', installSpec, ...options?.saveExact === true ? ['--save-exact'] : [], ...registryArguments(registry)], control.abort.signal, requestId) + run = await this.runPnpm( + ['add', installSpec, ...options?.saveExact === true ? ['--save-exact'] : [], ...registryArguments(registry)], + control.abort.signal, requestId, options?.expected, + ) result.packageResult = run if (stopped()) throw new InstallCancelledError() // A compatibility refusal is the package's own answer, so no other registry is asked. if (run.incompatible !== undefined) throw new ManagementFailure('incompatible-version', run.incompatible) + // So is a run that installed another package, version, or no bundle where the caller expects one. + if (run.unmet !== undefined) throw new ManagementFailure(run.unmet) // A run this manager terminated is not a success, even when pnpm trapped the signal and exited 0. if (run.exitCode === 0 && run.timedOut !== true) break /* v8 ignore next 2 -- runPnpm classifies every run it does not report as succeeded */ @@ -1110,9 +1208,9 @@ export class PluginManager extends TypertRemoteService { return { rows, overrides } } - /** Run one pnpm command in the profile, streaming its output as install-log chunks. */ + /** Run one pnpm command in the profile, streaming its output as install-log chunks; `expected` is the package an `add` must install. */ private async runPnpm( - args: readonly string[], signal?: AbortSignal, requestId?: PluginInstallRequestId, + args: readonly string[], signal?: AbortSignal, requestId?: PluginInstallRequestId, expected?: InstallBundleOptions['expected'], ): Promise { const jobId = randomUUID() const argv = ['pnpm', ...args] @@ -1122,7 +1220,7 @@ export class PluginManager extends TypertRemoteService { execution: 'service', ...this.profile.packageManager ?? { command: this.pnpmCommand }, signal: signal === undefined ? this.abort.signal : AbortSignal.any([this.abort.signal, signal]), outputBytes: this.outputBytes, activateNewBundles: false, idleTimeoutMs: this.idleTimeoutMs, - lookupTimeoutMs: this.inspectTimeoutMs, + lookupTimeoutMs: this.inspectTimeoutMs, ...expected === undefined ? {} : { expected }, onOutput: (text, stream) => { this.ownerContext.emit('plugin-manager/install-log', { ...identity, jobId, argv, cwd, stream, text }) }, diff --git a/packages/boot/plugin-manager/src/install-spec.ts b/packages/boot/plugin-manager/src/install-spec.ts index 2fb6b64cca..e5cfd67c39 100644 --- a/packages/boot/plugin-manager/src/install-spec.ts +++ b/packages/boot/plugin-manager/src/install-spec.ts @@ -1,6 +1,7 @@ /** * Reading an install spec before pnpm sees it: which of pnpm's spec forms it - * takes, and for a registry name whether it is one the registry can accept. + * takes, for a registry name whether it is one the registry can accept, and + * for a Git spec pinned to a commit the repository directory it installs from. * @module @deepseek-ai/dsh-plugin-manager/install-spec */ @@ -91,6 +92,41 @@ export function parseInstallSpec(raw: string): ParsedInstallSpec { return range === undefined ? { kind: 'registry', spec, name } : { kind: 'registry', spec, name, range } } +/** A full SHA-1 commit name, as a fragment parameter of a Git spec. */ +const FULL_COMMIT = /^[0-9a-f]{40}$/i + +/** + * Where a Git spec pinned to a full commit installs from: the lower-case host and the repository path, without + * transport, user, or `.git`, then `#` and the `path:` directory. The origin ignores the form pnpm saves, so the SSH + * form saved for an HTTPS repository it cannot read, and the host shortcut saved for a known host, have the HTTPS + * form's origin. Equal origins name one repository on a host that serves it at the same path over every transport, + * as Git forges do; on a plain SSH server, an scp-like path is relative to the login's home directory. + * @param spec - an install spec, such as a saved dependency or an update's target. + * @returns the origin, or undefined for a spec that is not Git, or whose fragment is not one full commit and at most + * one `path:` directory. + */ +export function pinnedGitOrigin(spec: string): string | undefined { + let parsed: ParsedInstallSpec + try { parsed = parseInstallSpec(spec) } catch (error) { + /* v8 ignore next -- parseInstallSpec throws nothing but its own refusal */ + if (!(error instanceof InvalidInstallSpecError)) throw error + return undefined + } + const hash = parsed.spec.indexOf('#') + if (parsed.kind !== 'git' || hash === -1) return undefined + const parameters = parsed.spec.slice(hash + 1).split('&') + const paths = parameters.filter(part => part.startsWith('path:')) + const others = parameters.filter(part => !part.startsWith('path:')) + // pnpm reads the last of repeated parameters, so a spec that repeats one names no single origin. + if (paths.length > 1 || others.length !== 1 || !FULL_COMMIT.test(others.join(''))) return undefined + const path = (paths[0]?.slice('path:'.length) ?? '').replace(/^\/+|\/+$/g, '') + const location = parsed.spec.slice(0, hash) + .replace(/^(github|gitlab|bitbucket|gist):/i, (_prefix, shorthand: string) => `${GIT_SHORTHAND_HOSTS[shorthand.toLowerCase()] as string}/`) + .replace(/^git\+/i, '').replace(/^[a-z][a-z0-9+.-]*:\/\//i, '').replace(/^[^@/]+@/, '').replace(/^([^/:]+):(?!\d)/, '$1/') + .replace(/\/+$/, '').replace(/\.git$/i, '').replace(/^[^/]+/, host => host.toLowerCase()) + return `${location}#${path}` +} + /** A recorded dependency value pnpm resolves outside a registry: a protocol other than a registry alias, or an scp-like git address. */ const NON_REGISTRY_VALUE = /^(?!(?:npm|jsr|workspace|catalog):)[a-z][a-z0-9+.-]*:|^[^@/:\s]+@[^:/\s]+:/i /** The user information of an http(s) URL, up to the last `@` before the path, which holds a password or access token when present. */ diff --git a/packages/boot/plugin-manager/src/operations.ts b/packages/boot/plugin-manager/src/operations.ts index d0ab1dccab..945c20d078 100644 --- a/packages/boot/plugin-manager/src/operations.ts +++ b/packages/boot/plugin-manager/src/operations.ts @@ -53,6 +53,12 @@ export interface PackageOperationOptions { idleTimeoutMs?: number /** Bound on the pre-install registry lookup, in milliseconds; without one a fixed bound applies. */ lookupTimeoutMs?: number + /** + * The bundle package and manifest version the run must install. A run that leaves this package missing, at another + * version, or without a bundle declaration or bundle patches that load, or that adds or changes another direct + * dependency, is undone like a compatibility rejection after it. + */ + expected?: { readonly name: string; readonly version: string } } /** Resolve relative package specs against the caller's directory. @@ -281,16 +287,19 @@ function bundleComponentManifests(manifest: ProfileManifest, dir: string, anchor } /** Execute pnpm inside a profile whose caller already holds the profile write lock. - * Newly installed or updated direct dependencies are checked even when activation is disabled; - * an untouched dependency never blocks an unrelated operation and stays denied at startup. - * Compatibility denial restores the profile manifest and lockfile, but leaves downloaded modules on disk. + * Newly installed or updated direct dependencies are checked even when activation is disabled. With `expected`, each + * of them must be the expected package, which must be installed at its version with a bundle declaration. Any other + * untouched dependency never blocks an unrelated operation and stays denied at startup. + * A denial before pnpm runs changes nothing; one after it restores the profile manifest and lockfile and reinstalls + * the restored lockfile, while downloaded files stay in the pnpm store. * @param context Launcher-owned profile and resolution locations. * @param args Pnpm arguments, before relative path anchoring. * @param options Output, activation and cancellation policy. * @returns Exit status, whether the silence bound stopped the run, and the diagnostic path. - * A compatibility denial returns exit code 1. Service output is bounded; CLI output uses inherited descriptors. + * A compatibility or `expected` denial returns exit code 1 and names what it refused. * Unsupported Host activation returns exit code 1 with named diagnostics; installed dependencies remain, * and no bundle selection changes are saved. Other reconciliation or filesystem errors reject. + * Service output is bounded; CLI output uses inherited descriptors. * The diagnostic file is closed on return or rejection, including preparation and spawn failures. */ export async function runProfilePnpm( @@ -333,6 +342,14 @@ export async function runProfilePnpm( } /** Packages a compatibility check refused; callers render them for their own surface. */ const incompatible: IncompatiblePlugin[] = [] + /** Why the post-install check undoes a successful run. */ + const warnings: string[] = [] + /** What `options.expected` found wrong with the run, `unexpected-package` taking precedence. */ + let unmet: PackageResult['unmet'] + const fail = (code: NonNullable, warning: string): void => { + if (unmet !== 'unexpected-package') unmet = code + warnings.push(warning) + } const rejected = async (warnings: readonly string[], restoration: string): Promise => { const diagnostic = `\ndsh: installation rejected: ${warnings.join('\n')}\ndsh: ${restoration}.\n` await log.write(diagnostic) @@ -478,7 +495,8 @@ export async function runProfilePnpm( // A terminated run's exit status says nothing about what it wrote, so it never reconciles the selection. if (exitCode === 0 && !control.stalled) { const after = readProfileManifest('dsh', dir) - const warnings: string[] = [] + const { expected } = options + let seen = false for (const [name, spec] of Object.entries(directDependencies(after))) { const packageDir = join(dir, 'node_modules', name) const installed = optionalFile(join(packageDir, 'package.json')) @@ -489,6 +507,18 @@ export async function runProfilePnpm( const issues: IncompatiblePlugin[] = [] try { const manifest = readProfileManifest('dsh', packageDir) + const installedAs = `${name}@${String(manifest.version)}` + const wanted = expected === undefined ? undefined : `${expected.name}@${expected.version}` + // The expected package is checked even when the run left it as it was, as adding its saved spec again does. + if (name === expected?.name) { + seen = true + if (installedAs !== wanted) fail('unexpected-package', `${installedAs} is installed where ${String(wanted)} was expected`) + else if (manifest.dsh?.bundle === undefined) fail('not-bundle', `${installedAs} declares no bundle`) + // The caller loads these patches next; one that cannot load is refused here, while its modules can go back. + else for (const file of bundlePatchPaths(packageDir, manifest.dsh.bundle)) loadOverlayPatches('dsh', file) + } else if (wanted !== undefined && !untouched) { + fail('unexpected-package', `${installedAs} is installed where ${wanted} was expected`) + } for (const candidate of [manifest, ...bundleComponentManifests(manifest, packageDir, context.installAnchor)]) { const issue = evaluatePluginCompatibility(candidate, readProfileVersionExemptions(dir)) if (issue !== undefined && !issue.exempted) { @@ -510,6 +540,7 @@ export async function runProfilePnpm( options.onOutput?.(notice, 'stderr') } } + if (expected !== undefined && !seen) fail('unexpected-package', `${expected.name}@${expected.version} is not installed`) if (warnings.length > 0) { // A bundle component's requirements need installed contents, so this rejection lands after pnpm // replaced the tree: restore the files, then reinstall the restored lockfile so the version @@ -553,6 +584,7 @@ export async function runProfilePnpm( return { exitCode, output: output.toString('utf8'), truncated, logPath, ...control.stalled ? { timedOut: true } : {}, ...incompatible.length > 0 ? { incompatible } : {}, + ...unmet === undefined ? {} : { unmet }, } } diff --git a/packages/boot/plugin-manager/src/tools.ts b/packages/boot/plugin-manager/src/tools.ts index 57f7fda5ac..26632d24de 100644 --- a/packages/boot/plugin-manager/src/tools.ts +++ b/packages/boot/plugin-manager/src/tools.ts @@ -24,6 +24,7 @@ const ERROR_STEPS: Record = { 'stale-approval': 'Call install_bundle again without approvedBuilds to read the current pendingBuilds.', 'incompatible-version': 'Install a version compatible with the running DSH. A version exemption needs the user\'s explicit acceptance of crash and data-loss risk.', 'unsupported-platform': 'This bundle cannot run on the current Host platform; ask the user to use a supported Host.', + 'unexpected-package': 'The source installed another package or version than requested and was rolled back; report this to the user.', 'operation-error': 'Report the diagnostic to the user; retry only after its cause is addressed.', } @@ -130,7 +131,7 @@ export function apply(ctx: Context): void { const entries = rows.slice(offset, offset + limit).map(({ meta: _meta, ...row }) => 'rows' in row // GUI listing and update eligibility do not change the tool's management operations. - ? (({ updatable: _updatable, listedByDefault: _listedByDefault, ...bundle }) => ({ + ? (({ update: _update, listedByDefault: _listedByDefault, ...bundle }) => ({ ...bundle, rows: bundle.rows.map(({ meta: _rowMeta, ...declared }) => declared), }))(row) : row) diff --git a/packages/boot/plugin-manager/src/types.ts b/packages/boot/plugin-manager/src/types.ts index 221607f1e8..f7655644b6 100644 --- a/packages/boot/plugin-manager/src/types.ts +++ b/packages/boot/plugin-manager/src/types.ts @@ -36,7 +36,7 @@ export interface IncompatiblePlugin { /** Localizable management failure and optional external diagnostic. */ export interface ManagementError { - code: ReadOnlyReason | 'unknown-plugin' | 'invalid-spec' | 'already-installed' | 'ambiguous-install' | 'not-bundle' | 'not-removable' | 'stop-profile' | 'bundle-in-use' | 'stale-approval' | 'incompatible-version' | 'unsupported-platform' | 'operation-error' + code: ReadOnlyReason | 'unknown-plugin' | 'invalid-spec' | 'already-installed' | 'ambiguous-install' | 'not-bundle' | 'not-removable' | 'stop-profile' | 'bundle-in-use' | 'stale-approval' | 'incompatible-version' | 'unsupported-platform' | 'unexpected-package' | 'operation-error' diagnostic?: string /** Present with `incompatible-version`: the packages the running DSH version rejects. */ incompatible?: IncompatiblePlugin[] @@ -101,10 +101,12 @@ export interface BundleInfo { optional: boolean removable: boolean /** - * Whether `listVersions` and `updateBundle` accept the bundle: a third-party profile dependency saved as a registry - * spec under its own name, whose installed manifest declares a semver version, and that carries no plugin management. + * How `updateBundle` replaces the bundle, absent when it cannot: `registry` for a third-party profile dependency saved + * as a registry spec under its own name, whose newer versions `listVersions` reads; `git` for one saved as a Git spec + * pinned to a full commit, which updates to another commit of the same repository directory. Either needs a semver + * manifest version and no plugin management. */ - updatable: boolean + update?: 'registry' | 'git' readOnlyReason?: ReadOnlyReason error?: ManagementError /** The rows the bundle's patch inserts, in declaration order; empty when the patch cannot be read. */ @@ -154,6 +156,12 @@ export interface PackageResult { timedOut?: boolean /** Present when a compatibility check refused the run: the packages the running DSH version rejects. */ incompatible?: IncompatiblePlugin[] + /** + * Present when an `expected` check refused the run: `unexpected-package` when the run left the expected package + * missing or at another version, or added or changed another direct dependency; otherwise `not-bundle` when the + * expected package declares no bundle. The output names what the run installed. + */ + unmet?: 'unexpected-package' | 'not-bundle' } /** Persisted change and independently observed application outcome. */ @@ -201,6 +209,13 @@ export interface InstallBundleOptions { approvedBuilds?: string[] /** The registry asked first; absent, the configured one. The configured fallbacks follow while a registry is unreachable or stale. */ registry?: Registry + /** + * The package and manifest version the spec must install. An installation that leaves this package missing or at + * another version, or that adds or changes another direct dependency, is undone with its profile files and modules + * before its bundle loads, and fails with `unexpected-package`; one whose package declares no bundle is undone the + * same way and fails with `not-bundle`, and one whose bundle patches cannot load fails as an operation error. + */ + expected?: { readonly name: string; readonly version: string } } /** Where an inspection asks. */ @@ -310,6 +325,11 @@ export interface ListVersionsOptions { export interface UpdateBundleOptions { /** The registry asked first, normally the one that answered `listVersions`; absent, as for `listVersions`. */ readonly registry?: Registry + /** + * The Git spec of the target version: required for a bundle listed with `update: 'git'`, where it must pin a full + * commit of the installed copy's repository directory, and refused for any other. + */ + readonly spec?: string /** Names the update's progress and the target installation's logs; `cancelInstall` stops only that installation. */ readonly requestId?: PluginInstallRequestId } diff --git a/packages/boot/plugin-manager/tests/bundle-detail.spec.ts b/packages/boot/plugin-manager/tests/bundle-detail.spec.ts index dc3ad0c268..732f08e6aa 100644 --- a/packages/boot/plugin-manager/tests/bundle-detail.spec.ts +++ b/packages/boot/plugin-manager/tests/bundle-detail.spec.ts @@ -159,7 +159,7 @@ it('reports a listed bundle whose directory is gone, and keeps failures other th const { manager, image } = await fixture() const list = manager.listBundles.bind(manager) const listing = vi.spyOn(manager, 'listBundles').mockImplementation(async () => [...await list(), { - name: 'vanished', official: false, availability: 'profile', enabled: false, installed: true, optional: false, removable: true, updatable: false, rows: [], overrides: [], + name: 'vanished', official: false, availability: 'profile', enabled: false, installed: true, optional: false, removable: true, rows: [], overrides: [], }]) expect(await failureOf(manager.readBundleDetail('vanished', 'en'))).toMatchObject({ code: 'plugin-manager/unknown-bundle', details: { name: 'vanished' } }) listing.mockRestore() diff --git a/packages/boot/plugin-manager/tests/github-connection.spec.ts b/packages/boot/plugin-manager/tests/github-connection.spec.ts index e87ac53b8c..1d76e1eaf1 100644 --- a/packages/boot/plugin-manager/tests/github-connection.spec.ts +++ b/packages/boot/plugin-manager/tests/github-connection.spec.ts @@ -9,7 +9,7 @@ import { join } from 'node:path' import { pathToFileURL } from 'node:url' import * as subprocess from 'execa' import { afterAll, beforeAll, expect, it, onTestFinished, vi } from 'vitest' -import { checkGithubConnection } from '../src/github-connection.ts' +import { checkGitConnection, checkGithubConnection } from '../src/github-connection.ts' import { parseInstallSpec } from '../src/install-spec.ts' import { isolateGitCommandLineConfig } from './git-environment.ts' @@ -40,7 +40,14 @@ async function fixture() { tasks.push(task) return task } - return { dir, config, controller, check, env } + const checkAny = (spec: string) => { + const parsed = parseInstallSpec(spec) + if (parsed.kind !== 'git') throw new Error(`${spec} is not a Git spec`) + const task = checkGitConnection(parsed, dir, { timeoutMs: 3000, outputBytes: 4096, signal: controller.signal, env }) + tasks.push(task) + return task + } + return { dir, config, controller, check, checkAny, env } } /** The exact command line a check runs for one repository URL. */ @@ -136,7 +143,7 @@ it.each([undefined, 'Git could not start'])('retains a diagnostic when Git write onTestFinished(() => { run.mockRestore() }) const result = await check('github:acme/plugin') expect(result).toMatchObject({ - exitCode: 1, kind: 'unknown', output: shortMessage ?? 'GitHub connection check failed', + exitCode: 1, kind: 'unknown', output: shortMessage ?? 'Git connection check failed', argv: checkArgv('https://github.com/acme/plugin'), }) expect(await readFile(result!.logPath, 'utf8')).toBe(result!.output) @@ -211,3 +218,16 @@ it('does not invoke configured credential helpers or askpass for an authenticati expect(await check('github:acme/private.git')).toMatchObject({ exitCode: 128, kind: 'unknown' }) await expect(readFile(marker)).rejects.toMatchObject({ code: 'ENOENT' }) }) + +it.each([ + ['gitlab:acme/plugin#main', 'https://gitlab.com/'], + ['bitbucket:acme/plugin#main', 'https://bitbucket.org/'], + [`git+https://git.example/acme/plugin.git#${'a'.repeat(40)}&path:/packages/a`, 'https://git.example/'], + ['git+ssh://git@git.example/acme/plugin.git#main', 'ssh://git@git.example/'], +])('checks the repository of %s on any host', async (spec, prefix) => { + const { checkAny, dir, config } = await fixture() + const repository = join(dir, 'empty.git') + await subprocess.execa('git', ['init', '--bare', repository]) + await writeFile(config, `[url "${pathToFileURL(repository).href}"]\n insteadOf = ${prefix}acme/plugin.git\n insteadOf = ${prefix}acme/plugin\n`) + expect(await checkAny(spec)).toBeUndefined() +}) diff --git a/packages/boot/plugin-manager/tests/install-spec.spec.ts b/packages/boot/plugin-manager/tests/install-spec.spec.ts index 26f591cc6c..f77ce658e0 100644 --- a/packages/boot/plugin-manager/tests/install-spec.spec.ts +++ b/packages/boot/plugin-manager/tests/install-spec.spec.ts @@ -1,7 +1,7 @@ /** Reading install specs and classifying pnpm failures: pure, table-driven. */ import { describe, expect, it } from 'vitest' -import { classifyInstallFailure, InvalidInstallSpecError, parseInstallSpec } from '@deepseek-ai/dsh-plugin-manager' +import { classifyInstallFailure, InvalidInstallSpecError, parseInstallSpec, pinnedGitOrigin } from '@deepseek-ai/dsh-plugin-manager' describe('parseInstallSpec', () => { it('reads registry names with an optional range, scoped or not', () => { @@ -82,3 +82,32 @@ describe('classifyInstallFailure', () => { expect(classifyInstallFailure({ log: '' })).toBe('unknown') }) }) + +describe('pinnedGitOrigin', () => { + const COMMIT = '0123456789abcdef0123456789abcdef01234567' + + it('reads the repository directory of a Git spec pinned to a full commit, whatever form pnpm saved', () => { + for (const spec of [ + `git+https://GitLab.Example.com/g/plugins.git#${COMMIT}&path:/packages/a`, + `git+ssh://git@gitlab.example.com/g/plugins.git#path:/packages/a/&${COMMIT.toUpperCase()}`, + `git@gitlab.example.com:g/plugins.git/#${COMMIT}&path:packages/a`, + ]) { + expect(pinnedGitOrigin(spec)).toBe('gitlab.example.com/g/plugins#packages/a') + } + // pnpm saves a known host's shortcut for the URL it was given. + expect(pinnedGitOrigin(`github:o/r#${COMMIT}&path:/p`)).toBe(pinnedGitOrigin(`git+https://github.com/o/r.git#${COMMIT}&path:/p`)) + expect(pinnedGitOrigin(`https://github.com/a/b#${COMMIT}`)).toBe('github.com/a/b#') + // A port is part of the host, not an scp-like path separator. + expect(pinnedGitOrigin(`git+ssh://git@host:2222/a/b.git#${COMMIT}`)).toBe('host:2222/a/b#') + }) + + it('reads nothing from a spec that is not Git or names no single full commit and directory', () => { + for (const spec of [ + '', 'dsh-x@1.0.0', '/plugins/dsh-x', 'git+https://host/a/b.git', 'git+https://host/a/b.git#main&path:/a', + `git+https://host/a/b.git#${COMMIT.slice(1)}`, `git+https://host/a/b.git#${COMMIT}&path:/a&path:/b`, + `git+https://host/a/b.git#${COMMIT}&${COMMIT}`, + ]) { + expect(pinnedGitOrigin(spec)).toBeUndefined() + } + }) +}) diff --git a/packages/boot/plugin-manager/tests/manager.spec.ts b/packages/boot/plugin-manager/tests/manager.spec.ts index 8187c45a9b..ae71a3db8f 100644 --- a/packages/boot/plugin-manager/tests/manager.spec.ts +++ b/packages/boot/plugin-manager/tests/manager.spec.ts @@ -81,7 +81,8 @@ async function fixture(reload: 'live' | 'startup' = 'live', overlay = false, pre // What pnpm's own configuration names is read from pnpm before every registry plan; the fixture answers npm's own registry. const registry = vi.spyOn(operations, 'readProfileRegistry').mockResolvedValue('https://registry.npmjs.org/') const connection = vi.spyOn(githubConnection, 'checkGithubConnection').mockResolvedValue(undefined) - onTestFinished(() => { registry.mockRestore(); connection.mockRestore() }) + const gitConnection = vi.spyOn(githubConnection, 'checkGitConnection').mockResolvedValue(undefined) + onTestFinished(() => { registry.mockRestore(); connection.mockRestore(); gitConnection.mockRestore() }) let stopHmr = async () => {} if (reload === 'live') { await ctx.plugin(Timer) @@ -89,7 +90,7 @@ async function fixture(reload: 'live' | 'startup' = 'live', overlay = false, pre stopHmr = () => owner.dispose() await ctx.hmr.runExclusive(async () => {}) } - return { ctx, dir, manager: ctx.pluginManager, bundle, profile, stopHmr, overlays, connection } + return { ctx, dir, manager: ctx.pluginManager, bundle, profile, stopHmr, overlays, connection, gitConnection } } it.each(['live', 'startup'] as const)('refuses an unsupported bundle before saving or applying it with %s reload', async (reload) => { @@ -414,12 +415,12 @@ it('lists bundle versions and current-profile plugin targets', async () => { expect(plugins.find(row => row.entryId === 'include:manager')?.readOnlyReason).toBe('management-required') expect((await manager.listBundles()).filter(bundle => bundle.installTarget === undefined)).toEqual([ { - name: 'core', version: '1.0.0', enabled: true, installed: false, official: false, availability: 'profile', optional: false, removable: false, updatable: false, readOnlyReason: 'management-required', + name: 'core', version: '1.0.0', enabled: true, installed: false, official: false, availability: 'profile', optional: false, removable: false, readOnlyReason: 'management-required', meta: { title: 'core' }, rows: [{ rowId: 'manager', moduleName: 'cordis:manager', entryId: 'include:manager' }], overrides: [], }, { - name: 'extra', version: '1.0.0', source: 'extra@1.0.0', enabled: true, installed: true, official: false, availability: 'profile', optional: false, removable: true, updatable: true, + name: 'extra', version: '1.0.0', source: 'extra@1.0.0', enabled: true, installed: true, official: false, availability: 'profile', optional: false, removable: true, update: 'registry', meta: { title: 'extra' }, rows: [{ rowId: 'managed', moduleName: pathToFileURL(join(dir, 'node_modules', 'extra', 'plugin.mjs')).href, entryId: 'include:managed' }], overrides: [], }, @@ -441,7 +442,7 @@ it('describes a bundle by its manifest and patch: one-liner, rows without a live writeFileSync(join(dir, 'package.json'), JSON.stringify(manifest)) const moduleName = pathToFileURL(join(dir, 'node_modules', 'described', 'plugin.mjs')).href expect((await manager.listBundles()).find(row => row.name === 'described')).toEqual({ - name: 'described', version: '2.0.0', description: 'Describes itself.', source: 'described@2.0.0', enabled: false, installed: true, official: false, availability: 'profile', optional: false, removable: true, updatable: true, + name: 'described', version: '2.0.0', description: 'Describes itself.', source: 'described@2.0.0', enabled: false, installed: true, official: false, availability: 'profile', optional: false, removable: true, update: 'registry', meta: { title: 'described', description: 'Describes itself.' }, rows: [{ rowId: 'described-row', moduleName }], overrides: ['managed'], }) @@ -584,7 +585,7 @@ it.each([ writeFileSync(join(dir, 'package.json'), JSON.stringify(manifest)) expect((await manager.listBundles()).find(row => row.name === 'unnamed')).toEqual({ - name: 'unnamed', version: '1.0.0', source: 'unnamed@1.0.0', enabled: false, installed: true, official: false, availability: 'profile', optional: false, removable: true, updatable: true, + name: 'unnamed', version: '1.0.0', source: 'unnamed@1.0.0', enabled: false, installed: true, official: false, availability: 'profile', optional: false, removable: true, update: 'registry', rows: [], overrides: [], ...expected, }) }) @@ -1420,7 +1421,7 @@ it.each(OPTIONAL_BUNDLES)('offers %s switched off and never removable', async (o expect((await manager.listBundles()).find(row => row.name === offered)).toEqual({ name: offered, version: '3.0.0', description: 'Package one-liner.', meta: { title: offered, description: 'Package one-liner.' }, - enabled: false, installed: false, official: true, availability: 'installation', optional: true, removable: false, updatable: false, + enabled: false, installed: false, official: true, availability: 'installation', optional: true, removable: false, rows: [{ rowId: 'offered-row', moduleName: pathToFileURL(join(supplied, 'plugin.mjs')).href }], overrides: [], }) expect(await manager.setBundleEnabled(offered, true)).toMatchObject({ application: 'applied' }) @@ -1810,7 +1811,7 @@ it('lists absent Official entries offline without resolving their packages or co expect(bundles.filter(bundle => bundle.name === entry.packageName)).toHaveLength(1) expect(bundles.filter(bundle => bundle.name === entry.packageName)).toMatchObject([{ name: entry.packageName, official: true, availability: 'missing', enabled: false, installed: false, optional: false, - removable: false, updatable: false, rows: [], overrides: [], meta: entry.meta, + removable: false, rows: [], overrides: [], meta: entry.meta, ...entry.platforms !== undefined && !entry.platforms.includes(process.platform) ? { listedByDefault: false, error: { code: 'unsupported-platform' } } : {}, installTarget: { spec: `${entry.packageName}@${getDshRuntimeVersion()}`, version: getDshRuntimeVersion() }, @@ -2093,17 +2094,20 @@ it('refuses to list versions of a package not installed from a registry', async const manifest = readProfileManifest('test', dir) manifest.dependencies = { ...manifest.dependencies, linked: `link:${join(dir, 'node_modules', 'linked')}` } writeFileSync(join(dir, 'package.json'), JSON.stringify(manifest)) - expect(await manager.listVersions('linked')).toEqual(refusal('linked is not installed from a registry under its own name')) + expect(await manager.listVersions('linked')).toEqual(refusal('linked is installed neither from a registry under its own name nor from a Git commit')) // An alias saves another package under the dependency's name; its versions are not the alias's. const aliased = readProfileManifest('test', dir) aliased.dependencies = { ...aliased.dependencies, linked: 'npm:other-plugin@1.0.0' } writeFileSync(join(dir, 'package.json'), JSON.stringify(aliased)) - expect(await manager.listVersions('linked')).toEqual(refusal('linked is not installed from a registry under its own name')) + expect(await manager.listVersions('linked')).toEqual(refusal('linked is installed neither from a registry under its own name nor from a Git commit')) writeFileSync(join(dir, 'node_modules', 'extra', 'package.json'), JSON.stringify({ name: 'extra', version: 'nightly', dsh: BUNDLE })) expect(await manager.listVersions('extra')).toEqual(refusal('extra declares no semver version')) // A bundle carrying plugin management cannot be removed, so it is not offered for an update. + writeFileSync(join(dir, 'node_modules', 'extra', 'package.json'), JSON.stringify({ name: 'extra', version: '1.0.0', dsh: BUNDLE })) writeFileSync(join(dir, 'node_modules', 'extra', 'cordis.patch.yml'), JSON.stringify([{ insert: [{ id: 'pm', name: '@deepseek-ai/dsh-plugin-manager' }] }])) - expect((await manager.listBundles()).find(item => item.name === 'extra')).toMatchObject({ readOnlyReason: 'management-required', updatable: false }) + const managed = (await manager.listBundles()).find(item => item.name === 'extra') + expect(managed).toMatchObject({ readOnlyReason: 'management-required' }) + expect(managed).not.toHaveProperty('update') expect(await manager.listVersions('extra')).toEqual(refusal('extra carries plugin management and cannot be removed')) expect(view).not.toHaveBeenCalled() }) @@ -2143,6 +2147,8 @@ it('updates a registry bundle by removing it and installing the exact newer vers expect(updated.restore).toBeUndefined() expect(steps).toEqual(['checking', 'removing', 'installing']) expect(pnpm.mock.calls.map(call => call[1])).toEqual([['remove', 'extra'], ['add', 'extra@1.2.0', '--save-exact', `--registry=${MIRROR}`]]) + // The run itself is undone unless it installs this package at this version. + expect(pnpm.mock.calls[1]?.[2]).toMatchObject({ expected: { name: 'extra', version: '1.2.0' } }) expect(readProfileManifest('test', dir).dependencies?.extra).toBe('1.2.0') expect(ctx.get('managedProbe')).toBe(true) }) @@ -2318,3 +2324,193 @@ it('omits exemption identity when only the runtime version is invalid', async () expect(fact.version).toBeUndefined() } }) + +const GIT_REPOSITORY = 'git+https://gitlab.example/g/plugins.git' +/** A Git spec of the `extra` directory pinned to one commit. */ +const gitSpec = (commit: string): string => `${GIT_REPOSITORY}#${commit.repeat(40)}&path:/packages/extra` +const GIT_A = gitSpec('a') +const GIT_B = gitSpec('b') + +/** Save the fixture's `extra` 1.0.0 as installed from `spec`. */ +function installedFromGit(dir: string, spec = GIT_A): void { + const manifest = readProfileManifest('test', dir) + manifest.dependencies = { ...manifest.dependencies, extra: spec } + writeFileSync(join(dir, 'package.json'), JSON.stringify(manifest)) +} + +/** + * A pnpm stand-in that removes the package a `remove` names and installs the package `installs` maps a Git spec to. A run + * whose package differs from the one the caller expects, or declares no bundle, is refused without changes, as + * `runProfilePnpm` undoes it. + */ +function gitPnpm( + dir: string, bundle: (name: string, rows: unknown[]) => void, + installs: Readonly>, +) { + const pnpm = vi.spyOn(operations, 'runProfilePnpm').mockImplementation(async (_context, args, options) => { + const manifest = readProfileManifest('test', dir) + const spec = String(args[1]) + const installed = installs[spec] + if (args[0] === 'remove') { + delete manifest.dependencies?.[spec] + rmSync(join(dir, 'node_modules', spec), { recursive: true, force: true }) + } else if (installed === undefined) { + return { exitCode: 1, output: `ERR_PNPM_GIT_CHECKOUT_FAILED ${spec}`, truncated: false, logPath: '/install.log', kind: 'unknown' as const } + } else if (options.expected !== undefined && `${installed.name}@${installed.version}` !== `${options.expected.name}@${options.expected.version}`) { + const found = `${installed.name}@${installed.version}` + const output = `dsh: installation rejected: ${found} is installed where ${options.expected.name}@${options.expected.version} was expected` + return { exitCode: 1, output, truncated: false, logPath: '/install.log', kind: 'unknown' as const, unmet: 'unexpected-package' as const } + } else if (options.expected !== undefined && installed.bundle === false) { + const output = `dsh: installation rejected: ${installed.name}@${installed.version} declares no bundle` + return { exitCode: 1, output, truncated: false, logPath: '/install.log', kind: 'unknown' as const, unmet: 'not-bundle' as const } + } else { + bundle(installed.name, [{ id: `${installed.name}-row`, name: './plugin.mjs', config: { service: `${installed.name}Probe` } }]) + writeFileSync(join(dir, 'node_modules', installed.name, 'package.json'), JSON.stringify({ name: installed.name, version: installed.version, dsh: BUNDLE })) + manifest.dependencies = { ...manifest.dependencies, [installed.name]: spec } + } + writeFileSync(join(dir, 'package.json'), JSON.stringify(manifest)) + return { exitCode: 0, output: '', truncated: false, logPath: '/install.log' } + }) + onTestFinished(() => { pnpm.mockRestore() }) + return pnpm +} + +it('lists a copy pinned to a Git commit for an update and refuses to list its versions', async () => { + const { manager, dir } = await fixture() + const view = viewAnswers(extraRegistry) + installedFromGit(dir) + expect((await manager.listBundles()).find(item => item.name === 'extra')).toMatchObject({ source: GIT_A, update: 'git' }) + expect(await manager.listVersions('extra')).toEqual({ + status: 'refused', problem: 'not-updatable', reason: 'extra is installed from Git, which lists no versions', attempts: [], + }) + // A branch moves, so reinstalling it would not bring back the removed version. + installedFromGit(dir, `${GIT_REPOSITORY}#main&path:/packages/extra`) + expect((await manager.listBundles()).find(item => item.name === 'extra')?.update).toBeUndefined() + expect((await manager.updateBundle('extra', '1.1.0', { spec: GIT_B })).result.error).toEqual({ + code: 'invalid-spec', diagnostic: 'extra is installed neither from a registry under its own name nor from a Git commit', + }) + expect(view).not.toHaveBeenCalled() +}) + +it('updates a Git copy by removing it and installing the expected package from another commit with its selection', async () => { + const { ctx, manager, dir, bundle } = await fixture() + const view = viewAnswers(extraRegistry) + installedFromGit(dir) + const pnpm = gitPnpm(dir, bundle, { [GIT_B]: { name: 'extra', version: '1.1.0' } }) + const steps: string[] = [] + ctx.on('plugin-manager/update-state', ({ step }) => { steps.push(step) }) + const requestId = '0b6f3c2a-9d8e-4f71-a5b4-3c2d1e0f9a8b' as PluginInstallRequestId + const updated = await manager.updateBundle('extra', '1.1.0', { spec: GIT_B, requestId }) + expect(updated).toMatchObject({ from: '1.0.0', to: '1.1.0', step: 'install', result: { application: 'applied', bundle: 'extra', version: '1.1.0', enabled: true } }) + expect(updated.restore).toBeUndefined() + expect(steps).toEqual(['checking', 'removing', 'installing']) + // The Git target names its own commit, so nothing is viewed on a registry and the save is not exact. + expect(pnpm.mock.calls.map(call => call[1])).toEqual([['remove', 'extra'], ['add', GIT_B]]) + expect(pnpm.mock.calls[1]?.[2]).toMatchObject({ expected: { name: 'extra', version: '1.1.0' } }) + expect(view).not.toHaveBeenCalled() + expect(readProfileManifest('test', dir).dependencies?.extra).toBe(GIT_B) + expect(ctx.get('extraProbe')).toBe(true) +}) + +it.each([ + ['another package', { name: 'other', version: '1.1.0' }], + ['another version', { name: 'extra', version: '1.0.5' }], +] as const)('installs the removed Git copy again from its saved spec when the target installs %s', async (_name, installed) => { + const { manager, dir, bundle } = await fixture() + installedFromGit(dir) + const pnpm = gitPnpm(dir, bundle, { [GIT_A]: { name: 'extra', version: '1.0.0' }, [GIT_B]: installed }) + const updated = await manager.updateBundle('extra', '1.1.0', { spec: GIT_B }) + expect(updated).toMatchObject({ + step: 'install', restore: { application: 'applied', version: '1.0.0' }, + result: { + application: 'failed', error: { code: 'unexpected-package' }, + packageResult: { unmet: 'unexpected-package', output: expect.stringContaining(`${installed.name}@${installed.version} is installed`) as string }, + }, + }) + expect(pnpm.mock.calls.map(call => call[1])).toEqual([['remove', 'extra'], ['add', GIT_B], ['add', GIT_A]]) + expect(readProfileManifest('test', dir).dependencies).toEqual({ extra: GIT_A }) +}) + +it('updates a copy pnpm saved as a known host\'s shortcut to the HTTPS spec of another commit', async () => { + const { manager, dir, bundle } = await fixture() + const target = `git+https://github.com/o/plugins.git#${'b'.repeat(40)}&path:/packages/extra` + installedFromGit(dir, `github:o/plugins#${'a'.repeat(40)}&path:/packages/extra`) + gitPnpm(dir, bundle, { [target]: { name: 'extra', version: '1.1.0' } }) + expect(await manager.updateBundle('extra', '1.1.0', { spec: target })).toMatchObject({ step: 'install', result: { application: 'applied', version: '1.1.0' } }) +}) + +it('installs a Git target over the running version without HMR and reports the restart', async () => { + const { manager, dir, bundle, gitConnection } = await fixture('startup') + installedFromGit(dir) + const pnpm = gitPnpm(dir, bundle, { [GIT_B]: { name: 'extra', version: '1.1.0' } }) + expect(await manager.updateBundle('extra', '1.1.0', { spec: GIT_B })).toMatchObject({ + step: 'install', result: { application: 'restart-required', restartReason: 'no-hmr', version: '1.1.0' }, + }) + expect(pnpm.mock.calls.map(call => call[1])).toEqual([['add', GIT_B]]) + // Without HMR nothing is removed, so the repository is left to the installation. + expect(gitConnection).not.toHaveBeenCalled() +}) + +it('keeps the running Git copy without HMR when the target declares no bundle', async () => { + const { manager, dir, bundle } = await fixture('startup') + installedFromGit(dir) + const before = readFileSync(join(dir, 'package.json'), 'utf8') + gitPnpm(dir, bundle, { [GIT_B]: { name: 'extra', version: '1.1.0', bundle: false } }) + expect(await manager.updateBundle('extra', '1.1.0', { spec: GIT_B })).toMatchObject({ + step: 'install', result: { application: 'failed', error: { code: 'not-bundle' }, packageResult: { unmet: 'not-bundle' } }, + }) + expect(readFileSync(join(dir, 'package.json'), 'utf8')).toBe(before) +}) + +it.each(['network', 'timeout'] as const)('keeps a Git copy when a %s stops reaching the target repository before its removal', async (kind) => { + const env = { GIT_CONFIG_GLOBAL: '/application/git.config' } + const { manager, dir, bundle, gitConnection } = await fixture('live', false, undefined, {}, { command: 'pnpm', args: [], env }) + installedFromGit(dir) + const before = readFileSync(join(dir, 'package.json'), 'utf8') + const pnpm = gitPnpm(dir, bundle, { [GIT_B]: { name: 'extra', version: '1.1.0' } }) + const failure = { exitCode: 128, output: 'fatal: unable to access', truncated: false, logPath: join(dir, 'git.log'), kind } + // The command line the check ran stays out of the answer. + gitConnection.mockResolvedValue({ ...failure, argv: ['git', 'ls-remote', '--', 'https://gitlab.example/g/plugins.git', 'HEAD'] }) + expect(await manager.updateBundle('extra', '1.1.0', { spec: GIT_B })).toEqual({ + from: '1.0.0', to: '1.1.0', step: 'check', + result: { + changed: false, application: 'failed', stage: 'install', target: 'extra', failedAt: 'spec-host', packageResult: failure, + error: { code: 'operation-error', diagnostic: 'fatal: unable to access' }, + }, + }) + expect(gitConnection).toHaveBeenCalledWith(expect.objectContaining({ kind: 'git', spec: GIT_B }), dir, expect.objectContaining({ timeoutMs: 5000, env })) + expect(pnpm).not.toHaveBeenCalled() + expect(readFileSync(join(dir, 'package.json'), 'utf8')).toBe(before) +}) + +it('leaves a Git repository check that is not a connection failure to pnpm', async () => { + const { manager, dir, bundle, gitConnection } = await fixture() + installedFromGit(dir) + gitPnpm(dir, bundle, { [GIT_B]: { name: 'extra', version: '1.1.0' } }) + gitConnection.mockResolvedValue({ + exitCode: 127, output: 'spawn git ENOENT', truncated: false, logPath: join(dir, 'git.log'), kind: 'unknown', argv: ['git'], + }) + expect(await manager.updateBundle('extra', '1.1.0', { spec: GIT_B })).toMatchObject({ step: 'install', result: { application: 'applied' } }) +}) + +it('refuses an update whose target does not match where the copy was installed from, before changing anything', async () => { + const { manager, dir, bundle } = await fixture() + const view = viewAnswers(extraRegistry) + const pnpm = gitPnpm(dir, bundle, {}) + const refused = async (version: string, spec?: string) => (await manager.updateBundle('extra', version, spec === undefined ? {} : { spec })).result.error + expect(await refused('1.2.0', GIT_B)).toEqual({ code: 'invalid-spec', diagnostic: 'extra is installed from a registry and updates only to extra@1.2.0' }) + installedFromGit(dir) + const before = readFileSync(join(dir, 'package.json'), 'utf8') + const pinnedOnly = { code: 'invalid-spec', diagnostic: 'extra is installed from Git and updates only to a Git spec pinned to a full commit' } + expect(await refused('1.1.0')).toEqual(pinnedOnly) + expect(await refused('1.1.0', 'extra@1.1.0')).toEqual(pinnedOnly) + expect(await refused('1.1.0', `${GIT_REPOSITORY}#main&path:/packages/extra`)).toEqual(pinnedOnly) + const sibling = `${GIT_REPOSITORY}#${'b'.repeat(40)}&path:/packages/other` + expect(await refused('1.1.0', sibling)).toEqual({ + code: 'invalid-spec', diagnostic: `${sibling} is not in the repository directory extra is installed from, gitlab.example/g/plugins#packages/extra`, + }) + expect(await refused('0.9.0', GIT_B)).toEqual({ code: 'invalid-spec', diagnostic: '0.9.0 is not a version newer than the installed 1.0.0' }) + expect(pnpm).not.toHaveBeenCalled() + expect(view).not.toHaveBeenCalled() + expect(readFileSync(join(dir, 'package.json'), 'utf8')).toBe(before) +}) diff --git a/packages/boot/plugin-manager/tests/operations.spec.ts b/packages/boot/plugin-manager/tests/operations.spec.ts index b6a87a6475..3f1a30d226 100644 --- a/packages/boot/plugin-manager/tests/operations.spec.ts +++ b/packages/boot/plugin-manager/tests/operations.spec.ts @@ -412,6 +412,102 @@ it.each([true, false])('rejects incompatible installed manifests before activati expect(outcome.output).toContain('and node_modules') }) +it.each([ + ['another package', 'other', '1.1.0'], + ['another version', 'extra', '1.0.5'], +] as const)('undoes a run that installs %s than the caller expects, with its modules', async (_name, name, version) => { + const { dir, context, pnpm } = fixture() + const manifestBefore = readFileSync(join(dir, 'package.json'), 'utf8') + writeFileSync(join(dir, 'pnpm-lock.yaml'), 'original-lock\n') + pnpm.mutate = (target) => { + installGuarded(target, name, name, version, '*') + writeFileSync(join(target, 'pnpm-lock.yaml'), 'changed-lock\n') + } + const outcome = await runProfilePnpm(context, ['add', `git+https://host.example/g/r.git#${'b'.repeat(40)}`], { + execution: 'service', outputBytes: 8192, activateNewBundles: false, expected: { name: 'extra', version: '1.1.0' }, + }) + expect(outcome.exitCode).toBe(1) + expect(outcome.incompatible).toBeUndefined() + expect(outcome.unmet).toBe('unexpected-package') + expect(outcome.output).toContain(`${name}@${version} is installed where extra@1.1.0 was expected`) + expect(readFileSync(join(dir, 'package.json'), 'utf8')).toBe(manifestBefore) + expect(readFileSync(join(dir, 'pnpm-lock.yaml'), 'utf8')).toBe('original-lock\n') + expect(command.run).toHaveBeenLastCalledWith(expect.anything(), ['install', '--frozen-lockfile'], expect.anything()) +}) + +it('undoes a run that leaves the expected package at another version without changing it', async () => { + const { dir, context, pnpm } = fixture() + // Adding the saved spec again leaves the dependency as it was. + installGuarded(dir, 'extra', 'extra', '1.0.0', '*') + pnpm.mutate = () => {} + const outcome = await runProfilePnpm(context, ['add', 'extra@1'], { + execution: 'service', outputBytes: 8192, activateNewBundles: false, expected: { name: 'extra', version: '1.1.0' }, + }) + expect(outcome).toMatchObject({ exitCode: 1, unmet: 'unexpected-package' }) + expect(outcome.output).toContain('extra@1.0.0 is installed where extra@1.1.0 was expected') +}) + +it('undoes a run whose expected package declares no bundle, with its modules', async () => { + const { dir, context, pnpm } = fixture() + const manifestBefore = readFileSync(join(dir, 'package.json'), 'utf8') + writeFileSync(join(dir, 'pnpm-lock.yaml'), 'original-lock\n') + pnpm.mutate = (target) => { + install(target, 'extra') + writeFileSync(join(target, 'node_modules', 'extra', 'package.json'), JSON.stringify({ name: 'extra', version: '1.1.0' })) + } + const outcome = await runProfilePnpm(context, ['add', `git+https://host.example/g/r.git#${'b'.repeat(40)}`], { + execution: 'service', outputBytes: 8192, activateNewBundles: false, expected: { name: 'extra', version: '1.1.0' }, + }) + expect(outcome).toMatchObject({ exitCode: 1, unmet: 'not-bundle' }) + expect(outcome.output).toContain('extra@1.1.0 declares no bundle') + expect(readFileSync(join(dir, 'package.json'), 'utf8')).toBe(manifestBefore) + expect(command.run).toHaveBeenLastCalledWith(expect.anything(), ['install', '--frozen-lockfile'], expect.anything()) +}) + +it('undoes a run whose expected bundle patch cannot load, with its modules', async () => { + const { dir, context, pnpm } = fixture() + const manifestBefore = readFileSync(join(dir, 'package.json'), 'utf8') + writeFileSync(join(dir, 'pnpm-lock.yaml'), 'original-lock\n') + pnpm.mutate = (target) => { + install(target, 'extra') + writeFileSync(join(target, 'node_modules', 'extra', 'package.json'), JSON.stringify({ name: 'extra', version: '1.1.0', dsh: { bundle: { patch: './cordis.patch.yml' } } })) + writeFileSync(join(target, 'node_modules', 'extra', 'cordis.patch.yml'), 'insert: [\n') + } + const outcome = await runProfilePnpm(context, ['add', `git+https://host.example/g/r.git#${'b'.repeat(40)}`], { + execution: 'service', outputBytes: 8192, activateNewBundles: false, expected: { name: 'extra', version: '1.1.0' }, + }) + expect(outcome.exitCode).toBe(1) + expect(outcome.unmet).toBeUndefined() + expect(outcome.output).toContain('Cannot validate installed package extra') + expect(readFileSync(join(dir, 'package.json'), 'utf8')).toBe(manifestBefore) + expect(command.run).toHaveBeenLastCalledWith(expect.anything(), ['install', '--frozen-lockfile'], expect.anything()) +}) + +it('reports another package over a missing bundle declaration, and a run that installs nothing as unexpected', async () => { + const { context, pnpm } = fixture() + const options = { execution: 'service', outputBytes: 8192, activateNewBundles: false, expected: { name: 'extra', version: '1.1.0' } } as const + pnpm.mutate = (target) => { + install(target, 'other') + install(target, 'extra') + writeFileSync(join(target, 'node_modules', 'extra', 'package.json'), JSON.stringify({ name: 'extra', version: '1.1.0' })) + } + expect(await runProfilePnpm(context, ['add', 'extra@1.1.0'], options)).toMatchObject({ exitCode: 1, unmet: 'unexpected-package' }) + pnpm.mutate = () => {} + const empty = await runProfilePnpm(context, ['add', 'extra@1.1.0'], options) + expect(empty).toMatchObject({ exitCode: 1, unmet: 'unexpected-package' }) + expect(empty.output).toContain('extra@1.1.0 is not installed') +}) + +it('accepts a run that installs the package the caller expects', async () => { + const { context, pnpm } = fixture() + pnpm.mutate = (target) => { installGuarded(target, 'extra', 'extra', '1.1.0', '*') } + const outcome = await runProfilePnpm(context, ['add', `git+https://host.example/g/r.git#${'b'.repeat(40)}`], { + execution: 'service', outputBytes: 8192, activateNewBundles: false, expected: { name: 'extra', version: '1.1.0' }, + }) + expect(outcome).toMatchObject({ exitCode: 0 }) + expect(command.run).toHaveBeenCalledOnce() +}) + it('repairs a profile that had no lockfile without creating one', async () => { const { dir, context, pnpm } = fixture() pnpm.mutate = (target) => { diff --git a/packages/boot/plugin-manager/tests/tools.spec.ts b/packages/boot/plugin-manager/tests/tools.spec.ts index a9a5978899..ade243b05a 100644 --- a/packages/boot/plugin-manager/tests/tools.spec.ts +++ b/packages/boot/plugin-manager/tests/tools.spec.ts @@ -89,7 +89,7 @@ it('exposes an absent Official bundle installation target without display metada const { manager, call } = await fixture() const offered: BundleInfo = { name: '@deepseek-ai/dsh-subagent-codex', official: true, availability: 'missing', - installed: false, enabled: false, optional: false, removable: false, updatable: false, rows: [], overrides: [], + installed: false, enabled: false, optional: false, removable: false, rows: [], overrides: [], installTarget: { spec: '@deepseek-ai/dsh-subagent-codex@1.2.3', version: '1.2.3' }, meta: { title: 'Codex subagent' }, } diff --git a/packages/boot/plugin-market/README.i18n.yaml b/packages/boot/plugin-market/README.i18n.yaml index e8352139a5..2adde60540 100644 --- a/packages/boot/plugin-market/README.i18n.yaml +++ b/packages/boot/plugin-market/README.i18n.yaml @@ -21,8 +21,8 @@ en: b5d96ca29cf76f31 zh: e5099b4f694cf4c9 /deepseek-ai-dsh-plugin-market/understand-the-implementation: - en: 678705f57d986377 - zh: 68d090edfe04acde + en: cb56b9191fc680a1 + zh: eee0a1dfee9ea5bb /deepseek-ai-dsh-plugin-market/model-experience: en: 215e7ba838619b7b zh: a311da3843709f90 @@ -39,8 +39,8 @@ en: 02cf6033092eb236 zh: cb928bfa6d39a7f7 /deepseek-ai-dsh-plugin-market/known-limitations-and-deferred-work: - en: 54122c96969ebbc2 - zh: 3fbe84bc6069cb46 + en: a5f8aad8e45b333d + zh: bb84782c37db2574 /deepseek-ai-dsh-plugin-market/known-limitations-and-deferred-work/dev-note: en: e67f633dc6fe7773 zh: 3bb0c20c93f16f98 diff --git a/packages/boot/plugin-market/README.md b/packages/boot/plugin-market/README.md index cdd9b211c2..255ea7fc2c 100644 --- a/packages/boot/plugin-market/README.md +++ b/packages/boot/plugin-market/README.md @@ -93,7 +93,7 @@ Concurrent reads of one URL share one fetch. A successful document is cached for Filtering and ordering read only the document; installation status and compatibility are computed for the returned page on every call. The first query with text in a document builds its text index with [MiniSearch](https://github.com/lucaong/minisearch), 500 plugins per event-loop turn, and later queries reuse it until the document is replaced. -Compatibility is computed per version with `evaluatePluginCompatibility`, the version's `engines.dsh`, and the profile's version exemptions, the same check installation applies. A plugin's default version is its highest compatible version without a prerelease tag, or its highest compatible prerelease when it lists no version without one; a plugin with no compatible version has none. Installation status comes from `pluginManager.listBundles()` by package name, measured against the default version, or the highest version without one: `not-installed`, also for an on-demand catalog entry the profile has not installed; `installed` at that version; `update-available` when that version is greater; `different-version` when the installed version is unknown, not valid SemVer, or greater; and `provided` for a bundle the DSH installation ships. +Compatibility is computed per version with `evaluatePluginCompatibility`, the version's `engines.dsh`, and the profile's version exemptions, the same check installation applies. A plugin's default version is its highest compatible version without a prerelease tag, or its highest compatible prerelease when it lists no version without one; a plugin with no compatible version has none. Installation status comes from `pluginManager.listBundles()` by package name, measured against the default version, or the highest version without one: `not-installed`, also for an on-demand catalog entry the profile has not installed; `installed` at that version; `update-available` when that version is greater; `different-version` when the installed version is unknown, not valid SemVer, or greater; and `provided` for a bundle the DSH installation ships. A version's `update` is true when it is newer than a copy `listBundles` marks `update: 'git'` and pins a commit of the repository and directory that copy pins, so the Plugins page can replace that copy with it through `pluginManager.updateBundle`; a registry copy's newer versions come from its registry instead. Entry detail reads share one reader that the service owns; document selection and the npm file CDNs come from [`dsh-plugin-detail`](../../util/plugin-detail/README.md). Each partial clone is a bare repository under `detailCacheDir`, named by a hash of its URL, initialized in a staging directory and renamed into place, and fetched with `--depth 1 --filter=blob:none` so only commits and trees arrive; Git downloads a file's contents when it is read. Commands run without inherited `GIT_*` variables other than the ssh program, with hooks, the file-system monitor, submodule recursion, and every transport but https, http, and ssh disabled, and no working tree is created. The directory size is checked every 200 ms while Git downloads; growth past `detailRepositoryMaxBytes` within one command stops Git, and output keeps a file's final newline byte. A file read stops once its contents pass the read's limit, and an image read completes even when the browser abandons the request, so leaving a page never interrupts a clone. A ref under `refs/dsh/` records each fetched commit, so a commit is never fetched lazily without the size check. Operations on one repository run one at a time, also across processes that share `detailCacheDir`, such as Web and Desktop on one profile, through a lock file beside the repository; eviction deletes a repository only after them, enforces `detailCacheMaxBytes` after every fetch and every file read, and deletes the repository in use last, after its operation, when it alone exceeds the total; one that fails or is cancelled, for any reason but a file over its read's limit, deletes the repository, so the next read starts from a fresh clone. Eviction also removes staging directories older than `detailGitTimeoutMs`. Disposal stops Git and waits for it. @@ -122,7 +122,7 @@ Append-only: results enter the conversation as ordinary tool results and do not -- **No upgrade** — an installed bundle with a newer index version is reported as `update-available`; upgrading remains uninstall followed by install. +- **Git updates only** — only a copy installed from a Git commit updates to a newer index version, and only within the repository directory it came from; a copy installed from a registry updates from its registry, and one installed from another source, such as a local directory, a branch, or another repository, is uninstalled and installed again. - **Unsigned indexes** — index content, including a Git entry's name and version, is the index's claim; the installed manifest decides what is installed. - **Last write wins** — `setIndexes` replaces the whole list, so two windows editing sources at the same time can overwrite each other's change. - **Redirects refused** — an index served only behind a redirect must be configured at its final URL. diff --git a/packages/boot/plugin-market/README.zh.md b/packages/boot/plugin-market/README.zh.md index ca213854bd..0fdf245efe 100644 --- a/packages/boot/plugin-market/README.zh.md +++ b/packages/boot/plugin-market/README.zh.md @@ -94,7 +94,7 @@ Host 使用进程的 `fetch` 获取,因此配置的出站代理生效,并拒 过滤和排序只读取文档;安装状态和兼容性在每次调用时只为返回的页计算。一份文档上第一次带文本的查询使用 [MiniSearch](https://github.com/lucaong/minisearch) 构建其文本索引,每轮事件循环处理 500 个插件,之后的查询复用该索引,直到文档被替换。 -兼容性按版本计算:使用 `evaluatePluginCompatibility`,传入该版本的 `engines.dsh` 和 profile 的版本豁免,与安装时执行的检查相同。插件的默认版本是其不带先行版本号的最高兼容版本;插件完全没有此类版本时,使用其最高兼容的先行版本;没有兼容版本的插件没有默认版本。安装状态按包名根据 `pluginManager.listBundles()` 计算,以默认版本为准,没有默认版本时以最高版本为准:`not-installed`(包括当前 profile 尚未安装的按需目录条目);版本相同时为 `installed`;该版本更高时为 `update-available`;已安装版本未知、不是有效的 SemVer 或更高时为 `different-version`;DSH 安装随附的组合包(bundle)为 `provided`。 +兼容性按版本计算:使用 `evaluatePluginCompatibility`,传入该版本的 `engines.dsh` 和 profile 的版本豁免,与安装时执行的检查相同。插件的默认版本是其不带先行版本号的最高兼容版本;插件完全没有此类版本时,使用其最高兼容的先行版本;没有兼容版本的插件没有默认版本。安装状态按包名根据 `pluginManager.listBundles()` 计算,以默认版本为准,没有默认版本时以最高版本为准:`not-installed`(包括当前 profile 尚未安装的按需目录条目);版本相同时为 `installed`;该版本更高时为 `update-available`;已安装版本未知、不是有效的 SemVer 或更高时为 `different-version`;DSH 安装随附的组合包(bundle)为 `provided`。版本比 `listBundles` 标为 `update: 'git'` 的副本新,且固定到该副本所固定的仓库与目录中的一个提交时,其 `update` 为 true,插件页因此可以通过 `pluginManager.updateBundle` 用它替换该副本;注册表副本的新版本则来自其注册表。 条目详细介绍的读取共用服务拥有的一个读取器;文档选择和 npm 文件 CDN 来自 [`dsh-plugin-detail`](../../util/plugin-detail/README.zh.md)。每个部分克隆是 `detailCacheDir` 下的一个裸仓库,以其 URL 的哈希命名,先在暂存目录中初始化,再重命名到目标位置,并使用 `--depth 1 --filter=blob:none` 获取,因此只下载提交和树对象;读取文件时 Git 才下载其内容。命令运行时不继承除 ssh 程序之外的 `GIT_*` 变量,禁用 hook、文件系统监视器、子模块递归,以及 https、http 和 ssh 之外的所有传输协议,且不创建工作树。Git 下载期间每 200 毫秒检查一次目录大小;单条命令执行期间增长超过 `detailRepositoryMaxBytes` 时停止 Git,且输出保留文件末尾的换行字节。文件内容超过本次读取的限制时,读取即停止;浏览器放弃请求时,图片读取仍会完成,因此离开页面不会中断克隆。`refs/dsh/` 下的 ref 记录每个已获取的提交,因此不会在没有大小检查的情况下延迟获取提交。对同一仓库的操作逐个执行,共享 `detailCacheDir` 的多个进程(例如同一 profile 上的 Web 和 Desktop)之间也是如此,由仓库旁的一个锁文件实现;淘汰只在这些操作结束后删除该仓库,并在每次获取和每次文件读取后执行 `detailCacheMaxBytes` 总量限制;正在使用的仓库单独超过该总量时,淘汰最后删除它,并等到其操作结束后再删除;除文件超过本次读取的限制外,任何原因导致操作失败或被取消时,都删除该仓库,使下一次读取从新的克隆开始。淘汰时还会删除存在时间超过 `detailGitTimeoutMs` 的暂存目录。销毁时停止 Git 并等待其退出。 @@ -123,7 +123,7 @@ Host 使用进程的 `fetch` 获取,因此配置的出站代理生效,并拒 -- **不支持升级**——索引版本更新的已安装组合包报告为 `update-available`;升级仍需先卸载再安装。 +- **只支持 Git 更新**——只有从 Git 提交安装的副本会更新到索引中较新的版本,且只在其来源的仓库目录内更新;从注册表安装的副本从其注册表更新,从其他来源(例如本地目录、分支或其他仓库)安装的副本需要先卸载再安装。 - **索引没有签名**——索引内容(包括 Git 条目的名称和版本)是索引的声明;实际安装什么由已安装的 manifest(元数据清单)决定。 - **后写入者覆盖**——`setIndexes` 会替换整个列表,因此两个窗口同时编辑索引源时,可能互相覆盖对方的修改。 - **拒绝重定向**——只能经重定向访问的索引必须以其最终 URL 配置。 diff --git a/packages/boot/plugin-market/src/index.ts b/packages/boot/plugin-market/src/index.ts index b64d36e702..eb8a517da2 100644 --- a/packages/boot/plugin-market/src/index.ts +++ b/packages/boot/plugin-market/src/index.ts @@ -7,6 +7,8 @@ import semver from 'semver' import { evaluatePluginCompatibility, getDshRuntimeVersion } from '@deepseek-ai/dsh-app-boot' import { marketInstallTarget, type MarketIndex, type MarketPlugin, type MarketVersion } from '@deepseek-ai/dsh-plugin-market-index' import type { BundleInfo } from '@deepseek-ai/dsh-plugin-manager/types' +// Also declares the `pluginManager` service this one reads bundles from. +import { pinnedGitOrigin } from '@deepseek-ai/dsh-plugin-manager' import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout' import { Remote, RemoteError, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol' import { @@ -19,9 +21,7 @@ import { normalizeIndexUrl } from './index-url.ts' import { RepositoryCache } from './repository-cache.ts' import { parseIndexInWorker } from './parse-thread.ts' import { MarketSearch } from './search.ts' -// Type-only: the `pluginManager` service, `settings.update`, the Loader's `loader/volatile-update` event, and the -// `connection` Fetch routes. -import type {} from '@deepseek-ai/dsh-plugin-manager' +// Type-only: `settings.update`, the Loader's `loader/volatile-update` event, and the `connection` Fetch routes. import type {} from '@deepseek-ai/dsh-settings' import type {} from '@deepseek-ai/cordis-plugin-loader' import type {} from '@deepseek-ai/dsh-client-connection' @@ -476,11 +476,16 @@ export default class PluginMarket extends TypertRemoteService { function listingOf( plugin: MarketPlugin, bundles: readonly BundleInfo[], exemptions: Readonly>, runtimeVersion: string, ): MarketListingEntry { - const versions = plugin.versions.map((version): MarketListingVersion => ({ - ...version, - install: marketInstallTarget(plugin.name, version), - compatibility: compatibilityOf(plugin.name, version, exemptions, runtimeVersion), - })) + const bundle = bundles.find(candidate => candidate.name === plugin.name) + // A Git copy updates only to another commit of the repository directory it came from. + const origin = bundle?.update === 'git' && bundle.source !== undefined ? pinnedGitOrigin(bundle.source) : undefined + const installedVersion = bundle?.version + const versions = plugin.versions.map((version): MarketListingVersion => { + const install = marketInstallTarget(plugin.name, version) + const update = origin !== undefined && installedVersion !== undefined && semver.gt(version.version, installedVersion) + && pinnedGitOrigin(install.spec) === origin + return { ...version, install, compatibility: compatibilityOf(plugin.name, version, exemptions, runtimeVersion), update } + }) const compatible = versions.filter(version => version.compatibility.compatible) const stable = plugin.versions.some(version => semver.prerelease(version.version) === null) const defaultVersion = compatible.find(version => !stable || semver.prerelease(version.version) === null)?.version @@ -490,12 +495,11 @@ function listingOf( ...plugin, versions, ...defaultVersion === undefined ? {} : { defaultVersion }, - installation: installationOf(plugin.name, measured, bundles), + installation: installationOf(bundle, measured), } } -function installationOf(name: string, version: string, bundles: readonly BundleInfo[]): MarketInstallation { - const bundle = bundles.find(candidate => candidate.name === name) +function installationOf(bundle: BundleInfo | undefined, version: string): MarketInstallation { // An on-demand catalog entry is listed before the profile installs it. if (bundle === undefined || (!bundle.installed && bundle.availability !== 'installation')) return { status: 'not-installed' } if (!bundle.installed) return { status: 'provided', ...bundle.version === undefined ? {} : { installedVersion: bundle.version } } diff --git a/packages/boot/plugin-market/src/types.ts b/packages/boot/plugin-market/src/types.ts index 4a4663ad77..fec4179c41 100644 --- a/packages/boot/plugin-market/src/types.ts +++ b/packages/boot/plugin-market/src/types.ts @@ -71,6 +71,12 @@ export type MarketCompatibility = export interface MarketListingVersion extends MarketVersion { readonly install: MarketInstallTarget readonly compatibility: MarketCompatibility + /** + * Whether `pluginManager.updateBundle` accepts this version for the installed copy: the copy came from a Git commit + * (`pluginManager` lists it with `update: 'git'`), this version is newer, and it pins a commit in the same repository + * and `path:` directory. `compatibility` reports separately whether the running DSH accepts it. + */ + readonly update: boolean } /** One index plugin with the profile-specific facts a market list displays. */ diff --git a/packages/boot/plugin-market/tests/market.spec.ts b/packages/boot/plugin-market/tests/market.spec.ts index 2bd2e998b5..0520e23d77 100644 --- a/packages/boot/plugin-market/tests/market.spec.ts +++ b/packages/boot/plugin-market/tests/market.spec.ts @@ -24,7 +24,7 @@ const URL_B = 'https://other.example/index.json' afterEach(() => { vi.unstubAllGlobals(); vi.useRealTimers() }) function bundle(name: string, fields: Partial): BundleInfo { - return { name, official: false, availability: 'profile', enabled: true, installed: true, optional: false, removable: true, updatable: false, rows: [], overrides: [], ...fields } + return { name, official: false, availability: 'profile', enabled: true, installed: true, optional: false, removable: true, rows: [], overrides: [], ...fields } } function index(plugins: unknown[] = []): unknown { @@ -165,6 +165,35 @@ it('reports a provided bundle without a version', async () => { expect((await read(market, URL_A)).entries[0]?.installation).toEqual({ status: 'provided' }) }) +it('marks the versions newer than a Git copy, from the repository directory it pins, as updates', async () => { + const commit = (digit: string): string => digit.repeat(40) + const git = (version: string, digit: string, path = 'packages/p') => release(version, { source: { type: 'git', url: 'https://git.example/o/plugins', commit: commit(digit), path } }) + const versions = [git('1.3.0', 'd', 'packages/q'), git('1.2.0', 'c'), release('1.1.0'), git('1.0.0', 'a'), git('0.9.0', '9')] + serve(json(index(['from-git', 'from-registry', 'no-source', 'no-version', 'unpinned'].map(name => entry(name, {}, versions))))) + const pinned = `git+ssh://git@git.example/o/plugins.git#${commit('a')}&path:/packages/p` + const { market } = await mount({}, { + bundles: [ + bundle('from-git', { version: '1.0.0', update: 'git', source: pinned }), + bundle('from-registry', { version: '1.0.0', update: 'registry', source: 'from-registry@1.0.0' }), + bundle('no-source', { version: '1.0.0', update: 'git' }), + bundle('no-version', { update: 'git', source: pinned }), + bundle('unpinned', { version: '1.0.0', update: 'git', source: 'git+https://git.example/o/plugins.git#main&path:/packages/p' }), + ], + }) + const by = Object.fromEntries((await read(market, URL_A)).entries.map(item => [item.name, item])) + // The SSH form a profile saved has the HTTPS form's origin; another directory of the repository is another plugin. + expect(by['from-git']?.versions.map(version => [version.version, version.update])).toEqual([['1.3.0', false], ['1.2.0', true], ['1.1.0', false], ['1.0.0', false], ['0.9.0', false]]) + expect(by['from-git']?.versions[1]?.install).toEqual({ spec: `git+https://git.example/o/plugins.git#${commit('c')}&path:/packages/p` }) + for (const name of ['from-registry', 'no-source', 'no-version', 'unpinned']) expect(by[name]?.versions.some(version => version.update)).toBe(false) +}) + +it('matches a copy pnpm saved as a known host\'s shortcut with the index\'s HTTPS versions', async () => { + const commit = (digit: string): string => digit.repeat(40) + serve(json(index([entry('hosted', {}, [release('1.1.0', { source: { type: 'git', url: 'https://github.com/o/plugins', commit: commit('b'), path: 'p' } })])]))) + const { market } = await mount({}, { bundles: [bundle('hosted', { version: '1.0.0', update: 'git', source: `github:o/plugins#${commit('a')}&path:/p` })] }) + expect((await read(market, URL_A)).entries[0]?.versions[0]?.update).toBe(true) +}) + it('shares concurrent reads, caches documents, and refetches on refresh or expiry', async () => { vi.useFakeTimers() const release = Promise.withResolvers() diff --git a/packages/client/ui-plugin-manager/README.i18n.yaml b/packages/client/ui-plugin-manager/README.i18n.yaml index d09c4925a2..7ac4ca91dc 100644 --- a/packages/client/ui-plugin-manager/README.i18n.yaml +++ b/packages/client/ui-plugin-manager/README.i18n.yaml @@ -15,14 +15,14 @@ en: d152484eb41ac6b4 zh: 09388d293f9be9cb /deepseek-ai-dsh-client-ui-plugin-manager/use-this-package: - en: 01ba764b51f010e8 - zh: e43a158a5b09d987 + en: 0c76e6c00cc7388d + zh: e56ddb7b78111b73 /deepseek-ai-dsh-client-ui-plugin-manager/use-this-package/installing-a-bundle: en: 1fc6c5c2a54d8db4 zh: 9e53f5164e2b7161 -/deepseek-ai-dsh-client-ui-plugin-manager/use-this-package/updating-a-registry-bundle: - en: 9312d8b1b381378f - zh: 8d0ae7ad26ada2ac +/deepseek-ai-dsh-client-ui-plugin-manager/use-this-package/updating-a-bundle: + en: 2a44a378364912db + zh: 8c514ce17469747f /deepseek-ai-dsh-client-ui-plugin-manager/use-this-package/switching-a-bundle: en: 390dc85aceac033a zh: 746ceae45f70dd33 @@ -36,8 +36,8 @@ en: d5e3cad5c31cae59 zh: 6dc38f09b410d247 /deepseek-ai-dsh-client-ui-plugin-manager/use-this-package/page-tabs: - en: fc469bab4fbd6028 - zh: 8f5a04e1b9a46471 + en: 99c06790c2163746 + zh: daa87ad1ce9a76ae /deepseek-ai-dsh-client-ui-plugin-manager/understand-the-implementation: en: 203168fc87e939bd zh: 72d0ccd2c54f3f44 diff --git a/packages/client/ui-plugin-manager/README.md b/packages/client/ui-plugin-manager/README.md index 8aaa4a3e74..412df1b9af 100644 --- a/packages/client/ui-plugin-manager/README.md +++ b/packages/client/ui-plugin-manager/README.md @@ -45,7 +45,7 @@ The first list read shows a short group-heading placeholder and four skeleton ca A bundle unavailable on the DSH Host operating system shows the reason on its card and details page. Its disabled switch prevents enabling it; an already selected bundle can still be switched off. [Plugin Manager](../../boot/plugin-manager/README.md#use-this-package) enforces the same restriction for direct requests. -The **Refresh** button shows its tooltip after 500 ms of hovering or keyboard focus. It reads the list again, then checks every updatable bundle for newer versions one after another; a failed read checks nothing. During a manual refresh, the button shows a spinner and stays disabled until all pending reads and those checks settle and at least 400 ms have elapsed, preventing a brief spinner flash; repeated refresh requests are ignored and the existing list remains visible. Success shows no toast. With a loaded list, a failed manual refresh keeps the cached content and shows a failure toast, including after leaving Plugins; select the button again to retry. Before any successful list read, failures show an in-place message with **Retry**. Background reads do not show the refresh spinner. +The **Refresh** button shows its tooltip after 500 ms of hovering or keyboard focus. It reads the list again, then checks every bundle installed from a registry for newer versions one after another; a failed read checks nothing. During a manual refresh, the button shows a spinner and stays disabled until all pending reads and those checks settle and at least 400 ms have elapsed, preventing a brief spinner flash; repeated refresh requests are ignored and the existing list remains visible. Success shows no toast. With a loaded list, a failed manual refresh keeps the cached content and shows a failure toast, including after leaving Plugins; select the button again to retry. Before any successful list read, failures show an in-place message with **Retry**. Background reads do not show the refresh spinner. ### Installing a bundle @@ -68,13 +68,13 @@ During preparation and download, **Cancel install** asks the Host to stop the ru When the Host attributes a network failure or timeout to a GitHub address and offers npmmirror, the dialog says **Cannot access GitHub**, or **GitHub connection timed out** for a timeout, and offers **Use mainland China mirror** or **Cancel**. Choosing the mirror returns to an empty package-name field and remembers the selected registry without starting another installation. When the install already asks npmmirror, whether picked, typed, or named by pnpm's own configuration, the button reads **Try another way** instead and returns to the same empty field with **Install guide and examples** unfolded and the registry unchanged. Other failures retain their existing diagnostics and actions. The mirror supplies registry packages and dependencies, not the GitHub repository itself. -### Updating a registry bundle +### Updating a bundle -A third-party bundle installed from a registry, which the Host marks `updatable`, has a **Versions** section on its page below its rows. Opening the page asks the Host for [its newer versions](../../boot/plugin-manager/README.md#use-this-package) once; **Check again** asks again. The section shows one summary row: checking, up to date with the time of the last check, an update to the newest version that declares a bundle, accepts the running DSH, and is neither deprecated nor a prerelease, newer versions that need a DSH update with the DSH peers they need, or a failed check with the registry that failed last and **Retry**. A summary with newer versions unfolds into one row per version, newest first, with its dist-tags, prerelease and deprecated tags, why it cannot be installed, its publication date, and **Update** for an installable version; the installed version closes the list. Below it, the registry that answered is marked **Fallback** when an earlier registry failed, beside the time of the check and **Show prereleases**. A failed check unfolds through **Details** into every registry asked, its role, what it failed with, and pnpm's words. Bundles installed from a link, a path, git, or a tarball, and Official bundles, show no Versions section. Older versions are never offered. +A third-party bundle installed from a registry, which the Host marks `update: 'registry'`, has a **Versions** section on its page below its rows. Opening the page asks the Host for [its newer versions](../../boot/plugin-manager/README.md#use-this-package) once; **Check again** asks again. The section shows one summary row: checking, up to date with the time of the last check, an update to the newest version that declares a bundle, accepts the running DSH, and is neither deprecated nor a prerelease, newer versions that need a DSH update with the DSH peers they need, or a failed check with the registry that failed last and **Retry**. A summary with newer versions unfolds into one row per version, newest first, with its dist-tags, prerelease and deprecated tags, why it cannot be installed, its publication date, and **Update** for an installable version; the installed version closes the list. Below it, the registry that answered is marked **Fallback** when an earlier registry failed, beside the time of the check and **Show prereleases**. A failed check unfolds through **Details** into every registry asked, its role, what it failed with, and pnpm's words. Bundles installed from a link, a path, git, or a tarball, and Official bundles, show no Versions section; a bundle installed from Git updates from a tab such as **Discover**. Older versions are never offered. After the checks, the list toolbar says how many plugins can be updated, that all are up to date, or that some checks failed, and cards with an update show **Update available**. The detail head adds **Update to** with the offered version. -**Update** opens a confirmation with the installed and target versions, the registry it installs from, and that a failed installation keeps or reinstalls the installed version. While the Host runs the update the dialog lists the steps it announced and stays open. The outcome says that the new version runs, that it waits for a restart with the Host's reason, that the target was refused before anything changed, that removing the installed version failed, that the target could not be installed while the installed version stayed in place, that the update failed and the previous version was reinstalled, or that the reinstallation failed too and the plugin is not installed; a failed run unfolds pnpm's output. A lost answer says that the result is unknown and the list is read again. +**Update** opens a confirmation with the installed and target versions, the registry it installs from, or the host and path of a Git target's repository, and that a failed installation keeps or reinstalls the installed version. While the Host runs the update the dialog lists the steps it announced and stays open. The outcome says that the new version runs, that it waits for a restart with the Host's reason, that the target was refused before anything changed, that removing the installed version failed, that the target could not be installed while the installed version stayed in place, that the update failed and the previous version was reinstalled, or that the reinstallation failed too and the plugin is not installed; a failed run unfolds pnpm's output. A lost answer says that the result is unknown and the list is read again. ### Switching a bundle @@ -116,7 +116,7 @@ Component lists and detail pages show current startup diagnostics from the Host ### Page tabs -Another plugin adds a tab after the page's own **Installed** tab by registering into the `plugins.tab` list slot; the entry's `label` names the tab and `order` places it. The tabs appear under the page header once one entry exists and are hidden on a bundle's, row's, or official plugin's page; the selected tab survives leaving **Plugins**, and a tab whose entry leaves falls back to **Installed**. The page renders the selected entry in place of its groups and hides its own refresh and add buttons there. The owner props carry `install({ spec, registry?, title, version })` and `openPackage(name)`. `install` is for a version the person already confirmed in the tab: the install dialog opens at its check screen, without the spec form, names the plugin with `title` and `version` above the spec, which it shows in smaller monospace text, keeps that name on the progress and result screens, with the installed version once done, asks `registry` first for that install only and never remembers it, and offers **Retry** after a refusal; a hidden installation reopens instead. `openPackage` switches to **Installed** and opens the bundle's page when that tab lists it, and that page's back button returns to the tab that opened it; a bundle the page leaves out, such as a built-in profile bundle, leaves the Installed list open. A confirmed install offers no GitHub mirror recovery, since that recovery reopens the spec form, and its back button cancels a running install. [`ui-plugin-market`](../ui-plugin-market/README.md) contributes the **Discover** tab here. +Another plugin adds a tab after the page's own **Installed** tab by registering into the `plugins.tab` list slot; the entry's `label` names the tab and `order` places it. The tabs appear under the page header once one entry exists and are hidden on a bundle's, row's, or official plugin's page; the selected tab survives leaving **Plugins**, and a tab whose entry leaves falls back to **Installed**. The page renders the selected entry in place of its groups and hides its own refresh and add buttons there. The owner props carry `install({ spec, registry?, title, version })`, `update({ packageName, version, spec })`, and `openPackage(name)`. `install` is for a version the person already confirmed in the tab: the install dialog opens at its check screen, without the spec form, names the plugin with `title` and `version` above the spec, which it shows in smaller monospace text, keeps that name on the progress and result screens, with the installed version once done, asks `registry` first for that install only and never remembers it, and offers **Retry** after a refusal; a hidden installation reopens instead. `openPackage` switches to **Installed** and opens the bundle's page when that tab lists it, and that page's back button returns to the tab that opened it; a bundle the page leaves out, such as a built-in profile bundle, leaves the Installed list open. A confirmed install offers no GitHub mirror recovery, since that recovery reopens the spec form, and its back button cancels a running install. `update` is for a newer Git version of a bundle the Host marks `update: 'git'`, from the repository directory that bundle came from: it opens the **Update** confirmation from the version the list holds, which names the repository and runs the same steps as a registry update with the pinned spec, and does nothing before the list holds the bundle or while another update runs. [`ui-plugin-market`](../ui-plugin-market/README.md) contributes the **Discover** tab here. Access: the Plugins entry, page, refresh notice, Host change subscriptions, and `pluginNavigation` exist only while the current Peer may call `pluginManager/listBundles`; each later grant re-reads a list already loaded, and `pluginChanges` receives no updates while access is withdrawn. The owner always has access. diff --git a/packages/client/ui-plugin-manager/README.zh.md b/packages/client/ui-plugin-manager/README.zh.md index a89ed56c46..8d945eea5b 100644 --- a/packages/client/ui-plugin-manager/README.zh.md +++ b/packages/client/ui-plugin-manager/README.zh.md @@ -45,7 +45,7 @@ kind: "package-reference" 组合包不支持 DSH Host 的操作系统时,卡片和详情页会显示原因,关闭状态的开关不可启用;已选中的组合包仍可关闭。[插件管理器](../../boot/plugin-manager/README.zh.md#use-this-package)对直接请求执行同一限制。 -**刷新**按钮在悬停或键盘聚焦 500 毫秒后显示提示。它重新读取列表,再依次检查每个可更新组合包的新版本;读取失败时不做检查。手动刷新期间,按钮显示旋转加载指示并保持禁用,直到所有待处理读取和这些检查结束且至少经过 400 毫秒,避免加载指示一闪而过;重复刷新请求会被忽略,已有列表保持可见。成功不显示 toast。已有列表时,手动刷新失败保留缓存内容并显示失败 toast,即使已离开插件页也能看到;再次选择该按钮即可重试。在首次成功读取列表之前,失败仍原位显示提示与**重试**。后台读取不显示刷新按钮的加载指示。 +**刷新**按钮在悬停或键盘聚焦 500 毫秒后显示提示。它重新读取列表,再依次检查每个从注册表安装的组合包的新版本;读取失败时不做检查。手动刷新期间,按钮显示旋转加载指示并保持禁用,直到所有待处理读取和这些检查结束且至少经过 400 毫秒,避免加载指示一闪而过;重复刷新请求会被忽略,已有列表保持可见。成功不显示 toast。已有列表时,手动刷新失败保留缓存内容并显示失败 toast,即使已离开插件页也能看到;再次选择该按钮即可重试。在首次成功读取列表之前,失败仍原位显示提示与**重试**。后台读取不显示刷新按钮的加载指示。 ### 安装一个组合包 @@ -68,13 +68,13 @@ Host 通过普通 fetch 代理向 `https://registry.npmjs.org/-/ping` 和 `https Host 将网络失败或超时归因于 GitHub 地址,且提供 npmmirror 时,对话框显示**无法访问 GitHub**,超时时显示**连接 GitHub 超时**,提供**改用国内镜像**和**取消**。选择镜像后回到空的包名输入框,记住所选安装源,不自动开始下一次安装。安装已在使用 npmmirror 时(无论是选中、手动输入,还是 pnpm 自身配置指向它),按钮改为**试试其他方式**,同样回到空的包名输入框并展开**插件安装引导和示例**,安装源保持不变。其他失败保留原有诊断和操作。镜像提供注册表中的包及依赖,不代替 GitHub 仓库下载。 -### 更新注册表组合包 +### 更新组合包 -从注册表安装、Host 标为 `updatable` 的第三方组合包,在其页面的行列表下方有 **版本** 区块。打开页面时向 Host 询问一次[它的新版本](../../boot/plugin-manager/README.zh.md#use-this-package);**重新检查** 再问一次。区块显示一行摘要:正在检查;已是最新及上次检查时间;可更新到声明组合包、接受当前 DSH、未弃用且非预发布的最新版本;新版本需要更新 DSH 及其所需的 DSH peer;或检查失败、最后失败的注册表及 **重试**。有新版本的摘要可展开为每个版本一行,从新到旧,带 dist-tag、预发布与已弃用标签、无法安装的原因、发布日期,以及可安装版本的 **更新**;列表以已安装版本结尾。其下显示作答的注册表,前面的注册表失败时标为 **备选源**,旁边是检查时间和 **显示预发布版本**。检查失败时可通过 **详情** 展开问过的每个注册表、其角色、失败类型和 pnpm 的原文。从链接、路径、git 或压缩包安装的组合包以及官方组合包不显示版本区块。不会提供更旧的版本。 +从注册表安装、Host 标为 `update: 'registry'` 的第三方组合包,在其页面的行列表下方有 **版本** 区块。打开页面时向 Host 询问一次[它的新版本](../../boot/plugin-manager/README.zh.md#use-this-package);**重新检查** 再问一次。区块显示一行摘要:正在检查;已是最新及上次检查时间;可更新到声明组合包、接受当前 DSH、未弃用且非预发布的最新版本;新版本需要更新 DSH 及其所需的 DSH peer;或检查失败、最后失败的注册表及 **重试**。有新版本的摘要可展开为每个版本一行,从新到旧,带 dist-tag、预发布与已弃用标签、无法安装的原因、发布日期,以及可安装版本的 **更新**;列表以已安装版本结尾。其下显示作答的注册表,前面的注册表失败时标为 **备选源**,旁边是检查时间和 **显示预发布版本**。检查失败时可通过 **详情** 展开问过的每个注册表、其角色、失败类型和 pnpm 的原文。从链接、路径、git 或压缩包安装的组合包以及官方组合包不显示版本区块;从 Git 安装的组合包通过 **发现** 等标签页更新。不会提供更旧的版本。 检查结束后,列表工具栏说明有几个插件可更新、全部已是最新,或部分检查失败,有更新的卡片显示 **可更新**。详情页头部增加带目标版本的 **更新到**。 -**更新** 打开确认框,显示已安装版本与目标版本、安装来源,以及安装失败时会保留或重新安装已安装版本。Host 运行更新期间,对话框列出它通告的步骤并保持打开。结果说明新版本已开始运行、需要重启才生效及 Host 给出的原因、目标版本在任何改动前被拒绝、移除已安装版本失败、目标版本未能安装而已安装版本保持不变、更新失败且已重新安装原版本,或重新安装也失败、插件未安装;失败的运行可展开 pnpm 输出。答复丢失时说明结果未知,并重新读取列表。 +**更新** 打开确认框,显示已安装版本与目标版本、安装来源的注册表或 Git 目标仓库的主机与路径,以及安装失败时会保留或重新安装已安装版本。Host 运行更新期间,对话框列出它通告的步骤并保持打开。结果说明新版本已开始运行、需要重启才生效及 Host 给出的原因、目标版本在任何改动前被拒绝、移除已安装版本失败、目标版本未能安装而已安装版本保持不变、更新失败且已重新安装原版本,或重新安装也失败、插件未安装;失败的运行可展开 pnpm 输出。答复丢失时说明结果未知,并重新读取列表。 ### 切换一个组合包 @@ -116,7 +116,7 @@ ctx.slots.inject('plugins.detail.section', () => ctx.slots.register({ ### 页面标签页 -其他插件向 `plugins.tab` 列表 slot 注册条目,即可在页面自带的**已安装**标签页之后添加一个标签页;条目的 `label` 为标签页命名,`order` 决定其位置。至少存在一个条目时,标签页显示在页头下方;在组合包、行或官方插件的页面上隐藏。离开**插件**后,所选标签页保持不变;所选标签页的条目被移除时,回到**已安装**。页面渲染所选条目以代替自身的分组,并在此时隐藏自身的刷新和添加按钮。owner props 提供 `install({ spec, registry?, title, version })` 和 `openPackage(name)`。`install` 用于用户已在标签页中确认的版本:安装对话框在检查界面打开,不显示 spec 表单,在以较小等宽字显示的 spec 上方用 `title` 和 `version` 标明插件,并在进度和结果界面保持该名称,完成后显示已安装的版本;只在这一次安装中首先使用 `registry`,且从不记住它;被拒绝后提供**重试**;如果有隐藏中的安装任务,则改为重新打开该任务。`openPackage` 切换到**已安装**,并在该标签页列出该组合包时打开其页面,该页面的返回按钮回到打开它的标签页;页面不列出的组合包(例如内置 profile 组合包)只打开「已安装」列表。已确认的安装不提供 GitHub 镜像恢复,因为该恢复会重新打开 spec 表单;其返回按钮用于取消正在进行的安装。[`ui-plugin-market`](../ui-plugin-market/README.zh.md) 在这里贡献**发现**标签页。 +其他插件向 `plugins.tab` 列表 slot 注册条目,即可在页面自带的**已安装**标签页之后添加一个标签页;条目的 `label` 为标签页命名,`order` 决定其位置。至少存在一个条目时,标签页显示在页头下方;在组合包、行或官方插件的页面上隐藏。离开**插件**后,所选标签页保持不变;所选标签页的条目被移除时,回到**已安装**。页面渲染所选条目以代替自身的分组,并在此时隐藏自身的刷新和添加按钮。owner props 提供 `install({ spec, registry?, title, version })`、`update({ packageName, version, spec })` 和 `openPackage(name)`。`install` 用于用户已在标签页中确认的版本:安装对话框在检查界面打开,不显示 spec 表单,在以较小等宽字显示的 spec 上方用 `title` 和 `version` 标明插件,并在进度和结果界面保持该名称,完成后显示已安装的版本;只在这一次安装中首先使用 `registry`,且从不记住它;被拒绝后提供**重试**;如果有隐藏中的安装任务,则改为重新打开该任务。`openPackage` 切换到**已安装**,并在该标签页列出该组合包时打开其页面,该页面的返回按钮回到打开它的标签页;页面不列出的组合包(例如内置 profile 组合包)只打开「已安装」列表。已确认的安装不提供 GitHub 镜像恢复,因为该恢复会重新打开 spec 表单;其返回按钮用于取消正在进行的安装。`update` 用于 Host 标为 `update: 'git'` 的组合包、来自其来源仓库目录的较新 Git 版本:它以列表中记录的版本为原版本打开**更新**确认框,该确认框显示仓库,并以固定的 spec 执行与注册表更新相同的步骤;列表尚未包含该组合包或另一个更新正在运行时,它不做任何事。[`ui-plugin-market`](../ui-plugin-market/README.zh.md) 在这里贡献**发现**标签页。 访问控制:插件入口、页面、刷新提示、Host 变更订阅与 `pluginNavigation` 只在当前 Peer 可以调用 `pluginManager/listBundles` 时存在;之后每次授权都会重读已加载过的列表,撤权期间 `pluginChanges` 不更新。owner 始终有权限。 diff --git a/packages/client/ui-plugin-manager/src/client/PluginManagerPage.tsx b/packages/client/ui-plugin-manager/src/client/PluginManagerPage.tsx index 71c63c3db7..41df8e96a2 100644 --- a/packages/client/ui-plugin-manager/src/client/PluginManagerPage.tsx +++ b/packages/client/ui-plugin-manager/src/client/PluginManagerPage.tsx @@ -511,9 +511,9 @@ function versionUpdate(check: VersionCheck | undefined): string | undefined { return check?.phase === 'done' ? updateCandidate(check.list)?.version : undefined } -/** The list toolbar's sentence about the last update check of every updatable package. */ +/** The list toolbar's sentence about the last update check of every registry package. */ function updatesNote(packages: readonly PackageView[], versions: PluginManagerState['versions'], t: Translate): { text: string; tone?: 'info' | 'error' } | undefined { - const checks = packages.filter(pkg => pkg.updatable).map(pkg => versions[pkg.name]) + const checks = packages.filter(pkg => pkg.update === 'registry').map(pkg => versions[pkg.name]) if (checks.length === 0 || checks.some(check => check === undefined || check.phase === 'checking')) return undefined const available = checks.filter(check => versionUpdate(check) !== undefined).length if (available > 0) return { text: t('updatesAvailable', { count: String(available) }), tone: 'info' } @@ -1731,6 +1731,7 @@ export function PluginManagerPage(props: PluginManagerPageProps): ReactNode {
{renderSlot('plugins.tab', { install: props.installFrom, + update: props.openGitUpdate, openPackage: (name) => { setTab(INSTALLED_TAB); setView({ kind: 'package', name, returnTab: tab }) }, }, { only: tab })}
diff --git a/packages/client/ui-plugin-manager/src/client/index.ts b/packages/client/ui-plugin-manager/src/client/index.ts index f5908227a4..eadd359408 100644 --- a/packages/client/ui-plugin-manager/src/client/index.ts +++ b/packages/client/ui-plugin-manager/src/client/index.ts @@ -65,7 +65,7 @@ export type { ConfigLedger, OfficialItem } from './config-ledger.ts' export type { PluginManagerFace } from './manager-store.ts' export type { PluginManagerLocaleKey } from './locales.ts' export type { - ConfigPageForm, PluginActivationOwnerProps, PluginAddActionsProps, PluginTabInstall, PluginTabProps, + ConfigPageForm, PluginActivationOwnerProps, PluginAddActionsProps, PluginTabInstall, PluginTabProps, PluginTabUpdate, PluginConfigViewProps, PluginDetailProps, PluginPackageRef, PluginRowRef, PluginsSubject, } from './slot-contract.ts' diff --git a/packages/client/ui-plugin-manager/src/client/locales.ts b/packages/client/ui-plugin-manager/src/client/locales.ts index 6b31b7ffa8..b9847473c5 100644 --- a/packages/client/ui-plugin-manager/src/client/locales.ts +++ b/packages/client/ui-plugin-manager/src/client/locales.ts @@ -164,7 +164,7 @@ export const zh = { updateRunning: '正在更新', updateStepChecking: '检查 {version}', updateStepRemoving: '停止并移除 {version}', - updateStepInstalling: '从 {registry} 安装 {version}', + updateStepInstalling: '从 {source} 安装 {version}', updateStepRestoring: '重新安装 {version}', updateDone: '已更新到 {version}', updateDoneDetail: '新版本已经开始运行。', @@ -290,6 +290,7 @@ export const zh = { reasonUnsupportedPlatform: '此插件不支持运行 DSH 的主机操作系统', reasonIncompatibleInstall: '请安装与当前 DSH 兼容的插件版本。', reasonIncompatibleInstalled: '请卸载后重新安装与当前 DSH 兼容的版本。', + reasonUnexpectedPackage: '插件来源安装了与所请求不同的包或版本,已撤销这次安装。请向插件发布者或插件索引维护者报告此问题。', reasonOperationError: 'Host 报告了一个错误', detailTitle: '详细介绍', detailLoading: '正在读取详细介绍', @@ -462,7 +463,7 @@ export const en = { updateRunning: 'Updating', updateStepChecking: 'Check {version}', updateStepRemoving: 'Stop and remove {version}', - updateStepInstalling: 'Install {version} from {registry}', + updateStepInstalling: 'Install {version} from {source}', updateStepRestoring: 'Install {version} again', updateDone: 'Updated to {version}', updateDoneDetail: 'The new version is running.', @@ -588,6 +589,7 @@ export const en = { reasonUnsupportedPlatform: 'This plugin does not support the operating system running DSH.', reasonIncompatibleInstall: 'Install a plugin version compatible with this DSH.', reasonIncompatibleInstalled: 'Uninstall it and install a version compatible with this DSH.', + reasonUnexpectedPackage: 'The plugin source installed another package or version than requested; the installation was undone. Report it to the plugin\'s publisher or the index maintainer.', reasonOperationError: 'The Host reported an error.', detailTitle: 'Details', detailLoading: 'Reading details', @@ -758,7 +760,7 @@ export const ja = { 'updateRunning': 'アップデート中', 'updateStepChecking': '{version}を確認', 'updateStepRemoving': '{version}を停止して削除', - 'updateStepInstalling': '{registry}から{version}をインストール', + 'updateStepInstalling': '{source}から{version}をインストール', 'updateStepRestoring': '{version}を再インストール', 'updateDone': '{version}にアップデートしました', 'updateDoneDetail': '新しいバージョンが実行中です。', @@ -883,6 +885,7 @@ export const ja = { 'reasonIncompatibleVersionUnnamed': 'このプラグインは実行中の DSH バージョンと互換性がありません。実行するとクラッシュやデータ損失を引き起こす可能性があります。', 'reasonIncompatibleInstall': 'この DSH と互換性のあるプラグインバージョンをインストールしてください。', 'reasonIncompatibleInstalled': 'アンインストールして、この DSH と互換性のあるバージョンをインストールしてください。', + 'reasonUnexpectedPackage': 'プラグインのソースが要求とは異なるパッケージまたはバージョンをインストールしました。インストールを元に戻しました。プラグインの公開者またはプラグインインデックスの管理者に報告してください。', 'reasonOperationError': 'Host がエラーを報告しました。', 'detailTitle': '詳細', 'detailLoading': '詳細を読み込み中', @@ -1053,7 +1056,7 @@ export const es = { updateRunning: 'Actualizando', updateStepChecking: 'Comprobar {version}', updateStepRemoving: 'Detener y eliminar {version}', - updateStepInstalling: 'Instale {version} desde {registry}', + updateStepInstalling: 'Instale {version} desde {source}', updateStepRestoring: 'Instale {version} nuevamente', updateDone: 'Actualizado a {version}', updateDoneDetail: 'La nueva versión se está ejecutando.', @@ -1178,6 +1181,7 @@ export const es = { reasonIncompatibleVersionUnnamed: 'Este complemento es incompatible con la versión DSH en ejecución; ejecutarlo puede causar fallas o pérdida de datos.', reasonIncompatibleInstall: 'Instale una versión del complemento compatible con este DSH.', reasonIncompatibleInstalled: 'Desinstálalo e instala una versión compatible con este DSH.', + reasonUnexpectedPackage: 'La fuente del complemento instaló un paquete o una versión distintos de lo solicitado; se deshizo la instalación. Informe el problema al editor del complemento o al responsable del índice de complementos.', reasonOperationError: 'El anfitrión informó un error.', detailTitle: 'Detalles', detailLoading: 'Detalles de lectura', @@ -1348,7 +1352,7 @@ export const fr = { updateRunning: 'Mise à jour', updateStepChecking: 'Vérifiez {version}', updateStepRemoving: 'Arrêtez et supprimez {version}', - updateStepInstalling: 'Installez {version} à partir de {registry}', + updateStepInstalling: 'Installez {version} à partir de {source}', updateStepRestoring: 'Installez à nouveau {version}', updateDone: 'Mis à jour vers {version}', updateDoneDetail: "La nouvelle version est en cours d'exécution.", @@ -1473,6 +1477,7 @@ export const fr = { reasonIncompatibleVersionUnnamed: "Ce plugin est incompatible avec la version DSH en cours d'exécution ; son exécution peut provoquer des plantages ou une perte de données.", reasonIncompatibleInstall: 'Installez une version du plugin compatible avec ce DSH.', reasonIncompatibleInstalled: 'Désinstallez-le et installez une version compatible avec ce DSH.', + reasonUnexpectedPackage: "La source du plugin a installé un autre package ou une autre version que ceux demandés ; l'installation a été annulée. Signalez-le à l'éditeur du plugin ou au responsable de l'index de plugins.", reasonOperationError: "L'hôte a signalé une erreur.", detailTitle: 'Détails', detailLoading: 'Détails de lecture', @@ -1643,7 +1648,7 @@ export const de = { updateRunning: 'Aktualisierung', updateStepChecking: 'Überprüfen Sie {version}', updateStepRemoving: 'Stoppen und entfernen Sie {version}', - updateStepInstalling: 'Installieren Sie {version} von {registry}', + updateStepInstalling: 'Installieren Sie {version} von {source}', updateStepRestoring: 'Installieren Sie {version} erneut', updateDone: 'Aktualisiert auf {version}', updateDoneDetail: 'Die neue Version läuft.', @@ -1768,6 +1773,7 @@ export const de = { reasonIncompatibleVersionUnnamed: 'Dieses Plugin ist nicht mit der laufenden DSH-Version kompatibel. Die Ausführung kann zu Abstürzen oder Datenverlust führen.', reasonIncompatibleInstall: 'Installieren Sie eine mit diesem DSH kompatible Plugin-Version.', reasonIncompatibleInstalled: 'Deinstallieren Sie es und installieren Sie eine mit diesem DSH kompatible Version.', + reasonUnexpectedPackage: 'Die Plugin-Quelle hat ein anderes Paket oder eine andere Version als angefordert installiert; die Installation wurde rückgängig gemacht. Melden Sie dies dem Herausgeber des Plugins oder dem Betreuer des Plugin-Index.', reasonOperationError: 'Der Host hat einen Fehler gemeldet.', detailTitle: 'Details', detailLoading: 'Details lesen', @@ -1938,7 +1944,7 @@ export const ru = { updateRunning: 'Обновление', updateStepChecking: 'Проверьте {version}', updateStepRemoving: 'Остановите и удалите {version}.', - updateStepInstalling: 'Установите {version} из {registry}.', + updateStepInstalling: 'Установите {version} из {source}.', updateStepRestoring: 'Установите {version} еще раз.', updateDone: 'Обновлено до {version}.', updateDoneDetail: 'Новая версия работает.', @@ -2063,6 +2069,7 @@ export const ru = { reasonIncompatibleVersionUnnamed: 'Этот плагин несовместим с работающей версией DSH; его запуск может привести к сбоям или потере данных.', reasonIncompatibleInstall: 'Установите версию плагина, совместимую с этим DSH.', reasonIncompatibleInstalled: 'Удалите его и установите версию, совместимую с этим DSH.', + reasonUnexpectedPackage: 'Источник плагина установил не тот пакет или не ту версию, которые были запрошены; установка отменена. Сообщите об этом издателю плагина или сопровождающему индекса плагинов.', reasonOperationError: 'Хост сообщил об ошибке.', detailTitle: 'Подробности', detailLoading: 'Читаем подробности', @@ -2233,7 +2240,7 @@ export const pt = { updateRunning: 'Atualizando', updateStepChecking: 'Verifique {version}', updateStepRemoving: 'Pare e remova {version}', - updateStepInstalling: 'Instalar {version} de {registry}', + updateStepInstalling: 'Instalar {version} de {source}', updateStepRestoring: 'Instale {version} novamente', updateDone: 'Atualizado para {version}', updateDoneDetail: 'A nova versão está em execução.', @@ -2358,6 +2365,7 @@ export const pt = { reasonIncompatibleVersionUnnamed: 'Este plug-in é incompatível com a versão DSH em execução; executá-lo pode causar travamentos ou perda de dados.', reasonIncompatibleInstall: 'Instale uma versão de plug-in compatível com este DSH.', reasonIncompatibleInstalled: 'Desinstale-o e instale uma versão compatível com este DSH.', + reasonUnexpectedPackage: 'A fonte do plug-in instalou um pacote ou uma versão diferente do que foi solicitado; a instalação foi desfeita. Informe o problema ao editor do plug-in ou ao responsável pelo índice de plug-ins.', reasonOperationError: 'O Host relatou um erro.', detailTitle: 'Detalhes', detailLoading: 'Lendo detalhes', diff --git a/packages/client/ui-plugin-manager/src/client/manager-store.ts b/packages/client/ui-plugin-manager/src/client/manager-store.ts index 2ca4f1cdb6..5554b1fe0c 100644 --- a/packages/client/ui-plugin-manager/src/client/manager-store.ts +++ b/packages/client/ui-plugin-manager/src/client/manager-store.ts @@ -38,7 +38,7 @@ import type { HostObservable } from '@deepseek-ai/dsh-client-ui-slots' import type { LocalizedText, PluginLocalizedMeta } from '@deepseek-ai/dsh-package-manifest' import type { SettingsDescribeFace, ConfigForms } from '@deepseek-ai/dsh-client-ui-settings/client' import type { ConfigLedger } from './config-ledger.ts' -import type { PluginTabInstall } from './slot-contract.ts' +import type { PluginTabInstall, PluginTabUpdate } from './slot-contract.ts' import type { PluginTab } from './tab-ledger.ts' import { shortName } from './presentation.ts' import { BundleDetailController, type BundleDetailsState } from './detail-store.ts' @@ -123,8 +123,8 @@ export interface PackageView { readonly optional: boolean /** Whether the Host removes the bundle: a profile dependency, or a selection that neither the profile nor the installation holds. */ readonly removable: boolean - /** Whether the Host lists and installs newer registry versions of the bundle. */ - readonly updatable: boolean + /** How the Host updates the bundle: `registry` lists and installs newer registry versions; `git` installs a Git spec a tab names. */ + readonly update?: BundleInfo['update'] /** Whether the bundle is in the profile's layer list. */ readonly enabled: boolean /** Why the Host refuses to switch the bundle off or remove it, when it does. */ @@ -361,13 +361,20 @@ export function installable(version: PluginVersion): boolean { return version.bundle && version.incompatible === undefined } +/** + * Where an update's target comes from: the registry that listed it, which the Host checks and installs it from, or + * the pinned Git spec a Discover entry names. + */ +export type UpdateSource = + | { readonly kind: 'registry'; readonly registry: Registry } + | { readonly kind: 'git'; readonly spec: string } + /** One update from its confirmation to its outcome. */ export interface UpdateState { readonly packageName: string readonly from: string readonly to: string - /** The registry that listed the target; the Host checks and installs it there. */ - readonly registry: Registry + readonly source: UpdateSource readonly phase: 'confirm' | 'running' | 'done' /** The steps the Host announced, in order; the last one runs while the update does. */ readonly steps: readonly PluginUpdateStep[] @@ -431,7 +438,7 @@ export interface PluginManagerFace { detailImage: BundleDetailController['image'] /** Read the Host once the tab first renders. */ ensure: () => void - /** Read the Host again, then check every updatable package for newer versions. */ + /** Read the Host again, then check every registry package for newer versions. */ refresh: () => void openInstall: () => void /** @@ -476,12 +483,18 @@ export interface PluginManagerFace { /** Switch one of a bundle's rows on or off in the profile's user layer. */ setRowEnabled: (entryId: PluginEntryId, enabled: boolean) => void dismissNotice: () => void - /** Ask the Host for an updatable package's newer versions, again when asked again. */ + /** Ask the Host for a registry package's newer versions, again when asked again. */ checkVersions: (packageName: string) => void /** Check a package's versions once: a package already checked or being checked is left as it is. */ ensureVersions: (packageName: string) => void /** Ask before replacing a package's installed version with a listed newer one. */ openUpdate: (packageName: string, version: string) => void + /** + * Ask before replacing a Git copy with a newer Git version another tab names, such as a Discover entry, from the + * version the list holds; ignored unless the list holds the package as a Git copy, and while an update runs. + * @param target - the package, the target version, and its Git spec. + */ + openGitUpdate: (target: PluginTabUpdate) => void /** Start the confirmed update. */ runUpdate: () => void /** Leave the confirmation or the outcome; a running update keeps its dialog. */ @@ -575,7 +588,7 @@ export function packageView(bundle: BundleInfo, plugins: readonly PluginInfo[]): ...bundle.listedByDefault === undefined ? {} : { listedByDefault: bundle.listedByDefault }, optional: bundle.optional, removable: bundle.removable, - updatable: bundle.updatable, + ...bundle.update === undefined ? {} : { update: bundle.update }, enabled: bundle.enabled, rows, ...bundle.version === undefined ? {} : { version: bundle.version }, @@ -867,9 +880,13 @@ export class PluginManagerController { openUpdate: (packageName, version) => { const check = this.getSnapshot().versions[packageName] if (this.getSnapshot().update?.phase === 'running' || check?.phase !== 'done' || check.list.status !== 'listed') return - this.patch({ update: { - packageName, from: check.list.installed, to: version, registry: check.list.registry, phase: 'confirm', steps: [], result: null, outputOpen: false, - } }) + this.openUpdateDialog(packageName, check.list.installed, version, { kind: 'registry', registry: check.list.registry }) + }, + openGitUpdate: ({ packageName, version, spec }) => { + const { packages, update } = this.getSnapshot() + const pkg = packages.find(item => item.name === packageName) + if (update?.phase === 'running' || pkg?.update !== 'git' || pkg.version === undefined) return + this.openUpdateDialog(packageName, pkg.version, version, { kind: 'git', spec }) }, runUpdate: () => { void this.runUpdate() }, closeUpdate: () => { if (this.getSnapshot().update?.phase !== 'running') this.patch({ update: null }) }, @@ -900,6 +917,11 @@ export class PluginManagerController { return started } + /** Show an update's confirmation. */ + private openUpdateDialog(packageName: string, from: string, to: string, source: UpdateSource): void { + this.patch({ update: { packageName, from, to, source, phase: 'confirm', steps: [], result: null, outputOpen: false } }) + } + /** * Follow the Host's steps of this tab's update. * @param progress - a request id and step received from the Host. @@ -922,35 +944,37 @@ export class PluginManagerController { this.patchVersions(packageName, { phase: 'done', list, checkedAt: Date.now() }) } - /** Check the updatable packages one after another, so one click starts at most one package's registry lookups. */ + /** Check the registry packages one after another, so one click starts at most one package's registry lookups. */ private async checkAllVersions(): Promise { - for (const pkg of this.getSnapshot().packages.filter(item => item.updatable)) await this.checkVersions(pkg.name) + for (const pkg of this.getSnapshot().packages.filter(item => item.update === 'registry')) await this.checkVersions(pkg.name) } - /** Hand the confirmed update to the Host, keep its outcome, then read the package and its versions again. */ + /** Hand the confirmed update to the Host, keep its outcome, then read the package and a registry package's versions again. */ private async runUpdate(): Promise { const update = this.getSnapshot().update if (this.disposed || update === null || update.phase !== 'confirm') return const requestId = randomUUID() as PluginInstallRequestId const running: UpdateState = { ...update, phase: 'running', steps: [], requestId, result: null } this.patch({ update: running }) - const answer = await this.ctx.remote.pluginManager.updateBundle(update.packageName, update.to, { registry: update.registry, requestId }) + const { source } = update + const answer = await this.ctx.remote.pluginManager.updateBundle(update.packageName, update.to, + source.kind === 'registry' ? { registry: source.registry, requestId } : { spec: source.spec, requestId }) // Nothing closes or replaces a running update before its answer; after disposal `patch` publishes nothing. const current = this.getSnapshot().update as UpdateState this.patch({ update: { ...current, phase: 'done', ...answer.ok ? { result: answer.value } : { lost: answer.error.message } } }) await this.load() - await this.checkVersions(update.packageName) + if (source.kind === 'registry') await this.checkVersions(update.packageName) } /** - * Forget the checks that no longer describe a listed package: one that left the list or stopped being updatable, and - * one answered for another installed version, after a reinstallation or a change from another surface. + * Forget the checks that no longer describe a listed package: one that left the list or stopped being a registry + * package, and one answered for another installed version, after a reinstallation or a change from another surface. */ private dropStaleVersions(): void { const { packages, versions } = this.getSnapshot() const current = Object.fromEntries(Object.entries(versions).filter(([name, check]) => { const pkg = packages.find(item => item.name === name) - return pkg?.updatable === true && (check.phase === 'checking' || check.list.status !== 'listed' || check.list.installed === pkg.version) + return pkg?.update === 'registry' && (check.phase === 'checking' || check.list.status !== 'listed' || check.list.installed === pkg.version) })) if (Object.keys(current).length !== Object.keys(versions).length) this.patch({ versions: current }) } @@ -1051,7 +1075,7 @@ export class PluginManagerController { } /** - * Keep manual refresh feedback until its coalesced reads and the version checks of the updatable packages settle, + * Keep manual refresh feedback until its coalesced reads and the version checks of the registry packages settle, * without clearing cached cards; a failed read checks nothing. */ private async refresh(): Promise { diff --git a/packages/client/ui-plugin-manager/src/client/presentation.ts b/packages/client/ui-plugin-manager/src/client/presentation.ts index e532eadc9e..8709103837 100644 --- a/packages/client/ui-plugin-manager/src/client/presentation.ts +++ b/packages/client/ui-plugin-manager/src/client/presentation.ts @@ -60,6 +60,7 @@ const CODE_KEYS = { 'stale-approval': 'reasonStaleApproval', 'incompatible-version': 'reasonIncompatibleVersionUnnamed', 'unsupported-platform': 'reasonUnsupportedPlatform', + 'unexpected-package': 'reasonUnexpectedPackage', 'operation-error': 'reasonOperationError', } satisfies Record diff --git a/packages/client/ui-plugin-manager/src/client/slot-contract.ts b/packages/client/ui-plugin-manager/src/client/slot-contract.ts index 1450f2bb96..a432712b4c 100644 --- a/packages/client/ui-plugin-manager/src/client/slot-contract.ts +++ b/packages/client/ui-plugin-manager/src/client/slot-contract.ts @@ -9,7 +9,7 @@ * `import type` and registers through `ctx.slots`; it never imports this * package at runtime. * - * A contributed tab, such as Discover, installs through the page's own dialog. + * A contributed tab, such as Discover, installs and updates through the page's own dialogs. * * Entries accept `page` for the form with its own save control and `summary` * for an official card's one-liner or a row's missing-description fallback. @@ -91,6 +91,16 @@ export interface PluginTabInstall { readonly version: string } +/** A newer Git version of an installed bundle that a tab asks the Plugins page to update to. */ +export interface PluginTabUpdate { + /** npm package name of the installed bundle. */ + readonly packageName: string + /** The target version. */ + readonly version: string + /** The Git spec that installs `version`, pinned to a full commit of the repository directory the bundle came from. */ + readonly spec: string +} + /** Owner props of a page tab another plugin contributes, such as Discover. */ export interface PluginTabProps { /** @@ -99,6 +109,13 @@ export interface PluginTabProps { * @param target - the spec, optional registry, and display text. */ readonly install: (target: PluginTabInstall) => void + /** + * Open the page's update dialog at its confirmation for a bundle the Host lists with `update: 'git'`, from the + * version the page lists; nothing opens before the page has listed the bundle or while another update runs. The + * update removes the installed version, installs the target, and installs the removed version again when that fails. + * @param target - the package, the target version, and its Git spec. + */ + readonly update: (target: PluginTabUpdate) => void /** * Switch to the Installed tab and open a bundle's page, whose back button returns to the calling tab. * @param packageName - npm package name of the bundle. @@ -118,8 +135,8 @@ declare module '@deepseek-ai/dsh-client-ui-slots' { 'plugins.add.actions': { kind: 'list'; scope: 'root'; owner: PluginAddActionsProps } /** * Tabs after the page's own Installed tab, such as Discover: `label` names the tab and `order` places it. The - * page renders the selected entry under its header and tabs; the entry draws its own content and installs - * through the owner's `install`. + * page renders the selected entry under its header and tabs; the entry draws its own content and installs and + * updates through the owner's `install` and `update`. */ 'plugins.tab': { kind: 'list'; scope: 'root'; owner: PluginTabProps } /** Optional guidance after the user enables a bundle from the list, keyed by npm package name. */ diff --git a/packages/client/ui-plugin-manager/src/client/versions.tsx b/packages/client/ui-plugin-manager/src/client/versions.tsx index dfb352a890..22231e4672 100644 --- a/packages/client/ui-plugin-manager/src/client/versions.tsx +++ b/packages/client/ui-plugin-manager/src/client/versions.tsx @@ -1,7 +1,8 @@ /** - * A registry bundle's Versions section and its update dialog. The section summarizes the Host's version check in one - * row and unfolds into the newer versions; the dialog confirms one update, follows the Host's steps, and reports how - * the update ended, including the reinstallation of the removed version after a failed installation. + * A registry bundle's Versions section and the update dialog. The section summarizes the Host's version check in one + * row and unfolds into the newer versions; the dialog confirms one update of a registry or Git copy, follows the + * Host's steps, and reports how the update ended, including the reinstallation of the removed version after a failed + * installation. */ import { useEffect, useState, type ReactNode } from 'react' @@ -12,7 +13,7 @@ import { Button, Checkbox, IconCheckOutlineRegular, IconChevronDownOutlineRegular, IconChevronRightOutlineRegular, IconCloseOutlineRegular, IconDownloadOutlineRegular, IconRefreshOutlineRegular, IconWarningOutlineRegular, Modal, StateDot, Tag, Tooltip, } from '@deepseek-ai/dsh-client-ui-primitives' -import { installable, updateCandidate, type PackageView, type UpdateState, type VersionCheck } from './manager-store.ts' +import { installable, updateCandidate, type PackageView, type UpdateSource, type UpdateState, type VersionCheck } from './manager-store.ts' import { managementText, registryText, restartText, type Translate } from './presentation.ts' import css from './PluginManagerPage.module.css' @@ -42,6 +43,12 @@ function registryName(registry: Registry, t: Translate): string { return registryText(registry, t, null).name } +/** Where an update installs from: the registry by name, or a pinned Git spec's repository as host and path. */ +function sourceName(source: UpdateSource, t: Translate): string { + if (source.kind === 'registry') return registryName(source.registry, t) + return source.spec.replace(/^git\+/i, '').replace(/#.*$/s, '').replace(/^[a-z]+:\/\//i, '').replace(/\.git$/i, '') +} + /** The DSH requirements an incompatible version states, as one phrase: `engines.dsh` or DSH peer ranges. */ function peersText(incompatible: IncompatiblePlugin): string { return Object.entries(incompatible.requirements).map(([name, range]) => `${name} ${range}`).join(', ') @@ -221,7 +228,7 @@ function VersionList({ list, checkedAt, t, busy, onUpdate }: { } /** - * The Versions section of an updatable bundle's page. It checks once when the page opens and folds the newer versions + * The Versions section of a registry bundle's page. It checks once when the page opens and folds the newer versions * behind a summary row. */ export function VersionSection({ pkg, check, t, busy, onEnsure, onCheck, onUpdate }: { @@ -235,9 +242,10 @@ export function VersionSection({ pkg, check, t, busy, onEnsure, onCheck, onUpdat readonly onUpdate: (version: string) => void }): ReactNode { const [open, setOpen] = useState(false) - const { name, updatable } = pkg - useEffect(() => { if (updatable) onEnsure(name) }, [name, updatable, onEnsure]) - if (!updatable) return null + const { name } = pkg + const listsVersions = pkg.update === 'registry' + useEffect(() => { if (listsVersions) onEnsure(name) }, [name, listsVersions, onEnsure]) + if (!listsVersions) return null const done = check?.phase === 'done' ? check : undefined const summary = summaryOf(check, t) const refused = done?.list.status === 'refused' ? done.list : undefined @@ -304,7 +312,7 @@ function stepLabel(step: PluginUpdateStep, update: UpdateState, t: Translate): s switch (step) { case 'checking': return t('updateStepChecking', { version: update.to }) case 'removing': return t('updateStepRemoving', { version: update.from }) - case 'installing': return t('updateStepInstalling', { version: update.to, registry: registryName(update.registry, t) }) + case 'installing': return t('updateStepInstalling', { version: update.to, source: sourceName(update.source, t) }) case 'restoring': return t('updateStepRestoring', { version: update.from }) } } @@ -367,7 +375,14 @@ function outcomeOf(update: UpdateState, title: string, t: Translate): Omit
{t('updateSource')}
-
{registryName(update.registry, t)}
+
{sourceName(update.source, t)}

{t('updateDescription', { from: update.from, to: update.to })}

diff --git a/packages/client/ui-plugin-manager/tests/components.client.spec.tsx b/packages/client/ui-plugin-manager/tests/components.client.spec.tsx index 79c530ccf4..640f2428c9 100644 --- a/packages/client/ui-plugin-manager/tests/components.client.spec.tsx +++ b/packages/client/ui-plugin-manager/tests/components.client.spec.tsx @@ -43,7 +43,6 @@ function pkg(overrides: Partial = {}): PackageView { installed: true, optional: false, removable: true, - updatable: false, enabled: true, rows: [], ...overrides, @@ -143,6 +142,7 @@ function renderTab( checkVersions: vi.fn(), ensureVersions: vi.fn(), openUpdate: vi.fn(), + openGitUpdate: vi.fn(), runUpdate: vi.fn(), closeUpdate: vi.fn(), toggleUpdateOutput: vi.fn(), @@ -235,6 +235,7 @@ describe('PluginManagerPage', () => { return ( <> + ) @@ -254,6 +255,8 @@ describe('PluginManagerPage', () => { expect(screen.queryByRole('button', { name: en.refresh })).toBeNull() fireEvent.click(screen.getByRole('button', { name: 'Discover install' })) expect(actions.installFrom).toHaveBeenCalledWith({ spec: 'dsh-a@1.0.0', registry: 'https://r.example/', title: 'Alpha', version: '1.0.0' }) + fireEvent.click(screen.getByRole('button', { name: 'Discover update' })) + expect(actions.openGitUpdate).toHaveBeenCalledWith({ packageName: 'dsh-a', version: '1.1.0', spec: 'git+https://git.example/o/a.git#bbbbbbb' }) fireEvent.click(screen.getByRole('button', { name: 'Discover open' })) expect(navigation.getSnapshot()).toEqual({ tab: 'installed', view: { kind: 'package', name: 'dsh-better-sidebar', returnTab: 'discover' } }) expect(document.querySelector('[data-plugin-detail="dsh-better-sidebar"]')).not.toBeNull() @@ -530,9 +533,9 @@ describe('PluginManagerPage', () => { it('says where a bundle comes from: the spec that installs it, or built in, with its version', () => { const b = renderTab({ packages: [ pkg({ source: 'github:someone/dsh-better-sidebar' }), - { name: 'dsh-official', installed: false, official: true, availability: 'installation', optional: true, removable: false, updatable: false, enabled: false, rows: [] }, - { name: 'dsh-shadowed', installed: true, official: false, availability: 'profile', optional: false, removable: false, updatable: false, enabled: true, rows: [] }, - { name: 'dsh-missing', installed: false, official: false, availability: 'missing', optional: false, removable: true, updatable: false, enabled: true, error: { code: 'unknown-plugin' }, rows: [] }, + { name: 'dsh-official', installed: false, official: true, availability: 'installation', optional: true, removable: false, enabled: false, rows: [] }, + { name: 'dsh-shadowed', installed: true, official: false, availability: 'profile', optional: false, removable: false, enabled: true, rows: [] }, + { name: 'dsh-missing', installed: false, official: false, availability: 'missing', optional: false, removable: true, enabled: true, error: { code: 'unknown-plugin' }, rows: [] }, ] }) const facts = (): string[] => [...document.querySelectorAll('[data-plugin-source] dt, [data-plugin-source] dd')].map(node => node.textContent) act(() => { b.navigation.actions.setView({ kind: 'package', name: 'dsh-better-sidebar' }) }) @@ -1524,7 +1527,7 @@ describe('PluginManagerPage', () => { }) it('leaves the version out of a bundle the Host reports none for', () => { - const unversioned: PackageView = { name: 'dsh-better-sidebar', installed: true, official: false, availability: 'profile', optional: false, removable: true, updatable: false, enabled: true, rows: [] } + const unversioned: PackageView = { name: 'dsh-better-sidebar', installed: true, official: false, availability: 'profile', optional: false, removable: true, enabled: true, rows: [] } renderTab({ packages: [unversioned] }, {}, bodies) fireEvent.click(screen.getByRole('button', { name: en.openDetail.replace('{name}', 'dsh-better-sidebar') })) expect(subjects.at(-1)).toEqual({ kind: 'bundle', pkg: { name: 'dsh-better-sidebar', installed: true, official: false, availability: 'profile', enabled: true, rows: [] } }) @@ -2692,7 +2695,7 @@ describe('on-demand Official bundles', () => { const name = '@deepseek-ai/dsh-subagent-codex' const title = 'Codex subagent' const catalog = (): PackageView => ({ name, meta: { title }, official: true, availability: 'missing', installed: false, - optional: false, removable: false, updatable: false, enabled: false, rows: [], + optional: false, removable: false, enabled: false, rows: [], installTarget: { spec: `${name}@2.0.0`, version: '2.0.0' }, }) @@ -2834,7 +2837,7 @@ it('keeps bundle configuration mounted while its operation is pending', () => { describe('registry versions and updates', () => { const MIRROR = 'https://registry.npmmirror.com/' - const updatable = (): PackageView => pkg({ updatable: true, version: '0.16.0', source: 'dsh-better-sidebar@0.16.0' }) + const updatable = (): PackageView => pkg({ update: 'registry', version: '0.16.0', source: 'dsh-better-sidebar@0.16.0' }) const runtime = '0.8.3' const listed = (versions: PluginVersion[], attempts: RegistryAttempt[] = [{ registry: null }]): VersionCheck => ({ phase: 'done', checkedAt: 0, @@ -2947,8 +2950,11 @@ describe('registry versions and updates', () => { expect(within(section()).queryByRole('checkbox')).toBeNull() }) - it('shows no Versions section for a package that is not updatable', () => { - const { actions, navigation } = renderTab({ packages: [pkg({ source: 'link:/dev/dsh-better-sidebar' })] }) + it.each([ + ['linked', pkg({ source: 'link:/dev/dsh-better-sidebar' })], + ['Git', pkg({ update: 'git', version: '0.16.0', source: 'git+https://git.example/o/plugins.git#aaaaaaa' })], + ])('shows no Versions section for a %s package', (_name, linked) => { + const { actions, navigation } = renderTab({ packages: [linked] }) open(navigation) expect(section()).toBeNull() expect(actions.ensureVersions).not.toHaveBeenCalled() @@ -2966,7 +2972,9 @@ describe('registry versions and updates', () => { expect(screen.getByText(en.updatesFailed)).toBeTruthy() }) - const base: UpdateState = { packageName: 'dsh-better-sidebar', from: '1.3.0', to: '1.4.0', registry: MIRROR, phase: 'confirm', steps: [], result: null, outputOpen: false } + const base: UpdateState = { + packageName: 'dsh-better-sidebar', from: '1.3.0', to: '1.4.0', source: { kind: 'registry', registry: MIRROR }, phase: 'confirm', steps: [], result: null, outputOpen: false, + } const change = (application: ChangeResult['application'], more: Partial = {}): ChangeResult => ({ changed: true, application, stage: 'install', target: 'dsh-better-sidebar', ...more, }) @@ -2983,6 +2991,14 @@ describe('registry versions and updates', () => { expect(actions.closeUpdate).toHaveBeenCalledOnce() }) + it('names a Git update\'s repository as the place it installs from', () => { + const source = { kind: 'git', spec: 'git+https://git.example/o/plugins.git#bbbbbbb&path:/packages/sidebar' } as const + const { set } = renderTab({ packages: [updatable()], update: { ...base, source } }) + expect(within(dialog()).getByText('git.example/o/plugins')).toBeTruthy() + set({ update: { ...base, source, phase: 'running', steps: ['checking', 'removing', 'installing'] } }) + expect(within(dialog()).getByText('Install 1.4.0 from git.example/o/plugins')).toBeTruthy() + }) + it('names a package that left the list by its package name', () => { renderTab({ packages: [], update: base }) expect(within(dialog()).getByText(en.updateTitle.replace('{name}', 'dsh-better-sidebar'))).toBeTruthy() @@ -3029,6 +3045,16 @@ describe('registry versions and updates', () => { expect(actions.closeUpdate).toHaveBeenCalledTimes(2) }) + it.each([ + ['a typed refusal', { code: 'unexpected-package' as const }, `1.4.0 failed to install; 1.3.0 was installed again. ${en.reasonUnexpectedPackage}`], + ['pnpm\'s own failure', { code: 'operation-error' as const, diagnostic: 'ERR_PNPM_FETCH_404' }, '1.4.0 failed to install; 1.3.0 was installed again.'], + ])('says why a restored update failed for %s', (_name, error, detail) => { + renderTab({ packages: [updatable()], update: { + ...base, phase: 'done', result: { from: '1.3.0', to: '1.4.0', step: 'install', result: change('failed', { error }), restore: change('applied') }, + } }) + expect(within(dialog()).getByText(detail)).toBeTruthy() + }) + it('lists the steps the answer implies beside the announced ones', () => { renderTab({ packages: [updatable()], update: { ...base, phase: 'done', steps: ['checking', 'removing'], diff --git a/packages/client/ui-plugin-manager/tests/manager-store.client.spec.ts b/packages/client/ui-plugin-manager/tests/manager-store.client.spec.ts index dd8cb347e7..eb44f8eeb5 100644 --- a/packages/client/ui-plugin-manager/tests/manager-store.client.spec.ts +++ b/packages/client/ui-plugin-manager/tests/manager-store.client.spec.ts @@ -25,7 +25,6 @@ const BUNDLE: BundleInfo = { source: 'dsh-better-sidebar@^0.16.0', optional: false, removable: true, - updatable: false, rows: [{ rowId: 'sidebar', moduleName: 'dsh-better-sidebar', entryId: ROW_ENTRY }, { rowId: 'theme', moduleName: 'dsh-better-sidebar/theme' }], overrides: [], } @@ -132,7 +131,7 @@ describe('packageView', () => { it('joins a bundle with the entries its rows run as', () => { expect(packageView(BUNDLE, PLUGINS)).toEqual({ name: 'dsh-better-sidebar', version: '0.16.0', description: 'A sidebar.', source: 'dsh-better-sidebar@^0.16.0', - installed: true, official: false, availability: 'profile', optional: false, removable: true, updatable: false, enabled: false, + installed: true, official: false, availability: 'profile', optional: false, removable: true, enabled: false, rows: [ { rowId: 'sidebar', moduleName: 'dsh-better-sidebar', entryId: ROW_ENTRY, enabled: true, phase: 'active' }, { rowId: 'theme', moduleName: 'dsh-better-sidebar/theme', enabled: false, phase: null }, @@ -140,13 +139,13 @@ describe('packageView', () => { }) // A row the inventory no longer lists, a protected row, and a bundle the Host cannot read. const protectedBundle: BundleInfo = { - name: '@deepseek-ai/dsh-base', enabled: true, installed: false, official: false, availability: 'installation', optional: false, removable: false, updatable: false, readOnlyReason: 'management-required', + name: '@deepseek-ai/dsh-base', enabled: true, installed: false, official: false, availability: 'installation', optional: false, removable: false, readOnlyReason: 'management-required', error: { code: 'operation-error', diagnostic: 'broken' }, rows: [{ rowId: 'core', moduleName: '@deepseek-ai/dsh-base', entryId: 'include:core' as PluginEntryId }, { rowId: 'gone', moduleName: 'x', entryId: 'include:gone' as PluginEntryId }], overrides: [], } expect(packageView(protectedBundle, PLUGINS)).toEqual({ - name: '@deepseek-ai/dsh-base', installed: false, official: false, availability: 'installation', optional: false, removable: false, updatable: false, enabled: true, readOnlyReason: 'management-required', + name: '@deepseek-ai/dsh-base', installed: false, official: false, availability: 'installation', optional: false, removable: false, enabled: true, readOnlyReason: 'management-required', error: { code: 'operation-error', diagnostic: 'broken' }, rows: [ { rowId: 'core', moduleName: '@deepseek-ai/dsh-base', entryId: 'include:core', enabled: true, phase: 'active', readOnlyReason: 'management-required' }, @@ -158,7 +157,7 @@ describe('packageView', () => { describe('sortPackages', () => { it('orders packages by the short name a person reads, not by the Host order or enablement', async () => { - const plain = { enabled: true, installed: true, official: false, availability: 'profile' as const, optional: false, removable: true, updatable: false, rows: [], overrides: [] } + const plain = { enabled: true, installed: true, official: false, availability: 'profile' as const, optional: false, removable: true, rows: [], overrides: [] } const zeta: BundleInfo = { ...plain, name: 'dsh-zeta' } const alpha: BundleInfo = { ...plain, name: '@acme/dsh-alpha', enabled: false } const views = [zeta, BUNDLE, alpha].map(bundle => packageView(bundle, PLUGINS)) @@ -769,7 +768,7 @@ describe('PluginManagerController', () => { it('accepts a bundled package without optional version or description and reports a lost activation reply', async () => { const shipped: BundleInfo = { name: '@acme/shipped-exploration', installed: false, official: true, enabled: false, - availability: 'installation', optional: true, removable: false, updatable: false, listedByDefault: false, rows: [], overrides: [] } + availability: 'installation', optional: true, removable: false, listedByDefault: false, rows: [], overrides: [] } const { plugins, face, state, controller } = bench({ listBundles: vi.fn(() => Promise.resolve(ok([shipped]))), setBundleEnabled: vi.fn(() => Promise.resolve(refused('gateway/internal', 'offline'))), @@ -1975,7 +1974,7 @@ it('reports a successful install with only documented result fields', async () = const CATALOG: BundleInfo = { name: '@deepseek-ai/dsh-subagent-codex', official: true, availability: 'missing', installed: false, optional: false, - enabled: false, removable: false, updatable: false, rows: [], overrides: [], + enabled: false, removable: false, rows: [], overrides: [], installTarget: { spec: '@deepseek-ai/dsh-subagent-codex@2.0.0', version: '2.0.0' }, } @@ -2210,7 +2209,7 @@ it('deselects a source bundle whose target is unavailable without starting insta }) describe('version checks and updates', () => { - const UPDATABLE: BundleInfo = { ...BUNDLE, updatable: true } + const UPDATABLE: BundleInfo = { ...BUNDLE, update: 'registry' } const LISTED = { status: 'listed' as const, installed: '0.16.0', registry: MIRROR, omitted: 0, attempts: [{ registry: null, problem: 'network' as const, reason: 'ETIMEDOUT' }, { registry: MIRROR }], @@ -2257,7 +2256,7 @@ describe('version checks and updates', () => { await vi.waitFor(() => { expect(state().versions['dsh-a']?.phase).toBe('done') }) await vi.waitFor(() => { expect(state().versions['dsh-better-sidebar']?.phase).toBe('done') }) // Another surface reinstalled one package at another version and replaced the other by a linked copy. - listBundles.mockResolvedValue(ok([{ ...UPDATABLE, version: '0.17.0' }, { ...UPDATABLE, name: 'dsh-a', updatable: false }])) + listBundles.mockResolvedValue(ok([{ ...UPDATABLE, version: '0.17.0' }, { ...BUNDLE, name: 'dsh-a' }])) await controller.load() expect(state().versions).toEqual({}) }) @@ -2299,7 +2298,8 @@ describe('version checks and updates', () => { await vi.waitFor(() => { expect(state().versions['dsh-better-sidebar']?.phase).toBe('done') }) face.openUpdate('dsh-better-sidebar', '0.17.0') expect(state().update).toEqual({ - packageName: 'dsh-better-sidebar', from: '0.16.0', to: '0.17.0', registry: MIRROR, phase: 'confirm', steps: [], result: null, outputOpen: false, + packageName: 'dsh-better-sidebar', from: '0.16.0', to: '0.17.0', source: { kind: 'registry', registry: MIRROR }, + phase: 'confirm', steps: [], result: null, outputOpen: false, }) face.closeUpdate() expect(state().update).toBeNull() @@ -2362,6 +2362,39 @@ describe('version checks and updates', () => { await late.promise expect(state().update?.phase).toBe('running') }) + + it('confirms a Git update another tab names once the list holds the package, runs it with the spec, and checks no versions', async () => { + const spec = 'git+https://git.example/o/plugins.git#bbbbbbb&path:/packages/sidebar' + const answer = deferred>>() + const updateBundle = vi.fn(() => answer.promise) + const listVersions = vi.fn(() => Promise.resolve(ok(LISTED))) + const listBundles = vi.fn(() => Promise.resolve(ok([ + { ...BUNDLE, update: 'git' as const, source: 'git+https://git.example/o/plugins.git#aaaaaaa&path:/packages/sidebar' }, + { ...BUNDLE, name: 'dsh-registry', update: 'registry' as const }, + ]))) + const { controller, face, state } = bench({ updateBundle, listVersions, listBundles }) + const target = { packageName: 'dsh-better-sidebar', version: '0.17.0', spec } + // The installed version comes from the list, which holds nothing before its first read and only Git copies update so. + face.openGitUpdate(target) + expect(state().update).toBeNull() + await controller.load() + face.openGitUpdate({ ...target, packageName: 'dsh-registry' }) + expect(state().update).toBeNull() + face.openGitUpdate(target) + expect(state().update).toEqual({ + packageName: 'dsh-better-sidebar', from: '0.16.0', to: '0.17.0', source: { kind: 'git', spec }, phase: 'confirm', steps: [], result: null, outputOpen: false, + }) + face.runUpdate() + const requestId = state().update?.requestId as PluginInstallRequestId + expect(updateBundle).toHaveBeenCalledWith('dsh-better-sidebar', '0.17.0', { spec, requestId }) + face.openGitUpdate({ ...target, version: '1.0.0' }) + expect(state().update).toMatchObject({ phase: 'running', to: '0.17.0' }) + answer.resolve(ok(UPDATED)) + await vi.waitFor(() => { expect(state().update).toMatchObject({ phase: 'done', result: UPDATED }) }) + await vi.waitFor(() => { expect(listBundles).toHaveBeenCalledTimes(2) }) + await new Promise((resolve) => { setTimeout(resolve, 0) }) + expect(listVersions).not.toHaveBeenCalled() + }) }) diff --git a/packages/client/ui-plugin-market/README.i18n.yaml b/packages/client/ui-plugin-market/README.i18n.yaml index f7f071560f..64bebd3d61 100644 --- a/packages/client/ui-plugin-market/README.i18n.yaml +++ b/packages/client/ui-plugin-market/README.i18n.yaml @@ -3,23 +3,23 @@ # After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/ui-plugin-market/README.md /: - en: b77d6d729562a88f - zh: b1634cc1d027424c + en: 3685af81d8cbfba4 + zh: 2ec4eeb51778e2c8 /deepseek-ai-dsh-client-ui-plugin-market: en: 62972eb0dff8c145 zh: 682cc03b492cd730 /deepseek-ai-dsh-client-ui-plugin-market/summary: - en: 71328d505b984fe5 - zh: 9281e7dbaa01ef9a + en: ee0bd00afa1e0887 + zh: b54e1bdd523437dd /deepseek-ai-dsh-client-ui-plugin-market/table-of-contents: en: 76368b6b1da79c80 zh: 52384aee991ac542 /deepseek-ai-dsh-client-ui-plugin-market/use-this-package: - en: 4e582bcd8434b513 - zh: 39334bf8ae435e09 + en: d633d36f830d052c + zh: 07de610c0b1bb34c /deepseek-ai-dsh-client-ui-plugin-market/understand-the-implementation: - en: e1a480caaff8efea - zh: 0b725fa5bc929f22 + en: 66cef49ded72a290 + zh: c89a13fa5e5fb965 /deepseek-ai-dsh-client-ui-plugin-market/model-experience: en: 08d25ab9cbdd7567 zh: 26e6e75c123951ee @@ -27,8 +27,8 @@ en: 4f0eadf94828e66e zh: 4f8a6ec170ebbfb6 /deepseek-ai-dsh-client-ui-plugin-market/known-limitations-and-deferred-work: - en: 493d1326b1645f3f - zh: 878a53ed6cb7878d + en: 1a06b23ccce41074 + zh: 8c4f5d607b342ef9 /deepseek-ai-dsh-client-ui-plugin-market/known-limitations-and-deferred-work/dev-note: en: e67f633dc6fe7773 zh: 3bb0c20c93f16f98 diff --git a/packages/client/ui-plugin-market/README.md b/packages/client/ui-plugin-market/README.md index ccffb81311..b4d70f233b 100644 --- a/packages/client/ui-plugin-market/README.md +++ b/packages/client/ui-plugin-market/README.md @@ -1,5 +1,5 @@ --- -description: "Discover tab of the Web Plugins page: search configured plugin indexes, read an entry, and install it." +description: "Discover tab of the Web Plugins page: search configured plugin indexes, read an entry, and install or update it." kind: "package-reference" --- @@ -9,7 +9,7 @@ English | [中文](README.zh.md) ## Summary -Discover is the second tab of the **Plugins** page, after **Installed**. It lists the bundles that plugin indexes offer; users manage index sources, switch between indexes, search, filter by category and label, sort, open an entry's page, and install an entry after confirming it. Entry pages show each entry's long description as Markdown with images. The Host [`pluginMarket`](../../boot/plugin-market/README.md) service fetches and validates indexes; this package only presents them. +Discover is the second tab of the **Plugins** page, after **Installed**. It lists the bundles that plugin indexes offer; users manage index sources, switch between indexes, search, filter by category and label, sort, open an entry's page, install an entry after confirming it, and update a bundle installed from Git to a newer Git version. Entry pages show each entry's long description as Markdown with images. The Host [`pluginMarket`](../../boot/plugin-market/README.md) service fetches and validates indexes; this package only presents them. ## Table of Contents @@ -28,9 +28,9 @@ Open **Plugins** and select **Discover**. With no index source, the tab shows an The toolbar holds the index menu, which switches indexes and ends with **Manage index sources…**; the search field; a **Labels** menu with multi-select checkmarks when the index declares labels; the **Sort** menu; and a refresh button. Controls wrap onto additional rows when space is limited, keeping search, filtering, sorting, and refresh available on mobile. Category pills, with **All** first, follow when the index declares categories. -Each card shows the icon, localized title, label tags, a one-line description, and the default version, author, and publish date, with one action: **Install** for an installable entry; a disabled **Install** whose tooltip says no version is compatible with the running DSH when the plugin has no default version and its highest version is neither compatible nor exempted; or **Installed**, **Update available**, or **Provided by DSH**, which switches to the Installed tab and opens that bundle's page when that tab lists it. Selecting a card opens the entry's page with its full description, compatibility notice, and, under the source heading, the default version's facts: author; homepage and source code repository, as links that open in a new tab, with the package directory beside the repository; the npm package and registry, or the Git repository, full commit, and directory; publish date; category; labels; and index title. The page shows the long description first, without a heading of its own, then **Versions**, then the source facts. The **Versions** list is in descending SemVer order; each row tags the recommended, prerelease, installed, incompatible, and exempted versions and offers **Install** for any other version installation accepts, except for a bundle DSH provides. The page's back link returns to the list with its search, filters, and sort intact. +Each card shows the icon, localized title, label tags, a one-line description, and the default version, author, and publish date, with one action: **Update** for a bundle installed from a Git commit when the default version, else the newest stable version, is a newer commit of the repository directory it came from; **Install** for an installable entry; a disabled **Install** whose tooltip says no version is compatible with the running DSH when the plugin has no default version and its highest version is neither compatible nor exempted; or **Installed**, **Update available**, or **Provided by DSH**, which switches to the Installed tab and opens that bundle's page when that tab lists it. Selecting a card opens the entry's page with its full description, compatibility notice, and, under the source heading, the default version's facts: author; homepage and source code repository, as links that open in a new tab, with the package directory beside the repository; the npm package and registry, or the Git repository, full commit, and directory; publish date; category; labels; and index title. The page shows the long description first, without a heading of its own, then **Versions**, then the source facts. The **Versions** list is in descending SemVer order; each row tags the recommended, prerelease, installed, incompatible, and exempted versions. A row offers **Update** for each version the Host marks as an update of a bundle installed from a Git commit, and **Install** while the entry is not installed, each only for a version installation accepts; installation never goes over an installed or provided bundle. The page's back link returns to the list with its search, filters, and sort intact. -**Install** chooses the default version and opens a confirmation dialog with the entry's icon, title, that version, the author, its source facts, an exemption notice when one applies, and a trust notice. Confirming opens the Plugins install dialog at its check screen with the entry's exact spec and registry; there is no spec form, and a refusal offers **Retry**. The registry an entry names applies to that install only. +**Install** chooses the default version and opens a confirmation dialog with the entry's icon, title, that version, the author, its source facts, an exemption notice when one applies, and a trust notice. Confirming opens the Plugins install dialog at its check screen with the entry's exact spec and registry; there is no spec form, and a refusal offers **Retry**. The registry an entry names applies to that install only. **Update** opens the Plugins page's update confirmation without a dialog of its own; it names the installed and target versions and the repository, and the update removes the installed copy, installs the version's pinned Git spec, and installs the removed copy again when that fails. The Host [`pluginMarket.search`](../../boot/plugin-market/README.md#use-this-package) matches, filters, orders, and pages; the tab sends the search text 250 ms after typing stops. Search matches names, authors, keywords, and every language of titles, descriptions, categories, and labels, fuzzily. Label filters must all match. **Sort** offers **Best match** while there is search text, **Default order**, **Publish date**, and each declared sort key, which starts in its declared direction; a directed sort adds a row that reverses it. Typing switches **Default order** to **Best match**, and clearing the text switches back. Categories, labels, and sort keys are listed by localized title. The list loads 50 entries and adds 50 with each **Show more**. @@ -45,7 +45,7 @@ Reading an index shows card skeletons. A failed read shows a short sentence with Detail reads are kept in a second snapshot store keyed by index URL, entry, version, and language; a section starts its read when it renders, an npm version's read first asks the Host's registry comparison and passes `npmFileSources: ['npmmirror']` when npmmirror answered fastest, only the latest read of a key settles it, **Retry** asks the Host to read again instead of returning its cached result, and a connection reset forgets every read, because a restarted Host no longer knows the document ids its images use, so each shown section reads its document again. Image URLs resolve `api/plugin-market/detail-file` against `document.baseURI`, so they keep a deployment prefix and use the Desktop `dsh-app://app/` origin, which [`MarkdownText`](../ui-primitives/README.md) accepts for that route. -The plugin registers the `discover` entry, labelled **Discover**, into `plugins.tab`, the tab slot that [`ui-plugin-manager`](../ui-plugin-manager/README.md) declares; the owner props provide `install({ spec, registry?, title, version })` and `openPackage(name)`; Discover passes the entry's localized title and the chosen version. A React-free controller owns source reads, source edits, and the selected index's query and loaded pages in a snapshot store; selecting another index starts from the initial query. Later pages send the first page's `fetchedAt`, and a page from a replaced document reloads the list once; a second replacement fails the read. The listing keeps the query its results answer, so the empty-state wording follows the shown results while a new query loads or after it fails, and **Retry** repeats the read that failed: the first pages of a query or the next page. A change to the profile's bundles reloads every loaded page to refresh installation status, and a filter or sort key the index no longer declares leaves the query. The unsent search text, the open entry, and the confirmation are component state. Pure helpers in `presentation.ts` derive the query order from the text, drop undeclared ids, order declarations with `Intl.Collator` in the active UI language, and list source facts. Icons load lazily with `referrerpolicy="no-referrer"` and fall back to the default artwork. +The plugin registers the `discover` entry, labelled **Discover**, into `plugins.tab`, the tab slot that [`ui-plugin-manager`](../ui-plugin-manager/README.md) declares; the owner props provide `install({ spec, registry?, title, version })`, `update({ packageName, version, spec })`, and `openPackage(name)`; Discover passes the entry's localized title and the chosen version to `install`, and the version a Host listing marks `update` with its install spec to `update`. A React-free controller owns source reads, source edits, and the selected index's query and loaded pages in a snapshot store; selecting another index starts from the initial query. Later pages send the first page's `fetchedAt`, and a page from a replaced document reloads the list once; a second replacement fails the read. The listing keeps the query its results answer, so the empty-state wording follows the shown results while a new query loads or after it fails, and **Retry** repeats the read that failed: the first pages of a query or the next page. A change to the profile's bundles reloads every loaded page to refresh installation status, and a filter or sort key the index no longer declares leaves the query. The unsent search text, the open entry, and the confirmation are component state. Pure helpers in `presentation.ts` derive the query order from the text, drop undeclared ids, order declarations with `Intl.Collator` in the active UI language, and list source facts. Icons load lazily with `referrerpolicy="no-referrer"` and fall back to the default artwork. ----- @@ -62,7 +62,7 @@ None. -- **No upgrade action** — an available update is shown as a status only. +- **Git updates only** — Discover updates only a bundle installed from a Git commit. For a bundle installed from a registry, **Update available** opens its Installed page, whose Versions section updates it from the registry. For any other bundle with a newer index version, including a Git copy whose newer versions come from npm, another repository, or are incompatible, **Update available** opens an Installed page that offers no update; uninstall it and install it again. - **Plain Markdown only** — detail documents omit raw HTML, including HTML images and layout, and relative links, including in-page anchors, are not clickable. - **Remote icons** — URL icons are requested by the browser, which discloses the user's IP address to the image host. When the Web UI is served over HTTPS, the browser blocks or upgrades `http:` icons from an `http:` index, and a failed icon shows the default artwork. diff --git a/packages/client/ui-plugin-market/README.zh.md b/packages/client/ui-plugin-market/README.zh.md index db4e749056..77c1d7d638 100644 --- a/packages/client/ui-plugin-market/README.zh.md +++ b/packages/client/ui-plugin-market/README.zh.md @@ -1,5 +1,5 @@ --- -description: "Web 插件页的「发现」标签页:搜索已配置的插件索引,查看条目并安装。" +description: "Web 插件页的「发现」标签页:搜索已配置的插件索引,查看条目并安装或更新。" kind: "package-reference" --- @@ -9,7 +9,7 @@ kind: "package-reference" ## 概述 -「发现」是**插件**页的第二个标签页,位于**已安装**之后。它列出插件索引提供的组合包(bundle);用户可以管理索引源、在插件索引之间切换、搜索、按分类和标签筛选、排序、打开条目页面,并在确认后安装条目。条目页面以带图片的 Markdown 显示每个条目的详细介绍。Host 的 [`pluginMarket`](../../boot/plugin-market/README.zh.md) 服务负责获取和校验索引;本包只负责展示。 +「发现」是**插件**页的第二个标签页,位于**已安装**之后。它列出插件索引提供的组合包(bundle);用户可以管理索引源、在插件索引之间切换、搜索、按分类和标签筛选、排序、打开条目页面、在确认后安装条目,以及把从 Git 安装的组合包更新到较新的 Git 版本。条目页面以带图片的 Markdown 显示每个条目的详细介绍。Host 的 [`pluginMarket`](../../boot/plugin-market/README.zh.md) 服务负责获取和校验索引;本包只负责展示。 ## 目录 @@ -28,9 +28,9 @@ kind: "package-reference" 工具栏包含:插件索引菜单,用于切换插件索引,最后一项为**管理索引源…**;搜索框;插件索引声明了标签时显示的**标签**菜单,以勾选标记支持多选;**排序**菜单;以及刷新按钮。空间不足时,控件自动换行,确保手机端仍可使用搜索、筛选、排序和刷新。插件索引声明了分类时,其后显示分类按钮,**全部**排在第一位。 -每张卡片显示图标、本地化标题、标签、一行描述,以及默认版本、作者和发布日期,并只有一个操作:可安装的条目显示**安装**;没有默认版本、且最高版本既不兼容也未豁免的条目显示禁用的**安装**,其提示框说明没有与当前运行的 DSH 兼容的版本;其余条目显示**已安装**、**有新版本**或**随 DSH 提供**,点击后切换到「已安装」标签页,并在该标签页列出该组合包时打开其页面。选择卡片会打开条目页面,显示其完整描述、兼容性提示,并在来源标题下显示默认版本的信息:作者;主页和源代码仓库(在新标签页中打开的链接,仓库旁显示包所在目录);npm 包和注册表,或 Git 仓库、完整提交和目录;发布日期;分类;标签;以及插件索引标题。页面先显示详细介绍,不带单独的标题,然后是**版本**,然后是来源信息。**版本**列表按 SemVer 降序排列;每行标出推荐、预发布、已安装、不兼容和已豁免的版本,并为安装检查接受的其他版本提供**安装**,DSH 随附的组合包除外。页面的返回链接回到列表,搜索、筛选和排序保持不变。 +每张卡片显示图标、本地化标题、标签、一行描述,以及默认版本、作者和发布日期,并只有一个操作:从 Git 提交安装的组合包在默认版本(没有默认版本时为最新的稳定版本)是其来源仓库目录中较新的提交时显示**更新**;可安装的条目显示**安装**;没有默认版本、且最高版本既不兼容也未豁免的条目显示禁用的**安装**,其提示框说明没有与当前运行的 DSH 兼容的版本;其余条目显示**已安装**、**有新版本**或**随 DSH 提供**,点击后切换到「已安装」标签页,并在该标签页列出该组合包时打开其页面。选择卡片会打开条目页面,显示其完整描述、兼容性提示,并在来源标题下显示默认版本的信息:作者;主页和源代码仓库(在新标签页中打开的链接,仓库旁显示包所在目录);npm 包和注册表,或 Git 仓库、完整提交和目录;发布日期;分类;标签;以及插件索引标题。页面先显示详细介绍,不带单独的标题,然后是**版本**,然后是来源信息。**版本**列表按 SemVer 降序排列;每行标出推荐、预发布、已安装、不兼容和已豁免的版本。Host 标为某个从 Git 提交安装的组合包之更新的每个版本,其行提供**更新**;条目未安装时,行提供**安装**;两者都只针对安装检查接受的版本。安装从不覆盖已安装或 DSH 随附的组合包。页面的返回链接回到列表,搜索、筛选和排序保持不变。 -**安装**选择默认版本并打开确认对话框,显示条目的图标、标题、该版本、作者、来源信息、适用时的豁免提示,以及信任提示。确认后,插件页的安装对话框以该条目的精确 spec 和注册表在检查界面打开;不显示 spec 表单,被拒绝时提供**重试**。条目指定的注册表只用于这一次安装。 +**安装**选择默认版本并打开确认对话框,显示条目的图标、标题、该版本、作者、来源信息、适用时的豁免提示,以及信任提示。确认后,插件页的安装对话框以该条目的精确 spec 和注册表在检查界面打开;不显示 spec 表单,被拒绝时提供**重试**。条目指定的注册表只用于这一次安装。**更新**不打开自己的对话框,直接打开插件页的更新确认框;该确认框显示已安装版本、目标版本和仓库;更新会移除已安装副本,安装该版本固定的 Git spec,安装失败时重新安装被移除的副本。 Host 的 [`pluginMarket.search`](../../boot/plugin-market/README.zh.md#use-this-package) 负责匹配、过滤、排序和分页;标签页在停止输入 250 毫秒后发送搜索文本。搜索模糊匹配名称、作者、关键词,以及标题、描述、分类和标签的所有语言。所选标签必须全部匹配。有搜索文本时,**排序**提供**最佳匹配**,此外还提供**默认顺序**、**发布时间**以及每个已声明的排序键,排序键以其声明的方向开始;有方向的排序会额外显示一行,用于反转方向。输入文本会把**默认顺序**切换为**最佳匹配**,清空文本时切换回来。分类、标签和排序键按本地化标题列出。列表加载 50 个条目,每次点击**显示更多**再增加 50 个。 @@ -45,7 +45,7 @@ Host 的 [`pluginMarket.search`](../../boot/plugin-market/README.zh.md#use-this- 详细介绍的读取结果保存在第二个快照存储中,以索引 URL、条目、版本和语言为键;该部分在渲染时开始读取;npm 版本的读取先询问 Host 的注册表比较,npmmirror 响应最快时传入 `npmFileSources: ['npmmirror']`;只有某个键最新一次的读取才会让该部分得到结果;**重试**要求 Host 重新读取,而不是返回其缓存的结果;连接重置时丢弃所有读取结果,因为重启后的 Host 不再知道其图片使用的文档 id,所以每个已显示的部分会重新读取其文档。图片 URL 基于 `document.baseURI` 解析 `api/plugin-market/detail-file`,因此保留部署前缀,并在 Desktop 中使用 `dsh-app://app/` origin;[`MarkdownText`](../ui-primitives/README.zh.md) 对该路由接受此 origin。 -本插件把标签为**发现**的 `discover` 条目注册到 `plugins.tab`,即 [`ui-plugin-manager`](../ui-plugin-manager/README.zh.md) 声明的标签页 slot;owner props 提供 `install({ spec, registry?, title, version })` 和 `openPackage(name)`;发现页传入条目的本地化标题和所选版本。一个不依赖 React 的控制器在快照存储中拥有索引源读取、索引源编辑,以及所选索引的查询和已加载的页;选择另一个插件索引时从初始查询开始。之后的页发送第一页的 `fetchedAt`,来自已替换文档的页会让列表重新加载一次;再次被替换时读取失败。列表保留其结果所回答的查询,因此新查询加载期间或失败后,空状态文案仍与显示的结果一致;**重试**重新执行失败的读取:某个查询的前几页,或下一页。profile 的组合包变化时,重新加载所有已加载的页以刷新安装状态;索引不再声明的过滤条件或排序键会从查询中移除。尚未发送的搜索文本、打开的条目和确认对话框是组件状态。`presentation.ts` 中的纯函数根据文本得出查询顺序、移除未声明的 id、使用当前界面语言的 `Intl.Collator` 对声明排序,以及列出来源信息。图标以 `referrerpolicy="no-referrer"` 延迟加载,失败时回退到默认插画。 +本插件把标签为**发现**的 `discover` 条目注册到 `plugins.tab`,即 [`ui-plugin-manager`](../ui-plugin-manager/README.zh.md) 声明的标签页 slot;owner props 提供 `install({ spec, registry?, title, version })`、`update({ packageName, version, spec })` 和 `openPackage(name)`;发现页向 `install` 传入条目的本地化标题和所选版本,向 `update` 传入 Host 列表标有 `update` 的版本及其安装 spec。一个不依赖 React 的控制器在快照存储中拥有索引源读取、索引源编辑,以及所选索引的查询和已加载的页;选择另一个插件市场时从初始查询开始。之后的页发送第一页的 `fetchedAt`,来自已替换文档的页会让列表重新加载一次;再次被替换时读取失败。列表保留其结果所回答的查询,因此新查询加载期间或失败后,空状态文案仍与显示的结果一致;**重试**重新执行失败的读取:某个查询的前几页,或下一页。profile 的组合包变化时,重新加载所有已加载的页以刷新安装状态;索引不再声明的过滤条件或排序键会从查询中移除。尚未发送的搜索文本、打开的条目和确认对话框是组件状态。`presentation.ts` 中的纯函数根据文本得出查询顺序、移除未声明的 id、使用当前界面语言的 `Intl.Collator` 对声明排序,以及列出来源信息。图标以 `referrerpolicy="no-referrer"` 延迟加载,失败时回退到默认插画。 ----- @@ -62,7 +62,7 @@ Host 的 [`pluginMarket.search`](../../boot/plugin-market/README.zh.md#use-this- -- **没有升级操作**——可用更新只显示为状态。 +- **只支持 Git 更新**——「发现」只更新从 Git 提交安装的组合包。对于从注册表安装的组合包,**有新版本**会打开其在「已安装」中的页面,由该页面的版本区块从注册表更新。对于插件索引中有较新版本的其他组合包,包括较新版本来自 npm、来自另一个仓库或不兼容的 Git 副本,**有新版本**会打开其在「已安装」中的页面,但该页面不提供更新;需要先卸载再安装。 - **只支持纯 Markdown**——详细介绍文档省略原始 HTML(包括 HTML 图片和排版),相对链接(包括页内锚点)不可点击。 - **远程图标**——URL 图标由浏览器请求,会向图片主机暴露用户的 IP 地址。Web UI 通过 HTTPS 提供时,浏览器会拦截或升级来自 `http:` 索引的 `http:` 图标,加载失败的图标显示默认插画。 diff --git a/packages/client/ui-plugin-market/src/client/DiscoverSection.tsx b/packages/client/ui-plugin-market/src/client/DiscoverSection.tsx index cad12593bf..ff59f12906 100644 --- a/packages/client/ui-plugin-market/src/client/DiscoverSection.tsx +++ b/packages/client/ui-plugin-market/src/client/DiscoverSection.tsx @@ -1,4 +1,4 @@ -/** Discover: search one plugin market index, read an entry's page, and install a version. */ +/** Discover: search one plugin market index, read an entry's page, and install or update a version. */ import { useEffect, useMemo, useState, type ReactNode } from 'react' import { Button, IconChevronDownOutlineRegular, IconPlusOutlineRegular, IconRefreshOutlineRegular, IconSearchOutlineRegular, @@ -75,19 +75,46 @@ function installable(compatibility: MarketCompatibility): boolean { return compatibility.compatible || compatibility.exempted } +/** Whether a version carries a SemVer prerelease suffix. */ +function prerelease(version: string): boolean { + return version.split('+')[0]?.includes('-') === true +} + +/** The version Update offers for an entry: the presented one when it updates the installed copy, else the newest stable update. */ +function updateTarget(entry: MarketListingEntry): MarketListingVersion | undefined { + const updates = (version: MarketListingVersion): boolean => version.update && installable(version.compatibility) + const presented = presentedVersion(entry) + return updates(presented) ? presented : entry.versions.find(version => updates(version) && !prerelease(version.version)) +} + function formatDate(timestamp: number, language: string, style: 'medium' | 'long'): string { return new Intl.DateTimeFormat(language, { dateStyle: style }).format(new Date(timestamp * 1000)) } -/** The one action an entry offers in its current state; Install chooses its default version. */ -function EntryAction({ entry, title, t, onInstall, onOpenInstalled }: { +/** What an entry's card and page do for its actions: install or update a version, or open the installed bundle. */ +interface EntryCallbacks { + readonly onInstall: (version: MarketListingVersion) => void + readonly onUpdate: (version: MarketListingVersion) => void + readonly onOpenInstalled: () => void +} + +/** The one action an entry offers in its current state; Install chooses its default version, and Update its update target. */ +function EntryAction({ entry, title, t, onInstall, onUpdate, onOpenInstalled }: { readonly entry: MarketListingEntry readonly title: string readonly t: Translate - readonly onInstall: (version: MarketListingVersion) => void - readonly onOpenInstalled: () => void -}): ReactNode { +} & EntryCallbacks): ReactNode { const { status } = entry.installation + const target = updateTarget(entry) + if (target !== undefined) { + return ( + + ) + } + const version = presentedVersion(entry) if (status !== 'not-installed') { return ( ) } - const version = presentedVersion(entry) if (!version.compatibility.compatible && !version.compatibility.exempted) { return ( @@ -108,16 +134,14 @@ function EntryAction({ entry, title, t, onInstall, onOpenInstalled }: { return } -function EntryCard({ entry, t, face, language, labels, onOpen, onInstall, onOpenInstalled }: { +function EntryCard({ entry, t, face, language, labels, onOpen, onInstall, onUpdate, onOpenInstalled }: { readonly entry: MarketListingEntry readonly t: Translate readonly face: DiscoverActions readonly language: string readonly labels: readonly DisplayDeclaration[] readonly onOpen: () => void - readonly onInstall: (version: MarketListingVersion) => void - readonly onOpenInstalled: () => void -}): ReactNode { +} & EntryCallbacks): ReactNode { const title = entryTitle(entry, face) const version = presentedVersion(entry) const date = formatDate(version.publishTimestamp, language, 'medium') @@ -137,32 +161,32 @@ function EntryCard({ entry, t, face, language, labels, onOpen, onInstall, onOpen
- +
) } -/** One row of the version list: its state tags and an Install button when installation accepts it. */ -function VersionRow({ entry, version, title, t, language, onInstall }: { +/** One row of the version list: its state tags, and Update for a newer Git version of a Git copy or Install while nothing is installed. */ +function VersionRow({ entry, version, title, t, language, onInstall, onUpdate }: { readonly entry: MarketListingEntry readonly version: MarketListingVersion readonly title: string readonly t: Translate readonly language: string readonly onInstall: (version: MarketListingVersion) => void + readonly onUpdate: (version: MarketListingVersion) => void }): ReactNode { const current = entry.installation.installedVersion === version.version - // DSH ships a provided bundle; the market cannot replace it. - const offered = !current && entry.installation.status !== 'provided' && installable(version.compatibility) const { compatibility } = version + const accepted = installable(compatibility) return (
  • {version.version} {version.version === entry.defaultVersion ? {t('versionDefault')} : null} - {version.version.split('+')[0]?.includes('-') === true ? {t('versionPrerelease')} : null} + {prerelease(version.version) ? {t('versionPrerelease')} : null} {current ? {t('versionInstalled')} : null} {compatibility.compatible ? null : {t(compatibility.exempted ? 'versionExempted' : 'versionIncompatible')}}
    @@ -174,17 +198,24 @@ function VersionRow({ entry, version, title, t, language, onInstall }: { : t('incompatible', { runtime: compatibility.runtimeVersion, range: Object.values(compatibility.requirements).join(' ') })}
    - {!offered ? null : ( + {accepted && version.update ? ( + + ) : null} + {/* Installation never goes over an installed copy; only an update replaces it. */} + {accepted && entry.installation.status === 'not-installed' ? ( - )} + ) : null}
  • ) } -function EntryDetail({ entry, listing, t, face, hooks, language, onBack, onInstall, onOpenInstalled }: { +function EntryDetail({ entry, listing, t, face, hooks, language, onBack, onInstall, onUpdate, onOpenInstalled }: { readonly entry: MarketListingEntry readonly listing: MarketSearchResult readonly t: Translate @@ -193,9 +224,7 @@ function EntryDetail({ entry, listing, t, face, hooks, language, onBack, onInsta readonly hooks: Pick readonly language: string readonly onBack: () => void - readonly onInstall: (version: MarketListingVersion) => void - readonly onOpenInstalled: () => void -}): ReactNode { +} & EntryCallbacks): ReactNode { const title = entryTitle(entry, face) const version = presentedVersion(entry) const date = formatDate(version.publishTimestamp, language, 'long') @@ -219,7 +248,7 @@ function EntryDetail({ entry, listing, t, face, hooks, language, onBack, onInsta {title === entry.name ? null : {entry.name}} - + {entry.description === undefined ? null :

    {face.resolveText(entry.description)}

    } {compatibility.compatible ? null : ( @@ -241,7 +270,8 @@ function EntryDetail({ entry, listing, t, face, hooks, language, onBack, onInsta

    {t('versionsTitle')}

      {entry.versions.map(item => ( - + ))}
    @@ -394,7 +424,7 @@ function unfiltered(query: DiscoverQuery): boolean { return query.text.trim() === '' && query.category === null && query.labels.length === 0 } -function Browser({ state, listing, t, face, hooks, language, onInstall, openPackage }: { +function Browser({ state, listing, t, face, hooks, language, onInstall, onUpdate, openPackage }: { readonly state: DiscoverState readonly listing: ListingState readonly t: Translate @@ -402,6 +432,7 @@ function Browser({ state, listing, t, face, hooks, language, onInstall, openPack readonly hooks: Pick readonly language: string readonly onInstall: (choice: InstallChoice) => void + readonly onUpdate: (choice: InstallChoice) => void readonly openPackage: (name: string) => void }): ReactNode { const { query, value } = listing @@ -423,7 +454,9 @@ function Browser({ state, listing, t, face, hooks, language, onInstall, openPack if (openEntry !== undefined && value !== null) { return ( { setOpened(null) }} - onInstall={(version) => { onInstall({ entry: openEntry, version }) }} onOpenInstalled={() => { openPackage(openEntry.name) }} /> + onInstall={(version) => { onInstall({ entry: openEntry, version }) }} + onUpdate={(version) => { onUpdate({ entry: openEntry, version }) }} + onOpenInstalled={() => { openPackage(openEntry.name) }} /> ) } const selected = state.indexes.find(index => index.url === listing.url) ?? { url: listing.url } @@ -542,7 +575,7 @@ function Browser({ state, listing, t, face, hooks, language, onInstall, openPack {value.entries.map(entry => ( { setOpened(entry) }} onInstall={(version) => { onInstall({ entry, version }) }} - onOpenInstalled={() => { openPackage(entry.name) }} /> + onUpdate={(version) => { onUpdate({ entry, version }) }} onOpenInstalled={() => { openPackage(entry.name) }} /> ))} {value.nextOffset === null @@ -560,7 +593,7 @@ function Browser({ state, listing, t, face, hooks, language, onInstall, openPack /** Render the Discover tab of the Plugins page. */ export function DiscoverSection(props: DiscoverSectionProps): ReactNode { - const { t, install, openPackage } = props + const { t, install, update, openPackage } = props const face: DiscoverActions = props const state = props.useDiscover(snapshot => snapshot) const language = props.useLocale(snapshot => snapshot.active) @@ -591,7 +624,10 @@ export function DiscoverSection(props: DiscoverSectionProps): ReactNode { body = ( { setConfirming(choice) }} openPackage={openPackage} /> + onInstall={(choice) => { setConfirming(choice) }} + /* The Plugins page confirms an update in its own dialog, which names both versions. */ + onUpdate={({ entry, version }) => { update({ packageName: entry.name, version: version.version, spec: version.install.spec }) }} + openPackage={openPackage} /> ) } return ( diff --git a/packages/client/ui-plugin-market/src/client/locales.ts b/packages/client/ui-plugin-market/src/client/locales.ts index 52753bd63a..3bcadd89cc 100644 --- a/packages/client/ui-plugin-market/src/client/locales.ts +++ b/packages/client/ui-plugin-market/src/client/locales.ts @@ -50,6 +50,8 @@ export const zh = { install: '安装', installTitle: '安装 {name}', installVersionTitle: '安装 {name} {version}', + update: '更新', + updateTitle: '将 {name} 更新到 {version}', openInstalled: '在已安装中查看 {name}', statusInstalled: '已安装', statusUpdate: '有新版本', @@ -144,6 +146,8 @@ export const en = { install: 'Install', installTitle: 'Install {name}', installVersionTitle: 'Install {name} {version}', + update: 'Update', + updateTitle: 'Update {name} to {version}', openInstalled: 'View {name} in Installed', statusInstalled: 'Installed', statusUpdate: 'Update available', @@ -235,6 +239,8 @@ export const ja = { 'install': 'インストール', 'installTitle': '{name}をインストール', 'installVersionTitle': '{name} {version}をインストール', + 'update': 'アップデート', + 'updateTitle': '{name}を{version}へアップデート', 'openInstalled': 'インストール済みで{name}を表示', 'statusInstalled': 'インストール済み', 'statusUpdate': 'アップデートあり', @@ -326,6 +332,8 @@ export const es = { install: 'Instalar', installTitle: 'Instalar {name}', installVersionTitle: 'Instalar {name} {version}', + update: 'Actualizar', + updateTitle: 'Actualizar {name} a {version}', openInstalled: 'Ver {name} en Instalado', statusInstalled: 'Instalado', statusUpdate: 'Actualización disponible', @@ -417,6 +425,8 @@ export const fr = { install: 'Installer', installTitle: 'Installer {name}', installVersionTitle: 'Installer {name} {version}', + update: 'Mise à jour', + updateTitle: 'Mettre à jour {name} vers {version}', openInstalled: 'Afficher {name} dans Installé', statusInstalled: 'Installé', statusUpdate: 'Mise à jour disponible', @@ -508,6 +518,8 @@ export const de = { install: 'Installieren', installTitle: 'Installieren Sie {name}', installVersionTitle: 'Installieren Sie {name} {version}', + update: 'Aktualisieren', + updateTitle: '{name} auf {version} aktualisieren', openInstalled: '{name} unter „Installiert“ anzeigen', statusInstalled: 'Installiert', statusUpdate: 'Update verfügbar', @@ -599,6 +611,8 @@ export const ru = { install: 'Установить', installTitle: 'Установить {name}', installVersionTitle: 'Установить {name} {version}', + update: 'Обновить', + updateTitle: 'Обновить {name} до {version}', openInstalled: 'Посмотреть {name} в списке «Установлено»', statusInstalled: 'Установлено', statusUpdate: 'Доступно обновление', @@ -690,6 +704,8 @@ export const pt = { install: 'Instalar', installTitle: 'Instalar {name}', installVersionTitle: 'Instalar {name} {version}', + update: 'Atualizar', + updateTitle: 'Atualizar {name} para {version}', openInstalled: 'Ver {name} em Instalado', statusInstalled: 'Instalado', statusUpdate: 'Atualização disponível', diff --git a/packages/client/ui-plugin-market/tests/discover.client.spec.tsx b/packages/client/ui-plugin-market/tests/discover.client.spec.tsx index 9597605788..d2b58fd9b8 100644 --- a/packages/client/ui-plugin-market/tests/discover.client.spec.tsx +++ b/packages/client/ui-plugin-market/tests/discover.client.spec.tsx @@ -24,7 +24,7 @@ const COMMIT = '0123456789abcdef0123456789abcdef01234567' function release(name: string, version: string, fields: Partial = {}): MarketListingVersion { return { version, publishTimestamp: 1790476800, source: { type: 'npm' }, engines: {}, - install: { spec: `${name}@${version}` }, compatibility: { compatible: true }, ...fields, + install: { spec: `${name}@${version}` }, compatibility: { compatible: true }, update: false, ...fields, } } @@ -89,9 +89,10 @@ function renderTab(state: Partial = {}, dictionary: Record undefined), } const install = vi.fn() + const update = vi.fn() const openPackage = vi.fn() const props = { - t: translator(dictionary), install, openPackage, ...actions, + t: translator(dictionary), install, update, openPackage, ...actions, resolveText: (text: string | Record) => typeof text === 'string' ? text : text[language] ?? text.en, useDiscover: bindSnapshotSelector(store), useLocale: bindSnapshotSelector(locale), @@ -103,7 +104,7 @@ function renderTab(state: Partial = {}, dictionary: Record = {}): void => { set({ listing: { ...store.getSnapshot().listing as ListingState, query, value: { ...LISTING, ...value }, answered: query } }) } - return { actions, install, openPackage, set, answer } + return { actions, install, update, openPackage, set, answer } } const names = (): string[] => Array.from(document.querySelectorAll('[data-market-entry]')).map(node => node.getAttribute('data-market-entry') ?? '') @@ -322,7 +323,7 @@ describe('DiscoverSection', () => { expect(detail.getByRole('link', { name: 'https://git.example/o/c' }).closest('dd')?.querySelector('code')).toBeNull() }) - it('lists every version on the entry page and installs the one chosen', () => { + it('lists every version on the entry page and installs the one chosen while none is installed', () => { const name = 'dsh-v' const versions = [ release(name, '3.0.0-rc.1'), @@ -334,7 +335,8 @@ describe('DiscoverSection', () => { name, versions, defaultVersion: '1.1.0', installation: { status: 'different-version', installedVersion: '1.0.0+build.1' }, metadata: { keywords: [], labels: [], sortKeys: {} }, } - const { install } = renderTab({ listing: { url: A, query: INITIAL_QUERY, status: 'ready', value: { ...LISTING, entries: [multi] }, answered: INITIAL_QUERY, failure: null } }) + const listing = (entry: MarketListingEntry): ListingState => ({ url: A, query: INITIAL_QUERY, status: 'ready', value: { ...LISTING, entries: [entry] }, answered: INITIAL_QUERY, failure: null }) + const { install, set } = renderTab({ listing: listing(multi) }) expect(card(name).getByText(/^1\.1\.0 · /)).toBeTruthy() fireEvent.click(card(name).getByRole('button', { name: `View ${name}` })) const list = within(screen.getByRole('list', { name: `Versions of ${name}` })) @@ -343,11 +345,13 @@ describe('DiscoverSection', () => { .toEqual(['3.0.0-rc.1', '2.0.0', '1.1.0', '1.0.0+build.1']) expect(row('3.0.0-rc.1').getByText(en.versionPrerelease)).toBeTruthy() expect(row('2.0.0').getByText(en.versionIncompatible)).toBeTruthy() - expect(row('2.0.0').queryByRole('button')).toBeNull() expect(row('1.1.0').getByText(en.versionDefault)).toBeTruthy() expect(row('1.0.0+build.1').getByText(en.versionInstalled)).toBeTruthy() expect(row('1.0.0+build.1').queryByText(en.versionPrerelease)).toBeNull() - expect(row('1.0.0+build.1').queryByRole('button')).toBeNull() + // Installation never goes over an installed copy. + expect(list.queryByRole('button')).toBeNull() + set({ listing: listing({ ...multi, installation: { status: 'not-installed' } }) }) + expect(row('2.0.0').queryByRole('button')).toBeNull() fireEvent.click(list.getByRole('button', { name: `Install ${name} 3.0.0-rc.1` })) const dialog = within(screen.getByRole('dialog')) expect(dialog.getByText('dsh-v@3.0.0-rc.1')).toBeTruthy() @@ -355,6 +359,44 @@ describe('DiscoverSection', () => { expect(install).toHaveBeenCalledWith({ spec: 'dsh-v@3.0.0-rc.1', title: 'dsh-v', version: '3.0.0-rc.1' }) }) + it('updates a Git copy to a newer Git version from its card and its version list through the Plugins page', () => { + const name = 'dsh-u' + const git = (version: string, update: boolean): MarketListingVersion => release(name, version, { + source: { type: 'git', url: 'https://git.example/o/u', commit: COMMIT }, install: { spec: `git+https://git.example/o/u.git#${version}` }, update, + }) + const upgradable: MarketListingEntry = { + name, title: 'Upgrade', versions: [git('1.2.0', true), git('1.1.0', true), git('1.0.0', false), git('0.9.0', false)], defaultVersion: '1.2.0', + installation: { status: 'update-available', installedVersion: '1.0.0' }, metadata: { keywords: [], labels: [], sortKeys: {} }, + } + const { install, update } = renderTab({ listing: { url: A, query: INITIAL_QUERY, status: 'ready', value: { ...LISTING, entries: [upgradable] }, answered: INITIAL_QUERY, failure: null } }) + fireEvent.click(card(name).getByRole('button', { name: 'Update Upgrade to 1.2.0' })) + expect(update).toHaveBeenLastCalledWith({ packageName: name, version: '1.2.0', spec: 'git+https://git.example/o/u.git#1.2.0' }) + // The Plugins page confirms the update, so Discover opens no dialog of its own. + expect(screen.queryByRole('dialog')).toBeNull() + fireEvent.click(card(name).getByRole('button', { name: 'View Upgrade' })) + const row = (version: string) => within(document.querySelector(`[data-market-version="${version}"]`) as HTMLElement) + fireEvent.click(row('1.1.0').getByRole('button', { name: 'Update Upgrade to 1.1.0' })) + expect(update).toHaveBeenLastCalledWith({ packageName: name, version: '1.1.0', spec: 'git+https://git.example/o/u.git#1.1.0' }) + expect(row('1.0.0').queryByRole('button')).toBeNull() + expect(row('0.9.0').queryByRole('button')).toBeNull() + expect(within(document.querySelector('[data-market-detail="dsh-u"]') as HTMLElement).getAllByRole('button', { name: /^Update Upgrade to 1\.2\.0$/ })).toHaveLength(2) + expect(install).not.toHaveBeenCalled() + }) + + it('offers the newest stable update on the card when the default version does not update the Git copy', () => { + const name = 'dsh-w' + const git = (version: string, update: boolean): MarketListingVersion => release(name, version, { + source: { type: 'git', url: 'https://git.example/o/w', commit: COMMIT }, install: { spec: `git+https://git.example/o/w.git#${version}` }, update, + }) + const moved: MarketListingEntry = { + name, versions: [git('2.0.0-rc.1', true), release(name, '1.3.0'), git('1.2.0', true), git('1.0.0', false)], defaultVersion: '1.3.0', + installation: { status: 'update-available', installedVersion: '1.0.0' }, metadata: { keywords: [], labels: [], sortKeys: {} }, + } + const { update } = renderTab({ listing: { url: A, query: INITIAL_QUERY, status: 'ready', value: { ...LISTING, entries: [moved] }, answered: INITIAL_QUERY, failure: null } }) + fireEvent.click(card(name).getByRole('button', { name: `Update ${name} to 1.2.0` })) + expect(update).toHaveBeenLastCalledWith({ packageName: name, version: '1.2.0', spec: 'git+https://git.example/o/w.git#1.2.0' }) + }) + it('marks exempted versions and offers no version of a bundle DSH provides', () => { const name = 'dsh-x' const exempted = release(name, '2.0.0', { compatibility: { ...INCOMPATIBLE, exempted: true } }) diff --git a/packages/client/ui-plugin-market/tests/presentation.client.spec.ts b/packages/client/ui-plugin-market/tests/presentation.client.spec.ts index c00c66e9a4..94ad6c7c84 100644 --- a/packages/client/ui-plugin-market/tests/presentation.client.spec.ts +++ b/packages/client/ui-plugin-market/tests/presentation.client.spec.ts @@ -6,7 +6,7 @@ import { declaredQuery, displayDeclarations, indexLabel, INITIAL_QUERY, presente const resolveText = (text: LocalizedText): string => typeof text === 'string' ? text : text.zh ?? text.en function release(version: string, fields: Partial = {}): MarketListingVersion { - return { version, publishTimestamp: 0, source: { type: 'npm' }, engines: {}, install: { spec: `x@${version}` }, compatibility: { compatible: true }, ...fields } + return { version, publishTimestamp: 0, source: { type: 'npm' }, engines: {}, install: { spec: `x@${version}` }, compatibility: { compatible: true }, update: false, ...fields } } function entry(name: string, fields: Partial = {}, metadata: Partial = {}): MarketListingEntry { diff --git a/packages/experimental/creation-recommendations/tests/fixture.ts b/packages/experimental/creation-recommendations/tests/fixture.ts index 27f0457c26..f602c27809 100644 --- a/packages/experimental/creation-recommendations/tests/fixture.ts +++ b/packages/experimental/creation-recommendations/tests/fixture.ts @@ -36,7 +36,7 @@ export function recordCard(session: Agent['session'], { callId: id, phase, ...da export function installedBundle(name: string): BundleInfo { return { name, official: false, optional: false, availability: 'profile', installed: true, - enabled: true, removable: true, updatable: true, rows: [], overrides: [] } + enabled: true, removable: true, update: 'registry', rows: [], overrides: [] } } export async function writeBundle(directory: string, name: string): Promise { diff --git a/packages/experimental/plugin-recommendations/tests/fixture.ts b/packages/experimental/plugin-recommendations/tests/fixture.ts index bebea06cab..ec338f8570 100644 --- a/packages/experimental/plugin-recommendations/tests/fixture.ts +++ b/packages/experimental/plugin-recommendations/tests/fixture.ts @@ -16,7 +16,7 @@ export const ACTION = 'enable_official_plugin' export const bundle: BundleInfo = { name: 'official-search', official: true, optional: true, availability: 'installation', - enabled: false, installed: false, removable: false, updatable: false, rows: [], overrides: [], + enabled: false, installed: false, removable: false, rows: [], overrides: [], meta: { title: { en: 'Search', zh: '搜索' }, description: 'Search past conversations.' }, } /** Text of an accepted `enable_official_plugin` call's result. */ diff --git a/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts b/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts index 0dd57706a4..d3c3aefc0f 100644 --- a/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts +++ b/packages/extensions/cordis-client-runner/src/client/slot-catalog.ts @@ -2719,7 +2719,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ ], replaceRisk: 'none', example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'plugins.add.actions\', () => ctx.slots.register(\n { name: \'plugins.add.actions\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', - source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:118', + source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:135', }, { key: 'plugins.bundle.activation', @@ -2761,7 +2761,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ ], replaceRisk: 'shadows-shipped-ui', example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'plugins.bundle.activation\', () => ctx.slots.register(\n { name: \'plugins.bundle.activation\', key: \'\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', - source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:126', + source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:143', }, { key: 'plugins.bundle.config', @@ -2806,7 +2806,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ ], replaceRisk: 'shadows-shipped-ui', example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'plugins.bundle.config\', () => ctx.slots.register(\n { name: \'plugins.bundle.config\', key: \'\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', - source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:142', + source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:159', }, { key: 'plugins.detail.actions', @@ -2859,7 +2859,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ occupants: [], replaceRisk: 'none', example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'plugins.detail.actions\', () => ctx.slots.register(\n { name: \'plugins.detail.actions\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', - source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:156', + source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:173', }, { key: 'plugins.detail.badge', @@ -2912,7 +2912,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ occupants: [], replaceRisk: 'none', example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'plugins.detail.badge\', () => ctx.slots.register(\n { name: \'plugins.detail.badge\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', - source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:161', + source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:178', }, { key: 'plugins.detail.section', @@ -2965,7 +2965,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ occupants: [], replaceRisk: 'none', example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'plugins.detail.section\', () => ctx.slots.register(\n { name: \'plugins.detail.section\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', - source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:168', + source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:185', }, { key: 'plugins.item', @@ -3023,7 +3023,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ ], replaceRisk: 'none', example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'plugins.item\', () => ctx.slots.register(\n { name: \'plugins.item\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', - source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:136', + source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:153', }, { key: 'plugins.row.config', @@ -3064,14 +3064,14 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ occupants: [], replaceRisk: 'none', example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'plugins.row.config\', () => ctx.slots.register(\n { name: \'plugins.row.config\', key: \'\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', - source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:150', + source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:167', }, { key: 'plugins.tab', kind: 'list', scope: 'root', summary: 'Tabs after the page\'s own Installed tab, such as Discover: `label` names the tab and `order` places it.', - doc: 'Tabs after the page\'s own Installed tab, such as Discover: `label` names the tab and `order` places it. The\npage renders the selected entry under its header and tabs; the entry draws its own content and installs\nthrough the owner\'s `install`.', + doc: 'Tabs after the page\'s own Installed tab, such as Discover: `label` names the tab and `order` places it. The\npage renders the selected entry under its header and tabs; the entry draws its own content and installs and\nupdates through the owner\'s `install` and `update`.', registerOptions: [ { name: 'id', @@ -3093,10 +3093,12 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ }, ], ownerProps: [ - '/** Owner props of a page tab another plugin contributes, such as Discover. */\nexport interface PluginTabProps {\n /**\n * Install an exact version the person already confirmed: the page\'s install dialog opens at its check and progress\n * screens, without the spec form, and names the plugin and version above the spec.\n * @param target - the spec, optional registry, and display text.\n */\n readonly install: (target: PluginTabInstall) => void\n /**\n * Switch to the Installed tab and open a bundle\'s page, whose back button returns to the calling tab.\n * @param packageName - npm package name of the bundle.\n */\n readonly openPackage: (packageName: string) => void\n}', + '/** Owner props of a page tab another plugin contributes, such as Discover. */\nexport interface PluginTabProps {\n /**\n * Install an exact version the person already confirmed: the page\'s install dialog opens at its check and progress\n * screens, without the spec form, and names the plugin and version above the spec.\n * @param target - the spec, optional registry, and display text.\n */\n readonly install: (target: PluginTabInstall) => void\n /**\n * Open the page\'s update dialog at its confirmation for a bundle the Host lists with `update: \'git\'`, from the\n * version the page lists; nothing opens before the page has listed the bundle or while another update runs. The\n * update removes the installed version, installs the target, and installs the removed version again when that fails.\n * @param target - the package, the target version, and its Git spec.\n */\n readonly update: (target: PluginTabUpdate) => void\n /**\n * Switch to the Installed tab and open a bundle\'s page, whose back button returns to the calling tab.\n * @param packageName - npm package name of the bundle.\n */\n readonly openPackage: (packageName: string) => void\n}', ], ownerPropsReferences: [ + 'Git', 'PluginTabInstall', + 'PluginTabUpdate', ], standardProps: [ 'useRemoteAccess: (action: RemoteEndpoint) => boolean', @@ -3119,7 +3121,7 @@ export const CLIENT_SLOT_API: readonly ClientSlotEntry[] = [ ], replaceRisk: 'none', example: 'return {\n inject: [\'slots\'],\n apply(ctx) {\n ctx.slots.inject(\'plugins.tab\', () => ctx.slots.register(\n { name: \'plugins.tab\', id: \'my-entry\', order: 100, label: \'My entry\' },\n () => React.createElement(\'div\', null, \'hello\'),\n ))\n },\n}', - source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:124', + source: 'packages/client/ui-plugin-manager/src/client/slot-contract.ts:141', }, { key: 'rightbar', diff --git a/packages/extensions/tool-cordis/src/api-catalog.ts b/packages/extensions/tool-cordis/src/api-catalog.ts index 54067ccec8..2c90374bec 100644 --- a/packages/extensions/tool-cordis/src/api-catalog.ts +++ b/packages/extensions/tool-cordis/src/api-catalog.ts @@ -2317,8 +2317,8 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ }, { signature: '@Remote async updateBundle(name: string, version: string, options?: UpdateBundleOptions): Promise', - description: 'Replace an installed registry bundle with a newer published version. The target is checked on the registry first: it must exist, declare a bundle, and accept the running DSH version, or nothing changes. With HMR the installed version is removed, so its plugins stop, and the target is installed with the same selection; when that installation fails or is cancelled, the removed version is installed again from the registry it came from. Without HMR the target is installed over the running version and the result reports the restart.', - parameters: [{ name: 'name', description: 'Installed profile dependency name.' }, { name: 'version', description: 'Exact published version newer than the installed one.' }, { name: 'options', description: 'The registry asked first, as for `listVersions`, and the request id naming the progress and the target installation\'s logs and cancellation.' }], + description: 'Replace an installed bundle with a newer version. A registry copy updates to `name@version`, checked on the registry first: it must exist, declare a bundle, and accept the running DSH version, or nothing changes. A Git copy updates to the Git spec `options.spec` names, which must pin a full commit of the repository directory the copy came from; with HMR, a network failure or timeout reaching its repository stops the update before anything changes. pnpm reads a Git package only while installing it, so its bundle and compatibility are checked then. Either target installation is undone, profile files and modules included, before its bundle loads unless it installs `name` at `version`. With HMR the installed version is removed, so its plugins stop, and the target is installed with the same selection; when that installation fails or is cancelled, the removed version is installed again: a registry copy from the registry it came from, a Git copy from its saved spec. Without HMR the target is installed over the running version and the result reports the restart.', + parameters: [{ name: 'name', description: 'Installed profile dependency name.' }, { name: 'version', description: 'Exact version newer than the installed one.' }, { name: 'options', description: 'The registry asked first, as for `listVersions`; the Git spec of `version`, required for and accepted only by a bundle `listBundles` lists with `update: \'git\'`; and the request id naming the progress and the target installation\'s logs and cancellation.' }], returns: 'The versions, the last step that ran with its result, and the reinstallation when one ran.', }, { @@ -5827,7 +5827,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'BundleInfo', - declaration: 'export interface BundleInfo {\n name: string;\n official: boolean;\n availability: \'installation\' | \'profile\' | \'missing\';\n installTarget?: {\n spec: string;\n version: string;\n };\n version?: string;\n meta?: PluginLocalizedMeta;\n description?: string;\n listedByDefault?: boolean;\n enabled: boolean;\n installed: boolean;\n source?: string;\n optional: boolean;\n removable: boolean;\n updatable: boolean;\n readOnlyReason?: ReadOnlyReason;\n error?: ManagementError;\n rows: BundleRowInfo[];\n overrides: string[];\n}', + declaration: 'export interface BundleInfo {\n name: string;\n official: boolean;\n availability: \'installation\' | \'profile\' | \'missing\';\n installTarget?: {\n spec: string;\n version: string;\n };\n version?: string;\n meta?: PluginLocalizedMeta;\n description?: string;\n listedByDefault?: boolean;\n enabled: boolean;\n installed: boolean;\n source?: string;\n optional: boolean;\n removable: boolean;\n update?: \'registry\' | \'git\';\n readOnlyReason?: ReadOnlyReason;\n error?: ManagementError;\n rows: BundleRowInfo[];\n overrides: string[];\n}', }, { name: 'BundleRowInfo', @@ -6795,7 +6795,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'InstallBundleOptions', - declaration: 'export interface InstallBundleOptions {\n enabled?: boolean;\n saveExact?: boolean;\n requestId?: PluginInstallRequestId;\n approvedBuilds?: string[];\n registry?: Registry;\n}', + declaration: 'export interface InstallBundleOptions {\n enabled?: boolean;\n saveExact?: boolean;\n requestId?: PluginInstallRequestId;\n approvedBuilds?: string[];\n registry?: Registry;\n expected?: {\n readonly name: string;\n readonly version: string;\n };\n}', }, { name: 'InstallSpecKind', @@ -7115,7 +7115,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'ManagementError', - declaration: 'export interface ManagementError {\n code: ReadOnlyReason | \'unknown-plugin\' | \'invalid-spec\' | \'already-installed\' | \'ambiguous-install\' | \'not-bundle\' | \'not-removable\' | \'stop-profile\' | \'bundle-in-use\' | \'stale-approval\' | \'incompatible-version\' | \'unsupported-platform\' | \'operation-error\';\n diagnostic?: string;\n incompatible?: IncompatiblePlugin[];\n}', + declaration: 'export interface ManagementError {\n code: ReadOnlyReason | \'unknown-plugin\' | \'invalid-spec\' | \'already-installed\' | \'ambiguous-install\' | \'not-bundle\' | \'not-removable\' | \'stop-profile\' | \'bundle-in-use\' | \'stale-approval\' | \'incompatible-version\' | \'unsupported-platform\' | \'unexpected-package\' | \'operation-error\';\n diagnostic?: string;\n incompatible?: IncompatiblePlugin[];\n}', }, { name: 'ManualCompactAgentContext', @@ -7159,7 +7159,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'MarketListingVersion', - declaration: 'export interface MarketListingVersion extends MarketVersion {\n readonly install: MarketInstallTarget;\n readonly compatibility: MarketCompatibility;\n}', + declaration: 'export interface MarketListingVersion extends MarketVersion {\n readonly install: MarketInstallTarget;\n readonly compatibility: MarketCompatibility;\n readonly update: boolean;\n}', }, { name: 'MarketPlugin', @@ -7555,7 +7555,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'PackageResult', - declaration: 'export interface PackageResult {\n exitCode: number;\n output: string;\n truncated: boolean;\n logPath: string;\n kind?: PluginInstallFailureKind;\n timedOut?: boolean;\n incompatible?: IncompatiblePlugin[];\n}', + declaration: 'export interface PackageResult {\n exitCode: number;\n output: string;\n truncated: boolean;\n logPath: string;\n kind?: PluginInstallFailureKind;\n timedOut?: boolean;\n incompatible?: IncompatiblePlugin[];\n unmet?: \'unexpected-package\' | \'not-bundle\';\n}', }, { name: 'PaidTranslationProvider', @@ -10023,7 +10023,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'UpdateBundleOptions', - declaration: 'export interface UpdateBundleOptions {\n readonly registry?: Registry;\n readonly requestId?: PluginInstallRequestId;\n}', + declaration: 'export interface UpdateBundleOptions {\n readonly registry?: Registry;\n readonly spec?: string;\n readonly requestId?: PluginInstallRequestId;\n}', }, { name: 'UpdateSharePermissionsRequest', From 6b8d86800848fe72f2e59a074fa17204368e8949 Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Thu, 5 Nov 2026 02:18:05 +0800 Subject: [PATCH 2/4] chore(deps): unify zod to 4.6.6 Raise all 88 workspace zod declarations to exact 4.6.6 and move @browserbasehq/stagehand to 4.4.0, which pins the same version, so the lock keeps a single zod copy. Test expectations and the Typert consumer's node types follow the new declarations; pnpm-workspace.yaml records the release-age exemption for stagehand@4.4.0. --- apps/desktop/package.json | 2 +- benchmarks/package.json | 2 +- package.json | 2 +- packages/api/account-controller/package.json | 2 +- packages/api/gateway/package.json | 2 +- packages/api/job-controller/package.json | 2 +- packages/api/remote-access/package.json | 2 +- packages/api/remotes/package.json | 2 +- packages/api/session-controller/package.json | 2 +- packages/api/settings-controller/package.json | 2 +- packages/api/terminal-controller/package.json | 2 +- .../api/workspace-controller/package.json | 2 +- packages/api/workspace-files/package.json | 2 +- packages/boot/plugin-manager/package.json | 2 +- packages/boot/plugin-market/package.json | 2 +- packages/client/connection/package.json | 2 +- packages/client/file-upload/package.json | 2 +- .../client/product-analytics/package.json | 2 +- packages/client/ui-conversation/package.json | 2 +- .../client/ui-plugin-manager/package.json | 2 +- packages/client/ui-sidebar-right/package.json | 2 +- packages/client/ui-trajectory/package.json | 2 +- packages/client/web/package.json | 2 +- .../context/harness-dev-context/package.json | 2 +- .../context/session-reference/package.json | 2 +- packages/context/time-context/package.json | 2 +- packages/context/tmux-context/package.json | 2 +- packages/core/agent-loop/package.json | 2 +- packages/core/agent/package.json | 2 +- .../deepseek-account-platform/package.json | 2 +- packages/document/office-kit/package.json | 2 +- .../experimental/action-cards/package.json | 2 +- .../api-speech-to-text/package.json | 2 +- .../browser-use-stagehand-native/package.json | 4 +- .../claude-code-mods/package.json | 2 +- .../client-ui-cot-translation/package.json | 2 +- .../package.json | 2 +- .../experimental/client-ui-forge/package.json | 2 +- .../package.json | 2 +- .../package.json | 2 +- .../creation-recommendations/package.json | 2 +- packages/experimental/cua-macos/package.json | 2 +- packages/experimental/cua/package.json | 2 +- .../experimental/forge-github/package.json | 2 +- .../experimental/forge-session/package.json | 2 +- packages/experimental/liveshare/package.json | 2 +- .../plugin-recommendations/package.json | 2 +- .../speech-to-text-sensevoice/package.json | 2 +- .../cordis-host-runner/package.json | 2 +- .../feedback/command-feedback/package.json | 2 +- .../feedback/message-feedback/package.json | 2 +- packages/goal/goal/package.json | 2 +- packages/host/plugin-inventory/package.json | 2 +- packages/interaction/commands/package.json | 2 +- .../permission-presets/package.json | 2 +- .../interaction/user-approval/package.json | 2 +- .../interaction/user-questions/package.json | 2 +- packages/llm/llm-retry/package.json | 2 +- packages/llm/llm/package.json | 2 +- packages/llm/token-meter/package.json | 2 +- packages/mcp/mcp-client/package.json | 2 +- packages/mcp/mcp-manager/package.json | 2 +- packages/plan/plan-mode/package.json | 2 +- .../preset/agent-preset-registry/package.json | 2 +- packages/sandbox/sandbox-policy/package.json | 2 +- packages/schedule/schedule/package.json | 2 +- .../session-projection-cache/package.json | 2 +- .../session/session-projection/package.json | 2 +- packages/session/session-stats/package.json | 2 +- packages/session/session-title/package.json | 2 +- .../session/session-turn-outline/package.json | 2 +- .../session/working-directory/package.json | 2 +- packages/ssh/fs-ssh/package.json | 2 +- packages/ssh/sandbox-ssh/package.json | 2 +- packages/ssh/ssh/package.json | 2 +- packages/ssh/subprocess-ssh/package.json | 2 +- packages/storage/storage-domain/package.json | 2 +- .../subagent-claude-code/package.json | 2 +- .../tests/subagent-claude-code.spec.ts | 2 +- packages/subagent/subagent/package.json | 2 +- packages/subagent/tool-subagent/package.json | 2 +- packages/team/agent-team/package.json | 2 +- packages/todo/tool-todo/package.json | 2 +- packages/typert/generator/package.json | 2 +- .../typert/generator/tests/type-model.spec.ts | 3 +- packages/typert/loader/package.json | 2 +- packages/typert/protocol/package.json | 2 +- packages/typert/registry/package.json | 2 +- packages/util/chunked-list/package.json | 2 +- packages/workspace/workspace/package.json | 2 +- pnpm-lock.yaml | 486 +++++++++--------- pnpm-workspace.yaml | 2 + scripts/client-bundle-purity.spec.ts | 2 +- 93 files changed, 338 insertions(+), 335 deletions(-) diff --git a/apps/desktop/package.json b/apps/desktop/package.json index cd44225184..c3cf87bfa4 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -87,7 +87,7 @@ "@deepseek-ai/dsh-atomic-write": "workspace:*", "@deepseek-ai/dsh-client-product-analytics": "workspace:*", "@deepseek-ai/dsh-plugin-manager": "workspace:*", - "zod": "4.4.3", + "zod": "4.6.6", "@deepseek-ai/dsh-util-file-drag": "workspace:*", "@deepseek-ai/dsh-client-ui-file-actions": "workspace:*", "tencentcloud-sdk-nodejs-teo": "4.1.330" diff --git a/benchmarks/package.json b/benchmarks/package.json index 4601a6e972..1978b41bd1 100644 --- a/benchmarks/package.json +++ b/benchmarks/package.json @@ -47,7 +47,7 @@ "@deepseek-ai/dsh-token-meter": "workspace:*", "@deepseek-ai/dsh-typert-protocol": "workspace:*", "@deepseek-ai/dsh-util-values": "workspace:*", - "zod": "^4.4.3" + "zod": "4.6.6" }, "peerDependencies": { "@deepseek-ai/cordis": "workspace:~" diff --git a/package.json b/package.json index c4f130943d..55ff3a62a4 100644 --- a/package.json +++ b/package.json @@ -263,7 +263,7 @@ "@yarnpkg/cli-dist": "4.18.0", "eslint-plugin-sonarjs": "^4.2.0", "execa": "^10.0.1", - "zod": "^4.4.3", + "zod": "4.6.6", "extract-zip": "^2.0.1", "fast-check": "^4.9.0", "fflate": "^0.8.2", diff --git a/packages/api/account-controller/package.json b/packages/api/account-controller/package.json index 92a16ff401..7731b7150d 100644 --- a/packages/api/account-controller/package.json +++ b/packages/api/account-controller/package.json @@ -50,7 +50,7 @@ ], "license": "MIT", "dependencies": { - "zod": "4.4.3" + "zod": "4.6.6" }, "peerDependencies": { "@deepseek-ai/cordis": "workspace:~", diff --git a/packages/api/gateway/package.json b/packages/api/gateway/package.json index 2cb64bbef0..99c310776c 100644 --- a/packages/api/gateway/package.json +++ b/packages/api/gateway/package.json @@ -83,6 +83,6 @@ "@deepseek-ai/dsh-util-crypto": "workspace:*", "@types/ws": "^8.18.1", "@deepseek-ai/cordis": "workspace:~", - "zod": "4.4.3" + "zod": "4.6.6" } } diff --git a/packages/api/job-controller/package.json b/packages/api/job-controller/package.json index e260a52a6c..cfd09bcfb5 100644 --- a/packages/api/job-controller/package.json +++ b/packages/api/job-controller/package.json @@ -73,7 +73,7 @@ "dependencies": { "@deepseek-ai/dsh-typert-protocol": "workspace:*", "@deepseek-ai/schemastery": "workspace:~", - "zod": "4.4.3" + "zod": "4.6.6" }, "peerDependencies": { "@deepseek-ai/cordis": "workspace:~" diff --git a/packages/api/remote-access/package.json b/packages/api/remote-access/package.json index bd5eeae185..5d9c311210 100644 --- a/packages/api/remote-access/package.json +++ b/packages/api/remote-access/package.json @@ -72,7 +72,7 @@ "license": "MIT", "dependencies": { "@deepseek-ai/dsh-typert-protocol": "workspace:*", - "zod": "4.4.3" + "zod": "4.6.6" }, "peerDependencies": { "@deepseek-ai/cordis": "workspace:~" diff --git a/packages/api/remotes/package.json b/packages/api/remotes/package.json index 565e54a21c..bf7dbbfa08 100644 --- a/packages/api/remotes/package.json +++ b/packages/api/remotes/package.json @@ -92,7 +92,7 @@ "@deepseek-ai/dsh-user-approval": "workspace:*", "@deepseek-ai/dsh-user-questions": "workspace:*", "@deepseek-ai/cordis": "workspace:~", - "zod": "4.4.3", + "zod": "4.6.6", "@deepseek-ai/dsh-command-feedback": "workspace:*", "@deepseek-ai/dsh-api-terminal-controller": "workspace:*", "@deepseek-ai/dsh-plugin-manager": "workspace:*", diff --git a/packages/api/session-controller/package.json b/packages/api/session-controller/package.json index de01132dfe..4f29e46eac 100644 --- a/packages/api/session-controller/package.json +++ b/packages/api/session-controller/package.json @@ -80,7 +80,7 @@ "@deepseek-ai/dsh-timeout": "workspace:*", "@deepseek-ai/schemastery": "workspace:~", "mime-types": "^3.0.2", - "zod": "4.4.3" + "zod": "4.6.6" }, "peerDependencies": { "@deepseek-ai/cordis": "workspace:~", diff --git a/packages/api/settings-controller/package.json b/packages/api/settings-controller/package.json index 58b97828be..7c16a75482 100644 --- a/packages/api/settings-controller/package.json +++ b/packages/api/settings-controller/package.json @@ -51,7 +51,7 @@ "license": "MIT", "dependencies": { "@deepseek-ai/schemastery": "workspace:~", - "zod": "4.4.3" + "zod": "4.6.6" }, "peerDependencies": { "@deepseek-ai/cordis": "workspace:~", diff --git a/packages/api/terminal-controller/package.json b/packages/api/terminal-controller/package.json index 10accccc71..873fb7dc94 100644 --- a/packages/api/terminal-controller/package.json +++ b/packages/api/terminal-controller/package.json @@ -62,7 +62,7 @@ "@deepseek-ai/schemastery": "workspace:~", "@xterm/addon-serialize": "^0.14.0", "@xterm/headless": "^6.0.0", - "zod": "4.4.3" + "zod": "4.6.6" }, "peerDependencies": { "@deepseek-ai/dsh-subprocess": "workspace:*", diff --git a/packages/api/workspace-controller/package.json b/packages/api/workspace-controller/package.json index c3044168fe..a534bfeb96 100644 --- a/packages/api/workspace-controller/package.json +++ b/packages/api/workspace-controller/package.json @@ -78,7 +78,7 @@ "@deepseek-ai/dsh-deque": "workspace:*", "@deepseek-ai/dsh-native-command": "workspace:*", "@deepseek-ai/schemastery": "workspace:~", - "zod": "4.4.3" + "zod": "4.6.6" }, "peerDependencies": { "@deepseek-ai/cordis": "workspace:~", diff --git a/packages/api/workspace-files/package.json b/packages/api/workspace-files/package.json index f24b37ef8c..5e39822c0a 100644 --- a/packages/api/workspace-files/package.json +++ b/packages/api/workspace-files/package.json @@ -56,7 +56,7 @@ "@deepseek-ai/dsh-deque": "workspace:*", "@deepseek-ai/dsh-typert-protocol": "workspace:*", "@deepseek-ai/schemastery": "workspace:~", - "zod": "4.4.3" + "zod": "4.6.6" }, "peerDependencies": { "@deepseek-ai/cordis": "workspace:~" diff --git a/packages/boot/plugin-manager/package.json b/packages/boot/plugin-manager/package.json index 9db014068e..3a2a651a30 100644 --- a/packages/boot/plugin-manager/package.json +++ b/packages/boot/plugin-manager/package.json @@ -75,7 +75,7 @@ "execa": "^10.0.1", "semver": "^7.8.5", "yaml": "^2.9.0", - "zod": "4.4.3", + "zod": "4.6.6", "@deepseek-ai/dsh-util-values": "workspace:*", "@deepseek-ai/dsh-plugin-detail": "workspace:*", "@deepseek-ai/dsh-timeout": "workspace:*", diff --git a/packages/boot/plugin-market/package.json b/packages/boot/plugin-market/package.json index 5d65a78b94..a2b1ea64d9 100644 --- a/packages/boot/plugin-market/package.json +++ b/packages/boot/plugin-market/package.json @@ -68,7 +68,7 @@ "hosted-git-info": "^9.0.3", "minisearch": "^7.2.0", "semver": "^7.8.5", - "zod": "^4.4.3" + "zod": "4.6.6" }, "peerDependencies": { "@deepseek-ai/cordis": "workspace:~", diff --git a/packages/client/connection/package.json b/packages/client/connection/package.json index 06f11a1548..d2dd72b601 100644 --- a/packages/client/connection/package.json +++ b/packages/client/connection/package.json @@ -39,7 +39,7 @@ "dependencies": { "@deepseek-ai/dsh-credentials": "workspace:*", "@deepseek-ai/schemastery": "workspace:~", - "zod": "4.4.3" + "zod": "4.6.6" }, "files": [ "locale/*.json", diff --git a/packages/client/file-upload/package.json b/packages/client/file-upload/package.json index 46eeee5abe..ef19d8c3c5 100644 --- a/packages/client/file-upload/package.json +++ b/packages/client/file-upload/package.json @@ -68,7 +68,7 @@ "src" ], "dependencies": { - "zod": "4.4.3", + "zod": "4.6.6", "@deepseek-ai/dsh-typert-protocol": "workspace:*", "@deepseek-ai/dsh-brand": "workspace:*" }, diff --git a/packages/client/product-analytics/package.json b/packages/client/product-analytics/package.json index 6f03c85430..7baf25596d 100644 --- a/packages/client/product-analytics/package.json +++ b/packages/client/product-analytics/package.json @@ -85,6 +85,6 @@ "dependencies": { "@deepseek-ai/dsh-typert-protocol": "workspace:*", "@deepseek-ai/schemastery": "workspace:~", - "zod": "4.4.3" + "zod": "4.6.6" } } diff --git a/packages/client/ui-conversation/package.json b/packages/client/ui-conversation/package.json index 3491b214e3..612f987620 100644 --- a/packages/client/ui-conversation/package.json +++ b/packages/client/ui-conversation/package.json @@ -99,7 +99,7 @@ "@deepseek-ai/dsh-message-quotes": "workspace:*", "@deepseek-ai/dsh-session-projection": "workspace:*", "@deepseek-ai/dsh-brand": "workspace:*", - "zod": "4.4.3", + "zod": "4.6.6", "@deepseek-ai/dsh-client-ui-context-menu": "workspace:*" }, "files": [ diff --git a/packages/client/ui-plugin-manager/package.json b/packages/client/ui-plugin-manager/package.json index 93b59863a4..633f76347e 100644 --- a/packages/client/ui-plugin-manager/package.json +++ b/packages/client/ui-plugin-manager/package.json @@ -93,7 +93,7 @@ "dependencies": { "@deepseek-ai/dsh-timeout": "workspace:*", "@deepseek-ai/schemastery": "workspace:~", - "zod": "4.4.3", + "zod": "4.6.6", "@deepseek-ai/dsh-typert-protocol": "workspace:*" } } diff --git a/packages/client/ui-sidebar-right/package.json b/packages/client/ui-sidebar-right/package.json index 23fb7b79fc..d01fdf16ff 100644 --- a/packages/client/ui-sidebar-right/package.json +++ b/packages/client/ui-sidebar-right/package.json @@ -70,7 +70,7 @@ "picomatch": "^4.0.7", "clsx": "^2.0.0", "@deepseek-ai/dsh-util-crypto": "workspace:*", - "zod": "4.4.3", + "zod": "4.6.6", "@deepseek-ai/dsh-client-shortcuts": "workspace:*", "@deepseek-ai/dsh-brand": "workspace:*", "@deepseek-ai/dsh-client-ui-context-menu": "workspace:*" diff --git a/packages/client/ui-trajectory/package.json b/packages/client/ui-trajectory/package.json index 30cc536266..efee2c8c57 100644 --- a/packages/client/ui-trajectory/package.json +++ b/packages/client/ui-trajectory/package.json @@ -99,7 +99,7 @@ ], "dependencies": { "@deepseek-ai/dsh-typert-protocol": "workspace:*", - "zod": "4.4.3", + "zod": "4.6.6", "@deepseek-ai/schemastery": "workspace:~" } } diff --git a/packages/client/web/package.json b/packages/client/web/package.json index 9680069d29..b342335bec 100644 --- a/packages/client/web/package.json +++ b/packages/client/web/package.json @@ -39,7 +39,7 @@ "react-dom": "^18.2.0", "typescript": "^6.0.3", "@deepseek-ai/dsh-host-webserver": "workspace:*", - "zod": "4.4.3", + "zod": "4.6.6", "@tanstack/react-virtual": "3.14.10" }, "peerDependencies": { diff --git a/packages/context/harness-dev-context/package.json b/packages/context/harness-dev-context/package.json index 5af889019d..d0faa968ab 100644 --- a/packages/context/harness-dev-context/package.json +++ b/packages/context/harness-dev-context/package.json @@ -32,7 +32,7 @@ ], "license": "MIT", "dependencies": { - "zod": "4.4.3" + "zod": "4.6.6" }, "peerDependencies": { "@deepseek-ai/cordis": "workspace:~", diff --git a/packages/context/session-reference/package.json b/packages/context/session-reference/package.json index 7ba212f147..b53b90d171 100644 --- a/packages/context/session-reference/package.json +++ b/packages/context/session-reference/package.json @@ -53,7 +53,7 @@ "@deepseek-ai/dsh-brand": "workspace:*", "@deepseek-ai/dsh-util-values": "workspace:*", "@deepseek-ai/schemastery": "workspace:~", - "zod": "4.4.3" + "zod": "4.6.6" }, "peerDependencies": { "@deepseek-ai/cordis": "workspace:~", diff --git a/packages/context/time-context/package.json b/packages/context/time-context/package.json index 2ace73c0b7..2c831d5c3f 100644 --- a/packages/context/time-context/package.json +++ b/packages/context/time-context/package.json @@ -34,7 +34,7 @@ "dependencies": { "@deepseek-ai/dsh-util-values": "workspace:*", "@deepseek-ai/schemastery": "workspace:~", - "zod": "4.4.3" + "zod": "4.6.6" }, "peerDependencies": { "@deepseek-ai/cordis": "workspace:~", diff --git a/packages/context/tmux-context/package.json b/packages/context/tmux-context/package.json index e37a47b08b..92bbaa76ae 100644 --- a/packages/context/tmux-context/package.json +++ b/packages/context/tmux-context/package.json @@ -33,7 +33,7 @@ "license": "MIT", "dependencies": { "@deepseek-ai/schemastery": "workspace:~", - "zod": "4.4.3" + "zod": "4.6.6" }, "peerDependencies": { "@deepseek-ai/dsh-agent": "workspace:*", diff --git a/packages/core/agent-loop/package.json b/packages/core/agent-loop/package.json index 984b24167a..f2555c5f81 100644 --- a/packages/core/agent-loop/package.json +++ b/packages/core/agent-loop/package.json @@ -45,7 +45,7 @@ "@deepseek-ai/dsh-brand": "workspace:*", "@deepseek-ai/dsh-util-values": "workspace:*", "@deepseek-ai/schemastery": "workspace:~", - "zod": "4.4.3" + "zod": "4.6.6" }, "devDependencies": { "@deepseek-ai/dsh-llm-deepseek-api-key": "workspace:*", diff --git a/packages/core/agent/package.json b/packages/core/agent/package.json index 1138bed9bd..445a7d0646 100644 --- a/packages/core/agent/package.json +++ b/packages/core/agent/package.json @@ -66,6 +66,6 @@ }, "dependencies": { "@deepseek-ai/dsh-brand": "workspace:*", - "zod": "4.4.3" + "zod": "4.6.6" } } diff --git a/packages/credentials/deepseek-account-platform/package.json b/packages/credentials/deepseek-account-platform/package.json index 69b5fb4228..9c4f09ff16 100644 --- a/packages/credentials/deepseek-account-platform/package.json +++ b/packages/credentials/deepseek-account-platform/package.json @@ -52,6 +52,6 @@ }, "dependencies": { "@deepseek-ai/schemastery": "workspace:~", - "zod": "4.4.3" + "zod": "4.6.6" } } diff --git a/packages/document/office-kit/package.json b/packages/document/office-kit/package.json index 98ca669f49..1349791924 100644 --- a/packages/document/office-kit/package.json +++ b/packages/document/office-kit/package.json @@ -65,7 +65,7 @@ "@deepseek-ai/schemastery": "workspace:~", "@deepseek-ai/dsh-brand": "workspace:*", "@deepseek-ai/dsh-typert-protocol": "workspace:*", - "zod": "4.4.3", + "zod": "4.6.6", "@deepseek-ai/dsh-home-paths": "workspace:*" } } diff --git a/packages/experimental/action-cards/package.json b/packages/experimental/action-cards/package.json index e54347d2d0..bc9f55715d 100644 --- a/packages/experimental/action-cards/package.json +++ b/packages/experimental/action-cards/package.json @@ -60,7 +60,7 @@ "@deepseek-ai/dsh-util-values": "workspace:*" }, "dependencies": { - "zod": "4.4.3" + "zod": "4.6.6" }, "files": [ "locale/*.json", diff --git a/packages/experimental/api-speech-to-text/package.json b/packages/experimental/api-speech-to-text/package.json index 5d80d968e6..dfe124dca3 100644 --- a/packages/experimental/api-speech-to-text/package.json +++ b/packages/experimental/api-speech-to-text/package.json @@ -51,7 +51,7 @@ "license": "MIT", "dependencies": { "@deepseek-ai/schemastery": "workspace:~", - "zod": "4.4.3" + "zod": "4.6.6" }, "peerDependencies": { "@deepseek-ai/cordis": "workspace:~", diff --git a/packages/experimental/browser-use-stagehand-native/package.json b/packages/experimental/browser-use-stagehand-native/package.json index 0e3588735e..6af18876c5 100644 --- a/packages/experimental/browser-use-stagehand-native/package.json +++ b/packages/experimental/browser-use-stagehand-native/package.json @@ -44,12 +44,12 @@ "@deepseek-ai/dsh-tools": "workspace:*" }, "dependencies": { - "@browserbasehq/stagehand": "4.1.0", + "@browserbasehq/stagehand": "4.4.0", "@deepseek-ai/dsh-experimental-browser-use-runtime": "workspace:*", "@deepseek-ai/dsh-mcp-client": "workspace:*", "@deepseek-ai/dsh-util-values": "workspace:*", "@deepseek-ai/schemastery": "workspace:~", - "zod": "4.4.3", + "zod": "4.6.6", "@deepseek-ai/dsh-subprocess": "workspace:*", "@puppeteer/browsers": "3.2.2" }, diff --git a/packages/experimental/claude-code-mods/package.json b/packages/experimental/claude-code-mods/package.json index 77b4571bfe..c2381389ae 100644 --- a/packages/experimental/claude-code-mods/package.json +++ b/packages/experimental/claude-code-mods/package.json @@ -58,7 +58,7 @@ "@deepseek-ai/dsh-util-crypto": "workspace:*", "@deepseek-ai/dsh-util-values": "workspace:*", "@deepseek-ai/schemastery": "workspace:~", - "zod": "^4.4.3" + "zod": "4.6.6" }, "peerDependencies": { "@deepseek-ai/cordis": "workspace:~", diff --git a/packages/experimental/client-ui-cot-translation/package.json b/packages/experimental/client-ui-cot-translation/package.json index e2de0ff4e6..9c435f70f6 100644 --- a/packages/experimental/client-ui-cot-translation/package.json +++ b/packages/experimental/client-ui-cot-translation/package.json @@ -60,7 +60,7 @@ }, "dependencies": { "@deepseek-ai/schemastery": "workspace:~", - "zod": "^4.4.3", + "zod": "4.6.6", "@deepseek-ai/dsh-typert-protocol": "workspace:*" }, "devDependencies": { diff --git a/packages/experimental/client-ui-creation-recommendations/package.json b/packages/experimental/client-ui-creation-recommendations/package.json index 31cfadc132..94a464d9f8 100644 --- a/packages/experimental/client-ui-creation-recommendations/package.json +++ b/packages/experimental/client-ui-creation-recommendations/package.json @@ -50,7 +50,7 @@ "@deepseek-ai/dsh-client-store": "workspace:*", "@deepseek-ai/dsh-client-ui-action-cards": "workspace:*", "@deepseek-ai/dsh-action-cards": "workspace:*", - "zod": "4.4.3", + "zod": "4.6.6", "@deepseek-ai/dsh-client-test-runtime": "workspace:*", "@deepseek-ai/dsh-llm": "workspace:*", "@deepseek-ai/dsh-session": "workspace:*" diff --git a/packages/experimental/client-ui-forge/package.json b/packages/experimental/client-ui-forge/package.json index 4133c97e52..76ad557823 100644 --- a/packages/experimental/client-ui-forge/package.json +++ b/packages/experimental/client-ui-forge/package.json @@ -67,7 +67,7 @@ "@deepseek-ai/schemastery": "workspace:~", "@deepseek-ai/dsh-util-values": "workspace:*", "diff": "^9.0.0", - "zod": "^4.4.3", + "zod": "4.6.6", "@deepseek-ai/dsh-typert-protocol": "workspace:*", "@deepseek-ai/dsh-storage-domain": "workspace:*" }, diff --git a/packages/experimental/client-ui-plugin-recommendations/package.json b/packages/experimental/client-ui-plugin-recommendations/package.json index cdd2975bed..bf0ee8d37e 100644 --- a/packages/experimental/client-ui-plugin-recommendations/package.json +++ b/packages/experimental/client-ui-plugin-recommendations/package.json @@ -57,7 +57,7 @@ "@deepseek-ai/dsh-package-manifest": "workspace:*", "@deepseek-ai/dsh-client-ui-action-cards": "workspace:*", "@deepseek-ai/dsh-action-cards": "workspace:*", - "zod": "4.4.3", + "zod": "4.6.6", "@deepseek-ai/dsh-client-test-runtime": "workspace:*" }, "files": [ diff --git a/packages/experimental/computer-use-cua-driver-native/package.json b/packages/experimental/computer-use-cua-driver-native/package.json index d2a48733ec..7538c33097 100644 --- a/packages/experimental/computer-use-cua-driver-native/package.json +++ b/packages/experimental/computer-use-cua-driver-native/package.json @@ -41,7 +41,7 @@ "@deepseek-ai/dsh-mcp-client": "workspace:*", "@deepseek-ai/schemastery": "workspace:~", "@trycua/cua-driver": "0.28.0", - "zod": "4.4.3" + "zod": "4.6.6" }, "devDependencies": { "@deepseek-ai/cordis": "workspace:~", diff --git a/packages/experimental/creation-recommendations/package.json b/packages/experimental/creation-recommendations/package.json index e7aa9bc456..39ed50e143 100644 --- a/packages/experimental/creation-recommendations/package.json +++ b/packages/experimental/creation-recommendations/package.json @@ -54,7 +54,7 @@ "dependencies": { "yaml": "^2.9.0", "resolve.exports": "^2.0.3", - "zod": "4.4.3", + "zod": "4.6.6", "@deepseek-ai/dsh-client-ui-creation-recommendations": "workspace:*", "@deepseek-ai/dsh-action-cards": "workspace:*", "@deepseek-ai/dsh-client-ui-action-cards": "workspace:*" diff --git a/packages/experimental/cua-macos/package.json b/packages/experimental/cua-macos/package.json index a77e27886b..c09f0a64d2 100644 --- a/packages/experimental/cua-macos/package.json +++ b/packages/experimental/cua-macos/package.json @@ -57,7 +57,7 @@ }, "dependencies": { "@deepseek-ai/schemastery": "workspace:~", - "zod": "^4.4.3", + "zod": "4.6.6", "@deepseek-ai/dsh-atomic-write": "workspace:*", "@deepseek-ai/dsh-cua": "workspace:*" }, diff --git a/packages/experimental/cua/package.json b/packages/experimental/cua/package.json index 625a8c0e00..646e9be509 100644 --- a/packages/experimental/cua/package.json +++ b/packages/experimental/cua/package.json @@ -83,7 +83,7 @@ }, "dependencies": { "@deepseek-ai/schemastery": "workspace:~", - "zod": "^4.4.3", + "zod": "4.6.6", "@deepseek-ai/dsh-atomic-write": "workspace:*", "@deepseek-ai/dsh-typert-protocol": "workspace:*", "@deepseek-ai/dsh-computer-use": "workspace:*", diff --git a/packages/experimental/forge-github/package.json b/packages/experimental/forge-github/package.json index bf31945ec1..d0270615b4 100644 --- a/packages/experimental/forge-github/package.json +++ b/packages/experimental/forge-github/package.json @@ -35,7 +35,7 @@ "@deepseek-ai/schemastery": "workspace:~", "@deepseek-ai/dsh-timeout": "workspace:*", "execa": "^10.0.1", - "zod": "^4.4.3" + "zod": "4.6.6" }, "peerDependencies": { "@deepseek-ai/cordis": "workspace:~", diff --git a/packages/experimental/forge-session/package.json b/packages/experimental/forge-session/package.json index 9f7a6b4f81..02c95e86a3 100644 --- a/packages/experimental/forge-session/package.json +++ b/packages/experimental/forge-session/package.json @@ -39,7 +39,7 @@ "@deepseek-ai/dsh-util-values": "workspace:*", "@deepseek-ai/schemastery": "workspace:~", "shell-quote": "^1.12.0", - "zod": "^4.4.3" + "zod": "4.6.6" }, "peerDependencies": { "@deepseek-ai/cordis": "workspace:~", diff --git a/packages/experimental/liveshare/package.json b/packages/experimental/liveshare/package.json index 5691e75c9f..eb8d5898fb 100644 --- a/packages/experimental/liveshare/package.json +++ b/packages/experimental/liveshare/package.json @@ -70,7 +70,7 @@ "@deepseek-ai/dsh-util-values": "workspace:*", "undici": "8.11.2", "ws": "^8.18.3", - "zod": "4.4.3" + "zod": "4.6.6" }, "peerDependencies": { "@deepseek-ai/cordis": "workspace:~", diff --git a/packages/experimental/plugin-recommendations/package.json b/packages/experimental/plugin-recommendations/package.json index 801a58f359..197c332ac2 100644 --- a/packages/experimental/plugin-recommendations/package.json +++ b/packages/experimental/plugin-recommendations/package.json @@ -54,7 +54,7 @@ "@deepseek-ai/dsh-skill": "workspace:*" }, "dependencies": { - "zod": "4.4.3", + "zod": "4.6.6", "@deepseek-ai/dsh-client-ui-plugin-recommendations": "workspace:*", "@deepseek-ai/dsh-action-cards": "workspace:*", "@deepseek-ai/dsh-client-ui-action-cards": "workspace:*" diff --git a/packages/experimental/speech-to-text-sensevoice/package.json b/packages/experimental/speech-to-text-sensevoice/package.json index e0ceeb6594..63a849a506 100644 --- a/packages/experimental/speech-to-text-sensevoice/package.json +++ b/packages/experimental/speech-to-text-sensevoice/package.json @@ -40,7 +40,7 @@ "dependencies": { "@deepseek-ai/schemastery": "workspace:~", "@deepseek-ai/dsh-timeout": "workspace:*", - "zod": "4.4.3", + "zod": "4.6.6", "sherpa-onnx-node": "1.13.8" }, "peerDependencies": { diff --git a/packages/extensions/cordis-host-runner/package.json b/packages/extensions/cordis-host-runner/package.json index ef0e595c7a..76a4a95915 100644 --- a/packages/extensions/cordis-host-runner/package.json +++ b/packages/extensions/cordis-host-runner/package.json @@ -52,7 +52,7 @@ "dependencies": { "@deepseek-ai/dsh-util-values": "workspace:*", "@deepseek-ai/schemastery": "workspace:~", - "zod": "4.4.3" + "zod": "4.6.6" }, "peerDependencies": { "@deepseek-ai/cordis": "workspace:~", diff --git a/packages/feedback/command-feedback/package.json b/packages/feedback/command-feedback/package.json index 551b0cee9d..d6f2b90963 100644 --- a/packages/feedback/command-feedback/package.json +++ b/packages/feedback/command-feedback/package.json @@ -57,7 +57,7 @@ "@deepseek-ai/dsh-typert-protocol": "workspace:*" }, "dependencies": { - "zod": "4.4.3" + "zod": "4.6.6" }, "devDependencies": { "@deepseek-ai/cordis-plugin-include": "workspace:~", diff --git a/packages/feedback/message-feedback/package.json b/packages/feedback/message-feedback/package.json index 4cc28f568e..0b3b527dcf 100644 --- a/packages/feedback/message-feedback/package.json +++ b/packages/feedback/message-feedback/package.json @@ -60,7 +60,7 @@ }, "dependencies": { "@deepseek-ai/schemastery": "workspace:~", - "zod": "4.4.3" + "zod": "4.6.6" }, "devDependencies": { "@deepseek-ai/cordis-plugin-include": "workspace:~", diff --git a/packages/goal/goal/package.json b/packages/goal/goal/package.json index d3e18ee5d9..544b86b3ba 100644 --- a/packages/goal/goal/package.json +++ b/packages/goal/goal/package.json @@ -65,7 +65,7 @@ }, "dependencies": { "@deepseek-ai/schemastery": "workspace:~", - "zod": "4.4.3" + "zod": "4.6.6" }, "devDependencies": { "@deepseek-ai/dsh-agent": "workspace:*", diff --git a/packages/host/plugin-inventory/package.json b/packages/host/plugin-inventory/package.json index 90fb43fc2d..d5f1800a83 100644 --- a/packages/host/plugin-inventory/package.json +++ b/packages/host/plugin-inventory/package.json @@ -50,7 +50,7 @@ ], "license": "MIT", "dependencies": { - "zod": "4.4.3" + "zod": "4.6.6" }, "peerDependencies": { "@deepseek-ai/cordis-plugin-loader": "workspace:~", diff --git a/packages/interaction/commands/package.json b/packages/interaction/commands/package.json index dab73482c5..80af110922 100644 --- a/packages/interaction/commands/package.json +++ b/packages/interaction/commands/package.json @@ -69,7 +69,7 @@ }, "dependencies": { "@deepseek-ai/dsh-util-crypto": "workspace:*", - "zod": "4.4.3" + "zod": "4.6.6" }, "devDependencies": { "@deepseek-ai/dsh-agent": "workspace:*", diff --git a/packages/interaction/permission-presets/package.json b/packages/interaction/permission-presets/package.json index 44cfe0aca2..d01845efb6 100644 --- a/packages/interaction/permission-presets/package.json +++ b/packages/interaction/permission-presets/package.json @@ -67,7 +67,7 @@ }, "dependencies": { "@deepseek-ai/schemastery": "workspace:~", - "zod": "4.4.3" + "zod": "4.6.6" }, "devDependencies": { "@deepseek-ai/dsh-agent": "workspace:*", diff --git a/packages/interaction/user-approval/package.json b/packages/interaction/user-approval/package.json index 80427f5392..4bb977d9fa 100644 --- a/packages/interaction/user-approval/package.json +++ b/packages/interaction/user-approval/package.json @@ -48,7 +48,7 @@ }, "dependencies": { "@deepseek-ai/schemastery": "workspace:~", - "zod": "4.4.3" + "zod": "4.6.6" }, "devDependencies": { "@deepseek-ai/dsh-agent": "workspace:*", diff --git a/packages/interaction/user-questions/package.json b/packages/interaction/user-questions/package.json index d99bc3e736..3eb448ad83 100644 --- a/packages/interaction/user-questions/package.json +++ b/packages/interaction/user-questions/package.json @@ -70,7 +70,7 @@ "@deepseek-ai/dsh-session-persistence-jsonl": "workspace:*" }, "dependencies": { - "zod": "4.4.3", + "zod": "4.6.6", "@deepseek-ai/schemastery": "workspace:~" } } diff --git a/packages/llm/llm-retry/package.json b/packages/llm/llm-retry/package.json index 04dffa4051..ab18cd6fde 100644 --- a/packages/llm/llm-retry/package.json +++ b/packages/llm/llm-retry/package.json @@ -46,7 +46,7 @@ }, "dependencies": { "@deepseek-ai/schemastery": "workspace:~", - "zod": "4.4.3" + "zod": "4.6.6" }, "devDependencies": { "@deepseek-ai/dsh-brand": "workspace:*", diff --git a/packages/llm/llm/package.json b/packages/llm/llm/package.json index 8adb0ba5db..2a15031cb9 100644 --- a/packages/llm/llm/package.json +++ b/packages/llm/llm/package.json @@ -67,7 +67,7 @@ "dependencies": { "@deepseek-ai/dsh-util-crypto": "workspace:*", "@deepseek-ai/schemastery": "workspace:~", - "zod": "4.4.3", + "zod": "4.6.6", "@deepseek-ai/dsh-timeout": "workspace:*", "@deepseek-ai/dsh-brand": "workspace:*", "@deepseek-ai/dsh-util-values": "workspace:*", diff --git a/packages/llm/token-meter/package.json b/packages/llm/token-meter/package.json index 94dbeeb6eb..9ac2389295 100644 --- a/packages/llm/token-meter/package.json +++ b/packages/llm/token-meter/package.json @@ -52,7 +52,7 @@ "dependencies": { "@deepseek-ai/dsh-util-values": "workspace:*", "@deepseek-ai/schemastery": "workspace:~", - "zod": "4.4.3" + "zod": "4.6.6" }, "devDependencies": { "@deepseek-ai/cordis": "workspace:~", diff --git a/packages/mcp/mcp-client/package.json b/packages/mcp/mcp-client/package.json index c848aa4b99..628e73ef92 100644 --- a/packages/mcp/mcp-client/package.json +++ b/packages/mcp/mcp-client/package.json @@ -69,7 +69,7 @@ "@deepseek-ai/dsh-http-proxy": "workspace:*", "@modelcontextprotocol/server": "2.2.0", "@modelcontextprotocol/node": "2.0.0", - "zod": "4.4.3", + "zod": "4.6.6", "@deepseek-ai/dsh-mcp-resources": "workspace:*", "@deepseek-ai/dsh-system-prompt": "workspace:*", "@deepseek-ai/dsh-credentials": "workspace:*" diff --git a/packages/mcp/mcp-manager/package.json b/packages/mcp/mcp-manager/package.json index da3a466d3d..613891fbf5 100644 --- a/packages/mcp/mcp-manager/package.json +++ b/packages/mcp/mcp-manager/package.json @@ -60,7 +60,7 @@ "dependencies": { "@deepseek-ai/dsh-timeout": "workspace:*", "@deepseek-ai/schemastery": "workspace:~", - "zod": "^4.4.3" + "zod": "4.6.6" }, "devDependencies": { "@modelcontextprotocol/client": "2.2.0", diff --git a/packages/plan/plan-mode/package.json b/packages/plan/plan-mode/package.json index 1dc10cc19c..b4685d4062 100644 --- a/packages/plan/plan-mode/package.json +++ b/packages/plan/plan-mode/package.json @@ -58,7 +58,7 @@ }, "dependencies": { "@deepseek-ai/dsh-brand": "workspace:*", - "zod": "4.4.3" + "zod": "4.6.6" }, "devDependencies": { "@deepseek-ai/dsh-agent": "workspace:*", diff --git a/packages/preset/agent-preset-registry/package.json b/packages/preset/agent-preset-registry/package.json index 3428d6df1e..0e44182b4b 100644 --- a/packages/preset/agent-preset-registry/package.json +++ b/packages/preset/agent-preset-registry/package.json @@ -69,7 +69,7 @@ "dependencies": { "@deepseek-ai/dsh-app-boot": "workspace:*", "@deepseek-ai/schemastery": "workspace:~", - "zod": "4.4.3", + "zod": "4.6.6", "js-yaml": "^4.3.2" }, "devDependencies": { diff --git a/packages/sandbox/sandbox-policy/package.json b/packages/sandbox/sandbox-policy/package.json index 5c1885539d..b4f96947c2 100644 --- a/packages/sandbox/sandbox-policy/package.json +++ b/packages/sandbox/sandbox-policy/package.json @@ -41,7 +41,7 @@ }, "dependencies": { "@deepseek-ai/schemastery": "workspace:~", - "zod": "4.4.3" + "zod": "4.6.6" }, "devDependencies": { "@deepseek-ai/dsh-agent": "workspace:*", diff --git a/packages/schedule/schedule/package.json b/packages/schedule/schedule/package.json index 3b53294dd8..e2c4ec1283 100644 --- a/packages/schedule/schedule/package.json +++ b/packages/schedule/schedule/package.json @@ -68,7 +68,7 @@ "dependencies": { "@deepseek-ai/schemastery": "workspace:~", "@js-temporal/polyfill": "0.5.1", - "zod": "4.4.3" + "zod": "4.6.6" }, "devDependencies": { "@deepseek-ai/cordis-plugin-loader": "workspace:~", diff --git a/packages/session/session-projection-cache/package.json b/packages/session/session-projection-cache/package.json index 460677d3bb..a6ffad2e62 100644 --- a/packages/session/session-projection-cache/package.json +++ b/packages/session/session-projection-cache/package.json @@ -34,7 +34,7 @@ "dependencies": { "@deepseek-ai/dsh-util-values": "workspace:*", "@deepseek-ai/schemastery": "workspace:~", - "zod": "4.4.3" + "zod": "4.6.6" }, "peerDependencies": { "@deepseek-ai/cordis": "workspace:~", diff --git a/packages/session/session-projection/package.json b/packages/session/session-projection/package.json index 63e2285c02..0b23457bcf 100644 --- a/packages/session/session-projection/package.json +++ b/packages/session/session-projection/package.json @@ -37,7 +37,7 @@ ], "license": "MIT", "dependencies": { - "zod": "4.4.3" + "zod": "4.6.6" }, "peerDependencies": { "@deepseek-ai/dsh-session": "workspace:*", diff --git a/packages/session/session-stats/package.json b/packages/session/session-stats/package.json index c5b207e47d..6f5bcdca9d 100644 --- a/packages/session/session-stats/package.json +++ b/packages/session/session-stats/package.json @@ -47,7 +47,7 @@ "@deepseek-ai/cordis": "workspace:~" }, "dependencies": { - "zod": "4.4.3" + "zod": "4.6.6" }, "devDependencies": { "@deepseek-ai/cordis-plugin-include": "workspace:~", diff --git a/packages/session/session-title/package.json b/packages/session/session-title/package.json index 8cf0c9db67..6f04527bce 100644 --- a/packages/session/session-title/package.json +++ b/packages/session/session-title/package.json @@ -51,7 +51,7 @@ "dependencies": { "@deepseek-ai/dsh-util-values": "workspace:*", "@deepseek-ai/schemastery": "workspace:~", - "zod": "4.4.3" + "zod": "4.6.6" }, "devDependencies": { "@deepseek-ai/cordis": "workspace:~", diff --git a/packages/session/session-turn-outline/package.json b/packages/session/session-turn-outline/package.json index b395b20ef8..ef8be0cb1e 100644 --- a/packages/session/session-turn-outline/package.json +++ b/packages/session/session-turn-outline/package.json @@ -47,7 +47,7 @@ "@deepseek-ai/dsh-session-projection": "workspace:*" }, "dependencies": { - "zod": "4.4.3" + "zod": "4.6.6" }, "devDependencies": { "@deepseek-ai/cordis": "workspace:~", diff --git a/packages/session/working-directory/package.json b/packages/session/working-directory/package.json index af6666f0e0..e7433cb7ca 100644 --- a/packages/session/working-directory/package.json +++ b/packages/session/working-directory/package.json @@ -58,6 +58,6 @@ }, "dependencies": { "@deepseek-ai/schemastery": "workspace:~", - "zod": "^4.4.3" + "zod": "4.6.6" } } diff --git a/packages/ssh/fs-ssh/package.json b/packages/ssh/fs-ssh/package.json index 1062d4bef4..de38d322fe 100644 --- a/packages/ssh/fs-ssh/package.json +++ b/packages/ssh/fs-ssh/package.json @@ -40,7 +40,7 @@ }, "dependencies": { "@deepseek-ai/schemastery": "workspace:~", - "zod": "4.4.3" + "zod": "4.6.6" }, "devDependencies": { "@deepseek-ai/cordis": "workspace:~", diff --git a/packages/ssh/sandbox-ssh/package.json b/packages/ssh/sandbox-ssh/package.json index 11eb8a66df..792e726545 100644 --- a/packages/ssh/sandbox-ssh/package.json +++ b/packages/ssh/sandbox-ssh/package.json @@ -38,7 +38,7 @@ }, "dependencies": { "@deepseek-ai/schemastery": "workspace:~", - "zod": "4.4.3" + "zod": "4.6.6" }, "devDependencies": { "@deepseek-ai/cordis": "workspace:~", diff --git a/packages/ssh/ssh/package.json b/packages/ssh/ssh/package.json index 6085e4f057..4b5f407dfa 100644 --- a/packages/ssh/ssh/package.json +++ b/packages/ssh/ssh/package.json @@ -65,7 +65,7 @@ }, "dependencies": { "@deepseek-ai/schemastery": "workspace:~", - "zod": "4.4.3" + "zod": "4.6.6" }, "devDependencies": { "@deepseek-ai/cordis": "workspace:~", diff --git a/packages/ssh/subprocess-ssh/package.json b/packages/ssh/subprocess-ssh/package.json index 5a8583d4dd..9c2edac302 100644 --- a/packages/ssh/subprocess-ssh/package.json +++ b/packages/ssh/subprocess-ssh/package.json @@ -39,7 +39,7 @@ }, "dependencies": { "@deepseek-ai/schemastery": "workspace:~", - "zod": "4.4.3" + "zod": "4.6.6" }, "devDependencies": { "@deepseek-ai/cordis": "workspace:~", diff --git a/packages/storage/storage-domain/package.json b/packages/storage/storage-domain/package.json index d59737b889..f3e7df2405 100644 --- a/packages/storage/storage-domain/package.json +++ b/packages/storage/storage-domain/package.json @@ -37,7 +37,7 @@ }, "dependencies": { "@deepseek-ai/schemastery": "workspace:~", - "zod": "4.4.3" + "zod": "4.6.6" }, "devDependencies": { "@deepseek-ai/dsh-storage": "workspace:*", diff --git a/packages/subagent/subagent-claude-code/package.json b/packages/subagent/subagent-claude-code/package.json index 13ee4081cb..3cf5c4206d 100644 --- a/packages/subagent/subagent-claude-code/package.json +++ b/packages/subagent/subagent-claude-code/package.json @@ -53,7 +53,7 @@ "@deepseek-ai/dsh-brand": "workspace:*", "@deepseek-ai/schemastery": "workspace:~", "@modelcontextprotocol/sdk": "^1.31.0", - "zod": "4.4.3", + "zod": "4.6.6", "@deepseek-ai/dsh-tool-subagent": "workspace:*" }, "devDependencies": { diff --git a/packages/subagent/subagent-claude-code/tests/subagent-claude-code.spec.ts b/packages/subagent/subagent-claude-code/tests/subagent-claude-code.spec.ts index 4b46c397d5..53af128a5d 100644 --- a/packages/subagent/subagent-claude-code/tests/subagent-claude-code.spec.ts +++ b/packages/subagent/subagent-claude-code/tests/subagent-claude-code.spec.ts @@ -360,7 +360,7 @@ describe('task admission and package contracts', () => { '@modelcontextprotocol/sdk', '^1.31.0', ) - expect(manifest.dependencies).toHaveProperty('zod', '4.4.3') + expect(manifest.dependencies).toHaveProperty('zod', '4.6.6') expect(manifest.dependencies).not.toHaveProperty('@deepseek-ai/dsh-subagent-codex') const sdkRoot = dirname(fileURLToPath( diff --git a/packages/subagent/subagent/package.json b/packages/subagent/subagent/package.json index 0b6494b2c4..6bff46b9ee 100644 --- a/packages/subagent/subagent/package.json +++ b/packages/subagent/subagent/package.json @@ -54,7 +54,7 @@ "@deepseek-ai/dsh-chunked-list": "workspace:*", "@deepseek-ai/dsh-util-values": "workspace:*", "@deepseek-ai/schemastery": "workspace:~", - "zod": "4.4.3" + "zod": "4.6.6" }, "peerDependencies": { "@deepseek-ai/cordis": "workspace:~", diff --git a/packages/subagent/tool-subagent/package.json b/packages/subagent/tool-subagent/package.json index 53f48ccb7a..a2f5c9b540 100644 --- a/packages/subagent/tool-subagent/package.json +++ b/packages/subagent/tool-subagent/package.json @@ -56,7 +56,7 @@ }, "dependencies": { "@deepseek-ai/schemastery": "workspace:~", - "zod": "4.4.3" + "zod": "4.6.6" }, "devDependencies": { "@deepseek-ai/cordis-plugin-loader": "workspace:~", diff --git a/packages/team/agent-team/package.json b/packages/team/agent-team/package.json index c72efe2730..87a3fa314a 100644 --- a/packages/team/agent-team/package.json +++ b/packages/team/agent-team/package.json @@ -56,7 +56,7 @@ "dependencies": { "@deepseek-ai/dsh-brand": "workspace:*", "@deepseek-ai/schemastery": "workspace:~", - "zod": "4.4.3" + "zod": "4.6.6" }, "peerDependencies": { "@deepseek-ai/cordis": "workspace:~", diff --git a/packages/todo/tool-todo/package.json b/packages/todo/tool-todo/package.json index ff7ade870e..8108320ac6 100644 --- a/packages/todo/tool-todo/package.json +++ b/packages/todo/tool-todo/package.json @@ -38,7 +38,7 @@ "license": "MIT", "dependencies": { "@deepseek-ai/schemastery": "workspace:~", - "zod": "4.4.3" + "zod": "4.6.6" }, "peerDependencies": { "@deepseek-ai/dsh-agent": "workspace:*", diff --git a/packages/typert/generator/package.json b/packages/typert/generator/package.json index fb276c98bb..032fc908a6 100644 --- a/packages/typert/generator/package.json +++ b/packages/typert/generator/package.json @@ -46,6 +46,6 @@ "@deepseek-ai/dsh-tool-cordis": "workspace:*", "@deepseek-ai/dsh-typert-registry": "workspace:*", "@deepseek-ai/cordis": "workspace:~", - "zod": "4.4.3" + "zod": "4.6.6" } } diff --git a/packages/typert/generator/tests/type-model.spec.ts b/packages/typert/generator/tests/type-model.spec.ts index 2cfb332711..53d711d0c1 100644 --- a/packages/typert/generator/tests/type-model.spec.ts +++ b/packages/typert/generator/tests/type-model.spec.ts @@ -1722,7 +1722,8 @@ describe('FaceModelEmitter', { timeout: 60_000 }, () => { skipLibCheck: false, target: ts.ScriptTarget.ES2024, lib: ['lib.es2024.d.ts'], - types: [], + // zod 4.6 declares its URL schemas with the global URL type; host consumers load node types. + types: ['node'], module: ts.ModuleKind.ESNext, moduleResolution: ts.ModuleResolutionKind.Bundler, }, diff --git a/packages/typert/loader/package.json b/packages/typert/loader/package.json index 9c882553cd..58ea7423f6 100644 --- a/packages/typert/loader/package.json +++ b/packages/typert/loader/package.json @@ -46,6 +46,6 @@ "@deepseek-ai/dsh-typert-protocol": "workspace:*", "@deepseek-ai/cordis": "workspace:~", "typescript": "^6.0.3", - "zod": "4.4.3" + "zod": "4.6.6" } } diff --git a/packages/typert/protocol/package.json b/packages/typert/protocol/package.json index 4f00323188..7fcc8f87f0 100644 --- a/packages/typert/protocol/package.json +++ b/packages/typert/protocol/package.json @@ -40,7 +40,7 @@ "license": "MIT", "dependencies": { "@deepseek-ai/dsh-brand": "workspace:*", - "zod": "^4.4.3" + "zod": "4.6.6" }, "peerDependencies": { "@deepseek-ai/cordis": "workspace:~" diff --git a/packages/typert/registry/package.json b/packages/typert/registry/package.json index f67e19a064..0ed5da68a2 100644 --- a/packages/typert/registry/package.json +++ b/packages/typert/registry/package.json @@ -56,7 +56,7 @@ ], "license": "MIT", "dependencies": { - "zod": "4.4.3" + "zod": "4.6.6" }, "peerDependencies": { "@deepseek-ai/cordis": "workspace:~" diff --git a/packages/util/chunked-list/package.json b/packages/util/chunked-list/package.json index b43d17fcc4..1700488ca1 100644 --- a/packages/util/chunked-list/package.json +++ b/packages/util/chunked-list/package.json @@ -36,6 +36,6 @@ "@deepseek-ai/cordis": "workspace:~" }, "dependencies": { - "zod": "4.4.3" + "zod": "4.6.6" } } diff --git a/packages/workspace/workspace/package.json b/packages/workspace/workspace/package.json index cbf421f847..787298f626 100644 --- a/packages/workspace/workspace/package.json +++ b/packages/workspace/workspace/package.json @@ -46,7 +46,7 @@ }, "dependencies": { "@deepseek-ai/dsh-brand": "workspace:*", - "zod": "4.4.3" + "zod": "4.6.6" }, "devDependencies": { "@deepseek-ai/cordis": "workspace:~", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c68c7f87f8..4fa3ede0ef 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -274,8 +274,8 @@ importers: specifier: ^4.1.11 version: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@22.20.1)(@vitest/coverage-v8@4.1.11)(happy-dom@20.11.6)(jsdom@29.1.1(@noble/hashes@2.4.0))(vite@8.2.2(@types/node@22.20.1)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.13)(yaml@2.9.0)) zod: - specifier: ^4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 apps/cli: dependencies: @@ -552,7 +552,7 @@ importers: devDependencies: '@agentclientprotocol/sdk': specifier: 1.4.0 - version: 1.4.0(zod@4.4.3) + version: 1.4.0(zod@4.6.6) '@deepseek-ai/dsh-acp': specifier: workspace:* version: link:../../packages/acp/acp @@ -837,8 +837,8 @@ importers: specifier: ^6.4.3 version: 6.4.3(@types/node@22.20.1)(jiti@2.7.0)(lightningcss@1.33.0)(tsx@4.23.13)(yaml@2.9.0) zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 apps/desktop-host: dependencies: @@ -1203,8 +1203,8 @@ importers: specifier: ^1.49.0 version: 1.61.1 zod: - specifier: ^4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 native/system: devDependencies: @@ -1248,7 +1248,7 @@ importers: dependencies: '@agentclientprotocol/sdk': specifier: 1.4.0 - version: 1.4.0(zod@4.4.3) + version: 1.4.0(zod@4.6.6) '@deepseek-ai/dsh-brand': specifier: workspace:* version: link:../../util/brand @@ -1308,8 +1308,8 @@ importers: packages/api/account-controller: dependencies: zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -1373,8 +1373,8 @@ importers: specifier: ^8.18.1 version: 8.18.1 zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 packages/api/job-controller: dependencies: @@ -1385,8 +1385,8 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -1422,8 +1422,8 @@ importers: specifier: workspace:* version: link:../../typert/protocol zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -1583,8 +1583,8 @@ importers: specifier: workspace:* version: link:../../web/web zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 packages/api/session-controller: dependencies: @@ -1604,8 +1604,8 @@ importers: specifier: ^3.0.2 version: 3.0.2 zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -1752,8 +1752,8 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -1801,8 +1801,8 @@ importers: specifier: ^6.0.0 version: 6.0.0 zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -1865,8 +1865,8 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -1917,8 +1917,8 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -2183,8 +2183,8 @@ importers: specifier: ^2.9.0 version: 2.9.0 zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -2298,8 +2298,8 @@ importers: specifier: ^7.8.5 version: 7.8.5 zod: - specifier: ^4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -3433,8 +3433,8 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -3470,8 +3470,8 @@ importers: specifier: workspace:* version: link:../../typert/protocol zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -3590,8 +3590,8 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -4347,8 +4347,8 @@ importers: specifier: ^18.2.0 version: 18.3.1(react@18.3.1) zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 packages/client/ui-deliverables: dependencies: @@ -5215,8 +5215,8 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -6544,8 +6544,8 @@ importers: specifier: ^18.2.0 version: 18.3.1(react@18.3.1) zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 packages/client/ui-sidebar-terminal: devDependencies: @@ -6938,8 +6938,8 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -7278,8 +7278,8 @@ importers: specifier: ^6.0.3 version: 6.0.3 zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 packages/compaction/command-compact: devDependencies: @@ -7592,8 +7592,8 @@ importers: packages/context/harness-dev-context: dependencies: zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -7632,8 +7632,8 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -7717,8 +7717,8 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -7778,8 +7778,8 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -7812,8 +7812,8 @@ importers: specifier: workspace:* version: link:../../util/brand zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -7880,8 +7880,8 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -8197,8 +8197,8 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -8362,8 +8362,8 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -8460,8 +8460,8 @@ importers: packages/experimental/action-cards: dependencies: zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -8515,8 +8515,8 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -8815,8 +8815,8 @@ importers: packages/experimental/browser-use-stagehand-native: dependencies: '@browserbasehq/stagehand': - specifier: 4.1.0 - version: 4.1.0 + specifier: 4.4.0 + version: 4.4.0 '@deepseek-ai/dsh-experimental-browser-use-runtime': specifier: workspace:* version: link:../browser-use-runtime @@ -8836,8 +8836,8 @@ importers: specifier: 3.2.2 version: 3.2.2(yauzl@3.4.0) zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -8903,8 +8903,8 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: ^4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -9105,8 +9105,8 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: ^4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -9220,8 +9220,8 @@ importers: specifier: ^18.2.0 version: 18.3.1 zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 packages/experimental/client-ui-forge: dependencies: @@ -9241,8 +9241,8 @@ importers: specifier: ^9.0.0 version: 9.0.0 zod: - specifier: ^4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -9416,8 +9416,8 @@ importers: specifier: ^18.2.0 version: 18.3.1 zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 packages/experimental/client-ui-stats: devDependencies: @@ -9621,8 +9621,8 @@ importers: specifier: 0.28.0 version: 0.28.0 zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -9710,8 +9710,8 @@ importers: specifier: ^2.9.0 version: 2.9.0 zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -9771,8 +9771,8 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: ^4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -9969,8 +9969,8 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: ^4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -10134,8 +10134,8 @@ importers: specifier: ^10.0.1 version: 10.0.1 zod: - specifier: ^4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -10165,8 +10165,8 @@ importers: specifier: ^1.12.0 version: 1.12.0 zod: - specifier: ^4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -10500,8 +10500,8 @@ importers: specifier: ^8.18.3 version: 8.21.3 zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -10822,8 +10822,8 @@ importers: specifier: workspace:* version: link:../client-ui-plugin-recommendations zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -11057,8 +11057,8 @@ importers: specifier: 1.13.8 version: 1.13.8 zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -11080,7 +11080,7 @@ importers: dependencies: '@agentclientprotocol/sdk': specifier: ^1.4.0 - version: 1.4.0(zod@4.4.3) + version: 1.4.0(zod@4.6.6) '@deepseek-ai/dsh-brand': specifier: workspace:* version: link:../../util/brand @@ -11095,10 +11095,10 @@ importers: version: link:../../../vendor/schemastery '@earendil-works/pi-ai': specifier: ^1.3.0 - version: 1.3.0(patch_hash=4e1c90fd00aea42989664504668633033c1962e53c07860901edb4f72d843765)(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.4.3))(@smithy/signature-v4@5.7.4)(supports-color@9.4.0)(undici@8.10.2)(ws@8.21.3)(zod@4.4.3) + version: 1.3.0(patch_hash=4e1c90fd00aea42989664504668633033c1962e53c07860901edb4f72d843765)(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.6.6))(@smithy/signature-v4@5.7.4)(supports-color@9.4.0)(undici@8.10.2)(ws@8.21.3)(zod@4.6.6) '@earendil-works/pi-coding-agent': specifier: ^1.3.0 - version: 1.3.0(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.4.3))(@smithy/signature-v4@5.7.4)(supports-color@9.4.0)(ws@8.21.3)(zod@4.4.3) + version: 1.3.0(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.6.6))(@smithy/signature-v4@5.7.4)(supports-color@9.4.0)(ws@8.21.3)(zod@4.6.6) cross-spawn: specifier: ^7.0.6 version: 7.0.6 @@ -11111,7 +11111,7 @@ importers: devDependencies: '@botiverse/oar': specifier: 0.67.0 - version: 0.67.0(patch_hash=65de2ca1966421e30893d8d4fed6a3d88e4247e131e7575fe791246e594538d6)(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.4.3))(@smithy/signature-v4@5.7.4)(supports-color@9.4.0)(ws@8.21.3)(zod@4.4.3) + version: 0.67.0(patch_hash=65de2ca1966421e30893d8d4fed6a3d88e4247e131e7575fe791246e594538d6)(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.6.6))(@smithy/signature-v4@5.7.4)(supports-color@9.4.0)(ws@8.21.3)(zod@4.6.6) '@deepseek-ai/cordis': specifier: workspace:~ version: link:../../../vendor/cordis @@ -11861,8 +11861,8 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -11985,8 +11985,8 @@ importers: packages/feedback/command-feedback: dependencies: zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -12025,8 +12025,8 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -12391,8 +12391,8 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -12702,8 +12702,8 @@ importers: packages/host/plugin-inventory: dependencies: zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -12807,8 +12807,8 @@ importers: specifier: workspace:* version: link:../../util/crypto zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -12841,8 +12841,8 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -12918,8 +12918,8 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -12952,8 +12952,8 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -13105,8 +13105,8 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -13305,7 +13305,7 @@ importers: version: link:../../../vendor/schemastery '@earendil-works/pi-ai': specifier: ^1.3.0 - version: 1.3.0(patch_hash=4e1c90fd00aea42989664504668633033c1962e53c07860901edb4f72d843765)(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.4.3))(@smithy/signature-v4@5.7.4)(supports-color@9.4.0)(undici@8.11.2)(ws@8.21.3)(zod@4.4.3) + version: 1.3.0(patch_hash=4e1c90fd00aea42989664504668633033c1962e53c07860901edb4f72d843765)(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.6.6))(@smithy/signature-v4@5.7.4)(supports-color@9.4.0)(undici@8.11.2)(ws@8.21.3)(zod@4.6.6) devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -13365,8 +13365,8 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -13475,8 +13475,8 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -13678,10 +13678,10 @@ importers: version: 2026.8.31(supports-color@9.4.0) '@modelcontextprotocol/server-filesystem': specifier: ^2026.8.31 - version: 2026.8.31(supports-color@9.4.0)(zod@4.4.3) + version: 2026.8.31(supports-color@9.4.0)(zod@4.6.6) zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 packages/mcp/mcp-manager: dependencies: @@ -13692,8 +13692,8 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: ^4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -13781,8 +13781,8 @@ importers: specifier: workspace:* version: link:../../util/brand zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -13876,8 +13876,8 @@ importers: specifier: ^4.3.2 version: 4.3.2 zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -14091,8 +14091,8 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -14162,8 +14162,8 @@ importers: specifier: 0.5.1 version: 0.5.1 zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -14873,8 +14873,8 @@ importers: packages/session/session-projection: dependencies: zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -14898,8 +14898,8 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -14923,8 +14923,8 @@ importers: packages/session/session-stats: dependencies: zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -15027,8 +15027,8 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -15129,8 +15129,8 @@ importers: packages/session/session-turn-outline: dependencies: zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -15190,8 +15190,8 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: ^4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -16004,8 +16004,8 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -16029,8 +16029,8 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -16048,8 +16048,8 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -16221,8 +16221,8 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -16249,8 +16249,8 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -16300,8 +16300,8 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -16392,7 +16392,7 @@ importers: dependencies: '@agentclientprotocol/sdk': specifier: 1.4.0 - version: 1.4.0(zod@4.4.3) + version: 1.4.0(zod@4.6.6) '@deepseek-ai/dsh-brand': specifier: workspace:* version: link:../../util/brand @@ -16450,10 +16450,10 @@ importers: dependencies: '@anthropic-ai/claude-agent-sdk': specifier: 0.3.263 - version: 0.3.263(@anthropic-ai/sdk@0.93.0(zod@4.4.3))(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.4.3))(zod@4.4.3) + version: 0.3.263(@anthropic-ai/sdk@0.93.0(zod@4.6.6))(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.6.6))(zod@4.6.6) '@anthropic-ai/sdk': specifier: 0.93.0 - version: 0.93.0(zod@4.4.3) + version: 0.93.0(zod@4.6.6) '@deepseek-ai/dsh-brand': specifier: workspace:* version: link:../../util/brand @@ -16465,10 +16465,10 @@ importers: version: link:../../../vendor/schemastery '@modelcontextprotocol/sdk': specifier: ^1.31.0 - version: 1.31.0(supports-color@9.4.0)(zod@4.4.3) + version: 1.31.0(supports-color@9.4.0)(zod@4.6.6) zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -16757,8 +16757,8 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -16926,8 +16926,8 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -17389,7 +17389,7 @@ importers: dependencies: '@agentclientprotocol/sdk': specifier: 1.4.0 - version: 1.4.0(zod@4.4.3) + version: 1.4.0(zod@4.6.6) '@deepseek-ai/cordis-plugin-include': specifier: workspace:~ version: link:../../../vendor/include @@ -17479,8 +17479,8 @@ importers: specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -17547,8 +17547,8 @@ importers: specifier: workspace:* version: link:../registry zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 packages/typert/loader: dependencies: @@ -17575,8 +17575,8 @@ importers: specifier: ^6.0.3 version: 6.0.3 zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 packages/typert/protocol: dependencies: @@ -17584,8 +17584,8 @@ importers: specifier: workspace:* version: link:../../util/brand zod: - specifier: ^4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -17594,8 +17594,8 @@ importers: packages/typert/registry: dependencies: zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -17619,8 +17619,8 @@ importers: packages/util/chunked-list: dependencies: zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -18139,8 +18139,8 @@ importers: specifier: workspace:* version: link:../../util/brand zod: - specifier: 4.4.3 - version: 4.4.3 + specifier: 4.6.6 + version: 4.6.6 devDependencies: '@deepseek-ai/cordis': specifier: workspace:~ @@ -19197,8 +19197,8 @@ packages: '@browserbasehq/sdk@2.20.0': resolution: {integrity: sha512-zDDsgrwF6/iF+Ob0LIqe6EPBa4n7m+5YfKn0g0+kZR5jIncTXBPmJSW6tAsu9go3wA9tQQP+zm6qUOPVi1Gutw==} - '@browserbasehq/stagehand@4.1.0': - resolution: {integrity: sha512-PJikMBVoaCRh6TFD7GcmeISmsMq4IwUu1BD5FOsGUVDUxrVqZomWa6W6dF+a/zu4xRZu2Z2xX1nXVMDaCuZWsw==} + '@browserbasehq/stagehand@4.4.0': + resolution: {integrity: sha512-Va2HZwdKjmqbCXIMc3wzCcG54hJToscEKoHo+pdRNIKmTqHJDF/ZvqrvMUQbRi8fXtXNncnoo9qhwvlXbsH2SA==} engines: {node: '>=22.18.0'} '@chevrotain/types@11.1.2': @@ -26504,8 +26504,8 @@ packages: peerDependencies: zod: ^3.25.28 || ^4 - zod@4.4.3: - resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + zod@4.6.6: + resolution: {integrity: sha512-4UmzQUF5fxvfcYHlbjBmejU+w+hbDnWFx4aS8kBx7nPBHcO2SqPbpAG8snbdg+Xse4pRefR98hgjFgIMCI/UhA==} zustand@4.5.7: resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==} @@ -26527,9 +26527,9 @@ packages: snapshots: - '@agentclientprotocol/sdk@1.4.0(zod@4.4.3)': + '@agentclientprotocol/sdk@1.4.0(zod@4.6.6)': dependencies: - zod: 4.4.3 + zod: 4.6.6 '@algolia/abtesting@1.21.2': dependencies: @@ -26672,11 +26672,11 @@ snapshots: '@anthropic-ai/claude-agent-sdk-win32-x64@0.3.263': optional: true - '@anthropic-ai/claude-agent-sdk@0.3.263(@anthropic-ai/sdk@0.93.0(zod@4.4.3))(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.4.3))(zod@4.4.3)': + '@anthropic-ai/claude-agent-sdk@0.3.263(@anthropic-ai/sdk@0.93.0(zod@4.6.6))(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.6.6))(zod@4.6.6)': dependencies: - '@anthropic-ai/sdk': 0.93.0(zod@4.4.3) - '@modelcontextprotocol/sdk': 1.31.0(supports-color@9.4.0)(zod@4.4.3) - zod: 4.4.3 + '@anthropic-ai/sdk': 0.93.0(zod@4.6.6) + '@modelcontextprotocol/sdk': 1.31.0(supports-color@9.4.0)(zod@4.6.6) + zod: 4.6.6 optionalDependencies: '@anthropic-ai/claude-agent-sdk-darwin-arm64': 0.3.263 '@anthropic-ai/claude-agent-sdk-darwin-x64': 0.3.263 @@ -26687,18 +26687,18 @@ snapshots: '@anthropic-ai/claude-agent-sdk-win32-arm64': 0.3.263 '@anthropic-ai/claude-agent-sdk-win32-x64': 0.3.263 - '@anthropic-ai/sdk@0.129.0(zod@4.4.3)': + '@anthropic-ai/sdk@0.129.0(zod@4.6.6)': dependencies: json-schema-to-ts: 3.1.1 standardwebhooks: 1.1.1 optionalDependencies: - zod: 4.4.3 + zod: 4.6.6 - '@anthropic-ai/sdk@0.93.0(zod@4.4.3)': + '@anthropic-ai/sdk@0.93.0(zod@4.6.6)': dependencies: json-schema-to-ts: 3.1.1 optionalDependencies: - zod: 4.4.3 + zod: 4.6.6 '@asamuzakjp/css-color@5.1.11': dependencies: @@ -27049,11 +27049,11 @@ snapshots: '@bcoe/v8-coverage@1.0.2': {} - '@botiverse/oar@0.67.0(patch_hash=65de2ca1966421e30893d8d4fed6a3d88e4247e131e7575fe791246e594538d6)(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.4.3))(@smithy/signature-v4@5.7.4)(supports-color@9.4.0)(ws@8.21.3)(zod@4.4.3)': + '@botiverse/oar@0.67.0(patch_hash=65de2ca1966421e30893d8d4fed6a3d88e4247e131e7575fe791246e594538d6)(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.6.6))(@smithy/signature-v4@5.7.4)(supports-color@9.4.0)(ws@8.21.3)(zod@4.6.6)': dependencies: - '@agentclientprotocol/sdk': 1.4.0(zod@4.4.3) - '@earendil-works/pi-ai': 1.3.0(patch_hash=4e1c90fd00aea42989664504668633033c1962e53c07860901edb4f72d843765)(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.4.3))(@smithy/signature-v4@5.7.4)(supports-color@9.4.0)(undici@8.10.2)(ws@8.21.3)(zod@4.4.3) - '@earendil-works/pi-coding-agent': 1.3.0(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.4.3))(@smithy/signature-v4@5.7.4)(supports-color@9.4.0)(ws@8.21.3)(zod@4.4.3) + '@agentclientprotocol/sdk': 1.4.0(zod@4.6.6) + '@earendil-works/pi-ai': 1.3.0(patch_hash=4e1c90fd00aea42989664504668633033c1962e53c07860901edb4f72d843765)(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.6.6))(@smithy/signature-v4@5.7.4)(supports-color@9.4.0)(undici@8.10.2)(ws@8.21.3)(zod@4.6.6) + '@earendil-works/pi-coding-agent': 1.3.0(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.6.6))(@smithy/signature-v4@5.7.4)(supports-color@9.4.0)(ws@8.21.3)(zod@4.6.6) cross-spawn: 7.0.6 undici: 8.10.2 which: 2.0.2 @@ -27090,12 +27090,12 @@ snapshots: transitivePeerDependencies: - encoding - '@browserbasehq/stagehand@4.1.0': + '@browserbasehq/stagehand@4.4.0': dependencies: '@browserbasehq/sdk': 2.20.0 '@opentelemetry/api': 1.9.1 '@opentelemetry/core': 2.9.0(@opentelemetry/api@1.9.1) - zod: 4.4.3 + zod: 4.6.6 transitivePeerDependencies: - encoding @@ -27181,9 +27181,9 @@ snapshots: transitivePeerDependencies: - '@algolia/client-search' - '@earendil-works/pi-agent-core@1.3.0(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.4.3))(@smithy/signature-v4@5.7.4)(supports-color@9.4.0)(undici@8.10.2)(ws@8.21.3)(zod@4.4.3)': + '@earendil-works/pi-agent-core@1.3.0(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.6.6))(@smithy/signature-v4@5.7.4)(supports-color@9.4.0)(undici@8.10.2)(ws@8.21.3)(zod@4.6.6)': dependencies: - '@earendil-works/pi-ai': 1.3.0(patch_hash=4e1c90fd00aea42989664504668633033c1962e53c07860901edb4f72d843765)(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.4.3))(@smithy/signature-v4@5.7.4)(supports-color@9.4.0)(undici@8.10.2)(ws@8.21.3)(zod@4.4.3) + '@earendil-works/pi-ai': 1.3.0(patch_hash=4e1c90fd00aea42989664504668633033c1962e53c07860901edb4f72d843765)(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.6.6))(@smithy/signature-v4@5.7.4)(supports-color@9.4.0)(undici@8.10.2)(ws@8.21.3)(zod@4.6.6) typebox: 1.3.27 transitivePeerDependencies: - '@aws-sdk/credential-provider-node' @@ -27198,16 +27198,16 @@ snapshots: - ws - zod - '@earendil-works/pi-ai@1.3.0(patch_hash=4e1c90fd00aea42989664504668633033c1962e53c07860901edb4f72d843765)(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.4.3))(@smithy/signature-v4@5.7.4)(supports-color@9.4.0)(undici@8.10.2)(ws@8.21.3)(zod@4.4.3)': + '@earendil-works/pi-ai@1.3.0(patch_hash=4e1c90fd00aea42989664504668633033c1962e53c07860901edb4f72d843765)(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.6.6))(@smithy/signature-v4@5.7.4)(supports-color@9.4.0)(undici@8.10.2)(ws@8.21.3)(zod@4.6.6)': dependencies: - '@anthropic-ai/sdk': 0.129.0(zod@4.4.3) + '@anthropic-ai/sdk': 0.129.0(zod@4.6.6) '@aws-sdk/client-bedrock-runtime': 3.1127.0 '@earendil-works/pi-telemetry': 1.3.0 - '@google/genai': 2.21.0(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.4.3))(supports-color@9.4.0) + '@google/genai': 2.21.0(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.6.6))(supports-color@9.4.0) '@smithy/node-http-handler': 4.12.1 http-proxy-agent: 9.1.0(supports-color@9.4.0) https-proxy-agent: 9.1.0(supports-color@9.4.0) - openai: 7.19.0(@aws-sdk/credential-provider-node@3.972.84)(@smithy/signature-v4@5.7.4)(undici@8.10.2)(ws@8.21.3)(zod@4.4.3) + openai: 7.19.0(@aws-sdk/credential-provider-node@3.972.84)(@smithy/signature-v4@5.7.4)(undici@8.10.2)(ws@8.21.3)(zod@4.6.6) partial-json: 0.1.7 typebox: 1.3.27 transitivePeerDependencies: @@ -27223,16 +27223,16 @@ snapshots: - ws - zod - '@earendil-works/pi-ai@1.3.0(patch_hash=4e1c90fd00aea42989664504668633033c1962e53c07860901edb4f72d843765)(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.4.3))(@smithy/signature-v4@5.7.4)(supports-color@9.4.0)(undici@8.11.2)(ws@8.21.3)(zod@4.4.3)': + '@earendil-works/pi-ai@1.3.0(patch_hash=4e1c90fd00aea42989664504668633033c1962e53c07860901edb4f72d843765)(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.6.6))(@smithy/signature-v4@5.7.4)(supports-color@9.4.0)(undici@8.11.2)(ws@8.21.3)(zod@4.6.6)': dependencies: - '@anthropic-ai/sdk': 0.129.0(zod@4.4.3) + '@anthropic-ai/sdk': 0.129.0(zod@4.6.6) '@aws-sdk/client-bedrock-runtime': 3.1127.0 '@earendil-works/pi-telemetry': 1.3.0 - '@google/genai': 2.21.0(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.4.3))(supports-color@9.4.0) + '@google/genai': 2.21.0(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.6.6))(supports-color@9.4.0) '@smithy/node-http-handler': 4.12.1 http-proxy-agent: 9.1.0(supports-color@9.4.0) https-proxy-agent: 9.1.0(supports-color@9.4.0) - openai: 7.19.0(@aws-sdk/credential-provider-node@3.972.84)(@smithy/signature-v4@5.7.4)(undici@8.11.2)(ws@8.21.3)(zod@4.4.3) + openai: 7.19.0(@aws-sdk/credential-provider-node@3.972.84)(@smithy/signature-v4@5.7.4)(undici@8.11.2)(ws@8.21.3)(zod@4.6.6) partial-json: 0.1.7 typebox: 1.3.27 transitivePeerDependencies: @@ -27252,10 +27252,10 @@ snapshots: dependencies: quickjs-wasi: 3.6.2 - '@earendil-works/pi-coding-agent@1.3.0(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.4.3))(@smithy/signature-v4@5.7.4)(supports-color@9.4.0)(ws@8.21.3)(zod@4.4.3)': + '@earendil-works/pi-coding-agent@1.3.0(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.6.6))(@smithy/signature-v4@5.7.4)(supports-color@9.4.0)(ws@8.21.3)(zod@4.6.6)': dependencies: - '@earendil-works/pi-agent-core': 1.3.0(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.4.3))(@smithy/signature-v4@5.7.4)(supports-color@9.4.0)(undici@8.10.2)(ws@8.21.3)(zod@4.4.3) - '@earendil-works/pi-ai': 1.3.0(patch_hash=4e1c90fd00aea42989664504668633033c1962e53c07860901edb4f72d843765)(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.4.3))(@smithy/signature-v4@5.7.4)(supports-color@9.4.0)(undici@8.10.2)(ws@8.21.3)(zod@4.4.3) + '@earendil-works/pi-agent-core': 1.3.0(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.6.6))(@smithy/signature-v4@5.7.4)(supports-color@9.4.0)(undici@8.10.2)(ws@8.21.3)(zod@4.6.6) + '@earendil-works/pi-ai': 1.3.0(patch_hash=4e1c90fd00aea42989664504668633033c1962e53c07860901edb4f72d843765)(@aws-sdk/credential-provider-node@3.972.84)(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.6.6))(@smithy/signature-v4@5.7.4)(supports-color@9.4.0)(undici@8.10.2)(ws@8.21.3)(zod@4.6.6) '@earendil-works/pi-codemode': 1.3.0 '@earendil-works/pi-mcp': 1.3.0 '@earendil-works/pi-tui': 1.3.0 @@ -27815,14 +27815,14 @@ snapshots: react-dom: 18.3.1(react@18.3.1) regenerator-runtime: 0.14.1 - '@google/genai@2.21.0(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.4.3))(supports-color@9.4.0)': + '@google/genai@2.21.0(@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.6.6))(supports-color@9.4.0)': dependencies: google-auth-library: 10.7.0(supports-color@9.4.0) p-retry: 4.6.2 protobufjs: 7.6.5 ws: 8.21.3 optionalDependencies: - '@modelcontextprotocol/sdk': 1.31.0(supports-color@9.4.0)(zod@4.4.3) + '@modelcontextprotocol/sdk': 1.31.0(supports-color@9.4.0)(zod@4.6.6) transitivePeerDependencies: - bufferutil - supports-color @@ -28185,11 +28185,11 @@ snapshots: eventsource-parser: 3.1.0 jose: 6.2.3 pkce-challenge: 5.0.1 - zod: 4.4.3 + zod: 4.6.6 '@modelcontextprotocol/core@2.2.0': dependencies: - zod: 4.4.3 + zod: 4.6.6 '@modelcontextprotocol/node@2.0.0(@modelcontextprotocol/server@2.2.0)(hono@4.13.7)': dependencies: @@ -28198,7 +28198,7 @@ snapshots: optionalDependencies: hono: 4.13.7 - '@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.4.3)': + '@modelcontextprotocol/sdk@1.31.0(supports-color@9.4.0)(zod@4.6.6)': dependencies: '@hono/node-server': 1.19.17(hono@4.13.7) ajv: 8.20.0 @@ -28215,25 +28215,25 @@ snapshots: json-schema-typed: 8.0.2 pkce-challenge: 5.0.1 raw-body: 3.0.2 - zod: 4.4.3 - zod-to-json-schema: 3.25.2(zod@4.4.3) + zod: 4.6.6 + zod-to-json-schema: 3.25.2(zod@4.6.6) transitivePeerDependencies: - supports-color '@modelcontextprotocol/server-everything@2026.8.31(supports-color@9.4.0)': dependencies: - '@modelcontextprotocol/sdk': 1.31.0(supports-color@9.4.0)(zod@4.4.3) + '@modelcontextprotocol/sdk': 1.31.0(supports-color@9.4.0)(zod@4.6.6) cors: 2.8.6 express: 5.2.1(supports-color@9.4.0) jszip: 3.10.1 - zod: 4.4.3 + zod: 4.6.6 transitivePeerDependencies: - '@cfworker/json-schema' - supports-color - '@modelcontextprotocol/server-filesystem@2026.8.31(supports-color@9.4.0)(zod@4.4.3)': + '@modelcontextprotocol/server-filesystem@2026.8.31(supports-color@9.4.0)(zod@4.6.6)': dependencies: - '@modelcontextprotocol/sdk': 1.31.0(supports-color@9.4.0)(zod@4.4.3) + '@modelcontextprotocol/sdk': 1.31.0(supports-color@9.4.0)(zod@4.6.6) diff: 8.0.4 glob: 13.0.6 minimatch: 10.2.5 @@ -28245,7 +28245,7 @@ snapshots: '@modelcontextprotocol/server@2.2.0': dependencies: '@modelcontextprotocol/core': 2.2.0 - zod: 4.4.3 + zod: 4.6.6 '@napi-rs/canvas-android-arm64@1.0.8': optional: true @@ -32809,21 +32809,21 @@ snapshots: powershell-utils: 0.2.1 wsl-utils: 1.0.0 - openai@7.19.0(@aws-sdk/credential-provider-node@3.972.84)(@smithy/signature-v4@5.7.4)(undici@8.10.2)(ws@8.21.3)(zod@4.4.3): + openai@7.19.0(@aws-sdk/credential-provider-node@3.972.84)(@smithy/signature-v4@5.7.4)(undici@8.10.2)(ws@8.21.3)(zod@4.6.6): optionalDependencies: '@aws-sdk/credential-provider-node': 3.972.84 '@smithy/signature-v4': 5.7.4 undici: 8.10.2 ws: 8.21.3 - zod: 4.4.3 + zod: 4.6.6 - openai@7.19.0(@aws-sdk/credential-provider-node@3.972.84)(@smithy/signature-v4@5.7.4)(undici@8.11.2)(ws@8.21.3)(zod@4.4.3): + openai@7.19.0(@aws-sdk/credential-provider-node@3.972.84)(@smithy/signature-v4@5.7.4)(undici@8.11.2)(ws@8.21.3)(zod@4.6.6): optionalDependencies: '@aws-sdk/credential-provider-node': 3.972.84 '@smithy/signature-v4': 5.7.4 undici: 8.11.2 ws: 8.21.3 - zod: 4.4.3 + zod: 4.6.6 opener@1.5.2: {} @@ -34503,11 +34503,11 @@ snapshots: compress-commons: 4.1.2 readable-stream: 3.6.2 - zod-to-json-schema@3.25.2(zod@4.4.3): + zod-to-json-schema@3.25.2(zod@4.6.6): dependencies: - zod: 4.4.3 + zod: 4.6.6 - zod@4.4.3: {} + zod@4.6.6: {} zustand@4.5.7(@types/react@18.3.31)(immer@10.2.0)(react@18.3.1): dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 0cfde00505..02bb4f0fb5 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -107,6 +107,8 @@ minimumReleaseAgeExclude: - pnpm@11.28.5 # Desktop packaging requires this Electron fingerprint with node-addon-require-builtin 0.1.9. - electron@44.7.0 + # Stagehand 4.4.0 carries the reviewed zod 4.6.6 pin; install it without the release-age wait. + - '@browserbasehq/stagehand@4.4.0' patchedDependencies: '@botiverse/oar@0.67.0': patches/@botiverse__oar@0.67.0.patch '@earendil-works/pi-ai@1.3.0': patches/@earendil-works__pi-ai@1.3.0.patch diff --git a/scripts/client-bundle-purity.spec.ts b/scripts/client-bundle-purity.spec.ts index 70d9b809a3..8eec7ba848 100644 --- a/scripts/client-bundle-purity.spec.ts +++ b/scripts/client-bundle-purity.spec.ts @@ -627,7 +627,7 @@ describe('client bundle debug artifacts', () => { const resolved = new URL(workspaceSource, 'https://dsh.test/plugins/@deepseek-ai/dsh-client-connection/client.js.map') expect(resolved.pathname).toBe('/packages/client/connection/src/rpc.ts') - const dependencySource = '../../../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/index.js' + const dependencySource = '../../../../node_modules/.pnpm/zod@4.6.6/node_modules/zod/index.js' expect(transform(dependencySource, sourceMapPath)).toBe(dependencySource) }) }) From 0e35813b35619a7e147d6a3eaa1c52131bb17e0b Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Thu, 5 Nov 2026 02:32:29 +0800 Subject: [PATCH 3/4] fix(stagehand): drop the deprecated extensionId option Stagehand 4.4.0 ignores extensionId and discovers the installed extension itself, so the option no longer selects anything. Remove it from the provider config and worker plumbing, update the READMEs and regenerated config catalog, and adjust the attach test fixture. --- docs/config-catalog.md | 2 -- docs/config-catalog.zh.md | 2 -- .../experimental/browser-use-stagehand-native/README.md | 1 - .../browser-use-stagehand-native/README.zh.md | 1 - .../browser-use-stagehand-native/src/index.ts | 8 ++------ .../browser-use-stagehand-native/src/native.ts | 3 --- .../browser-use-stagehand-native/src/worker.ts | 5 ++--- .../browser-use-stagehand-native/tests/provider.spec.ts | 4 ++-- 8 files changed, 6 insertions(+), 20 deletions(-) diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 0d9387b5fc..30a3ea3db6 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -961,8 +961,6 @@ export interface Config { mode: 'launch' | 'attach' /** CDP HTTP or WebSocket endpoint, required only for attach mode. */ cdpEndpoint?: string - /** Optional Stagehand extension id for an existing browser. */ - extensionId?: string /** Installed Chrome/Chromium executable used in launch mode. */ executablePath?: string /** Hide an owned browser's window. */ diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index a91c24c6bb..b3c7ea9d1d 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -963,8 +963,6 @@ export interface Config { mode: 'launch' | 'attach' /** CDP HTTP or WebSocket endpoint, required only for attach mode. */ cdpEndpoint?: string - /** Optional Stagehand extension id for an existing browser. */ - extensionId?: string /** Installed Chrome/Chromium executable used in launch mode. */ executablePath?: string /** Hide an owned browser's window. */ diff --git a/packages/experimental/browser-use-stagehand-native/README.md b/packages/experimental/browser-use-stagehand-native/README.md index ec1922366f..b6726b3650 100644 --- a/packages/experimental/browser-use-stagehand-native/README.md +++ b/packages/experimental/browser-use-stagehand-native/README.md @@ -50,7 +50,6 @@ Install a Chrome or Chromium executable that supports the pinned Stagehand SDK. |---|---|---| | `mode` | `launch` | Start a fresh browser or `attach` to an existing browser. | | `cdpEndpoint` | Required for `attach` | HTTP or WebSocket debugging endpoint selected by the profile. | -| `extensionId` | Load bundled extension | Installed Stagehand extension to use in an existing browser. | | `executablePath` | Installed stable Chrome | Chrome or Chromium executable, for `launch` only. | | `headless` | `true` | Hide a launched browser's window. | | `operationTimeoutMs` | `30000` | Deadline for Chromium startup, navigation, and natural-language actions. | diff --git a/packages/experimental/browser-use-stagehand-native/README.zh.md b/packages/experimental/browser-use-stagehand-native/README.zh.md index 47f7e908d6..d980760c27 100644 --- a/packages/experimental/browser-use-stagehand-native/README.zh.md +++ b/packages/experimental/browser-use-stagehand-native/README.zh.md @@ -50,7 +50,6 @@ kind: "package-reference" |---|---|---| | `mode` | `launch` | 启动独立浏览器,或以 `attach` 连接现有浏览器。 | | `cdpEndpoint` | `attach` 必填 | 由 profile 选择的 HTTP 或 WebSocket 调试端点。 | -| `extensionId` | 加载内置扩展 | 在现有浏览器中使用的已安装 Stagehand 扩展。 | | `executablePath` | 已安装的稳定版 Chrome | Chrome 或 Chromium 可执行程序,仅适用于 `launch`。 | | `headless` | `true` | 隐藏启动的浏览器窗口。 | | `operationTimeoutMs` | `30000` | Chromium 启动、导航和自然语言操作的超时时间。 | diff --git a/packages/experimental/browser-use-stagehand-native/src/index.ts b/packages/experimental/browser-use-stagehand-native/src/index.ts index 4709839b71..a8e5a26dd9 100644 --- a/packages/experimental/browser-use-stagehand-native/src/index.ts +++ b/packages/experimental/browser-use-stagehand-native/src/index.ts @@ -32,8 +32,6 @@ export interface Config { mode: 'launch' | 'attach' /** CDP HTTP or WebSocket endpoint, required only for attach mode. */ cdpEndpoint?: string - /** Optional Stagehand extension id for an existing browser. */ - extensionId?: string /** Installed Chrome/Chromium executable used in launch mode. */ executablePath?: string /** Hide an owned browser's window. */ @@ -55,7 +53,6 @@ export const Config: Schema = Schema.object({ }).required(), value => stagehandModelSchema.parse(value)).required(), mode: Schema.union(['launch', 'attach']).default('launch'), cdpEndpoint: Schema.string(), - extensionId: Schema.string(), executablePath: Schema.string(), headless: Schema.boolean().default(true), // Stagehand adds ten seconds to the action RPC timeout before arming its timer. @@ -86,8 +83,8 @@ export function apply(ctx: Context, input: Config): void { if (config.mode === 'attach' && !config.cdpEndpoint?.trim()) { throw new Error('Stagehand attach mode requires cdpEndpoint') } - if (config.mode === 'launch' && (config.cdpEndpoint !== undefined || config.extensionId !== undefined)) { - throw new Error('Stagehand cdpEndpoint and extensionId require attach mode') + if (config.mode === 'launch' && config.cdpEndpoint !== undefined) { + throw new Error('Stagehand cdpEndpoint requires attach mode') } if (config.mode === 'attach' && config.executablePath !== undefined) { throw new Error('Stagehand executablePath requires launch mode') @@ -106,7 +103,6 @@ export function apply(ctx: Context, input: Config): void { const connect = (connectionSignal: AbortSignal) => openBrowserWorker({ mode: 'attach', model: config.model, headless: config.headless, operationTimeoutMs: config.operationTimeoutMs, shutdownGraceMs: config.shutdownGraceMs, - ...config.extensionId === undefined ? {} : { extensionId: config.extensionId }, ...config.cdpEndpoint === undefined ? {} : { cdpEndpoint: config.cdpEndpoint }, ...chromium === undefined ? {} : { cdpEndpoint: chromium.endpoint }, }, connectionSignal, (message) => { ctx.logger.warn(message) }) diff --git a/packages/experimental/browser-use-stagehand-native/src/native.ts b/packages/experimental/browser-use-stagehand-native/src/native.ts index 7ef4fae2e1..34cab652b2 100644 --- a/packages/experimental/browser-use-stagehand-native/src/native.ts +++ b/packages/experimental/browser-use-stagehand-native/src/native.ts @@ -38,8 +38,6 @@ export interface NativeBrowserConfig { mode: 'launch' | 'attach' /** Existing browser's configured debugging endpoint. */ cdpEndpoint?: string - /** Optional existing Stagehand extension id. */ - extensionId?: string /** Executable selected for an owned Chromium instance. */ executablePath?: string /** Whether to hide an owned browser's window. */ @@ -96,7 +94,6 @@ export async function openNativeBrowser(config: NativeBrowserConfig): Promise]) const opening = openNativeBrowser( { - ...config, ...extensionId === undefined ? {} : { extensionId }, + ...config, ...cdpEndpoint === undefined ? {} : { cdpEndpoint }, ...executablePath === undefined ? {} : { executablePath }, }, diff --git a/packages/experimental/browser-use-stagehand-native/tests/provider.spec.ts b/packages/experimental/browser-use-stagehand-native/tests/provider.spec.ts index abe6bd38be..7dac15931f 100644 --- a/packages/experimental/browser-use-stagehand-native/tests/provider.spec.ts +++ b/packages/experimental/browser-use-stagehand-native/tests/provider.spec.ts @@ -117,11 +117,11 @@ it('lazily launches distinct browsers and preserves each Session tab state', asy }) it('reserves an attached browser for one live owner and rejects model connection overrides', async () => { - await ctx.plugin(Provider, { model: nativeModel, mode: 'attach', cdpEndpoint: 'http://localhost:9222', extensionId: 'fixture-extension' }) + await ctx.plugin(Provider, { model: nativeModel, mode: 'attach', cdpEndpoint: 'http://localhost:9222' }) expect((await execute(first, 'tabs', { action: 'list' })).isError).toBe(false) expect((await execute(second, 'tabs', { action: 'list' })).isError).toBe(true) expect(fixture.browsers).toHaveLength(1) - expect(fixture.browsers[0]?.options).toEqual({ cdpUrl: 'http://localhost:9222', extensionId: 'fixture-extension' }) + expect(fixture.browsers[0]?.options).toEqual({ cdpUrl: 'http://localhost:9222' }) expect((await execute(first, 'navigate', { url: 'https://example.com', cdpEndpoint: 'http://other' })).isError).toBe(true) expect((await execute(first, 'act', { instruction: 'click', model: 'other' })).isError).toBe(true) }) From 6c3285795a2b467066b476dee301eaa8182e0403 Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Thu, 5 Nov 2026 03:00:31 +0800 Subject: [PATCH 4/4] test(bench): widen the browser stream overlap window The input-overlap probe can start after the paced synthetic reply finishes on a loaded runner, so double the per-chunk pace and re-record the README translation hash after the extensionId removal. --- benchmarks/long-session-browser/synthetic-history.ts | 4 ++-- .../browser-use-stagehand-native/README.i18n.yaml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/benchmarks/long-session-browser/synthetic-history.ts b/benchmarks/long-session-browser/synthetic-history.ts index cc2ecec551..2426d61a4c 100644 --- a/benchmarks/long-session-browser/synthetic-history.ts +++ b/benchmarks/long-session-browser/synthetic-history.ts @@ -16,8 +16,8 @@ export const FIRST = 'SYNTHETIC_REPLY_FIRST' export const DONE = 'SYNTHETIC_REPLY_DONE' /** Paced text chunks per continuation. */ export const DELTAS = 120 -/** Replay delay per stream chunk, in milliseconds. */ -export const PACE_MS = 16 +/** Replay delay per stream chunk, in milliseconds; keeps the input-overlap probe inside the stream on loaded CI runners. */ +export const PACE_MS = 32 /** Create mixed prose, code, reasoning and tool history without reading user data. * @returns Current Session JSONL accepted by the shared Web seeder. diff --git a/packages/experimental/browser-use-stagehand-native/README.i18n.yaml b/packages/experimental/browser-use-stagehand-native/README.i18n.yaml index e6668a0d43..21daa32757 100644 --- a/packages/experimental/browser-use-stagehand-native/README.i18n.yaml +++ b/packages/experimental/browser-use-stagehand-native/README.i18n.yaml @@ -18,8 +18,8 @@ en: de47f71ab56fd0ed zh: fdc58f273a7dba3d /deepseek-ai-dsh-experimental-browser-use-stagehand-native/use-this-package/minimal-configuration: - en: 1554d55fd5be9735 - zh: d39e7dd0b8ff1b7d + en: 80f10a24e30b6d82 + zh: 26d3f5e350715a38 /deepseek-ai-dsh-experimental-browser-use-stagehand-native/use-this-package/verification: en: fea3ad16a77fc478 zh: cf551a8a2d183f93