@dsh-workbench/core 0.1.18 → 0.2.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.
Files changed (60) hide show
  1. package/client.js +1003 -537
  2. package/dist/client/client.body.js +1003 -537
  3. package/dist/client/index.js +1004 -538
  4. package/dist/host/index.js +1162 -145
  5. package/dist/types/app/client.d.ts +99 -39
  6. package/dist/types/app/layouts/market/__tests__/add-module-stages.test.d.ts +1 -0
  7. package/dist/types/app/layouts/market/add-module-text.d.ts +16 -1
  8. package/dist/types/app/layouts/market/use-add-module.d.ts +2 -8
  9. package/dist/types/app/layouts/market/use-module-versions.d.ts +2 -1
  10. package/dist/types/app/layouts/rightbar/MarketView.d.ts +5 -1
  11. package/dist/types/app/layouts/rightbar/ModulesPage.d.ts +4 -2
  12. package/dist/types/app/layouts/rightbar/PanelModules.d.ts +2 -2
  13. package/dist/types/app/layouts/rightbar/PanelPluginRows.d.ts +9 -0
  14. package/dist/types/app/layouts/rightbar/PanelRowControls.d.ts +14 -0
  15. package/dist/types/app/layouts/rightbar/__tests__/market-row-icon.test.d.ts +1 -0
  16. package/dist/types/app/layouts/rightbar/__tests__/market-section-icon.test.d.ts +1 -0
  17. package/dist/types/app/layouts/rightbar/__tests__/market-workbench.test.d.ts +1 -0
  18. package/dist/types/app/layouts/rightbar/__tests__/panel-plugin-rows.test.d.ts +1 -0
  19. package/dist/types/app/layouts/rightbar/__tests__/panel-scope-ui.test.d.ts +1 -0
  20. package/dist/types/app/layouts/rightbar/board-selection.d.ts +11 -0
  21. package/dist/types/app/layouts/rightbar/market-act.d.ts +21 -0
  22. package/dist/types/app/layouts/rightbar/market-mock.d.ts +6 -1
  23. package/dist/types/app/layouts/rightbar/market-plugin-rows.d.ts +3 -1
  24. package/dist/types/app/layouts/rightbar/market-row-icon.d.ts +7 -0
  25. package/dist/types/app/layouts/rightbar/market-row-types.d.ts +35 -0
  26. package/dist/types/app/layouts/rightbar/market-rows.d.ts +8 -35
  27. package/dist/types/app/layouts/rightbar/market-section-icon.d.ts +3 -0
  28. package/dist/types/app/layouts/rightbar/market-section.d.ts +18 -0
  29. package/dist/types/app/layouts/rightbar/market-view-text.d.ts +7 -1
  30. package/dist/types/app/layouts/rightbar/market-workbench-row.d.ts +5 -0
  31. package/dist/types/app/layouts/rightbar/panel-mock.d.ts +2 -1
  32. package/dist/types/app/layouts/rightbar/use-panels.d.ts +15 -1
  33. package/dist/types/app/units/registry.d.ts +1 -1
  34. package/dist/types/modules/panels/__tests__/plugin-panel-market-parity.test.d.ts +1 -0
  35. package/dist/types/modules/panels/__tests__/plugin-panel.test.d.ts +1 -0
  36. package/dist/types/modules/panels/__tests__/plugin-tool-units.test.d.ts +1 -0
  37. package/dist/types/modules/panels/__tests__/scope-machine.test.d.ts +1 -0
  38. package/dist/types/modules/panels/__tests__/scope-support.d.ts +51 -0
  39. package/dist/types/modules/panels/model/config.d.ts +19 -0
  40. package/dist/types/modules/panels/model/market-dispatch.d.ts +4 -0
  41. package/dist/types/modules/panels/model/market-registry.d.ts +12 -0
  42. package/dist/types/modules/panels/model/market-registry.test.d.ts +1 -0
  43. package/dist/types/modules/panels/model/market-types.d.ts +1 -0
  44. package/dist/types/modules/panels/model/market.d.ts +3 -2
  45. package/dist/types/modules/panels/model/plugin-panel.d.ts +3 -0
  46. package/dist/types/modules/panels/model/prefs.d.ts +3 -1
  47. package/dist/types/modules/panels/model/service.d.ts +5 -1
  48. package/dist/types/shared/api/bundle-install.d.ts +15 -1
  49. package/dist/types/shared/api/host/__tests__/plugin-catalog.test.d.ts +1 -0
  50. package/dist/types/shared/api/host/__tests__/plugin-login-hint.test.d.ts +1 -0
  51. package/dist/types/shared/api/host/__tests__/plugin-tools.test.d.ts +1 -0
  52. package/dist/types/shared/api/host/bundle-inspect-kind.test.d.ts +1 -0
  53. package/dist/types/shared/api/host/bundle-routes-self.test.d.ts +1 -0
  54. package/dist/types/shared/api/host/installed-module.d.ts +10 -0
  55. package/dist/types/shared/api/host/plugin-catalog.d.ts +60 -0
  56. package/dist/types/shared/api/host/plugin-login-hint.d.ts +2 -0
  57. package/dist/types/shared/api/host/plugin-tools.d.ts +10 -0
  58. package/dist/types/shared/api/host/plugin-units.d.ts +47 -0
  59. package/dist/types/shared/api/host/unit-registry.d.ts +3 -0
  60. package/package.json +2 -2
@@ -213,6 +213,71 @@ function parseJsonLoose(text) {
213
213
  }
214
214
  }
215
215
  //#endregion
