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

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: 2cbe6e4a68994e2f
19
- zh: ba0328f5538d8746
18
+ en: 4eef24eeb95ebb30
19
+ zh: 99fb6a7e4e3f759f
20
20
  /deepseek-ai-dsh-plugin-manager/use-this-package/version-compatibility-and-exemptions:
21
21
  en: 920c0acb3b9feaf3
22
22
  zh: ba6e832a256880a7
package/README.md CHANGED
@@ -43,8 +43,14 @@ A selected bundle that cannot load remains in `listBundles` with an `error`; `en
43
43
 
44
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
+ `listBundles` marks shipped optional bundles and on-demand catalog entries as `official`, independently of package installation. `availability` identifies a readable installation-provided or profile package, or `missing`; on-demand catalog entries exclude undeclared transitive copies. `installed` only records whether the profile declares the dependency. An absent on-demand entry carries embedded localized metadata and a host-derived `installTarget` containing the running DSH version and install spec, without registry access. Profile-declared packages with unreadable files retain their declaration and expose the normal error.
47
+
46
48
  `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
49
 
50
+ Published installations offer `package@<running DSH version>`. Source Web and development Desktop offer `link:<absolute workspace package directory>` from the running CLI installation, including its development symlink. Package names resolve through workspace manifests; current working directory and prerelease suffixes do not select a checkout. Missing packages, bundle patches, or built entry points produce a diagnostic without registry fallback. Prepare the checkout with `pnpm install` and `pnpm run build`. Linked packages use that checkout’s dependencies and peers; rebuilding and restarting remain necessary after code changes. A registry copy or another checkout’s link can need replacement even when its version matches.
51
+
52
+ Official on-demand installs and explicit updates call the same `installBundle` operation as other bundles. Registry targets use `saveExact: true`; development targets retain the ordinary `link:` dependency. Updates pass the current `enabled` selection; replacing an existing dependency reports `restart-required`. 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.
53
+
48
54
  `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. 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.
49
55
 
50
56
  Registries are asked in turn. The plan starts at `options.registry`, else the configured `registry` (`null` is the one pnpm's own configuration names), and continues through `fallbackRegistries` while a registry is unreachable, times out, or answers that it has no such package or version, which a mirror not yet synced does. A registry outside the configured set is asked alone, so a private registry never falls through to a public one; pnpm's own registry counts as part of the set only while what it names, read through `pnpm config get registry` before each plan, is npm's own registry or one of the fallbacks, and is otherwise asked alone as a private one. A registry pnpm's own configuration already names is asked once. The lookup runs `pnpm view` with `--registry` and without pnpm's own retries, so a dead registry is reported within `inspectTimeoutMs` and the next one is asked; pnpm prints its refusal as JSON on stdout, which is read like stderr. The install keeps pnpm's retry settings. `registries()` answers the configured set and what pnpm names, for a picker. The [registry Agent Note](../../../.agents/notes/implemented/architecture/2026-09-18-plugin-install-registries.md) owns the rationale.
@@ -55,7 +61,7 @@ The browser-safe `@deepseek-ai/dsh-plugin-manager/registry` entry exports `OFFIC
55
61
 
56
62
  `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
63
 
58
- When pnpm 11 blocks dependency scripts, the failed installation reports every pending package name in the profile under `pendingBuilds`, including names left by earlier attempts; a failed run restores `package.json` and `pnpm-lock.yaml` but deliberately not `pnpm-workspace.yaml`, where pnpm records them. The Web plugin page offers **Allow these scripts and retry**; the tool can grant permission on the user's behalf through `approvedBuilds` on `install_bundle`, after the user approves those scripts in the conversation. The service validates pending names; it does not verify conversation approval. Approval persists by package name in this profile, permits commands with the host user's permissions, and survives another installation failure. Only currently undecided names can be approved; existing denials and wildcard rules cannot be overridden through this action. Approval rejects YAML anchors or aliases inside `allowBuilds`. Retry preserves the original activation choice.
64
+ When pnpm 11 blocks dependency scripts, the failed installation reports every pending package name in the profile under `pendingBuilds`, including names left by earlier attempts and names the run reported as ignored. pnpm writes those entries into `pnpm-workspace.yaml` only when it can prompt, so the manager records the reported names itself; a failed run restores `package.json` and `pnpm-lock.yaml` but deliberately not `pnpm-workspace.yaml`. The Web plugin page offers **Allow these scripts and retry**; the tool can grant permission on the user's behalf through `approvedBuilds` on `install_bundle`, after the user approves those scripts in the conversation. The service validates pending names; it does not verify conversation approval. Approval persists by package name in this profile, permits commands with the host user's permissions, and survives another installation failure. Only currently undecided names can be approved; existing denials and wildcard rules cannot be overridden through this action. Approval rejects YAML anchors or aliases inside `allowBuilds`. Retry preserves the original activation choice.
59
65
 
60
66
  <a id="version-compatibility-and-exemptions"></a>
61
67
  ### Version compatibility and exemptions
package/README.zh.md CHANGED
@@ -43,8 +43,14 @@ kind: "package-reference"
43
43
 
44
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
+ `listBundles` 将随附可选组合包和按需目录项标为 `official`,与包的安装状态独立。`availability` 表示可读取的安装随附包、profile 包,或 `missing`;按需目录项不计入未声明的传递依赖副本。`installed` 仅表示 profile 是否声明该依赖。尚未安装的按需条目携带内嵌的本地化元信息,以及 Host 生成、包含当前 DSH 版本与安装 spec 的 `installTarget`,无需访问注册表。profile 已声明但文件不可读的包保留依赖声明,并展示原有错误。
47
+
46
48
  `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
49
 
50
+ 已发布的安装提供 `package@<当前 DSH 版本>`。源码 Web 与开发 Desktop 提供 `link:<工作区包的绝对目录>`,目录来自当前 CLI 的安装位置,包括其开发符号链接。包名通过工作区清单解析;当前工作目录和预发布版本后缀不用于选择 checkout。包、bundle patch 或已构建入口缺失时报告诊断,不回退到注册表。使用 `pnpm install` 和 `pnpm run build` 准备 checkout。链接包使用该 checkout 的依赖与 peer;代码改变后仍需重新构建并重启。即使版本相同,注册表副本或其他 checkout 的链接也可能需要替换。
51
+
52
+ 官方按需安装与显式更新调用和其他组合包相同的 `installBundle` 操作。注册表目标使用 `saveExact: true`;开发目标保留普通 `link:` 依赖。更新传入当前 `enabled` 选择状态;替换已有依赖会报告 `restart-required`。关闭仅取消选择配置层,移除使用原有移除检查。安装失败恢复 profile 清单和锁文件;安装成功后激活失败可能保留已保存的依赖和选择。原有 peer 兼容性检查继续生效,不增加仅针对官方条目的版本准入规则。
53
+
48
54
  `installBundle` 在启动 pnpm 前通过 `git ls-remote` 检查 GitHub 仓库,使用 profile 目录及安装器的 Git 与代理配置。`githubConnectionTimeoutMs` 默认为 5000 毫秒,只限制这次检查,不限制包下载或构建。检查禁用凭据助手和认证提示;只有网络失败与超时会停止安装,通过现有失败类型与诊断日志返回,并标记 `failedAt: 'spec-host'`。认证、仓库查找及其他失败继续交给 pnpm,包括其 HTTPS 到 SSH 的回退。取消安装或销毁管理器会停止检查及其子进程。注册表包、本地路径、压缩包和其他 Git 主机跳过此检查。仓库可达后,下载或组合包验证仍可能失败。
49
55
 
50
56
  注册表按顺序询问。计划从 `options.registry` 开始,否则从配置的 `registry`(`null` 即 pnpm 自身配置指定的那个)开始,并在一个注册表不可达、超时或答复没有这个包或版本(尚未同步的镜像会如此)时继续问 `fallbackRegistries`。配置集合之外的注册表只问它自己,因而私有源永远不会落到公共源;pnpm 自身的注册表只在它指向的地址(每次做计划前经 `pnpm config get registry` 读出)是 npm 官方源或某个备选源时才算集合成员,否则视为私有源只问它自己。pnpm 自身配置已经指向的注册表只问一次。查询以 `--registry` 运行 `pnpm view` 且不带 pnpm 自身的重试,所以死掉的注册表会在 `inspectTimeoutMs` 内报告并转问下一个;pnpm 把拒绝以 JSON 打印在 stdout,读法与 stderr 相同。安装保留 pnpm 的重试设置。`registries()` 把配置集合和 pnpm 指向的地址答复给选择器。[注册表 Agent Note](../../../.agents/notes/implemented/architecture/2026-09-18-plugin-install-registries.zh.md) 拥有理由。
@@ -55,7 +61,7 @@ kind: "package-reference"
55
61
 
56
62
  `waitForInstall(requestId)` 让客户端在响应丢失后等待活动安装的结果,包括不可取消的应用阶段。它返回与原调用相同的结果,请求不在活动中时返回 `null`。已完成的结果不保留;`null` 不表示成功或已取消。
57
63
 
