dsh-m 0.3.0 → 0.4.0

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/lib/client.js CHANGED
@@ -306,7 +306,10 @@ function installedViewModel(it) {
306
306
  sourceLabelKey: { npm: "src.npm", github: "src.github", link: "src.link", file: "src.file", unknown: "src.unknown" }[it.source] || null,
307
307
  guard: uninstallGuardKeys(it),
308
308
  latestLabel: it.latestTag || (it.latestVersion ? `v${it.latestVersion}` : ""),
309
- latestLabelDetail: it.latestTag || (it.latestVersion ? `v${it.latestVersion}` : "\u2014")
309
+ latestLabelDetail: it.latestTag || (it.latestVersion ? `v${it.latestVersion}` : "\u2014"),
310
+ // 0.4.0(Task 17):运行相位/开关状态的 sub 行渲染原料;相位点 class 映射归 toggle-view
311
+ phaseKey: it.phase ? `phase.${it.phase}` : null,
312
+ enabledLabelKey: it.enabled === false ? "toggle.state.off" : "toggle.state.on"
310
313
  };
311
314
  }
312
315
  function registrySourceKey(data) {
@@ -334,6 +337,51 @@ var init_installed_view = __esm({
334
337
  }
335
338
  });
336
339
 
340
+ // src/client/toggle-view.js
341
+ var toggle_view_exports = {};
342
+ __export(toggle_view_exports, {
343
+ toggleNoticeKeys: () => toggleNoticeKeys,
344
+ toggleViewModel: () => toggleViewModel
345
+ });
346
+ function toggleViewModel(it) {
347
+ const phase = PHASE_VIEW[it.phase] ?? { phaseDotClass: "", phaseLabelKey: null };
348
+ const toggleable = it.toggleable !== false && it.lockReason === void 0;
349
+ return {
350
+ phaseDotClass: phase.phaseDotClass,
351
+ phaseLabelKey: phase.phaseLabelKey,
352
+ switchOn: it.enabled === true,
353
+ switchDisabled: !toggleable,
354
+ switchTitleKey: it.lockReason ? LOCK_TITLE_KEYS[it.lockReason] || null : null
355
+ };
356
+ }
357
+ function toggleNoticeKeys(result) {
358
+ const direction = result.enabled === true ? "on" : "off";
359
+ const applied = result.applied === "live" ? "live" : "restart";
360
+ return {
361
+ textKey: `notify.toggled.${direction}.${applied}`,
362
+ params: { pkg: result.pkg || "" },
363
+ needsRestart: applied === "restart"
364
+ };
365
+ }
366
+ var PHASE_VIEW, LOCK_TITLE_KEYS;
367
+ var init_toggle_view = __esm({
368
+ "src/client/toggle-view.js"() {
369
+ "use strict";
370
+ PHASE_VIEW = {
371
+ active: { phaseDotClass: "ok", phaseLabelKey: "phase.active" },
372
+ failed: { phaseDotClass: "err", phaseLabelKey: "phase.failed" },
373
+ pending: { phaseDotClass: "idle", phaseLabelKey: "phase.pending" },
374
+ loading: { phaseDotClass: "busy", phaseLabelKey: "phase.loading" },
375
+ unloading: { phaseDotClass: "busy", phaseLabelKey: "phase.unloading" }
376
+ };
377
+ LOCK_TITLE_KEYS = {
378
+ self: "toggle.lock.self",
379
+ protected: "toggle.lock.protected",
380
+ "no-entry": "toggle.lock.noentry"
381
+ };
382
+ }
383
+ });
384
+
337
385
  // src/client/tool-view.js
338
386
  var tool_view_exports = {};