216
+ //#region ../shared/dist/host/cli-error.js
217
+ function unquote(raw) {
218
+ const value = raw.trim();
219
+ if (value.length >= 2 && (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'"))) return value.slice(1, -1);
220
+ return value;
221
+ }
222
+ function joinBlock(previous, line, fold) {
223
+ if (previous === "") return line;
224
+ return fold ? `${previous} ${line}` : `${previous}\n${line}`;
225
+ }
226
+ function parseCliErrorEnvelope(text) {
227
+ if (text === void 0 || text.trim() === "") return void 0;
228
+ const lines = text.split("\n");
229
+ const start = lines.findIndex((line) => /^error:\s*$/.test(line.trim()) || /^error:\s*\S/.test(line.trim()));
230
+ if (start === -1) return void 0;
231
+ const scalars = {};
232
+ let current;
233
+ let block;
234
+ const errorIndent = lines[start]?.search(/\S/) ?? 0;
235
+ for (const line of lines.slice(start + 1)) {
236
+ if (line.trim() === "") continue;
237
+ const indent = line.search(/\S/);
238
+ if (block !== void 0 && indent > block.indent) {
239
+ scalars[block.key] = joinBlock(scalars[block.key] ?? "", line.trim(), block.fold);
240
+ continue;
241
+ }
242
+ block = void 0;
243
+ if (indent <= errorIndent) break;
244
+ const match = /^\s*(code|message|help|exitCode):\s*(.*)$/.exec(line);
245
+ if (match === null) {
246
+ if (current !== void 0) scalars[current] = `${scalars[current] ?? ""} ${line.trim()}`.trim();
247
+ continue;
248
+ }
249
+ const [, key, value] = match;
250
+ if (key === void 0) continue;
251
+ const raw = (value ?? "").trim();
252
+ const marker = /^([|>])[0-9]*([+-]?)$/.exec(raw);
253
+ if (marker !== null) {
254
+ scalars[key] = "";
255
+ block = {
256
+ key,
257
+ fold: marker[1] === ">",
258
+ indent
259
+ };
260
+ current = key;
261
+ continue;
262
+ }
263
+ scalars[key] = unquote(raw);
264
+ current = key;
265
+ }
266
+ const code = scalars["code"];
267
+ const message = scalars["message"];
268
+ if (code === void 0 || code === "") return void 0;
269
+ const exitCode = scalars["exitCode"];
270
+ return {
271
+ code,
272
+ message: message ?? code,
273
+ ...scalars["help"] === void 0 || scalars["help"] === "" ? {} : { help: scalars["help"] },
274
+ ...exitCode === void 0 || Number.isNaN(Number(exitCode)) ? {} : { exitCode: Number(exitCode) }
275
+ };
276
+ }
277
+ function helpUrlOf(envelope) {
278
+ return /https?:\/\/[^\s"'`]+/.exec(envelope.help ?? "")?.[0];
279
+ }
280
+ //#endregion
216
281
  //#region ../shared/dist/host/data-root.js
217
282
  function dshHome() {
218
283
  const configured = process.env.DSH_HOME?.trim();
@@ -282,20 +347,20 @@ var PERMISSION_CODES = /* @__PURE__ */ new Set([
282
347
  function asRecord$9(value) {
283
348
  return typeof value === "object" && value !== null ? value : void 0;
284
349
  }
285
- function textOf$4(value) {
350
+ function textOf$5(value) {
286
351
  return typeof value === "string" ? value.trim() : "";
287
352
  }
288
353
  function codeOf(page) {
289
354
  const record = asRecord$9(page);
290
- return record === void 0 ? "" : textOf$4(record["code"]);
355
+ return record === void 0 ? "" : textOf$5(record["code"]);
291
356
  }
292
- function messageOf$1(page) {
357
+ function messageOf$2(page) {
293
358
  const record = asRecord$9(page);
294
- return record === void 0 ? "" : textOf$4(record["message"]);
359
+ return record === void 0 ? "" : textOf$5(record["message"]);
295
360
  }
296
361
  function hintOf$1(page) {
297
362
  const record = asRecord$9(page);
298
- const hint = record === void 0 ? "" : textOf$4(record["hint"]);
363
+ const hint = record === void 0 ? "" : textOf$5(record["hint"]);
299
364
  return hint === "" ? void 0 : hint;
300
365
  }
301
366
  function pageFailed(page) {
@@ -484,7 +549,7 @@ async function browserRead(options) {
484
549
  message: "读到了"
485
550
  };
486
551
  const pageCode = codeOf(page) || "read-failed";
487
- const message = messageOf$1(page) || `读不到(${pageCode})`;
552
+ const message = messageOf$2(page) || `读不到(${pageCode})`;
488
553
  const blocker = blockerFor(pageCode);
489
554
  if (blocker === "auth-required") return {
490
555
  ok: false,
@@ -526,7 +591,7 @@ var DEFAULT_TEXT_MAX = 200;
526
591
  function asRecord$8(value) {
527
592
  return typeof value === "object" && value !== null ? value : void 0;
528
593
  }
529
- function textOf$3(value) {
594
+ function textOf$4(value) {
530
595
  return typeof value === "string" ? value.trim() : "";
531
596
  }
532
597
  function entriesOf(value) {
@@ -539,8 +604,8 @@ function entriesOf(value) {
539
604
  if (item === void 0) continue;
540
605
  const nth = typeof item["nth"] === "number" ? item["nth"] : entries.length;
541
606
  entries.push({
542
- text: textOf$3(item["text"]),
543
- tag: textOf$3(item["tag"]),
607
+ text: textOf$4(item["text"]),
608
+ tag: textOf$4(item["tag"]),
544
609
  nth
545
610
  });
546
611
  }
@@ -549,7 +614,7 @@ function entriesOf(value) {
549
614
  function reasonOf(stdout, stderr) {
550
615
  const parsed = asRecord$8(parseJsonLoose(stdout));
551
616
  const error = parsed === void 0 ? void 0 : asRecord$8(parsed["error"]);
552
- const message = error === void 0 ? "" : textOf$3(error["message"]);
617
+ const message = error === void 0 ? "" : textOf$4(error["message"]);
553
618
  if (message !== "") return message;
554
619
  const lines = (stderr.trim() === "" ? stdout : stderr).split("\n").map((one) => one.trim()).filter((one) => one !== "");
555
620
  return (lines[lines.length - 1] ?? "").slice(-200);
@@ -557,7 +622,7 @@ function reasonOf(stdout, stderr) {
557
622
  function hintOf(stdout) {
558
623
  const parsed = asRecord$8(parseJsonLoose(stdout));
559
624
  const error = parsed === void 0 ? void 0 : asRecord$8(parsed["error"]);
560
- const hint = error === void 0 ? "" : textOf$3(error["hint"]);
625
+ const hint = error === void 0 ? "" : textOf$4(error["hint"]);
561
626
  return hint === "" ? void 0 : hint;
562
627
  }
563
628
  function sameOrigin(left, right) {
@@ -571,7 +636,7 @@ function urlOf(stdout) {
571
636
  const bare = (stdout.split("\n").map((one) => one.trim()).filter((one) => one !== "")[0] ?? "").replace(/^url:\s*/i, "").trim().replace(/^["']+|["']+$/g, "");
572
637
  if (bare.startsWith("{")) {
573
638
  const parsed = asRecord$8(parseJsonLoose(bare));
574
- const url = parsed === void 0 ? "" : textOf$3(parsed["url"]);
639
+ const url = parsed === void 0 ? "" : textOf$4(parsed["url"]);
575
640
  if (url !== "") return url;
576
641
  }
577
642
  return bare;
@@ -796,7 +861,8 @@ var PROVISION_ACTIONS = [
796
861
  "cancel-install",
797
862
  "update-dependency",
798
863
  "update-plugin",
799
- "uninstall-plugin"
864
+ "uninstall-plugin",
865
+ "update-workbench"
800
866
  ];
801
867
  var DEPENDENCY_ACTIONS = ["update-dependency"];
802
868
  function isDependencyAction(action) {
@@ -807,6 +873,7 @@ function provisionTargetOf(action) {
807
873
  if (action === "install-bundle") return "spec";
808
874
  if (action === "remove-bundle") return "name";
809
875
  if (action === "update-plugin" || action === "uninstall-plugin") return "name";
876
+ if (action === "update-workbench") return "name";
810
877
  if (action === "cancel-install") return "requestId";
811
878
  return "unit";
812
879
  }
@@ -820,7 +887,7 @@ var TARGET_CODE = {
820
887
  function asRecord$7(value) {
821
888
  return typeof value === "object" && value !== null ? value : {};
822
889
  }
823
- function stringsOf$2(value) {
890
+ function stringsOf$3(value) {
824
891
  if (!Array.isArray(value)) return void 0;
825
892
  const items = value.filter((item) => typeof item === "string" && item.trim() !== "");
826
893
  return items.length === 0 ? void 0 : items;
@@ -842,6 +909,10 @@ function requestOf$1(action, value) {
842
909
  action,
843
910
  name: value
844
911
  };
912
+ if (action === "update-workbench") return {
913
+ action,
914
+ name: value
915
+ };
845
916
  if (action === "cancel-install") return {
846
917
  action,
847
918
  requestId: value
@@ -872,7 +943,7 @@ function parseProvisionBody(body) {
872
943
  request: requestOf$1(wanted, value.trim())
873
944
  };
874
945
  const registry = typeof raw.registry === "string" && raw.registry.trim() !== "" ? raw.registry.trim() : void 0;
875
- const approvedBuilds = stringsOf$2(raw.approvedBuilds);
946
+ const approvedBuilds = stringsOf$3(raw.approvedBuilds);
876
947
  const requestId = typeof raw.requestId === "string" && raw.requestId.trim() !== "" ? raw.requestId.trim() : void 0;
877
948
  return {
878
949
  ok: true,
@@ -886,6 +957,7 @@ function parseProvisionBody(body) {
886
957
  }
887
958
  //#endregion
888
959
  //#region ../shared/dist/host/opencli.js
960
+ var FORBIDDEN_CODES = /* @__PURE__ */ new Set(["FORBIDDEN", "PERMISSION_DENIED"]);
889
961
  var SITE_SESSION_ARGS = ["--site-session", "persistent"];
890
962
  var WorkbenchDataError = class extends Error {
891
963
  code;
@@ -913,6 +985,102 @@ function opencliCommandLine(command, argv, session = "persistent") {
913
985
  "json"
914
986
  ].join(" ");
915
987
  }
988
+ function createOpencli(options) {
989
+ const { shell, plugin, logger, command = "opencli", timeoutMs = OPENCLI_TIMEOUT_MS, maxConcurrent = 4 } = options;
990
+ const inflight = /* @__PURE__ */ new Map();
991
+ const MAX_CONCURRENT = maxConcurrent;
992
+ let running = 0;
993
+ const waiting = [];
994
+ async function acquire() {
995
+ if (running < MAX_CONCURRENT) {
996
+ running += 1;
997
+ return;
998
+ }
999
+ await new Promise((resolve) => waiting.push(resolve));
1000
+ }
1001
+ function release() {
1002
+ const next = waiting.shift();
1003
+ if (next === void 0) running -= 1;
1004
+ else next();
1005
+ }
1006
+ async function runLimited(task) {
1007
+ await acquire();
1008
+ try {
1009
+ return await task();
1010
+ } finally {
1011
+ release();
1012
+ }
1013
+ }
1014
+ async function call(commandLine, workdir) {
1015
+ let result;
1016
+ try {
1017
+ result = await runShell(shell, commandLine, {
1018
+ ...workdir === void 0 ? {} : { workdir },
1019
+ timeoutMs
1020
+ });
1021
+ } catch (error) {
1022
+ throw new WorkbenchDataError("cli-exec-failed", `执行 opencli 失败:${String(error)}`, { hint: "确认 opencli 已安装且在 PATH 里(which opencli)。" });
1023
+ }
1024
+ if (result.timedOut) throw new WorkbenchDataError("cli-timeout", `opencli 超过 ${Math.round(timeoutMs / 1e3)} 秒没有返回`, { hint: "无头浏览器可能没起来,重试一次通常就好。" });
1025
+ if (result.exitCode !== 0) {
1026
+ const detail = tailLines(result.stderr || result.stdout, 6);
1027
+ const envelope = parseCliErrorEnvelope(result.stderr) ?? parseCliErrorEnvelope(result.stdout);
1028
+ const url = envelope === void 0 ? void 0 : helpUrlOf(envelope);
1029
+ if (envelope?.code === "AUTH_REQUIRED") throw new WorkbenchDataError("auth-required", envelope.message, {
1030
+ detail,
1031
+ ...url === void 0 ? {} : { helpUrl: url }
1032
+ });
1033
+ if (envelope !== void 0 && FORBIDDEN_CODES.has(envelope.code)) throw new WorkbenchDataError("permission-denied", envelope.message, {
1034
+ detail,
1035
+ ...url === void 0 ? {} : { helpUrl: url }
1036
+ });
1037
+ if (envelope !== void 0) throw new WorkbenchDataError("cli-command-failed", `${envelope.code}:${envelope.message}`, {
1038
+ detail,
1039
+ ...envelope.help === void 0 ? {} : { hint: envelope.help }
1040
+ });
1041
+ throw new WorkbenchDataError("cli-unclassified", `opencli 这条命令没跑通(退出码 ${result.exitCode}),而且没有结构化错误码`, {
1042
+ detail,
1043
+ hint: "多半是插件里没有这条命令(版本旧就会缺),也可能是命令自己失败了:先确认命令存在,再更新插件重试。"
1044
+ });
1045
+ }
1046
+ const parsed = parseJsonLoose(result.stdout);
1047
+ if (parsed === void 0) throw new WorkbenchDataError("cli-bad-output", "opencli 输出不是 JSON", { detail: tailLines(result.stdout, 4) });
1048
+ return parsed;
1049
+ }
1050
+ async function json(args, callOptions = {}) {
1051
+ const { key, workdir } = callOptions;
1052
+ const session = callOptions.session ?? "persistent";
1053
+ const callKey = key ?? args.map((arg) => String(arg)).join(" ");
1054
+ const pending = inflight.get(callKey);
1055
+ if (pending !== void 0) return pending;
1056
+ const commandLine = opencliCommandLine(command, [plugin, ...args], session);
1057
+ const once = async () => {
1058
+ return {
1059
+ value: await call(commandLine, workdir),
1060
+ at: Date.now()
1061
+ };
1062
+ };
1063
+ const task = runLimited(async () => {
1064
+ try {
1065
+ return await once();
1066
+ } catch (error) {
1067
+ if (!(error instanceof WorkbenchDataError) || error.code !== "auth-required") throw error;
1068
+ return await once();
1069
+ }
1070
+ });
1071
+ inflight.set(callKey, task);
1072
+ try {
1073
+ return await task;
1074
+ } catch (error) {
1075
+ const message = error instanceof Error ? error.message : String(error);
1076
+ logger?.warn?.(`[dsh-workbench] opencli 调用失败:${message}`);
1077
+ throw error;
1078
+ } finally {
1079
+ inflight.delete(callKey);
1080
+ }
1081
+ }
1082
+ return { json };
1083
+ }
916
1084
  //#endregion
917
1085
  //#region ../shared/dist/host/write-result.js
918
1086
  function writeResultForClient(value, code) {
@@ -1447,7 +1615,7 @@ var PLUGIN_LEDGER_FILE = "plugins.json";
1447
1615
  function ledgerKeyOf(entry) {
1448
1616
  return `${entry.opencliHome}\u0000${entry.package}`;
1449
1617
  }
1450
- function textOf$2(value) {
1618
+ function textOf$3(value) {
1451
1619
  if (typeof value !== "string") return void 0;
1452
1620
  const trimmed = value.trim();
1453
1621
  return trimmed === "" ? void 0 : trimmed;
@@ -1455,13 +1623,13 @@ function textOf$2(value) {
1455
1623
  function entryOf(value) {
1456
1624
  if (typeof value !== "object" || value === null) return void 0;
1457
1625
  const box = value;
1458
- const opencliHome = textOf$2(box["opencliHome"]);
1459
- const pkg = textOf$2(box["package"]);
1460
- const installedAt = textOf$2(box["installedAt"]);
1626
+ const opencliHome = textOf$3(box["opencliHome"]);
1627
+ const pkg = textOf$3(box["package"]);
1628
+ const installedAt = textOf$3(box["installedAt"]);
1461
1629
  if (opencliHome === void 0 || pkg === void 0 || installedAt === void 0) return void 0;
1462
- const name = textOf$2(box["name"]);
1463
- const registry = textOf$2(box["registry"]);
1464
- const releasedAt = textOf$2(box["releasedAt"]);
1630
+ const name = textOf$3(box["name"]);
1631
+ const registry = textOf$3(box["registry"]);
1632
+ const releasedAt = textOf$3(box["releasedAt"]);
1465
1633
  return {
1466
1634
  opencliHome,
1467
1635
  package: pkg,
@@ -1628,7 +1796,7 @@ function resolvePluginSource(source, env, options = {}) {
1628
1796
  }
1629
1797
  //#endregion
1630
1798
  //#region ../shared/dist/host/tool-approval.js
1631
- function messageOf(error) {
1799
+ function messageOf$1(error) {
1632
1800
  return error instanceof Error ? error.message : String(error);
1633
1801
  }
1634
1802
  function compose(detail, verdict) {
@@ -1646,7 +1814,7 @@ async function approvalFor(call, context = {}) {
1646
1814
  } catch (error) {
1647
1815
  return {
1648
1816
  kind: "deny",
1649
- reason: `问"要不要审批"的时候套件的策略抛了错,已经停下:${messageOf(error)}`
1817
+ reason: `问"要不要审批"的时候套件的策略抛了错,已经停下:${messageOf$1(error)}`
1650
1818
  };
1651
1819
  }
1652
1820
  if (verdict?.kind === "read") return void 0;
@@ -1660,7 +1828,7 @@ async function approvalFor(call, context = {}) {
1660
1828
  } catch (error) {
1661
1829
  return {
1662
1830
  kind: "deny",
1663
- reason: `拿不到"这次会发生什么",已经停下:${messageOf(error)}`
1831
+ reason: `拿不到"这次会发生什么",已经停下:${messageOf$1(error)}`
1664
1832
  };
1665
1833
  }
1666
1834
  if (facts !== void 0 && "blocked" in facts) return {
@@ -1679,6 +1847,15 @@ function sessionWorkspace(exec) {
1679
1847
  const cwd = exec?.agent?.session?.header?.cwd;
1680
1848
  return typeof cwd === "string" && cwd.trim() !== "" ? cwd.trim() : null;
1681
1849
  }
1850
+ function textOutput(text) {
1851
+ return [{
1852
+ type: "text",
1853
+ text
1854
+ }];
1855
+ }
1856
+ function jsonText(value) {
1857
+ return JSON.stringify(value);
1858
+ }
1682
1859
  //#endregion
1683
1860
  //#region ../shared/dist/host/value-handoff.js
1684
1861
  function handoffId(entry) {
@@ -2000,7 +2177,7 @@ function profileDirOf(ctx) {
2000
2177
  const dir = typeof profile?.dir === "string" ? profile.dir.trim() : "";
2001
2178
  return dir === "" ? null : dir;
2002
2179
  }
2003
- function stringsOf$1(value) {
2180
+ function stringsOf$2(value) {
2004
2181
  return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
2005
2182
  }
2006
2183
  function profileManifestOf(profileDir) {
@@ -2014,7 +2191,7 @@ function profileManifestOf(profileDir) {
2014
2191
  function profileModules(profileDir) {
2015
2192
  const record = profileManifestOf(profileDir);
2016
2193
  if (record === void 0) return [];
2017
- const bundles = stringsOf$1(asRecord$5(asRecord$5(record.dsh)?.profile)?.bundles);
2194
+ const bundles = stringsOf$2(asRecord$5(asRecord$5(record.dsh)?.profile)?.bundles);
2018
2195
  const dependencies = asRecord$5(record.dependencies) ?? {};
2019
2196
  const names = bundles.length > 0 ? bundles : Object.keys(dependencies);
2020
2197
  const found = [];
@@ -2025,7 +2202,7 @@ function profileModules(profileDir) {
2025
2202
  unit: read.unit,
2026
2203
  version: read.version,
2027
2204
  ...read.registry === void 0 ? {} : { registry: read.registry },
2028
- local: asText$3(dependencies[read.pkg]).startsWith("link:")
2205
+ local: isLocalDependencySpec(asText$3(dependencies[read.pkg]))
2029
2206
  });
2030
2207
  }
2031
2208
  return found;
@@ -2070,6 +2247,20 @@ function declarationGaps(profileDir) {
2070
2247
  }
2071
2248
  return gaps;
2072
2249
  }
2250
+ function declaredPluginCommands(profileDir) {
2251
+ const declared = /* @__PURE__ */ new Map();
2252
+ for (const module of profileModules(profileDir)) {
2253
+ const read = readDeclaration(module.pkg, moduleDeclarationOf(profileDir, module.pkg));
2254
+ for (const need of read.manifest?.env?.plugins ?? []) {
2255
+ const name = need.name.trim();
2256
+ if (name === "") continue;
2257
+ const commands = declared.get(name) ?? /* @__PURE__ */ new Set();
2258
+ for (const command of need.commands ?? []) commands.add(command);
2259
+ declared.set(name, commands);
2260
+ }
2261
+ }
2262
+ return declared;
2263
+ }
2073
2264
  function readInstalledModule(profileDir, pkg) {
2074
2265
  if (profileDir === null || !isPackageName(pkg)) return {
2075
2266
  kind: "unknown",
@@ -2105,6 +2296,32 @@ function readInstalledModule(profileDir, pkg) {
2105
2296
  ...registry === "" ? {} : { registry }
2106
2297
  };
2107
2298
  }
2299
+ function isLocalDependencySpec(spec) {
2300
+ const trimmed = spec.trim();
2301
+ return trimmed.startsWith("link:") || trimmed.startsWith("file:") || trimmed.startsWith("./") || trimmed.startsWith("../") || trimmed.startsWith("/");
2302
+ }
2303
+ function readProfilePackage(profileDir, pkg) {
2304
+ if (profileDir === null || !isPackageName(pkg)) return void 0;
2305
+ let manifest;
2306
+ try {
2307
+ manifest = JSON.parse(fs.readFileSync(path.join(profileDir, "node_modules", pkg, "package.json"), "utf8"));
2308
+ } catch {
2309
+ return;
2310
+ }
2311
+ const record = asRecord$5(manifest);
2312
+ if (record === void 0) return void 0;
2313
+ const version = asText$3(record.version);
2314
+ if (version === "") return void 0;
2315
+ const declaredRegistry = asText$3(asRecord$5(record.publishConfig)?.registry);
2316
+ const registry = declaredRegistry === "" ? registryFromLockfile(profileDir, pkg) : declaredRegistry;
2317
+ const local = isLocalDependencySpec(asText$3((asRecord$5(profileManifestOf(profileDir)?.dependencies) ?? {})[pkg]));
2318
+ return {
2319
+ pkg,
2320
+ version,
2321
+ ...registry === "" ? {} : { registry },
2322
+ ...local ? { local: true } : {}
2323
+ };
2324
+ }
2108
2325
  //#endregion
2109
2326
  //#region src/shared/api/host/profile-registries.ts
2110
2327
  var REGISTRY_MARKER = "# dsh-workbench 写的:这一行的包只在这个源上(dsh 卸载没有源参数,pnpm 得问得到)";
@@ -2350,6 +2567,13 @@ var panelsApproval = (call) => {
2350
2567
  zh: `agent 想${verb} opencli 插件 ${what}(会改这台机器上装的东西)。允许吗?`
2351
2568
  });
2352
2569
  }
2570
+ if (action === "update-workbench") return askOf("workbench_modules update-workbench replaces the workbench itself with the new version from the registry", {
2571
+ en: "it updates the workbench itself on this machine (the panel picks the new version up by itself). Allow?",
2572
+ zh: "会把工作台自己更新到源上的新版(面板会自己换新)。允许吗?"
2573
+ }, {
2574
+ en: "The agent wants to update the workbench itself to the new version from the registry (it changes what is installed on this machine; the panel picks the new version up by itself). Allow?",
2575
+ zh: "agent 想把工作台自己更新到源上的新版(会改这台机器上装的东西,面板会自己换新)。允许吗?"
2576
+ });
2353
2577
  if (action === "update-dependency") return askOf(`workbench_modules update-dependency ${id ?? ""} upgrades a shared dependency on this machine`, {
2354
2578
  en: "it upgrades that shared dependency on this machine. Allow?",
2355
2579
  zh: `会升级共用依赖 ${what}。允许吗?`
@@ -2423,13 +2647,20 @@ var EMPTY_BOARD = {
2423
2647
  };
2424
2648
  var BOARD_TARGETS = ["workspace", "machine"];
2425
2649
  var DEFAULT_BOARD_TARGET = "workspace";
2426
- function resolveScope(query) {
2427
- const { id, boards } = query;
2428
- const board = boards.workspace ?? boards.machine;
2650
+ function overrideOf(board, id) {
2651
+ if (board === null) return void 0;
2652
+ if (board.hidden.includes(id)) return false;
2653
+ if (board.shown.includes(id)) return true;
2654
+ }
2655
+ function defaultOf(board, id) {
2429
2656
  if (board === null) return false;
2430
2657
  if (board.hidden.includes(id)) return false;
2431
2658
  return board.shown.includes(id);
2432
2659
  }
2660
+ function resolveScope(query) {
2661
+ const { id, boards } = query;
2662
+ return overrideOf(boards.workspace, id) ?? defaultOf(boards.machine, id);
2663
+ }
2433
2664
  function strings(raw) {
2434
2665
  return Array.isArray(raw) ? raw.filter((item) => typeof item === "string") : [];
2435
2666
  }
@@ -2446,7 +2677,7 @@ function parseReleased(raw) {
2446
2677
  return strings(raw.ids);
2447
2678
  }
2448
2679
  function resolvePanels(manifests, boards) {
2449
- const ordered = boards.workspace?.shown ?? boards.machine?.shown ?? [];
2680
+ const ordered = [.../* @__PURE__ */ new Set([...boards.workspace?.shown ?? [], ...boards.machine?.shown ?? []])];
2450
2681
  const rank = new Map(ordered.map((id, index) => [id, index]));
2451
2682
  return manifests.filter((manifest) => manifest.card !== null).map((manifest) => {
2452
2683
  const on = resolveScope({
@@ -2459,11 +2690,39 @@ function resolvePanels(manifests, boards) {
2459
2690
  titleKey: manifest.card?.titleKey ?? manifest.id,
2460
2691
  order,
2461
2692
  shown: on,
2462
- source: on && (boards.workspace?.shown.includes(manifest.id) ?? false) ? "user" : "default",
2693
+ source: overrideOf(boards.workspace, manifest.id) === void 0 ? "default" : "user",
2463
2694
  hasCard: true
2464
2695
  };
2465
2696
  }).sort((a, b) => a.order - b.order || a.id.localeCompare(b.id));
2466
2697
  }
2698
+ var PLUGIN_PANEL_PREFIX = "plugin:";
2699
+ function pluginPanelId(pkg) {
2700
+ return `${PLUGIN_PANEL_PREFIX}${pkg}`;
2701
+ }
2702
+ function resolvePluginPanels(plugins, boards) {
2703
+ const ordered = [.../* @__PURE__ */ new Set([...boards.workspace?.shown ?? [], ...boards.machine?.shown ?? []])];
2704
+ const rank = new Map(ordered.map((id, index) => [id, index]));
2705
+ const seen = /* @__PURE__ */ new Set();
2706
+ return plugins.map((plugin) => {
2707
+ const id = pluginPanelId(plugin.pkg);
2708
+ return {
2709
+ id,
2710
+ pkg: plugin.pkg,
2711
+ title: plugin.title,
2712
+ ...plugin.name === void 0 ? {} : { name: plugin.name },
2713
+ order: rank.get(id) ?? ordered.length,
2714
+ shown: resolveScope({
2715
+ id,
2716
+ boards
2717
+ }),
2718
+ source: overrideOf(boards.workspace, id) === void 0 ? "default" : "user"
2719
+ };
2720
+ }).filter((entry) => {
2721
+ if (seen.has(entry.id)) return false;
2722
+ seen.add(entry.id);
2723
+ return true;
2724
+ }).sort((a, b) => a.order - b.order || a.pkg.localeCompare(b.pkg));
2725
+ }
2467
2726
  function withoutReleased(released, id) {
2468
2727
  return released.filter((item) => item !== id);
2469
2728
  }
@@ -2601,7 +2860,7 @@ var PLUGIN_PREFIX = "opencli-plugin-";
2601
2860
  function text(value) {
2602
2861
  return typeof value === "string" ? value.trim() : "";
2603
2862
  }
2604
- function stringsOf(value) {
2863
+ function stringsOf$1(value) {
2605
2864
  return Array.isArray(value) ? value.filter((one) => typeof one === "string") : [];
2606
2865
  }
2607
2866
  function manifestCommandsOf(raw) {
@@ -2632,7 +2891,7 @@ function manifestCommandsOf(raw) {
2632
2891
  strategy: text(row["strategy"]).toLowerCase(),
2633
2892
  browser: row["browser"] === true,
2634
2893
  example: text(row["example"]),
2635
- columns: stringsOf(row["columns"])
2894
+ columns: stringsOf$1(row["columns"])
2636
2895
  });
2637
2896
  }
2638
2897
  return commands;
@@ -2724,7 +2983,7 @@ function reconOf(commands, plugins, keywords) {
2724
2983
  };
2725
2984
  return {
2726
2985
  ...withNext,
2727
- text: textOf$1(withNext)
2986
+ text: textOf$2(withNext)
2728
2987
  };
2729
2988
  }
2730
2989
  function nextOf(answer) {
@@ -2737,7 +2996,7 @@ function nextOf(answer) {
2737
2996
  if (plugin !== void 0) return `这台机器另装的插件 ${plugin.name} 里有匹配:opencli ${plugin.name} --help 看全部命令`;
2738
2997
  return "这台机器上没有现成的 —— 两条路:① 给这个站点写 adapter:opencli skills read opencli-adapter-author;② 只读一次:workbench_diy action=probe(script 读接口 / css 读 DOM)";
2739
2998
  }
2740
- function textOf$1(answer) {
2999
+ function textOf$2(answer) {
2741
3000
  const lines = [];
2742
3001
  if (answer.sites.length > 0) {
2743
3002
  lines.push(`opencli 自带这 ${String(answer.sites.length)} 组能力(不用装、不用点头):`);
@@ -2782,7 +3041,7 @@ function installedPluginsOf(dir = vendorPluginsDir()) {
2782
3041
  }
2783
3042
  //#endregion
2784
3043
  //#region src/modules/panels/model/diy-tools.ts
2785
- var ACTIONS$1 = [
3044
+ var ACTIONS$2 = [
2786
3045
  "list",
2787
3046
  "status",
2788
3047
  "probe",
@@ -2919,7 +3178,7 @@ function registerDiyTools(ctx, market, panels) {
2919
3178
  action: {
2920
3179
  type: "string",
2921
3180
  description: "list=工坊里有哪些模块(默认);status=某个模块现在走到哪一步、下一步做什么;probe=在用户浏览器里探一条读;recon=先查 opencli 现成的能力(自带 adapter / 本机插件);open=只把它在这个项目上板(不装);install=装上并上板(第一次、会让用户点头)。",
2922
- enum: [...ACTIONS$1]
3181
+ enum: [...ACTIONS$2]
2923
3182
  },
2924
3183
  unit: {
2925
3184
  type: "string",
@@ -2978,7 +3237,7 @@ function registerDiyTools(ctx, market, panels) {
2978
3237
  async execute(raw, exec) {
2979
3238
  const args = asRecord$3(raw) ?? {};
2980
3239
  const action = asText$1(args.action) || "list";
2981
- if (!ACTIONS$1.includes(action)) throw new Error(`不认识的 action:${action}(可用:${ACTIONS$1.join(" / ")})`);
3240
+ if (!ACTIONS$2.includes(action)) throw new Error(`不认识的 action:${action}(可用:${ACTIONS$2.join(" / ")})`);
2982
3241
  const packagesDir = workshopPackagesDir(ctx);
2983
3242
  if (packagesDir === null) throw new Error("算不出 DIY 工坊在哪(这台机器没给出 dshHomePath)—— 让用户跑一次 dsh-workbench setup");
2984
3243
  const session = sessionWorkspace(exec);
@@ -3907,9 +4166,45 @@ function createEnvDropper(options) {
3907
4166
  };
3908
4167
  }
3909
4168
  //#endregion
4169
+ //#region src/modules/panels/model/market-registry.ts
4170
+ async function rememberInstalledSource(deps, manager, pkg, registry) {
4171
+ const { registries, logger } = deps;
4172
+ if (registries === void 0 || registry === void 0) return;
4173
+ const scope = scopeOf(pkg);
4174
+ if (scope === null) return;
4175
+ const known = await manager.registries().catch(() => null);
4176
+ if (known !== null && sameRegistry(known.resolved, registry)) return;
4177
+ try {
4178
+ const added = registries.remember([{
4179
+ scope,
4180
+ registry
4181
+ }]);
4182
+ if (added.length > 0) logger?.info?.(`[dsh-workbench] 把 ${added.join("、")} 记进 profile 的 .npmrc(卸载只认 pnpm 的配置)`);
4183
+ } catch (error) {
4184
+ logger?.warn?.(`[dsh-workbench] 记这个 scope 的源失败:${String(error)}`);
4185
+ }
4186
+ }
4187
+ async function retryAfterRegistryFix(deps, manager, pkg, change) {
4188
+ const { registries, logger } = deps;
4189
+ if (registries === void 0 || !registryFailure(change)) return change;
4190
+ const known = await manager.registries().catch(() => null);
4191
+ const missing = registries.sources().filter((entry) => !sameRegistry(entry.registry, known?.resolved ?? null));
4192
+ if (missing.length === 0) return change;
4193
+ let added;
4194
+ try {
4195
+ added = registries.remember(missing);
4196
+ } catch (error) {
4197
+ logger?.warn?.(`[dsh-workbench] 卸 ${pkg} 撞上源,但补配置失败:${String(error)}`);
4198
+ return change;
4199
+ }
4200
+ if (added.length === 0) return change;
4201
+ logger?.warn?.(`[dsh-workbench] 卸 ${pkg} 撞上源:补 ${added.join("、")} 后重试一次`);
4202
+ return await manager.remove(pkg).catch(() => null) ?? change;
4203
+ }
4204
+ //#endregion
3910
4205
  //#region src/modules/panels/model/market.ts
3911
4206
  function createMarketService(options) {
3912
- const { manifests, manifestsNow, probe, shell, logger, onInstalled, onUninstalled, isReleased, bundles, installed, modules, registries, ledger, references } = options;
4207
+ const { manifests, manifestsNow, probe, shell, logger, onInstalled, onUninstalled, isReleased, bundles, installed, modules, workbench, registries, ledger, references } = options;
3913
4208
  const command = options.command ?? "opencli";
3914
4209
  const catalog = () => (manifestsNow?.() ?? manifests).filter((manifest) => (manifest.env?.plugins ?? []).length > 0 || (manifest.env?.libs ?? []).length > 0);
3915
4210
  function requireEntry(unit) {
@@ -4054,44 +4349,16 @@ function createMarketService(options) {
4054
4349
  };
4055
4350
  }
4056
4351
  }
4057
- async function rememberInstalledSource(manager, pkg, registry) {
4058
- if (registries === void 0 || registry === void 0) return;
4059
- const scope = scopeOf(pkg);
4060
- if (scope === null) return;
4061
- const known = await manager.registries().catch(() => null);
4062
- if (known !== null && sameRegistry(known.resolved, registry)) return;
4063
- try {
4064
- const added = registries.remember([{
4065
- scope,
4066
- registry
4067
- }]);
4068
- if (added.length > 0) logger?.info?.(`[dsh-workbench] 把 ${added.join("、")} 记进 profile 的 .npmrc(卸载只认 pnpm 的配置)`);
4069
- } catch (error) {
4070
- logger?.warn?.(`[dsh-workbench] 记这个 scope 的源失败:${String(error)}`);
4071
- }
4072
- }
4073
- async function retryAfterRegistryFix(manager, pkg, change) {
4074
- if (registries === void 0 || !registryFailure(change)) return change;
4075
- const known = await manager.registries().catch(() => null);
4076
- const missing = registries.sources().filter((entry) => !sameRegistry(entry.registry, known?.resolved ?? null));
4077
- if (missing.length === 0) return change;
4078
- let added;
4079
- try {
4080
- added = registries.remember(missing);
4081
- } catch (error) {
4082
- logger?.warn?.(`[dsh-workbench] 卸 ${pkg} 撞上源,但补配置失败:${String(error)}`);
4083
- return change;
4084
- }
4085
- if (added.length === 0) return change;
4086
- logger?.warn?.(`[dsh-workbench] 卸 ${pkg} 撞上源:补 ${added.join("、")} 后重试一次`);
4087
- return await manager.remove(pkg).catch(() => null) ?? change;
4088
- }
4352
+ const registryFix = {
4353
+ ...registries === void 0 ? {} : { registries },
4354
+ ...logger === void 0 ? {} : { logger }
4355
+ };
4089
4356
  function routeDeps(manager) {
4090
4357
  return {
4091
4358
  manager,
4092
4359
  command,
4093
4360
  runAll,
4094
- rememberSource: rememberInstalledSource,
4361
+ rememberSource: (manager, pkg, registry) => rememberInstalledSource(registryFix, manager, pkg, registry),
4095
4362
  supplyAfter,
4096
4363
  ...shell === void 0 ? {} : { shell },
4097
4364
  ...installed === void 0 ? {} : { installed },
@@ -4237,7 +4504,7 @@ function createMarketService(options) {
4237
4504
  const before = unit === void 0 ? void 0 : await capabilityOf(unit, true);
4238
4505
  const first = await manager.remove(pkg);
4239
4506
  if (first === null) throw new Error("插件管理器回的卸载结果看不懂。");
4240
- const change = await retryAfterRegistryFix(manager, pkg, first);
4507
+ const change = await retryAfterRegistryFix(registryFix, manager, pkg, first);
4241
4508
  if (change.application !== "applied" || unit === void 0) return {
4242
4509
  change,
4243
4510
  keptFor: []
@@ -4273,6 +4540,24 @@ function createMarketService(options) {
4273
4540
  await service.update(unit).catch(() => void 0);
4274
4541
  return outcome;
4275
4542
  },
4543
+ async updateWorkbench(pkg) {
4544
+ const target = pkg.trim() === "" ? PLUGIN_ID : pkg.trim();
4545
+ if (target !== "@dsh-workbench/core") throw new Error(`这条路只更新工作台自己那一份(${PLUGIN_ID}):${target}`);
4546
+ const read = workbench?.(target);
4547
+ if (read === void 0) throw new Error(`读不到 profile 里装的 ${target} —— 没法判断装的是哪一份,更新做不了。`);
4548
+ if (read.local === true) throw new Error("这份工作台是从本仓装进来的(开发副本),不从源上更新。");
4549
+ const registry = read.registry;
4550
+ const latest = shell === void 0 || registry === void 0 ? void 0 : await latestVersion({
4551
+ shell,
4552
+ name: read.pkg,
4553
+ registry,
4554
+ force: true
4555
+ });
4556
+ const spec = latest === void 0 ? `${read.pkg}@latest` : `${read.pkg}@${latest}`;
4557
+ const outcome = await installDshBundle(routeDeps(requireBundles()), spec, { ...registry === void 0 ? {} : { registry } });
4558
+ logger?.info?.(`[dsh-workbench] 已更新工作台 ${read.version} → ${latest ?? "最新版"}`);
4559
+ return outcome;
4560
+ },
4276
4561
  async updatePlugin(pkg) {
4277
4562
  const outcome = await plugins.update(pkg);
4278
4563
  await reprobePlugin(pkg, outcome.name);
@@ -4337,6 +4622,11 @@ async function dispatchMarket(market, request) {
4337
4622
  action: "cancel-install",
4338
4623
  cancellation: await market.cancelInstall(request.requestId ?? "")
4339
4624
  };
4625
+ if (request.action === "update-workbench") return {
4626
+ ok: true,
4627
+ action: "update-workbench",
4628
+ change: (await market.updateWorkbench(request.name ?? "")).change
4629
+ };
4340
4630
  if (request.action === "update-plugin") {
4341
4631
  await market.updatePlugin(request.name ?? "");
4342
4632
  return {
@@ -4431,12 +4721,16 @@ function describePluginUninstall(pkg, outcome) {
4431
4721
  if (outcome.removed) return `已卸载 opencli 插件 ${pkg}:没人再用它,文件也删了。`;
4432
4722
  return `已从「你装的插件」里去掉 ${pkg}。${outcome.kept === void 0 ? "" : pluginKeptText(outcome.kept)}`;
4433
4723
  }
4724
+ function describeWorkbenchUpdate(change) {
4725
+ if (change.application === "failed" || change.application === "cancelled") return `没成功:${change.errorCode ?? change.application}`;
4726
+ return change.application === "restart-required" ? "工作台更新到新版了 —— 这一版要重启一次 dsh 才生效(重启由用户来,别自己重启)。" : "工作台更新到新版了 —— 客户端与宿主自己换上,不用重启。";
4727
+ }
4434
4728
  function usersOf(shared, names) {
4435
4729
  const hit = shared.find((item) => names.some((name) => name !== void 0 && name !== "" && item.id === `plugin:${name}`));
4436
4730
  if (hit === void 0 || hit.usedBy.length === 0) return "";
4437
4731
  return `它被 ${hit.usedBy.join("、")} 用着(动的是它们的供给,它们的状态跟着重探了一遍)`;
4438
4732
  }
4439
- var ACTIONS = [
4733
+ var ACTIONS$1 = [
4440
4734
  "list",
4441
4735
  "install",
4442
4736
  "update",
@@ -4444,7 +4738,8 @@ var ACTIONS = [
4444
4738
  "install-bundle",
4445
4739
  "update-dependency",
4446
4740
  "update-plugin",
4447
- "uninstall-plugin"
4741
+ "uninstall-plugin",
4742
+ "update-workbench"
4448
4743
  ];
4449
4744
  function registerMarketTools(ctx, market) {
4450
4745
  const tools = ctx.get("tools");
@@ -4454,14 +4749,14 @@ function registerMarketTools(ctx, market) {
4454
4749
  }
4455
4750
  const tool = {
4456
4751
  name: "workbench_modules",
4457
- description: "看 / 装 / 升 / 卸工作台的面板模块,以及用户自己装的 opencli 插件。install-bundle 按 spec(包名,可带版本;git / tarball / 本地路径也行)把模块包装进 profile —— 装完会连带装它声明的 opencli 插件;其余 action 按模块 id 动它要的外部供给;update-plugin / uninstall-plugin 按包名或插件名动用户装的那份插件。卸载只动工作台运行时,不影响终端那套;共用依赖只有升级。",
4752
+ description: "看 / 装 / 升 / 卸工作台的面板模块,以及用户自己装的 opencli 插件。install-bundle 按 spec(包名,可带版本;git / tarball / 本地路径也行)把模块包装进 profile —— 装完会连带装它声明的 opencli 插件;其余 action 按模块 id 动它要的外部供给;update-plugin / uninstall-plugin 按包名或插件名动用户装的那份插件;update-workbench 把工作台自己更新到源上的新版(客户端与宿主一般会自己换上;真要重启时结果里会说,重启由用户来,别自己重启)。卸载只动工作台运行时,不影响终端那套;共用依赖只有升级。",
4458
4753
  parameters: {
4459
4754
  type: "object",
4460
4755
  properties: {
4461
4756
  action: {
4462
4757
  type: "string",
4463
- description: "install-bundle=按 spec 装模块包(要 spec),update-dependency=升级共用依赖(要 id),update-plugin / uninstall-plugin=动用户自己装的 opencli 插件(要 plugin),其余=对模块(要 id)。",
4464
- enum: [...ACTIONS]
4758
+ description: "install-bundle=按 spec 装模块包(要 spec),update-dependency=升级共用依赖(要 id),update-plugin / uninstall-plugin=动用户自己装的 opencli 插件(要 plugin),update-workbench=把工作台自己更新到新版(不用别的参数),其余=对模块(要 id)。",
4759
+ enum: [...ACTIONS$1]
4465
4760
  },
4466
4761
  id: {
4467
4762
  type: "string",
@@ -4495,7 +4790,7 @@ function registerMarketTools(ctx, market) {
4495
4790
  },
4496
4791
  async execute(args) {
4497
4792
  const action = args?.action ?? "list";
4498
- if (!ACTIONS.includes(action)) throw new Error(`不认识的 action:${action}(可用:${ACTIONS.join(" / ")})`);
4793
+ if (!ACTIONS$1.includes(action)) throw new Error(`不认识的 action:${action}(可用:${ACTIONS$1.join(" / ")})`);
4499
4794
  if (action === "list") {
4500
4795
  const { modules, shared } = await market.list();
4501
4796
  return {
@@ -4516,6 +4811,13 @@ function registerMarketTools(ctx, market) {
4516
4811
  text: describeInstall(spec, outcome.module, outcome.supply, outcome.change)
4517
4812
  };
4518
4813
  }
4814
+ if (action === "update-workbench") {
4815
+ const outcome = await market.updateWorkbench("");
4816
+ return {
4817
+ change: outcome.change,
4818
+ text: describeWorkbenchUpdate(outcome.change)
4819
+ };
4820
+ }
4519
4821
  if (action === "update-plugin" || action === "uninstall-plugin") {
4520
4822
  const plugin = args?.plugin?.trim() ?? "";
4521
4823
  if (plugin === "") throw new Error(`${action} 需要一个 plugin:opencli 插件的包名或插件名 —— 就是市场「已安装 opencli 插件」那一段里的名字。`);
@@ -4566,53 +4868,70 @@ function registerMarketTools(ctx, market) {
4566
4868
  //#endregion
4567
4869
  //#region src/modules/panels/model/prefs.ts
4568
4870
  var PANELS_PREF_RESOURCE = "panels";
4871
+ var ACTIONS = [
4872
+ "show",
4873
+ "hide",
4874
+ "move",
4875
+ "reset",
4876
+ "follow"
4877
+ ];
4569
4878
  function parsePanelsPrefAction(payload) {
4570
- if (typeof payload !== "object" || payload === null) throw new Error("偏好写需要一个对象:{ action, id?, index?, path? }");
4879
+ if (typeof payload !== "object" || payload === null) throw new Error("偏好写需要一个对象:{ action, id?, index?, path?, scope? }");
4571
4880
  const value = payload;
4572
4881
  const action = value.action;
4573
- if (action !== "show" && action !== "hide" && action !== "enable" && action !== "disable" && action !== "move" && action !== "reset") throw new Error(`不认识的 action:${String(action)}(可用:show / hide / enable / disable / move / reset)`);
4882
+ if (typeof action !== "string" || !ACTIONS.includes(action)) throw new Error(`不认识的 action:${String(action)}(可用:${ACTIONS.join(" / ")})`);
4883
+ if (value.scope !== void 0 && value.scope !== "machine" && value.scope !== "workspace") throw new Error(`不认识的 scope:${String(value.scope)}(可用:workspace / machine)`);
4574
4884
  return {
4575
4885
  action,
4576
4886
  ...typeof value.id === "string" ? { id: value.id } : {},
4577
4887
  ...typeof value.index === "number" ? { index: value.index } : {},
4578
- ...typeof value.path === "string" ? { path: value.path } : {}
4888
+ ...typeof value.path === "string" ? { path: value.path } : {},
4889
+ ...value.scope === void 0 ? {} : { scope: value.scope }
4579
4890
  };
4580
4891
  }
4581
4892
  function applyPanelsPref(panels, payload) {
4582
4893
  const action = parsePanelsPrefAction(payload);
4583
4894
  const path = action.path ?? null;
4895
+ const scope = action.scope ?? "workspace";
4896
+ if (action.action === "follow") {
4897
+ if (scope === "machine") throw new Error("机器级默认板没有\"回到跟随\"一说:它就是那份默认本身(要回到的是\"没配过\")。");
4898
+ return {
4899
+ ok: true,
4900
+ units: action.id === void 0 ? panels.follow(path) : panels.follow(path, action.id)
4901
+ };
4902
+ }
4584
4903
  if (action.action === "reset") return {
4585
4904
  ok: true,
4586
- units: panels.reset(path)
4905
+ units: panels.reset(path, scope)
4587
4906
  };
4588
4907
  if (action.id === void 0) throw new Error(`${action.action} 需要一个 id(当前套件:${panels.list(path).map((entry) => entry.id).join(", ")})`);
4589
4908
  if (action.action === "show") return {
4590
4909
  ok: true,
4591
- units: panels.show(action.id, path)
4910
+ units: panels.show(action.id, path, scope)
4592
4911
  };
4593
4912
  if (action.action === "hide") return {
4594
4913
  ok: true,
4595
- units: panels.hide(action.id, path)
4914
+ units: panels.hide(action.id, path, scope)
4596
4915
  };
4597
4916
  return {
4598
4917
  ok: true,
4599
- units: panels.move(action.id, action.index ?? 0, path)
4918
+ units: panels.move(action.id, action.index ?? 0, path, scope)
4600
4919
  };
4601
4920
  }
4602
4921
  //#endregion
4603
4922
  //#region src/modules/panels/model/handoff.ts
4604
4923
  var PANELS_FACT = "panels";
4605
4924
  var CAPABILITY_FACT = "capability";
4606
- function textOf(value) {
4925
+ function textOf$1(value) {
4607
4926
  return typeof value === "string" ? value.trim() : "";
4608
4927
  }
4609
4928
  function actionOf(call) {
4610
- return textOf(call.args["action"]) || "list";
4929
+ return textOf$1(call.args["action"]) || "list";
4611
4930
  }
4612
4931
  function requestOf(call) {
4613
4932
  const action = actionOf(call);
4614
4933
  if (!PROVISION_ACTIONS.includes(action)) return void 0;
4615
- const target = textOf(call.args["unit"]) || textOf(call.args["id"]);
4934
+ const target = textOf$1(call.args["unit"]) || textOf$1(call.args["id"]);
4616
4935
  if (target === "") return void 0;
4617
4936
  const wanted = action;
4618
4937
  return isDependencyAction(wanted) ? {
@@ -4624,7 +4943,7 @@ function requestOf(call) {
4624
4943
  };
4625
4944
  }
4626
4945
  function panelsHandoffEntry(target = {}) {
4627
- const path = textOf(target.path);
4946
+ const path = textOf$1(target.path);
4628
4947
  return path === "" ? void 0 : {
4629
4948
  fact: PANELS_FACT,
4630
4949
  key: `path:${path}`
@@ -4635,6 +4954,7 @@ function panelsFactsOf(call, manifests) {
4635
4954
  if (call.source === "workbench_diy" && actionOf(call) === "list") return [PANELS_FACT];
4636
4955
  if (call.source !== "workbench_modules" && call.source !== "workbench_diy") return void 0;
4637
4956
  if (actionOf(call) === "list") return [PANELS_FACT, CAPABILITY_FACT];
4957
+ if (actionOf(call) === "update-workbench") return [PANELS_FACT, CAPABILITY_FACT];
4638
4958
  const request = requestOf(call);
4639
4959
  if (request === void 0) return void 0;
4640
4960
  return [
@@ -4722,7 +5042,14 @@ function createPanelsService(options) {
4722
5042
  const all = manifests();
4723
5043
  if (!all.some((manifest) => manifest.id === id)) throw new Error(`没有这个套件:${id}(可用:${all.map((manifest) => manifest.id).join(", ")})`);
4724
5044
  }
5045
+ function pluginRefs() {
5046
+ return options.plugins?.() ?? [];
5047
+ }
5048
+ function pluginIds() {
5049
+ return new Set(pluginRefs().map((plugin) => pluginPanelId(plugin.pkg)));
5050
+ }
4725
5051
  function requireSwitchable(id) {
5052
+ if (pluginIds().has(id)) return;
4726
5053
  requireKnown(id);
4727
5054
  if (!manifests().some((manifest) => manifest.id === id && manifest.card !== null)) throw new Error(`${id} 没有卡片,不能收起(它是把别的套件放回来的入口)`);
4728
5055
  }
@@ -4735,6 +5062,9 @@ function createPanelsService(options) {
4735
5062
  list(workspace, target) {
4736
5063
  return resolvePanels(manifests(), target === "machine" ? boardsOf(null) : boardsOf(workspace));
4737
5064
  },
5065
+ listPlugins(workspace, target) {
5066
+ return resolvePluginPanels(pluginRefs(), target === "machine" ? boardsOf(null) : boardsOf(workspace));
5067
+ },
4738
5068
  boards(workspace) {
4739
5069
  return boardsOf(workspace);
4740
5070
  },
@@ -4792,6 +5122,24 @@ function createPanelsService(options) {
4792
5122
  reset(workspace, target) {
4793
5123
  const { folder, key } = targetOf(target ?? "workspace", workspace);
4794
5124
  return withPersisted(folder, EMPTY_BOARD, key);
5125
+ },
5126
+ changedBy() {
5127
+ const counts = /* @__PURE__ */ new Map();
5128
+ for (const folder of store.scan(PANELS_MODULE_ID)) {
5129
+ const board = readBoard(folder);
5130
+ if (board === null) continue;
5131
+ for (const id of /* @__PURE__ */ new Set([...board.shown, ...board.hidden])) counts.set(id, (counts.get(id) ?? 0) + 1);
5132
+ }
5133
+ return counts;
5134
+ },
5135
+ follow(workspace, id) {
5136
+ const { folder, key } = targetOf("workspace", workspace);
5137
+ if (id === void 0) {
5138
+ folder.remove(PANELS_FILE);
5139
+ notify();
5140
+ return resolvePanels(manifests(), boardsOf(key));
5141
+ }
5142
+ return withPersisted(folder, withoutPanel(boardIn(folder), id), key);
4795
5143
  }
4796
5144
  };
4797
5145
  }
@@ -4802,13 +5150,15 @@ function panelRoutes(panels, values) {
4802
5150
  const path = param(search, "path") ?? null;
4803
5151
  const entry = panelsHandoffEntry(path === null ? {} : { path });
4804
5152
  const handed = entry === void 0 ? void 0 : values?.take(entry);
4805
- if (handed !== void 0) return {
4806
- ok: true,
4807
- units: handed.value
4808
- };
5153
+ const defaults = [...panels.list(null, "machine").filter((unit) => unit.shown).map((unit) => unit.id), ...panels.listPlugins(null, "machine").filter((plugin) => plugin.shown).map((plugin) => plugin.id)];
5154
+ const own = panels.boards(path).workspace !== null;
4809
5155
  return {
4810
5156
  ok: true,
4811
- units: panels.list(path)
5157
+ units: handed === void 0 ? panels.list(path) : handed.value,
5158
+ plugins: panels.listPlugins(path),
5159
+ defaults,
5160
+ own,
5161
+ changed: Object.fromEntries(panels.changedBy())
4812
5162
  };
4813
5163
  } };
4814
5164
  }
@@ -4867,6 +5217,18 @@ function createPluginLedger(options) {
4867
5217
  };
4868
5218
  }
4869
5219
  //#endregion
5220
+ //#region src/modules/panels/model/plugin-panel.ts
5221
+ function pluginPanelRefs(ledger) {
5222
+ return ledger.list().map((entry) => {
5223
+ const name = pluginNameOf(entry);
5224
+ return {
5225
+ pkg: entry.package,
5226
+ title: name ?? entry.package,
5227
+ ...name === void 0 ? {} : { name }
5228
+ };
5229
+ });
5230
+ }
5231
+ //#endregion
4870
5232
  //#region src/shared/api/installed-plugins.ts
4871
5233
  var INSTALLED_PLUGINS_ROUTE = "panels/plugins";
4872
5234
  //#endregion
@@ -4956,24 +5318,25 @@ function registerPanelTools(ctx, panels, values) {
4956
5318
  }
4957
5319
  const tool = {
4958
5320
  name: "workbench_panels",
4959
- description: "看或调右侧面板:启用哪些套件、什么顺序。启用/收起 = 卡片与 agent 工具一起上下线(别的项目不受影响,不删数据)。收起是\"这个工作区明确不要它\",连那些在机器级默认板上开着的套件也照样在这个工作区收起来,之后 show 能放回来。默认只动当前工作区(scope=workspace);scope=machine 动的是\"机器级默认板\" —— 机器级默认板只是还没配过的工作区的默认值,已经自己配过的工作区保持自己的选择,不会被它改。在那份板上 hide = 明确写下\"不要它\",这一笔比名单更管用:名单里还列着它也算不要,没配过的工作区因此看不到它;show 会把这笔划掉、把它放回去。",
5321
+ description: "看或调右侧面板:启用哪些套件、什么顺序。启用/收起 = 卡片与 agent 工具一起上下线(别的项目不受影响,不删数据)。收起是\"这个工作区明确不要它\",连那些在机器级默认板上开着的套件也照样在这个工作区收起来,之后 show 能放回来。默认只动当前工作区(scope=workspace);scope=machine 动的是\"机器级默认板\" —— 机器级默认板只是还没配过的工作区的默认值,已经自己配过的工作区保持自己的选择,不会被它改。在那份板上 hide = 明确写下\"不要它\",这一笔比名单更管用:名单里还列着它也算不要,没提过它的工作区因此看不到它;show 会把这笔划掉、把它放回去。本工作区那份板只记**改过的那几行**(逐行覆盖):没提到的行跟着机器级默认板走,所以那份板以后改了,没改过的工作区会跟着变。workbench_panels 默认只动当前工作区,动机器级默认板要显式给 scope=machine。",
4960
5322
  parameters: {
4961
5323
  type: "object",
4962
5324
  properties: {
4963
5325
  action: {
4964
5326
  type: "string",
4965
- description: "show=这个工作区要它(之前收起的也放回来);hide=这个工作区明确不要它(卡片与工具一起下线,默认在机器上也开着的套件同样收起,别的项目不受影响);move=调顺序;reset=这个工作区不再有自己的选择(之前收起、启用的都忘掉;它从此明确\"什么都不要\",机器级默认板也管不到它,要哪一块用 show 放回来)。",
5327
+ description: "show=这个工作区要它(之前收起的也放回来);hide=这个工作区明确不要它(卡片与工具一起下线,默认在机器上也开着的套件同样收起,别的项目不受影响);move=调顺序;reset=清掉这个工作区改过的那些行(改完就全部回到跟着机器级默认板走);follow=把某一行放回跟随(带 id = 只放回那一行;不带 id = 全部放回)。",
4966
5328
  enum: [
4967
5329
  "list",
4968
5330
  "show",
4969
5331
  "hide",
4970
5332
  "move",
4971
- "reset"
5333
+ "reset",
5334
+ "follow"
4972
5335
  ]
4973
5336
  },
4974
5337
  id: {
4975
5338
  type: "string",
4976
- description: "show / hide / move 必填,如 coding-issues。"
5339
+ description: "show / hide / move 必填;follow 可选(带 id = 只把那一行放回跟随)。"
4977
5340
  },
4978
5341
  index: {
4979
5342
  type: "integer",
@@ -5009,8 +5372,9 @@ function registerPanelTools(ctx, panels, values) {
5009
5372
  "show",
5010
5373
  "hide",
5011
5374
  "move",
5012
- "reset"
5013
- ].includes(action)) throw new Error(`不认识的 action:${action}(可用:list / show / hide / move / reset)`);
5375
+ "reset",
5376
+ "follow"
5377
+ ].includes(action)) throw new Error(`不认识的 action:${action}(可用:list / show / hide / move / reset / follow)`);
5014
5378
  if (action === "list") {
5015
5379
  const units = list();
5016
5380
  if (scope === "workspace") {
@@ -5022,11 +5386,19 @@ function registerPanelTools(ctx, panels, values) {
5022
5386
  text: render(units, workspace, scope)
5023
5387
  };
5024
5388
  }
5389
+ if (action === "follow") {
5390
+ if (scope === "machine") throw new Error("机器级默认板没有\"放回跟随\"一说:它就是那份默认本身,直接 show / hide 写它。");
5391
+ const units = panels.follow(workspace, id);
5392
+ return {
5393
+ units,
5394
+ text: id === void 0 ? `这个工作区改过的行全部清掉 —— 每一行都回到跟着机器级默认板走(那份板以后改了,这里跟着变)。\n${render(units, workspace, scope)}` : `${id} 放回跟随 —— 它不再由这个工作区定,改跟机器级默认板走。\n${render(units, workspace, scope)}`
5395
+ };
5396
+ }
5025
5397
  if (action === "reset") {
5026
5398
  const units = panels.reset(workspace, scope);
5027
5399
  return {
5028
5400
  units,
5029
- text: scope === "machine" ? `机器级默认板已清空(是"配成空",不是"没配过")——还没配过的工作区从此默认什么都不开。${MACHINE_CAVEAT}\n${render(units, workspace, scope)}` : `这个工作区不再有自己的面板选择(之前收起、启用的都忘掉了;它从此明确"什么都不要"—— 机器级默认板也管不到它,要哪一块用 show 放回来)。\n${render(units, workspace, scope)}`
5401
+ text: scope === "machine" ? `机器级默认板已清空 —— 从今往后,没在这一行改过的工作区默认什么都不开。${MACHINE_CAVEAT}\n${render(units, workspace, scope)}` : `这个工作区改过的行已全部清掉 —— 每一行都回到跟着机器级默认板走。\n${render(units, workspace, scope)}`
5030
5402
  };
5031
5403
  }
5032
5404
  if (id === void 0) throw new Error(`${action} 需要一个 id(例如 coding-issues)。当前套件:${list().map((entry) => entry.id).join(", ")}`);
@@ -5059,19 +5431,20 @@ var OWNED = [{
5059
5431
  unit: panelsUnit.id,
5060
5432
  manifest: panelsUnit,
5061
5433
  mount({ ctx, logger, manifests, ownedUnits, opencliCommand, probe, store, values }) {
5434
+ const ledger = createPluginLedger({
5435
+ store,
5436
+ home: isolatedHome()
5437
+ });
5062
5438
  const panels = createPanelsService({
5063
5439
  manifests,
5064
5440
  store,
5065
5441
  owned: ownedUnits,
5442
+ plugins: () => pluginPanelRefs(ledger),
5066
5443
  logger
5067
5444
  });
5068
5445
  ctx.provide(PANELS_SERVICE, panels);
5069
5446
  const profileDir = profileDirOf(ctx);
5070
5447
  const shell = ctx.get("shell");
5071
- const ledger = createPluginLedger({
5072
- store,
5073
- home: isolatedHome()
5074
- });
5075
5448
  const market = createMarketService({
5076
5449
  manifests: manifests(),
5077
5450
  manifestsNow: manifests,
@@ -5081,6 +5454,7 @@ var OWNED = [{
5081
5454
  bundles: readBundleManager(ctx),
5082
5455
  installed: (pkg) => readInstalledModule(profileDir, pkg),
5083
5456
  modules: () => profileModules(profileDir),
5457
+ workbench: (pkg) => readProfilePackage(profileDir, pkg),
5084
5458
  registries: createRegistryNotebook(profileDir),
5085
5459
  ledger,
5086
5460
  references: () => {
@@ -5460,6 +5834,24 @@ function bundleRegistriesHandler(ctx) {
5460
5834
  };
5461
5835
  }
5462
5836
  var refusalLog = /* @__PURE__ */ new Map();
5837
+ async function workbenchVersion(input) {
5838
+ const read = readProfilePackage(input.dir, PLUGIN_ID);
5839
+ if (read === void 0) return void 0;
5840
+ const registry = read.registry ?? input.fallback;
5841
+ const queried = read.local === true || input.shell === void 0 || registry === void 0 ? void 0 : await latestVersion({
5842
+ shell: input.shell,
5843
+ name: read.pkg,
5844
+ registry,
5845
+ force: input.force
5846
+ });
5847
+ const latest = queried !== void 0 && isNewerVersion(queried, read.version) ? queried : void 0;
5848
+ return {
5849
+ pkg: read.pkg,
5850
+ installed: read.version,
5851
+ ...latest === void 0 ? {} : { latest },
5852
+ ...read.local === true ? { local: true } : {}
5853
+ };
5854
+ }
5463
5855
  function moduleVersionsHandler(ctx, shell) {
5464
5856
  return async (search) => {
5465
5857
  const manager = readBundleManager(ctx);
@@ -5472,36 +5864,44 @@ function moduleVersionsHandler(ctx, shell) {
5472
5864
  const refusals = new Map((ctx.get("workbenchUnits")?.refusals() ?? []).filter((one) => one.unit !== "").map((one) => [one.unit, one.reasons]));
5473
5865
  const force = param(search, "force") === "1";
5474
5866
  const fallback = (await manager.registries())?.resolved ?? void 0;
5867
+ const modules = await Promise.all(found.map(async (module) => {
5868
+ const registry = module.registry ?? fallback;
5869
+ const latest = shell === void 0 || registry === void 0 ? void 0 : await latestVersion({
5870
+ shell,
5871
+ name: module.pkg,
5872
+ registry,
5873
+ force
5874
+ });
5875
+ const raw = moduleDeclarationOf(dir, module.pkg);
5876
+ const declared = raw === void 0 ? [] : declarationProblemsOf(module.pkg, raw);
5877
+ const refused = refusals.get(module.unit) ?? (declared.length === 0 ? void 0 : declared);
5878
+ if (refused === void 0) refusalLog.delete(module.pkg);
5879
+ else {
5880
+ const line = refused.join(";");
5881
+ if (refusalLog.get(module.pkg) !== line) {
5882
+ refusalLog.set(module.pkg, line);
5883
+ ctx.logger?.warn?.(`[dsh-workbench] ${module.pkg} 装上了但用不了:${line}`);
5884
+ }
5885
+ }
5886
+ return {
5887
+ unit: module.unit,
5888
+ pkg: module.pkg,
5889
+ installed: module.version,
5890
+ ...latest === void 0 ? {} : { latest },
5891
+ ...module.local === true ? { local: true } : {},
5892
+ ...refused === void 0 ? {} : { refused }
5893
+ };
5894
+ }));
5895
+ const workbench = await workbenchVersion({
5896
+ dir,
5897
+ shell,
5898
+ fallback,
5899
+ force
5900
+ });
5475
5901
  return {
5476
5902
  ok: true,
5477
- modules: await Promise.all(found.map(async (module) => {
5478
- const registry = module.registry ?? fallback;
5479
- const latest = shell === void 0 || registry === void 0 ? void 0 : await latestVersion({
5480
- shell,
5481
- name: module.pkg,
5482
- registry,
5483
- force
5484
- });
5485
- const raw = moduleDeclarationOf(dir, module.pkg);
5486
- const declared = raw === void 0 ? [] : declarationProblemsOf(module.pkg, raw);
5487
- const refused = refusals.get(module.unit) ?? (declared.length === 0 ? void 0 : declared);
5488
- if (refused === void 0) refusalLog.delete(module.pkg);
5489
- else {
5490
- const line = refused.join(";");
5491
- if (refusalLog.get(module.pkg) !== line) {
5492
- refusalLog.set(module.pkg, line);
5493
- ctx.logger?.warn?.(`[dsh-workbench] ${module.pkg} 装上了但用不了:${line}`);
5494
- }
5495
- }
5496
- return {
5497
- unit: module.unit,
5498
- pkg: module.pkg,
5499
- installed: module.version,
5500
- ...latest === void 0 ? {} : { latest },
5501
- ...module.local === true ? { local: true } : {},
5502
- ...refused === void 0 ? {} : { refused }
5503
- };
5504
- }))
5903
+ modules,
5904
+ ...workbench === void 0 ? {} : { workbench }
5505
5905
  };
5506
5906
  };
5507
5907
  }
@@ -5528,15 +5928,26 @@ async function declarationRefusal(spec, inspection, ctx, manager) {
5528
5928
  });
5529
5929
  return refusedInspection(declarationRefusalOf(inspection.name, raw));
5530
5930
  }
5531
- async function packageRefusal(spec, inspection, ctx, manager) {
5532
- if (inspection.status !== "accepted") return void 0;
5931
+ async function packageOverride(spec, inspection, ctx, manager, asked) {
5533
5932
  const shell = ctx.get("shell");
5534
- const registry = typeof inspection.registry === "string" ? inspection.registry : (await manager.registries())?.resolved ?? void 0;
5535
- return (await packageRouteOf({
5933
+ const registry = (inspection.status === "accepted" && typeof inspection.registry === "string" ? inspection.registry : void 0) ?? (typeof asked === "string" ? asked : (await manager.registries())?.resolved ?? void 0);
5934
+ const route = await packageRouteOf({
5536
5935
  spec,
5537
5936
  ...shell === void 0 ? {} : { shell },
5538
5937
  ...registry === void 0 ? {} : { registry }
5539
- }))?.kind === "neither" ? {
5938
+ });
5939
+ if (route?.kind === "opencli-plugin") {
5940
+ if (inspection.status !== "refused") return void 0;
5941
+ const ref = splitNpmSpec(spec);
5942
+ return {
5943
+ status: "plugin",
5944
+ pkg: route.pkg ?? ref?.name ?? spec.trim(),
5945
+ registry: registry ?? null,
5946
+ ...route.pluginName === void 0 ? {} : { pluginName: route.pluginName },
5947
+ ...ref?.version === void 0 ? {} : { version: ref.version }
5948
+ };
5949
+ }
5950
+ return route?.kind === "neither" && inspection.status === "accepted" ? {
5540
5951
  status: "refused",
5541
5952
  problem: "not-a-module-or-plugin",
5542
5953
  reason: refusalReason()
@@ -5560,7 +5971,8 @@ function bundleInspectHandler(ctx) {
5560
5971
  message: "这个 profile 里没有插件管理器,装不了模块。"
5561
5972
  }
5562
5973
  };
5563
- const inspection = await manager.inspect(spec, registryOf(search));
5974
+ const asked = registryOf(search);
5975
+ const inspection = await manager.inspect(spec, asked);
5564
5976
  if (inspection === null) return {
5565
5977
  ok: false,
5566
5978
  error: {
@@ -5570,7 +5982,7 @@ function bundleInspectHandler(ctx) {
5570
5982
  };
5571
5983
  return {
5572
5984
  ok: true,
5573
- inspection: await packageRefusal(spec, inspection, ctx, manager) ?? await declarationRefusal(spec, inspection, ctx, manager) ?? inspection
5985
+ inspection: await packageOverride(spec, inspection, ctx, manager, asked) ?? await declarationRefusal(spec, inspection, ctx, manager) ?? inspection
5574
5986
  };
5575
5987
  };
5576
5988
  }
@@ -6496,7 +6908,9 @@ function mountUnits(units, deps) {
6496
6908
  //#endregion
6497
6909
  //#region src/shared/api/host/unit-registry.ts
6498
6910
  function sourceLabel(source) {
6499
- return source.kind === "owned" ? "外壳自己" : source.pkg;
6911
+ if (source.kind === "owned") return "外壳自己";
6912
+ if (source.kind === "plugin") return "按已装 opencli 插件合成";
6913
+ return source.pkg;
6500
6914
  }
6501
6915
  function createUnitRegistry(options) {
6502
6916
  const refusals = [];
@@ -6556,6 +6970,14 @@ function createUnitRegistry(options) {
6556
6970
  source: { kind: "owned" }
6557
6971
  });
6558
6972
  },
6973
+ registerPlugin(face) {
6974
+ return add({
6975
+ unit: face.manifest.id,
6976
+ manifest: face.manifest,
6977
+ face,
6978
+ source: { kind: "plugin" }
6979
+ });
6980
+ },
6559
6981
  all: () => hosts,
6560
6982
  owned: () => hosts.filter((host) => host.source.kind === "owned").map((host) => host.unit),
6561
6983
  refusals: () => refusals,
@@ -6581,6 +7003,563 @@ function bindRosterFacts(options) {
6581
7003
  });
6582
7004
  }
6583
7005
  //#endregion
7006
+ //#region src/shared/api/host/plugin-catalog.ts
7007
+ function recordOf(value) {
7008
+ return typeof value === "object" && value !== null ? value : void 0;
7009
+ }
7010
+ function textOf(value) {
7011
+ return typeof value === "string" ? value : "";
7012
+ }
7013
+ function stringsOf(value) {
7014
+ return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
7015
+ }
7016
+ function argsOf(raw) {
7017
+ if (!Array.isArray(raw)) return [];
7018
+ return raw.flatMap((item) => {
7019
+ const record = recordOf(item);
7020
+ if (record === void 0) return [];
7021
+ const name = textOf(record["name"]).trim();
7022
+ if (name === "") return [];
7023
+ const fallback = record["default"];
7024
+ return [{
7025
+ name,
7026
+ type: textOf(record["type"]) || "string",
7027
+ required: record["required"] === true,
7028
+ positional: record["positional"] === true,
7029
+ valueRequired: record["valueRequired"] === true,
7030
+ choices: stringsOf(record["choices"]),
7031
+ ...fallback === void 0 || fallback === null ? {} : { defaultValue: fallback },
7032
+ help: textOf(record["help"])
7033
+ }];
7034
+ });
7035
+ }
7036
+ function parsePluginRows(stdout) {
7037
+ const parsed = parseJsonLoose(stdout);
7038
+ if (!Array.isArray(parsed)) return void 0;
7039
+ const rows = [];
7040
+ for (const item of parsed) {
7041
+ const record = recordOf(item);
7042
+ if (record === void 0) return void 0;
7043
+ const name = textOf(record["name"]).trim();
7044
+ if (name === "") return void 0;
7045
+ rows.push({
7046
+ name,
7047
+ version: textOf(record["version"]),
7048
+ commands: stringsOf(record["commands"])
7049
+ });
7050
+ }
7051
+ return rows;
7052
+ }
7053
+ function parseCommandRows(stdout) {
7054
+ const parsed = parseJsonLoose(stdout);
7055
+ if (!Array.isArray(parsed)) return void 0;
7056
+ const rows = [];
7057
+ for (const item of parsed) {
7058
+ const record = recordOf(item);
7059
+ if (record === void 0) return void 0;
7060
+ const site = textOf(record["site"]).trim();
7061
+ const name = textOf(record["name"]).trim();
7062
+ if (site === "" || name === "") return void 0;
7063
+ rows.push({
7064
+ site,
7065
+ name,
7066
+ description: textOf(record["description"]),
7067
+ access: textOf(record["access"]),
7068
+ ...textOf(record["domain"]) === "" ? {} : { domain: textOf(record["domain"]) },
7069
+ args: argsOf(record["args"])
7070
+ });
7071
+ }
7072
+ return rows;
7073
+ }
7074
+ function commandOf(plugin, row) {
7075
+ return {
7076
+ plugin,
7077
+ name: row.name,
7078
+ description: row.description,
7079
+ access: row.access,
7080
+ args: row.args
7081
+ };
7082
+ }
7083
+ function attributeCommands(plugins, rows) {
7084
+ const bySite = /* @__PURE__ */ new Map();
7085
+ for (const row of rows) {
7086
+ const site = bySite.get(row.site) ?? /* @__PURE__ */ new Map();
7087
+ site.set(row.name, row);
7088
+ bySite.set(row.site, site);
7089
+ }
7090
+ const sets = [];
7091
+ const problems = [];
7092
+ if (plugins.length > 0 && rows.length === 0) problems.push(`opencli 报了 ${String(plugins.length)} 个插件,但命令面(opencli list -f json)里一条命令都没有 —— 两份清单对不上`);
7093
+ for (const plugin of plugins) {
7094
+ const found = bySite.get(plugin.name) ?? /* @__PURE__ */ new Map();
7095
+ if (found.size === 0) {
7096
+ problems.push(`「${plugin.name}」在 opencli 插件清单里,但命令面里没有一条 site=${plugin.name} 的命令 —— 归属不了,它这次不合成工具`);
7097
+ continue;
7098
+ }
7099
+ const declared = plugin.commands.length > 0 ? plugin.commands : [...found.keys()];
7100
+ const missing = declared.filter((name) => !found.has(name));
7101
+ if (missing.length > 0) problems.push(`「${plugin.name}」自己的清单里有 ${String(missing.length)} 条命令在命令面里找不到:${missing.join("、")}(插件版本旧或命令面没更新)`);
7102
+ const commands = declared.flatMap((name) => {
7103
+ const row = found.get(name);
7104
+ return row === void 0 ? [] : [commandOf(plugin.name, row)];
7105
+ });
7106
+ if (commands.length === 0) {
7107
+ problems.push(`「${plugin.name}」没有任何一条能归属的命令 —— 它这次不合成工具`);
7108
+ continue;
7109
+ }
7110
+ sets.push({
7111
+ plugin: plugin.name,
7112
+ version: plugin.version,
7113
+ commands
7114
+ });
7115
+ }
7116
+ return {
7117
+ sets,
7118
+ problems
7119
+ };
7120
+ }
7121
+ function fingerprintOf(plugins) {
7122
+ return plugins.map((plugin) => `${plugin.name}@${plugin.version}:${[...plugin.commands].sort().join(",")}`).sort().join("|");
7123
+ }
7124
+ function messageOf(error) {
7125
+ return error instanceof Error ? error.message : String(error);
7126
+ }
7127
+ function createPluginCatalog(options) {
7128
+ const timeoutMs = options.timeoutMs ?? 12e4;
7129
+ let current = {
7130
+ ok: false,
7131
+ at: 0,
7132
+ sets: [],
7133
+ problems: ["还没探过 opencli 的插件命令面"]
7134
+ };
7135
+ let heavy;
7136
+ async function read(line) {
7137
+ const shell = options.shell;
7138
+ if (shell === void 0) return {
7139
+ ok: false,
7140
+ problem: "没有执行缝(shell 服务不可用),探不了这个命令面"
7141
+ };
7142
+ try {
7143
+ const result = await runShell(shell, line, { timeoutMs });
7144
+ if (result.timedOut) return {
7145
+ ok: false,
7146
+ problem: `${line} 超过 ${String(Math.round(timeoutMs / 1e3))} 秒没返回`
7147
+ };
7148
+ if (result.exitCode !== 0) {
7149
+ const tail = result.stderr.trim() || result.stdout.trim();
7150
+ return {
7151
+ ok: false,
7152
+ problem: `${line} 退出码 ${String(result.exitCode)}${tail === "" ? "" : `:${tail.split("\n").slice(-3).join(" ")}`}`
7153
+ };
7154
+ }
7155
+ return {
7156
+ ok: true,
7157
+ stdout: result.stdout
7158
+ };
7159
+ } catch (error) {
7160
+ return {
7161
+ ok: false,
7162
+ problem: `${line} 没跑起来:${messageOf(error)}`
7163
+ };
7164
+ }
7165
+ }
7166
+ function fail(problem) {
7167
+ current = {
7168
+ ok: false,
7169
+ at: Date.now(),
7170
+ sets: current.sets,
7171
+ problems: [problem]
7172
+ };
7173
+ return current;
7174
+ }
7175
+ async function refresh(callOptions = {}) {
7176
+ const pluginsRun = await read(`${options.command} plugin list -f json`);
7177
+ if (!pluginsRun.ok) return fail(pluginsRun.problem);
7178
+ const plugins = parsePluginRows(pluginsRun.stdout);
7179
+ if (plugins === void 0) return fail("opencli plugin list -f json 的输出读不懂(不是带 name 的 JSON 数组)");
7180
+ const fingerprint = fingerprintOf(plugins);
7181
+ if (!callOptions.force && heavy !== void 0 && heavy.fingerprint === fingerprint) {
7182
+ current = {
7183
+ ok: true,
7184
+ at: Date.now(),
7185
+ sets: heavy.sets,
7186
+ problems: heavy.problems
7187
+ };
7188
+ return current;
7189
+ }
7190
+ const commandsRun = await read(`${options.command} list -f json`);
7191
+ if (!commandsRun.ok) return fail(commandsRun.problem);
7192
+ const rows = parseCommandRows(commandsRun.stdout);
7193
+ if (rows === void 0) return fail("opencli list -f json 的输出读不懂(不是带 site / name 的 JSON 数组)");
7194
+ const attributed = attributeCommands(plugins, rows);
7195
+ heavy = {
7196
+ fingerprint,
7197
+ sets: attributed.sets,
7198
+ problems: attributed.problems
7199
+ };
7200
+ current = {
7201
+ ok: true,
7202
+ at: Date.now(),
7203
+ sets: attributed.sets,
7204
+ problems: attributed.problems
7205
+ };
7206
+ return current;
7207
+ }
7208
+ return {
7209
+ refresh,
7210
+ snapshot: () => current
7211
+ };
7212
+ }
7213
+ //#endregion
7214
+ //#region src/shared/api/host/plugin-login-hint.ts
7215
+ var NOT_LOGGED_IN = /未登录|请先登录|not logged in/i;
7216
+ function propertyOf$1(error, key) {
7217
+ return typeof error === "object" && error !== null ? error[key] : void 0;
7218
+ }
7219
+ function helpUrlOfError(error) {
7220
+ const raw = propertyOf$1(error, "helpUrl");
7221
+ if (typeof raw !== "string") return void 0;
7222
+ const trimmed = raw.trim();
7223
+ return trimmed === "" ? void 0 : trimmed;
7224
+ }
7225
+ function isAuthFailure(error, message) {
7226
+ if (NOT_LOGGED_IN.test(message)) return true;
7227
+ return propertyOf$1(error, "code") === "auth-required";
7228
+ }
7229
+ function loginUrlOfDomain(domain) {
7230
+ const bare = (domain ?? "").trim().replace(/\/+$/, "");
7231
+ if (bare === "" || bare.includes("*")) return void 0;
7232
+ return /^https?:\/\//.test(bare) ? bare : `https://${bare}/login`;
7233
+ }
7234
+ function pluginLoginHint(error, domain) {
7235
+ const message = error instanceof Error ? error.message : String(error);
7236
+ if (!isAuthFailure(error, message)) return message;
7237
+ const url = helpUrlOfError(error) ?? loginUrlOfDomain(domain);
7238
+ if (url !== void 0) return `${message}\n在 opencli 那扇浏览器窗里打开 ${url} 登一次,然后重试。`;
7239
+ return `${message}\n在 opencli 那扇浏览器窗里登录这个站点(插件没给出具体地址,工作台拼不出来)—— 或跑一次工作台的体检/预热,然后重试。`;
7240
+ }
7241
+ //#endregion
7242
+ //#region src/shared/api/host/plugin-tools.ts
7243
+ var SCHEMA_TYPE = {
7244
+ int: "integer",
7245
+ integer: "integer",
7246
+ number: "number",
7247
+ float: "number",
7248
+ bool: "boolean",
7249
+ boolean: "boolean",
7250
+ string: "string"
7251
+ };
7252
+ var KEY_ARG_LIMIT = 3;
7253
+ function pluginToolName(plugin, command) {
7254
+ return `${plugin}_${command}`.toLowerCase().replace(/[^a-z0-9_]+/g, "_").replace(/_{2,}/g, "_").replace(/^_+|_+$/g, "");
7255
+ }
7256
+ function schemaTypeOf(arg) {
7257
+ return SCHEMA_TYPE[arg.type.toLowerCase()] ?? "string";
7258
+ }
7259
+ function propertyOf(arg) {
7260
+ const type = schemaTypeOf(arg);
7261
+ const enumValues = arg.choices.length > 0 ? arg.choices.map((choice) => choice) : void 0;
7262
+ return {
7263
+ type,
7264
+ ...arg.help.trim() === "" ? {} : { description: arg.help.trim() },
7265
+ ...enumValues === void 0 ? {} : { enum: enumValues },
7266
+ ...arg.defaultValue === void 0 ? {} : { default: arg.defaultValue }
7267
+ };
7268
+ }
7269
+ function pluginToolParameters(command) {
7270
+ const properties = {};
7271
+ for (const arg of command.args) properties[arg.name] = propertyOf(arg);
7272
+ const required = command.args.filter((arg) => arg.required).map((arg) => arg.name);
7273
+ return {
7274
+ type: "object",
7275
+ properties,
7276
+ ...required.length === 0 ? {} : { required },
7277
+ additionalProperties: false
7278
+ };
7279
+ }
7280
+ function pluginToolDescription(command) {
7281
+ const described = command.description.trim();
7282
+ return described === "" ? `${command.plugin} ${command.name}(opencli 命令)` : described;
7283
+ }
7284
+ function pluginToolWrites(command) {
7285
+ return command.access !== "read";
7286
+ }
7287
+ function valuesOf(raw) {
7288
+ return typeof raw === "object" && raw !== null ? raw : {};
7289
+ }
7290
+ function pluginToolArgv(command, raw) {
7291
+ const values = valuesOf(raw);
7292
+ const argv = [];
7293
+ for (const arg of command.args) {
7294
+ if (!arg.positional) continue;
7295
+ const value = values[arg.name];
7296
+ if (value === void 0 || value === null) continue;
7297
+ argv.push(String(value));
7298
+ }
7299
+ for (const arg of command.args) {
7300
+ if (arg.positional) continue;
7301
+ const value = values[arg.name];
7302
+ if (value === void 0 || value === null) continue;
7303
+ if (typeof value === "boolean") argv.push(`--${arg.name}`, value ? "true" : "false");
7304
+ else if (Array.isArray(value)) argv.push(`--${arg.name}`, value.map((item) => String(item)).join(","));
7305
+ else argv.push(`--${arg.name}`, String(value));
7306
+ }
7307
+ return argv;
7308
+ }
7309
+ function pluginKeyArgs(command, raw) {
7310
+ const values = valuesOf(raw);
7311
+ const ordered = [...command.args.filter((arg) => arg.required), ...command.args.filter((arg) => !arg.required)];
7312
+ const parts = [];
7313
+ for (const arg of ordered) {
7314
+ const value = values[arg.name];
7315
+ if (value === void 0 || value === null || value === "") continue;
7316
+ parts.push(`${arg.name}=${String(value)}`);
7317
+ if (parts.length === KEY_ARG_LIMIT) break;
7318
+ }
7319
+ return parts.length === 0 ? "没有带参数" : parts.join("、");
7320
+ }
7321
+ function pluginApprovalQuestion(command, raw) {
7322
+ const key = pluginKeyArgs(command, raw);
7323
+ return {
7324
+ zh: `允许执行 ${command.plugin} 的 ${command.name}(${key})吗?`,
7325
+ en: `Run ${command.plugin} ${command.name} (${key})?`
7326
+ };
7327
+ }
7328
+ function pluginApprovalFor(command) {
7329
+ const denied = command.access === "read";
7330
+ return (call) => {
7331
+ if (denied) return { kind: "read" };
7332
+ const question = pluginApprovalQuestion(command, call.arguments);
7333
+ return {
7334
+ kind: "ask",
7335
+ reason: command.access === "write" ? `${command.plugin} ${command.name} 在 opencli 里标着 write` : `${command.plugin} ${command.name} 没有拿到 access(fail-safe:按写处理)`,
7336
+ question,
7337
+ fallback: question
7338
+ };
7339
+ };
7340
+ }
7341
+ //#endregion
7342
+ //#region src/shared/api/host/plugin-units.ts
7343
+ var TOOL_OUTPUT_SCHEMA = {
7344
+ type: "object",
7345
+ properties: {},
7346
+ additionalProperties: true
7347
+ };
7348
+ function signatureOf(version, commands) {
7349
+ return [version, ...commands.map((command) => `${command.name}:${command.access}:${command.args.map((arg) => `${arg.name}${arg.required ? "!" : ""}`).join("+")}`)].join("|");
7350
+ }
7351
+ function toolsOf(plugin, commands, taken) {
7352
+ const tools = [];
7353
+ const problems = [];
7354
+ const claimed = /* @__PURE__ */ new Set();
7355
+ for (const command of commands) {
7356
+ const name = pluginToolName(plugin, command.name);
7357
+ if (taken.has(name) || claimed.has(name)) {
7358
+ problems.push(`工具名 ${name}(${plugin} ${command.name})已经被占着 —— 这条命令这次不合成(先来的那份说了算)`);
7359
+ continue;
7360
+ }
7361
+ claimed.add(name);
7362
+ tools.push({
7363
+ name,
7364
+ command
7365
+ });
7366
+ }
7367
+ return {
7368
+ tools,
7369
+ problems
7370
+ };
7371
+ }
7372
+ function planOf(options, registered) {
7373
+ const sets = new Map(options.catalog.snapshot().sets.map((set) => [set.plugin, set]));
7374
+ const mine = new Set([...registered.values()].flatMap((item) => item.tools));
7375
+ const taken = new Set([...options.toolNames?.() ?? []].filter((name) => !mine.has(name)));
7376
+ const units = [];
7377
+ const rows = [];
7378
+ const problems = [];
7379
+ for (const row of options.rows()) {
7380
+ const plugin = row.name?.trim() ?? "";
7381
+ if (plugin === "") {
7382
+ problems.push(`面板上的插件行 ${row.id}(${row.pkg})没有 opencli 插件名 —— 认不出它,这次不给它合成工具`);
7383
+ continue;
7384
+ }
7385
+ const set = sets.get(plugin);
7386
+ if (set === void 0) {
7387
+ problems.push(`已装插件「${plugin}」在 opencli 的插件清单 / 命令面里找不到 —— 归属不了,这次不给它合成工具`);
7388
+ continue;
7389
+ }
7390
+ sets.delete(plugin);
7391
+ const declared = options.declared(plugin);
7392
+ const overlapped = set.commands.filter((command) => declared.has(command.name));
7393
+ if (overlapped.length > 0) problems.push(`「${plugin}」有 ${overlapped.length} 条命令由已装模块自己提供(${overlapped.map((command) => command.name).join("、")})—— 通用桥不重复合成`);
7394
+ const commands = set.commands.filter((command) => !declared.has(command.name));
7395
+ const built = toolsOf(plugin, commands, taken);
7396
+ problems.push(...built.problems);
7397
+ for (const tool of built.tools) taken.add(tool.name);
7398
+ if (built.tools.length === 0 && commands.length === set.commands.length) problems.push(`「${plugin}」的命令一条都没能合成工具`);
7399
+ rows.push({
7400
+ plugin,
7401
+ pkg: row.pkg,
7402
+ version: set.version,
7403
+ commands: set.commands.length,
7404
+ synthesized: built.tools.length,
7405
+ tools: built.tools.map((tool) => tool.name)
7406
+ });
7407
+ if (built.tools.length === 0) continue;
7408
+ units.push({
7409
+ id: row.id,
7410
+ plugin,
7411
+ pkg: row.pkg,
7412
+ version: set.version,
7413
+ signature: signatureOf(set.version, commands),
7414
+ commands,
7415
+ tools: built.tools
7416
+ });
7417
+ }
7418
+ for (const set of sets.values()) problems.push(`opencli 里装着「${set.plugin}」(${String(set.commands.length)} 条命令),但它不在插件台账里(不是从市场装的)—— 面板上没有它的开关,它的命令这次不进工具面`);
7419
+ return {
7420
+ units,
7421
+ rows,
7422
+ problems
7423
+ };
7424
+ }
7425
+ function faceOf(unit, opencliFor) {
7426
+ const byTool = new Map(unit.tools.map((tool) => [tool.name, tool.command]));
7427
+ const manifest = {
7428
+ id: unit.id,
7429
+ card: null,
7430
+ tools: unit.tools.map((tool) => ({
7431
+ name: tool.name,
7432
+ writes: pluginToolWrites(tool.command)
7433
+ })),
7434
+ host: { inject: [] }
7435
+ };
7436
+ return {
7437
+ unit: unit.id,
7438
+ manifest,
7439
+ mount(deps) {
7440
+ const tools = deps.ctx.get("tools");
7441
+ if (tools === void 0) {
7442
+ deps.logger?.info?.(`[dsh-workbench] 没有 tools 服务,跳过 ${unit.plugin} 的命令工具注册`);
7443
+ return {
7444
+ routes: {},
7445
+ registerTools: () => []
7446
+ };
7447
+ }
7448
+ const opencli = opencliFor(unit.plugin);
7449
+ const definitions = unit.tools.map((tool) => ({
7450
+ name: tool.name,
7451
+ description: pluginToolDescription(tool.command),
7452
+ parameters: pluginToolParameters(tool.command),
7453
+ output: {
7454
+ schema: TOOL_OUTPUT_SCHEMA,
7455
+ render: (_args, value) => textOutput(jsonText(value))
7456
+ },
7457
+ async execute(raw, exec) {
7458
+ const workspace = sessionWorkspace(exec);
7459
+ try {
7460
+ return (await opencli.json([tool.command.name, ...pluginToolArgv(tool.command, raw)], workspace === null ? {} : { workdir: workspace })).value;
7461
+ } catch (error) {
7462
+ throw new Error(pluginLoginHint(error, tool.command.domain), { cause: error });
7463
+ }
7464
+ }
7465
+ }));
7466
+ return {
7467
+ routes: {},
7468
+ factsOf: () => [],
7469
+ registerTools: () => definitions.map((definition) => tools.register(definition)),
7470
+ needsApproval: (call) => {
7471
+ const command = call.name === void 0 ? void 0 : byTool.get(call.name);
7472
+ return command === void 0 ? void 0 : pluginApprovalFor(command)(call);
7473
+ }
7474
+ };
7475
+ }
7476
+ };
7477
+ }
7478
+ function reconcile(registered, units, options) {
7479
+ const wanted = new Map(units.map((unit) => [unit.id, unit]));
7480
+ for (const [id, item] of [...registered]) {
7481
+ const next = wanted.get(id);
7482
+ if (next !== void 0 && next.signature === item.signature) continue;
7483
+ item.drop();
7484
+ registered.delete(id);
7485
+ }
7486
+ for (const unit of units) {
7487
+ if (registered.has(unit.id)) continue;
7488
+ registered.set(unit.id, {
7489
+ signature: unit.signature,
7490
+ tools: unit.tools.map((tool) => tool.name),
7491
+ drop: options.registry.registerPlugin(faceOf(unit, options.opencliFor))
7492
+ });
7493
+ }
7494
+ }
7495
+ function createPluginUnits(options) {
7496
+ const registered = /* @__PURE__ */ new Map();
7497
+ let report = {
7498
+ ok: false,
7499
+ at: 0,
7500
+ plugins: [],
7501
+ problems: ["还没探过 opencli 的插件命令面"]
7502
+ };
7503
+ let inflight;
7504
+ let lastTouch = 0;
7505
+ let warned = "";
7506
+ function warn(problems) {
7507
+ const text = [...problems].sort().join("\n");
7508
+ if (text === "" || text === warned) return;
7509
+ warned = text;
7510
+ options.logger?.warn?.(`[dsh-workbench] opencli 插件命令面:\n${text}`);
7511
+ }
7512
+ async function run(force) {
7513
+ const probed = await options.catalog.refresh(force ? { force: true } : {});
7514
+ if (!probed.ok) {
7515
+ const problems = [...probed.problems, "这次探测没成功:上面这些插件行的工具面维持在上一份(不是\"这些插件没有命令\")"];
7516
+ report = {
7517
+ ok: false,
7518
+ at: Date.now(),
7519
+ plugins: report.plugins,
7520
+ problems
7521
+ };
7522
+ warn(problems);
7523
+ return report;
7524
+ }
7525
+ const plan = planOf(options, registered);
7526
+ reconcile(registered, plan.units, options);
7527
+ report = {
7528
+ ok: true,
7529
+ at: Date.now(),
7530
+ plugins: plan.rows,
7531
+ problems: plan.problems
7532
+ };
7533
+ warn(plan.problems);
7534
+ return report;
7535
+ }
7536
+ function refresh(callOptions = {}) {
7537
+ if (inflight !== void 0) return inflight;
7538
+ const task = run(callOptions.force === true).finally(() => {
7539
+ inflight = void 0;
7540
+ });
7541
+ inflight = task;
7542
+ return task;
7543
+ }
7544
+ return {
7545
+ refresh,
7546
+ touch() {
7547
+ const now = Date.now();
7548
+ if (inflight !== void 0) return;
7549
+ if (now - lastTouch < 3e4) return;
7550
+ lastTouch = now;
7551
+ refresh().catch((error) => {
7552
+ options.logger?.warn?.(`[dsh-workbench] 探 opencli 插件命令面时抛了错:${String(error)}`);
7553
+ });
7554
+ },
7555
+ report: () => report,
7556
+ dispose() {
7557
+ for (const item of registered.values()) item.drop();
7558
+ registered.clear();
7559
+ }
7560
+ };
7561
+ }
7562
+ //#endregion
6584
7563
  //#region src/shared/api/host/tool-scope.ts
6585
7564
  function agentOf(value) {
6586
7565
  return typeof value === "object" && value !== null ? value : void 0;
@@ -6767,6 +7746,23 @@ function apply(ctx) {
6767
7746
  deniedTools: (workspace) => deniedNow(workspace),
6768
7747
  logger: ctx.logger
6769
7748
  });
7749
+ const pluginTools = shell === void 0 ? void 0 : createPluginUnits({
7750
+ registry: units,
7751
+ catalog: createPluginCatalog({
7752
+ shell,
7753
+ command: opencli.command
7754
+ }),
7755
+ rows: () => ctx.get("workbenchPanels")?.listPlugins(null) ?? [],
7756
+ declared: (plugin) => declaredPluginCommands(profileDir).get(plugin) ?? /* @__PURE__ */ new Set(),
7757
+ opencliFor: (plugin) => createOpencli({
7758
+ shell,
7759
+ plugin,
7760
+ command: opencli.command,
7761
+ logger: ctx.logger
7762
+ }),
7763
+ toolNames: () => new Set(unitsOfTool().keys()),
7764
+ logger: ctx.logger
7765
+ });
6770
7766
  const mounted = /* @__PURE__ */ new Map();
6771
7767
  const mountFailures = /* @__PURE__ */ new Map();
6772
7768
  const factsOfMap = /* @__PURE__ */ new Map();
@@ -6920,6 +7916,23 @@ function apply(ctx) {
6920
7916
  });
6921
7917
  }
6922
7918
  sync();
7919
+ if (pluginTools !== void 0) {
7920
+ const health = routes.health;
7921
+ routes.health = async (search) => ({
7922
+ ...await health?.(search),
7923
+ pluginTools: pluginTools.report()
7924
+ });
7925
+ ctx.effect(() => {
7926
+ pluginTools.refresh().catch((error) => {
7927
+ warn(ctx, `探 opencli 插件的命令面时抛了错:${String(error)}`);
7928
+ });
7929
+ return () => pluginTools.dispose();
7930
+ }, `${PLUGIN_ID}: 已装且上板的 opencli 插件各合成一个单元(工具面照旧由面板那道闸收窄)`);
7931
+ ctx.effect(() => {
7932
+ const off = ctx.get(PANELS_SERVICE)?.subscribe?.(() => pluginTools.touch());
7933
+ return () => off?.();
7934
+ }, `${PLUGIN_ID}: 面板配置一动就重探插件命令面(装上 / 收起都在那一条链上)`);
7935
+ }
6923
7936
  ctx.effect(() => () => {
6924
7937
  for (const id of [...mounted.keys()]) unmountUnit(id);
6925
7938
  for (const dispose of undoPost) dispose?.();
@@ -6949,11 +7962,15 @@ function apply(ctx) {
6949
7962
  return () => off?.();
6950
7963
  }, `${PLUGIN_ID}: 事实归属(读工具与写工具同一处声明)`);
6951
7964
  ctx.effect(() => {
6952
- const off = ctx.on?.("agent/created", (payload) => toolScope.apply(payload?.agent));
7965
+ const off = ctx.on?.("agent/created", (payload) => {
7966
+ toolScope.apply(payload?.agent);
7967
+ pluginTools?.touch();
7968
+ });
6953
7969
  return () => off?.();
6954
7970
  }, `${PLUGIN_ID}: agent 的工具面按工作区收窄(创建时)`);
6955
7971
  ctx.effect(() => {
6956
7972
  const off = ctx.on?.("system-prompt/assemble", async (_assembly, context, next) => {
7973
+ pluginTools?.touch();
6957
7974
  const resolved = await next();
6958
7975
  return toolScope.narrow(resolved, context?.scope);
6959
7976
  });