dsh-theone 0.3.15 → 0.3.16

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.md CHANGED
@@ -57,7 +57,7 @@ Each project gets its own background session that reasons, runs tools and compac
57
57
  2. Open **Plugins → Add plugin**, paste `https://github.com/YunongDai2005/dsh-theone`, and install.
58
58
  3. Click **TheOne · Main chat** in the sidebar and start talking.
59
59
 
60
- From the command line: `dsh plugin --profile web add github:YunongDai2005/dsh-theone --ignore-scripts`. Works with DSH `0.2.0-rc.2` and Node.js 24; the interface follows DSH's language (English / Simplified Chinese). Installing `dsh-theone` from npm also works, but DSH's package manager only accepts a version once it has been published for 24 hours, so the GitHub address gets new versions first.
60
+ From the command line: `dsh plugin --profile web add github:YunongDai2005/dsh-theone --ignore-scripts`. Works with DSH `0.2.0-rc.2` and Node.js 24; the interface follows DSH's language (English / Simplified Chinese). Installing `dsh-theone` from npm also works, but DSH's package manager only accepts a version once it has been published for 24 hours, so the GitHub address gets new versions first. When an update is newer than that, the Update button explains it and can exempt TheOne alone so it installs right away.
61
61
 
62
62
  > Unofficial community project, maintained independently. It is not affiliated with or endorsed by DeepSeek.
63
63
 
package/README.zh.md CHANGED
@@ -57,7 +57,7 @@
57
57
  2. 打开 **插件 → 添加插件**,粘贴 `https://github.com/YunongDai2005/dsh-theone`,点击安装。
58
58
  3. 点击左侧 **TheOne · 主聊天**,开始说话。
59
59
 
60
- 命令行安装:`dsh plugin --profile web add github:YunongDai2005/dsh-theone --ignore-scripts`。兼容 DSH `0.2.0-rc.2` 与 Node.js 24;界面跟随 DSH 的语言(简体中文 / English)。也可以从 npm 安装 `dsh-theone`,但 DSH 的包管理器只接受发布满 24 小时的版本,所以用 GitHub 地址安装能最早拿到新版本。
60
+ 命令行安装:`dsh plugin --profile web add github:YunongDai2005/dsh-theone --ignore-scripts`。兼容 DSH `0.2.0-rc.2` 与 Node.js 24;界面跟随 DSH 的语言(简体中文 / English)。也可以从 npm 安装 `dsh-theone`,但 DSH 的包管理器只接受发布满 24 小时的版本,所以用 GitHub 地址安装能最早拿到新版本。遇到这种情况,「更新」按钮会说明原因,并可以只为 TheOne 放行、立即安装。
61
61
 
62
62
  > 非官方社区项目,由社区成员独立维护,与 DeepSeek 不存在隶属或背书关系。
63
63
 
