dsh-packycode-api 1.0.3 → 1.0.4

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/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.4
4
+
5
+ - Settings can check for and install a newer npm release. **Update plugin** installs the exact published version into the owning profile with the host's bundled pnpm, then reads `package.json` back before reporting success. A `link:` checkout is left alone. The desktop app still needs a full restart afterwards.
6
+ - The update runs with `minimumReleaseAge=0`, so neither a fresh release of this plugin nor a fresh release of a sibling plugin already in the profile lockfile blocks it. The README FAQ explains the `ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION` that a Plugins-page install can still hit.
7
+
3
8
  ## 1.0.3
4
9
 
5
10
  - The Claude CLI timeout test holds the event loop open. The runner unrefs its timer and the test stub is not a real process, so Node 22's test runner cancelled the still-pending test and stopped the publish workflow. Plugin behavior is unchanged. `1.0.2` was tagged but not published.
package/README.en.md CHANGED
@@ -42,7 +42,7 @@ Open the desktop application's **Plugins** page, click **Add plugin**, and enter
42
42
  dsh-packycode-api
43
43
  ```
44
44
 
45
- Choose the **official** install source, then click **Install**. To pin a version, enter `dsh-packycode-api@1.0.3`.
45
+ Choose the **official** install source, then click **Install**. To pin a version, enter `dsh-packycode-api@1.0.4`.
46
46
 
47
47
  Then **manually restart the desktop application**. Refreshing the page does not reload the plugin inside the Host process.
48
48
 
@@ -61,7 +61,7 @@ Choose a model from the `PackyCode` group. `deepseek-flash` and `gpt-6.1-sol` wo
61
61
 
62
62
  The **Plugins** page shows the installed state and version, and is also where you enable, disable, or uninstall.
63
63
 
64
- **For non-desktop profiles only:** `dsh plugin --profile <name> add dsh-packycode-api@1.0.3`. Do not use it for `desktop` — the CLI refuses it (`profile "desktop" is managed exclusively by the Electron application`).
64
+ **For non-desktop profiles only:** `dsh plugin --profile <name> add dsh-packycode-api@1.0.4`. Do not use it for `desktop` — the CLI refuses it (`profile "desktop" is managed exclusively by the Electron application`).
65
65
 
66
66
  A local checkout can be linked with `dsh plugin --profile <name> add link:/path/to/dsh-packycode-api`.
67
67
 
@@ -117,7 +117,7 @@ A model in the list does not mean the current token group allows it. When changi
117
117
 
118
118
  ## Update and uninstall
119
119
 
120
- **Update:** on the desktop **Plugins** page, install `dsh-packycode-api` again (or `dsh-packycode-api@version`), then quit and reopen the desktop application. A page refresh does not reload the Host.
120
+ **Update:** open **Settings → PackyCode** and, when a newer version is listed, click **Update plugin**. It installs that exact version into the owning profile with DSH's bundled pnpm and reads `package.json` back before reporting success. A local `link:` checkout does not get the button. Then quit and reopen the desktop application. A page refresh does not reload the Host. Reinstalling `dsh-packycode-api` from the **Plugins** page does the same job.
121
121
 
122
122
  **Uninstall:** remove this plugin on the **Plugins** page, then restart the desktop application. Uninstalling does not remove other plugins, the profile, or the token in the PackyCode console.
123
123
 
@@ -129,6 +129,8 @@ A model in the list does not mean the current token group allows it. When changi
129
129
 
130
130
  **A rate limit disabled the key?** It should not. HTTP 429 does not rotate. Exhausted quota or HTTP 401 does; after a top-up, click **Re-enable**.
131
131
 
132
+ **Installing from the Plugins page fails with `ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION`?** pnpm refuses packages published less than 24 hours ago and checks the whole profile lockfile. The package it names is often another plugin that was just updated, not this one. Retry once it is 24 hours old, or add the reported `name@version` under `minimumReleaseAgeExclude` in the profile's `pnpm-workspace.yaml`. Once this plugin is installed, **Update plugin** in Settings is not subject to that check.
133
+
132
134
  **The CLI is installed, but Claude still fails?** A host launched from Finder or the Start menu usually has a shorter `PATH` than a terminal. The plugin also checks the official install location, common package-manager directories, and `npm prefix -g`. If it still cannot find the binary, set `claudeCliPath` or click **Check again**. After a Host change or a reinstall, quit and reopen the desktop application.
133
135
 
134
136
  ## Security and scope
package/README.md CHANGED
@@ -42,7 +42,7 @@
42
42
  dsh-packycode-api
43
43
  ```
44
44
 
45
- 在**安装源**中选择**官方源**,然后点击**安装**。需要固定版本时填入 `dsh-packycode-api@1.0.3`。
45
+ 在**安装源**中选择**官方源**,然后点击**安装**。需要固定版本时填入 `dsh-packycode-api@1.0.4`。
46
46
 
47
47
  完成后**手动重启桌面应用**。仅刷新页面不会重新加载 Host 里的插件。
48
48
 
@@ -61,7 +61,7 @@ dsh-packycode-api
61
61
 
62
62
  桌面应用的**插件**页可以看到已安装状态与版本,也可以在那里启用、停用或卸载。
63
63
 