58
- pnpm 11 拦下依赖脚本时,失败的安装在 `pendingBuilds` 里报告 profile 中所有待决定的包名,包括先前尝试留下的;失败的运行会恢复 `package.json` 与 `pnpm-lock.yaml`,但有意不恢复 pnpm 记录这些名字的 `pnpm-workspace.yaml`。Web 插件页提供**允许这些脚本并重试**;工具可以在用户于对话中批准这些脚本后,通过 `install_bundle` 的 `approvedBuilds` 代为授权。服务只校验待决定的名字,不核实对话中的批准。授权按包名保存在当前 profile,允许以宿主用户的权限执行命令,并在再次安装失败后保留。只能批准当前未决定的名字;已有的拒绝与通配规则不能通过此操作覆盖。`allowBuilds` 里出现 YAML 锚点或别名时拒绝授权。重试保留原来的启用选择。
64
+ pnpm 11 拦下依赖脚本时,失败的安装在 `pendingBuilds` 里报告 profile 中所有待决定的包名,包括先前尝试留下的,以及本次运行报告为已忽略的。pnpm 只在能够交互提示时才把这些条目写入 `pnpm-workspace.yaml`,因此由管理器自行记录本次运行报告的包名;失败的运行会恢复 `package.json` 与 `pnpm-lock.yaml`,但有意不恢复 `pnpm-workspace.yaml`。Web 插件页提供**允许这些脚本并重试**;工具可以在用户于对话中批准这些脚本后,通过 `install_bundle` 的 `approvedBuilds` 代为授权。服务只校验待决定的名字,不核实对话中的批准。授权按包名保存在当前 profile,允许以宿主用户的权限执行命令,并在再次安装失败后保留。只能批准当前未决定的名字;已有的拒绝与通配规则不能通过此操作覆盖。`allowBuilds` 里出现 YAML 锚点或别名时拒绝授权。重试保留原来的启用选择。
59
65
 
60
66
  <a id="version-compatibility-and-exemptions"></a>
61
67
  ### 版本兼容性与豁免
package/lib/index.js CHANGED
@@ -1,13 +1,13 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { existsSync, readFileSync } from "node:fs";
2
+ import { existsSync, globSync, readFileSync, realpathSync, statSync } from "node:fs";
3
3
  import { mkdir, mkdtemp, open, readFile, rm } from "node:fs/promises";
4
- import { isAbsolute, join, resolve } from "node:path";
4
+ import { dirname, isAbsolute, join, resolve } from "node:path";
5
5
  import { pathToFileURL } from "node:url";
6
6
  import { withFileLock, writeFileAtomic } from "@deepseek-ai/dsh-atomic-write";
7
7
  import z from "@deepseek-ai/schemastery";
8
8
  import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
9
9
  import { pluginEntryId, readPluginInventory } from "@deepseek-ai/dsh-host-plugin-inventory";
10
- import { OPTIONAL_BUNDLES, PROFILE_COMPATIBILITY_FILENAME, bundlePatchPaths, composeEntries, evaluatePluginCompatibility, loadOptionalPatches, loadOverlayPatches, pluginCompatibilityWarning, readPluginMeta, readProfileCompatibility, readProfileManifest, readProfilePatches, readProfileVersionExemptions, reconcileProfilePatches, resolveBundleDir, resolveProfileDir, setProfileVersionExemption } from "@deepseek-ai/dsh-app-boot";
10
+ import { OFFICIAL_ON_DEMAND_CATALOG, OPTIONAL_BUNDLES, PROFILE_COMPATIBILITY_FILENAME, bundlePatchPaths, composeEntries, evaluatePluginCompatibility, getDshRuntimeVersion, loadOptionalPatches, loadOverlayPatches, pluginCompatibilityWarning, readPluginMeta, readProfileCompatibility, readProfileManifest, readProfilePatches, readProfileVersionExemptions, reconcileProfilePatches, resolveBundleDir, resolveProfileDir, setProfileVersionExemption } from "@deepseek-ai/dsh-app-boot";
11
11
  import { once } from "node:events";
12
12
  import { execa } from "execa";
13
13
  import { scrubbedParentEnv } from "@deepseek-ai/dsh-subprocess";
@@ -819,6 +819,48 @@ function classifyInstallFailure(facts) {
819
819
  for (const [kind, pattern] of LOG_KINDS) if (pattern.test(facts.log)) return kind;
820
820
  return "unknown";
821
821
  }
822
+ //#endregion
823
+ //#region lib/types/official-install-target.js
824
+ /** Select Official bundle packages from the running installation without importing provider code. */
825
+ function manifestAt(file) {
826
+ return JSON.parse(readFileSync(file, "utf8"));
827
+ }
828
+ /**
829
+ * Offer a source checkout link or an exact published package version. Source installations are
830
+ * identified by the real CLI manifest and its unpacked workspace dependencies, including Desktop's
831
+ * development symlink. An incomplete checkout is an error, never a registry fallback.
832
+ * @param name - Official catalog package name.
833
+ * @param installAnchor - Running application's DSH package.json, possibly reached through a symlink.
834
+ * @param version - Running DSH version used by its release family.
835
+ * @returns An ordinary package-manager spec and its package version.
836
+ * @throws when a source installation lacks its workspace package, bundle patch, or built runtime.
837
+ */
838
+ function officialBundleInstallTarget(name, installAnchor, version) {
839
+ const anchor = realpathSync(installAnchor);
840
+ const installation = manifestAt(anchor);
841
+ if (!(installation.name === "@deepseek-ai/dsh" && Object.values(installation.dependencies ?? {}).some((value) => value.startsWith("workspace:")))) return {
842
+ spec: `${name}@${version}`,
843
+ version
844
+ };
845
+ const root = resolve(dirname(anchor), "../..");
846
+ if (anchor !== join(root, "apps", "cli", "package.json") || !existsSync(join(root, "package.json")) || manifestAt(join(root, "package.json")).name !== "@deepseek-ai/dsh-root") throw new Error(`Official bundle ${name}: source DSH installation ${anchor} requires its complete workspace checkout`);
847
+ const matches = globSync("packages/*/*/package.json", { cwd: root }).map((file) => ({
848
+ file: join(root, file),
849
+ manifest: manifestAt(join(root, file))
850
+ })).filter((entry) => entry.manifest.name === name);
851
+ const selected = matches[0];
852
+ if (matches.length !== 1 || selected === void 0) throw new Error(`Official bundle ${name}: expected one workspace package in ${root}, found ${matches.length}; restore the checkout and run pnpm install`);
853
+ const { file, manifest } = selected;
854
+ const directory = realpathSync(dirname(file));
855
+ if (manifest.version !== version || manifest.private === true || manifest.dsh?.bundle === void 0) throw new Error(`Official bundle ${name}: ${file} must declare a public bundle at DSH version ${version}`);
856
+ const main = typeof manifest.main === "string" ? resolve(directory, manifest.main) : void 0;
857
+ if (main === void 0 || !existsSync(main) || !statSync(main).isFile()) throw new Error(`Official bundle ${name}: built runtime is missing in ${directory}; run pnpm install and pnpm run build in ${root}`);
858
+ for (const patch of bundlePatchPaths(directory, manifest.dsh.bundle)) if (!existsSync(patch) || !statSync(patch).isFile()) throw new Error(`Official bundle ${name}: bundle patch ${patch} is missing; restore the checkout`);
859
+ return {
860
+ spec: `link:${directory}`,
861
+ version
862
+ };
863
+ }
822
864
  /** Public npmmirror URL shared by the fallback configuration and public-registry comparison. */
823
865
  const NPMMIRROR_REGISTRY = "https://registry.npmmirror.com/";
824
866
  /**
@@ -940,6 +982,10 @@ async function writePluginEnabled(filename, id, name, enabled) {
940
982
  //#endregion
941
983
  //#region lib/types/build-approval.js
942
984
  /** Approve pnpm's pending dependency scripts in the current profile's workspace settings. */