@@ -17,6 +17,16 @@ export declare const zh: {
17
17
  readonly 'update.available': "更新";
18
18
  readonly 'update.installing': "更新中…";
19
19
  readonly 'update.restart': "重启生效";
20
+ readonly 'update.waiting': "新版本";
21
+ readonly 'update.waitingHint': "有新版本 {latest},发布还不满 24 小时。点击查看如何现在安装。";
22
+ readonly 'age.title': "新版本发布还不满 24 小时";
23
+ readonly 'age.why': "DSH 用 pnpm 安装插件,它默认只安装发布满 24 小时的版本,用来防范被篡改的新包。TheOne {version} 刚发布不久,所以暂时装不上。";
24
+ readonly 'age.allowHow': "你可以只为 TheOne 放行:在这个 DSH 配置的 pnpm 设置(pnpm-workspace.yaml)里把 dsh-theone 加入例外,然后立即安装。其他插件仍然受 24 小时规则保护。";
25
+ readonly 'age.cannot': "当前无法自动修改这个 DSH 配置的 pnpm 设置。可以等它满 24 小时,或改用 GitHub 地址重新安装。";
26
+ readonly 'age.readyAt': "不放行的话,{time} 之后就能直接更新。";
27
+ readonly 'age.wait': "等满 24 小时";
28
+ readonly 'age.allow': "放行并更新";
29
+ readonly 'age.ok': "知道了";
20
30
  readonly 'update.reloading': "正在重新加载…";
21
31
  readonly 'update.reloadingHint': "TheOne 正在用新版本重新加载,DSH 不需要重启;完成后页面会自动刷新。";
22
32
  readonly 'update.busy': "有回复正在进行,等它结束后再点更新。";
@@ -17,6 +17,16 @@ export const zh = {
17
17
  'update.available': '更新',
18
18
  'update.installing': '更新中…',
19
19
  'update.restart': '重启生效',
20
+ 'update.waiting': "新版本",
21
+ 'update.waitingHint': "有新版本 {latest},发布还不满 24 小时。点击查看如何现在安装。",
22
+ 'age.title': "新版本发布还不满 24 小时",
23
+ 'age.why': "DSH 用 pnpm 安装插件,它默认只安装发布满 24 小时的版本,用来防范被篡改的新包。TheOne {version} 刚发布不久,所以暂时装不上。",
24
+ 'age.allowHow': "你可以只为 TheOne 放行:在这个 DSH 配置的 pnpm 设置(pnpm-workspace.yaml)里把 dsh-theone 加入例外,然后立即安装。其他插件仍然受 24 小时规则保护。",
25
+ 'age.cannot': "当前无法自动修改这个 DSH 配置的 pnpm 设置。可以等它满 24 小时,或改用 GitHub 地址重新安装。",
26
+ 'age.readyAt': "不放行的话,{time} 之后就能直接更新。",
27
+ 'age.wait': "等满 24 小时",
28
+ 'age.allow': "放行并更新",
29
+ 'age.ok': "知道了",
20
30
  'update.reloading': '正在重新加载…',
21
31
  'update.reloadingHint': 'TheOne 正在用新版本重新加载,DSH 不需要重启;完成后页面会自动刷新。',
22
32
  'update.busy': '有回复正在进行,等它结束后再点更新。',
@@ -209,6 +219,16 @@ export const en = {
209
219
  'update.available': 'Update',
210
220
  'update.installing': 'Updating…',
211
221
  'update.restart': 'Restart to apply',
222
+ 'update.waiting': "New version",
223
+ 'update.waitingHint': "{latest} is out but less than 24 hours old. Click to see how to install it now.",
224
+ 'age.title': "This version is less than 24 hours old",
225
+ 'age.why': "DSH installs plugins with pnpm, which by default only installs versions published at least 24 hours ago, as a guard against tampered packages. TheOne {version} was published recently, so it cannot be installed yet.",
226
+ 'age.allowHow': "You can exempt TheOne only: dsh-theone is added to the exceptions in this DSH profile's pnpm settings (pnpm-workspace.yaml) and installed now. Other plugins keep the 24-hour rule.",
227
+ 'age.cannot': "This DSH profile's pnpm settings cannot be changed from here. Wait 24 hours, or reinstall from the GitHub address.",
228
+ 'age.readyAt': "Without the exemption, you can update after {time}.",
229
+ 'age.wait': "Wait 24 hours",
230
+ 'age.allow': "Exempt and update",
231
+ 'age.ok': "OK",
212
232
  'update.reloading': 'Reloading…',
213
233
  'update.reloadingHint': 'TheOne is reloading with the new version; DSH keeps running, and the page refreshes when it is done.',
214
234
  'update.busy': 'A reply is in progress. Update once it finishes.',
package/dist/client.js CHANGED
@@ -100,6 +100,16 @@ var zh = {
100
100
  "update.available": "\u66F4\u65B0",
101
101
  "update.installing": "\u66F4\u65B0\u4E2D\u2026",
102
102
  "update.restart": "\u91CD\u542F\u751F\u6548",
103
+ "update.waiting": "\u65B0\u7248\u672C",
104
+ "update.waitingHint": "\u6709\u65B0\u7248\u672C {latest}\uFF0C\u53D1\u5E03\u8FD8\u4E0D\u6EE1 24 \u5C0F\u65F6\u3002\u70B9\u51FB\u67E5\u770B\u5982\u4F55\u73B0\u5728\u5B89\u88C5\u3002",
105
+ "age.title": "\u65B0\u7248\u672C\u53D1\u5E03\u8FD8\u4E0D\u6EE1 24 \u5C0F\u65F6",
106
+ "age.why": "DSH \u7528 pnpm \u5B89\u88C5\u63D2\u4EF6\uFF0C\u5B83\u9ED8\u8BA4\u53EA\u5B89\u88C5\u53D1\u5E03\u6EE1 24 \u5C0F\u65F6\u7684\u7248\u672C\uFF0C\u7528\u6765\u9632\u8303\u88AB\u7BE1\u6539\u7684\u65B0\u5305\u3002TheOne {version} \u521A\u53D1\u5E03\u4E0D\u4E45\uFF0C\u6240\u4EE5\u6682\u65F6\u88C5\u4E0D\u4E0A\u3002",
107
+ "age.allowHow": "\u4F60\u53EF\u4EE5\u53EA\u4E3A TheOne \u653E\u884C\uFF1A\u5728\u8FD9\u4E2A DSH \u914D\u7F6E\u7684 pnpm \u8BBE\u7F6E\uFF08pnpm-workspace.yaml\uFF09\u91CC\u628A dsh-theone \u52A0\u5165\u4F8B\u5916\uFF0C\u7136\u540E\u7ACB\u5373\u5B89\u88C5\u3002\u5176\u4ED6\u63D2\u4EF6\u4ECD\u7136\u53D7 24 \u5C0F\u65F6\u89C4\u5219\u4FDD\u62A4\u3002",
108
+ "age.cannot": "\u5F53\u524D\u65E0\u6CD5\u81EA\u52A8\u4FEE\u6539\u8FD9\u4E2A DSH \u914D\u7F6E\u7684 pnpm \u8BBE\u7F6E\u3002\u53EF\u4EE5\u7B49\u5B83\u6EE1 24 \u5C0F\u65F6\uFF0C\u6216\u6539\u7528 GitHub \u5730\u5740\u91CD\u65B0\u5B89\u88C5\u3002",
109
+ "age.readyAt": "\u4E0D\u653E\u884C\u7684\u8BDD\uFF0C{time} \u4E4B\u540E\u5C31\u80FD\u76F4\u63A5\u66F4\u65B0\u3002",
110
+ "age.wait": "\u7B49\u6EE1 24 \u5C0F\u65F6",
111
+ "age.allow": "\u653E\u884C\u5E76\u66F4\u65B0",
112
+ "age.ok": "\u77E5\u9053\u4E86",
103
113
  "update.reloading": "\u6B63\u5728\u91CD\u65B0\u52A0\u8F7D\u2026",
104
114
  "update.reloadingHint": "TheOne \u6B63\u5728\u7528\u65B0\u7248\u672C\u91CD\u65B0\u52A0\u8F7D\uFF0CDSH \u4E0D\u9700\u8981\u91CD\u542F\uFF1B\u5B8C\u6210\u540E\u9875\u9762\u4F1A\u81EA\u52A8\u5237\u65B0\u3002",
105
115
  "update.busy": "\u6709\u56DE\u590D\u6B63\u5728\u8FDB\u884C\uFF0C\u7B49\u5B83\u7ED3\u675F\u540E\u518D\u70B9\u66F4\u65B0\u3002",
@@ -292,6 +302,16 @@ var en = {
292
302
  "update.available": "Update",
293
303
  "update.installing": "Updating\u2026",
294
304
  "update.restart": "Restart to apply",
305
+ "update.waiting": "New version",
306
+ "update.waitingHint": "{latest} is out but less than 24 hours old. Click to see how to install it now.",
307
+ "age.title": "This version is less than 24 hours old",
308
+ "age.why": "DSH installs plugins with pnpm, which by default only installs versions published at least 24 hours ago, as a guard against tampered packages. TheOne {version} was published recently, so it cannot be installed yet.",
309
+ "age.allowHow": "You can exempt TheOne only: dsh-theone is added to the exceptions in this DSH profile's pnpm settings (pnpm-workspace.yaml) and installed now. Other plugins keep the 24-hour rule.",
310
+ "age.cannot": "This DSH profile's pnpm settings cannot be changed from here. Wait 24 hours, or reinstall from the GitHub address.",
311
+ "age.readyAt": "Without the exemption, you can update after {time}.",
312
+ "age.wait": "Wait 24 hours",
313
+ "age.allow": "Exempt and update",
314
+ "age.ok": "OK",
295
315
  "update.reloading": "Reloading\u2026",
296
316
  "update.reloadingHint": "TheOne is reloading with the new version; DSH keeps running, and the page refreshes when it is done.",
297
317
  "update.busy": "A reply is in progress. Update once it finishes.",
@@ -555,9 +575,14 @@ function apply(ctx) {
555
575
  updateListeners.delete(listener);
556
576
  };
557
577
  };
558
- const readUpdate = async (method = "GET") => {
578
+ const readUpdate = async (method = "GET", body) => {
559
579
  try {
560
- const response = await fetch("/api/theone/update", { method, signal: lifetime.signal, cache: "no-store" });
580
+ const response = await fetch("/api/theone/update", {
581
+ method,
582
+ signal: lifetime.signal,
583
+ cache: "no-store",
584
+ ...body ? { headers: { "content-type": "application/json" }, body: JSON.stringify(body) } : {}
585
+ });
561
586
  if (response.ok || response.status === 409) setUpdate(await response.json());
562
587
  if (update?.state === "reloading") void awaitReload(update.current);
563
588
  } catch {
@@ -584,16 +609,78 @@ function apply(ctx) {
584
609
  }, 6 * 36e5);
585
610
  return () => clearInterval(timer);
586
611
  });
612
+ function openReleaseAgeDialog(status) {
613
+ const backdrop = document.createElement("div");
614
+ backdrop.className = "theone-dialog-backdrop";
615
+ backdrop.setAttribute("translate", "no");
616
+ const card = document.createElement("div");
617
+ card.className = "theone-dialog";
618
+ card.setAttribute("role", "dialog");
619
+ card.setAttribute("aria-modal", "true");
620
+ const title = document.createElement("h2");
621
+ title.id = "theone-dialog-title";
622
+ title.textContent = t("age.title");
623
+ card.setAttribute("aria-labelledby", title.id);
624
+ const paragraph = (text, className) => {
625
+ const p = document.createElement("p");
626
+ p.textContent = text;
627
+ if (className) p.className = className;
628
+ return p;
629
+ };
630
+ const version = status.waiting?.version ?? status.latest ?? "";
631
+ card.append(title, paragraph(t("age.why", { version })), paragraph(t(status.canExempt ? "age.allowHow" : "age.cannot")));
632
+ if (status.waiting) card.append(paragraph(t("age.readyAt", { time: new Date(status.waiting.readyAt).toLocaleString(localeSnapshot().active, { month: "numeric", day: "numeric", hour: "2-digit", minute: "2-digit" }) }), "theone-dialog-note"));
633
+ const footer = document.createElement("div");
634
+ footer.className = "theone-dialog-actions";
635
+ const button = (text, primary, act) => {
636
+ const element = document.createElement("button");
637
+ element.type = "button";
638
+ element.textContent = text;
639
+ if (primary) element.className = "theone-dialog-primary";
640
+ element.addEventListener("click", act);
641
+ footer.append(element);
642
+ return element;
643
+ };
644
+ const close = () => {
645
+ backdrop.remove();
646
+ document.removeEventListener("keydown", keydown, true);
647
+ };
648
+ const keydown = (event) => {
649
+ if (event.key === "Escape") {
650
+ event.preventDefault();
651
+ close();
652
+ }
653
+ };
654
+ button(t(status.canExempt ? "age.wait" : "age.ok"), !status.canExempt, close);
655
+ const allow = status.canExempt ? button(t("age.allow"), true, () => {
656
+ close();
657
+ setUpdate({ ...status, state: "installing", error: void 0 });
658
+ void readUpdate("POST", { allowFresh: true });
659
+ }) : void 0;
660
+ card.append(footer);
661
+ backdrop.append(card);
662
+ backdrop.addEventListener("pointerdown", (event) => {
663
+ if (event.target === backdrop) close();
664
+ });
665
+ document.addEventListener("keydown", keydown, true);
666
+ document.body.append(backdrop);
667
+ (allow ?? footer.querySelector("button"))?.focus();
668
+ }
587
669
  function UpdateButton() {
588
670
  const t2 = useText();
589
671
  const status = (0, import_react.useSyncExternalStore)(subscribeUpdate, () => update);
590
- if (!status || !status.available && !status.state) return null;
591
- const label = status.state === "installing" ? t2("update.installing") : status.state === "reloading" ? t2("update.reloading") : status.state === "restart" ? t2("update.restart") : status.state === "failed" ? t2("update.failed") : t2("update.available");
592
- const title = status.error === "GATEWAY_BUSY" ? t2("update.busy") : status.state === "reloading" ? t2("update.reloadingHint") : status.state === "restart" ? t2("update.restartHint") : status.state === "failed" ? status.error === "MINIMUM_RELEASE_AGE" ? t2("update.tooNew") : status.error === "NETWORK" ? t2("update.network") : t2("update.failedHint", { error: status.error ?? "" }) : status.installable ? t2("update.hint", { current: status.current, latest: status.latest ?? "" }) : t2("update.manualHint", { current: status.current, latest: status.latest ?? "" });
672
+ const waiting = !!status?.waiting && !status.available && !status.state;
673
+ if (!status || !status.available && !status.state && !waiting) return null;
674
+ const label = waiting ? t2("update.waiting") : status.state === "installing" ? t2("update.installing") : status.state === "reloading" ? t2("update.reloading") : status.state === "restart" ? t2("update.restart") : status.state === "failed" ? t2("update.failed") : t2("update.available");
675
+ const title = waiting ? t2("update.waitingHint", { latest: status.waiting.version }) : status.error === "GATEWAY_BUSY" ? t2("update.busy") : status.state === "reloading" ? t2("update.reloadingHint") : status.state === "restart" ? t2("update.restartHint") : status.state === "failed" ? status.error === "MINIMUM_RELEASE_AGE" ? t2("update.tooNew") : status.error === "NETWORK" ? t2("update.network") : t2("update.failedHint", { error: status.error ?? "" }) : status.installable ? t2("update.hint", { current: status.current, latest: status.latest ?? "" }) : t2("update.manualHint", { current: status.current, latest: status.latest ?? "" });
593
676
  const act = (event) => {
594
677
  event.preventDefault();
595
678
  event.stopPropagation();
596
679
  if (status.state === "installing" || status.state === "reloading" || status.state === "restart") return;
680
+ if (waiting || status.state === "failed" && status.error === "MINIMUM_RELEASE_AGE") {
681
+ openReleaseAgeDialog(status);
682
+ return;
683
+ }
597
684
  if (!status.installable) {
598
685
  window.open("https://github.com/YunongDai2005/dsh-theone#readme", "_blank", "noopener");
599
686
  return;
@@ -609,7 +696,7 @@ function apply(ctx) {
609
696
  tabIndex: 0,
610
697
  title,
611
698
  "aria-label": title,
612
- "data-state": status.state ?? "available",
699
+ "data-state": waiting ? "waiting" : status.state ?? "available",
613
700
  onClick: act,
614
701
  onPointerDown: (event) => event.stopPropagation(),
615
702
  onKeyDown: (event) => {
@@ -618,7 +705,7 @@ function apply(ctx) {
618
705
  },
619
706
  status.state === "installing" || status.state === "reloading" ? (0, import_react.createElement)("span", { className: "theone-update-spin", "aria-hidden": true }) : null,
620
707
  label,
621
- !status.state && status.latest ? (0, import_react.createElement)("small", null, `v${status.latest}`) : null
708
+ !status.state && (waiting ? status.waiting.version : status.latest) ? (0, import_react.createElement)("small", null, `v${waiting ? status.waiting.version : status.latest}`) : null
622
709
  );
623
710
  }
624
711
  const symbol = () => (0, import_react.createElement)(
@@ -1718,6 +1805,18 @@ button:has(.theone-nav[data-wide=true])>span:has(.theone-nav){flex:1;min-width:0
1718
1805
  .theone-update small{font-size:11px;opacity:.75}
1719
1806
  .theone-update[data-state=installing],.theone-update[data-state=reloading],.theone-update[data-state=restart]{cursor:default}
1720
1807
  .theone-update[data-state=failed]{color:#d9480f;border-color:#d9480f66;background:#d9480f14}
1808
+ .theone-update[data-state=waiting]{opacity:.8;border-style:dashed}
1809
+ .theone-dialog-backdrop{position:fixed;inset:0;z-index:10001;display:flex;align-items:center;justify-content:center;padding:16px;background:#0008}
1810
+ .theone-dialog{width:min(440px,100%);box-sizing:border-box;padding:22px 22px 18px;border:1px solid var(--dsw-alias-border-l2,#ffffff1f);border-radius:16px;background:var(--dsw-specific-sidebar-fill,#232326);color:var(--dsw-alias-label-primary,#e8e8ea);box-shadow:0 16px 48px #0005;font:inherit;font-size:14px;line-height:1.65}
1811
+ .theone-dialog h2{margin:0 0 10px;font-size:17px}
1812
+ .theone-dialog p{margin:0 0 10px;color:var(--dsw-alias-label-secondary,#a0a0a6)}
1813
+ .theone-dialog .theone-dialog-note{font-size:13px}
1814
+ .theone-dialog-actions{display:flex;justify-content:flex-end;gap:10px;margin-top:16px}
1815
+ .theone-dialog-actions button{padding:8px 14px;border:1px solid var(--dsw-alias-border-l2,#ffffff1f);border-radius:9px;background:transparent;color:inherit;font:inherit;cursor:pointer}
1816
+ .theone-dialog-actions button:hover{background:var(--dsw-alias-interactive-bg-hover,#ffffff12)}
1817
+ .theone-dialog-actions .theone-dialog-primary{border-color:transparent;background:#3b6fb0;color:#fff}
1818
+ .theone-dialog-actions .theone-dialog-primary:hover{background:#4a7fc0}
1819
+ .theone-dialog-actions button:focus-visible{outline:2px solid #4a7fc0;outline-offset:2px}
1721
1820
  .theone-update-spin{width:10px;height:10px;border-radius:50%;border:1.5px solid currentColor;border-right-color:transparent;animation:theone-spin 800ms linear infinite}
1722
1821
  @keyframes theone-spin{to{transform:rotate(360deg)}}
1723
1822
  @media(prefers-reduced-motion:reduce){.theone-update-spin{animation:none}}
package/dist/index.js CHANGED
@@ -205,9 +205,10 @@ export default class TheOne extends Service {
205
205
  }
206
206
  descriptors ??= config.contextsPath ? readDescriptors(config.contextsPath) : [];
207
207
  this.gatewayDirectory = resolve(dirname(databasePath), 'gateway');
208
+ const profileDir = () => ctx.profileContext?.dir;
208
209
  this.updater = new Updater(readOwnVersion(), () => {
209
210
  // The profile's package.json records how this plugin was installed (GitHub, npm or a local path).
210
- const dir = ctx.profileContext?.dir;
211
+ const dir = profileDir();
211
212
  if (!dir)
212
213
  return undefined;
213
214
  try {
@@ -216,7 +217,7 @@ export default class TheOne extends Service {
216
217
  catch {
217
218
  return undefined;
218
219
  }
219
- });
220
+ }, fetch, Date.now, () => { const dir = profileDir(); return dir ? join(dir, 'pnpm-workspace.yaml') : undefined; });
220
221
  ctx.effect(() => async () => {
221
222
  try {
222
223
  await Promise.all([...this.workers.values()].map(handle => handle.dispose()));
@@ -628,6 +629,18 @@ export default class TheOne extends Service {
628
629
  const manager = this.ctx.get('pluginManager');
629
630
  // With DSH's hot reload, switching the bundle off and on loads the new version without a DSH restart.
630
631
  const live = !!this.ctx.get('hmr') && typeof manager?.setBundleEnabled === 'function';
632
+ if (request.method === 'POST') {
633
+ // The user chose to let TheOne skip pnpm's one-day wait; only this package is exempted.
634
+ const body = await request.json().catch(() => ({}));
635
+ if (body?.allowFresh === true) {
636
+ try {
637
+ this.updater.allowFresh();
638
+ }
639
+ catch (error) {
640
+ return Response.json({ ...await this.updater.status(), error: error instanceof Error && /^[A-Z_]+$/.test(error.message) ? error.message : 'EXEMPT_FAILED' }, { status: 400 });
641
+ }
642
+ }
643
+ }
631
644
  const status = request.method === 'POST'
632
645
  ? await this.updater.install(manager, live ? bundle => reloadBundle(manager, bundle) : undefined)
633
646
  : await this.updater.status(new URL(request.url).searchParams.has('force'));
package/dist/update.d.ts CHANGED
@@ -19,7 +19,18 @@ export interface UpdateStatus {
19
19
  version: string;
20
20
  readyAt: number;
21
21
  };
22
+ /** TheOne is exempt from that rule in this DSH profile, so fresh versions install right away. */
23
+ exempt?: boolean;
24
+ /** Whether this profile's pnpm settings can be edited to grant that exemption. */
25
+ canExempt?: boolean;
22
26
  }
27
+ /** The packages a pnpm-workspace.yaml exempts from the release-age rule (block or flow list). */
28
+ export declare function releaseAgeExemptions(text: string): string[];
29
+ /**
30
+ * The same pnpm-workspace.yaml with `name` added to the release-age exemptions, every other line
31
+ * kept as it was. Throws for a layout it does not recognise rather than guess.
32
+ */
33
+ export declare function withReleaseAgeExemption(text: string, name?: string): string;
23
34
  /**
24
35
  * pnpm, which DSH installs plugins with, refuses npm versions published less than a day ago
25
36
  * (minimumReleaseAge, a supply-chain safeguard). Updates from npm wait until then.
@@ -51,11 +62,20 @@ export declare class Updater {
51
62
  private readonly spec;
52
63
  private readonly fetcher;
53
64
  private readonly now;
65
+ private readonly workspace;
54
66
  private checked?;
55
67
  private state?;
56
68
  private error?;
57
69
  private pending?;
58
- constructor(current: string, spec: () => string | undefined, fetcher?: typeof fetch, now?: () => number);
70
+ /**
71
+ * @param workspace - this DSH profile's pnpm-workspace.yaml, where pnpm reads the release-age
72
+ * exemptions; undefined when the profile is unknown.
73
+ */
74
+ constructor(current: string, spec: () => string | undefined, fetcher?: typeof fetch, now?: () => number, workspace?: () => string | undefined);
75
+ /** Whether pnpm in this profile already lets TheOne install versions under a day old. */
76
+ get exempt(): boolean;
77
+ /** Exempt TheOne, and only TheOne, from pnpm's release-age rule in this profile. */
78
+ allowFresh(): void;
59
79
  get source(): UpdateSource;
60
80
  /** The latest known status; checks again at most every six hours (or now, when forced). */
61
81
  status(force?: boolean): Promise<UpdateStatus>;
package/dist/update.js CHANGED
@@ -1,5 +1,48 @@
1
+ import { readFileSync, renameSync, writeFileSync } from 'node:fs';
1
2
  export const REPOSITORY = 'YunongDai2005/dsh-theone';
2
3
  export const PACKAGE_NAME = 'dsh-theone';
4
+ const EXCLUDE_KEY = 'minimumReleaseAgeExclude';
5
+ const unquote = (value) => value.trim().replace(/^['"]|['"]$/g, '');
6
+ /** The packages a pnpm-workspace.yaml exempts from the release-age rule (block or flow list). */
7
+ export function releaseAgeExemptions(text) {
8
+ const lines = text.split(/\r?\n/);
9
+ const index = lines.findIndex(line => line.startsWith(`${EXCLUDE_KEY}:`));
10
+ if (index < 0)
11
+ return [];
12
+ const inline = lines[index].slice(EXCLUDE_KEY.length + 1).trim();
13
+ if (inline.startsWith('['))
14
+ return inline.replace(/^\[|\]$/g, '').split(',').map(unquote).filter(Boolean);
15
+ const items = [];
16
+ for (let line = index + 1; line < lines.length && /^\s+-/.test(lines[line]); line++)
17
+ items.push(unquote(lines[line].replace(/^\s+-\s*/, '')));
18
+ return items;
19
+ }
20
+ /**
21
+ * The same pnpm-workspace.yaml with `name` added to the release-age exemptions, every other line
22
+ * kept as it was. Throws for a layout it does not recognise rather than guess.
23
+ */
24
+ export function withReleaseAgeExemption(text, name = PACKAGE_NAME) {
25
+ if (releaseAgeExemptions(text).includes(name))
26
+ return text;
27
+ const newline = text.includes('\r\n') ? '\r\n' : '\n';
28
+ const lines = text.split(/\r?\n/);
29
+ const index = lines.findIndex(line => line.startsWith(`${EXCLUDE_KEY}:`));
30
+ if (index < 0)
31
+ return `${text}${text && !/\r?\n$/.test(text) ? newline : ''}${EXCLUDE_KEY}:${newline} - ${name}${newline}`;
32
+ const inline = lines[index].slice(EXCLUDE_KEY.length + 1).trim();
33
+ if (inline.startsWith('[') && inline.endsWith(']')) {
34
+ lines[index] = `${EXCLUDE_KEY}: [${[...releaseAgeExemptions(text), name].map(item => JSON.stringify(item)).join(', ')}]`;
35
+ return lines.join(newline);
36
+ }
37
+ if (inline)
38
+ throw new Error('UNRECOGNISED_WORKSPACE');
39
+ let end = index + 1;
40
+ while (end < lines.length && /^\s+-/.test(lines[end]))
41
+ end++;
42
+ const indent = end > index + 1 ? lines[index + 1].match(/^\s+/)[0] : ' ';
43
+ lines.splice(end, 0, `${indent}- ${name}`);
44
+ return lines.join(newline);
45
+ }
3
46
  /**
4
47
  * pnpm, which DSH installs plugins with, refuses npm versions published less than a day ago
5
48
  * (minimumReleaseAge, a supply-chain safeguard). Updates from npm wait until then.
@@ -38,15 +81,56 @@ export class Updater {
38
81
  spec;
39
82
  fetcher;
40
83
  now;
84
+ workspace;
41
85
  checked;
42
86
  state;
43
87
  error;
44
88
  pending;
45
- constructor(current, spec, fetcher = fetch, now = Date.now) {
89
+ /**
90
+ * @param workspace - this DSH profile's pnpm-workspace.yaml, where pnpm reads the release-age
91
+ * exemptions; undefined when the profile is unknown.
92
+ */
93
+ constructor(current, spec, fetcher = fetch, now = Date.now, workspace = () => undefined) {
46
94
  this.current = current;
47
95
  this.spec = spec;
48
96
  this.fetcher = fetcher;
49
97
  this.now = now;
98
+ this.workspace = workspace;
99
+ }
100
+ /** Whether pnpm in this profile already lets TheOne install versions under a day old. */
101
+ get exempt() {
102
+ const file = this.workspace();
103
+ if (!file)
104
+ return false;
105
+ try {
106
+ return releaseAgeExemptions(readFileSync(file, 'utf8')).includes(PACKAGE_NAME);
107
+ }
108
+ catch {
109
+ return false;
110
+ }
111
+ }
112
+ /** Exempt TheOne, and only TheOne, from pnpm's release-age rule in this profile. */
113
+ allowFresh() {
114
+ const file = this.workspace();
115
+ if (!file)
116
+ throw new Error('NO_PROFILE');
117
+ let text = '';
118
+ try {
119
+ text = readFileSync(file, 'utf8');
120
+ }
121
+ catch (error) {
122
+ if (error.code !== 'ENOENT')
123
+ throw error;
124
+ }
125
+ const next = withReleaseAgeExemption(text);
126
+ if (next === text)
127
+ return;
128
+ writeFileSync(`${file}.theone-tmp`, next, { mode: 0o600 });
129
+ renameSync(`${file}.theone-tmp`, file);
130
+ if (this.state === 'failed') {
131
+ this.state = undefined;
132
+ this.error = undefined;
133
+ }
50
134
  }
51
135
  get source() { return installSource(this.spec()); }
52
136
  /** The latest known status; checks again at most every six hours (or now, when forced). */
@@ -68,7 +152,7 @@ export class Updater {
68
152
  const status = await this.status();
69
153
  if (!status.available || !status.installable || !installer || this.state === 'installing' || this.state === 'reloading' || this.state === 'restart')
70
154
  return this.snapshot();
71
- const spec = this.source === 'github' ? `github:${REPOSITORY}#${this.checked.sha}` : `${PACKAGE_NAME}@${this.checked.latest}`;
155
+ const spec = this.source === 'github' ? `github:${REPOSITORY}#${this.checked.sha}` : `${PACKAGE_NAME}@${status.latest}`;
72
156
  this.state = 'installing';
73
157
  this.error = undefined;
74
158
  try {
@@ -94,11 +178,15 @@ export class Updater {
94
178
  return this.snapshot();
95
179
  }
96
180
  snapshot() {
97
- const latest = this.checked?.latest;
181
+ const exempt = this.source === 'npm' && this.exempt;
182
+ // Exempt from the release-age rule, the newest version is ready as soon as it is published.
183
+ const fresh = this.checked?.waiting && compareVersions(this.checked.waiting.version, this.current) > 0 ? this.checked.waiting : undefined;
184
+ const latest = exempt && fresh ? fresh.version : this.checked?.latest;
98
185
  const source = this.source;
99
186
  const available = !!latest && compareVersions(latest, this.current) > 0;
100
- const waiting = this.checked?.waiting && compareVersions(this.checked.waiting.version, this.current) > 0 ? { waiting: this.checked.waiting } : {};
187
+ const waiting = fresh && !exempt ? { waiting: fresh } : {};
101
188
  return { current: this.current, ...(latest ? { latest } : {}), available, source, ...waiting,
189
+ ...(source === 'npm' ? { exempt, canExempt: !!this.workspace() } : {}),
102
190
  installable: available && (source === 'npm' || (source === 'github' && !!this.checked?.sha)),
103
191
  ...(this.state ? { state: this.state } : {}), ...(this.error ?? this.checked?.error ? { error: this.error ?? this.checked?.error } : {}) };
104
192
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-theone",
3
- "version": "0.3.15",
3
+ "version": "0.3.16",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Automatic context routing for DeepSeek Harness",