64
- **仅用于非 desktop profile:** `dsh plugin --profile <name> add dsh-packycode-api@1.0.3`。不要对 `desktop` 使用它 —— CLI 会直接拒绝(`profile "desktop" is managed exclusively by the Electron application`)。
64
+ **仅用于非 desktop profile:** `dsh plugin --profile <name> add dsh-packycode-api@1.0.4`。不要对 `desktop` 使用它 —— CLI 会直接拒绝(`profile "desktop" is managed exclusively by the Electron application`)。
65
65
 
66
66
  本地开发可以用绝对路径:`dsh plugin --profile <name> add link:/path/to/dsh-packycode-api`。
67
67
 
@@ -117,7 +117,7 @@ dsh-packycode-api
117
117
 
118
118
  ## 更新与卸载
119
119
 
120
- **更新:** 在桌面应用的**插件**页重新安装 `dsh-packycode-api`(或 `dsh-packycode-api@版本`),然后完全退出并重新打开桌面应用。只刷新页面不会重载 Host。
120
+ **更新:** 打开 **设置 → PackyCode**,有新版本时点「更新插件」。它会在当前 profile 目录里用 DSH 自带的 pnpm 安装精确版本,并读回 `package.json` 确认版本落地。本地 `link:` 开发安装不会出现这个按钮。完成后完全退出并重新打开桌面应用。只刷新页面不会重载 Host。也可以在**插件**页重新安装 `dsh-packycode-api`。
121
121
 
122
122
  **卸载:** 在**插件**页卸载本插件,然后重启桌面应用。卸载不会删除其它插件、整个 profile,也不会撤销 PackyCode 控制台里的令牌。
123
123
 
@@ -129,6 +129,8 @@ dsh-packycode-api
129
129
 
130
130
  **限速之后 Key 被停用了?** 不该发生。429 不会切换 Key。额度用尽或 401 才会停用;充值后点「重新启用」。
131
131
 
132
+ **插件页安装报 `ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION`?** pnpm 默认拒绝发布不满 24 小时的包,并且会检查 profile 里的整个 lockfile。报错里点名的往往是另一个刚更新过的插件,不是本插件。等它发布满 24 小时再重试,或把报错里的 `包名@版本` 加到 profile 目录 `pnpm-workspace.yaml` 的 `minimumReleaseAgeExclude` 下。已安装本插件后,设置页的「更新插件」不受这个限制。
133
+
132
134
  **装了 CLI 还是不能用 Claude?** 从 Finder 或开始菜单启动时,`PATH` 往往比终端短。插件会再查官方安装位置、常见包管理器目录和 `npm prefix -g`。仍找不到就在设置里填 `claudeCliPath`,或点「重新检测」。改的是 Host 或重新安装了插件时,要完全退出并重启桌面应用。
133
135
 
134
136
  ## 安全边界