339
387
  __export(tool_view_exports, {
@@ -438,6 +486,7 @@ var { MARKET_PAGE_SIZE: MARKET_PAGE_SIZE2, normalizeMarketQuery: normalizeMarket
438
486
  var { createMarkdown: createMarkdown2 } = (init_markdown(), __toCommonJS(markdown_exports));
439
487
  var { ExtLink, MdImg, renderMarkdown } = createMarkdown2(h);
440
488
  var { installedViewModel: installedViewModel2, registrySourceKey: registrySourceKey2 } = (init_installed_view(), __toCommonJS(installed_view_exports));
489
+ var { toggleViewModel: toggleViewModel2, toggleNoticeKeys: toggleNoticeKeys2 } = (init_toggle_view(), __toCommonJS(toggle_view_exports));
441
490
  var { pickPayload: pickPayload2, parseToolArgs: parseToolArgs2 } = (init_tool_view(), __toCommonJS(tool_view_exports));
442
491
  var { RESTART_POLL_MS: RESTART_POLL_MS2, RESTART_DEADLINE_MS: RESTART_DEADLINE_MS2, nextRestartWait: nextRestartWait2, isAmbiguousRestartRequestError: isAmbiguousRestartRequestError2 } = (init_restart_wait(), __toCommonJS(restart_wait_exports));
443
492
  var { refreshAfterMutation: refreshAfterMutation2 } = (init_view_refresh(), __toCommonJS(view_refresh_exports));
@@ -572,6 +621,37 @@ var ZH = {
572
621
  "registry.refreshed": "\u6536\u5F55\u6E05\u5355\u5DF2\u5F3A\u5236\u5237\u65B0",
573
622
  "notify.installed": "\u5DF2\u5B89\u88C5 {pkg}{version}",
574
623
  "notify.allowbuilds": "\uFF08\u6CE8\u610F\uFF1A\u8BE5\u63D2\u4EF6\u6267\u884C\u4E86\u6784\u5EFA\u811A\u672C\uFF0C\u5DF2\u6309\u7B56\u7565\u653E\u884C\uFF09",
624
+ "notify.builds": "\uFF08\u5DF2\u7CBE\u786E\u653E\u884C\u6784\u5EFA\u811A\u672C\uFF1A{names}\uFF09",
625
+ "notify.builds.fallback": "\uFF08\u6CE8\u610F\uFF1A\u6784\u5EFA\u811A\u672C\u540D\u5355\u4E0D\u53EF\u8BFB\uFF0C\u5DF2\u5168\u91CF\u515C\u5E95\u653E\u884C\uFF09",
626
+ "notify.bundlewarning": "\uFF08\u6CE8\u610F\uFF1A\u8BE5\u5305\u65E0\u8865\u4E01\u5C42\uFF0C\u5DF2\u88C5\u5165\u4E3A\u7EAF\u4F9D\u8D56\u4E0D\u4F1A\u751F\u6548\uFF1B\u53EF\u5378\u8F7D\u6216\u5230\u6536\u5F55\u4ED3\u5E93\u53CD\u9988\uFF09",
627
+ "notify.toggled.on.live": "\u5DF2\u542F\u7528 {pkg}\uFF08\u5373\u65F6\u751F\u6548\uFF09",
628
+ "notify.toggled.off.live": "\u5DF2\u505C\u7528 {pkg}\uFF08\u5373\u65F6\u751F\u6548\uFF09",
629
+ "notify.toggled.on.restart": "\u5DF2\u542F\u7528 {pkg}\uFF08\u9700\u91CD\u542F\u751F\u6548\uFF09",
630
+ "notify.toggled.off.restart": "\u5DF2\u505C\u7528 {pkg}\uFF08\u9700\u91CD\u542F\u751F\u6548\uFF09",
631
+ "toggle.state.on": "\u8FD0\u884C\u4E2D",
632
+ "toggle.state.off": "\u5DF2\u505C\u7528",
633
+ "toggle.lock.self": "dsh-m \u81EA\u8EAB\u4E0D\u53EF\u5F00\u5173",
634
+ "toggle.lock.protected": "\u5B98\u65B9\u5BBF\u4E3B\u547D\u8109\uFF0C\u4E0D\u53EF\u5F00\u5173",
635
+ "toggle.lock.noentry": "\u672A\u88C5\u8F7D\uFF08\u4E0D\u5728\u52A0\u8F7D\u6811\uFF09",
636
+ "toggle.failed": "\u5F00\u5173\u64CD\u4F5C\u5931\u8D25\uFF1A{err}",
637
+ "phase.active": "active",
638
+ "phase.failed": "failed",
639
+ "phase.pending": "pending",
640
+ "phase.loading": "loading",
641
+ "phase.unloading": "unloading",
642
+ "compat.title": "\u517C\u5BB9\u6027\u98CE\u9669\u786E\u8BA4",
643
+ "compat.body": "{pkg}@{version} \u58F0\u660E\u7684 peerDependencies \u4E0E\u5F53\u524D DSH {runtime} \u4E0D\u517C\u5BB9\uFF1A",
644
+ "compat.risk": "\u7EE7\u7EED\u5B89\u88C5\u53EF\u80FD\u5BFC\u81F4\u5D29\u6E83\u6216\u6570\u636E\u4E22\u5931\u3002\u786E\u5B9A\u4ECD\u8981\u5B89\u88C5\u5417\uFF1F",
645
+ "compat.force": "\u4ECD\u8981\u5B89\u88C5",
646
+ "common.cancel": "\u53D6\u6D88",
647
+ "settings.probe": "\u5143\u6570\u636E\u6E90",
648
+ "settings.probe.npmjs": "npmjs\uFF08\u7ADE\u901F\u80DC\u51FA\uFF09",
649
+ "settings.probe.mirror": "npmmirror\uFF08\u7ADE\u901F\u80DC\u51FA\uFF09",
650
+ "settings.probe.none": "\u9ED8\u8BA4 npmjs\uFF08\u672A\u63A2\u6D4B/\u53CC\u4E0D\u901A\uFF09",
651
+ "settings.probe.toggle": "\u7ADE\u901F\u63A2\u6D4B",
652
+ "settings.probe.apply": "\u5E94\u7528",
653
+ "settings.probe.applied": "\u63A2\u6D4B\u914D\u7F6E\u5DF2\u66F4\u65B0\uFF08\u5373\u65F6\u751F\u6548\uFF09",
654
+ "settings.probe.failed": "\u63A2\u6D4B\u914D\u7F6E\u4FDD\u5B58\u5931\u8D25\uFF1A{err}",
575
655
  "notify.uninstalled": "\u5DF2\u5378\u8F7D {pkg}",
576
656
  "notify.livedisabled": "\uFF08\u5DF2\u5148\u4E0B\u7EBF\u8FD0\u884C\u4E2D\u7684\u754C\u9762\uFF09",
577
657
  "notify.leftovers": "\uFF1B\u68C0\u6D4B\u5230\u7591\u4F3C\u6B8B\u7559\u6570\u636E\uFF1A{paths}",
@@ -740,6 +820,37 @@ var EN = {
740
820
  "registry.refreshed": "Registry force-refreshed",
741
821
  "notify.installed": "Installed {pkg}{version}",
742
822
  "notify.allowbuilds": " (note: this plugin ran build scripts, allowed by policy)",
823
+ "notify.builds": " (build scripts precisely allowed: {names})",
824
+ "notify.builds.fallback": " (note: pending list unreadable; all builds allowed as fallback)",
825
+ "notify.bundlewarning": " (note: no patch layer \u2014 installed as a plain dependency; uninstall or report to the listing repo)",
826
+ "notify.toggled.on.live": "Enabled {pkg} (applied live)",
827
+ "notify.toggled.off.live": "Disabled {pkg} (applied live)",
828
+ "notify.toggled.on.restart": "Enabled {pkg} (restart required)",
829
+ "notify.toggled.off.restart": "Disabled {pkg} (restart required)",
830
+ "toggle.state.on": "running",
831
+ "toggle.state.off": "disabled",
832
+ "toggle.lock.self": "dsh-m itself cannot be toggled",
833
+ "toggle.lock.protected": "host lifeline module; not toggleable",
834
+ "toggle.lock.noentry": "not composed (absent from loader tree)",
835
+ "toggle.failed": "Toggle failed: {err}",
836
+ "phase.active": "active",
837
+ "phase.failed": "failed",
838
+ "phase.pending": "pending",
839
+ "phase.loading": "loading",
840
+ "phase.unloading": "unloading",
841
+ "compat.title": "Compatibility risk",
842
+ "compat.body": "{pkg}@{version} declares peerDependencies incompatible with DSH {runtime}:",
843
+ "compat.risk": "Proceeding may cause crashes or data loss. Install anyway?",
844
+ "compat.force": "Install anyway",
845
+ "common.cancel": "Cancel",
846
+ "settings.probe": "Metadata source",
847
+ "settings.probe.npmjs": "npmjs (probe winner)",
848
+ "settings.probe.mirror": "npmmirror (probe winner)",
849
+ "settings.probe.none": "default npmjs (not probed/both down)",
850
+ "settings.probe.toggle": "Probe",
851
+ "settings.probe.apply": "Apply",
852
+ "settings.probe.applied": "Probe settings updated (live)",
853
+ "settings.probe.failed": "Failed to save probe settings: {err}",
743
854
  "notify.uninstalled": "Uninstalled {pkg}",
744
855
  "notify.livedisabled": " (live UI disabled first)",
745
856
  "notify.leftovers": "; possible leftover data: {paths}",
@@ -897,6 +1008,26 @@ var CSS = `
897
1008
  .dshm-dshchip:hover .dshm-dshchip-tip,.dshm-dshchip:focus-visible .dshm-dshchip-tip{visibility:visible;opacity:1}
898
1009
  .dshm-dshchip-tiprow{font:12px/16px ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-weight:600;color:var(--dsw-alias-label-primary,inherit)}
899
1010
  .dshm-dshchip-tipsub{display:block;margin-top:2px;font-size:10px;line-height:14px;color:var(--dsw-alias-label-caption,#9ca3af)}
1011
+
1012
+ /* 0.4.0\uFF1A\u5F00\u5173 / \u76F8\u4F4D\u70B9 / \u517C\u5BB9\u786E\u8BA4\u5F39\u7A97 */
1013
+ .dshm-dot{font-size:9px;line-height:1;vertical-align:middle;margin-right:2px}
1014
+ .dshm-dot.ok{color:#22c55e}
1015
+ .dshm-dot.err{color:#ef4444}
1016
+ .dshm-dot.idle{color:#9ca3af}
1017
+ .dshm-dot.busy{color:#f59e0b}
1018
+ .dshm-switch{position:relative;display:inline-flex;align-items:center;width:34px;height:19px;border-radius:10px;background:var(--dsw-alias-fill-secondary,#d1d5db);cursor:pointer;transition:background .15s;flex:none;margin-left:auto}
1019
+ .dshm-switch input{display:none}
1020
+ .dshm-switch.on{background:#22c55e}
1021
+ .dshm-switch.locked{opacity:.45;cursor:not-allowed}
1022
+ .dshm-switch-slider{position:absolute;left:2px;top:2px;width:15px;height:15px;border-radius:50%;background:#fff;transition:left .15s;box-shadow:0 1px 2px rgba(0,0,0,.25)}
1023
+ .dshm-switch.on .dshm-switch-slider{left:17px}
1024
+ .dshm-compat-overlay{position:fixed;inset:0;background:rgba(0,0,0,.45);display:flex;align-items:center;justify-content:center;z-index:60}
1025
+ .dshm-compat-dialog{max-width:420px;width:calc(100% - 32px);background:var(--dsw-alias-bg-layer-2,#1c2230);border:1px solid var(--dsw-alias-border-l2,#333b4d);border-radius:12px;padding:16px}
1026
+ .dshm-compat-title{font-weight:600;margin-bottom:8px;color:#f87171}
1027
+ .dshm-compat-body{font-size:12px;margin-bottom:6px}
1028
+ .dshm-compat-peers{margin:0 0 8px;padding-left:18px;font-size:12px}
1029
+ .dshm-compat-peers li{font-family:ui-monospace,monospace}
1030
+ .dshm-compat-risk{font-size:12px;color:#f87171;margin-bottom:12px}
900
1031
  `;
901
1032
  function ensureCss() {
902
1033
  if (typeof document === "undefined" || document.getElementById("dshm-css")) return;
@@ -913,7 +1044,11 @@ async function api(method, params, signal) {
913
1044
  ...signal ? { signal } : {}
914
1045
  });
915
1046
  const data = await res.json().catch(() => ({}));
916
- if (!res.ok || data.ok === false) throw new Error(data.error || `API ${res.status}`);
1047
+ if (!res.ok || data.ok === false) {
1048
+ const err = new Error(data.error || `API ${res.status}`);
1049
+ if (data && typeof data === "object" && data.issue) err.issue = data.issue;
1050
+ throw err;
1051
+ }
917
1052
  return data;
918
1053
  }
919
1054
  function fmtDate(iso) {
@@ -1012,6 +1147,29 @@ function LinksRow(props) {
1012
1147
  });
1013
1148
  return h("div", { className: "dshm-links" }, h("span", { className: "dshm-links-k" }, `${lookup("detail.links")}\uFF1A`), ...kids);
1014
1149
  }
1150
+ function PhaseDot({ vm }) {
1151
+ if (!vm.phaseDotClass) return null;
1152
+ return h(
1153
+ "span",
1154
+ { className: `dshm-dot ${vm.phaseDotClass}`, title: vm.phaseLabelKey ? lookup(vm.phaseLabelKey) : null },
1155
+ "\u25CF"
1156
+ );
1157
+ }
1158
+ function ToggleSwitch({ vm, disabled, onChange, title }) {
1159
+ return h(
1160
+ "label",
1161
+ {
1162
+ className: `dshm-switch${vm.switchOn ? " on" : ""}${vm.switchDisabled ? " locked" : ""}`,
1163
+ title: title || (vm.switchTitleKey ? lookup(vm.switchTitleKey) : null),
1164
+ onClick: (e) => {
1165
+ e.stopPropagation();
1166
+ if (!vm.switchDisabled && !disabled) onChange(!vm.switchOn);
1167
+ }
1168
+ },
1169
+ h("input", { type: "checkbox", checked: vm.switchOn, disabled: vm.switchDisabled || disabled, readOnly: true }),
1170
+ h("span", { className: "dshm-switch-slider" })
1171
+ );
1172
+ }
1015
1173
  function TwoStepButton({ label, confirmLabel, className, onConfirm, disabled }) {
1016
1174
  const [arm, setArm] = useState(false);
1017
1175
  useEffect(() => {
@@ -1105,25 +1263,70 @@ function MarketTab({ notify, market, onMutation }) {
1105
1263
  if (debounceRef.current) clearTimeout(debounceRef.current);
1106
1264
  debounceRef.current = setTimeout(() => updateQuery({ query: value }), 300);
1107
1265
  };
1108
- const doInstall = async (it, version) => {
1266
+ const [compatConfirm, setCompatConfirm] = useState(null);
1267
+ const installDone = (res) => {
1268
+ notify({
1269
+ kind: "ok",
1270
+ needsRestart: true,
1271
+ text: lookup("notify.installed", { pkg: res.pkg, version: res.version ? ` v${res.version}` : "" }) + (res.buildApprovals && res.buildApprovals.length ? lookup("notify.builds", { names: res.buildApprovals.join(", ") }) : res.fallbackAllBuilds ? lookup("notify.builds.fallback") : "") + (res.bundleWarning === "no-patch-layer" ? lookup("notify.bundlewarning") : "")
1272
+ });
1273
+ };
1274
+ const doInstall = async (it, version, forceIncompatible) => {
1109
1275
  setBusyId(it.id);
1110
1276
  try {
1111
- const res = await api("install", { id: it.id, ...version ? { version } : {} });
1112
- notify({
1113
- kind: "ok",
1114
- needsRestart: true,
1115
- text: lookup("notify.installed", { pkg: res.pkg, version: res.version ? ` v${res.version}` : "" }) + (res.usedAllowAllBuilds ? lookup("notify.allowbuilds") : "")
1116
- });
1277
+ const res = await api("install", { id: it.id, ...version ? { version } : {}, ...forceIncompatible ? { forceIncompatible: true } : {} });
1278
+ installDone(res);
1117
1279
  await (onMutation ? onMutation() : reload(false));
1118
1280
  } catch (e) {
1119
- notify({ kind: "err", text: lookup("failed.install", { err: e && e.message || e }) });
1281
+ if (e && e.issue) {
1282
+ setCompatConfirm({ it, version, issue: e.issue });
1283
+ } else {
1284
+ notify({ kind: "err", text: lookup("failed.install", { err: e && e.message || e }) });
1285
+ }
1120
1286
  } finally {
1121
1287
  setBusyId(null);
1122
1288
  }
1123
1289
  };
1290
+ const CompatDialog = compatConfirm ? h(
1291
+ "div",
1292
+ { className: "dshm-compat-overlay", onClick: () => setCompatConfirm(null) },
1293
+ h(
1294
+ "div",
1295
+ { className: "dshm-compat-dialog", onClick: (e) => e.stopPropagation() },
1296
+ h("div", { className: "dshm-compat-title" }, lookup("compat.title")),
1297
+ h("div", { className: "dshm-compat-body" }, lookup("compat.body", {
1298
+ pkg: compatConfirm.issue.pkg || compatConfirm.it.pkg,
1299
+ version: compatConfirm.issue.version || "?",
1300
+ runtime: compatConfirm.issue.runtimeVersion || "?"
1301
+ })),
1302
+ h(
1303
+ "ul",
1304
+ { className: "dshm-compat-peers" },
1305
+ ...Object.entries(compatConfirm.issue.peers || {}).map(([name, range]) => h("li", { key: name }, `${name}: ${range}`))
1306
+ ),
1307
+ h("div", { className: "dshm-compat-risk" }, lookup("compat.risk")),
1308
+ h(
1309
+ "div",
1310
+ { className: "dshm-actions" },
1311
+ h("button", {
1312
+ className: "dshm-btn sm",
1313
+ onClick: () => setCompatConfirm(null)
1314
+ }, lookup("common.cancel")),
1315
+ h("button", {
1316
+ className: "dshm-btn primary sm",
1317
+ onClick: () => {
1318
+ const pending = compatConfirm;
1319
+ setCompatConfirm(null);
1320
+ doInstall(pending.it, pending.version, true);
1321
+ }
1322
+ }, lookup("compat.force"))
1323
+ )
1324
+ )
1325
+ ) : null;
1124
1326
  return h(
1125
1327
  React.Fragment,
1126
1328
  null,
1329
+ CompatDialog,
1127
1330
  notice ? h(
1128
1331
  "div",
1129
1332
  { className: notice.key === "notice.unavailable" ? "dshm-err" : "dshm-hint" },
@@ -1277,6 +1480,27 @@ function InstalledTab({ notify, installed, onMutation }) {
1277
1480
  const [openPkg, setOpenPkg] = useState(null);
1278
1481
  const [readmePkg, setReadmePkg] = useState(null);
1279
1482
  const [busyPkg, setBusyPkg] = useState(null);
1483
+ const doToggle = async (it, enabled) => {
1484
+ setBusyPkg(it.pkg);
1485
+ const call = () => api("set-enabled", { pkg: it.pkg, enabled });
1486
+ try {
1487
+ let res;
1488
+ try {
1489
+ res = await call();
1490
+ } catch (first) {
1491
+ await new Promise((r) => setTimeout(r, 1500));
1492
+ res = await call();
1493
+ }
1494
+ const note = toggleNoticeKeys2(res);
1495
+ const extra = res.warnings && res.warnings.length ? `\uFF08${res.warnings.join("\uFF1B")}\uFF09` : "";
1496
+ notify({ kind: "ok", needsRestart: note.needsRestart, text: lookup(note.textKey, note.params) + extra });
1497
+ await (onMutation ? onMutation() : reload());
1498
+ } catch (e) {
1499
+ notify({ kind: "err", text: lookup("toggle.failed", { err: e && e.message || e }) });
1500
+ } finally {
1501
+ setBusyPkg(null);
1502
+ }
1503
+ };
1280
1504
  const doUninstall = async (it) => {
1281
1505
  setBusyPkg(it.pkg);
1282
1506
  try {
@@ -1304,7 +1528,7 @@ function InstalledTab({ notify, installed, onMutation }) {
1304
1528
  pkg: res.pkg,
1305
1529
  from: res.fromVersion ? `v${res.fromVersion}` : "\u2014",
1306
1530
  to: res.version ? `v${res.version}` : res.sha ? res.sha.slice(0, 7) : "latest"
1307
- }) + (res.usedAllowAllBuilds ? lookup("notify.upgradehint") : "")
1531
+ }) + (res.buildApprovals && res.buildApprovals.length ? lookup("notify.builds", { names: res.buildApprovals.join(", ") }) : res.fallbackAllBuilds ? lookup("notify.builds.fallback") : "")
1308
1532
  });
1309
1533
  await (onMutation ? onMutation() : reload());
1310
1534
  } catch (e) {
@@ -1328,19 +1552,27 @@ function InstalledTab({ notify, installed, onMutation }) {
1328
1552
  { className: "dshm-cards" },
1329
1553
  items.map((it) => {
1330
1554
  const vm = installedViewModel2(it);
1555
+ const tvm = toggleViewModel2(it);
1331
1556
  return Card({
1332
1557
  key: it.pkg,
1333
1558
  icon: h(Icon, { entry: { name: it.name, github: vm.githubRepo, icon: null } }),
1334
1559
  name: it.name,
1560
+ topRight: h(ToggleSwitch, {
1561
+ vm: tvm,
1562
+ disabled: busyPkg === it.pkg,
1563
+ onChange: (enabled) => doToggle(it, enabled)
1564
+ }),
1335
1565
  badges: [
1336
1566
  it.outdated ? h("span", { className: "dshm-badge warn", key: "u" }, `\u2B06 ${vm.latestLabel}`.trim()) : null,
1337
1567
  it.registryId ? h("span", { className: "dshm-badge", key: "r" }, lookup("badge.market")) : h("span", { className: "dshm-badge info", key: "r" }, lookup("badge.nonmarket"))
1338
1568
  ],
1339
1569
  desc: it.description || "\uFF08\u65E0\u63CF\u8FF0\uFF09",
1340
1570
  sub: [
1571
+ h(PhaseDot, { key: "dot", vm: tvm }),
1572
+ vm.phaseKey ? lookup(vm.phaseKey) : lookup(vm.enabledLabelKey),
1341
1573
  `v${it.version || "?"}`,
1342
1574
  vm.sourceLabelKey ? lookup(vm.sourceLabelKey) : it.source
1343
- ].join(" \xB7 "),
1575
+ ].map((part, i) => i === 0 ? part : [" \xB7 ", part]).flat(),
1344
1576
  links: h(LinksRow, {
1345
1577
  npm: it.source === "npm" ? it.pkg : null,
1346
1578
  github: vm.githubRepo
@@ -1399,10 +1631,41 @@ function configStatusLabel(status) {
1399
1631
  return lookup(`settings.status.${status || "loading"}` || "settings.status.loading");
1400
1632
  }
1401
1633
  var STATUS_BADGE = { ready: "", pending: "info", rejected: "warn", unavailable: "err", loading: "info" };
1634
+ function probeLabel(statusInfo) {
1635
+ const probe = statusInfo.data && statusInfo.data.probe;
1636
+ if (!probe) return lookup("settings.probe.none");
1637
+ if (probe.source === "npmjs") return lookup("settings.probe.npmjs");
1638
+ if (probe.source === "npmmirror") return lookup("settings.probe.mirror");
1639
+ return lookup("settings.probe.none");
1640
+ }
1402
1641
  function SettingsTab({ notify, onRegistryChanged }) {
1403
1642
  const reg = useAsync((force) => api("registry", force ? { force: true } : {}), []);
1404
1643
  const cfgState = useAsync(() => api("registry-config"), []);
1405
1644
  const self = useAsync(() => api("self-check"), []);
1645
+ const statusInfo = useAsync(() => api("status"), []);
1646
+ const probeCfg = useAsync(() => api("probe-config"), []);
1647
+ const [probeDraft, setProbeDraft] = useState(null);
1648
+ const [probeBusy, setProbeBusy] = useState(false);
1649
+ useEffect(() => {
1650
+ if (probeCfg.data && probeDraft === null) setProbeDraft({ ...probeCfg.data });
1651
+ }, [probeCfg.data, probeDraft]);
1652
+ const applyProbe = async () => {
1653
+ if (!probeDraft) return;
1654
+ setProbeBusy(true);
1655
+ try {
1656
+ const patch = {};
1657
+ if (probeDraft.probeEnabled !== void 0) patch.probeEnabled = probeDraft.probeEnabled === true;
1658
+ if (Number.isFinite(Number(probeDraft.probeTimeoutMs))) patch.probeTimeoutMs = Number(probeDraft.probeTimeoutMs);
1659
+ if (Number.isFinite(Number(probeDraft.probeCacheTtlMin))) patch.probeCacheTtlMin = Number(probeDraft.probeCacheTtlMin);
1660
+ const res = await api("probe-config-set", patch);
1661
+ setProbeDraft({ ...res });
1662
+ notify({ kind: "ok", text: lookup("settings.probe.applied"), needsRestart: false });
1663
+ } catch (e) {
1664
+ notify({ kind: "err", text: lookup("settings.probe.failed", { err: e && e.message || e }) });
1665
+ } finally {
1666
+ setProbeBusy(false);
1667
+ }
1668
+ };
1406
1669
  const [busy, setBusy] = useState(false);
1407
1670
  const [upgrading, setUpgrading] = useState(false);
1408
1671
  const [draftAddress, setDraftAddress] = useState(null);
@@ -1543,7 +1806,46 @@ function SettingsTab({ notify, onRegistryChanged }) {
1543
1806
  h("span", { className: "k" }, lookup("settings.count")),
1544
1807
  h("span", null, state ? lookup("settings.count.v", { n: state.count ?? 0 }) : "\u2014"),
1545
1808
  h("span", { className: "k" }, lookup("settings.policy")),
1546
- h("span", null, lookup("settings.policy.v"))
1809
+ h("span", null, lookup("settings.policy.v")),
1810
+ h("span", { className: "k" }, lookup("settings.probe")),
1811
+ h("span", null, probeLabel(statusInfo)),
1812
+ h("span", { className: "k" }, lookup("settings.probe.toggle")),
1813
+ h(
1814
+ "span",
1815
+ { className: "dshm-probe-ctl" },
1816
+ probeDraft ? [
1817
+ h(ToggleSwitch, {
1818
+ key: "sw",
1819
+ vm: { switchOn: probeDraft.probeEnabled === true, switchDisabled: false },
1820
+ title: null,
1821
+ onChange: (v) => setProbeDraft({ ...probeDraft, probeEnabled: v })
1822
+ }),
1823
+ h("input", {
1824
+ key: "timeout",
1825
+ className: "dshm-input",
1826
+ type: "number",
1827
+ style: { maxWidth: "90px" },
1828
+ title: "probeTimeoutMs",
1829
+ value: probeDraft.probeTimeoutMs ?? "",
1830
+ onChange: (e) => setProbeDraft({ ...probeDraft, probeTimeoutMs: e.target.value })
1831
+ }),
1832
+ h("input", {
1833
+ key: "ttl",
1834
+ className: "dshm-input",
1835
+ type: "number",
1836
+ style: { maxWidth: "70px" },
1837
+ title: "probeCacheTtlMin",
1838
+ value: probeDraft.probeCacheTtlMin ?? "",
1839
+ onChange: (e) => setProbeDraft({ ...probeDraft, probeCacheTtlMin: e.target.value })
1840
+ }),
1841
+ h("button", {
1842
+ key: "apply",
1843
+ className: "dshm-btn sm",
1844
+ disabled: probeBusy,
1845
+ onClick: applyProbe
1846
+ }, probeBusy ? h(Spin) : lookup("settings.probe.apply"))
1847
+ ] : lookup("settings.probe.none")
1848
+ )
1547
1849
  ),
1548
1850
  state && state.stale && state.status !== "unavailable" ? h("div", { className: "dshm-note" }, lookup("settings.cache.hint")) : null,
1549
1851
  state && !state.isDefault ? h("div", { className: "dshm-note warn" }, lookup("settings.trust.hint")) : null,
@@ -1628,7 +1930,7 @@ function DetailRows(rows) {
1628
1930
  ])
1629
1931
  );
1630
1932
  }
1631
- function Card({ icon, name, badges, desc, sub, links, open, onToggle, detail, actions }) {
1933
+ function Card({ icon, name, badges, desc, sub, links, open, onToggle, detail, actions, topRight }) {
1632
1934
  return h(
1633
1935
  "div",
1634
1936
  {
@@ -1644,7 +1946,7 @@ function Card({ icon, name, badges, desc, sub, links, open, onToggle, detail, ac
1644
1946
  h(
1645
1947
  "div",
1646
1948
  { className: "dshm-meta" },
1647
- h("div", { className: "dshm-top" }, h("span", { className: "dshm-name" }, name), ...badges.filter(Boolean)),
1949
+ h("div", { className: "dshm-top" }, h("span", { className: "dshm-name" }, name), ...badges.filter(Boolean), topRight || null),
1648
1950
  h("div", { className: "dshm-desc", style: open ? { WebkitLineClamp: "unset" } : null }, desc),
1649
1951
  sub ? h("div", { className: "dshm-sub" }, sub) : null,
1650
1952
  links || null,
@@ -0,0 +1,88 @@
1
+ /**
2
+ * 精确构建放行(0.4.0,ADR-0002;plan Task 3):
3
+ * pnpm 11 拦下依赖构建脚本时会在 `pnpm-workspace.yaml` 的 `allowBuilds` 映射里留下
4
+ * 待决占位(值为 "set this to true or false")。本模块提供两个纯函数:
5
+ *
6
+ * - readPendingBuilds:解析待决名单(键不含通配符、值精确等于占位串)——官方
7
+ * `readPendingBuilds`(dsh-plugin-manager L941)同款判定;官方对坏结构 throw,
8
+ * 此处收窄为返回 [](调用方据此走全量兜底,ADR-0002 的保守回退);
9
+ * - applyPreciseBuilds:为名单逐键写 `allowBuilds.<name>: true`(保注释 YAML 编辑)。
10
+ *
11
+ * 官方 `approveBuilds` 的调用前提是「caller 持 profile manifest 锁」;dsh-m 事务
12
+ * 只有进程内 FIFO,与官方 UI 并发安装时此处存在低概率写竞态窗口——按 ADR-0002
13
+ * 声明为已知风险,相关整单失败走既有回滚语义,不额外加锁。
14
+ */
15
+ import { isAlias, isMap, isScalar, isSeq, parseDocument } from 'yaml';
16
+ /** pnpm 11 写下的待决占位值(逐字,官方同款)。 */
17
+ const PENDING_PLACEHOLDER = 'set this to true or false';
18
+ /** 键含通配符的规则不进精确放行(官方同款排除)。 */
19
+ function isPlainKey(key) {
20
+ return !/[*?]/.test(key);
21
+ }
22
+ /**
23
+ * 读 pnpm-workspace.yaml 文本的待决构建名单。
24
+ * 保守口径:坏 YAML / 根非映射 / allowBuilds 非映射 / 条目含锚点或别名 / 值非标量
25
+ * 一律返回 []——调用方走 `withDangerouslyAllowAllBuilds` 全量兜底并如实标注。
26
+ */
27
+ export function readPendingBuilds(text) {
28
+ let document;
29
+ try {
30
+ document = parseDocument(text);
31
+ }
32
+ catch {
33
+ return [];
34
+ }
35
+ if (document.errors.length > 0)
36
+ return [];
37
+ if (!isMap(document.contents))
38
+ return [];
39
+ const builds = document.contents.get('allowBuilds');
40
+ if (builds === undefined || builds === null)
41
+ return [];
42
+ if (!isMap(builds))
43
+ return [];
44
+ const pending = [];
45
+ for (const pair of builds.items) {
46
+ const { key, value } = pair;
47
+ if (!isScalar(key) || typeof key.value !== 'string')
48
+ continue;
49
+ if (isAlias(key) || isAlias(value))
50
+ return [];
51
+ // 锚点定义同样拒绝(官方 visit 检查的保守化)
52
+ if ((isMap(value) || isSeq(value)) && 'anchor' in value && value.anchor)
53
+ return [];
54
+ if (!isScalar(value) || value.value !== PENDING_PLACEHOLDER)
55
+ continue;
56
+ if (!isPlainKey(key.value))
57
+ continue;
58
+ pending.push(key.value);
59
+ }
60
+ return pending;
61
+ }
62
+ /**
63
+ * 为名单逐键写 `allowBuilds.<name>: true`(保注释;allowBuilds 缺失时创建)。
64
+ * 坏 YAML / 根非映射 → throw(调用方按放行写入失败处理,走既有 hard-fail 转换契约)。
65
+ * names 为空返回原文。
66
+ */
67
+ export function applyPreciseBuilds(text, names) {
68
+ if (names.length === 0)
69
+ return text;
70
+ const document = parseDocument(text);
71
+ const error = document.errors[0];
72
+ if (error !== undefined)
73
+ throw error;
74
+ if (!isMap(document.contents))
75
+ throw new Error('pnpm-workspace.yaml 必须是 YAML 映射');
76
+ const existing = document.contents.get('allowBuilds');
77
+ if (existing !== undefined && existing !== null && !isMap(existing)) {
78
+ throw new Error('allowBuilds 必须是 YAML 映射');
79
+ }
80
+ for (const name of names) {
81
+ if (typeof name !== 'string' || !isPlainKey(name)) {
82
+ throw new Error(`精确放行拒绝通配/非法键: ${JSON.stringify(name)}`);
83
+ }
84
+ // setIn 自动创建缺失的中间映射(allowBuilds 缺失时一并创建)
85
+ document.setIn(['allowBuilds', name], true);
86
+ }
87
+ return String(document);
88
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * peer 兼容预检(0.4.0,plan Task 6;语义逐条对齐官方 evaluatePluginCompatibility
3
+ * ——dsh-app-boot L286-313,源码在 app-boot 而非 plugin-manager 本体):
4
+ *
5
+ * - 只检 `@deepseek-ai/dsh` 与 `@deepseek-ai/dsh-*` 键;
6
+ * - `workspace:^` / `workspace:~` / `workspace:*` 视为当前运行时版本(workspace 依赖
7
+ * 在发布时由运行时替换);
8
+ * - 空 range 不满足;semver satisfies 带 includePrerelease(rc 生态必需);
9
+ * - 运行时版本解析失败(null)→ 不拦(结果由调用方注明「未检」);
10
+ * - github 源不在此路径(无 packument),由 market 层提示 compatSkipped;
11
+ * - 无豁免机制(grilling Q6 定稿):确认/force 即通道。
12
+ */
13
+ import { satisfies } from 'semver';
14
+ import { npmVersion } from './versions.js';
15
+ import { resolveDshVersion } from './dsh-version.js';
16
+ const WORKSPACE_SPECS = ['workspace:^', 'workspace:~', 'workspace:*'];
17
+ export class IncompatibleError extends Error {
18
+ issue;
19
+ constructor(issue) {
20
+ super(`插件 ${issue.pkg}@${issue.version} 与 DSH ${issue.runtimeVersion} 不兼容(peerDependencies: ${JSON.stringify(issue.peers)})`);
21
+ this.issue = issue;
22
+ }
23
+ }
24
+ /**
25
+ * 纯函数:peers 中不满足运行时版本的 dsh 系键(空对象 = 兼容)。
26
+ * 非法 range 按 semver 抛错口径让位给 satisfies(异常上抛,调用方如实报告)。
27
+ */
28
+ export function evaluatePeers(peers, runtimeVersion) {
29
+ const failing = {};
30
+ for (const [name, range] of Object.entries(peers)) {
31
+ if (typeof range !== 'string')
32
+ continue;
33
+ if (name !== '@deepseek-ai/dsh' && !name.startsWith('@deepseek-ai/dsh-'))
34
+ continue;
35
+ const requirement = WORKSPACE_SPECS.includes(range) ? runtimeVersion : range;
36
+ if (requirement.trim() === '' || !satisfies(runtimeVersion, requirement, { includePrerelease: true })) {
37
+ failing[name] = range;
38
+ }
39
+ }
40
+ return failing;
41
+ }
42
+ /**
43
+ * npm 源预检:拉该精确版本的 peerDependencies 与运行时版本比对。
44
+ * 返回 null = 兼容或无法判定(runtime 解析失败 → 不拦)。
45
+ */
46
+ export async function precheckNpmCompat(pkg, version, opts) {
47
+ const runtime = opts.runtimeVersion !== undefined ? opts.runtimeVersion : await resolveDshVersion();
48
+ if (runtime === null || runtime === '')
49
+ return null;
50
+ const fetchVersion = opts.fetchVersion ?? npmVersion;
51
+ const meta = await fetchVersion(pkg, version, opts.timeoutMs, opts.signal, opts.registry);
52
+ const failing = evaluatePeers(meta.peers ?? {}, runtime);
53
+ if (Object.keys(failing).length === 0)
54
+ return null;
55
+ return { pkg, version, runtimeVersion: runtime, peers: failing };
56
+ }