@deepseek-ai/dsh-plugin-manager 0.2.0-rc.1 → 0.2.1-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.i18n.yaml CHANGED
@@ -15,8 +15,8 @@
15
15
  en: 87679c51aa3693f5
16
16
  zh: 11684a48a5806afb
17
17
  /deepseek-ai-dsh-plugin-manager/use-this-package:
18
- en: b8d7061ee289b2a0
19
- zh: 49aba7df4761fb23
18
+ en: 2cbe6e4a68994e2f
19
+ zh: ba0328f5538d8746
20
20
  /deepseek-ai-dsh-plugin-manager/use-this-package/version-compatibility-and-exemptions:
21
21
  en: 920c0acb3b9feaf3
22
22
  zh: ba6e832a256880a7
@@ -24,8 +24,8 @@
24
24
  en: d5643a06f300c30a
25
25
  zh: 9348b6a7676a2849
26
26
  /deepseek-ai-dsh-plugin-manager/understand-the-implementation:
27
- en: 1c737c375aeac163
28
- zh: 009d7f139a749c79
27
+ en: 7b54d30e231bb325
28
+ zh: 08df9441fed9927f
29
29
  /deepseek-ai-dsh-plugin-manager/further-exploration:
30
30
  en: c6538243d8702a0d
31
31
  zh: 04a82d923353c75f
@@ -48,8 +48,8 @@
48
48
  en: fde2d9491fe5dd4f
49
49
  zh: ef6e6db15c4f19d2
50
50
  /deepseek-ai-dsh-plugin-manager/known-limitations-and-deferred-work/failure-behavior:
51
- en: 61450cf6e03c2819
52
- zh: 4d52f5306825787b
51
+ en: 4447e4df8d62b274
52
+ zh: c76443e2edb97b1d
53
53
  /deepseek-ai-dsh-plugin-manager/known-limitations-and-deferred-work/dev-note:
54
54
  en: e67f633dc6fe7773
55
55
  zh: 3bb0c20c93f16f98
package/README.md CHANGED
@@ -41,7 +41,7 @@ A plugin toggle updates only `disabled` in the last matching override in the pro
41
41
 
42
42
  A selected bundle that cannot load remains in `listBundles` with an `error`; `enabled` records the saved selection, not successful loading. The plugin page shows the error and allows deselection. A broken bundle cannot be enabled. Management bundles remain protected if their files become unreadable.
43
43
 
44
- `listBundles` includes optional display `meta` for each bundle and its declared plugin rows, including disabled bundles. The Client selects a language from these values. The separate `description` field is the bundle's raw `package.json.description`; a metadata diagnostic does not block management. The `plugin_manager` tool omits UI display metadata from list results.
44
+ `listBundles` includes optional display `meta` for each bundle and its declared plugin rows, including disabled bundles. The Client selects a language from these values. The separate `description` field is the bundle's raw `package.json.description`; a metadata diagnostic does not block management. `source` gives a profile dependency as a spec `pnpm add` accepts: a git address or URL as recorded without the user information of an http(s) URL (a query string stays as recorded), a `file:` or `link:` path made absolute against the profile or the home directory for `~`, and a registry range, tag, or alias after the package name, as is any spec of a package installed under another name; a bundle the installation supplies has none, even when the profile also depends on it, because the installation's copy is the one that loads. The `plugin_manager` tool omits UI display metadata from list results.
45
45
 
46
46
  `inspect(spec, options)` reads what a spec names before anything installs: a registry name is asked of the registry through `pnpm view`, run in the profile directory so the same proxy and authentication settings apply as to the install; an absolute path has its `package.json` read; a git address or tarball answers only its form and the `host` it is fetched from. The answer carries the name, version, description, whether the package declares a bundle, and the `registry` that answered, or a `problem`: `invalid-spec`, `already-installed`, `not-found`, `not-a-package`, `not-a-bundle`, `network`, or `unknown`, with the `registries` asked. A caller's `signal` or `inspectTimeoutMs` ends the lookup.
47
47
 
@@ -51,7 +51,7 @@ Registries are asked in turn. The plan starts at `options.registry`, else the co
51
51
 
52
52
  The browser-safe `@deepseek-ai/dsh-plugin-manager/registry` entry exports `OFFICIAL_NPM_REGISTRY` and `NPMMIRROR_REGISTRY` for consumers that identify those public registries.
53
53
 
54
- `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`; `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, and `bundle` names the package a finished run added. `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.
54
+ `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`; `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.
55
55
 
56
56
  `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.
57
57
 