985
+ /** Value pnpm leaves for a build script that still needs a decision. */
986
+ const UNDECIDED_BUILD = "set this to true or false";
987
+ /** Where a non-interactive run names the build scripts it skipped. */
988
+ const IGNORED_BUILDS = /\bIgnored build scripts: (.+)/;
943
989
  async function readPolicy(dir) {
944
990
  let text;
945
991
  try {
@@ -956,17 +1002,41 @@ async function readPolicy(dir) {
956
1002
  visit(builds ?? null, (_key, node) => {
957
1003
  if (isAlias(node) || isNode(node) && "anchor" in node && node.anchor) throw new Error("allowBuilds must not contain YAML anchors or aliases");
958
1004
  });
1005
+ const keys = isMap(builds) ? builds.items.flatMap(({ key }) => isScalar(key) && typeof key.value === "string" ? [key.value] : []) : [];
959
1006
  return {
960
1007
  document,
961
- pending: isMap(builds) ? builds.items.flatMap(({ key, value }) => isScalar(key) && typeof key.value === "string" && !/[*?]/.test(key.value) && isScalar(value) && value.value === "set this to true or false" ? [key.value] : []) : []
1008
+ pending: isMap(builds) ? builds.items.flatMap(({ key, value }) => isScalar(key) && typeof key.value === "string" && !/[*?]/.test(key.value) && isScalar(value) && value.value === UNDECIDED_BUILD ? [key.value] : []) : [],
1009
+ keys
962
1010
  };
963
1011
  }
964
- /** Read package names left undecided by pnpm 11, including after installation cleanup.
1012
+ /**
1013
+ * Record the build scripts a failed run reported as ignored, then list every name awaiting a decision.
1014
+ *
1015
+ * pnpm writes those policy entries itself only when it can prompt, and the
1016
+ * manager always runs it non-interactively, so the run's own report is the
1017
+ * record that survives.
965
1018
  * @param dir Current profile directory.
1019
+ * @param output Captured output of the failed run.
966
1020
  * @returns Exact package names awaiting a build decision; wildcard rules are excluded.
967
1021
  */
968
- async function readPendingBuilds(dir) {
969
- return (await readPolicy(dir)).pending;
1022
+ async function recordPendingBuilds(dir, output) {
1023
+ const { document, pending, keys } = await readPolicy(dir);
1024
+ const known = new Set(keys);
1025
+ const added = ignoredBuildNames(output).filter((name) => !known.has(name));
1026
+ if (added.length === 0) return pending;
1027
+ for (const name of added) document.setIn(["allowBuilds", name], UNDECIDED_BUILD);
1028
+ await writeFileAtomic(join(dir, "pnpm-workspace.yaml"), String(document), { mode: 384 });
1029
+ return [...pending, ...added];
1030
+ }
1031
+ /**
1032
+ * Read the package names a pnpm run reported as ignored build scripts.
1033
+ * @param output Captured output of the run.
1034
+ * @returns Exact names in report order; wildcard patterns are excluded.
1035
+ */
1036
+ function ignoredBuildNames(output) {
1037
+ const reported = IGNORED_BUILDS.exec(output)?.[1];
1038
+ if (reported === void 0) return [];
1039
+ return reported.split(",").map((name) => name.trim()).filter((name) => name !== "" && !/[*?]/.test(name));
970
1040
  }
971
1041
  /** Persist approval without running scripts; the caller holds the profile manifest lock.
972
1042
  * @param dir Current profile directory.
@@ -1475,7 +1545,7 @@ let PluginManager = (() => {
1475
1545
  };
1476
1546
  });
1477
1547
  }
1478
- /** Read the profile's installed bundles, the bundles this dsh installation supplies, and the selected names that are not bundles.
1548
+ /** Read installed, installation-provided, and offline Official catalog bundles, plus selected non-bundle names.
1479
1549
  * A dependency without a bundle patch is listed, as a `not-bundle` problem, only while it is selected.
1480
1550
  * @returns Package versions, manifest descriptions, the installable spec of profile dependencies, rows, optional
1481
1551
  * display metadata, activation selections, whether the installation offers the bundle, and removal availability.
@@ -1487,45 +1557,86 @@ let PluginManager = (() => {
1487
1557
  const recorded = manifest.dependencies ?? {};
1488
1558
  const dependencies = Object.keys(recorded);
1489
1559
  const installation = JSON.parse(readFileSync(this.profile.installAnchor, "utf8"));
1560
+ const catalog = new Map(OFFICIAL_ON_DEMAND_CATALOG.map((entry) => [entry.packageName, entry]));
1490
1561
  const names = [...new Set([
1491
1562
  ...selected,
1492
1563
  ...dependencies,
1493
- ...Object.keys(installation.dependencies ?? {})
1564
+ ...Object.keys(installation.dependencies ?? {}),
1565
+ ...catalog.keys()
1494
1566
  ])];
1495
1567
  const bundles = [];
1568
+ const runtimeVersion = getDshRuntimeVersion();
1496
1569
  for (const name of names) {
1497
1570
  const installed = dependencies.includes(name);
1498
1571
  const optional = OPTIONAL_BUNDLES.includes(name);
1499
1572
  const enabled = selected.includes(name);
1500
1573
  const shipped = Object.hasOwn(installation.dependencies ?? {}, name);
1574
+ const offered = catalog.get(name);
1575
+ const official = optional || offered !== void 0;
1576
+ let target = {};
1577
+ let targetError;
1578
+ if (offered !== void 0) try {
1579
+ target = { installTarget: officialBundleInstallTarget(name, this.profile.installAnchor, runtimeVersion) };
1580
+ } catch (error) {
1581
+ targetError = managementError(error);
1582
+ }
1583
+ const catalogMeta = offered === void 0 ? {} : { meta: offered.meta };
1584
+ if (offered !== void 0 && !installed && !shipped) {
1585
+ bundles.push({
1586
+ name,
1587
+ official,
1588
+ availability: "missing",
1589
+ ...catalogMeta,
1590
+ ...target,
1591
+ enabled,
1592
+ installed,
1593
+ optional,
1594
+ removable: enabled,
1595
+ rows: [],
1596
+ overrides: [],
1597
+ ...targetError !== void 0 ? { error: targetError } : enabled ? { error: { code: "not-bundle" } } : {}
1598
+ });
1599
+ continue;
1600
+ }
1501
1601
  const owned = installed && !shipped;
1502
1602
  const removable = owned || enabled && !installed && !shipped;
1503
1603
  const sourceOf = (packageName) => owned ? { source: dependencySpec(name, recorded[name], this.profile.dir, packageName) } : {};
1504
1604
  const readOnlyReason = this.protectsManager(name) ? "management-required" : void 0;
1605
+ let availability = "missing";
1606
+ let version;
1505
1607
  try {
1506
1608
  const info = bundleManifest(name, this.profile.dir, this.profile.installAnchor);
1609
+ availability = shipped ? "installation" : "profile";
1507
1610
  if (info === void 0) {
1508
- if (enabled) bundles.push({
1611
+ if (enabled || offered !== void 0) bundles.push({
1509
1612
  name,
1613
+ official,
1614
+ availability,
1615
+ ...catalogMeta,
1616
+ ...target,
1510
1617
  ...sourceOf(),
1511
1618
  enabled,
1512
1619
  installed,
1513
1620
  optional,
1514
1621
  removable: removable && readOnlyReason === void 0,
1515
1622
  ...readOnlyReason === void 0 ? {} : { readOnlyReason },
1516
- error: { code: "not-bundle" },
1623
+ error: targetError ?? { code: "not-bundle" },
1517
1624
  rows: [],
1518
1625
  overrides: []
1519
1626
  });
1520
1627
  continue;
1521
1628
  }
1629
+ version = info.version;
1522
1630
  const compatibility = evaluatePluginCompatibility(info, exemptions);
1523
1631
  if (compatibility !== void 0 && !compatibility.exempted) throw new ManagementFailure("incompatible-version", [incompatiblePlugin(compatibility)]);
1524
1632
  const dir = resolveBundleDir("dsh", name, this.profile.installAnchor, this.profile.dir);
1525
- const meta = readPluginMeta(info.name ?? name, pathToFileURL(join(dir, "package.json")).href);
1633
+ const meta = offered?.meta ?? readPluginMeta(info.name ?? name, pathToFileURL(join(dir, "package.json")).href);
1526
1634
  bundles.push({
1527
1635
  name,
1528
- ...info.version === void 0 ? {} : { version: info.version },
1636
+ official,
1637
+ availability,
1638
+ ...target,
1639
+ ...version === void 0 ? {} : { version },
1529
1640
  ...info.description === void 0 || info.description === "" ? {} : { description: info.description },
1530
1641
  ...meta === void 0 ? {} : { meta },
1531
1642
  ...sourceOf(info.name),
@@ -1534,18 +1645,24 @@ let PluginManager = (() => {
1534
1645
  optional,
1535
1646
  removable: removable && readOnlyReason === void 0,
1536
1647
  ...readOnlyReason === void 0 ? {} : { readOnlyReason },
1537
- ...this.declaredRows(name, info)
1648
+ ...this.declaredRows(name, info),
1649
+ ...targetError === void 0 ? {} : { error: targetError }
1538
1650
  });
1539
1651
  } catch (error) {
1540
- if (enabled || installed) bundles.push({
1652
+ if (enabled || installed || optional || offered !== void 0) bundles.push({
1541
1653
  name,
1654
+ official,
1655
+ availability,
1656
+ ...catalogMeta,
1657
+ ...target,
1658
+ ...version === void 0 ? {} : { version },
1542
1659
  ...sourceOf(),
1543
1660
  enabled,
1544
1661
  installed,
1545
1662
  optional,
1546
1663
  removable: removable && readOnlyReason === void 0,
1547
1664
  ...readOnlyReason === void 0 ? {} : { readOnlyReason },
1548
- error: managementError(error),
1665
+ error: targetError ?? managementError(error),
1549
1666
  rows: [],
1550
1667
  overrides: []
1551
1668
  });
@@ -1721,7 +1838,7 @@ let PluginManager = (() => {
1721
1838
  * `package.json` and `pnpm-lock.yaml` as they were; downloaded files can stay.
1722
1839
  * @param spec One package spec, including local paths relative to the invocation directory.
1723
1840
  * @param options Whether to activate the installed bundle (defaults to true), the request id a cancellation names,
1724
- * the pending build scripts to allow for this profile before pnpm runs, and the registry asked first.
1841
+ * the pending build scripts to allow, whether to save an exact dependency, and the registry asked first.
1725
1842
  * @returns Package-manager diagnostics, the registries asked, and the observed activation outcome.
1726
1843
  */
1727
1844
  installBundle(spec, options) {
@@ -1786,6 +1903,7 @@ let PluginManager = (() => {
1786
1903
  run = await this.runPnpm([
1787
1904
  "add",
1788
1905
  spec,
1906
+ ...options?.saveExact === true ? ["--save-exact"] : [],
1789
1907
  ...registryArguments(registry)
1790
1908
  ], control.abort.signal, requestId);
1791
1909
  result.packageResult = run;
@@ -1806,7 +1924,7 @@ let PluginManager = (() => {
1806
1924
  if (succeeded) delete result.failedAt;
1807
1925
  if (!succeeded) {
1808
1926
  try {
1809
- result.pendingBuilds = await readPendingBuilds(this.profile.dir);
1927
+ result.pendingBuilds = await recordPendingBuilds(this.profile.dir, run.output);
1810
1928
  } catch (error) {
1811
1929
  this.ownerContext.logger.warn("Could not read pending build approvals after pnpm failed", error);
1812
1930
  }
@@ -1814,7 +1932,11 @@ let PluginManager = (() => {
1814
1932
  }
1815
1933
  const after = readProfileManifest("dsh", this.profile.dir).dependencies ?? {};
1816
1934
  const installed = Object.keys(after).filter((name) => before[name] !== after[name]);
1817
- if (installed.length === 0) installed.push(...Object.keys(after).filter((name) => spec === name || spec.startsWith(`${name}@`)));
1935
+ if (installed.length === 0) {
1936
+ const parsed = parsedForRegistry(spec);
1937
+ const localName = parsed.kind === "path" ? readProfileManifest("dsh", parsed.path).name : void 0;
1938
+ installed.push(...Object.keys(after).filter((name) => name === localName || spec === name || spec.startsWith(`${name}@`)));
1939
+ }
1818
1940
  const target = installed[0];
1819
1941
  if (installed.length !== 1 || target === void 0) throw new ManagementFailure("ambiguous-install");
1820
1942
  name = target;
@@ -1924,7 +2046,7 @@ let PluginManager = (() => {
1924
2046
  const rows = [];
1925
2047
  const packages = this.ctx.get("pluginPackages");
1926
2048
  for (const row of flatten(composeEntries([patches.filter((item) => item.insert !== void 0)]))) {
1927
- if (typeof row.id !== "string" || typeof row.name !== "string") continue;
2049
+ if (typeof row.id !== "string" || typeof row.name !== "string" || row.group === true) continue;
1928
2050
  const active = live.get(row.id);
1929
2051
  const entryId = active?.entryId;
1930
2052
  const base = active?.baseUrl ?? pathToFileURL(join(dir, "package.json")).href;
@@ -34,6 +34,7 @@ const _deepseek_ai_dsh_plugin_manager_pluginManager_installBundle_parameter_0$sc
34
34
  let _deepseek_ai_dsh_plugin_manager_pluginManager_installBundle_parameter_1$schema$value
35
35
  const _deepseek_ai_dsh_plugin_manager_pluginManager_installBundle_parameter_1$schema = () => (_deepseek_ai_dsh_plugin_manager_pluginManager_installBundle_parameter_1$schema$value ??= z.union([z.undefined(), z.object({
36
36
  'enabled': z.boolean().optional(),
37
+ 'saveExact': z.boolean().optional(),
37
38
  'requestId': z.intersection(z.string(), z.unknown()).optional(),
38
39
  'approvedBuilds': z.array(z.string()).optional(),
39
40
  'registry': z.union([z.literal(null), z.string()]).optional(),
@@ -80,6 +81,12 @@ const _deepseek_ai_dsh_plugin_manager_pluginManager_installBundle_result$schema
80
81
  let _deepseek_ai_dsh_plugin_manager_pluginManager_listBundles_result$schema$value
81
82
  const _deepseek_ai_dsh_plugin_manager_pluginManager_listBundles_result$schema = () => (_deepseek_ai_dsh_plugin_manager_pluginManager_listBundles_result$schema$value ??= z.array(z.object({
82
83
  'name': z.string(),
84
+ 'official': z.boolean(),
85
+ 'availability': z.union([z.literal("profile"), z.literal("installation"), z.literal("missing")]),
86
+ 'installTarget': z.object({
87
+ 'spec': z.string(),
88
+ 'version': z.string(),
89
+ }).optional(),
83
90
  'version': z.string().optional(),
84
91
  'meta': z.object({
85
92
  'title': z.union([z.string(), z.intersection(z.object({
@@ -419,7 +426,7 @@ export const TYPERT = {
419
426
  typeSymbol: '@deepseek-ai/dsh-plugin-manager/types#PluginInstallCancellation',
420
427
  create: _deepseek_ai_dsh_plugin_manager_pluginManager_cancelInstall_result$schema,
421
428
  },
422
- sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":600,"column":9},
429
+ sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":628,"column":9},
423
430
  },
424
431
  {
425
432
  id: '@deepseek-ai/dsh-plugin-manager#pluginManager/inspect',
@@ -456,7 +463,7 @@ export const TYPERT = {
456
463
  typeSymbol: '@deepseek-ai/dsh-plugin-manager/types#PluginSpecInspection',
457
464
  create: _deepseek_ai_dsh_plugin_manager_pluginManager_inspect_result$schema,
458
465
  },
459
- sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":350,"column":9},
466
+ sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":374,"column":9},
460
467
  },
461
468
  {
462
469
  id: '@deepseek-ai/dsh-plugin-manager#pluginManager/installBundle',
@@ -492,7 +499,7 @@ export const TYPERT = {
492
499
  typeSymbol: '@deepseek-ai/dsh-plugin-manager/types#ChangeResult',
493
500
  create: _deepseek_ai_dsh_plugin_manager_pluginManager_installBundle_result$schema,
494
501
  },
495
- sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":473,"column":3},
502
+ sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":497,"column":3},
496
503
  },
497
504
  {
498
505
  id: '@deepseek-ai/dsh-plugin-manager#pluginManager/listBundles',
@@ -507,7 +514,7 @@ export const TYPERT = {
507
514
  typeSymbol: '@deepseek-ai/dsh-plugin-manager#pluginManager/listBundles:result',
508
515
  create: _deepseek_ai_dsh_plugin_manager_pluginManager_listBundles_result$schema,
509
516
  },
510
- sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":280,"column":3},
517
+ sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":282,"column":3},
511
518
  },
512
519
  {
513
520
  id: '@deepseek-ai/dsh-plugin-manager#pluginManager/listPlugins',
@@ -522,7 +529,7 @@ export const TYPERT = {
522
529
  typeSymbol: '@deepseek-ai/dsh-plugin-manager#pluginManager/listPlugins:result',
523
530
  create: _deepseek_ai_dsh_plugin_manager_pluginManager_listPlugins_result$schema,
524
531
  },
525
- sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":256,"column":9},
532
+ sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":258,"column":9},
526
533
  },
527
534
  {
528
535
  id: '@deepseek-ai/dsh-plugin-manager#pluginManager/listVersionExemptions',
@@ -537,7 +544,7 @@ export const TYPERT = {
537
544
  typeSymbol: '@deepseek-ai/dsh-plugin-manager#pluginManager/listVersionExemptions:result',
538
545
  create: _deepseek_ai_dsh_plugin_manager_pluginManager_listVersionExemptions_result$schema,
539
546
  },
540
- sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":232,"column":3},
547
+ sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":234,"column":3},
541
548
  },
542
549
  {
543
550
  id: '@deepseek-ai/dsh-plugin-manager#pluginManager/registries',
@@ -552,7 +559,7 @@ export const TYPERT = {
552
559
  typeSymbol: '@deepseek-ai/dsh-plugin-manager/types#PluginRegistries',
553
560
  create: _deepseek_ai_dsh_plugin_manager_pluginManager_registries_result$schema,
554
561
  },
555
- sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":334,"column":9},
562
+ sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":358,"column":9},
556
563
  },
557
564
  {
558
565
  id: '@deepseek-ai/dsh-plugin-manager#pluginManager/removeBundle',
@@ -577,7 +584,7 @@ export const TYPERT = {
577
584
  typeSymbol: '@deepseek-ai/dsh-plugin-manager/types#ChangeResult',
578
585
  create: _deepseek_ai_dsh_plugin_manager_pluginManager_removeBundle_result$schema,
579
586
  },
580
- sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":617,"column":3},
587
+ sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":645,"column":3},
581
588
  },
582
589
  {
583
590
  id: '@deepseek-ai/dsh-plugin-manager#pluginManager/setBundleEnabled',
@@ -612,7 +619,7 @@ export const TYPERT = {
612
619
  typeSymbol: '@deepseek-ai/dsh-plugin-manager/types#ChangeResult',
613
620
  create: _deepseek_ai_dsh_plugin_manager_pluginManager_setBundleEnabled_result$schema,
614
621
  },
615
- sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":452,"column":3},
622
+ sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":476,"column":3},
616
623
  },
617
624
  {
618
625
  id: '@deepseek-ai/dsh-plugin-manager#pluginManager/setPluginEnabled',
@@ -647,7 +654,7 @@ export const TYPERT = {
647
654
  typeSymbol: '@deepseek-ai/dsh-plugin-manager/types#ChangeResult',
648
655
  create: _deepseek_ai_dsh_plugin_manager_pluginManager_setPluginEnabled_result$schema,
649
656
  },
650
- sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":434,"column":3},
657
+ sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":458,"column":3},
651
658
  },
652
659
  {
653
660
  id: '@deepseek-ai/dsh-plugin-manager#pluginManager/setVersionExemption',
@@ -703,7 +710,7 @@ export const TYPERT = {
703
710
  typeSymbol: '@deepseek-ai/dsh-plugin-manager/types#ChangeResult',
704
711
  create: _deepseek_ai_dsh_plugin_manager_pluginManager_setVersionExemption_result$schema,
705
712
  },
706
- sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":245,"column":3},
713
+ sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":247,"column":3},
707
714
  },
708
715
  {
709
716
  id: '@deepseek-ai/dsh-plugin-manager#pluginManager/waitForInstall',
@@ -728,7 +735,7 @@ export const TYPERT = {
728
735
  typeSymbol: '@deepseek-ai/dsh-plugin-manager#pluginManager/waitForInstall:result',
729
736
  create: _deepseek_ai_dsh_plugin_manager_pluginManager_waitForInstall_result$schema,
730
737
  },
731
- sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":590,"column":9},
738
+ sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":618,"column":9},
732
739
  },
733
740
  ],
734
741
  model: {
@@ -766,8 +773,8 @@ export const TYPERT = {
766
773
  "kind": "method",
767
774
  "name": "listBundles",
768
775
  "signature": "@Remote listBundles(): Promise<BundleInfo[]>",
769
- "summary": "Read the profile's installed bundles, the bundles this dsh installation supplies, and the selected names that are not bundles.",
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 */"
776
+ "summary": "Read installed, installation-provided, and offline Official catalog bundles, plus selected non-bundle names.",
777
+ "jsDoc": "/** Read installed, installation-provided, and offline Official catalog bundles, plus selected non-bundle names.\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 */"
771
778
  },
772
779
  {
773
780
  "kind": "method",
@@ -802,7 +809,7 @@ export const TYPERT = {
802
809
  "name": "installBundle",
803
810
  "signature": "@Remote installBundle(spec: string, options?: InstallBundleOptions): Promise<ChangeResult>",
804
811
  "summary": "Install a package using the same pnpm implementation as dsh plugin.",
805
- "jsDoc": "/**\n * Install a package using the same pnpm implementation as dsh plugin. GitHub\n * repositories get a connection check bounded by githubConnectionTimeoutMs before pnpm starts;\n * only network failures or timeouts stop installation, while pnpm owns authentication and transport fallback. A run\n * that fails, is cancelled, or adds a package without a bundle patch restores\n * `package.json` and `pnpm-lock.yaml` as they were; downloaded files can stay.\n * @param spec One package spec, including local paths relative to the invocation directory.\n * @param options Whether to activate the installed bundle (defaults to true), the request id a cancellation names,\n * the pending build scripts to allow for this profile before pnpm runs, and the registry asked first.\n * @returns Package-manager diagnostics, the registries asked, and the observed activation outcome.\n */"
812
+ "jsDoc": "/**\n * Install a package using the same pnpm implementation as dsh plugin. GitHub\n * repositories get a connection check bounded by githubConnectionTimeoutMs before pnpm starts;\n * only network failures or timeouts stop installation, while pnpm owns authentication and transport fallback. A run\n * that fails, is cancelled, or adds a package without a bundle patch restores\n * `package.json` and `pnpm-lock.yaml` as they were; downloaded files can stay.\n * @param spec One package spec, including local paths relative to the invocation directory.\n * @param options Whether to activate the installed bundle (defaults to true), the request id a cancellation names,\n * the pending build scripts to allow, whether to save an exact dependency, and the registry asked first.\n * @returns Package-manager diagnostics, the registries asked, and the observed activation outcome.\n */"
806
813
  },
807
814
  {
808
815
  "kind": "method",
@@ -833,7 +840,7 @@ export const TYPERT = {
833
840
  },
834
841
  {
835
842
  "name": "BundleInfo",
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}"
843
+ "declaration": "export interface BundleInfo {\n name: string;\n official: boolean;\n availability: 'installation' | 'profile' | 'missing';\n installTarget?: { spec: string; version: 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}"
837
844
  },
838
845
  {
839
846
  "name": "BundleRowInfo",
@@ -853,7 +860,7 @@ export const TYPERT = {
853
860
  },
854
861
  {
855
862
  "name": "InstallBundleOptions",
856
- "declaration": "export interface InstallBundleOptions {\n enabled?: boolean;\n requestId?: PluginInstallRequestId;\n approvedBuilds?: string[];\n registry?: Registry;\n}"
863
+ "declaration": "export interface InstallBundleOptions {\n enabled?: boolean;\n saveExact?: boolean;\n requestId?: PluginInstallRequestId;\n approvedBuilds?: string[];\n registry?: Registry;\n}"
857
864
  },
858
865
  {
859
866
  "name": "InstallSpecKind",
@@ -34,6 +34,7 @@ const _deepseek_ai_dsh_plugin_manager_pluginManager_installBundle_parameter_0$sc
34
34
  let _deepseek_ai_dsh_plugin_manager_pluginManager_installBundle_parameter_1$schema$value
35
35
  const _deepseek_ai_dsh_plugin_manager_pluginManager_installBundle_parameter_1$schema = () => (_deepseek_ai_dsh_plugin_manager_pluginManager_installBundle_parameter_1$schema$value ??= z.union([z.undefined(), z.object({
36
36
  'enabled': z.boolean().optional(),
37
+ 'saveExact': z.boolean().optional(),
37
38
  'requestId': z.intersection(z.string(), z.unknown()).optional(),
38
39
  'approvedBuilds': z.array(z.string()).optional(),
39
40
  'registry': z.union([z.literal(null), z.string()]).optional(),
@@ -80,6 +81,12 @@ const _deepseek_ai_dsh_plugin_manager_pluginManager_installBundle_result$schema
80
81
  let _deepseek_ai_dsh_plugin_manager_pluginManager_listBundles_result$schema$value
81
82
  const _deepseek_ai_dsh_plugin_manager_pluginManager_listBundles_result$schema = () => (_deepseek_ai_dsh_plugin_manager_pluginManager_listBundles_result$schema$value ??= z.array(z.object({
82
83
  'name': z.string(),
84
+ 'official': z.boolean(),
85
+ 'availability': z.union([z.literal("profile"), z.literal("installation"), z.literal("missing")]),
86
+ 'installTarget': z.object({
87
+ 'spec': z.string(),
88
+ 'version': z.string(),
89
+ }).optional(),
83
90
  'version': z.string().optional(),
84
91
  'meta': z.object({
85
92
  'title': z.union([z.string(), z.intersection(z.object({
@@ -416,7 +423,7 @@ export const TYPERT_REMOTE = {
416
423
  typeSymbol: '@deepseek-ai/dsh-plugin-manager/types#PluginInstallCancellation',
417
424
  create: _deepseek_ai_dsh_plugin_manager_pluginManager_cancelInstall_result$schema,
418
425
  },
419
- sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":600,"column":9},
426
+ sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":628,"column":9},
420
427
  },
421
428
  {
422
429
  id: '@deepseek-ai/dsh-plugin-manager#pluginManager/inspect',
@@ -453,7 +460,7 @@ export const TYPERT_REMOTE = {
453
460
  typeSymbol: '@deepseek-ai/dsh-plugin-manager/types#PluginSpecInspection',
454
461
  create: _deepseek_ai_dsh_plugin_manager_pluginManager_inspect_result$schema,
455
462
  },
456
- sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":350,"column":9},
463
+ sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":374,"column":9},
457
464
  },
458
465
  {
459
466
  id: '@deepseek-ai/dsh-plugin-manager#pluginManager/installBundle',
@@ -489,7 +496,7 @@ export const TYPERT_REMOTE = {
489
496
  typeSymbol: '@deepseek-ai/dsh-plugin-manager/types#ChangeResult',
490
497
  create: _deepseek_ai_dsh_plugin_manager_pluginManager_installBundle_result$schema,
491
498
  },
492
- sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":473,"column":3},
499
+ sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":497,"column":3},
493
500
  },
494
501
  {
495
502
  id: '@deepseek-ai/dsh-plugin-manager#pluginManager/listBundles',
@@ -504,7 +511,7 @@ export const TYPERT_REMOTE = {
504
511
  typeSymbol: '@deepseek-ai/dsh-plugin-manager#pluginManager/listBundles:result',
505
512
  create: _deepseek_ai_dsh_plugin_manager_pluginManager_listBundles_result$schema,
506
513
  },
507
- sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":280,"column":3},
514
+ sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":282,"column":3},
508
515
  },
509
516
  {
510
517
  id: '@deepseek-ai/dsh-plugin-manager#pluginManager/listPlugins',
@@ -519,7 +526,7 @@ export const TYPERT_REMOTE = {
519
526
  typeSymbol: '@deepseek-ai/dsh-plugin-manager#pluginManager/listPlugins:result',
520
527
  create: _deepseek_ai_dsh_plugin_manager_pluginManager_listPlugins_result$schema,
521
528
  },
522
- sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":256,"column":9},
529
+ sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":258,"column":9},
523
530
  },
524
531
  {
525
532
  id: '@deepseek-ai/dsh-plugin-manager#pluginManager/listVersionExemptions',
@@ -534,7 +541,7 @@ export const TYPERT_REMOTE = {
534
541
  typeSymbol: '@deepseek-ai/dsh-plugin-manager#pluginManager/listVersionExemptions:result',
535
542
  create: _deepseek_ai_dsh_plugin_manager_pluginManager_listVersionExemptions_result$schema,
536
543
  },
537
- sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":232,"column":3},
544
+ sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":234,"column":3},
538
545
  },
539
546
  {
540
547
  id: '@deepseek-ai/dsh-plugin-manager#pluginManager/registries',
@@ -549,7 +556,7 @@ export const TYPERT_REMOTE = {
549
556
  typeSymbol: '@deepseek-ai/dsh-plugin-manager/types#PluginRegistries',
550
557
  create: _deepseek_ai_dsh_plugin_manager_pluginManager_registries_result$schema,
551
558
  },
552
- sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":334,"column":9},
559
+ sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":358,"column":9},
553
560
  },
554
561
  {
555
562
  id: '@deepseek-ai/dsh-plugin-manager#pluginManager/removeBundle',
@@ -574,7 +581,7 @@ export const TYPERT_REMOTE = {
574
581
  typeSymbol: '@deepseek-ai/dsh-plugin-manager/types#ChangeResult',
575
582
  create: _deepseek_ai_dsh_plugin_manager_pluginManager_removeBundle_result$schema,
576
583
  },
577
- sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":617,"column":3},
584
+ sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":645,"column":3},
578
585
  },
579
586
  {
580
587
  id: '@deepseek-ai/dsh-plugin-manager#pluginManager/setBundleEnabled',
@@ -609,7 +616,7 @@ export const TYPERT_REMOTE = {
609
616
  typeSymbol: '@deepseek-ai/dsh-plugin-manager/types#ChangeResult',
610
617
  create: _deepseek_ai_dsh_plugin_manager_pluginManager_setBundleEnabled_result$schema,
611
618
  },
612
- sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":452,"column":3},
619
+ sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":476,"column":3},
613
620
  },
614
621
  {
615
622
  id: '@deepseek-ai/dsh-plugin-manager#pluginManager/setPluginEnabled',
@@ -644,7 +651,7 @@ export const TYPERT_REMOTE = {
644
651
  typeSymbol: '@deepseek-ai/dsh-plugin-manager/types#ChangeResult',
645
652
  create: _deepseek_ai_dsh_plugin_manager_pluginManager_setPluginEnabled_result$schema,
646
653
  },
647
- sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":434,"column":3},
654
+ sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":458,"column":3},
648
655
  },
649
656
  {
650
657
  id: '@deepseek-ai/dsh-plugin-manager#pluginManager/setVersionExemption',
@@ -700,7 +707,7 @@ export const TYPERT_REMOTE = {
700
707
  typeSymbol: '@deepseek-ai/dsh-plugin-manager/types#ChangeResult',
701
708
  create: _deepseek_ai_dsh_plugin_manager_pluginManager_setVersionExemption_result$schema,
702
709
  },
703
- sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":245,"column":3},
710
+ sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":247,"column":3},
704
711
  },
705
712
  {
706
713
  id: '@deepseek-ai/dsh-plugin-manager#pluginManager/waitForInstall',
@@ -725,7 +732,7 @@ export const TYPERT_REMOTE = {
725
732
  typeSymbol: '@deepseek-ai/dsh-plugin-manager#pluginManager/waitForInstall:result',
726
733
  create: _deepseek_ai_dsh_plugin_manager_pluginManager_waitForInstall_result$schema,
727
734
  },
728
- sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":590,"column":9},
735
+ sourceLocation: {"file":"packages/boot/plugin-manager/src/index.ts","line":618,"column":9},
729
736
  },
730
737
  ],
731
738
  }
@@ -1,8 +1,20 @@
1
- /** Read package names left undecided by pnpm 11, including after installation cleanup.
1
+ /**
2
+ * Record the build scripts a failed run reported as ignored, then list every name awaiting a decision.
3
+ *
4
+ * pnpm writes those policy entries itself only when it can prompt, and the
5
+ * manager always runs it non-interactively, so the run's own report is the
6
+ * record that survives.
2
7
  * @param dir Current profile directory.
8
+ * @param output Captured output of the failed run.
3
9
  * @returns Exact package names awaiting a build decision; wildcard rules are excluded.
4
10
  */
5
- export declare function readPendingBuilds(dir: string): Promise<string[]>;
11
+ export declare function recordPendingBuilds(dir: string, output: string): Promise<string[]>;
12
+ /**
13
+ * Read the package names a pnpm run reported as ignored build scripts.
14
+ * @param output Captured output of the run.
15
+ * @returns Exact names in report order; wildcard patterns are excluded.
16
+ */
17
+ export declare function ignoredBuildNames(output: string): string[];
6
18
  /** Persist approval without running scripts; the caller holds the profile manifest lock.
7
19
  * @param dir Current profile directory.
8
20
  * @param names Explicit package names from the pending build list.
@@ -4,6 +4,10 @@ import { join } from 'node:path';
4
4
  import { isAlias, isMap, isNode, isScalar, parseDocument, visit } from 'yaml';
5
5
  import { ManagementFailure } from "./failure.js";
6
6
  import { writeFileAtomic } from '@deepseek-ai/dsh-atomic-write';
7
+ /** Value pnpm leaves for a build script that still needs a decision. */
8
+ const UNDECIDED_BUILD = 'set this to true or false';
9
+ /** Where a non-interactive run names the build scripts it skipped. */
10
+ const IGNORED_BUILDS = /\bIgnored build scripts: (.+)/;
7
11
  async function readPolicy(dir) {
8
12
  let text;
9
13
  try {
@@ -27,16 +31,42 @@ async function readPolicy(dir) {
27
31
  throw new Error('allowBuilds must not contain YAML anchors or aliases');
28
32
  }
29
33
  });
34
+ const keys = isMap(builds) ? builds.items.flatMap(({ key }) => isScalar(key) && typeof key.value === 'string' ? [key.value] : []) : [];
30
35
  const pending = isMap(builds) ? builds.items.flatMap(({ key, value }) => isScalar(key) && typeof key.value === 'string' && !/[*?]/.test(key.value)
31
- && isScalar(value) && value.value === 'set this to true or false' ? [key.value] : []) : [];
32
- return { document, pending };
36
+ && isScalar(value) && value.value === UNDECIDED_BUILD ? [key.value] : []) : [];
37
+ return { document, pending, keys };
33
38
  }
34
- /** Read package names left undecided by pnpm 11, including after installation cleanup.
39
+ /**
40
+ * Record the build scripts a failed run reported as ignored, then list every name awaiting a decision.
41
+ *
42
+ * pnpm writes those policy entries itself only when it can prompt, and the
43
+ * manager always runs it non-interactively, so the run's own report is the
44
+ * record that survives.
35
45
  * @param dir Current profile directory.
46
+ * @param output Captured output of the failed run.
36
47
  * @returns Exact package names awaiting a build decision; wildcard rules are excluded.
37
48
  */
38
- export async function readPendingBuilds(dir) {
39
- return (await readPolicy(dir)).pending;
49
+ export async function recordPendingBuilds(dir, output) {
50
+ const { document, pending, keys } = await readPolicy(dir);
51
+ const known = new Set(keys);
52
+ const added = ignoredBuildNames(output).filter(name => !known.has(name));
53
+ if (added.length === 0)
54
+ return pending;
55
+ for (const name of added)
56
+ document.setIn(['allowBuilds', name], UNDECIDED_BUILD);
57
+ await writeFileAtomic(join(dir, 'pnpm-workspace.yaml'), String(document), { mode: 0o600 });
58
+ return [...pending, ...added];
59
+ }
60
+ /**
61
+ * Read the package names a pnpm run reported as ignored build scripts.
62
+ * @param output Captured output of the run.
63
+ * @returns Exact names in report order; wildcard patterns are excluded.
64
+ */
65
+ export function ignoredBuildNames(output) {
66
+ const reported = IGNORED_BUILDS.exec(output)?.[1];
67
+ if (reported === undefined)
68
+ return [];
69
+ return reported.split(',').map(name => name.trim()).filter(name => name !== '' && !/[*?]/.test(name));
40
70
  }
41
71
  /** Persist approval without running scripts; the caller holds the profile manifest lock.
42
72
  * @param dir Current profile directory.
@@ -75,7 +75,7 @@ export declare class PluginManager extends TypertRemoteService {
75
75
  * @returns Current runtime entries with persistent patch targets.
76
76
  */
77
77
  listPlugins(): Promise<PluginInfo[]>;
78
- /** Read the profile's installed bundles, the bundles this dsh installation supplies, and the selected names that are not bundles.
78
+ /** Read installed, installation-provided, and offline Official catalog bundles, plus selected non-bundle names.
79
79
  * A dependency without a bundle patch is listed, as a `not-bundle` problem, only while it is selected.
80
80
  * @returns Package versions, manifest descriptions, the installable spec of profile dependencies, rows, optional
81
81
  * display metadata, activation selections, whether the installation offers the bundle, and removal availability.
@@ -112,7 +112,7 @@ export declare class PluginManager extends TypertRemoteService {
112
112
  * `package.json` and `pnpm-lock.yaml` as they were; downloaded files can stay.
113
113
  * @param spec One package spec, including local paths relative to the invocation directory.
114
114
  * @param options Whether to activate the installed bundle (defaults to true), the request id a cancellation names,
115
- * the pending build scripts to allow for this profile before pnpm runs, and the registry asked first.
115
+ * the pending build scripts to allow, whether to save an exact dependency, and the registry asked first.
116
116
  * @returns Package-manager diagnostics, the registries asked, and the observed activation outcome.
117
117
  */
118
118
  installBundle(spec: string, options?: InstallBundleOptions): Promise<ChangeResult>;
@@ -42,14 +42,15 @@ import { withFileLock, writeFileAtomic } from '@deepseek-ai/dsh-atomic-write';
42
42
  import z from '@deepseek-ai/schemastery';
43
43
  import { TypertRemoteService, Remote } from '@deepseek-ai/dsh-typert-protocol';
44
44
  import { pluginEntryId, readPluginInventory } from '@deepseek-ai/dsh-host-plugin-inventory';
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';
45
+ import { readPluginMeta, readProfileManifest, resolveBundleDir, loadOverlayPatches, composeEntries, reconcileProfilePatches, readProfilePatches, OPTIONAL_BUNDLES, bundlePatchPaths, evaluatePluginCompatibility, readProfileCompatibility, readProfileVersionExemptions, setProfileVersionExemption, PROFILE_COMPATIBILITY_FILENAME, OFFICIAL_ON_DEMAND_CATALOG, getDshRuntimeVersion, } 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 { officialBundleInstallTarget } from "./official-install-target.js";
48
49
  import { dependencySpec, InvalidInstallSpecError, parseInstallSpec } from "./install-spec.js";
49
50
  import { attributeFailure, normalizeRegistry, NPMMIRROR_REGISTRY, registryPlan } from "./registry.js";
50
51
  import { writePluginEnabled } from "./patch.js";
51
52
  import { incompatiblePlugin, ManagementFailure } from "./failure.js";
52
- import { approveBuilds, readPendingBuilds } from "./build-approval.js";
53
+ import { approveBuilds, recordPendingBuilds } from "./build-approval.js";
53
54
  import { checkGithubConnection } from "./github-connection.js";
54
55
  export { classifyInstallFailure } from "./install-failure.js";
55
56
  export { InvalidInstallSpecError, parseInstallSpec } from "./install-spec.js";
@@ -274,7 +275,7 @@ let PluginManager = (() => {
274
275
  return { ...entry, patchId: candidate.id };
275
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
+ /** Read installed, installation-provided, and offline Official catalog bundles, plus selected non-bundle names.
278
279
  * A dependency without a bundle patch is listed, as a `not-bundle` problem, only while it is selected.
279
280
  * @returns Package versions, manifest descriptions, the installable spec of profile dependencies, rows, optional
280
281
  * display metadata, activation selections, whether the installation offers the bundle, and removal availability.
@@ -286,45 +287,71 @@ let PluginManager = (() => {
286
287
  const recorded = manifest.dependencies ?? {};
287
288
  const dependencies = Object.keys(recorded);
288
289
  const installation = JSON.parse(readFileSync(this.profile.installAnchor, 'utf8'));
289
- const names = [...new Set([...selected, ...dependencies, ...Object.keys(installation.dependencies ?? {})])];
290
+ const catalog = new Map(OFFICIAL_ON_DEMAND_CATALOG.map(entry => [entry.packageName, entry]));
291
+ const names = [...new Set([...selected, ...dependencies, ...Object.keys(installation.dependencies ?? {}), ...catalog.keys()])];
290
292
  const bundles = [];
293
+ const runtimeVersion = getDshRuntimeVersion();
291
294
  for (const name of names) {
292
295
  const installed = dependencies.includes(name);
293
296
  const optional = OPTIONAL_BUNDLES.includes(name);
294
297
  const enabled = selected.includes(name);
295
298
  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.
299
+ const offered = catalog.get(name);
300
+ const official = optional || offered !== undefined;
301
+ let target = {};
302
+ let targetError;
303
+ if (offered !== undefined) {
304
+ try {
305
+ target = { installTarget: officialBundleInstallTarget(name, this.profile.installAnchor, runtimeVersion) };
306
+ }
307
+ catch (error) {
308
+ targetError = managementError(error);
309
+ }
310
+ }
311
+ const catalogMeta = offered === undefined ? {} : { meta: offered.meta };
312
+ if (offered !== undefined && !installed && !shipped) {
313
+ bundles.push({ name, official, availability: 'missing', ...catalogMeta, ...target, enabled, installed, optional,
314
+ removable: enabled, rows: [], overrides: [],
315
+ ...targetError !== undefined ? { error: targetError } : enabled ? { error: { code: 'not-bundle' } } : {} });
316
+ continue;
317
+ }
318
+ // Installation-owned copies remain protected even if a profile also declares their names.
298
319
  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
320
  const removable = owned || (enabled && !installed && !shipped);
301
321
  const sourceOf = (packageName) => owned ? { source: dependencySpec(name, recorded[name], this.profile.dir, packageName) } : {};
302
322
  const readOnlyReason = this.protectsManager(name) ? 'management-required' : undefined;
323
+ let availability = 'missing';
324
+ let version;
303
325
  try {
304
326
  const info = bundleManifest(name, this.profile.dir, this.profile.installAnchor);
327
+ availability = shipped ? 'installation' : 'profile';
305
328
  if (info === undefined) {
306
- if (enabled)
307
- bundles.push({ name, ...sourceOf(), enabled, installed, optional,
308
- removable: removable && readOnlyReason === undefined,
309
- ...(readOnlyReason === undefined ? {} : { readOnlyReason }), error: { code: 'not-bundle' }, rows: [], overrides: [] });
329
+ if (enabled || offered !== undefined)
330
+ bundles.push({ name, official, availability, ...catalogMeta, ...target,
331
+ ...sourceOf(), enabled, installed, optional, removable: removable && readOnlyReason === undefined,
332
+ ...(readOnlyReason === undefined ? {} : { readOnlyReason }), error: targetError ?? { code: 'not-bundle' }, rows: [], overrides: [] });
310
333
  continue;
311
334
  }
335
+ version = info.version;
312
336
  const compatibility = evaluatePluginCompatibility(info, exemptions);
313
337
  if (compatibility !== undefined && !compatibility.exempted)
314
338
  throw new ManagementFailure('incompatible-version', [incompatiblePlugin(compatibility)]);
315
339
  const dir = resolveBundleDir('dsh', name, this.profile.installAnchor, this.profile.dir);
316
- const meta = readPluginMeta(info.name ?? name, pathToFileURL(join(dir, 'package.json')).href);
317
- bundles.push({ name, ...(info.version === undefined ? {} : { version: info.version }),
340
+ const meta = offered?.meta ?? readPluginMeta(info.name ?? name, pathToFileURL(join(dir, 'package.json')).href);
341
+ bundles.push({ name, official, availability, ...target, ...(version === undefined ? {} : { version }),
318
342
  ...(info.description === undefined || info.description === '' ? {} : { description: info.description }),
319
343
  ...meta === undefined ? {} : { meta },
320
344
  ...sourceOf(info.name), enabled, installed, optional, removable: removable && readOnlyReason === undefined,
321
345
  ...(readOnlyReason === undefined ? {} : { readOnlyReason }),
322
- ...this.declaredRows(name, info) });
346
+ ...this.declaredRows(name, info),
347
+ ...targetError === undefined ? {} : { error: targetError } });
323
348
  }
324
349
  catch (error) {
325
- if (enabled || installed) {
326
- bundles.push({ name, ...sourceOf(), enabled, installed, optional, removable: removable && readOnlyReason === undefined,
327
- ...(readOnlyReason === undefined ? {} : { readOnlyReason }), error: managementError(error), rows: [], overrides: [] });
350
+ if (enabled || installed || optional || offered !== undefined) {
351
+ bundles.push({ name, official, availability, ...catalogMeta, ...target, ...(version === undefined ? {} : { version }),
352
+ ...sourceOf(), enabled, installed, optional, removable: removable && readOnlyReason === undefined,
353
+ ...(readOnlyReason === undefined ? {} : { readOnlyReason }),
354
+ error: targetError ?? managementError(error), rows: [], overrides: [] });
328
355
  }
329
356
  }
330
357
  }
@@ -479,7 +506,7 @@ let PluginManager = (() => {
479
506
  * `package.json` and `pnpm-lock.yaml` as they were; downloaded files can stay.
480
507
  * @param spec One package spec, including local paths relative to the invocation directory.
481
508
  * @param options Whether to activate the installed bundle (defaults to true), the request id a cancellation names,
482
- * the pending build scripts to allow for this profile before pnpm runs, and the registry asked first.
509
+ * the pending build scripts to allow, whether to save an exact dependency, and the registry asked first.
483
510
  * @returns Package-manager diagnostics, the registries asked, and the observed activation outcome.
484
511
  */
485
512
  installBundle(spec, options) {
@@ -538,7 +565,7 @@ let PluginManager = (() => {
538
565
  throw new InstallCancelledError();
539
566
  result.registries.push(registry);
540
567
  announce('installing', { registry, index: index + 1, total: plan.length });
541
- run = await this.runPnpm(['add', spec, ...registryArguments(registry)], control.abort.signal, requestId);
568
+ run = await this.runPnpm(['add', spec, ...options?.saveExact === true ? ['--save-exact'] : [], ...registryArguments(registry)], control.abort.signal, requestId);
542
569
  result.packageResult = run;
543
570
  if (stopped())
544
571
  throw new InstallCancelledError();
@@ -570,9 +597,9 @@ let PluginManager = (() => {
570
597
  if (succeeded)
571
598
  delete result.failedAt;
572
599
  if (!succeeded) {
573
- // pnpm-workspace.yaml is not restored, so the names pnpm left undecided there can be offered for approval.
600
+ // pnpm-workspace.yaml is not restored, so the names it left undecided, or reported as ignored, are offered for approval.
574
601
  try {
575
- result.pendingBuilds = await readPendingBuilds(this.profile.dir);
602
+ result.pendingBuilds = await recordPendingBuilds(this.profile.dir, run.output);
576
603
  }
577
604
  catch (error) {
578
605
  this.ownerContext.logger.warn('Could not read pending build approvals after pnpm failed', error);
@@ -582,8 +609,11 @@ let PluginManager = (() => {
582
609
  const after = readProfileManifest('dsh', this.profile.dir).dependencies ?? {};
583
610
  const installed = Object.keys(after).filter(name => before[name] !== after[name]);
584
611
  // Registry retries can retain the saved range after a partial installation.
585
- if (installed.length === 0)
586
- installed.push(...Object.keys(after).filter(name => spec === name || spec.startsWith(`${name}@`)));
612
+ if (installed.length === 0) {
613
+ const parsed = parsedForRegistry(spec);
614
+ const localName = parsed.kind === 'path' ? readProfileManifest('dsh', parsed.path).name : undefined;
615
+ installed.push(...Object.keys(after).filter(name => name === localName || spec === name || spec.startsWith(`${name}@`)));
616
+ }
587
617
  const target = installed[0];
588
618
  if (installed.length !== 1 || target === undefined)
589
619
  throw new ManagementFailure('ambiguous-install');
@@ -706,7 +736,8 @@ let PluginManager = (() => {
706
736
  const rows = [];
707
737
  const packages = this.ctx.get('pluginPackages');
708
738
  for (const row of flatten(composeEntries([patches.filter(item => item.insert !== undefined)]))) {
709
- if (typeof row.id !== 'string' || typeof row.name !== 'string')
739
+ // Group rows are containers without a plugin inventory entry; their children carry the switches.
740
+ if (typeof row.id !== 'string' || typeof row.name !== 'string' || row.group === true)
710
741
  continue;
711
742
  const active = live.get(row.id);
712
743
  const entryId = active?.entryId;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Offer a source checkout link or an exact published package version. Source installations are
3
+ * identified by the real CLI manifest and its unpacked workspace dependencies, including Desktop's
4
+ * development symlink. An incomplete checkout is an error, never a registry fallback.
5
+ * @param name - Official catalog package name.
6
+ * @param installAnchor - Running application's DSH package.json, possibly reached through a symlink.
7
+ * @param version - Running DSH version used by its release family.
8
+ * @returns An ordinary package-manager spec and its package version.
9
+ * @throws when a source installation lacks its workspace package, bundle patch, or built runtime.
10
+ */
11
+ export declare function officialBundleInstallTarget(name: string, installAnchor: string, version: string): {
12
+ spec: string;
13
+ version: string;
14
+ };
15
+ //# sourceMappingURL=official-install-target.d.ts.map
@@ -0,0 +1,54 @@
1
+ /** Select Official bundle packages from the running installation without importing provider code. */
2
+ import { existsSync, globSync, readFileSync, realpathSync, statSync } from 'node:fs';
3
+ import { dirname, join, resolve } from 'node:path';
4
+ import { bundlePatchPaths } from '@deepseek-ai/dsh-app-boot';
5
+ function manifestAt(file) {
6
+ return JSON.parse(readFileSync(file, 'utf8'));
7
+ }
8
+ /**
9
+ * Offer a source checkout link or an exact published package version. Source installations are
10
+ * identified by the real CLI manifest and its unpacked workspace dependencies, including Desktop's
11
+ * development symlink. An incomplete checkout is an error, never a registry fallback.
12
+ * @param name - Official catalog package name.
13
+ * @param installAnchor - Running application's DSH package.json, possibly reached through a symlink.
14
+ * @param version - Running DSH version used by its release family.
15
+ * @returns An ordinary package-manager spec and its package version.
16
+ * @throws when a source installation lacks its workspace package, bundle patch, or built runtime.
17
+ */
18
+ export function officialBundleInstallTarget(name, installAnchor, version) {
19
+ const anchor = realpathSync(installAnchor);
20
+ const installation = manifestAt(anchor);
21
+ const source = installation.name === '@deepseek-ai/dsh'
22
+ && Object.values(installation.dependencies ?? {}).some(value => value.startsWith('workspace:'));
23
+ if (!source)
24
+ return { spec: `${name}@${version}`, version };
25
+ const root = resolve(dirname(anchor), '../..');
26
+ if (anchor !== join(root, 'apps', 'cli', 'package.json')
27
+ || !existsSync(join(root, 'package.json'))
28
+ || manifestAt(join(root, 'package.json')).name !== '@deepseek-ai/dsh-root') {
29
+ throw new Error(`Official bundle ${name}: source DSH installation ${anchor} requires its complete workspace checkout`);
30
+ }
31
+ const matches = globSync('packages/*/*/package.json', { cwd: root })
32
+ .map(file => ({ file: join(root, file), manifest: manifestAt(join(root, file)) }))
33
+ .filter(entry => entry.manifest.name === name);
34
+ const selected = matches[0];
35
+ if (matches.length !== 1 || selected === undefined) {
36
+ throw new Error(`Official bundle ${name}: expected one workspace package in ${root}, found ${matches.length}; restore the checkout and run pnpm install`);
37
+ }
38
+ const { file, manifest } = selected;
39
+ const directory = realpathSync(dirname(file));
40
+ if (manifest.version !== version || manifest.private === true || manifest.dsh?.bundle === undefined) {
41
+ throw new Error(`Official bundle ${name}: ${file} must declare a public bundle at DSH version ${version}`);
42
+ }
43
+ const main = typeof manifest.main === 'string' ? resolve(directory, manifest.main) : undefined;
44
+ if (main === undefined || !existsSync(main) || !statSync(main).isFile()) {
45
+ throw new Error(`Official bundle ${name}: built runtime is missing in ${directory}; run pnpm install and pnpm run build in ${root}`);
46
+ }
47
+ for (const patch of bundlePatchPaths(directory, manifest.dsh.bundle)) {
48
+ if (!existsSync(patch) || !statSync(patch).isFile()) {
49
+ throw new Error(`Official bundle ${name}: bundle patch ${patch} is missing; restore the checkout`);
50
+ }
51
+ }
52
+ return { spec: `link:${directory}`, version };
53
+ }
54
+ //# sourceMappingURL=official-install-target.js.map
@@ -14,7 +14,7 @@ export function apply(ctx) {
14
14
  description: 'List plugins or bundles in the current profile, enable or disable them, install a bundle, or remove an installed bundle. Every action requires danger-full-access permission or approval for this call. Approval does not change the session permission mode. Changes affect every session in this profile. List first to obtain exact identifiers. Package installation can execute allowed build scripts. Live profiles apply changes immediately; startup profiles require restart. Incompatible DSH peer dependencies block installation and activation. Version exemptions risk crashes and data loss: warn the user and obtain explicit permission for the exact plugin and runtime versions before granting one.',
15
15
  parameters: {
16
16
  action: { type: 'string', required: true, enum: ['list_plugins', 'list_bundles', 'set_plugin', 'set_bundle', 'install_bundle', 'remove_bundle', 'list_version_exemptions', 'set_version_exemption'], description: 'Management operation.' },
17
- target: { type: 'string', description: 'Plugin entry id, bundle package name, or installation spec, according to action.' },
17
+ target: { type: 'string', description: 'Plugin entry id, bundle package name, or installation spec, according to action. For an Official catalog entry, pass list_bundles installTarget.spec to install_bundle.' },
18
18
  enabled: { type: 'boolean', description: 'Required for set operations; defaults to true for installation. For set_version_exemption, true grants and false revokes.' },
19
19
  runtimeVersion: { type: 'string', description: 'For set_version_exemption: exact DSH version from list_version_exemptions. Target must be the manifest package-name@version, not an alias or version range.' },
20
20
  acceptRisk: { type: 'boolean', description: 'For granting an exemption: true only after warning the user about possible crashes and data loss and receiving explicit permission for this exact plugin/runtime pair. General installation permission is not enough.' },
@@ -40,9 +40,18 @@ export interface BundleRowInfo {
40
40
  /** The Loader entry carrying this row, when exactly one live entry has its id. */
41
41
  entryId?: PluginEntryId;
42
42
  }
43
- /** One installed or installation-provided bundle. */
43
+ /** One shipped, profile-installed, or discoverable on-demand bundle. */
44
44
  export interface BundleInfo {
45
45
  name: string;
46
+ /** Project-maintained shipped optional bundle or on-demand catalog entry, independent of installation and selection. */
47
+ official: boolean;
48
+ /** Readable package location; on-demand catalog entries exclude undeclared transitive copies, and declared files may be missing. */
49
+ availability: 'installation' | 'profile' | 'missing';
50
+ /** Exact released Host version or same-checkout development link offered for an on-demand catalog entry. */
51
+ installTarget?: {
52
+ spec: string;
53
+ version: string;
54
+ };
46
55
  version?: string;
47
56
  /** Local display text with available translations or literal fallbacks, or a metadata diagnostic. */
48
57
  meta?: PluginLocalizedMeta;
@@ -50,7 +59,7 @@ export interface BundleInfo {
50
59
  description?: string;
51
60
  /** Selected in the profile manifest; a load error means its layer was skipped. */
52
61
  enabled: boolean;
53
- /** Whether the profile's own dependencies hold the package; false for a bundle the dsh installation supplies. */
62
+ /** Whether the profile declares this dependency; availability separately reports whether its files can be read. */
54
63
  installed: boolean;
55
64
  /**
56
65
  * Present for a profile dependency the installation does not also supply: the spec `pnpm add` accepts, with local
@@ -128,6 +137,8 @@ export type PluginInstallRequestId = Branded<'PluginInstallRequestId'>;
128
137
  /** Bundle installation defaults to activation; callers that offer cancellation supply their request id. */
129
138
  export interface InstallBundleOptions {
130
139
  enabled?: boolean;
140
+ /** Save the resolved dependency version without a range; applies to any bundle installation. */
141
+ saveExact?: boolean;
131
142
  requestId?: PluginInstallRequestId;
132
143
  /** Explicitly allow these pending packages' scripts for this profile, then install; a name no longer pending refuses the call. */
133
144
  approvedBuilds?: string[];
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.1-alpha.1",
4
+ "version": "0.2.1-alpha.2",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -56,26 +56,26 @@
56
56
  ],
57
57
  "license": "MIT",
58
58
  "dependencies": {
59
- "execa": "^10.0.0",
59
+ "@deepseek-ai/dsh-app-boot": "0.2.1-alpha.2",
60
+ "@deepseek-ai/dsh-atomic-write": "0.2.1-alpha.2",
61
+ "@deepseek-ai/schemastery": "~3.18.5-alpha.1",
62
+ "execa": "^10.0.1",
60
63
  "yaml": "^2.9.0",
61
64
  "zod": "^4.4.3",
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"
65
+ "@deepseek-ai/dsh-util-values": "0.2.1-alpha.2"
66
66
  },
67
67
  "peerDependencies": {
68
68
  "@deepseek-ai/cordis": "~4.0.5-alpha.1",
69
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"
70
+ "@deepseek-ai/dsh-host-plugin-inventory": "0.2.1-alpha.2",
71
+ "@deepseek-ai/dsh-subprocess": "0.2.1-alpha.2",
72
+ "@deepseek-ai/dsh-typert-protocol": "0.2.1-alpha.2",
73
+ "@deepseek-ai/dsh-tools": "0.2.1-alpha.2",
74
+ "@deepseek-ai/dsh-hmr": "0.2.1-alpha.2",
75
+ "@deepseek-ai/dsh-brand": "0.2.1-alpha.2",
76
+ "@deepseek-ai/dsh-sandbox-policy": "0.2.1-alpha.2",
77
+ "@deepseek-ai/dsh-sandbox": "0.2.1-alpha.2",
78
+ "@deepseek-ai/dsh-user-approval": "0.2.1-alpha.2"
79
79
  },
80
80
  "peerDependenciesMeta": {
81
81
  "@deepseek-ai/dsh-hmr": {
@@ -87,22 +87,22 @@
87
87
  },
88
88
  "devDependencies": {
89
89
  "chokidar": "^4.0.3",
90
- "@deepseek-ai/dsh-package-manifest": "0.2.1-alpha.1",
90
+ "@deepseek-ai/dsh-package-manifest": "0.2.1-alpha.2",
91
91
  "@deepseek-ai/cordis": "~4.0.5-alpha.1",
92
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",
93
+ "@deepseek-ai/dsh-host-plugin-inventory": "0.2.1-alpha.2",
94
+ "@deepseek-ai/dsh-subprocess": "0.2.1-alpha.2",
95
+ "@deepseek-ai/dsh-typert-protocol": "0.2.1-alpha.2",
96
+ "@deepseek-ai/dsh-tools": "0.2.1-alpha.2",
97
+ "@deepseek-ai/dsh-system-prompt": "0.2.1-alpha.2",
98
+ "@deepseek-ai/dsh-hmr": "0.2.1-alpha.2",
99
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"
100
+ "@deepseek-ai/dsh-brand": "0.2.1-alpha.2",
101
+ "@deepseek-ai/dsh-sandbox-policy": "0.2.1-alpha.2",
102
+ "@deepseek-ai/dsh-agent": "0.2.1-alpha.2",
103
+ "@deepseek-ai/dsh-session": "0.2.1-alpha.2",
104
+ "@deepseek-ai/dsh-session-projection": "0.2.1-alpha.2",
105
+ "@deepseek-ai/dsh-sandbox": "0.2.1-alpha.2",
106
+ "@deepseek-ai/dsh-user-approval": "0.2.1-alpha.2"
107
107
  }
108
108
  }