package/lib/client.js CHANGED
@@ -163,7 +163,26 @@ window.__ModuleLoader__.load({
163
163
  cliNetwork: "下载失败,请检查网络或 npm 镜像设置后重试。",
164
164
  cliTimeout: "安装超时,请检查网络后重试。",
165
165
  cliNotFoundAfterInstall: "npm 显示安装完成,但仍找不到 claude 命令。请在终端执行 claude --version 确认,然后点「重新检测」。",
166
- cliWindowsNote: "Windows 上 Claude Code 还需要 Git for Windows(https://git-scm.com/downloads/win)。"
166
+ cliWindowsNote: "Windows 上 Claude Code 还需要 Git for Windows(https://git-scm.com/downloads/win)。",
167
+ versionTitle: "插件版本",
168
+ versionCurrent: "当前版本",
169
+ versionLatest: "最新版本",
170
+ versionCheck: "检查更新",
171
+ versionChecking: "正在检查更新…",
172
+ versionFailed: "无法检查最新版本。",
173
+ versionUpToDate: "已是最新版本。",
174
+ versionAvailable: "发现新版本 v{version}。",
175
+ versionLinked: "当前是本地开发安装,请在插件仓库拉取代码并重新构建。",
176
+ versionManual: "无法在这里更新。请在桌面应用的插件页重新安装 dsh-packycode-api。",
177
+ versionUpdate: "更新插件",
178
+ versionUpdating: "正在更新…",
179
+ versionUpdateFailed: "更新失败,请重试,或在插件页重新安装。",
180
+ versionUpdated: "已更新到 v{version}。",
181
+ versionUpdatedHint: "新版本需要完全退出并重新打开桌面应用才会生效;只刷新页面不会重载 Host 里的插件。",
182
+ versionRestart: "如何重启",
183
+ versionRestartHint: "插件无法安全地重启桌面应用。请完全退出 DSH,再重新打开。",
184
+ versionRefresh: "刷新界面",
185
+ versionLater: "稍后"
167
186
  };
168
187
  const en = {
169
188
  nav: "PackyCode",
@@ -234,7 +253,26 @@ window.__ModuleLoader__.load({
234
253
  cliNetwork: "The download failed. Check your network or npm registry settings and retry.",
235
254
  cliTimeout: "The install timed out. Check your network and retry.",
236
255
  cliNotFoundAfterInstall: "npm reported success, but the claude command still cannot be found. Run claude --version in a terminal, then click Check again.",
237
- cliWindowsNote: "On Windows, Claude Code also needs Git for Windows (https://git-scm.com/downloads/win)."
256
+ cliWindowsNote: "On Windows, Claude Code also needs Git for Windows (https://git-scm.com/downloads/win).",
257
+ versionTitle: "Plugin version",
258
+ versionCurrent: "Current version",
259
+ versionLatest: "Latest version",
260
+ versionCheck: "Check for updates",
261
+ versionChecking: "Checking for updates…",
262
+ versionFailed: "Could not check the latest version.",
263
+ versionUpToDate: "You are on the latest version.",
264
+ versionAvailable: "A new version v{version} is available.",
265
+ versionLinked: "Installed from a local checkout. Pull and rebuild the plugin repository instead.",
266
+ versionManual: "This install cannot be updated here. Reinstall dsh-packycode-api from the desktop Plugins page.",
267
+ versionUpdate: "Update plugin",
268
+ versionUpdating: "Updating…",
269
+ versionUpdateFailed: "The update failed. Try again, or reinstall from the Plugins page.",
270
+ versionUpdated: "Updated to v{version}.",
271
+ versionUpdatedHint: "Quit and reopen the desktop application. Refreshing the page does not reload the plugin inside the Host.",
272
+ versionRestart: "How to restart",
273
+ versionRestartHint: "The plugin cannot safely restart the desktop application. Quit DSH completely, then open it again.",
274
+ versionRefresh: "Refresh page",
275
+ versionLater: "Later"
238
276
  };
239
277
  const STYLE = `
240
278
  .packyCodeSection{display:flex;flex-direction:column;gap:16px;font-size:13px;line-height:20px}
@@ -269,6 +307,9 @@ window.__ModuleLoader__.load({
269
307
  .packyCodeWarning{color:var(--dsw-alias-state-warning-primary,#c27c0e);font-weight:500}
270
308
  .packyCodeDetail{margin:0;padding:8px 10px;border-radius:8px;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px;white-space:pre-wrap;word-break:break-all;max-height:160px;overflow:auto}
271
309
  .packyCodeMono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;word-break:break-all}
310
+ .packyCodeVersionRows{display:flex;flex-direction:column;gap:6px}
311
+ .packyCodeVersionRow{display:flex;justify-content:space-between;gap:12px}
312
+ .packyCodeVersionValue{font-variant-numeric:tabular-nums;font-weight:600}
272
313
  .packyCodeComposer{display:inline-flex;align-items:center;flex:0 0 auto;height:28px;box-sizing:border-box;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:20px;font-weight:500;font-variant-numeric:tabular-nums;white-space:nowrap;user-select:none}
273
314
  @media(max-width:640px){.packyCodeRow{align-items:flex-start;flex-direction:column}.packyCodeActions{width:100%}}
274
315
  `;
@@ -451,6 +492,156 @@ window.__ModuleLoader__.load({
451
492
  ]
452
493
  });
453
494
  }
495
+ function VersionCard({ call, t }) {
496
+ const [info, setInfo] = react.useState();
497
+ const [loading, setLoading] = react.useState(true);
498
+ const [error, setError] = react.useState(false);
499
+ const [updating, setUpdating] = react.useState(false);
500
+ const [updateFailed, setUpdateFailed] = react.useState(false);
501
+ const [updated, setUpdated] = react.useState();
502
+ const [restartHint, setRestartHint] = react.useState(false);
503
+ const generation = react.useRef(0);
504
+ const load = (force) => {
505
+ const current = ++generation.current;
506
+ setLoading(true);
507
+ setError(false);
508
+ call("plugin/version", { force }).then((value) => {
509
+ if (generation.current === current) setInfo(value);
510
+ }).catch(() => {
511
+ if (generation.current === current) setError(true);
512
+ }).finally(() => {
513
+ if (generation.current === current) setLoading(false);
514
+ });
515
+ };
516
+ react.useEffect(() => {
517
+ load(false);
518
+ return () => {
519
+ generation.current += 1;
520
+ };
521
+ }, []);
522
+ const update = () => {
523
+ setUpdating(true);
524
+ setUpdateFailed(false);
525
+ call("plugin/update").then((value) => {
526
+ setUpdated(value);
527
+ setRestartHint(false);
528
+ }).catch(() => setUpdateFailed(true)).finally(() => setUpdating(false));
529
+ };
530
+ const installKind = info?.install?.kind;
531
+ const showStatus = info !== void 0 && updated === void 0;
532
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
533
+ className: "packyCodeCard",
534
+ children: [
535
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
536
+ className: "packyCodeCardHeader",
537
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
538
+ className: "packyCodeCardTitle",
539
+ children: t("versionTitle")
540
+ }), updated === void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
541
+ type: "button",
542
+ className: "packyCodeButton",
543
+ disabled: loading || updating,
544
+ onClick: () => load(true),
545
+ children: loading ? t("versionChecking") : t("versionCheck")
546
+ })]
547
+ }),
548
+ loading && info === void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
549
+ className: "packyCodeNote",
550
+ role: "status",
551
+ children: t("versionChecking")
552
+ }),
553
+ error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
554
+ className: "packyCodeError",
555
+ role: "alert",
556
+ children: t("versionFailed")
557
+ }),
558
+ info !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
559
+ className: "packyCodeVersionRows",
560
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
561
+ className: "packyCodeVersionRow",
562
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("versionCurrent") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
563
+ className: "packyCodeVersionValue",
564
+ children: ["v", info.current]
565
+ })]
566
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
567
+ className: "packyCodeVersionRow",
568
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("versionLatest") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
569
+ className: "packyCodeVersionValue",
570
+ children: ["v", info.latest]
571
+ })]
572
+ })]
573
+ }),
574
+ showStatus && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
575
+ className: "packyCodeNote",
576
+ role: "status",
577
+ children: info.updateAvailable ? t("versionAvailable").replace("{version}", info.latest) : t("versionUpToDate")
578
+ }),
579
+ showStatus && info.updateAvailable && installKind === "npm" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
580
+ className: "packyCodeActions",
581
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
582
+ type: "button",
583
+ className: "packyCodeButton packyCodeButtonPrimary",
584
+ disabled: updating,
585
+ onClick: update,
586
+ children: updating ? t("versionUpdating") : t("versionUpdate")
587
+ })
588
+ }),
589
+ showStatus && installKind === "link" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
590
+ className: "packyCodeNote",
591
+ children: t("versionLinked")
592
+ }),
593
+ showStatus && info.updateAvailable && installKind === "unknown" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
594
+ className: "packyCodeNote",
595
+ children: t("versionManual")
596
+ }),
597
+ updateFailed && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
598
+ className: "packyCodeError",
599
+ role: "alert",
600
+ children: t("versionUpdateFailed")
601
+ }),
602
+ updated !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
603
+ role: "status",
604
+ children: [
605
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
606
+ className: "packyCodeNote",
607
+ children: t("versionUpdated").replace("{version}", updated.version)
608
+ }),
609
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
610
+ className: "packyCodeNote",
611
+ children: t("versionUpdatedHint")
612
+ }),
613
+ restartHint && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
614
+ className: "packyCodeNote",
615
+ children: t("versionRestartHint")
616
+ }),
617
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
618
+ className: "packyCodeActions",
619
+ children: [
620
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
621
+ type: "button",
622
+ className: "packyCodeButton",
623
+ onClick: () => setRestartHint(true),
624
+ children: t("versionRestart")
625
+ }),
626
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
627
+ type: "button",
628
+ className: "packyCodeButton packyCodeButtonPrimary",
629
+ onClick: () => window.location.reload(),
630
+ children: t("versionRefresh")
631
+ }),
632
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
633
+ type: "button",
634
+ className: "packyCodeButton",
635
+ onClick: () => setUpdated(void 0),
636
+ children: t("versionLater")
637
+ })
638
+ ]
639
+ })
640
+ ]
641
+ })
642
+ ]
643
+ });
644
+ }
454
645
  function PackyCodeSection({ rpc, t }) {
455
646
  const call = react.useMemo(() => (endpoint, payload = {}) => rpc.call(CHANNEL, endpoint, payload).then(unwrap), [rpc]);
456
647
  const [status, setStatus] = react.useState();
@@ -555,6 +746,10 @@ window.__ModuleLoader__.load({
555
746
  className: "packyCodeIntro",
556
747
  children: t("intro")
557
748
  }),
749
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VersionCard, {
750
+ call,
751
+ t
752
+ }),
558
753
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ClaudeCliCard, {
559
754
  call,
560
755
  t
package/lib/index.js CHANGED
@@ -8,6 +8,8 @@ import { homedir, tmpdir } from "node:os";
8
8
  import { spawn } from "node:child_process";
9
9
  import { existsSync, readFileSync, readdirSync } from "node:fs";
10
10
  import { credentialKey } from "@deepseek-ai/dsh-credentials";
11
+ import { readFile, readdir, realpath } from "node:fs/promises";
12
+ import { fileURLToPath } from "node:url";
11
13
  import { createProvider } from "@earendil-works/pi-ai";
12
14
  import { anthropicMessagesApi } from "@earendil-works/pi-ai/api/anthropic-messages.lazy";
13
15
  import { openAICompletionsApi } from "@earendil-works/pi-ai/api/openai-completions.lazy";
@@ -67,7 +69,7 @@ const DEFAULT_REASONING_LEVEL = "high";
67
69
  * @module dsh-packycode-api/balance
68
70
  */
69
71
  const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
70
- const clone = (value) => structuredClone(value);
72
+ const clone$1 = (value) => structuredClone(value);
71
73
  function number(value) {
72
74
  if (typeof value === "number" && Number.isFinite(value)) return value;
73
75
  if (typeof value === "string" && value.trim() !== "") {
@@ -274,7 +276,7 @@ function createBalanceReader({ listKeys, getHosts, fetchImpl = fetch, now = Date
274
276
  return Object.freeze({
275
277
  async read({ force = false, signal } = {}) {
276
278
  signal?.throwIfAborted();
277
- if (!force && cached !== void 0 && now() - cached.fetchedAt < ttlMs) return clone(cached);
279
+ if (!force && cached !== void 0 && now() - cached.fetchedAt < ttlMs) return clone$1(cached);
278
280
  const createPending = (requestSignal) => {
279
281
  const observedGeneration = generation;
280
282
  return load(requestSignal).then((value) => {
@@ -282,7 +284,7 @@ function createBalanceReader({ listKeys, getHosts, fetchImpl = fetch, now = Date
282
284
  return value;
283
285
  });
284
286
  };
285
- if (force) return clone(await createPending(signal));
287
+ if (force) return clone$1(await createPending(signal));
286
288
  const pending = inFlight ?? createPending();
287
289
  if (inFlight === void 0) {
288
290
  inFlight = pending;
@@ -291,7 +293,7 @@ function createBalanceReader({ listKeys, getHosts, fetchImpl = fetch, now = Date
291
293
  };
292
294
  pending.then(clear, clear);
293
295
  }
294
- return clone(await pending);
296
+ return clone$1(await pending);
295
297
  },
296
298
  invalidate() {
297
299
  generation += 1;
@@ -1960,6 +1962,383 @@ function capClaudeMode(requested, ceiling) {
1960
1962
  return RANK[requested] > RANK[ceiling] ? ceiling : requested;
1961
1963
  }
1962
1964
  //#endregion
1965
+ //#region src/plugin-version.js
1966
+ /**
1967
+ * Version self-knowledge for the settings panel.
1968
+ *
1969
+ * The running version comes from this package. The latest version comes from
1970
+ * the npm registry. An in-place update is only offered for a registry install,
1971
+ * and it installs that exact version with the host's own pnpm — never through
1972
+ * `dsh plugin --profile desktop`, which the desktop app refuses. Registry
1973
+ * bodies and package-manager output stay host-side.
1974
+ *
1975
+ * @module dsh-packycode-api/plugin-version
1976
+ */
1977
+ const PACKAGE_NAME = "dsh-packycode-api";
1978
+ const NPM_REGISTRY_LATEST_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
1979
+ const DEFAULT_VERSION_TTL_MS = 5 * 6e4;
1980
+ const DEFAULT_VERSION_TIMEOUT_MS = 1e4;
1981
+ const DEFAULT_UPDATE_TIMEOUT_MS = 18e4;
1982
+ const clone = (value) => structuredClone(value);
1983
+ /**
1984
+ * Resolve fetch at call time. A sibling plugin may wrap `globalThis.fetch`
1985
+ * and later dismantle that wrapper; a binding captured during `apply()` would
1986
+ * keep the dead wrapper for the process lifetime.
1987
+ */
1988
+ function ambientFetch(input, init) {
1989
+ const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : void 0));
1990
+ if (!headers.has("accept-encoding")) headers.set("accept-encoding", "identity");
1991
+ return globalThis.fetch(input, {
1992
+ ...init,
1993
+ headers
1994
+ });
1995
+ }
1996
+ /** One caller's cancellation must not abort the shared registry read. */
1997
+ function waitForSharedRequest(promise, signal) {
1998
+ if (signal === void 0) return promise;
1999
+ signal.throwIfAborted();
2000
+ return new Promise((resolve, reject) => {
2001
+ const onAbort = () => reject(signal.reason);
2002
+ signal.addEventListener("abort", onAbort, { once: true });
2003
+ promise.then((value) => {
2004
+ signal.removeEventListener("abort", onAbort);
2005
+ resolve(value);
2006
+ }, (error) => {
2007
+ signal.removeEventListener("abort", onAbort);
2008
+ reject(error);
2009
+ });
2010
+ });
2011
+ }
2012
+ /** Parse a strict `x.y.z[-pre][+build]` version; anything else is undefined. */
2013
+ function parseSemver(value) {
2014
+ if (typeof value !== "string") return void 0;
2015
+ const match = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/u.exec(value.trim());
2016
+ if (match === null) return void 0;
2017
+ return {
2018
+ major: Number(match[1]),
2019
+ minor: Number(match[2]),
2020
+ patch: Number(match[3]),
2021
+ pre: match[4]
2022
+ };
2023
+ }
2024
+ /** Compare two versions: -1/0/1, or undefined when either side is not semver. */
2025
+ function compareSemver(a, b) {
2026
+ const left = parseSemver(a);
2027
+ const right = parseSemver(b);
2028
+ if (left === void 0 || right === void 0) return void 0;
2029
+ for (const key of [
2030
+ "major",
2031
+ "minor",
2032
+ "patch"
2033
+ ]) if (left[key] !== right[key]) return left[key] < right[key] ? -1 : 1;
2034
+ if (left.pre === right.pre) return 0;
2035
+ if (left.pre === void 0) return 1;
2036
+ if (right.pre === void 0) return -1;
2037
+ return left.pre < right.pre ? -1 : 1;
2038
+ }
2039
+ /**
2040
+ * Classify how the running package reached the profile. Only registry specs
2041
+ * may be updated in place; local checkouts belong to the owner's iteration.
2042
+ */
2043
+ function classifySpec(spec) {
2044
+ if (typeof spec !== "string" || spec === "") return "unknown";
2045
+ if (/^(?:link|file):/u.test(spec)) return "link";
2046
+ if (/^(?:\d|[~^*])/u.test(spec)) return "npm";
2047
+ return "unknown";
2048
+ }
2049
+ const readManifest = async (path) => {
2050
+ try {
2051
+ const parsed = JSON.parse(await readFile(path, "utf8"));
2052
+ return parsed !== null && typeof parsed === "object" ? parsed : void 0;
2053
+ } catch {
2054
+ return;
2055
+ }
2056
+ };
2057
+ /**
2058
+ * A zero exit only proves the command ran. pnpm can skip a too-new version
2059
+ * and still exit zero, so the landed manifest is what makes the answer true.
2060
+ */
2061
+ async function confirmInstalled({ profileDir, expected, settleDelayMs, attempts = 3 }) {
2062
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
2063
+ if ((await readManifest(join(profileDir, "node_modules", "dsh-packycode-api", "package.json")))?.version === expected) return;
2064
+ if (attempt < attempts) await new Promise((resolve) => {
2065
+ setTimeout(resolve, settleDelayMs);
2066
+ });
2067
+ }
2068
+ throw new Error("PackyCode plugin update did not install the requested version");
2069
+ }
2070
+ /**
2071
+ * Find the profile that owns this running installation. pnpm symlinks resolve
2072
+ * to the same realpath on both sides. When no realpath matches, fall back to
2073
+ * the only profile that declares the dependency at all.
2074
+ */
2075
+ async function findInstall({ dshHome, ownPackageJsonUrl }) {
2076
+ let ownReal;
2077
+ try {
2078
+ ownReal = await realpath(fileURLToPath(ownPackageJsonUrl));
2079
+ } catch {
2080
+ ownReal = void 0;
2081
+ }
2082
+ let entries = [];
2083
+ try {
2084
+ entries = await readdir(join(dshHome, "profiles"), { withFileTypes: true });
2085
+ } catch {
2086
+ entries = [];
2087
+ }
2088
+ const declared = [];
2089
+ for (const entry of entries) {
2090
+ if (!entry.isDirectory()) continue;
2091
+ const profileDir = join(dshHome, "profiles", entry.name);
2092
+ const spec = (await readManifest(join(profileDir, "package.json")))?.dependencies?.[PACKAGE_NAME];
2093
+ if (typeof spec !== "string") continue;
2094
+ declared.push({
2095
+ profile: entry.name,
2096
+ spec
2097
+ });
2098
+ if (ownReal === void 0) continue;
2099
+ try {
2100
+ if (await realpath(join(profileDir, "node_modules", "dsh-packycode-api", "package.json")) === ownReal) return {
2101
+ kind: classifySpec(spec),
2102
+ profile: entry.name
2103
+ };
2104
+ } catch {}
2105
+ }
2106
+ if (declared.length === 1) return {
2107
+ kind: classifySpec(declared[0].spec),
2108
+ profile: declared[0].profile
2109
+ };
2110
+ return { kind: "unknown" };
2111
+ }
2112
+ const isExistingPath = (value) => typeof value === "string" && value !== "" && existsSync(value);
2113
+ /** Anchored to the file name so a directory that merely contains the word cannot match. */
2114
+ const PNPM_SCRIPT = /[\\/]pnpm\.(?:mjs|cjs|js)$/u;
2115
+ const dshRuntimeRoot = (value) => isExistingPath(value) && existsSync(join(value, "node_modules", "@deepseek-ai", "dsh", "package.json")) ? value : void 0;
2116
+ /**
2117
+ * Locate the package manager that owns this profile.
2118
+ *
2119
+ * `dsh plugin --profile desktop` is refused by the desktop app, so the update
2120
+ * installs with pnpm directly. The desktop host puts the bundled pnpm in its
2121
+ * own arguments; nothing is guessed from the entry script.
2122
+ */
2123
+ function resolvePnpm({ argv } = {}) {
2124
+ const entries = Array.isArray(argv) ? argv : [];
2125
+ for (const entry of entries) if (isExistingPath(entry) && PNPM_SCRIPT.test(entry)) return entry;
2126
+ for (const entry of entries) {
2127
+ const root = dshRuntimeRoot(entry);
2128
+ if (root === void 0) continue;
2129
+ const script = join(root, "..", "pnpm", "bin", "pnpm.mjs");
2130
+ if (existsSync(script)) return script;
2131
+ }
2132
+ }
2133
+ function resolveLauncher({ argv, execPath }) {
2134
+ const pnpm = resolvePnpm({ argv });
2135
+ if (pnpm === void 0) return {
2136
+ command: "pnpm",
2137
+ prefix: []
2138
+ };
2139
+ return {
2140
+ command: execPath,
2141
+ prefix: [pnpm]
2142
+ };
2143
+ }
2144
+ /** Run pnpm host-side. Its output is never returned to the browser. */
2145
+ function spawnRunCommand({ command, args, cwd }, { signal, timeoutMs }) {
2146
+ return new Promise((resolve, reject) => {
2147
+ if (signal?.aborted) {
2148
+ reject(signal.reason instanceof Error ? signal.reason : /* @__PURE__ */ new Error("PackyCode plugin update aborted"));
2149
+ return;
2150
+ }
2151
+ const controller = new AbortController();
2152
+ const onAbort = () => controller.abort(signal.reason);
2153
+ signal?.addEventListener("abort", onAbort, { once: true });
2154
+ const timer = setTimeout(() => controller.abort(/* @__PURE__ */ new Error("PackyCode plugin update timed out")), timeoutMs);
2155
+ let settled = false;
2156
+ const settle = (callback, value) => {
2157
+ if (settled) return;
2158
+ settled = true;
2159
+ clearTimeout(timer);
2160
+ signal?.removeEventListener("abort", onAbort);
2161
+ callback(value);
2162
+ };
2163
+ let child;
2164
+ try {
2165
+ child = spawn(command, args, {
2166
+ stdio: [
2167
+ "ignore",
2168
+ "pipe",
2169
+ "pipe"
2170
+ ],
2171
+ signal: controller.signal,
2172
+ ...cwd === void 0 ? {} : { cwd }
2173
+ });
2174
+ } catch (error) {
2175
+ settle(reject, error);
2176
+ return;
2177
+ }
2178
+ let stdout = "";
2179
+ let stderr = "";
2180
+ child.stdout?.on("data", (chunk) => {
2181
+ stdout = (stdout + chunk).slice(-8192);
2182
+ });
2183
+ child.stderr?.on("data", (chunk) => {
2184
+ stderr = (stderr + chunk).slice(-8192);
2185
+ });
2186
+ child.on("error", (error) => settle(reject, error));
2187
+ child.on("close", (code) => {
2188
+ if (controller.signal.aborted) {
2189
+ const reason = controller.signal.reason;
2190
+ settle(reject, reason instanceof Error ? reason : /* @__PURE__ */ new Error("PackyCode plugin update aborted"));
2191
+ return;
2192
+ }
2193
+ settle(resolve, {
2194
+ code: code ?? 1,
2195
+ stdout,
2196
+ stderr
2197
+ });
2198
+ });
2199
+ });
2200
+ }
2201
+ /**
2202
+ * The running version, the latest registry version, which profile installed
2203
+ * this copy, and registry-spec updates against that profile.
2204
+ */
2205
+ function createPackyCodePluginManager({ fetchImpl = ambientFetch, runCommand = spawnRunCommand, execPath = process.execPath, argv = process.argv, env = process.env, ownPackageJsonUrl = new URL("../package.json", import.meta.url), registryUrl = NPM_REGISTRY_LATEST_URL, now = Date.now, ttlMs = DEFAULT_VERSION_TTL_MS, timeoutMs = DEFAULT_VERSION_TIMEOUT_MS, updateTimeoutMs = DEFAULT_UPDATE_TIMEOUT_MS, settleDelayMs = 250 } = {}) {
2206
+ if (typeof fetchImpl !== "function") throw new Error("PackyCode plugin manager requires fetch");
2207
+ if (typeof runCommand !== "function") throw new Error("PackyCode plugin manager requires runCommand");
2208
+ const dshHome = typeof env.DSH_HOME === "string" && env.DSH_HOME !== "" ? env.DSH_HOME : join(homedir(), ".dsh");
2209
+ const launcher = resolveLauncher({
2210
+ argv,
2211
+ execPath
2212
+ });
2213
+ let cached;
2214
+ let inFlight;
2215
+ let generation = 0;
2216
+ let updating = false;
2217
+ const fetchLatest = async (signal) => {
2218
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
2219
+ const requestSignal = signal === void 0 ? timeoutSignal : AbortSignal.any([signal, timeoutSignal]);
2220
+ let response;
2221
+ try {
2222
+ response = await fetchImpl(registryUrl, {
2223
+ headers: { Accept: "application/json" },
2224
+ redirect: "error",
2225
+ signal: requestSignal
2226
+ });
2227
+ } catch (error) {
2228
+ if (signal?.aborted) throw error;
2229
+ throw new Error("PackyCode plugin version check failed");
2230
+ }
2231
+ if (!response.ok) throw new Error("PackyCode plugin version check failed");
2232
+ let version;
2233
+ try {
2234
+ version = (await response.json())?.version;
2235
+ } catch {
2236
+ version = void 0;
2237
+ }
2238
+ if (parseSemver(version) === void 0) throw new Error("PackyCode plugin version check failed");
2239
+ return version;
2240
+ };
2241
+ const readOwnVersion = async () => {
2242
+ const version = (await readManifest(fileURLToPath(ownPackageJsonUrl)))?.version;
2243
+ if (parseSemver(version) === void 0) throw new Error("PackyCode plugin version is unavailable");
2244
+ return version;
2245
+ };
2246
+ const load = async (signal) => {
2247
+ signal?.throwIfAborted();
2248
+ const [current, install, latest] = await Promise.all([
2249
+ readOwnVersion(),
2250
+ findInstall({
2251
+ dshHome,
2252
+ ownPackageJsonUrl
2253
+ }),
2254
+ fetchLatest(signal)
2255
+ ]);
2256
+ return {
2257
+ current,
2258
+ latest,
2259
+ updateAvailable: compareSemver(latest, current) === 1,
2260
+ install,
2261
+ fetchedAt: now()
2262
+ };
2263
+ };
2264
+ return Object.freeze({
2265
+ async read({ force = false, signal } = {}) {
2266
+ signal?.throwIfAborted();
2267
+ if (!force && cached !== void 0 && now() - cached.fetchedAt < ttlMs) return clone(cached);
2268
+ const createPending = (requestSignal) => {
2269
+ const observedGeneration = generation;
2270
+ return load(requestSignal).then((value) => {
2271
+ if (generation === observedGeneration) cached = value;
2272
+ return value;
2273
+ });
2274
+ };
2275
+ if (force) return clone(await createPending(signal));
2276
+ const pending = inFlight ?? createPending();
2277
+ if (inFlight === void 0) {
2278
+ inFlight = pending;
2279
+ const clear = () => {
2280
+ if (inFlight === pending) inFlight = void 0;
2281
+ };
2282
+ pending.then(clear, clear);
2283
+ }
2284
+ return clone(await waitForSharedRequest(pending, signal));
2285
+ },
2286
+ async update({ signal } = {}) {
2287
+ signal?.throwIfAborted();
2288
+ if (updating) throw new Error("PackyCode plugin update is already running");
2289
+ updating = true;
2290
+ try {
2291
+ const install = await findInstall({
2292
+ dshHome,
2293
+ ownPackageJsonUrl
2294
+ });
2295
+ if (install.kind === "link") throw new Error("PackyCode plugin is installed from a local checkout");
2296
+ if (install.kind !== "npm" || install.profile === void 0) throw new Error("PackyCode plugin update could not find the owning profile");
2297
+ const latest = await fetchLatest(signal);
2298
+ const profileDir = join(dshHome, "profiles", install.profile);
2299
+ let result;
2300
+ try {
2301
+ result = await runCommand({
2302
+ command: launcher.command,
2303
+ args: [
2304
+ ...launcher.prefix,
2305
+ "add",
2306
+ "--save-exact",
2307
+ `--config.minimumReleaseAge=0`,
2308
+ `${PACKAGE_NAME}@${latest}`
2309
+ ],
2310
+ cwd: profileDir
2311
+ }, {
2312
+ signal,
2313
+ timeoutMs: updateTimeoutMs
2314
+ });
2315
+ } catch (error) {
2316
+ if (signal?.aborted) throw error;
2317
+ throw new Error("PackyCode plugin update failed");
2318
+ }
2319
+ if (result.code !== 0) throw new Error("PackyCode plugin update failed");
2320
+ await confirmInstalled({
2321
+ profileDir,
2322
+ expected: latest,
2323
+ settleDelayMs
2324
+ });
2325
+ generation += 1;
2326
+ cached = void 0;
2327
+ return {
2328
+ version: latest,
2329
+ profile: install.profile
2330
+ };
2331
+ } finally {
2332
+ updating = false;
2333
+ }
2334
+ },
2335
+ invalidate() {
2336
+ generation += 1;
2337
+ cached = void 0;
2338
+ }
2339
+ });
2340
+ }
2341
+ //#endregion
1963
2342
  //#region src/catalog.js
1964
2343
  /**
1965
2344
  * The built-in PackyCode model catalog.
@@ -2368,11 +2747,15 @@ function normalizeHost(value, fallback) {
2368
2747
  * Claude Code CLI manager (optional: without it the CLI endpoints refuse).
2369
2748
  * @returns a handler for `connection.rpc.handle(CHANNEL, handler)`.
2370
2749
  */
2371
- function createPackyCodeRpc({ ring, balance, hosts, preference, claudeCli }) {
2750
+ function createPackyCodeRpc({ ring, balance, hosts, preference, claudeCli, pluginManager }) {
2372
2751
  const requireCli = () => {
2373
2752
  if (claudeCli === void 0) throw new Error("PackyCode Claude Code CLI support is unavailable in this runtime");
2374
2753
  return claudeCli;
2375
2754
  };
2755
+ const requireManager = () => {
2756
+ if (pluginManager === void 0) throw new Error("PackyCode plugin version is unavailable");
2757
+ return pluginManager;
2758
+ };
2376
2759
  const status = async () => ({
2377
2760
  ...await ring.list(),
2378
2761
  hosts: hosts(),
@@ -2382,7 +2765,7 @@ function createPackyCodeRpc({ ring, balance, hosts, preference, claudeCli }) {
2382
2765
  statusHost: DEFAULT_STATUS_HOST
2383
2766
  }
2384
2767
  });
2385
- return async (endpoint, payload) => {
2768
+ return async (endpoint, payload, signal) => {
2386
2769
  try {
2387
2770
  switch (endpoint) {
2388
2771
  case "status": return ok(await status());
@@ -2431,6 +2814,11 @@ function createPackyCodeRpc({ ring, balance, hosts, preference, claudeCli }) {
2431
2814
  cli.install();
2432
2815
  return ok(await cli.status());
2433
2816
  }
2817
+ case "plugin/version": return ok(await requireManager().read({
2818
+ force: payload?.force === true,
2819
+ signal
2820
+ }));
2821
+ case "plugin/update": return ok(await requireManager().update({ signal }));
2434
2822
  default: return failed("Unknown PackyCode endpoint");
2435
2823
  }
2436
2824
  } catch (error) {
@@ -2721,7 +3109,8 @@ function apply(ctx, config) {
2721
3109
  balance,
2722
3110
  hosts,
2723
3111
  preference,
2724
- claudeCli
3112
+ claudeCli,
3113
+ pluginManager: createPackyCodePluginManager()
2725
3114
  });
2726
3115
  /**
2727
3116
  * Connection registers a channel through `owner.webServer`, read on its own
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-packycode-api",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "packageManager": "pnpm@10.32.1",
5
5
  "description": "Use a PackyCode (PackyAPI) API key in DeepSeek Harness through a built-in multi-protocol model route with key rotation and balance display",
6
6
  "type": "module",