@@ -89,9 +89,13 @@ The CLI exposes `dsh plugin --profile <profile> version-exemptions`, `allow-vers
89
89
  <details>
90
90
  <summary>Implementation internals — click to expand</summary>
91
91
 
92
- The service and `dsh plugin` share the package operations in [operations.ts](src/operations.ts). The launcher supplies the current profile; [DSH HMR](../hmr/README.md) serializes module reloads, file watching and management writes. Each refresh re-reads bundle selection and patch layers, updates the original root Include, and awaits removed plugin resources as well as the remaining Loader tree. CLI and service operations share the profile manifest writer lock to prevent concurrent package and manifest writes. HMR does not acquire that lock. Pnpm runs outside the HMR queue; installation selects the bundle after pnpm succeeds, while removal deselects and unloads the bundle before pnpm runs. A service run whose captured output stays silent for `idleTimeoutMs` is terminated, reports `timedOut` alongside its exit status, is classified `timeout` whatever status the signal left behind, and is not retried on the next registry, which bounds how long one operation can hold the profile lock; the CLI inherits the terminal, captures no output, and stays unbounded for its operator to interrupt. A run completes when its process exits, and the pipes then drain under a bounded grace period, so a descendant that inherited them cannot hold the operation open. A terminated run stops its whole process tree and waits for it, because a lifecycle script outlives the pnpm process that started it (issue #4981). Each operation records the pnpm run it starts in `.plugin-manager/run.json` and removes the record when the run ends. A lock whose holder process exited is taken over by the next writer, but that process's pnpm tree can still be running, so an operation that finds a record waits up to five seconds for the recorded run to stop and otherwise fails with a diagnostic that names the process and the record, without running pnpm. Dependency-only changes do not trigger configuration reloads.
92
+ The service and `dsh plugin` share the package operations in [operations.ts](src/operations.ts). The launcher supplies the current profile; [DSH HMR](../hmr/README.md) serializes module reloads, file watching and management writes. Each configuration refresh re-reads bundle selection and patch layers, updates the original root Include, and awaits removed plugin resources as well as the remaining Loader tree. CLI and service operations share the profile manifest writer lock to prevent concurrent package and manifest writes. HMR does not acquire that lock. Pnpm runs outside the HMR queue; installation selects the bundle after pnpm succeeds, while removal deselects and unloads the bundle before pnpm runs.
93
93
 
94
- Results contain the last attempted stage, target, saved-state change, application status and error codes. Web dictionaries render management text; pnpm and Loader diagnostics remain unmodified. Unrelated pre-existing inactive entries return warnings; new or changed failures and inactive explicit enablement targets fail the operation. A failed or cancelled installation restores the manifest and lockfile it snapshotted before pnpm ran ([rationale](../../../.agents/notes/implemented/architecture/2026-09-15-guided-plugin-installation.md)); a failed removal retains its partial changes and diagnostics. Installations are tracked by request id until their call settles, so a cancellation names one run and joins its settlement without taking the profile lock. The CLI inherits authentication variables and terminal descriptors; service operations use a scrubbed environment and captured output. No invariant companion is published because the manager reads files and Loader state directly and owns no independent state projection.
94
+ Installing a new package, enabling a bundle, disabling it with HMR after its plugins stop, and a successful removal publish the profile's runtime resolution. Shared dependencies may pass to another bundle's declaration when their normalized directory, version, and scope stay unchanged. Without HMR, a deselected startup bundle still runs: while any such bundle is absent from the saved selection, subsequent package operations retain the entire current table and report `restart-required`. Other new installations and removals of non-running bundles can still publish without HMR; activation waits for restart. Overwriting an installed package publishes nothing and reports `restart-required` ([decision](../../../.agents/notes/implemented/architecture/2026-09-30-profile-package-refresh-and-manifest-invalidation.md)).
95
+
96
+ A service run whose captured output stays silent for `idleTimeoutMs` is terminated, reports `timedOut` alongside its exit status, is classified `timeout` whatever status the signal left behind, and is not retried on the next registry, which bounds how long one operation can hold the profile lock; the CLI inherits the terminal, captures no output, and stays unbounded for its operator to interrupt. A run completes when its process exits, and the pipes then drain under a bounded grace period, so a descendant that inherited them cannot hold the operation open. A terminated run stops its whole process tree and waits for it, because a lifecycle script outlives the pnpm process that started it (issue #4981). Each operation records the pnpm run it starts in `.plugin-manager/run.json` and removes the record when the run ends. A lock whose holder process exited is taken over by the next writer, but that process's pnpm tree can still be running, so an operation that finds a record waits up to five seconds for the recorded run to stop and otherwise fails with a diagnostic that names the process and the record, without running pnpm. Dependency-only changes do not trigger configuration reloads.
97
+
98
+ Results contain the last attempted stage, target, saved-state change, application status and error codes. Web dictionaries render management text; pnpm and Loader diagnostics remain unmodified. Unrelated pre-existing inactive entries return warnings; new or changed failures and inactive explicit enablement targets fail the operation. A failed or cancelled package run, or failed bundle validation, restores the manifest and lockfile snapshotted before pnpm ran ([rationale](../../../.agents/notes/implemented/architecture/2026-09-15-guided-plugin-installation.md)); later failures retain saved changes as described in [Failure behavior](#failure-behavior). Installations are tracked by request id until their call settles, so a cancellation names one run and joins its settlement without taking the profile lock. The CLI inherits authentication variables and terminal descriptors; service operations use a scrubbed environment and captured output.
95
99
 
96
100
  </details>
97
101
 
@@ -143,9 +147,10 @@ Failures preserve completed steps and report the actual remaining state. Profile
143
147
  |---|---|
144
148
  | Install: pnpm or bundle validation fails | Restore `package.json` and `pnpm-lock.yaml` as snapshotted before pnpm ran; files pnpm downloaded may remain. Report installation failure. |
145
149
  | Enable: saving selection or loading fails | Keep the installed dependency and any saved selection. Report enablement failure; allow repair, disablement or removal. |
150
+ | Resolution publication after a successful package operation fails | Keep completed disk changes and the previous runtime table. Report `application: 'failed'` with the publication diagnostic; pnpm's zero exit code does not mean publication succeeded. |
146
151
  | Remove: any step fails | Stop at the failed step. Preserve completed changes, retain remaining dependencies for retry, and report removal failure. Do not re-enable the bundle. |
147
152
 
148
- Installation finishes after pnpm and bundle validation succeed; subsequent enablement failure does not undo installation. Removal proceeds in order: remove the bundle from `dsh.profile.bundles`, unload its runtime contributions, then run `pnpm remove`. A failed step prevents subsequent steps.
153
+ Installation finishes after pnpm and bundle validation succeed; subsequent publication or enablement failure does not undo installation. Removal proceeds in order: remove the bundle from `dsh.profile.bundles`, unload its runtime contributions, run `pnpm remove`, then publish runtime resolution unless a deselected startup bundle still runs without HMR. A failed step prevents subsequent steps; publication failure after removal does not restore deleted packages. A selected name that neither the profile's dependencies nor the installation holds, such as a retired bundle, is also removable; its removal stops after deselection and runs no pnpm.
149
154
 
150
155
  Restoration rewrites only the two snapshotted files; user-authored patch configuration, application data, diagnostic logs and files pnpm downloaded remain untouched, and the next package operation prunes packages no manifest references.
151
156
 
package/README.zh.md CHANGED
@@ -41,7 +41,7 @@ kind: "package-reference"
41
41
 
42
42
  已选择但无法加载的组合包仍会出现在 `listBundles` 中,并携带 `error`;`enabled` 表示保存的选择,不代表加载成功。插件页面显示错误并允许取消选择。损坏的组合包无法启用。管理组合包的文件变得不可读后仍受保护。
43
43
 
44
- `listBundles` 为各组合包及其声明的插件行提供可选的展示 `meta`,包括已禁用的组合包。Client 从这些值中选择语言。单独的 `description` 字段是该组合包原始的 `package.json.description`;元信息诊断不会阻止管理操作。`plugin_manager` 工具的列表结果不包含 UI 展示元信息。
44
+ `listBundles` 为各组合包及其声明的插件行提供可选的展示 `meta`,包括已禁用的组合包。Client 从这些值中选择语言。单独的 `description` 字段是该组合包原始的 `package.json.description`;元信息诊断不会阻止管理操作。`source` 以 `pnpm add` 接受的 spec 给出 profile 依赖:git 地址或 URL 按记录给出,但去掉 http(s) URL 的 user information(查询字符串保持原样);`file:` 或 `link:` 路径按 profile 目录解析为绝对路径,`~` 展开为主目录;注册表版本范围、标签或别名接在包名的 `@` 之后,以别名安装的包的 spec 也是如此。安装提供的组合包没有该字段,即使 profile 也依赖它,因为实际加载的是安装中的副本。`plugin_manager` 工具的列表结果不包含 UI 展示元信息。
45
45
 
46
46
  `inspect(spec, options)` 在任何东西安装之前读出 spec 指向什么:注册表包名通过 `pnpm view` 询问注册表,在 profile 目录中运行,因而与安装使用同样的代理与认证设置;绝对路径读取其 `package.json`;git 地址或 tarball 只答复自己的形式和它被拉取的 `host`。答复携带名称、版本、描述、该包是否声明组合包,以及作答的 `registry`,否则给出 `problem`:`invalid-spec`、`already-installed`、`not-found`、`not-a-package`、`not-a-bundle`、`network` 或 `unknown`,并附上问过的 `registries`。调用方的 `signal` 或 `inspectTimeoutMs` 会结束查询。
47
47
 
@@ -51,7 +51,7 @@ kind: "package-reference"
51
51
 
52
52
  可在浏览器使用的 `@deepseek-ai/dsh-plugin-manager/registry` 入口导出 `OFFICIAL_NPM_REGISTRY` 和 `NPMMIRROR_REGISTRY`,供消费方识别这两个公共注册表。
53
53
 
54
- `installBundle` 接受调用方生成的 `requestId`,`plugin-manager/install-log` 在其下流式转发每次 pnpm 运行的输出,`plugin-manager/install-state` 通告 `installing`、`cancelling` 与 `applying`;每问一个注册表通告一次 `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` 给出完成的运行新增的包。`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 或手工编辑后)不发通知,页面要到下一次读取才知道。
54
+ `installBundle` 接受调用方生成的 `requestId`,`plugin-manager/install-log` 在其下流式转发每次 pnpm 运行的输出,`plugin-manager/install-state` 通告 `installing`、`cancelling` 与 `applying`;每问一个注册表通告一次 `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 或手工编辑后)不发通知,页面要到下一次读取才知道。
55
55
 
56
56
  `waitForInstall(requestId)` 让客户端在响应丢失后等待活动安装的结果,包括不可取消的应用阶段。它返回与原调用相同的结果,请求不在活动中时返回 `null`。已完成的结果不保留;`null` 不表示成功或已取消。
57
57
 
@@ -89,9 +89,13 @@ CLI 提供 `dsh plugin --profile <profile> version-exemptions`、`allow-version
89
89
  <details>
90
90
  <summary>实现细节——点击展开</summary>
91
91
 
