@dsh-workbench/core 0.1.17 → 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 (64) hide show
  1. package/client.js +1026 -554
  2. package/dist/board.css +31 -0
  3. package/dist/client/client.body.js +1026 -554
  4. package/dist/client/index.js +1027 -555
  5. package/dist/host/index.js +1175 -146
  6. package/dist/types/app/client.d.ts +111 -69
  7. package/dist/types/app/layouts/market/__tests__/add-module-stages.test.d.ts +1 -0
  8. package/dist/types/app/layouts/market/add-module-text.d.ts +16 -1
  9. package/dist/types/app/layouts/market/use-add-module.d.ts +2 -8
  10. package/dist/types/app/layouts/market/use-module-versions.d.ts +2 -1
  11. package/dist/types/app/layouts/rightbar/MarketView.d.ts +5 -1
  12. package/dist/types/app/layouts/rightbar/ModulesPage.d.ts +4 -2
  13. package/dist/types/app/layouts/rightbar/PanelModules.d.ts +2 -2
  14. package/dist/types/app/layouts/rightbar/PanelPluginRows.d.ts +9 -0
  15. package/dist/types/app/layouts/rightbar/PanelRowControls.d.ts +14 -0
  16. package/dist/types/app/layouts/rightbar/__tests__/market-row-icon.test.d.ts +1 -0
  17. package/dist/types/app/layouts/rightbar/__tests__/market-section-icon.test.d.ts +1 -0
  18. package/dist/types/app/layouts/rightbar/__tests__/market-workbench.test.d.ts +1 -0
  19. package/dist/types/app/layouts/rightbar/__tests__/panel-plugin-rows.test.d.ts +1 -0
  20. package/dist/types/app/layouts/rightbar/__tests__/panel-scope-ui.test.d.ts +1 -0
  21. package/dist/types/app/layouts/rightbar/board-selection.d.ts +11 -0
  22. package/dist/types/app/layouts/rightbar/market-act.d.ts +21 -0
  23. package/dist/types/app/layouts/rightbar/market-freshness.d.ts +1 -0
  24. package/dist/types/app/layouts/rightbar/market-mock.d.ts +6 -1
  25. package/dist/types/app/layouts/rightbar/market-plugin-rows.d.ts +3 -1
  26. package/dist/types/app/layouts/rightbar/market-row-icon.d.ts +7 -0
  27. package/dist/types/app/layouts/rightbar/market-row-types.d.ts +35 -0
  28. package/dist/types/app/layouts/rightbar/market-rows.d.ts +8 -35
  29. package/dist/types/app/layouts/rightbar/market-section-icon.d.ts +3 -0
  30. package/dist/types/app/layouts/rightbar/market-section.d.ts +18 -0
  31. package/dist/types/app/layouts/rightbar/market-view-text.d.ts +7 -1
  32. package/dist/types/app/layouts/rightbar/market-workbench-row.d.ts +5 -0
  33. package/dist/types/app/layouts/rightbar/panel-mock.d.ts +2 -1
  34. package/dist/types/app/layouts/rightbar/use-panels.d.ts +15 -1
  35. package/dist/types/app/units/registry.d.ts +1 -1
  36. package/dist/types/modules/panels/__tests__/plugin-panel-market-parity.test.d.ts +1 -0
  37. package/dist/types/modules/panels/__tests__/plugin-panel.test.d.ts +1 -0
  38. package/dist/types/modules/panels/__tests__/plugin-tool-units.test.d.ts +1 -0
  39. package/dist/types/modules/panels/__tests__/scope-machine.test.d.ts +1 -0
  40. package/dist/types/modules/panels/__tests__/scope-support.d.ts +51 -0
  41. package/dist/types/modules/panels/model/config.d.ts +19 -0
  42. package/dist/types/modules/panels/model/market-dispatch.d.ts +4 -0
  43. package/dist/types/modules/panels/model/market-registry.d.ts +12 -0
  44. package/dist/types/modules/panels/model/market-registry.test.d.ts +1 -0
  45. package/dist/types/modules/panels/model/market-types.d.ts +1 -0
  46. package/dist/types/modules/panels/model/market.d.ts +3 -2
  47. package/dist/types/modules/panels/model/plugin-panel.d.ts +3 -0
  48. package/dist/types/modules/panels/model/prefs.d.ts +3 -1
  49. package/dist/types/modules/panels/model/service.d.ts +5 -1
  50. package/dist/types/shared/api/bundle-install.d.ts +15 -1
  51. package/dist/types/shared/api/host/__tests__/plugin-catalog.test.d.ts +1 -0
  52. package/dist/types/shared/api/host/__tests__/plugin-login-hint.test.d.ts +1 -0
  53. package/dist/types/shared/api/host/__tests__/plugin-tools.test.d.ts +1 -0
  54. package/dist/types/shared/api/host/bundle-inspect-kind.test.d.ts +1 -0
  55. package/dist/types/shared/api/host/bundle-routes-self.test.d.ts +1 -0
  56. package/dist/types/shared/api/host/installed-module.d.ts +10 -0
  57. package/dist/types/shared/api/host/plugin-catalog.d.ts +60 -0
  58. package/dist/types/shared/api/host/plugin-login-hint.d.ts +2 -0
  59. package/dist/types/shared/api/host/plugin-tools.d.ts +10 -0
  60. package/dist/types/shared/api/host/plugin-units.d.ts +47 -0
  61. package/dist/types/shared/api/host/unit-registry.d.ts +3 -0
  62. package/package.json +2 -2
  63. package/skills/workbench-module-diy/REVIEW.md +2 -2
  64. package/skills/workbench-module-diy/TOOLS.md +4 -1
@@ -67,7 +67,12 @@ var DECLARATION_REQUIRED_KEYS = [
67
67
  "host"
68
68
  ];
69
69
  var CARD_KEYS = ["titleKey", "order"];
70
- var TOOL_KEYS = ["name", "writes"];
70
+ var TOOL_KEYS = [
71
+ "name",
72
+ "writes",
73
+ "key"
74
+ ];
75
+ var TOOL_KEY_SHAPE = /^[a-z][a-zA-Z0-9]*$/;
71
76
  var HOST_KEYS = ["inject", "optional"];