92
- 服务与 `dsh plugin` 共用 [operations.ts](src/operations.ts) 中的包管理操作。启动器提供当前 profile;[DSH HMR](../hmr/README.zh.md) 串行执行模块重载、文件监听和管理写入。每次刷新重新读取组合包选择与 patch 层,更新原有根 Include,并等待已移除插件释放资源及剩余 Loader 树稳定。CLI 与 service 操作共用 profile manifest 写锁,防止并发包操作和 manifest 写入。HMR 不获取该锁。pnpm 在 HMR 队列之外执行;安装在 pnpm 成功后选入组合包,删除则在执行 pnpm 前取消选入并完成卸载。service 运行若在 `idleTimeoutMs` 内没有任何捕获输出即被终止,与退出状态一并报告 `timedOut`,不论信号留下什么退出状态都归类为 `timeout`,且不再转问下一个注册表,因此单次操作占用 profile 锁的时长有上界;CLI 继承终端、不捕获输出,因此不受此上界约束,由操作者中断。运行以进程退出为完成点,随后只在一个有界的宽限窗口内排空管道,因此继承管道的孙进程无法让操作挂起。被终止的运行会停止整棵进程树并等待其消失,因为生命周期脚本的存活时间超过启动它的 pnpm 进程(issue #4981)。每个操作把它启动的 pnpm 运行记录在 `.plugin-manager/run.json` 中,并在运行结束时删除该记录。持有者进程已退出的锁会被下一个写入方接管,但该进程的 pnpm 进程树可能仍在运行,因此发现记录的操作最多等待五秒让记录中的运行停止,否则不运行 pnpm,并以指明该进程与记录文件的诊断失败。仅依赖字段变化不会触发配置重载。
92
+ 服务与 `dsh plugin` 共用 [operations.ts](src/operations.ts) 中的包管理操作。启动器提供当前 profile;[DSH HMR](../hmr/README.zh.md) 串行执行模块重载、文件监听和管理写入。每次配置刷新重新读取组合包选择与 patch 层,更新原有根 Include,并等待已移除插件释放资源及剩余 Loader 树稳定。CLI 与 service 操作共用 profile manifest 写锁,防止并发包操作和 manifest 写入。HMR 不获取该锁。pnpm 在 HMR 队列之外执行;安装在 pnpm 成功后选入组合包,删除则在执行 pnpm 前取消选入并完成卸载。
93
93
 
94
- 结果包含最后尝试的阶段、目标、磁盘变化、应用状态和错误码。Web 词典呈现管理文案;pnpm 与 Loader 的诊断保持原样。无关的已有故障作为警告返回;新出现、配置变化后的故障,以及显式启用目标未激活,都会使操作失败。失败或被取消的安装会恢复 pnpm 运行前快照的 manifest 与 lockfile([理由](../../../.agents/notes/implemented/architecture/2026-09-15-guided-plugin-installation.zh.md));失败的删除保留部分改动和诊断。安装按 request id 跟踪到调用结束,因此取消只针对一次运行,并且不取 profile 锁就能等待它结束。CLI 继承认证环境和终端描述符;service 使用清理后的环境并捕获输出。管理器直接读取文件和 Loader 状态,不维护第二份目标状态注册表,因此不发布单独的运行时不变式伴生入口。
94
+ 安装新包、启用组合包、有 HMR 时停用组合包并等插件退出后、以及删除成功后,都会发布 profile 的 runtime resolution。共享依赖的规范化目录、版本和作用域不变时,可以改由另一个组合包声明。没有 HMR 时,已取消选入的启动组合包仍在运行:只要保存的选择列表中缺少任何这样的组合包,后续包操作就保留整张当前表,并报告 `restart-required`。其他新安装和未运行组合包的删除仍可在没有 HMR 时发布;激活等待重启。覆盖已安装的包不发布,并报告 `restart-required`([决策](../../../.agents/notes/implemented/architecture/2026-09-30-profile-package-refresh-and-manifest-invalidation.zh.md))。
95
+
96
+ service 运行若在 `idleTimeoutMs` 内没有任何捕获输出即被终止,与退出状态一并报告 `timedOut`,不论信号留下什么退出状态都归类为 `timeout`,且不再转问下一个注册表,因此单次操作占用 profile 锁的时长有上界;CLI 继承终端、不捕获输出,因此不受此上界约束,由操作者中断。运行以进程退出为完成点,随后只在一个有界的宽限窗口内排空管道,因此继承管道的孙进程无法让操作挂起。被终止的运行会停止整棵进程树并等待其消失,因为生命周期脚本的存活时间超过启动它的 pnpm 进程(issue #4981)。每个操作把它启动的 pnpm 运行记录在 `.plugin-manager/run.json` 中,并在运行结束时删除该记录。持有者进程已退出的锁会被下一个写入方接管,但该进程的 pnpm 进程树可能仍在运行,因此发现记录的操作最多等待五秒让记录中的运行停止,否则不运行 pnpm,并以指明该进程与记录文件的诊断失败。仅依赖字段变化不会触发配置重载。
97
+
98
+ 结果包含最后尝试的阶段、目标、磁盘变化、应用状态和错误码。Web 词典呈现管理文案;pnpm 与 Loader 的诊断保持原样。无关的已有故障作为警告返回;新出现、配置变化后的故障,以及显式启用目标未激活,都会使操作失败。包管理运行失败或被取消,或组合包校验失败时,会恢复 pnpm 运行前快照的 manifest 与 lockfile([理由](../../../.agents/notes/implemented/architecture/2026-09-15-guided-plugin-installation.zh.md));后续失败按[失败行为](#failure-behavior)所述保留已保存的改动。安装按 request id 跟踪到调用结束,因此取消只针对一次运行,并且不取 profile 锁就能等待它结束。CLI 继承认证环境和终端描述符;service 使用清理后的环境并捕获输出。管理器直接读取文件和 Loader 状态,不维护第二份目标状态注册表,因此不发布单独的运行时不变式伴生入口。
95
99
 
96
100
  </details>
97
101
 
@@ -143,9 +147,10 @@ CLI 提供 `dsh plugin --profile <profile> version-exemptions`、`allow-version
143
147
  |---|---|
144
148
  | 安装:pnpm 执行或组合包校验失败 | 恢复 pnpm 运行前快照的 `package.json` 与 `pnpm-lock.yaml`;pnpm 已下载的文件可能保留。报告安装失败。 |
145
149
  | 启用:保存选择项或加载失败 | 保留已安装的依赖和已保存的选择项。报告启用失败,允许修正、停用或卸载。 |
150
+ | 包操作成功后解析表发布失败 | 保留已完成的磁盘改动和上一张运行时表。报告 `application: 'failed'` 及发布诊断;pnpm 的零退出码不代表发布成功。 |
146
151
  | 卸载:任一步失败 | 停在失败步骤,保留已完成的改动和待重试删除的依赖,报告卸载失败。不重新启用组合包。 |
147
152
 
148
- pnpm 执行和组合包校验成功即完成安装,后续启用失败不撤销安装。卸载依次执行:从 `dsh.profile.bundles` 移除组合包、卸载运行时贡献、执行 `pnpm remove`。任一步失败都不继续执行后续步骤。
153
+ pnpm 执行和组合包校验成功即完成安装,后续发布或启用失败不撤销安装。卸载依次执行:从 `dsh.profile.bundles` 移除组合包、卸载运行时贡献、执行 `pnpm remove`,然后发布 runtime resolution,除非没有 HMR 且已取消选入的启动组合包仍在运行。任一步失败都不继续执行后续步骤;删除后的发布失败不会恢复已删除的包。 profile 依赖和安装都不持有的已选名称(例如已下线的组合包)同样可卸载;其卸载在取消选择后结束,不运行 pnpm。
149
154
 
150
155
  恢复只重写这两份快照文件;用户编写的 patch 配置、应用数据、诊断日志以及 pnpm 已下载的文件保持原样,没有 manifest 引用的包由下一次包操作清理。
151
156
 
package/lib/index.js CHANGED
@@ -11,6 +11,7 @@ import { OPTIONAL_BUNDLES, PROFILE_COMPATIBILITY_FILENAME, bundlePatchPaths, com
11
11
  import { once } from "node:events";
12
12
  import { execa } from "execa";
13
13
  import { scrubbedParentEnv } from "@deepseek-ai/dsh-subprocess";
14
+ import { homedir } from "node:os";
14
15
  import { isAlias, isMap, isNode, isScalar, isSeq, parseDocument, visit } from "yaml";
15
16
  //#region lib/types/install-spec.js
16
17
  /**
@@ -115,6 +116,32 @@ function parseInstallSpec(raw) {
115
116
  range
116
117
  };
117
118
  }
119
+ /** A recorded dependency value pnpm resolves outside a registry: a protocol other than a registry alias, or an scp-like git address. */
120
+ const NON_REGISTRY_VALUE = /^(?!(?:npm|jsr|workspace|catalog):)[a-z][a-z0-9+.-]*:|^[^@/:\s]+@[^:/\s]+:/i;
121
+ /** The user information of an http(s) URL, up to the last `@` before the path, which holds a password or access token when present. */
122
+ const HTTP_USER_INFO = /^((?:git\+)?https?:\/\/)[^/]*@/i;
123
+ /**
124
+ * The spec `pnpm add` accepts for a dependency the profile manifest records. A git address, URL, or other
125
+ * non-registry protocol is the recorded value without the user information of an http(s) URL, whose query string
126
+ * is kept; a `file:` or `link:` path, which pnpm records relative to the profile or with `~` for the home
127
+ * directory, becomes absolute. A registry range, tag, or alias follows the dependency name after `@`, as does a
128
+ * non-registry value installed under a name other than the package's own.
129
+ * @param name - the dependency name.
130
+ * @param recorded - the value the profile manifest records for it.
131
+ * @param profileDir - the profile directory a relative local path is resolved against.
132
+ * @param packageName - the name the installed package's manifest declares, when it could be read.
133
+ * @returns the installable spec.
134
+ */
135
+ function dependencySpec(name, recorded, profileDir, packageName = name) {
136
+ const local = /^(file|link):(.*)$/s.exec(recorded);
137
+ let spec;
138
+ if (local !== null) {
139
+ const path = local[2].replace(/^~(?=$|[\\/])/, () => homedir());
140
+ spec = `${local[1]}:${resolve(profileDir, path)}`;
141
+ } else if (NON_REGISTRY_VALUE.test(recorded)) spec = recorded.replace(HTTP_USER_INFO, "$1");
142
+ else return `${name}@${recorded}`;
143
+ return packageName === name ? spec : `${name}@${spec}`;
144
+ }
118
145
  //#endregion
119
146
  //#region lib/types/run-tree.js
120
147
  /** Waiting for one package run's process tree to disappear before its caller touches the profile. */
@@ -1450,14 +1477,15 @@ let PluginManager = (() => {
1450
1477
  }
1451
1478
  /** Read the profile's installed bundles, the bundles this dsh installation supplies, and the selected names that are not bundles.
1452
1479
  * A dependency without a bundle patch is listed, as a `not-bundle` problem, only while it is selected.
1453
- * @returns Package versions, manifest descriptions, rows, optional display metadata, activation selections,
1454
- * whether the installation offers the bundle, and removal availability.
1480
+ * @returns Package versions, manifest descriptions, the installable spec of profile dependencies, rows, optional
1481
+ * display metadata, activation selections, whether the installation offers the bundle, and removal availability.
1455
1482
  */
1456
1483
  listBundles() {
1457
1484
  const manifest = readProfileManifest("dsh", this.profile.dir);
1458
1485
  const exemptions = readProfileVersionExemptions(this.profile.dir);
1459
1486
  const selected = manifest.dsh?.profile?.bundles ?? [];
1460
- const dependencies = Object.keys(manifest.dependencies ?? {});
1487
+ const recorded = manifest.dependencies ?? {};
1488
+ const dependencies = Object.keys(recorded);
1461
1489
  const installation = JSON.parse(readFileSync(this.profile.installAnchor, "utf8"));
1462
1490
  const names = [...new Set([
1463
1491
  ...selected,
@@ -1468,14 +1496,18 @@ let PluginManager = (() => {
1468
1496
  for (const name of names) {
1469
1497
  const installed = dependencies.includes(name);
1470
1498
  const optional = OPTIONAL_BUNDLES.includes(name);
1471
- const removable = installed && !Object.hasOwn(installation.dependencies ?? {}, name);
1472
1499
  const enabled = selected.includes(name);
1500
+ const shipped = Object.hasOwn(installation.dependencies ?? {}, name);
1501
+ const owned = installed && !shipped;
1502
+ const removable = owned || enabled && !installed && !shipped;
1503
+ const sourceOf = (packageName) => owned ? { source: dependencySpec(name, recorded[name], this.profile.dir, packageName) } : {};
1473
1504
  const readOnlyReason = this.protectsManager(name) ? "management-required" : void 0;
1474
1505
  try {
1475
1506
  const info = bundleManifest(name, this.profile.dir, this.profile.installAnchor);
1476
1507
  if (info === void 0) {
1477
1508
  if (enabled) bundles.push({
1478
1509
  name,
1510
+ ...sourceOf(),
1479
1511
  enabled,
1480
1512
  installed,
1481
1513
  optional,
@@ -1496,6 +1528,7 @@ let PluginManager = (() => {
1496
1528
  ...info.version === void 0 ? {} : { version: info.version },
1497
1529
  ...info.description === void 0 || info.description === "" ? {} : { description: info.description },
1498
1530
  ...meta === void 0 ? {} : { meta },
1531
+ ...sourceOf(info.name),
1499
1532
  enabled,
1500
1533
  installed,
1501
1534
  optional,
@@ -1506,6 +1539,7 @@ let PluginManager = (() => {
1506
1539
  } catch (error) {
1507
1540
  if (enabled || installed) bundles.push({
1508
1541
  name,
1542
+ ...sourceOf(),
1509
1543
  enabled,
1510
1544
  installed,
1511
1545
  optional,
@@ -1670,7 +1704,9 @@ let PluginManager = (() => {
1670
1704
  setBundleEnabled(name, enabled) {
1671
1705
  return this.change((result) => this.configure(async () => {
1672
1706
  await this.selectBundle(name, enabled);
1707
+ if (enabled) await this.refreshPackages();
1673
1708
  result.warnings = await this.reload(enabled ? this.bundleRows(name).map((row) => row.id) : []);
1709
+ if (!enabled && this.ownerContext.get("hmr") !== void 0) await this.refreshPackages();
1674
1710
  }), {
1675
1711
  stage: "enable",
1676
1712
  target: name,
@@ -1714,6 +1750,7 @@ let PluginManager = (() => {
1714
1750
  const files = await this.readRestoredFiles();
1715
1751
  const before = readProfileManifest("dsh", this.profile.dir).dependencies ?? {};
1716
1752
  let name;
1753
+ let version;
1717
1754
  try {
1718
1755
  result.registries = [];
1719
1756
  const connection = checkGithubConnection(parsedForRegistry(spec), this.profile.dir, {
@@ -1787,6 +1824,7 @@ let PluginManager = (() => {
1787
1824
  const compatibility = evaluatePluginCompatibility(manifest, readProfileVersionExemptions(this.profile.dir));
1788
1825
  if (compatibility !== void 0 && !compatibility.exempted) throw new ManagementFailure("incompatible-version", [incompatiblePlugin(compatibility)]);
1789
1826
  for (const file of bundlePatchPaths(dir, manifest.dsh.bundle)) loadOverlayPatches("dsh", file);
1827
+ version = manifest.version;
1790
1828
  } catch (error) {
1791
1829
  await this.restoreFiles(files);
1792
1830
  throw error;
@@ -1794,11 +1832,13 @@ let PluginManager = (() => {
1794
1832
  control.phase = "applying";
1795
1833
  announce("applying");
1796
1834
  result.bundle = name;
1835
+ if (version !== void 0) result.version = version;
1797
1836
  result.target = name;
1798
1837
  result.stage = "enable";
1799
1838
  return this.configure(async () => {
1800
1839
  if (options?.enabled !== false) await this.selectBundle(name, true);
1801
1840
  if (Object.hasOwn(before, name)) return "restart-required";
1841
+ await this.refreshPackages();
1802
1842
  if (options?.enabled !== false) result.warnings = await this.reload();
1803
1843
  });
1804
1844
  }, {
@@ -1837,13 +1877,14 @@ let PluginManager = (() => {
1837
1877
  await control.result.then(() => void 0, () => void 0);
1838
1878
  return { status: "cancelled" };
1839
1879
  }
1840
- /** Unload and remove a profile-owned bundle dependency through dsh plugin's pnpm path.
1841
- * @param name Installed dependency name.
1880
+ /** Unload and remove a profile-owned bundle dependency through dsh plugin's pnpm path; a selected name no
1881
+ * dependency holds is only deselected.
1882
+ * @param name Installed dependency or selected bundle name.
1842
1883
  * @returns Removal diagnostics and the remaining profile state.
1843
1884
  */
1844
1885
  removeBundle(name) {
1845
1886
  return this.change(async (result) => {
1846
- await this.configure(async () => {
1887
+ if (!await this.configure(async () => {
1847
1888
  const bundle = (await this.listBundles()).find((item) => item.name === name);
1848
1889
  if (bundle === void 0 || !bundle.removable) throw new ManagementFailure("not-removable");
1849
1890
  if (this.ownerContext.get("hmr") === void 0 && (this.profile.startedBundles.includes(name) || bundle.error === void 0 && this.bundleRows(name).some((row) => [...this.ctx.loader.entries()].some((entry) => entry.options.id === row.id && entry.fiber !== void 0)))) throw new ManagementFailure("stop-profile");
@@ -1853,9 +1894,11 @@ let PluginManager = (() => {
1853
1894
  result.warnings = await this.reload();
1854
1895
  }
1855
1896
  if ([...this.ctx.loader.entries()].some((entry) => entry.fiber?.uid != null && contributions.some((row) => row.id === entry.options.id && row.name === entry.options.name))) throw new ManagementFailure("bundle-in-use");
1856
- });
1897
+ return bundle.installed;
1898
+ })) return;
1857
1899
  result.packageResult = await this.runPnpm(["remove", name]);
1858
1900
  if (result.packageResult.exitCode !== 0 || result.packageResult.timedOut === true) throw new Error(result.packageResult.output);
1901
+ await this.configure(() => this.refreshPackages());
1859
1902
  }, {
1860
1903
  stage: "remove",
1861
1904
  target: name
@@ -2028,6 +2071,13 @@ let PluginManager = (() => {
2028
2071
  };
2029
2072
  return hmr === void 0 ? apply() : hmr.runExclusive(apply);
2030
2073
  }
2074
+ async refreshPackages() {
2075
+ if (this.ownerContext.get("hmr") === void 0) {
2076
+ const selected = readProfileManifest("dsh", this.profile.dir).dsh?.profile?.bundles ?? [];
2077
+ if (this.profile.startedBundles.some((name) => !selected.includes(name))) return;
2078
+ }
2079
+ await this.ownerContext.get("pluginPackages")?.refresh();
2080
+ }
2031
2081
  async reload(requiredIds = []) {
2032
2082
  if (this.ownerContext.get("hmr") === void 0) return [];
2033
2083
  return reconcileProfilePatches(this.ownerContext.root, readProfilePatches("dsh", this.profile), "dsh", requiredIds);
@@ -71,6 +71,7 @@ const _deepseek_ai_dsh_plugin_manager_pluginManager_installBundle_result$schema
71
71
  })).optional(),
72
72
  }).optional(),
73
73
  'bundle': z.string().optional(),
74
+ 'version': z.string().optional(),
74
75
  'pendingBuilds': z.array(z.string()).optional(),
75
76
  'approvedBuilds': z.array(z.string()).optional(),
76
77
  'registries': z.array(z.union([z.literal(null), z.string()])).optional(),
@@ -93,6 +94,7 @@ const _deepseek_ai_dsh_plugin_manager_pluginManager_listBundles_result$schema =
93
94
  'description': z.string().optional(),
94
95
  'enabled': z.boolean(),
95
96
  'installed': z.boolean(),
97
+ 'source': z.string().optional(),
96
98
  'optional': z.boolean(),
97
99
  'removable': z.boolean(),
98
100
  'readOnlyReason': z.union([z.literal("management-required"), z.literal("unaddressable")]).optional(),
@@ -207,6 +209,7 @@ const _deepseek_ai_dsh_plugin_manager_pluginManager_removeBundle_result$schema =
207
209
  })).optional(),
208
210
  }).optional(),
209
211
  'bundle': z.string().optional(),
212
+ 'version': z.string().optional(),
210
213
  'pendingBuilds': z.array(z.string()).optional(),
211
214
  'approvedBuilds': z.array(z.string()).optional(),
212
215
  'registries': z.array(z.union([z.literal(null), z.string()])).optional(),
@@ -249,6 +252,7 @@ const _deepseek_ai_dsh_plugin_manager_pluginManager_setBundleEnabled_result$sche
249
252
  })).optional(),
250
253
  }).optional(),
251
254
  'bundle': z.string().optional(),
255
+ 'version': z.string().optional(),
252
256
  'pendingBuilds': z.array(z.string()).optional(),
253
257
  'approvedBuilds': z.array(z.string()).optional(),
254
258
  'registries': z.array(z.union([z.literal(null), z.string()])).optional(),
@@ -291,6 +295,7 @@ const _deepseek_ai_dsh_plugin_manager_pluginManager_setPluginEnabled_result$sche
291
295
  })).optional(),
292
296
  }).optional(),
293
297
  'bundle': z.string().optional(),
298
+ 'version': z.string().optional(),
294
299
  'pendingBuilds': z.array(z.string()).optional(),
295
300
  'approvedBuilds': z.array(z.string()).optional(),
296
301
  'registries': z.array(z.union([z.literal(null), z.string()])).optional(),
@@ -337,6 +342,7 @@ const _deepseek_ai_dsh_plugin_manager_pluginManager_setVersionExemption_result$s
337
342
  })).optional(),
338
343
  }).optional(),
339
344
  'bundle': z.string().optional(),
345
+ 'version': z.string().optional(),
340
346
  'pendingBuilds': z.array(z.string()).optional(),
341
347
  'approvedBuilds': z.array(z.string()).optional(),
342
348
  'registries': z.array(z.union([z.literal(null), z.string()])).optional(),
@@ -377,6 +383,7 @@ const _deepseek_ai_dsh_plugin_manager_pluginManager_waitForInstall_result$schema
377
383
  })).optional(),
378
384
  }).optional(),
379
385
  'bundle': z.string().optional(),
386
+ 'version': z.string().optional(),
380
387
  'pendingBuilds': z.array(z.string()).optional(),
381
388
  'approvedBuilds': z.array(z.string()).optional(),
382
389
  'registries': z.array(z.union([z.literal(null), z.string()])).optional(),
@@ -412,7 +419,7 @@ export const TYPERT = {
412
419
  typeSymbol: '@deepseek-ai/dsh-plugin-manager/types#PluginInstallCancellation',
413
420
  create: _deepseek_ai_dsh_plugin_manager_pluginManager_cancelInstall_result$schema,
414
421
  },
415
- sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":585,"column":9},
422
+ sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":600,"column":9},
416
423
  },
417
424
  {
418
425
  id: '@deepseek-ai/dsh-plugin-manager#pluginManager/inspect',
@@ -449,7 +456,7 @@ export const TYPERT = {
449
456
  typeSymbol: '@deepseek-ai/dsh-plugin-manager/types#PluginSpecInspection',
450
457
  create: _deepseek_ai_dsh_plugin_manager_pluginManager_inspect_result$schema,
451
458
  },
452
- sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":341,"column":9},
459
+ sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":350,"column":9},
453
460
  },
454
461
  {
455
462
  id: '@deepseek-ai/dsh-plugin-manager#pluginManager/installBundle',
@@ -485,7 +492,7 @@ export const TYPERT = {
485
492
  typeSymbol: '@deepseek-ai/dsh-plugin-manager/types#ChangeResult',
486
493
  create: _deepseek_ai_dsh_plugin_manager_pluginManager_installBundle_result$schema,
487
494
  },
488
- sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":462,"column":3},
495
+ sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":473,"column":3},
489
496
  },
490
497
  {
491
498
  id: '@deepseek-ai/dsh-plugin-manager#pluginManager/listBundles',
@@ -545,7 +552,7 @@ export const TYPERT = {
545
552
  typeSymbol: '@deepseek-ai/dsh-plugin-manager/types#PluginRegistries',
546
553
  create: _deepseek_ai_dsh_plugin_manager_pluginManager_registries_result$schema,
547
554
  },
548
- sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":325,"column":9},
555
+ sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":334,"column":9},
549
556
  },
550
557
  {
551
558
  id: '@deepseek-ai/dsh-plugin-manager#pluginManager/removeBundle',
@@ -570,7 +577,7 @@ export const TYPERT = {
570
577
  typeSymbol: '@deepseek-ai/dsh-plugin-manager/types#ChangeResult',
571
578
  create: _deepseek_ai_dsh_plugin_manager_pluginManager_removeBundle_result$schema,
572
579
  },
573
- sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":601,"column":3},
580
+ sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":617,"column":3},
574
581
  },
575
582
  {
576
583
  id: '@deepseek-ai/dsh-plugin-manager#pluginManager/setBundleEnabled',
@@ -605,7 +612,7 @@ export const TYPERT = {
605
612
  typeSymbol: '@deepseek-ai/dsh-plugin-manager/types#ChangeResult',
606
613
  create: _deepseek_ai_dsh_plugin_manager_pluginManager_setBundleEnabled_result$schema,
607
614
  },
608
- sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":443,"column":3},
615
+ sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":452,"column":3},
609
616
  },
610
617
  {
611
618
  id: '@deepseek-ai/dsh-plugin-manager#pluginManager/setPluginEnabled',
@@ -640,7 +647,7 @@ export const TYPERT = {
640
647
  typeSymbol: '@deepseek-ai/dsh-plugin-manager/types#ChangeResult',
641
648
  create: _deepseek_ai_dsh_plugin_manager_pluginManager_setPluginEnabled_result$schema,
642
649
  },
643
- sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":425,"column":3},
650
+ sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":434,"column":3},
644
651
  },
645
652
  {
646
653
  id: '@deepseek-ai/dsh-plugin-manager#pluginManager/setVersionExemption',
@@ -721,7 +728,7 @@ export const TYPERT = {
721
728
  typeSymbol: '@deepseek-ai/dsh-plugin-manager#pluginManager/waitForInstall:result',
722
729
  create: _deepseek_ai_dsh_plugin_manager_pluginManager_waitForInstall_result$schema,
723
730
  },
724
- sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":575,"column":9},
731
+ sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":590,"column":9},
725
732
  },
726
733
  ],
727
734
  model: {
@@ -760,7 +767,7 @@ export const TYPERT = {
760
767
  "name": "listBundles",
761
768
  "signature": "@Remote listBundles(): Promise<BundleInfo[]>",
762
769
  "summary": "Read the profile's installed bundles, the bundles this dsh installation supplies, and the selected names that are not bundles.",
763
- "jsDoc": "/** Read the profile's installed bundles, the bundles this dsh installation supplies, and the selected names that are not bundles.\n * A dependency without a bundle patch is listed, as a `not-bundle` problem, only while it is selected.\n * @returns Package versions, manifest descriptions, rows, optional display metadata, activation selections,\n * whether the installation offers the bundle, and removal availability.\n */"
770
+ "jsDoc": "/** Read the profile's installed bundles, the bundles this dsh installation supplies, and the selected names that are not bundles.\n * A dependency without a bundle patch is listed, as a `not-bundle` problem, only while it is selected.\n * @returns Package versions, manifest descriptions, the installable spec of profile dependencies, rows, optional\n * display metadata, activation selections, whether the installation offers the bundle, and removal availability.\n */"
764
771
  },
765
772
  {
766
773
  "kind": "method",
@@ -815,8 +822,8 @@ export const TYPERT = {
815
822
  "kind": "method",
816
823
  "name": "removeBundle",
817
824
  "signature": "@Remote removeBundle(name: string): Promise<ChangeResult>",
818
- "summary": "Unload and remove a profile-owned bundle dependency through dsh plugin's pnpm path.",
819
- "jsDoc": "/** Unload and remove a profile-owned bundle dependency through dsh plugin's pnpm path.\n * @param name Installed dependency name.\n * @returns Removal diagnostics and the remaining profile state.\n */"
825
+ "summary": "Unload and remove a profile-owned bundle dependency through dsh plugin's pnpm path; a selected name no dependency holds is only deselected.",
826
+ "jsDoc": "/** Unload and remove a profile-owned bundle dependency through dsh plugin's pnpm path; a selected name no\n * dependency holds is only deselected.\n * @param name Installed dependency or selected bundle name.\n * @returns Removal diagnostics and the remaining profile state.\n */"
820
827
  }
821
828
  ],
822
829
  "types": [
@@ -826,7 +833,7 @@ export const TYPERT = {
826
833
  },
827
834
  {
828
835
  "name": "BundleInfo",
829
- "declaration": "export interface BundleInfo {\n name: string;\n version?: string;\n meta?: PluginLocalizedMeta;\n description?: string;\n enabled: boolean;\n installed: boolean;\n optional: boolean;\n removable: boolean;\n readOnlyReason?: ReadOnlyReason;\n error?: ManagementError;\n rows: BundleRowInfo[];\n overrides: string[];\n}"
836
+ "declaration": "export interface BundleInfo {\n name: string;\n version?: string;\n meta?: PluginLocalizedMeta;\n description?: string;\n enabled: boolean;\n installed: boolean;\n source?: string;\n optional: boolean;\n removable: boolean;\n readOnlyReason?: ReadOnlyReason;\n error?: ManagementError;\n rows: BundleRowInfo[];\n overrides: string[];\n}"
830
837
  },
831
838
  {
832
839
  "name": "BundleRowInfo",
@@ -834,7 +841,7 @@ export const TYPERT = {
834
841
  },
835
842
  {
836
843
  "name": "ChangeResult",
837
- "declaration": "export interface ChangeResult {\n changed: boolean;\n application: 'applied' | 'restart-required' | 'overridden' | 'failed' | 'cancelled';\n stage: 'install' | 'enable' | 'remove';\n target: string;\n enabled?: boolean;\n error?: ManagementError;\n warnings?: string[];\n packageResult?: PackageResult;\n bundle?: string;\n pendingBuilds?: string[];\n approvedBuilds?: string[];\n registries?: Registry[];\n failedAt?: 'registry' | 'spec-host';\n}"
844
+ "declaration": "export interface ChangeResult {\n changed: boolean;\n application: 'applied' | 'restart-required' | 'overridden' | 'failed' | 'cancelled';\n stage: 'install' | 'enable' | 'remove';\n target: string;\n enabled?: boolean;\n error?: ManagementError;\n warnings?: string[];\n packageResult?: PackageResult;\n bundle?: string;\n version?: string;\n pendingBuilds?: string[];\n approvedBuilds?: string[];\n registries?: Registry[];\n failedAt?: 'registry' | 'spec-host';\n}"
838
845
  },
839
846
  {
840
847
  "name": "IncompatiblePlugin",
@@ -71,6 +71,7 @@ const _deepseek_ai_dsh_plugin_manager_pluginManager_installBundle_result$schema
71
71
  })).optional(),
72
72
  }).optional(),
73
73
  'bundle': z.string().optional(),
74
+ 'version': z.string().optional(),
74
75
  'pendingBuilds': z.array(z.string()).optional(),
75
76
  'approvedBuilds': z.array(z.string()).optional(),
76
77
  'registries': z.array(z.union([z.literal(null), z.string()])).optional(),
@@ -93,6 +94,7 @@ const _deepseek_ai_dsh_plugin_manager_pluginManager_listBundles_result$schema =
93
94
  'description': z.string().optional(),
94
95
  'enabled': z.boolean(),
95
96
  'installed': z.boolean(),
97
+ 'source': z.string().optional(),
96
98
  'optional': z.boolean(),
97
99
  'removable': z.boolean(),
98
100
  'readOnlyReason': z.union([z.literal("management-required"), z.literal("unaddressable")]).optional(),
@@ -207,6 +209,7 @@ const _deepseek_ai_dsh_plugin_manager_pluginManager_removeBundle_result$schema =
207
209
  })).optional(),
208
210
  }).optional(),
209
211
  'bundle': z.string().optional(),
212
+ 'version': z.string().optional(),
210
213
  'pendingBuilds': z.array(z.string()).optional(),
211
214
  'approvedBuilds': z.array(z.string()).optional(),
212
215
  'registries': z.array(z.union([z.literal(null), z.string()])).optional(),
@@ -249,6 +252,7 @@ const _deepseek_ai_dsh_plugin_manager_pluginManager_setBundleEnabled_result$sche
249
252
  })).optional(),
250
253
  }).optional(),
251
254
  'bundle': z.string().optional(),
255
+ 'version': z.string().optional(),
252
256
  'pendingBuilds': z.array(z.string()).optional(),
253
257
  'approvedBuilds': z.array(z.string()).optional(),
254
258
  'registries': z.array(z.union([z.literal(null), z.string()])).optional(),
@@ -291,6 +295,7 @@ const _deepseek_ai_dsh_plugin_manager_pluginManager_setPluginEnabled_result$sche
291
295
  })).optional(),
292
296
  }).optional(),
293
297
  'bundle': z.string().optional(),
298
+ 'version': z.string().optional(),
294
299
  'pendingBuilds': z.array(z.string()).optional(),
295
300
  'approvedBuilds': z.array(z.string()).optional(),
296
301
  'registries': z.array(z.union([z.literal(null), z.string()])).optional(),
@@ -337,6 +342,7 @@ const _deepseek_ai_dsh_plugin_manager_pluginManager_setVersionExemption_result$s
337
342
  })).optional(),
338
343
  }).optional(),
339
344
  'bundle': z.string().optional(),
345
+ 'version': z.string().optional(),
340
346
  'pendingBuilds': z.array(z.string()).optional(),
341
347
  'approvedBuilds': z.array(z.string()).optional(),
342
348
  'registries': z.array(z.union([z.literal(null), z.string()])).optional(),
@@ -377,6 +383,7 @@ const _deepseek_ai_dsh_plugin_manager_pluginManager_waitForInstall_result$schema
377
383
  })).optional(),
378
384
  }).optional(),
379
385
  'bundle': z.string().optional(),
386
+ 'version': z.string().optional(),
380
387
  'pendingBuilds': z.array(z.string()).optional(),
381
388
  'approvedBuilds': z.array(z.string()).optional(),
382
389
  'registries': z.array(z.union([z.literal(null), z.string()])).optional(),
@@ -409,7 +416,7 @@ export const TYPERT_REMOTE = {
409
416
  typeSymbol: '@deepseek-ai/dsh-plugin-manager/types#PluginInstallCancellation',
410
417
  create: _deepseek_ai_dsh_plugin_manager_pluginManager_cancelInstall_result$schema,
411
418
  },
412
- sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":585,"column":9},
419
+ sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":600,"column":9},
413
420
  },
414
421
  {
415
422
  id: '@deepseek-ai/dsh-plugin-manager#pluginManager/inspect',
@@ -446,7 +453,7 @@ export const TYPERT_REMOTE = {
446
453
  typeSymbol: '@deepseek-ai/dsh-plugin-manager/types#PluginSpecInspection',
447
454
  create: _deepseek_ai_dsh_plugin_manager_pluginManager_inspect_result$schema,
448
455
  },
449
- sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":341,"column":9},
456
+ sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":350,"column":9},
450
457
  },
451
458
  {
452
459
  id: '@deepseek-ai/dsh-plugin-manager#pluginManager/installBundle',
@@ -482,7 +489,7 @@ export const TYPERT_REMOTE = {
482
489
  typeSymbol: '@deepseek-ai/dsh-plugin-manager/types#ChangeResult',
483
490
  create: _deepseek_ai_dsh_plugin_manager_pluginManager_installBundle_result$schema,
484
491
  },
485
- sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":462,"column":3},
492
+ sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":473,"column":3},
486
493
  },
487
494
  {
488
495
  id: '@deepseek-ai/dsh-plugin-manager#pluginManager/listBundles',
@@ -542,7 +549,7 @@ export const TYPERT_REMOTE = {
542
549
  typeSymbol: '@deepseek-ai/dsh-plugin-manager/types#PluginRegistries',
543
550
  create: _deepseek_ai_dsh_plugin_manager_pluginManager_registries_result$schema,
544
551
  },
545
- sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":325,"column":9},
552
+ sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":334,"column":9},
546
553
  },
547
554
  {
548
555
  id: '@deepseek-ai/dsh-plugin-manager#pluginManager/removeBundle',
@@ -567,7 +574,7 @@ export const TYPERT_REMOTE = {
567
574
  typeSymbol: '@deepseek-ai/dsh-plugin-manager/types#ChangeResult',
568
575
  create: _deepseek_ai_dsh_plugin_manager_pluginManager_removeBundle_result$schema,
569
576
  },
570
- sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":601,"column":3},
577
+ sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":617,"column":3},
571
578
  },
572
579
  {
573
580
  id: '@deepseek-ai/dsh-plugin-manager#pluginManager/setBundleEnabled',
@@ -602,7 +609,7 @@ export const TYPERT_REMOTE = {
602
609
  typeSymbol: '@deepseek-ai/dsh-plugin-manager/types#ChangeResult',
603
610
  create: _deepseek_ai_dsh_plugin_manager_pluginManager_setBundleEnabled_result$schema,
604
611
  },
605
- sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":443,"column":3},
612
+ sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":452,"column":3},
606
613
  },
607
614
  {
608
615
  id: '@deepseek-ai/dsh-plugin-manager#pluginManager/setPluginEnabled',
@@ -637,7 +644,7 @@ export const TYPERT_REMOTE = {
637
644
  typeSymbol: '@deepseek-ai/dsh-plugin-manager/types#ChangeResult',
638
645
  create: _deepseek_ai_dsh_plugin_manager_pluginManager_setPluginEnabled_result$schema,
639
646
  },
640
- sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":425,"column":3},
647
+ sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":434,"column":3},
641
648
  },
642
649
  {
643
650
  id: '@deepseek-ai/dsh-plugin-manager#pluginManager/setVersionExemption',
@@ -718,7 +725,7 @@ export const TYPERT_REMOTE = {
718
725
  typeSymbol: '@deepseek-ai/dsh-plugin-manager#pluginManager/waitForInstall:result',
719
726
  create: _deepseek_ai_dsh_plugin_manager_pluginManager_waitForInstall_result$schema,
720
727
  },
721
- sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":575,"column":9},
728
+ sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":590,"column":9},
722
729
  },
723
730
  ],
724
731
  }
@@ -77,8 +77,8 @@ export declare class PluginManager extends TypertRemoteService {
77
77
  listPlugins(): Promise<PluginInfo[]>;
78
78
  /** Read the profile's installed bundles, the bundles this dsh installation supplies, and the selected names that are not bundles.
79
79
  * A dependency without a bundle patch is listed, as a `not-bundle` problem, only while it is selected.
80
- * @returns Package versions, manifest descriptions, rows, optional display metadata, activation selections,
81
- * whether the installation offers the bundle, and removal availability.
80
+ * @returns Package versions, manifest descriptions, the installable spec of profile dependencies, rows, optional
81
+ * display metadata, activation selections, whether the installation offers the bundle, and removal availability.
82
82
  */
83
83
  listBundles(): Promise<BundleInfo[]>;
84
84
  /** Read the registries this manager asks: the configured first one, its fallbacks in order, and what pnpm's own configuration names.
@@ -128,8 +128,9 @@ export declare class PluginManager extends TypertRemoteService {
128
128
  * applied, `not-running` for any other id.
129
129
  */
130
130
  cancelInstall(requestId: PluginInstallRequestId): Promise<PluginInstallCancellation>;
131
- /** Unload and remove a profile-owned bundle dependency through dsh plugin's pnpm path.
132
- * @param name Installed dependency name.
131
+ /** Unload and remove a profile-owned bundle dependency through dsh plugin's pnpm path; a selected name no
132
+ * dependency holds is only deselected.
133
+ * @param name Installed dependency or selected bundle name.
133
134
  * @returns Removal diagnostics and the remaining profile state.
134
135
  */
135
136
  removeBundle(name: string): Promise<ChangeResult>;
@@ -145,6 +146,7 @@ export declare class PluginManager extends TypertRemoteService {
145
146
  private bundleRows;
146
147
  private protectsManager;
147
148
  private configure;
149
+ private refreshPackages;
148
150
  private reload;
149
151
  private change;
150
152
  private diskState;
@@ -45,7 +45,7 @@ import { pluginEntryId, readPluginInventory } from '@deepseek-ai/dsh-host-plugin
45
45
  import { readPluginMeta, readProfileManifest, resolveBundleDir, loadOverlayPatches, composeEntries, reconcileProfilePatches, readProfilePatches, OPTIONAL_BUNDLES, bundlePatchPaths, evaluatePluginCompatibility, readProfileCompatibility, readProfileVersionExemptions, setProfileVersionExemption, PROFILE_COMPATIBILITY_FILENAME, } from '@deepseek-ai/dsh-app-boot';
46
46
  import { bundleManifest, readProfileRegistry, registryArguments, runProfilePnpm, saveManifest, viewProfilePackage } from "./operations.js";
47
47
  import { classifyInstallFailure } from "./install-failure.js";
48
- import { InvalidInstallSpecError, parseInstallSpec } from "./install-spec.js";
48
+ import { dependencySpec, InvalidInstallSpecError, parseInstallSpec } from "./install-spec.js";
49
49
  import { attributeFailure, normalizeRegistry, NPMMIRROR_REGISTRY, registryPlan } from "./registry.js";
50
50
  import { writePluginEnabled } from "./patch.js";
51
51
  import { incompatiblePlugin, ManagementFailure } from "./failure.js";
@@ -276,28 +276,36 @@ let PluginManager = (() => {
276
276
  }
277
277
  /** Read the profile's installed bundles, the bundles this dsh installation supplies, and the selected names that are not bundles.
278
278
  * A dependency without a bundle patch is listed, as a `not-bundle` problem, only while it is selected.
279
- * @returns Package versions, manifest descriptions, rows, optional display metadata, activation selections,
280
- * whether the installation offers the bundle, and removal availability.
279
+ * @returns Package versions, manifest descriptions, the installable spec of profile dependencies, rows, optional
280
+ * display metadata, activation selections, whether the installation offers the bundle, and removal availability.
281
281
  */
282
282
  listBundles() {
283
283
  const manifest = readProfileManifest('dsh', this.profile.dir);
284
284
  const exemptions = readProfileVersionExemptions(this.profile.dir);
285
285
  const selected = manifest.dsh?.profile?.bundles ?? [];
286
- const dependencies = Object.keys(manifest.dependencies ?? {});
286
+ const recorded = manifest.dependencies ?? {};
287
+ const dependencies = Object.keys(recorded);
287
288
  const installation = JSON.parse(readFileSync(this.profile.installAnchor, 'utf8'));
288
289
  const names = [...new Set([...selected, ...dependencies, ...Object.keys(installation.dependencies ?? {})])];
289
290
  const bundles = [];
290
291
  for (const name of names) {
291
292
  const installed = dependencies.includes(name);
292
293
  const optional = OPTIONAL_BUNDLES.includes(name);
293
- const removable = installed && !Object.hasOwn(installation.dependencies ?? {}, name);
294
294
  const enabled = selected.includes(name);
295
+ const shipped = Object.hasOwn(installation.dependencies ?? {}, name);
296
+ // Bundle resolution reads the installation first, so a profile dependency the installation manifest also names,
297
+ // like one it forbids removing, is not the loaded copy.
298
+ const owned = installed && !shipped;
299
+ // A selection neither the profile nor the installation holds, such as a retired bundle, is removed by deselecting it.
300
+ const removable = owned || (enabled && !installed && !shipped);
301
+ const sourceOf = (packageName) => owned ? { source: dependencySpec(name, recorded[name], this.profile.dir, packageName) } : {};
295
302
  const readOnlyReason = this.protectsManager(name) ? 'management-required' : undefined;
296
303
  try {
297
304
  const info = bundleManifest(name, this.profile.dir, this.profile.installAnchor);
298
305
  if (info === undefined) {
299
306
  if (enabled)
300
- bundles.push({ name, enabled, installed, optional, removable: removable && readOnlyReason === undefined,
307
+ bundles.push({ name, ...sourceOf(), enabled, installed, optional,
308
+ removable: removable && readOnlyReason === undefined,
301
309
  ...(readOnlyReason === undefined ? {} : { readOnlyReason }), error: { code: 'not-bundle' }, rows: [], overrides: [] });
302
310
  continue;
303
311
  }
@@ -309,13 +317,13 @@ let PluginManager = (() => {
309
317
  bundles.push({ name, ...(info.version === undefined ? {} : { version: info.version }),
310
318
  ...(info.description === undefined || info.description === '' ? {} : { description: info.description }),
311
319
  ...meta === undefined ? {} : { meta },
312
- enabled, installed, optional, removable: removable && readOnlyReason === undefined,
320
+ ...sourceOf(info.name), enabled, installed, optional, removable: removable && readOnlyReason === undefined,
313
321
  ...(readOnlyReason === undefined ? {} : { readOnlyReason }),
314
322
  ...this.declaredRows(name, info) });
315
323
  }
316
324
  catch (error) {
317
325
  if (enabled || installed) {
318
- bundles.push({ name, enabled, installed, optional, removable: removable && readOnlyReason === undefined,
326
+ bundles.push({ name, ...sourceOf(), enabled, installed, optional, removable: removable && readOnlyReason === undefined,
319
327
  ...(readOnlyReason === undefined ? {} : { readOnlyReason }), error: managementError(error), rows: [], overrides: [] });
320
328
  }
321
329
  }
@@ -456,7 +464,11 @@ let PluginManager = (() => {
456
464
  setBundleEnabled(name, enabled) {
457
465
  return this.change(result => this.configure(async () => {
458
466
  await this.selectBundle(name, enabled);
467
+ if (enabled)
468
+ await this.refreshPackages();
459
469
  result.warnings = await this.reload(enabled ? this.bundleRows(name).map(row => row.id) : []);
470
+ if (!enabled && this.ownerContext.get('hmr') !== undefined)
471
+ await this.refreshPackages();
460
472
  }), { stage: 'enable', target: name, enabled }, 'bundle');
461
473
  }
462
474
  /**
@@ -492,6 +504,7 @@ let PluginManager = (() => {
492
504
  const files = await this.readRestoredFiles();
493
505
  const before = readProfileManifest('dsh', this.profile.dir).dependencies ?? {};
494
506
  let name;
507
+ let version;
495
508
  try {
496
509
  result.registries = [];
497
510
  const connection = checkGithubConnection(parsedForRegistry(spec), this.profile.dir, {
@@ -584,6 +597,7 @@ let PluginManager = (() => {
584
597
  throw new ManagementFailure('incompatible-version', [incompatiblePlugin(compatibility)]);
585
598
  for (const file of bundlePatchPaths(dir, manifest.dsh.bundle))
586
599
  loadOverlayPatches('dsh', file);
600
+ version = manifest.version;
587
601
  }
588
602
  catch (error) {
589
603
  // pnpm has exited by now, so the files it rewrote go back as they were.
@@ -593,6 +607,8 @@ let PluginManager = (() => {
593
607
  control.phase = 'applying';
594
608
  announce('applying');
595
609
  result.bundle = name;
610
+ if (version !== undefined)
611
+ result.version = version;
596
612
  result.target = name;
597
613
  result.stage = 'enable';
598
614
  return this.configure(async () => {
@@ -600,6 +616,7 @@ let PluginManager = (() => {
600
616
  await this.selectBundle(name, true);
601
617
  if (Object.hasOwn(before, name))
602
618
  return 'restart-required';
619
+ await this.refreshPackages();
603
620
  if (options?.enabled !== false)
604
621
  result.warnings = await this.reload();
605
622
  });
@@ -633,13 +650,14 @@ let PluginManager = (() => {
633
650
  await control.result.then(() => undefined, () => undefined);
634
651
  return { status: 'cancelled' };
635
652
  }
636
- /** Unload and remove a profile-owned bundle dependency through dsh plugin's pnpm path.
637
- * @param name Installed dependency name.
653
+ /** Unload and remove a profile-owned bundle dependency through dsh plugin's pnpm path; a selected name no
654
+ * dependency holds is only deselected.
655
+ * @param name Installed dependency or selected bundle name.
638
656
  * @returns Removal diagnostics and the remaining profile state.
639
657
  */
640
658
  removeBundle(name) {
641
659
  return this.change(async (result) => {
642
- await this.configure(async () => {
660
+ const installed = await this.configure(async () => {
643
661
  const bundle = (await this.listBundles()).find(item => item.name === name);
644
662
  if (bundle === undefined || !bundle.removable)
645
663
  throw new ManagementFailure('not-removable');
@@ -657,11 +675,15 @@ let PluginManager = (() => {
657
675
  && contributions.some(row => row.id === entry.options.id && row.name === entry.options.name))) {
658
676
  throw new ManagementFailure('bundle-in-use');
659
677
  }
678
+ return bundle.installed;
660
679
  });
680
+ if (!installed)
681
+ return;
661
682
  result.packageResult = await this.runPnpm(['remove', name]);
662
683
  if (result.packageResult.exitCode !== 0 || result.packageResult.timedOut === true) {
663
684
  throw new Error(result.packageResult.output);
664
685
  }
686
+ await this.configure(() => this.refreshPackages());
665
687
  }, { stage: 'remove', target: name }, 'remove');
666
688
  }
667
689
  /** The rows a bundle's patch inserts and the existing rows it changes; an unreadable patch throws. */
@@ -806,6 +828,15 @@ let PluginManager = (() => {
806
828
  const apply = () => { this.abort.signal.throwIfAborted(); return operation(); };
807
829
  return hmr === undefined ? apply() : hmr.runExclusive(apply);
808
830
  }
831
+ async refreshPackages() {
832
+ if (this.ownerContext.get('hmr') === undefined) {
833
+ const selected = readProfileManifest('dsh', this.profile.dir).dsh?.profile?.bundles ?? [];
834
+ // Deselected startup bundles still run without HMR and need the existing package table.
835
+ if (this.profile.startedBundles.some(name => !selected.includes(name)))
836
+ return;
837
+ }
838
+ await this.ownerContext.get('pluginPackages')?.refresh();
839
+ }
809
840
  async reload(requiredIds = []) {
810
841
  if (this.ownerContext.get('hmr') === undefined)
811
842
  return [];
@@ -47,4 +47,17 @@ export declare class InvalidInstallSpecError extends Error {
47
47
  * registry would refuse, or a URL that is neither a git host nor a tarball.
48
48
  */
49
49
  export declare function parseInstallSpec(raw: string): ParsedInstallSpec;
50
+ /**
51
+ * The spec `pnpm add` accepts for a dependency the profile manifest records. A git address, URL, or other
52
+ * non-registry protocol is the recorded value without the user information of an http(s) URL, whose query string
53
+ * is kept; a `file:` or `link:` path, which pnpm records relative to the profile or with `~` for the home
54
+ * directory, becomes absolute. A registry range, tag, or alias follows the dependency name after `@`, as does a
55
+ * non-registry value installed under a name other than the package's own.
56
+ * @param name - the dependency name.
57
+ * @param recorded - the value the profile manifest records for it.
58
+ * @param profileDir - the profile directory a relative local path is resolved against.
59
+ * @param packageName - the name the installed package's manifest declares, when it could be read.
60
+ * @returns the installable spec.
61
+ */
62
+ export declare function dependencySpec(name: string, recorded: string, profileDir: string, packageName?: string): string;
50
63
  //# sourceMappingURL=install-spec.d.ts.map
@@ -3,7 +3,8 @@
3
3
  * takes, and for a registry name whether it is one the registry can accept.
4
4
  * @module @deepseek-ai/dsh-plugin-manager/install-spec
5
5
  */
6
- import { isAbsolute } from 'node:path';
6
+ import { homedir } from 'node:os';
7
+ import { isAbsolute, resolve } from 'node:path';
7
8
  /** The forms pnpm resolves through a git host: a host shorthand, a git URL, or a hosted repository URL. */
8
9
  const GIT_SHORTHAND = /^(?:github|gitlab|bitbucket|gist):/i;
9
10
  const GIT_URL = /^git(?:\+[a-z]+)?:\/\/|^git@[^:]+:/i;
@@ -84,4 +85,35 @@ export function parseInstallSpec(raw) {
84
85
  throw invalid(spec, 'a version after @ must not be empty');
85
86
  return range === undefined ? { kind: 'registry', spec, name } : { kind: 'registry', spec, name, range };
86
87
  }
88
+ /** A recorded dependency value pnpm resolves outside a registry: a protocol other than a registry alias, or an scp-like git address. */
89
+ const NON_REGISTRY_VALUE = /^(?!(?:npm|jsr|workspace|catalog):)[a-z][a-z0-9+.-]*:|^[^@/:\s]+@[^:/\s]+:/i;
90
+ /** The user information of an http(s) URL, up to the last `@` before the path, which holds a password or access token when present. */
91
+ const HTTP_USER_INFO = /^((?:git\+)?https?:\/\/)[^/]*@/i;
92
+ /**
93
+ * The spec `pnpm add` accepts for a dependency the profile manifest records. A git address, URL, or other
94
+ * non-registry protocol is the recorded value without the user information of an http(s) URL, whose query string
95
+ * is kept; a `file:` or `link:` path, which pnpm records relative to the profile or with `~` for the home
96
+ * directory, becomes absolute. A registry range, tag, or alias follows the dependency name after `@`, as does a
97
+ * non-registry value installed under a name other than the package's own.
98
+ * @param name - the dependency name.
99
+ * @param recorded - the value the profile manifest records for it.
100
+ * @param profileDir - the profile directory a relative local path is resolved against.
101
+ * @param packageName - the name the installed package's manifest declares, when it could be read.
102
+ * @returns the installable spec.
103
+ */
104
+ export function dependencySpec(name, recorded, profileDir, packageName = name) {
105
+ const local = /^(file|link):(.*)$/s.exec(recorded);
106
+ let spec;
107
+ if (local !== null) {
108
+ const path = local[2].replace(/^~(?=$|[\\/])/, () => homedir());
109
+ spec = `${local[1]}:${resolve(profileDir, path)}`;
110
+ }
111
+ else if (NON_REGISTRY_VALUE.test(recorded)) {
112
+ spec = recorded.replace(HTTP_USER_INFO, '$1');
113
+ }
114
+ else {
115
+ return `${name}@${recorded}`;
116
+ }
117
+ return packageName === name ? spec : `${name}@${spec}`;
118
+ }
87
119
  //# sourceMappingURL=install-spec.js.map
@@ -52,6 +52,11 @@ export interface BundleInfo {
52
52
  enabled: boolean;
53
53
  /** Whether the profile's own dependencies hold the package; false for a bundle the dsh installation supplies. */
54
54
  installed: boolean;
55
+ /**
56
+ * Present for a profile dependency the installation does not also supply: the spec `pnpm add` accepts, with local
57
+ * paths made absolute and the user information of an http(s) URL removed.
58
+ */
59
+ source?: string;
55
60
  /**
56
61
  * Whether the installation ships the bundle for the person to switch on: named by the launcher's `OPTIONAL_BUNDLES`,
57
62
  * held by the installation's dependencies, selected by no shipped template, and never removable.
@@ -104,6 +109,8 @@ export interface ChangeResult {
104
109
  packageResult?: PackageResult;
105
110
  /** The bundle an installation added, once pnpm and the bundle check accepted it. */
106
111
  bundle?: string;
112
+ /** The installed bundle's manifest version, when declared; pnpm's `minimumReleaseAge` can make it older than the newest release. */
113
+ version?: string;
107
114
  /** Exact package names awaiting explicit script approval in the profile's pnpm settings, read after a failed run. */
108
115
  pendingBuilds?: string[];
109
116
  /** Package script permissions saved before this installation attempt. */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@deepseek-ai/dsh-plugin-manager",
3
3
  "description": "Current-profile plugin and bundle management shared by dsh CLI, Web and agent tools",
4
- "version": "0.2.0-rc.1",
4
+ "version": "0.2.1-alpha.1",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -59,23 +59,23 @@
59
59
  "execa": "^10.0.0",
60
60
  "yaml": "^2.9.0",
61
61
  "zod": "^4.4.3",
62
- "@deepseek-ai/dsh-app-boot": "0.2.0-rc.1",
63
- "@deepseek-ai/dsh-atomic-write": "0.2.0-rc.1",
64
- "@deepseek-ai/schemastery": "~3.18.4",
65
- "@deepseek-ai/dsh-util-values": "0.2.0-rc.1"
62
+ "@deepseek-ai/dsh-app-boot": "0.2.1-alpha.1",
63
+ "@deepseek-ai/dsh-atomic-write": "0.2.1-alpha.1",
64
+ "@deepseek-ai/schemastery": "~3.18.5-alpha.1",
65
+ "@deepseek-ai/dsh-util-values": "0.2.1-alpha.1"
66
66
  },
67
67
  "peerDependencies": {
68
- "@deepseek-ai/cordis": "~4.0.4",
69
- "@deepseek-ai/cordis-plugin-loader": "~1.0.5",
70
- "@deepseek-ai/dsh-host-plugin-inventory": "0.2.0-rc.1",
71
- "@deepseek-ai/dsh-subprocess": "0.2.0-rc.1",
72
- "@deepseek-ai/dsh-typert-protocol": "0.2.0-rc.1",
73
- "@deepseek-ai/dsh-tools": "0.2.0-rc.1",
74
- "@deepseek-ai/dsh-hmr": "0.2.0-rc.1",
75
- "@deepseek-ai/dsh-brand": "0.2.0-rc.1",
76
- "@deepseek-ai/dsh-sandbox-policy": "0.2.0-rc.1",
77
- "@deepseek-ai/dsh-sandbox": "0.2.0-rc.1",
78
- "@deepseek-ai/dsh-user-approval": "0.2.0-rc.1"
68
+ "@deepseek-ai/cordis": "~4.0.5-alpha.1",
69
+ "@deepseek-ai/cordis-plugin-loader": "~1.0.6-alpha.1",
70
+ "@deepseek-ai/dsh-host-plugin-inventory": "0.2.1-alpha.1",
71
+ "@deepseek-ai/dsh-subprocess": "0.2.1-alpha.1",
72
+ "@deepseek-ai/dsh-typert-protocol": "0.2.1-alpha.1",
73
+ "@deepseek-ai/dsh-tools": "0.2.1-alpha.1",
74
+ "@deepseek-ai/dsh-hmr": "0.2.1-alpha.1",
75
+ "@deepseek-ai/dsh-brand": "0.2.1-alpha.1",
76
+ "@deepseek-ai/dsh-sandbox-policy": "0.2.1-alpha.1",
77
+ "@deepseek-ai/dsh-sandbox": "0.2.1-alpha.1",
78
+ "@deepseek-ai/dsh-user-approval": "0.2.1-alpha.1"
79
79
  },
80
80
  "peerDependenciesMeta": {
81
81
  "@deepseek-ai/dsh-hmr": {
@@ -86,22 +86,23 @@
86
86
  }
87
87
  },
88
88
  "devDependencies": {
89
- "@deepseek-ai/dsh-package-manifest": "0.2.0-rc.1",
90
- "@deepseek-ai/cordis": "~4.0.4",
91
- "@deepseek-ai/cordis-plugin-loader": "~1.0.5",
92
- "@deepseek-ai/dsh-host-plugin-inventory": "0.2.0-rc.1",
93
- "@deepseek-ai/dsh-subprocess": "0.2.0-rc.1",
94
- "@deepseek-ai/dsh-typert-protocol": "0.2.0-rc.1",
95
- "@deepseek-ai/dsh-tools": "0.2.0-rc.1",
96
- "@deepseek-ai/dsh-system-prompt": "0.2.0-rc.1",
97
- "@deepseek-ai/dsh-hmr": "0.2.0-rc.1",
98
- "@deepseek-ai/cordis-plugin-timer": "~1.1.6",
99
- "@deepseek-ai/dsh-brand": "0.2.0-rc.1",
100
- "@deepseek-ai/dsh-sandbox-policy": "0.2.0-rc.1",
101
- "@deepseek-ai/dsh-agent": "0.2.0-rc.1",
102
- "@deepseek-ai/dsh-session": "0.2.0-rc.1",
103
- "@deepseek-ai/dsh-session-projection": "0.2.0-rc.1",
104
- "@deepseek-ai/dsh-sandbox": "0.2.0-rc.1",
105
- "@deepseek-ai/dsh-user-approval": "0.2.0-rc.1"
89
+ "chokidar": "^4.0.3",
90
+ "@deepseek-ai/dsh-package-manifest": "0.2.1-alpha.1",
91
+ "@deepseek-ai/cordis": "~4.0.5-alpha.1",
92
+ "@deepseek-ai/cordis-plugin-loader": "~1.0.6-alpha.1",
93
+ "@deepseek-ai/dsh-host-plugin-inventory": "0.2.1-alpha.1",
94
+ "@deepseek-ai/dsh-subprocess": "0.2.1-alpha.1",
95
+ "@deepseek-ai/dsh-typert-protocol": "0.2.1-alpha.1",
96
+ "@deepseek-ai/dsh-tools": "0.2.1-alpha.1",
97
+ "@deepseek-ai/dsh-system-prompt": "0.2.1-alpha.1",
98
+ "@deepseek-ai/dsh-hmr": "0.2.1-alpha.1",
99
+ "@deepseek-ai/cordis-plugin-timer": "~1.1.7-alpha.1",
100
+ "@deepseek-ai/dsh-brand": "0.2.1-alpha.1",
101
+ "@deepseek-ai/dsh-agent": "0.2.1-alpha.1",
102
+ "@deepseek-ai/dsh-sandbox": "0.2.1-alpha.1",
103
+ "@deepseek-ai/dsh-user-approval": "0.2.1-alpha.1",
104
+ "@deepseek-ai/dsh-session": "0.2.1-alpha.1",
105
+ "@deepseek-ai/dsh-session-projection": "0.2.1-alpha.1",
106
+ "@deepseek-ai/dsh-sandbox-policy": "0.2.1-alpha.1"
106
107
  }
107
108
  }