72
77
  function text$1(value) {
73
78
  return typeof value === "string" ? value.trim() : "";
@@ -109,6 +114,7 @@ function declarationProblemsOf(subject, raw) {
109
114
  if (!Array.isArray(tools)) problems.push(`${subject}: dsh.workbench.tools 要是一串声明,现在是 ${JSON.stringify(tools)}`);
110
115
  else {
111
116
  const seen = /* @__PURE__ */ new Set();
117
+ const seenKeys = /* @__PURE__ */ new Set();
112
118
  tools.forEach((tool, index) => {
113
119
  const at = `dsh.workbench.tools[${index}]`;
114
120
  if (!isRecord(tool)) {
@@ -122,6 +128,12 @@ function declarationProblemsOf(subject, raw) {
122
128
  else if (seen.has(name)) problems.push(`${subject}: ${at}.name「${name}」与前面那条撞了 —— 工具名会进 dsh 的工具表,不许重`);
123
129
  else seen.add(name);
124
130
  if (typeof tool.writes !== "boolean") problems.push(`${subject}: ${at}.writes 要是 true / false(它是"这会写"的唯一声明,缺了审批无从认领)`);
131
+ if (tool.key !== void 0) {
132
+ const key = text$1(tool.key);
133
+ if (!TOOL_KEY_SHAPE.test(key)) problems.push(`${subject}: ${at}.key 要是 lowerCamelCase 的标识符(如 \`sync\` / \`podLogs\`),现在是 ${JSON.stringify(tool.key)} —— 它会变成生成物里的键名,所以不引号、不许带点或横线`);
134
+ else if (seenKeys.has(key)) problems.push(`${subject}: ${at}.key「${key}」与前面那条撞了 —— 生成物里的键只能有一个`);
135
+ else seenKeys.add(key);
136
+ }
125
137
  });
126
138
  }
127
139
  }
@@ -201,6 +213,71 @@ function parseJsonLoose(text) {
201
213
  }
202
214
  }
203
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
204
281
  //#region ../shared/dist/host/data-root.js
205
282
  function dshHome() {
206
283
  const configured = process.env.DSH_HOME?.trim();
@@ -270,20 +347,20 @@ var PERMISSION_CODES = /* @__PURE__ */ new Set([
270
347
  function asRecord$9(value) {
271
348
  return typeof value === "object" && value !== null ? value : void 0;
272
349
  }
273
- function textOf$4(value) {
350
+ function textOf$5(value) {
274
351
  return typeof value === "string" ? value.trim() : "";
275
352
  }
276
353
  function codeOf(page) {
277
354
  const record = asRecord$9(page);
278
- return record === void 0 ? "" : textOf$4(record["code"]);
355
+ return record === void 0 ? "" : textOf$5(record["code"]);
279
356
  }
280
- function messageOf$1(page) {
357
+ function messageOf$2(page) {
281
358
  const record = asRecord$9(page);
282
- return record === void 0 ? "" : textOf$4(record["message"]);
359
+ return record === void 0 ? "" : textOf$5(record["message"]);
283
360
  }
284
361
  function hintOf$1(page) {
285
362
  const record = asRecord$9(page);
286
- const hint = record === void 0 ? "" : textOf$4(record["hint"]);
363
+ const hint = record === void 0 ? "" : textOf$5(record["hint"]);
287
364
  return hint === "" ? void 0 : hint;
288
365
  }
289
366
  function pageFailed(page) {
@@ -472,7 +549,7 @@ async function browserRead(options) {
472
549
  message: "读到了"
473
550
  };
474
551
  const pageCode = codeOf(page) || "read-failed";
475
- const message = messageOf$1(page) || `读不到(${pageCode})`;
552
+ const message = messageOf$2(page) || `读不到(${pageCode})`;
476
553
  const blocker = blockerFor(pageCode);
477
554
  if (blocker === "auth-required") return {
478
555
  ok: false,
@@ -514,7 +591,7 @@ var DEFAULT_TEXT_MAX = 200;
514
591
  function asRecord$8(value) {
515
592
  return typeof value === "object" && value !== null ? value : void 0;
516
593
  }
517
- function textOf$3(value) {
594
+ function textOf$4(value) {
518
595
  return typeof value === "string" ? value.trim() : "";
519
596
  }
520
597
  function entriesOf(value) {
@@ -527,8 +604,8 @@ function entriesOf(value) {
527
604
  if (item === void 0) continue;
528
605
  const nth = typeof item["nth"] === "number" ? item["nth"] : entries.length;
529
606
  entries.push({
530
- text: textOf$3(item["text"]),
531
- tag: textOf$3(item["tag"]),
607
+ text: textOf$4(item["text"]),
608
+ tag: textOf$4(item["tag"]),
532
609
  nth
533
610
  });
534
611
  }
@@ -537,7 +614,7 @@ function entriesOf(value) {
537
614
  function reasonOf(stdout, stderr) {
538
615
  const parsed = asRecord$8(parseJsonLoose(stdout));
539
616
  const error = parsed === void 0 ? void 0 : asRecord$8(parsed["error"]);
540
- const message = error === void 0 ? "" : textOf$3(error["message"]);
617
+ const message = error === void 0 ? "" : textOf$4(error["message"]);
541
618
  if (message !== "") return message;
542
619
  const lines = (stderr.trim() === "" ? stdout : stderr).split("\n").map((one) => one.trim()).filter((one) => one !== "");
543
620
  return (lines[lines.length - 1] ?? "").slice(-200);
@@ -545,7 +622,7 @@ function reasonOf(stdout, stderr) {
545
622
  function hintOf(stdout) {
546
623
  const parsed = asRecord$8(parseJsonLoose(stdout));
547
624
  const error = parsed === void 0 ? void 0 : asRecord$8(parsed["error"]);
548
- const hint = error === void 0 ? "" : textOf$3(error["hint"]);
625
+ const hint = error === void 0 ? "" : textOf$4(error["hint"]);
549
626
  return hint === "" ? void 0 : hint;
550
627
  }
551
628
  function sameOrigin(left, right) {
@@ -559,7 +636,7 @@ function urlOf(stdout) {
559
636
  const bare = (stdout.split("\n").map((one) => one.trim()).filter((one) => one !== "")[0] ?? "").replace(/^url:\s*/i, "").trim().replace(/^["']+|["']+$/g, "");
560
637
  if (bare.startsWith("{")) {
561
638
  const parsed = asRecord$8(parseJsonLoose(bare));
562
- const url = parsed === void 0 ? "" : textOf$3(parsed["url"]);
639
+ const url = parsed === void 0 ? "" : textOf$4(parsed["url"]);
563
640
  if (url !== "") return url;
564
641
  }
565
642
  return bare;
@@ -784,7 +861,8 @@ var PROVISION_ACTIONS = [
784
861
  "cancel-install",
785
862
  "update-dependency",
786
863
  "update-plugin",
787
- "uninstall-plugin"
864
+ "uninstall-plugin",
865
+ "update-workbench"
788
866
  ];
789
867
  var DEPENDENCY_ACTIONS = ["update-dependency"];
790
868
  function isDependencyAction(action) {
@@ -795,6 +873,7 @@ function provisionTargetOf(action) {
795
873
  if (action === "install-bundle") return "spec";
796
874
  if (action === "remove-bundle") return "name";
797
875
  if (action === "update-plugin" || action === "uninstall-plugin") return "name";
876
+ if (action === "update-workbench") return "name";
798
877
  if (action === "cancel-install") return "requestId";
799
878
  return "unit";
800
879
  }
@@ -808,7 +887,7 @@ var TARGET_CODE = {
808
887
  function asRecord$7(value) {
809
888
  return typeof value === "object" && value !== null ? value : {};
810
889
  }
811
- function stringsOf$2(value) {
890
+ function stringsOf$3(value) {
812
891
  if (!Array.isArray(value)) return void 0;
813
892
  const items = value.filter((item) => typeof item === "string" && item.trim() !== "");
814
893
  return items.length === 0 ? void 0 : items;
@@ -830,6 +909,10 @@ function requestOf$1(action, value) {
830
909
  action,
831
910
  name: value
832
911
  };
912
+ if (action === "update-workbench") return {
913
+ action,
914
+ name: value
915
+ };
833
916
  if (action === "cancel-install") return {
834
917
  action,
835
918
  requestId: value
@@ -860,7 +943,7 @@ function parseProvisionBody(body) {
860
943
  request: requestOf$1(wanted, value.trim())
861
944
  };
862
945
  const registry = typeof raw.registry === "string" && raw.registry.trim() !== "" ? raw.registry.trim() : void 0;
863
- const approvedBuilds = stringsOf$2(raw.approvedBuilds);
946
+ const approvedBuilds = stringsOf$3(raw.approvedBuilds);
864
947
  const requestId = typeof raw.requestId === "string" && raw.requestId.trim() !== "" ? raw.requestId.trim() : void 0;
865
948
  return {
866
949
  ok: true,
@@ -874,6 +957,7 @@ function parseProvisionBody(body) {
874
957
  }
875
958
  //#endregion
876
959
  //#region ../shared/dist/host/opencli.js
960
+ var FORBIDDEN_CODES = /* @__PURE__ */ new Set(["FORBIDDEN", "PERMISSION_DENIED"]);
877
961
  var SITE_SESSION_ARGS = ["--site-session", "persistent"];
878
962
  var WorkbenchDataError = class extends Error {
879
963
  code;
@@ -901,6 +985,102 @@ function opencliCommandLine(command, argv, session = "persistent") {
901
985
  "json"
902
986
  ].join(" ");
903
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
+ }
904
1084
  //#endregion
905
1085
  //#region ../shared/dist/host/write-result.js
906
1086
  function writeResultForClient(value, code) {
@@ -1435,7 +1615,7 @@ var PLUGIN_LEDGER_FILE = "plugins.json";
1435
1615
  function ledgerKeyOf(entry) {
1436
1616
  return `${entry.opencliHome}\u0000${entry.package}`;
1437
1617
  }
1438
- function textOf$2(value) {
1618
+ function textOf$3(value) {
1439
1619
  if (typeof value !== "string") return void 0;
1440
1620
  const trimmed = value.trim();
1441
1621
  return trimmed === "" ? void 0 : trimmed;
@@ -1443,13 +1623,13 @@ function textOf$2(value) {
1443
1623
  function entryOf(value) {
1444
1624
  if (typeof value !== "object" || value === null) return void 0;
1445
1625
  const box = value;
1446
- const opencliHome = textOf$2(box["opencliHome"]);
1447
- const pkg = textOf$2(box["package"]);
1448
- 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"]);
1449
1629
  if (opencliHome === void 0 || pkg === void 0 || installedAt === void 0) return void 0;
1450
- const name = textOf$2(box["name"]);
1451
- const registry = textOf$2(box["registry"]);
1452
- 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"]);
1453
1633
  return {
1454
1634
  opencliHome,
1455
1635
  package: pkg,
@@ -1616,7 +1796,7 @@ function resolvePluginSource(source, env, options = {}) {
1616
1796
  }
1617
1797
  //#endregion
1618
1798
  //#region ../shared/dist/host/tool-approval.js
1619
- function messageOf(error) {
1799
+ function messageOf$1(error) {
1620
1800
  return error instanceof Error ? error.message : String(error);
1621
1801
  }
1622
1802
  function compose(detail, verdict) {
@@ -1634,7 +1814,7 @@ async function approvalFor(call, context = {}) {
1634
1814
  } catch (error) {
1635
1815
  return {
1636
1816
  kind: "deny",
1637
- reason: `问"要不要审批"的时候套件的策略抛了错,已经停下:${messageOf(error)}`
1817
+ reason: `问"要不要审批"的时候套件的策略抛了错,已经停下:${messageOf$1(error)}`
1638
1818
  };
1639
1819
  }
1640
1820
  if (verdict?.kind === "read") return void 0;
@@ -1648,7 +1828,7 @@ async function approvalFor(call, context = {}) {
1648
1828
  } catch (error) {
1649
1829
  return {
1650
1830
  kind: "deny",
1651
- reason: `拿不到"这次会发生什么",已经停下:${messageOf(error)}`
1831
+ reason: `拿不到"这次会发生什么",已经停下:${messageOf$1(error)}`
1652
1832
  };
1653
1833
  }
1654
1834
  if (facts !== void 0 && "blocked" in facts) return {
@@ -1667,6 +1847,15 @@ function sessionWorkspace(exec) {
1667
1847
  const cwd = exec?.agent?.session?.header?.cwd;
1668
1848
  return typeof cwd === "string" && cwd.trim() !== "" ? cwd.trim() : null;
1669
1849
  }
1850
+ function textOutput(text) {
1851
+ return [{
1852
+ type: "text",
1853
+ text
1854
+ }];
1855
+ }
1856
+ function jsonText(value) {
1857
+ return JSON.stringify(value);
1858
+ }
1670
1859
  //#endregion
1671
1860
  //#region ../shared/dist/host/value-handoff.js
1672
1861
  function handoffId(entry) {
@@ -1988,7 +2177,7 @@ function profileDirOf(ctx) {
1988
2177
  const dir = typeof profile?.dir === "string" ? profile.dir.trim() : "";
1989
2178
  return dir === "" ? null : dir;
1990
2179
  }
1991
- function stringsOf$1(value) {
2180
+ function stringsOf$2(value) {
1992
2181
  return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
1993
2182
  }
1994
2183
  function profileManifestOf(profileDir) {
@@ -2002,7 +2191,7 @@ function profileManifestOf(profileDir) {
2002
2191
  function profileModules(profileDir) {
2003
2192
  const record = profileManifestOf(profileDir);
2004
2193
  if (record === void 0) return [];
2005
- 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);
2006
2195
  const dependencies = asRecord$5(record.dependencies) ?? {};
2007
2196
  const names = bundles.length > 0 ? bundles : Object.keys(dependencies);
2008
2197
  const found = [];
@@ -2013,7 +2202,7 @@ function profileModules(profileDir) {
2013
2202
  unit: read.unit,
2014
2203
  version: read.version,
2015
2204
  ...read.registry === void 0 ? {} : { registry: read.registry },
2016
- local: asText$3(dependencies[read.pkg]).startsWith("link:")
2205
+ local: isLocalDependencySpec(asText$3(dependencies[read.pkg]))
2017
2206
  });
2018
2207
  }
2019
2208
  return found;
@@ -2058,6 +2247,20 @@ function declarationGaps(profileDir) {
2058
2247
  }
2059
2248
  return gaps;
2060
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
+ }
2061
2264
  function readInstalledModule(profileDir, pkg) {
2062
2265
  if (profileDir === null || !isPackageName(pkg)) return {
2063
2266
  kind: "unknown",
@@ -2093,6 +2296,32 @@ function readInstalledModule(profileDir, pkg) {
2093
2296
  ...registry === "" ? {} : { registry }
2094
2297
  };
2095
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
+ }
2096
2325
  //#endregion
2097
2326
  //#region src/shared/api/host/profile-registries.ts
2098
2327
  var REGISTRY_MARKER = "# dsh-workbench 写的:这一行的包只在这个源上(dsh 卸载没有源参数,pnpm 得问得到)";
@@ -2338,6 +2567,13 @@ var panelsApproval = (call) => {
2338
2567
  zh: `agent 想${verb} opencli 插件 ${what}(会改这台机器上装的东西)。允许吗?`
2339
2568
  });
2340
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
+ });
2341
2577
  if (action === "update-dependency") return askOf(`workbench_modules update-dependency ${id ?? ""} upgrades a shared dependency on this machine`, {
2342
2578
  en: "it upgrades that shared dependency on this machine. Allow?",
2343
2579
  zh: `会升级共用依赖 ${what}。允许吗?`
@@ -2411,13 +2647,20 @@ var EMPTY_BOARD = {
2411
2647
  };
2412
2648
  var BOARD_TARGETS = ["workspace", "machine"];
2413
2649
  var DEFAULT_BOARD_TARGET = "workspace";
2414
- function resolveScope(query) {
2415
- const { id, boards } = query;
2416
- 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) {
2417
2656
  if (board === null) return false;
2418
2657
  if (board.hidden.includes(id)) return false;
2419
2658
  return board.shown.includes(id);
2420
2659
  }
2660
+ function resolveScope(query) {
2661
+ const { id, boards } = query;
2662
+ return overrideOf(boards.workspace, id) ?? defaultOf(boards.machine, id);
2663
+ }
2421
2664
  function strings(raw) {
2422
2665
  return Array.isArray(raw) ? raw.filter((item) => typeof item === "string") : [];
2423
2666
  }
@@ -2434,7 +2677,7 @@ function parseReleased(raw) {
2434
2677
  return strings(raw.ids);
2435
2678
  }
2436
2679
  function resolvePanels(manifests, boards) {
2437
- const ordered = boards.workspace?.shown ?? boards.machine?.shown ?? [];
2680
+ const ordered = [.../* @__PURE__ */ new Set([...boards.workspace?.shown ?? [], ...boards.machine?.shown ?? []])];
2438
2681
  const rank = new Map(ordered.map((id, index) => [id, index]));
2439
2682
  return manifests.filter((manifest) => manifest.card !== null).map((manifest) => {
2440
2683
  const on = resolveScope({
@@ -2447,11 +2690,39 @@ function resolvePanels(manifests, boards) {
2447
2690
  titleKey: manifest.card?.titleKey ?? manifest.id,
2448
2691
  order,
2449
2692
  shown: on,
2450
- source: on && (boards.workspace?.shown.includes(manifest.id) ?? false) ? "user" : "default",
2693
+ source: overrideOf(boards.workspace, manifest.id) === void 0 ? "default" : "user",
2451
2694
  hasCard: true
2452
2695
  };
2453
2696
  }).sort((a, b) => a.order - b.order || a.id.localeCompare(b.id));
2454
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
+ }
2455
2726
  function withoutReleased(released, id) {
2456
2727
  return released.filter((item) => item !== id);
2457
2728
  }
@@ -2589,7 +2860,7 @@ var PLUGIN_PREFIX = "opencli-plugin-";
2589
2860
  function text(value) {
2590
2861
  return typeof value === "string" ? value.trim() : "";
2591
2862
  }
2592
- function stringsOf(value) {
2863
+ function stringsOf$1(value) {
2593
2864
  return Array.isArray(value) ? value.filter((one) => typeof one === "string") : [];
2594
2865
  }
2595
2866
  function manifestCommandsOf(raw) {
@@ -2620,7 +2891,7 @@ function manifestCommandsOf(raw) {
2620
2891
  strategy: text(row["strategy"]).toLowerCase(),
2621
2892
  browser: row["browser"] === true,
2622
2893
  example: text(row["example"]),
2623
- columns: stringsOf(row["columns"])
2894
+ columns: stringsOf$1(row["columns"])
2624
2895
  });
2625
2896
  }
2626
2897
  return commands;
@@ -2712,7 +2983,7 @@ function reconOf(commands, plugins, keywords) {
2712
2983
  };
2713
2984
  return {
2714
2985
  ...withNext,
2715
- text: textOf$1(withNext)
2986
+ text: textOf$2(withNext)
2716
2987
  };
2717
2988
  }
2718
2989
  function nextOf(answer) {
@@ -2725,7 +2996,7 @@ function nextOf(answer) {
2725
2996
  if (plugin !== void 0) return `这台机器另装的插件 ${plugin.name} 里有匹配:opencli ${plugin.name} --help 看全部命令`;
2726
2997
  return "这台机器上没有现成的 —— 两条路:① 给这个站点写 adapter:opencli skills read opencli-adapter-author;② 只读一次:workbench_diy action=probe(script 读接口 / css 读 DOM)";
2727
2998
  }
2728
- function textOf$1(answer) {
2999
+ function textOf$2(answer) {
2729
3000
  const lines = [];
2730
3001
  if (answer.sites.length > 0) {
2731
3002
  lines.push(`opencli 自带这 ${String(answer.sites.length)} 组能力(不用装、不用点头):`);
@@ -2770,7 +3041,7 @@ function installedPluginsOf(dir = vendorPluginsDir()) {
2770
3041
  }
2771
3042
  //#endregion
2772
3043
  //#region src/modules/panels/model/diy-tools.ts
2773
- var ACTIONS$1 = [
3044
+ var ACTIONS$2 = [
2774
3045
  "list",
2775
3046
  "status",
2776
3047
  "probe",
@@ -2907,7 +3178,7 @@ function registerDiyTools(ctx, market, panels) {
2907
3178
  action: {
2908
3179
  type: "string",
2909
3180
  description: "list=工坊里有哪些模块(默认);status=某个模块现在走到哪一步、下一步做什么;probe=在用户浏览器里探一条读;recon=先查 opencli 现成的能力(自带 adapter / 本机插件);open=只把它在这个项目上板(不装);install=装上并上板(第一次、会让用户点头)。",
2910
- enum: [...ACTIONS$1]
3181
+ enum: [...ACTIONS$2]
2911
3182
  },
2912
3183
  unit: {
2913
3184
  type: "string",
@@ -2966,7 +3237,7 @@ function registerDiyTools(ctx, market, panels) {
2966
3237
  async execute(raw, exec) {
2967
3238
  const args = asRecord$3(raw) ?? {};
2968
3239
  const action = asText$1(args.action) || "list";
2969
- 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(" / ")})`);
2970
3241
  const packagesDir = workshopPackagesDir(ctx);
2971
3242
  if (packagesDir === null) throw new Error("算不出 DIY 工坊在哪(这台机器没给出 dshHomePath)—— 让用户跑一次 dsh-workbench setup");
2972
3243
  const session = sessionWorkspace(exec);
@@ -3895,9 +4166,45 @@ function createEnvDropper(options) {
3895
4166
  };
3896
4167
  }
3897
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
3898
4205
  //#region src/modules/panels/model/market.ts
3899
4206
  function createMarketService(options) {
3900
- 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;
3901
4208
  const command = options.command ?? "opencli";
3902
4209
  const catalog = () => (manifestsNow?.() ?? manifests).filter((manifest) => (manifest.env?.plugins ?? []).length > 0 || (manifest.env?.libs ?? []).length > 0);
3903
4210
  function requireEntry(unit) {
@@ -4042,44 +4349,16 @@ function createMarketService(options) {
4042
4349
  };
4043
4350
  }
4044
4351
  }
4045
- async function rememberInstalledSource(manager, pkg, registry) {
4046
- if (registries === void 0 || registry === void 0) return;
4047
- const scope = scopeOf(pkg);
4048
- if (scope === null) return;
4049
- const known = await manager.registries().catch(() => null);
4050
- if (known !== null && sameRegistry(known.resolved, registry)) return;
4051
- try {
4052
- const added = registries.remember([{
4053
- scope,
4054
- registry
4055
- }]);
4056
- if (added.length > 0) logger?.info?.(`[dsh-workbench] 把 ${added.join("、")} 记进 profile 的 .npmrc(卸载只认 pnpm 的配置)`);
4057
- } catch (error) {
4058
- logger?.warn?.(`[dsh-workbench] 记这个 scope 的源失败:${String(error)}`);
4059
- }
4060
- }
4061
- async function retryAfterRegistryFix(manager, pkg, change) {
4062
- if (registries === void 0 || !registryFailure(change)) return change;
4063
- const known = await manager.registries().catch(() => null);
4064
- const missing = registries.sources().filter((entry) => !sameRegistry(entry.registry, known?.resolved ?? null));
4065
- if (missing.length === 0) return change;
4066
- let added;
4067
- try {
4068
- added = registries.remember(missing);
4069
- } catch (error) {
4070
- logger?.warn?.(`[dsh-workbench] 卸 ${pkg} 撞上源,但补配置失败:${String(error)}`);
4071
- return change;
4072
- }
4073
- if (added.length === 0) return change;
4074
- logger?.warn?.(`[dsh-workbench] 卸 ${pkg} 撞上源:补 ${added.join("、")} 后重试一次`);
4075
- return await manager.remove(pkg).catch(() => null) ?? change;
4076
- }
4352
+ const registryFix = {
4353
+ ...registries === void 0 ? {} : { registries },
4354
+ ...logger === void 0 ? {} : { logger }
4355
+ };
4077
4356
  function routeDeps(manager) {
4078
4357
  return {
4079
4358
  manager,
4080
4359
  command,
4081
4360
  runAll,
4082
- rememberSource: rememberInstalledSource,
4361
+ rememberSource: (manager, pkg, registry) => rememberInstalledSource(registryFix, manager, pkg, registry),
4083
4362
  supplyAfter,
4084
4363
  ...shell === void 0 ? {} : { shell },
4085
4364
  ...installed === void 0 ? {} : { installed },
@@ -4225,7 +4504,7 @@ function createMarketService(options) {
4225
4504
  const before = unit === void 0 ? void 0 : await capabilityOf(unit, true);
4226
4505
  const first = await manager.remove(pkg);
4227
4506
  if (first === null) throw new Error("插件管理器回的卸载结果看不懂。");
4228
- const change = await retryAfterRegistryFix(manager, pkg, first);
4507
+ const change = await retryAfterRegistryFix(registryFix, manager, pkg, first);
4229
4508
  if (change.application !== "applied" || unit === void 0) return {
4230
4509
  change,
4231
4510
  keptFor: []
@@ -4261,6 +4540,24 @@ function createMarketService(options) {
4261
4540
  await service.update(unit).catch(() => void 0);
4262
4541
  return outcome;
4263
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
+ },
4264
4561
  async updatePlugin(pkg) {
4265
4562
  const outcome = await plugins.update(pkg);
4266
4563
  await reprobePlugin(pkg, outcome.name);
@@ -4325,6 +4622,11 @@ async function dispatchMarket(market, request) {
4325
4622
  action: "cancel-install",
4326
4623
  cancellation: await market.cancelInstall(request.requestId ?? "")
4327
4624
  };
4625
+ if (request.action === "update-workbench") return {
4626
+ ok: true,
4627
+ action: "update-workbench",
4628
+ change: (await market.updateWorkbench(request.name ?? "")).change
4629
+ };
4328
4630
  if (request.action === "update-plugin") {
4329
4631
  await market.updatePlugin(request.name ?? "");
4330
4632
  return {
@@ -4419,12 +4721,16 @@ function describePluginUninstall(pkg, outcome) {
4419
4721
  if (outcome.removed) return `已卸载 opencli 插件 ${pkg}:没人再用它,文件也删了。`;
4420
4722
  return `已从「你装的插件」里去掉 ${pkg}。${outcome.kept === void 0 ? "" : pluginKeptText(outcome.kept)}`;
4421
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
+ }
4422
4728
  function usersOf(shared, names) {
4423
4729
  const hit = shared.find((item) => names.some((name) => name !== void 0 && name !== "" && item.id === `plugin:${name}`));
4424
4730
  if (hit === void 0 || hit.usedBy.length === 0) return "";
4425
4731
  return `它被 ${hit.usedBy.join("、")} 用着(动的是它们的供给,它们的状态跟着重探了一遍)`;
4426
4732
  }
4427
- var ACTIONS = [
4733
+ var ACTIONS$1 = [
4428
4734
  "list",
4429
4735
  "install",
4430
4736
  "update",
@@ -4432,7 +4738,8 @@ var ACTIONS = [
4432
4738
  "install-bundle",
4433
4739
  "update-dependency",
4434
4740
  "update-plugin",
4435
- "uninstall-plugin"
4741
+ "uninstall-plugin",
4742
+ "update-workbench"
4436
4743
  ];
4437
4744
  function registerMarketTools(ctx, market) {
4438
4745
  const tools = ctx.get("tools");
@@ -4442,14 +4749,14 @@ function registerMarketTools(ctx, market) {
4442
4749
  }
4443
4750
  const tool = {
4444
4751
  name: "workbench_modules",
4445
- 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 把工作台自己更新到源上的新版(客户端与宿主一般会自己换上;真要重启时结果里会说,重启由用户来,别自己重启)。卸载只动工作台运行时,不影响终端那套;共用依赖只有升级。",
4446
4753
  parameters: {
4447
4754
  type: "object",
4448
4755
  properties: {
4449
4756
  action: {
4450
4757
  type: "string",
4451
- description: "install-bundle=按 spec 装模块包(要 spec),update-dependency=升级共用依赖(要 id),update-plugin / uninstall-plugin=动用户自己装的 opencli 插件(要 plugin),其余=对模块(要 id)。",
4452
- enum: [...ACTIONS]
4758
+ description: "install-bundle=按 spec 装模块包(要 spec),update-dependency=升级共用依赖(要 id),update-plugin / uninstall-plugin=动用户自己装的 opencli 插件(要 plugin),update-workbench=把工作台自己更新到新版(不用别的参数),其余=对模块(要 id)。",
4759
+ enum: [...ACTIONS$1]
4453
4760
  },
4454
4761
  id: {
4455
4762
  type: "string",
@@ -4483,7 +4790,7 @@ function registerMarketTools(ctx, market) {
4483
4790
  },
4484
4791
  async execute(args) {
4485
4792
  const action = args?.action ?? "list";
4486
- 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(" / ")})`);
4487
4794
  if (action === "list") {
4488
4795
  const { modules, shared } = await market.list();
4489
4796
  return {
@@ -4504,6 +4811,13 @@ function registerMarketTools(ctx, market) {
4504
4811
  text: describeInstall(spec, outcome.module, outcome.supply, outcome.change)
4505
4812
  };
4506
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
+ }
4507
4821
  if (action === "update-plugin" || action === "uninstall-plugin") {
4508
4822
  const plugin = args?.plugin?.trim() ?? "";
4509
4823
  if (plugin === "") throw new Error(`${action} 需要一个 plugin:opencli 插件的包名或插件名 —— 就是市场「已安装 opencli 插件」那一段里的名字。`);
@@ -4554,53 +4868,70 @@ function registerMarketTools(ctx, market) {
4554
4868
  //#endregion
4555
4869
  //#region src/modules/panels/model/prefs.ts
4556
4870
  var PANELS_PREF_RESOURCE = "panels";
4871
+ var ACTIONS = [
4872
+ "show",
4873
+ "hide",
4874
+ "move",
4875
+ "reset",
4876
+ "follow"
4877
+ ];
4557
4878
  function parsePanelsPrefAction(payload) {
4558
- 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? }");
4559
4880
  const value = payload;
4560
4881
  const action = value.action;
4561
- 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)`);
4562
4884
  return {
4563
4885
  action,
4564
4886
  ...typeof value.id === "string" ? { id: value.id } : {},
4565
4887
  ...typeof value.index === "number" ? { index: value.index } : {},
4566
- ...typeof value.path === "string" ? { path: value.path } : {}
4888
+ ...typeof value.path === "string" ? { path: value.path } : {},
4889
+ ...value.scope === void 0 ? {} : { scope: value.scope }
4567
4890
  };
4568
4891
  }
4569
4892
  function applyPanelsPref(panels, payload) {
4570
4893
  const action = parsePanelsPrefAction(payload);
4571
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
+ }
4572
4903
  if (action.action === "reset") return {
4573
4904
  ok: true,
4574
- units: panels.reset(path)
4905
+ units: panels.reset(path, scope)
4575
4906
  };
4576
4907
  if (action.id === void 0) throw new Error(`${action.action} 需要一个 id(当前套件:${panels.list(path).map((entry) => entry.id).join(", ")})`);
4577
4908
  if (action.action === "show") return {
4578
4909
  ok: true,
4579
- units: panels.show(action.id, path)
4910
+ units: panels.show(action.id, path, scope)
4580
4911
  };
4581
4912
  if (action.action === "hide") return {
4582
4913
  ok: true,
4583
- units: panels.hide(action.id, path)
4914
+ units: panels.hide(action.id, path, scope)
4584
4915
  };
4585
4916
  return {
4586
4917
  ok: true,
4587
- units: panels.move(action.id, action.index ?? 0, path)
4918
+ units: panels.move(action.id, action.index ?? 0, path, scope)
4588
4919
  };
4589
4920
  }
4590
4921
  //#endregion
4591
4922
  //#region src/modules/panels/model/handoff.ts
4592
4923
  var PANELS_FACT = "panels";
4593
4924
  var CAPABILITY_FACT = "capability";
4594
- function textOf(value) {
4925
+ function textOf$1(value) {
4595
4926
  return typeof value === "string" ? value.trim() : "";
4596
4927
  }
4597
4928
  function actionOf(call) {
4598
- return textOf(call.args["action"]) || "list";
4929
+ return textOf$1(call.args["action"]) || "list";
4599
4930
  }
4600
4931
  function requestOf(call) {
4601
4932
  const action = actionOf(call);
4602
4933
  if (!PROVISION_ACTIONS.includes(action)) return void 0;
4603
- const target = textOf(call.args["unit"]) || textOf(call.args["id"]);
4934
+ const target = textOf$1(call.args["unit"]) || textOf$1(call.args["id"]);
4604
4935
  if (target === "") return void 0;
4605
4936
  const wanted = action;
4606
4937
  return isDependencyAction(wanted) ? {
@@ -4612,7 +4943,7 @@ function requestOf(call) {
4612
4943
  };
4613
4944
  }
4614
4945
  function panelsHandoffEntry(target = {}) {
4615
- const path = textOf(target.path);
4946
+ const path = textOf$1(target.path);
4616
4947
  return path === "" ? void 0 : {
4617
4948
  fact: PANELS_FACT,
4618
4949
  key: `path:${path}`
@@ -4623,6 +4954,7 @@ function panelsFactsOf(call, manifests) {
4623
4954
  if (call.source === "workbench_diy" && actionOf(call) === "list") return [PANELS_FACT];
4624
4955
  if (call.source !== "workbench_modules" && call.source !== "workbench_diy") return void 0;
4625
4956
  if (actionOf(call) === "list") return [PANELS_FACT, CAPABILITY_FACT];
4957
+ if (actionOf(call) === "update-workbench") return [PANELS_FACT, CAPABILITY_FACT];
4626
4958
  const request = requestOf(call);
4627
4959
  if (request === void 0) return void 0;
4628
4960
  return [
@@ -4710,7 +5042,14 @@ function createPanelsService(options) {
4710
5042
  const all = manifests();
4711
5043
  if (!all.some((manifest) => manifest.id === id)) throw new Error(`没有这个套件:${id}(可用:${all.map((manifest) => manifest.id).join(", ")})`);
4712
5044
  }
5045
+ function pluginRefs() {
5046
+ return options.plugins?.() ?? [];
5047
+ }
5048
+ function pluginIds() {
5049
+ return new Set(pluginRefs().map((plugin) => pluginPanelId(plugin.pkg)));
5050
+ }
4713
5051
  function requireSwitchable(id) {
5052
+ if (pluginIds().has(id)) return;
4714
5053
  requireKnown(id);
4715
5054
  if (!manifests().some((manifest) => manifest.id === id && manifest.card !== null)) throw new Error(`${id} 没有卡片,不能收起(它是把别的套件放回来的入口)`);
4716
5055
  }
@@ -4723,6 +5062,9 @@ function createPanelsService(options) {
4723
5062
  list(workspace, target) {
4724
5063
  return resolvePanels(manifests(), target === "machine" ? boardsOf(null) : boardsOf(workspace));
4725
5064
  },
5065
+ listPlugins(workspace, target) {
5066
+ return resolvePluginPanels(pluginRefs(), target === "machine" ? boardsOf(null) : boardsOf(workspace));
5067
+ },
4726
5068
  boards(workspace) {
4727
5069
  return boardsOf(workspace);
4728
5070
  },
@@ -4780,6 +5122,24 @@ function createPanelsService(options) {
4780
5122
  reset(workspace, target) {
4781
5123
  const { folder, key } = targetOf(target ?? "workspace", workspace);
4782
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);
4783
5143
  }
4784
5144
  };
4785
5145
  }
@@ -4790,13 +5150,15 @@ function panelRoutes(panels, values) {
4790
5150
  const path = param(search, "path") ?? null;
4791
5151
  const entry = panelsHandoffEntry(path === null ? {} : { path });
4792
5152
  const handed = entry === void 0 ? void 0 : values?.take(entry);
4793
- if (handed !== void 0) return {
4794
- ok: true,
4795
- units: handed.value
4796
- };
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;
4797
5155
  return {
4798
5156
  ok: true,
4799
- 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())
4800
5162
  };
4801
5163
  } };
4802
5164
  }
@@ -4855,6 +5217,18 @@ function createPluginLedger(options) {
4855
5217
  };
4856
5218
  }
4857
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
4858
5232
  //#region src/shared/api/installed-plugins.ts
4859
5233
  var INSTALLED_PLUGINS_ROUTE = "panels/plugins";
4860
5234
  //#endregion
@@ -4944,24 +5318,25 @@ function registerPanelTools(ctx, panels, values) {
4944
5318
  }
4945
5319
  const tool = {
4946
5320
  name: "workbench_panels",
4947
- description: "看或调右侧面板:启用哪些套件、什么顺序。启用/收起 = 卡片与 agent 工具一起上下线(别的项目不受影响,不删数据)。收起是\"这个工作区明确不要它\",连那些在机器级默认板上开着的套件也照样在这个工作区收起来,之后 show 能放回来。默认只动当前工作区(scope=workspace);scope=machine 动的是\"机器级默认板\" —— 机器级默认板只是还没配过的工作区的默认值,已经自己配过的工作区保持自己的选择,不会被它改。在那份板上 hide = 明确写下\"不要它\",这一笔比名单更管用:名单里还列着它也算不要,没配过的工作区因此看不到它;show 会把这笔划掉、把它放回去。",
5321
+ description: "看或调右侧面板:启用哪些套件、什么顺序。启用/收起 = 卡片与 agent 工具一起上下线(别的项目不受影响,不删数据)。收起是\"这个工作区明确不要它\",连那些在机器级默认板上开着的套件也照样在这个工作区收起来,之后 show 能放回来。默认只动当前工作区(scope=workspace);scope=machine 动的是\"机器级默认板\" —— 机器级默认板只是还没配过的工作区的默认值,已经自己配过的工作区保持自己的选择,不会被它改。在那份板上 hide = 明确写下\"不要它\",这一笔比名单更管用:名单里还列着它也算不要,没提过它的工作区因此看不到它;show 会把这笔划掉、把它放回去。本工作区那份板只记**改过的那几行**(逐行覆盖):没提到的行跟着机器级默认板走,所以那份板以后改了,没改过的工作区会跟着变。workbench_panels 默认只动当前工作区,动机器级默认板要显式给 scope=machine。",
4948
5322
  parameters: {
4949
5323
  type: "object",
4950
5324
  properties: {
4951
5325
  action: {
4952
5326
  type: "string",
4953
- description: "show=这个工作区要它(之前收起的也放回来);hide=这个工作区明确不要它(卡片与工具一起下线,默认在机器上也开着的套件同样收起,别的项目不受影响);move=调顺序;reset=这个工作区不再有自己的选择(之前收起、启用的都忘掉;它从此明确\"什么都不要\",机器级默认板也管不到它,要哪一块用 show 放回来)。",
5327
+ description: "show=这个工作区要它(之前收起的也放回来);hide=这个工作区明确不要它(卡片与工具一起下线,默认在机器上也开着的套件同样收起,别的项目不受影响);move=调顺序;reset=清掉这个工作区改过的那些行(改完就全部回到跟着机器级默认板走);follow=把某一行放回跟随(带 id = 只放回那一行;不带 id = 全部放回)。",
4954
5328
  enum: [
4955
5329
  "list",
4956
5330
  "show",
4957
5331
  "hide",
4958
5332
  "move",
4959
- "reset"
5333
+ "reset",
5334
+ "follow"
4960
5335
  ]
4961
5336
  },
4962
5337
  id: {
4963
5338
  type: "string",
4964
- description: "show / hide / move 必填,如 coding-issues。"
5339
+ description: "show / hide / move 必填;follow 可选(带 id = 只把那一行放回跟随)。"
4965
5340
  },
4966
5341
  index: {
4967
5342
  type: "integer",
@@ -4997,8 +5372,9 @@ function registerPanelTools(ctx, panels, values) {
4997
5372
  "show",
4998
5373
  "hide",
4999
5374
  "move",
5000
- "reset"
5001
- ].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)`);
5002
5378
  if (action === "list") {
5003
5379
  const units = list();
5004
5380
  if (scope === "workspace") {
@@ -5010,11 +5386,19 @@ function registerPanelTools(ctx, panels, values) {
5010
5386
  text: render(units, workspace, scope)
5011
5387
  };
5012
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
+ }
5013
5397
  if (action === "reset") {
5014
5398
  const units = panels.reset(workspace, scope);
5015
5399
  return {
5016
5400
  units,
5017
- 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)}`
5018
5402
  };
5019
5403
  }
5020
5404
  if (id === void 0) throw new Error(`${action} 需要一个 id(例如 coding-issues)。当前套件:${list().map((entry) => entry.id).join(", ")}`);
@@ -5047,19 +5431,20 @@ var OWNED = [{
5047
5431
  unit: panelsUnit.id,
5048
5432
  manifest: panelsUnit,
5049
5433
  mount({ ctx, logger, manifests, ownedUnits, opencliCommand, probe, store, values }) {
5434
+ const ledger = createPluginLedger({
5435
+ store,
5436
+ home: isolatedHome()
5437
+ });
5050
5438
  const panels = createPanelsService({
5051
5439
  manifests,
5052
5440
  store,
5053
5441
  owned: ownedUnits,
5442
+ plugins: () => pluginPanelRefs(ledger),
5054
5443
  logger
5055
5444
  });
5056
5445
  ctx.provide(PANELS_SERVICE, panels);
5057
5446
  const profileDir = profileDirOf(ctx);
5058
5447
  const shell = ctx.get("shell");
5059
- const ledger = createPluginLedger({
5060
- store,
5061
- home: isolatedHome()
5062
- });
5063
5448
  const market = createMarketService({
5064
5449
  manifests: manifests(),
5065
5450
  manifestsNow: manifests,
@@ -5069,6 +5454,7 @@ var OWNED = [{
5069
5454
  bundles: readBundleManager(ctx),
5070
5455
  installed: (pkg) => readInstalledModule(profileDir, pkg),
5071
5456
  modules: () => profileModules(profileDir),
5457
+ workbench: (pkg) => readProfilePackage(profileDir, pkg),
5072
5458
  registries: createRegistryNotebook(profileDir),
5073
5459
  ledger,
5074
5460
  references: () => {
@@ -5448,6 +5834,24 @@ function bundleRegistriesHandler(ctx) {
5448
5834
  };
5449
5835
  }
5450
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
+ }
5451
5855
  function moduleVersionsHandler(ctx, shell) {
5452
5856
  return async (search) => {
5453
5857
  const manager = readBundleManager(ctx);
@@ -5460,36 +5864,44 @@ function moduleVersionsHandler(ctx, shell) {
5460
5864
  const refusals = new Map((ctx.get("workbenchUnits")?.refusals() ?? []).filter((one) => one.unit !== "").map((one) => [one.unit, one.reasons]));
5461
5865
  const force = param(search, "force") === "1";
5462
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
+ });
5463
5901
  return {
5464
5902
  ok: true,
5465
- modules: await Promise.all(found.map(async (module) => {
5466
- const registry = module.registry ?? fallback;
5467
- const latest = shell === void 0 || registry === void 0 ? void 0 : await latestVersion({
5468
- shell,
5469
- name: module.pkg,
5470
- registry,
5471
- force
5472
- });
5473
- const raw = moduleDeclarationOf(dir, module.pkg);
5474
- const declared = raw === void 0 ? [] : declarationProblemsOf(module.pkg, raw);
5475
- const refused = refusals.get(module.unit) ?? (declared.length === 0 ? void 0 : declared);
5476
- if (refused === void 0) refusalLog.delete(module.pkg);
5477
- else {
5478
- const line = refused.join(";");
5479
- if (refusalLog.get(module.pkg) !== line) {
5480
- refusalLog.set(module.pkg, line);
5481
- ctx.logger?.warn?.(`[dsh-workbench] ${module.pkg} 装上了但用不了:${line}`);
5482
- }
5483
- }
5484
- return {
5485
- unit: module.unit,
5486
- pkg: module.pkg,
5487
- installed: module.version,
5488
- ...latest === void 0 ? {} : { latest },
5489
- ...module.local === true ? { local: true } : {},
5490
- ...refused === void 0 ? {} : { refused }
5491
- };
5492
- }))
5903
+ modules,
5904
+ ...workbench === void 0 ? {} : { workbench }
5493
5905
  };
5494
5906
  };
5495
5907
  }
@@ -5516,15 +5928,26 @@ async function declarationRefusal(spec, inspection, ctx, manager) {
5516
5928
  });
5517
5929
  return refusedInspection(declarationRefusalOf(inspection.name, raw));
5518
5930
  }
5519
- async function packageRefusal(spec, inspection, ctx, manager) {
5520
- if (inspection.status !== "accepted") return void 0;
5931
+ async function packageOverride(spec, inspection, ctx, manager, asked) {
5521
5932
  const shell = ctx.get("shell");
5522
- const registry = typeof inspection.registry === "string" ? inspection.registry : (await manager.registries())?.resolved ?? void 0;
5523
- 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({
5524
5935
  spec,
5525
5936
  ...shell === void 0 ? {} : { shell },
5526
5937
  ...registry === void 0 ? {} : { registry }
5527
- }))?.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" ? {
5528
5951
  status: "refused",
5529
5952
  problem: "not-a-module-or-plugin",
5530
5953
  reason: refusalReason()
@@ -5548,7 +5971,8 @@ function bundleInspectHandler(ctx) {
5548
5971
  message: "这个 profile 里没有插件管理器,装不了模块。"
5549
5972
  }
5550
5973
  };
5551
- const inspection = await manager.inspect(spec, registryOf(search));
5974
+ const asked = registryOf(search);
5975
+ const inspection = await manager.inspect(spec, asked);
5552
5976
  if (inspection === null) return {
5553
5977
  ok: false,
5554
5978
  error: {
@@ -5558,7 +5982,7 @@ function bundleInspectHandler(ctx) {
5558
5982
  };
5559
5983
  return {
5560
5984
  ok: true,
5561
- 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
5562
5986
  };
5563
5987
  };
5564
5988
  }
@@ -6484,7 +6908,9 @@ function mountUnits(units, deps) {
6484
6908
  //#endregion
6485
6909
  //#region src/shared/api/host/unit-registry.ts
6486
6910
  function sourceLabel(source) {
6487
- return source.kind === "owned" ? "外壳自己" : source.pkg;
6911
+ if (source.kind === "owned") return "外壳自己";
6912
+ if (source.kind === "plugin") return "按已装 opencli 插件合成";
6913
+ return source.pkg;
6488
6914
  }
6489
6915
  function createUnitRegistry(options) {
6490
6916
  const refusals = [];
@@ -6544,6 +6970,14 @@ function createUnitRegistry(options) {
6544
6970
  source: { kind: "owned" }
6545
6971
  });
6546
6972
  },
6973
+ registerPlugin(face) {
6974
+ return add({
6975
+ unit: face.manifest.id,
6976
+ manifest: face.manifest,
6977
+ face,
6978
+ source: { kind: "plugin" }
6979
+ });
6980
+ },
6547
6981
  all: () => hosts,
6548
6982
  owned: () => hosts.filter((host) => host.source.kind === "owned").map((host) => host.unit),
6549
6983
  refusals: () => refusals,
@@ -6569,6 +7003,563 @@ function bindRosterFacts(options) {
6569
7003
  });
6570
7004
  }
6571
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
6572
7563
  //#region src/shared/api/host/tool-scope.ts
6573
7564
  function agentOf(value) {
6574
7565
  return typeof value === "object" && value !== null ? value : void 0;
@@ -6755,6 +7746,23 @@ function apply(ctx) {
6755
7746
  deniedTools: (workspace) => deniedNow(workspace),
6756
7747
  logger: ctx.logger
6757
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
+ });
6758
7766
  const mounted = /* @__PURE__ */ new Map();
6759
7767
  const mountFailures = /* @__PURE__ */ new Map();
6760
7768
  const factsOfMap = /* @__PURE__ */ new Map();
@@ -6908,6 +7916,23 @@ function apply(ctx) {
6908
7916
  });
6909
7917
  }
6910
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
+ }
6911
7936
  ctx.effect(() => () => {
6912
7937
  for (const id of [...mounted.keys()]) unmountUnit(id);
6913
7938
  for (const dispose of undoPost) dispose?.();
@@ -6937,11 +7962,15 @@ function apply(ctx) {
6937
7962
  return () => off?.();
6938
7963
  }, `${PLUGIN_ID}: 事实归属(读工具与写工具同一处声明)`);
6939
7964
  ctx.effect(() => {
6940
- 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
+ });
6941
7969
  return () => off?.();
6942
7970
  }, `${PLUGIN_ID}: agent 的工具面按工作区收窄(创建时)`);
6943
7971
  ctx.effect(() => {
6944
7972
  const off = ctx.on?.("system-prompt/assemble", async (_assembly, context, next) => {
7973
+ pluginTools?.touch();
6945
7974
  const resolved = await next();
6946
7975
  return toolScope.narrow(resolved, context?.scope);
6947
7976
  });