@dsh-workbench/core 0.1.16 → 0.1.17

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 (66) hide show
  1. package/client.js +660 -128
  2. package/dist/client/client.body.js +660 -128
  3. package/dist/client/index.js +660 -128
  4. package/dist/host/index.js +1498 -263
  5. package/dist/types/app/client.d.ts +124 -9
  6. package/dist/types/app/layouts/market/add-module-text.d.ts +2 -0
  7. package/dist/types/app/layouts/market/use-add-module.d.ts +4 -3
  8. package/dist/types/app/layouts/market/use-module-versions.d.ts +2 -0
  9. package/dist/types/app/layouts/rightbar/MarketView.d.ts +3 -1
  10. package/dist/types/app/layouts/rightbar/__tests__/market-rows-plugins.test.d.ts +1 -0
  11. package/dist/types/app/layouts/rightbar/market-mock.d.ts +9 -0
  12. package/dist/types/app/layouts/rightbar/market-plugin-rows.d.ts +8 -0
  13. package/dist/types/app/layouts/rightbar/market-rows.d.ts +14 -3
  14. package/dist/types/app/layouts/rightbar/registry.d.ts +0 -1
  15. package/dist/types/app/units/__tests__/manifests.test.d.ts +1 -0
  16. package/dist/types/app/units/fixture-unit-mock.d.ts +3 -3
  17. package/dist/types/app/units/host.d.ts +2 -2
  18. package/dist/types/app/units/manifests.d.ts +5 -0
  19. package/dist/types/app/units/registry.d.ts +10 -2
  20. package/dist/types/modules/panels/__tests__/market-env.test.d.ts +1 -0
  21. package/dist/types/modules/panels/__tests__/market-plugin.test.d.ts +1 -0
  22. package/dist/types/modules/panels/__tests__/market-route.test.d.ts +1 -0
  23. package/dist/types/modules/panels/__tests__/plugin-actions.test.d.ts +1 -0
  24. package/dist/types/modules/panels/__tests__/plugin-ledger.test.d.ts +1 -0
  25. package/dist/types/modules/panels/__tests__/plugin-section.test.d.ts +1 -0
  26. package/dist/types/modules/panels/__tests__/scope.test.d.ts +1 -0
  27. package/dist/types/modules/panels/__tests__/service-release.test.d.ts +1 -0
  28. package/dist/types/modules/panels/index.d.ts +2 -2
  29. package/dist/types/modules/panels/model/config.d.ts +27 -9
  30. package/dist/types/modules/panels/model/market-dispatch.d.ts +9 -0
  31. package/dist/types/modules/panels/model/market-env.d.ts +20 -0
  32. package/dist/types/modules/panels/model/market-route.d.ts +26 -0
  33. package/dist/types/modules/panels/model/market-types.d.ts +71 -0
  34. package/dist/types/modules/panels/model/market.d.ts +7 -66
  35. package/dist/types/modules/panels/model/plugin-actions.d.ts +50 -0
  36. package/dist/types/modules/panels/model/plugin-ledger.d.ts +22 -0
  37. package/dist/types/modules/panels/model/plugin-name.d.ts +8 -0
  38. package/dist/types/modules/panels/model/plugin-name.test.d.ts +1 -0
  39. package/dist/types/modules/panels/model/plugin-section.d.ts +13 -0
  40. package/dist/types/modules/panels/model/service.d.ts +8 -6
  41. package/dist/types/shared/api/bundle-install.d.ts +8 -1
  42. package/dist/types/shared/api/host/__tests__/registry-declaration.test.d.ts +1 -0
  43. package/dist/types/shared/api/host/__tests__/unit-roster-facts.test.d.ts +1 -0
  44. package/dist/types/shared/api/host/bundle-routes.d.ts +1 -0
  45. package/dist/types/shared/api/host/installed-module.d.ts +7 -0
  46. package/dist/types/shared/api/host/mount-units.d.ts +5 -3
  47. package/dist/types/shared/api/host/package-content.d.ts +6 -0
  48. package/dist/types/shared/api/host/package-content.test.d.ts +1 -0
  49. package/dist/types/shared/api/host/package-kind.d.ts +21 -0
  50. package/dist/types/shared/api/host/package-kind.test.d.ts +1 -0
  51. package/dist/types/shared/api/host/package-route.d.ts +23 -0
  52. package/dist/types/shared/api/host/package-route.test.d.ts +1 -0
  53. package/dist/types/shared/api/host/registry-declaration.d.ts +23 -0
  54. package/dist/types/shared/api/host/unit-registry.d.ts +33 -4
  55. package/dist/types/shared/api/host/unit-roster-facts.d.ts +5 -0
  56. package/dist/types/shared/api/installed-plugins.d.ts +12 -0
  57. package/dist/types/shared/api/unit-manifests.d.ts +5 -0
  58. package/dist/types/shared/api/unit-manifests.test.d.ts +1 -0
  59. package/dist/types/shared/hooks/bundle-queries.d.ts +2 -0
  60. package/dist/types/shared/hooks/use-facts.d.ts +1 -0
  61. package/package.json +2 -2
  62. package/skills/workbench-module-diy/BUILD.md +5 -5
  63. package/skills/workbench-module-diy/DESIGN.md +7 -1
  64. package/skills/workbench-module-diy/REVIEW.md +3 -2
  65. package/skills/workbench-module-diy/TOOLING.md +2 -2
  66. package/skills/workbench-module-diy/TOOLS.md +9 -2
@@ -1,6 +1,7 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import fs, { chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, readdirSync, readlinkSync, realpathSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
3
3
  import path, { delimiter, dirname, join, resolve, sep } from "node:path";
4
+ import os from "node:os";
4
5
  import { spawn } from "node:child_process";
5
6
  import { fileURLToPath } from "node:url";
6
7
  //#region ../shared/dist/config/constants.js
@@ -49,6 +50,121 @@ function parseMarkers(text) {
49
50
  }
50
51
  return refs;
51
52
  }
53
+ var SUPPORTED_API_VERSIONS = [1];
54
+ var DECLARATION_KEYS = [
55
+ "apiVersion",
56
+ "unit",
57
+ "card",
58
+ "tools",
59
+ "host",
60
+ "env"
61
+ ];
62
+ var DECLARATION_REQUIRED_KEYS = [
63
+ "apiVersion",
64
+ "unit",
65
+ "card",
66
+ "tools",
67
+ "host"
68
+ ];
69
+ var CARD_KEYS = ["titleKey", "order"];
70
+ var TOOL_KEYS = ["name", "writes"];
71
+ var HOST_KEYS = ["inject", "optional"];
72
+ function text$1(value) {
73
+ return typeof value === "string" ? value.trim() : "";
74
+ }
75
+ function isRecord(value) {
76
+ return typeof value === "object" && value !== null && !Array.isArray(value);
77
+ }
78
+ function outsideKeys(value, allowed) {
79
+ return Object.keys(value).filter((key) => !allowed.includes(key));
80
+ }
81
+ function listOf(keys) {
82
+ return keys.map((key) => JSON.stringify(key)).join(" / ");
83
+ }
84
+ function stringArrayProblems(at, value) {
85
+ if (!Array.isArray(value)) return [`${at} 要是一串字符串,现在是 ${JSON.stringify(value)}`];
86
+ return value.flatMap((item, index) => text$1(item) === "" ? [`${at}[${index}] 不是非空字符串:${JSON.stringify(item)}`] : []);
87
+ }
88
+ function declarationProblemsOf(subject, raw) {
89
+ if (!isRecord(raw)) return [`${subject}: dsh.workbench 不是一个对象 —— ${JSON.stringify(raw)}`];
90
+ const version = raw.apiVersion;
91
+ if (version === void 0) return [`${subject}: dsh.workbench 没有 apiVersion —— 协议从 v1 起,写 \`"apiVersion": 1\`。缺它 = 外壳不认这个模块(ADR-0038 §2.3)`];
92
+ if (!Number.isInteger(version) || !SUPPORTED_API_VERSIONS.includes(version)) return [`${subject}: dsh.workbench.apiVersion 是 ${JSON.stringify(version)},这里只认 ${SUPPORTED_API_VERSIONS.join(" / ")}(要整数)`];
93
+ const problems = [];
94
+ const unknown = outsideKeys(raw, DECLARATION_KEYS);
95
+ if (unknown.length > 0) problems.push(`${subject}: dsh.workbench 里有协议 v1 不认识的键 ${listOf(unknown)} —— 只有 ${listOf(DECLARATION_KEYS)}。(拼错的键会被外壳**静默忽略**:那句声明等于没写)`);
96
+ for (const key of DECLARATION_REQUIRED_KEYS) if (raw[key] === void 0) problems.push(`${subject}: dsh.workbench 缺 ${key}` + (key === "card" ? "(这个模块没有卡片就写 `\"card\": null`)" : ""));
97
+ const card = raw.card;
98
+ if (card !== void 0 && card !== null) {
99
+ if (!isRecord(card)) problems.push(`${subject}: dsh.workbench.card 要么是对象、要么是 null,现在是 ${JSON.stringify(card)}`);
100
+ else {
101
+ const extra = outsideKeys(card, CARD_KEYS);
102
+ if (extra.length > 0) problems.push(`${subject}: dsh.workbench.card 里有不认的键 ${listOf(extra)} —— card 只有 ${listOf(CARD_KEYS)}(卡片默认显不显示由运行期的面板配置决定,清单里没有这一位)`);
103
+ if (text$1(card.titleKey) === "") problems.push(`${subject}: dsh.workbench.card.titleKey 要是非空字符串(面板那一格的文案键)`);
104
+ if (typeof card.order !== "number" || !Number.isFinite(card.order)) problems.push(`${subject}: dsh.workbench.card.order 要是数字,现在是 ${JSON.stringify(card.order)}`);
105
+ }
106
+ }
107
+ const tools = raw.tools;
108
+ if (tools !== void 0) {
109
+ if (!Array.isArray(tools)) problems.push(`${subject}: dsh.workbench.tools 要是一串声明,现在是 ${JSON.stringify(tools)}`);
110
+ else {
111
+ const seen = /* @__PURE__ */ new Set();
112
+ tools.forEach((tool, index) => {
113
+ const at = `dsh.workbench.tools[${index}]`;
114
+ if (!isRecord(tool)) {
115
+ problems.push(`${subject}: ${at} 要是对象,现在是 ${JSON.stringify(tool)}`);
116
+ return;
117
+ }
118
+ const extra = outsideKeys(tool, TOOL_KEYS);
119
+ if (extra.length > 0) problems.push(`${subject}: ${at} 里有不认的键 ${listOf(extra)} —— tools 只有 ${listOf(TOOL_KEYS)}`);
120
+ const name = text$1(tool.name);
121
+ if (name === "") problems.push(`${subject}: ${at}.name 要是非空字符串`);
122
+ else if (seen.has(name)) problems.push(`${subject}: ${at}.name「${name}」与前面那条撞了 —— 工具名会进 dsh 的工具表,不许重`);
123
+ else seen.add(name);
124
+ if (typeof tool.writes !== "boolean") problems.push(`${subject}: ${at}.writes 要是 true / false(它是"这会写"的唯一声明,缺了审批无从认领)`);
125
+ });
126
+ }
127
+ }
128
+ const host = raw.host;
129
+ if (host !== void 0) {
130
+ if (!isRecord(host)) problems.push(`${subject}: dsh.workbench.host 要是对象,现在是 ${JSON.stringify(host)}`);
131
+ else {
132
+ const extra = outsideKeys(host, HOST_KEYS);
133
+ if (extra.length > 0) problems.push(`${subject}: dsh.workbench.host 里有不认的键 ${listOf(extra)} —— host 只有 ${listOf(HOST_KEYS)}`);
134
+ if (host.inject === void 0) problems.push(`${subject}: dsh.workbench.host.inject 没写 —— 它声明这个模块要外壳的哪几个服务,外壳靠它判"能不能挂"。一个都不要也要写 \`"inject": []\``);
135
+ else problems.push(...stringArrayProblems(`${subject}: dsh.workbench.host.inject`, host.inject));
136
+ if (host.optional !== void 0) problems.push(...stringArrayProblems(`${subject}: dsh.workbench.host.optional`, host.optional));
137
+ }
138
+ }
139
+ if (raw.env !== void 0 && !isRecord(raw.env)) problems.push(`${subject}: dsh.workbench.env 要是对象(command / plugins / libs)—— 语义由门禁的 env 判据查,这里只查形状`);
140
+ return problems;
141
+ }
142
+ function readDeclaration(subject, raw) {
143
+ const problems = declarationProblemsOf(subject, raw);
144
+ if (problems.length > 0) return { problems };
145
+ const declaration = raw;
146
+ const card = declaration.card;
147
+ return {
148
+ declaration,
149
+ problems: [],
150
+ manifest: {
151
+ id: text$1(declaration.unit),
152
+ card: card === null ? null : {
153
+ titleKey: text$1(card.titleKey),
154
+ order: card.order
155
+ },
156
+ tools: declaration.tools.map((tool) => ({
157
+ name: text$1(tool.name),
158
+ writes: tool.writes
159
+ })),
160
+ host: {
161
+ inject: [...declaration.host.inject ?? []],
162
+ ...declaration.host.optional === void 0 ? {} : { optional: [...declaration.host.optional] }
163
+ },
164
+ ...declaration.env === void 0 ? {} : { env: declaration.env }
165
+ }
166
+ };
167
+ }
52
168
  function unitFactScope(unit) {
53
169
  return `${unit}*`;
54
170
  }
@@ -151,27 +267,27 @@ var PERMISSION_CODES = /* @__PURE__ */ new Set([
151
267
  "NO_PERMISSION",
152
268
  "EACCES"
153
269
  ]);
154
- function asRecord$7(value) {
270
+ function asRecord$9(value) {
155
271
  return typeof value === "object" && value !== null ? value : void 0;
156
272
  }
157
- function textOf$3(value) {
273
+ function textOf$4(value) {
158
274
  return typeof value === "string" ? value.trim() : "";
159
275
  }
160
276
  function codeOf(page) {
161
- const record = asRecord$7(page);
162
- return record === void 0 ? "" : textOf$3(record["code"]);
277
+ const record = asRecord$9(page);
278
+ return record === void 0 ? "" : textOf$4(record["code"]);
163
279
  }
164
280
  function messageOf$1(page) {
165
- const record = asRecord$7(page);
166
- return record === void 0 ? "" : textOf$3(record["message"]);
281
+ const record = asRecord$9(page);
282
+ return record === void 0 ? "" : textOf$4(record["message"]);
167
283
  }
168
284
  function hintOf$1(page) {
169
- const record = asRecord$7(page);
170
- const hint = record === void 0 ? "" : textOf$3(record["hint"]);
285
+ const record = asRecord$9(page);
286
+ const hint = record === void 0 ? "" : textOf$4(record["hint"]);
171
287
  return hint === "" ? void 0 : hint;
172
288
  }
173
289
  function pageFailed(page) {
174
- const record = asRecord$7(page);
290
+ const record = asRecord$9(page);
175
291
  return record !== void 0 && record["ok"] === false;
176
292
  }
177
293
  function blockerFor(code) {
@@ -395,41 +511,41 @@ var DEFAULT_SETTLE_MS = 2500;
395
511
  var URL_TIMEOUT_MS = 8e3;
396
512
  var DEFAULT_LIMIT = 50;
397
513
  var DEFAULT_TEXT_MAX = 200;
398
- function asRecord$6(value) {
514
+ function asRecord$8(value) {
399
515
  return typeof value === "object" && value !== null ? value : void 0;
400
516
  }
401
- function textOf$2(value) {
517
+ function textOf$3(value) {
402
518
  return typeof value === "string" ? value.trim() : "";
403
519
  }
404
520
  function entriesOf(value) {
405
- const record = asRecord$6(value);
521
+ const record = asRecord$8(value);
406
522
  const raw = record === void 0 ? void 0 : record["entries"];
407
523
  if (!Array.isArray(raw)) return [];
408
524
  const entries = [];
409
525
  for (const one of raw) {
410
- const item = asRecord$6(one);
526
+ const item = asRecord$8(one);
411
527
  if (item === void 0) continue;
412
528
  const nth = typeof item["nth"] === "number" ? item["nth"] : entries.length;
413
529
  entries.push({
414
- text: textOf$2(item["text"]),
415
- tag: textOf$2(item["tag"]),
530
+ text: textOf$3(item["text"]),
531
+ tag: textOf$3(item["tag"]),
416
532
  nth
417
533
  });
418
534
  }
419
535
  return entries;
420
536
  }
421
537
  function reasonOf(stdout, stderr) {
422
- const parsed = asRecord$6(parseJsonLoose(stdout));
423
- const error = parsed === void 0 ? void 0 : asRecord$6(parsed["error"]);
424
- const message = error === void 0 ? "" : textOf$2(error["message"]);
538
+ const parsed = asRecord$8(parseJsonLoose(stdout));
539
+ const error = parsed === void 0 ? void 0 : asRecord$8(parsed["error"]);
540
+ const message = error === void 0 ? "" : textOf$3(error["message"]);
425
541
  if (message !== "") return message;
426
542
  const lines = (stderr.trim() === "" ? stdout : stderr).split("\n").map((one) => one.trim()).filter((one) => one !== "");
427
543
  return (lines[lines.length - 1] ?? "").slice(-200);
428
544
  }
429
545
  function hintOf(stdout) {
430
- const parsed = asRecord$6(parseJsonLoose(stdout));
431
- const error = parsed === void 0 ? void 0 : asRecord$6(parsed["error"]);
432
- const hint = error === void 0 ? "" : textOf$2(error["hint"]);
546
+ const parsed = asRecord$8(parseJsonLoose(stdout));
547
+ const error = parsed === void 0 ? void 0 : asRecord$8(parsed["error"]);
548
+ const hint = error === void 0 ? "" : textOf$3(error["hint"]);
433
549
  return hint === "" ? void 0 : hint;
434
550
  }
435
551
  function sameOrigin(left, right) {
@@ -442,8 +558,8 @@ function sameOrigin(left, right) {
442
558
  function urlOf(stdout) {
443
559
  const bare = (stdout.split("\n").map((one) => one.trim()).filter((one) => one !== "")[0] ?? "").replace(/^url:\s*/i, "").trim().replace(/^["']+|["']+$/g, "");
444
560
  if (bare.startsWith("{")) {
445
- const parsed = asRecord$6(parseJsonLoose(bare));
446
- const url = parsed === void 0 ? "" : textOf$2(parsed["url"]);
561
+ const parsed = asRecord$8(parseJsonLoose(bare));
562
+ const url = parsed === void 0 ? "" : textOf$3(parsed["url"]);
447
563
  if (url !== "") return url;
448
564
  }
449
565
  return bare;
@@ -666,7 +782,9 @@ var PROVISION_ACTIONS = [
666
782
  "update-bundle",
667
783
  "remove-bundle",
668
784
  "cancel-install",
669
- "update-dependency"
785
+ "update-dependency",
786
+ "update-plugin",
787
+ "uninstall-plugin"
670
788
  ];
671
789
  var DEPENDENCY_ACTIONS = ["update-dependency"];
672
790
  function isDependencyAction(action) {
@@ -676,6 +794,7 @@ function provisionTargetOf(action) {
676
794
  if (action === "update-dependency") return "id";
677
795
  if (action === "install-bundle") return "spec";
678
796
  if (action === "remove-bundle") return "name";
797
+ if (action === "update-plugin" || action === "uninstall-plugin") return "name";
679
798
  if (action === "cancel-install") return "requestId";
680
799
  return "unit";
681
800
  }
@@ -686,7 +805,7 @@ var TARGET_CODE = {
686
805
  name: "workbench/no-name",
687
806
  requestId: "workbench/no-request-id"
688
807
  };
689
- function asRecord$5(value) {
808
+ function asRecord$7(value) {
690
809
  return typeof value === "object" && value !== null ? value : {};
691
810
  }
692
811
  function stringsOf$2(value) {
@@ -707,6 +826,10 @@ function requestOf$1(action, value) {
707
826
  action,
708
827
  name: value
709
828
  };
829
+ if (action === "update-plugin" || action === "uninstall-plugin") return {
830
+ action,
831
+ name: value
832
+ };
710
833
  if (action === "cancel-install") return {
711
834
  action,
712
835
  requestId: value
@@ -717,7 +840,7 @@ function requestOf$1(action, value) {
717
840
  };
718
841
  }
719
842
  function parseProvisionBody(body) {
720
- const raw = asRecord$5(body);
843
+ const raw = asRecord$7(body);
721
844
  const action = raw.action;
722
845
  if (typeof action !== "string" || !PROVISION_ACTIONS.includes(action)) return {
723
846
  ok: false,
@@ -1259,6 +1382,9 @@ function createLongTaskHost(options) {
1259
1382
  function isolatedRoot() {
1260
1383
  return `${dshHome()}/workbench/opencli`;
1261
1384
  }
1385
+ function isolatedHome() {
1386
+ return `${isolatedRoot()}/home`;
1387
+ }
1262
1388
  var OPENCLI_NPM_PACKAGE = "@jackwener/opencli";
1263
1389
  function toolchainDir() {
1264
1390
  return `${isolatedRoot()}/toolchain`;
@@ -1304,6 +1430,105 @@ function isolatedInstallCommand() {
1304
1430
  return `npm i --prefix '${toolchainDir()}' ${OPENCLI_NPM_PACKAGE}@${OPENCLI_PIN} --cache '${npmCacheDir()}'`;
1305
1431
  }
1306
1432
  //#endregion
1433
+ //#region ../shared/dist/host/plugin-ledger.js
1434
+ var PLUGIN_LEDGER_FILE = "plugins.json";
1435
+ function ledgerKeyOf(entry) {
1436
+ return `${entry.opencliHome}\u0000${entry.package}`;
1437
+ }
1438
+ function textOf$2(value) {
1439
+ if (typeof value !== "string") return void 0;
1440
+ const trimmed = value.trim();
1441
+ return trimmed === "" ? void 0 : trimmed;
1442
+ }
1443
+ function entryOf(value) {
1444
+ if (typeof value !== "object" || value === null) return void 0;
1445
+ const box = value;
1446
+ const opencliHome = textOf$2(box["opencliHome"]);
1447
+ const pkg = textOf$2(box["package"]);
1448
+ const installedAt = textOf$2(box["installedAt"]);
1449
+ 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"]);
1453
+ return {
1454
+ opencliHome,
1455
+ package: pkg,
1456
+ installedAt,
1457
+ ...name === void 0 ? {} : { name },
1458
+ ...registry === void 0 ? {} : { registry },
1459
+ ...releasedAt === void 0 ? {} : { releasedAt },
1460
+ ...box["local"] === true ? { local: true } : {}
1461
+ };
1462
+ }
1463
+ function parsePluginLedger(raw) {
1464
+ if (typeof raw !== "object" || raw === null) return [];
1465
+ const list = raw["entries"];
1466
+ if (!Array.isArray(list)) return [];
1467
+ const entries = [];
1468
+ for (const item of list) {
1469
+ const entry = entryOf(item);
1470
+ if (entry === void 0) continue;
1471
+ if (entries.some((known) => ledgerKeyOf(known) === ledgerKeyOf(entry))) continue;
1472
+ entries.push(entry);
1473
+ }
1474
+ return entries;
1475
+ }
1476
+ function readPluginLedger(store) {
1477
+ return parsePluginLedger(store.read(PLUGIN_LEDGER_FILE));
1478
+ }
1479
+ function writePluginLedger(store, entries) {
1480
+ if (entries.length === 0) {
1481
+ store.remove(PLUGIN_LEDGER_FILE);
1482
+ return;
1483
+ }
1484
+ store.write(PLUGIN_LEDGER_FILE, { entries });
1485
+ }
1486
+ function pluginEntriesOfHome(entries, opencliHome) {
1487
+ return entries.filter((entry) => entry.opencliHome === opencliHome);
1488
+ }
1489
+ function isHeldEntry(entry) {
1490
+ return entry.releasedAt === void 0;
1491
+ }
1492
+ function heldEntriesOfHome(entries, opencliHome) {
1493
+ return pluginEntriesOfHome(entries, opencliHome).filter(isHeldEntry);
1494
+ }
1495
+ function heldEntryOfPlugin(entries, opencliHome, plugin) {
1496
+ const key = plugin.trim();
1497
+ if (key === "") return void 0;
1498
+ return heldEntriesOfHome(entries, opencliHome).find((entry) => entry.name === key || entry.package === key);
1499
+ }
1500
+ function withReleasedEntry(entries, opencliHome, pkg, releasedAt) {
1501
+ const entry = pluginEntryOf(entries, opencliHome, pkg);
1502
+ if (entry === void 0 || entry.releasedAt !== void 0) return [...entries];
1503
+ return withPluginEntry(entries, {
1504
+ ...entry,
1505
+ releasedAt
1506
+ });
1507
+ }
1508
+ function pluginRetentionOf(options) {
1509
+ if (options.held) return "keep-held";
1510
+ if (!options.trustworthy) return "keep-unsure";
1511
+ return options.claimedBy.length > 0 ? "keep-claimed" : "remove";
1512
+ }
1513
+ function pluginEntryOf(entries, opencliHome, pkg) {
1514
+ const key = ledgerKeyOf({
1515
+ opencliHome,
1516
+ package: pkg
1517
+ });
1518
+ return entries.find((entry) => ledgerKeyOf(entry) === key);
1519
+ }
1520
+ function withPluginEntry(entries, entry) {
1521
+ const key = ledgerKeyOf(entry);
1522
+ return [...entries.filter((item) => ledgerKeyOf(item) !== key), entry];
1523
+ }
1524
+ function withoutPluginEntry(entries, opencliHome, pkg) {
1525
+ const key = ledgerKeyOf({
1526
+ opencliHome,
1527
+ package: pkg
1528
+ });
1529
+ return entries.filter((entry) => ledgerKeyOf(entry) !== key);
1530
+ }
1531
+ //#endregion
1307
1532
  //#region ../shared/dist/host/plugin-source.js
1308
1533
  function mergeLines(commands) {
1309
1534
  return commands.map((item) => item.optional === true ? `(${item.line} || true)` : item.line).join(" && ");
@@ -1591,6 +1816,7 @@ var PROBLEMS = [
1591
1816
  "not-found",
1592
1817
  "not-a-package",
1593
1818
  "not-a-bundle",
1819
+ "not-a-workbench-module",
1594
1820
  "network",
1595
1821
  "unknown"
1596
1822
  ];
@@ -1617,7 +1843,7 @@ var CANCELLATIONS = [
1617
1843
  "too-late",
1618
1844
  "not-running"
1619
1845
  ];
1620
- function asRecord$4(value) {
1846
+ function asRecord$6(value) {
1621
1847
  return typeof value === "object" && value !== null ? value : void 0;
1622
1848
  }
1623
1849
  function asText$4(value) {
@@ -1634,7 +1860,7 @@ function asTextArray(value) {
1634
1860
  return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
1635
1861
  }
1636
1862
  function asRegistries(raw) {
1637
- const record = asRecord$4(raw);
1863
+ const record = asRecord$6(raw);
1638
1864
  if (record === void 0) return null;
1639
1865
  const registry = asRegistry(record.registry);
1640
1866
  if (registry === void 0) return null;
@@ -1645,7 +1871,7 @@ function asRegistries(raw) {
1645
1871
  };
1646
1872
  }
1647
1873
  function asPackageResult(raw) {
1648
- const record = asRecord$4(raw);
1874
+ const record = asRecord$6(raw);
1649
1875
  if (record === void 0) return void 0;
1650
1876
  const kind = pick(FAILURE_KINDS, record.kind);
1651
1877
  return {
@@ -1657,12 +1883,12 @@ function asPackageResult(raw) {
1657
1883
  };
1658
1884
  }
1659
1885
  function asChange(raw) {
1660
- const record = asRecord$4(raw);
1886
+ const record = asRecord$6(raw);
1661
1887
  if (record === void 0) return null;
1662
1888
  const application = pick(APPLICATIONS, record.application);
1663
1889
  const stage = pick(STAGES, record.stage);
1664
1890
  if (application === void 0 || stage === void 0) return null;
1665
- const error = asRecord$4(record.error);
1891
+ const error = asRecord$6(record.error);
1666
1892
  const packageResult = asPackageResult(record.packageResult);
1667
1893
  const pendingBuilds = asTextArray(record.pendingBuilds);
1668
1894
  const failedAt = pick(["registry", "spec-host"], record.failedAt);
@@ -1683,7 +1909,7 @@ function asChange(raw) {
1683
1909
  };
1684
1910
  }
1685
1911
  function asInspection(raw) {
1686
- const record = asRecord$4(raw);
1912
+ const record = asRecord$6(raw);
1687
1913
  if (record === void 0) return null;
1688
1914
  const status = asText$4(record.status);
1689
1915
  if (status === "refused") {
@@ -1741,14 +1967,14 @@ function readBundleManager(ctx) {
1741
1967
  },
1742
1968
  async cancel(requestId) {
1743
1969
  if (typeof face.cancelInstall !== "function") return null;
1744
- return pick(CANCELLATIONS, (asRecord$4(await face.cancelInstall(requestId)) ?? {}).status) ?? null;
1970
+ return pick(CANCELLATIONS, (asRecord$6(await face.cancelInstall(requestId)) ?? {}).status) ?? null;
1745
1971
  }
1746
1972
  };
1747
1973
  }
1748
1974
  //#endregion
1749
1975
  //#region src/shared/api/host/installed-module.ts
1750
1976
  var PACKAGE_NAME$1 = /^(?:@[a-z0-9][a-z0-9._~-]*\/)?[a-z0-9][a-z0-9._~-]*$/;
1751
- function asRecord$3(value) {
1977
+ function asRecord$5(value) {
1752
1978
  return typeof value === "object" && value !== null ? value : void 0;
1753
1979
  }
1754
1980
  function asText$3(value) {
@@ -1765,18 +1991,19 @@ function profileDirOf(ctx) {
1765
1991
  function stringsOf$1(value) {
1766
1992
  return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
1767
1993
  }
1768
- function profileModules(profileDir) {
1769
- if (profileDir === null) return [];
1770
- let manifest;
1994
+ function profileManifestOf(profileDir) {
1995
+ if (profileDir === null) return void 0;
1771
1996
  try {
1772
- manifest = JSON.parse(fs.readFileSync(path.join(profileDir, "package.json"), "utf8"));
1997
+ return asRecord$5(JSON.parse(fs.readFileSync(path.join(profileDir, "package.json"), "utf8")));
1773
1998
  } catch {
1774
- return [];
1999
+ return;
1775
2000
  }
1776
- const record = asRecord$3(manifest);
2001
+ }
2002
+ function profileModules(profileDir) {
2003
+ const record = profileManifestOf(profileDir);
1777
2004
  if (record === void 0) return [];
1778
- const bundles = stringsOf$1(asRecord$3(asRecord$3(record.dsh)?.profile)?.bundles);
1779
- const dependencies = asRecord$3(record.dependencies) ?? {};
2005
+ const bundles = stringsOf$1(asRecord$5(asRecord$5(record.dsh)?.profile)?.bundles);
2006
+ const dependencies = asRecord$5(record.dependencies) ?? {};
1780
2007
  const names = bundles.length > 0 ? bundles : Object.keys(dependencies);
1781
2008
  const found = [];
1782
2009
  for (const pkg of names) {
@@ -1810,6 +2037,27 @@ function registryFromLockfile(profileDir, pkg) {
1810
2037
  if (cut <= 8) return void 0;
1811
2038
  return tarball.slice(0, cut);
1812
2039
  }
2040
+ function moduleDeclarationOf(profileDir, pkg) {
2041
+ if (profileDir === null || !isPackageName(pkg)) return void 0;
2042
+ try {
2043
+ const text = fs.readFileSync(path.join(profileDir, "node_modules", pkg, "package.json"), "utf8");
2044
+ return asRecord$5(asRecord$5(asRecord$5(JSON.parse(text))?.dsh)?.workbench);
2045
+ } catch {
2046
+ return;
2047
+ }
2048
+ }
2049
+ function declarationGaps(profileDir) {
2050
+ if (profileManifestOf(profileDir) === void 0) return void 0;
2051
+ const gaps = [];
2052
+ for (const module of profileModules(profileDir)) {
2053
+ const problems = declarationProblemsOf(module.pkg, moduleDeclarationOf(profileDir, module.pkg));
2054
+ if (problems.length > 0) gaps.push({
2055
+ pkg: module.pkg,
2056
+ problems
2057
+ });
2058
+ }
2059
+ return gaps;
2060
+ }
1813
2061
  function readInstalledModule(profileDir, pkg) {
1814
2062
  if (profileDir === null || !isPackageName(pkg)) return {
1815
2063
  kind: "unknown",
@@ -1824,18 +2072,18 @@ function readInstalledModule(profileDir, pkg) {
1824
2072
  pkg
1825
2073
  };
1826
2074
  }
1827
- const record = asRecord$3(manifest);
2075
+ const record = asRecord$5(manifest);
1828
2076
  if (record === void 0) return {
1829
2077
  kind: "unknown",
1830
2078
  pkg
1831
2079
  };
1832
- const declared = asRecord$3(asRecord$3(record.dsh)?.workbench);
2080
+ const declared = asRecord$5(asRecord$5(record.dsh)?.workbench);
1833
2081
  const unit = declared === void 0 ? "" : asText$3(declared.unit);
1834
2082
  if (unit === "") return {
1835
2083
  kind: "not-module",
1836
2084
  pkg
1837
2085
  };
1838
- const declaredRegistry = asText$3(asRecord$3(record.publishConfig)?.registry);
2086
+ const declaredRegistry = asText$3(asRecord$5(record.publishConfig)?.registry);
1839
2087
  const registry = declaredRegistry === "" ? registryFromLockfile(profileDir, pkg) : declaredRegistry;
1840
2088
  return {
1841
2089
  kind: "module",
@@ -1971,7 +2219,7 @@ function workshopPathOf(ctx) {
1971
2219
  }
1972
2220
  //#endregion
1973
2221
  //#region src/shared/api/host/diy-workshop.ts
1974
- function asRecord$2(value) {
2222
+ function asRecord$4(value) {
1975
2223
  return typeof value === "object" && value !== null ? value : void 0;
1976
2224
  }
1977
2225
  function asText$2(value) {
@@ -1997,8 +2245,8 @@ function workshopModules(packagesDir, installed = []) {
1997
2245
  } catch {
1998
2246
  return [];
1999
2247
  }
2000
- const record = asRecord$2(manifest);
2001
- const declared = record === void 0 ? void 0 : asRecord$2(asRecord$2(record.dsh)?.workbench);
2248
+ const record = asRecord$4(manifest);
2249
+ const declared = record === void 0 ? void 0 : asRecord$4(asRecord$4(record.dsh)?.workbench);
2002
2250
  const unit = declared === void 0 ? "" : asText$2(declared.unit);
2003
2251
  if (unit === "") return [];
2004
2252
  return [{
@@ -2021,7 +2269,6 @@ var MODULES_TOOL = "workbench_modules";
2021
2269
  var DIY_TOOL = "workbench_diy";
2022
2270
  var panelsUnit = {
2023
2271
  id: "panels",
2024
- kind: "ui",
2025
2272
  card: null,
2026
2273
  tools: [
2027
2274
  {
@@ -2056,9 +2303,10 @@ function actionOf$1(call) {
2056
2303
  const raw = call.arguments;
2057
2304
  if (typeof raw !== "object" || raw === null) return {};
2058
2305
  const value = raw;
2306
+ const target = typeof value.plugin === "string" ? value.plugin : typeof value.name === "string" ? value.name : typeof value.id === "string" ? value.id : typeof value.unit === "string" ? value.unit : void 0;
2059
2307
  return {
2060
2308
  ...typeof value.action === "string" ? { action: value.action } : {},
2061
- ...typeof value.id === "string" ? { id: value.id } : typeof value.unit === "string" ? { id: value.unit } : {}
2309
+ ...target === void 0 ? {} : { id: target }
2062
2310
  };
2063
2311
  }
2064
2312
  function whatOf(id) {
@@ -2079,6 +2327,17 @@ var panelsApproval = (call) => {
2079
2327
  const what = whatOf(id);
2080
2328
  if (call.name === "workbench_modules") {
2081
2329
  if (readAction) return READ;
2330
+ if (action === "update-plugin" || action === "uninstall-plugin") {
2331
+ const verb = action === "update-plugin" ? "更新" : "卸载";
2332
+ const verbEn = action === "update-plugin" ? "update" : "remove";
2333
+ return askOf(`workbench_modules ${action} ${id ?? ""} changes an opencli plugin on this machine`, {
2334
+ en: `it ${verbEn}s that opencli plugin on this machine. Allow?`,
2335
+ zh: `会${verb}这个 opencli 插件。允许吗?`
2336
+ }, {
2337
+ en: `The agent wants to ${verbEn} the opencli plugin ${what} (it changes what is installed on this machine). Allow?`,
2338
+ zh: `agent 想${verb} opencli 插件 ${what}(会改这台机器上装的东西)。允许吗?`
2339
+ });
2340
+ }
2082
2341
  if (action === "update-dependency") return askOf(`workbench_modules update-dependency ${id ?? ""} upgrades a shared dependency on this machine`, {
2083
2342
  en: "it upgrades that shared dependency on this machine. Allow?",
2084
2343
  zh: `会升级共用依赖 ${what}。允许吗?`
@@ -2145,13 +2404,102 @@ var panelsApproval = (call) => {
2145
2404
  });
2146
2405
  };
2147
2406
  //#endregion
2407
+ //#region src/modules/panels/model/config.ts
2408
+ var EMPTY_BOARD = {
2409
+ shown: [],
2410
+ hidden: []
2411
+ };
2412
+ var BOARD_TARGETS = ["workspace", "machine"];
2413
+ var DEFAULT_BOARD_TARGET = "workspace";
2414
+ function resolveScope(query) {
2415
+ const { id, boards } = query;
2416
+ const board = boards.workspace ?? boards.machine;
2417
+ if (board === null) return false;
2418
+ if (board.hidden.includes(id)) return false;
2419
+ return board.shown.includes(id);
2420
+ }
2421
+ function strings(raw) {
2422
+ return Array.isArray(raw) ? raw.filter((item) => typeof item === "string") : [];
2423
+ }
2424
+ function parseBoard(raw) {
2425
+ if (typeof raw !== "object" || raw === null) return EMPTY_BOARD;
2426
+ const value = raw;
2427
+ return {
2428
+ shown: strings(value.shown),
2429
+ hidden: strings(value.hidden)
2430
+ };
2431
+ }
2432
+ function parseReleased(raw) {
2433
+ if (typeof raw !== "object" || raw === null) return [];
2434
+ return strings(raw.ids);
2435
+ }
2436
+ function resolvePanels(manifests, boards) {
2437
+ const ordered = boards.workspace?.shown ?? boards.machine?.shown ?? [];
2438
+ const rank = new Map(ordered.map((id, index) => [id, index]));
2439
+ return manifests.filter((manifest) => manifest.card !== null).map((manifest) => {
2440
+ const on = resolveScope({
2441
+ id: manifest.id,
2442
+ boards
2443
+ });
2444
+ const order = rank.get(manifest.id) ?? ordered.length + (manifest.card?.order ?? 0);
2445
+ return {
2446
+ id: manifest.id,
2447
+ titleKey: manifest.card?.titleKey ?? manifest.id,
2448
+ order,
2449
+ shown: on,
2450
+ source: on && (boards.workspace?.shown.includes(manifest.id) ?? false) ? "user" : "default",
2451
+ hasCard: true
2452
+ };
2453
+ }).sort((a, b) => a.order - b.order || a.id.localeCompare(b.id));
2454
+ }
2455
+ function withoutReleased(released, id) {
2456
+ return released.filter((item) => item !== id);
2457
+ }
2458
+ function withReleased(released, id) {
2459
+ return [.../* @__PURE__ */ new Set([...released, id])];
2460
+ }
2461
+ function added(list, id) {
2462
+ return list.includes(id) ? [...list] : [...list, id];
2463
+ }
2464
+ function dropped(list, id) {
2465
+ return list.filter((item) => item !== id);
2466
+ }
2467
+ function withShown(board, id) {
2468
+ return {
2469
+ shown: added(board.shown, id),
2470
+ hidden: dropped(board.hidden, id)
2471
+ };
2472
+ }
2473
+ function withHidden(board, id) {
2474
+ return {
2475
+ shown: dropped(board.shown, id),
2476
+ hidden: added(board.hidden, id)
2477
+ };
2478
+ }
2479
+ function withoutPanel(board, id) {
2480
+ return {
2481
+ shown: dropped(board.shown, id),
2482
+ hidden: dropped(board.hidden, id)
2483
+ };
2484
+ }
2485
+ function withMoved(board, id, index) {
2486
+ const current = [...board.shown];
2487
+ const from = current.indexOf(id);
2488
+ const max = from === -1 ? current.length : current.length - 1;
2489
+ const to = Math.max(0, Math.min(max, Math.trunc(index)));
2490
+ if (from === -1) current.splice(to, 0, id);
2491
+ else if (from !== to) current.splice(to, 0, ...current.splice(from, 1));
2492
+ return {
2493
+ shown: current,
2494
+ hidden: dropped(board.hidden, id)
2495
+ };
2496
+ }
2497
+ //#endregion
2148
2498
  //#region src/shared/api/host/diy-facts.ts
2149
2499
  function asExported(dir) {
2150
2500
  try {
2151
2501
  const manifest = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
2152
- const built = (typeof manifest.exports === "object" && manifest.exports !== null ? Object.values(manifest.exports) : []).filter((one) => typeof one === "string");
2153
- if (built.length > 0) return built;
2154
- return Object.values(manifest.dsh?.workbench?.faces ?? {}).filter((one) => typeof one === "string");
2502
+ return (typeof manifest.exports === "object" && manifest.exports !== null ? Object.values(manifest.exports) : []).filter((one) => typeof one === "string");
2155
2503
  } catch {
2156
2504
  return [];
2157
2505
  }
@@ -2456,7 +2804,7 @@ var OUTPUT_SCHEMA$2 = {
2456
2804
  },
2457
2805
  additionalProperties: true
2458
2806
  };
2459
- function asRecord$1(value) {
2807
+ function asRecord$3(value) {
2460
2808
  return typeof value === "object" && value !== null ? value : void 0;
2461
2809
  }
2462
2810
  function asNumber(value) {
@@ -2472,7 +2820,7 @@ function asText$1(value) {
2472
2820
  function plainJson(value) {
2473
2821
  return JSON.parse(JSON.stringify(value));
2474
2822
  }
2475
- function verdictOf(change) {
2823
+ function verdictOf$1(change) {
2476
2824
  if (change.application === "applied") return "装好了,已生效";
2477
2825
  if (change.application === "restart-required") return "装好了,要重启一次才生效(装新模块、同名重装、替换已有包都走这一支)";
2478
2826
  if (change.application === "overridden") return "被更高层的声明盖住了,这次没生效";
@@ -2616,7 +2964,7 @@ function registerDiyTools(ctx, market, panels) {
2616
2964
  }
2617
2965
  },
2618
2966
  async execute(raw, exec) {
2619
- const args = asRecord$1(raw) ?? {};
2967
+ const args = asRecord$3(raw) ?? {};
2620
2968
  const action = asText$1(args.action) || "list";
2621
2969
  if (!ACTIONS$1.includes(action)) throw new Error(`不认识的 action:${action}(可用:${ACTIONS$1.join(" / ")})`);
2622
2970
  const packagesDir = workshopPackagesDir(ctx);
@@ -2741,7 +3089,7 @@ function registerDiyTools(ctx, market, panels) {
2741
3089
  });
2742
3090
  }
2743
3091
  const outcome = await market.installBundle(found.dir);
2744
- const verdict = verdictOf(outcome.change);
3092
+ const verdict = verdictOf$1(outcome.change);
2745
3093
  const restartRequired = outcome.change.application === "restart-required";
2746
3094
  const panel = outcome.change.application === "applied" ? showOrReason(panels, unit, workspace) : {
2747
3095
  ok: false,
@@ -2986,14 +3334,570 @@ function sourceProblemText(problem, env) {
2986
3334
  return "这份安装没声明安装方式 —— 没声明就不猜地址,只能自己动手。";
2987
3335
  }
2988
3336
  //#endregion
2989
- //#region src/modules/panels/model/market.ts
3337
+ //#region src/shared/api/host/registry-declaration.ts
3338
+ var cache$1 = /* @__PURE__ */ new Map();
3339
+ function asRecord$2(value) {
3340
+ return typeof value === "object" && value !== null ? value : void 0;
3341
+ }
3342
+ function localDirOf(spec) {
3343
+ const dir = spec.trim().replace(/^(?:file|link):/, "");
3344
+ return path.isAbsolute(dir) ? dir : void 0;
3345
+ }
3346
+ function packageManifestOf(dir) {
3347
+ try {
3348
+ return asRecord$2(JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf8")));
3349
+ } catch {
3350
+ return;
3351
+ }
3352
+ }
3353
+ function dshRecordOf(dir) {
3354
+ return packageManifestOf(dir)?.dsh;
3355
+ }
3356
+ function packageNameOf(dir) {
3357
+ const name = packageManifestOf(dir)?.name;
3358
+ return typeof name === "string" && name.trim() !== "" ? name.trim() : void 0;
3359
+ }
3360
+ function pathDeclarationOf(spec) {
3361
+ const dir = localDirOf(spec);
3362
+ return dir === void 0 ? void 0 : asRecord$2(dshRecordOf(dir))?.workbench;
3363
+ }
3364
+ async function registryDshOf(options) {
3365
+ const { shell, name, registry, force = false } = options;
3366
+ const key = `${registry ?? ""}|${name}`;
3367
+ const hit = cache$1.get(key);
3368
+ if (!force && hit !== void 0 && Date.now() - hit.at < 6e5) return {
3369
+ readable: true,
3370
+ raw: hit.raw
3371
+ };
3372
+ const at = registry === void 0 ? "" : ` --registry ${quoteShellArg(registry)}`;
3373
+ const result = await runShell(shell, `npm view ${quoteShellArg(name)} dsh --json${at} --cache ${quoteShellArg(npmCacheDir())}`, { timeoutMs: VERSION_CHECK_TIMEOUT_MS }).catch(() => void 0);
3374
+ const parsed = result?.exitCode === 0 ? parseJsonLoose(result.stdout) : void 0;
3375
+ if (parsed === void 0) return {
3376
+ readable: false,
3377
+ raw: void 0
3378
+ };
3379
+ cache$1.set(key, {
3380
+ at: Date.now(),
3381
+ raw: parsed
3382
+ });
3383
+ return {
3384
+ readable: true,
3385
+ raw: parsed
3386
+ };
3387
+ }
3388
+ async function registryDeclarationOf(options) {
3389
+ const found = await registryDshOf(options);
3390
+ return found.readable ? asRecord$2(found.raw)?.workbench : void 0;
3391
+ }
3392
+ function declarationRefusalOf(name, raw) {
3393
+ if (raw === void 0) return [];
3394
+ return declarationProblemsOf(name, raw);
3395
+ }
3396
+ //#endregion
3397
+ //#region src/shared/api/host/package-kind.ts
3398
+ var OPENCLI_MANIFEST_FILE = "opencli-plugin.json";
3399
+ var VERSION = /^[a-zA-Z0-9][a-zA-Z0-9._+-]*$/;
3400
+ function splitNpmSpec(spec) {
3401
+ const value = spec.trim();
3402
+ const at = value.lastIndexOf("@");
3403
+ const name = at > 0 ? value.slice(0, at) : value;
3404
+ const version = at > 0 ? value.slice(at + 1) : "";
3405
+ if (!isPackageName(name)) return void 0;
3406
+ if (version !== "" && !VERSION.test(version)) return void 0;
3407
+ return {
3408
+ name,
3409
+ ...version === "" ? {} : { version }
3410
+ };
3411
+ }
3412
+ function asRecord$1(value) {
3413
+ return typeof value === "object" && value !== null ? value : void 0;
3414
+ }
3415
+ function isCommandFile(name) {
3416
+ if (name.endsWith(".d.ts") || name.endsWith(".test.ts")) return false;
3417
+ if (name.endsWith(".d.js")) return false;
3418
+ return name.endsWith(".ts") || name.endsWith(".js");
3419
+ }
3420
+ function pluginManifestOf(dir) {
3421
+ try {
3422
+ const parsed = JSON.parse(fs.readFileSync(path.join(dir, OPENCLI_MANIFEST_FILE), "utf8"));
3423
+ if (Array.isArray(parsed)) return void 0;
3424
+ return asRecord$1(parsed);
3425
+ } catch {
3426
+ return;
3427
+ }
3428
+ }
3429
+ function hasCommandFile(dir) {
3430
+ try {
3431
+ return fs.readdirSync(dir).some(isCommandFile);
3432
+ } catch {
3433
+ return false;
3434
+ }
3435
+ }
3436
+ function pluginAt(dir) {
3437
+ const manifest = pluginManifestOf(dir);
3438
+ if (manifest === void 0 || !hasCommandFile(dir)) return void 0;
3439
+ const name = manifest.name;
3440
+ return typeof name === "string" && name.trim() !== "" ? { name: name.trim() } : {};
3441
+ }
3442
+ function dshMarkersOf(raw) {
3443
+ const record = asRecord$1(raw);
3444
+ return record !== void 0 && ("bundle" in record || "workbench" in record);
3445
+ }
3446
+ function verdictOf(dir) {
3447
+ const plugin = pluginAt(dir);
3448
+ if (packageManifestOf(dir) === void 0 && plugin === void 0) return void 0;
3449
+ const dsh = dshMarkersOf(dshRecordOf(dir));
3450
+ const kind = plugin !== void 0 && dsh ? "both" : plugin !== void 0 ? "opencli-plugin" : dsh ? "dsh-bundle" : "neither";
3451
+ const pkg = packageNameOf(dir);
3452
+ return {
3453
+ kind,
3454
+ ...pkg === void 0 ? {} : { pkg },
3455
+ ...plugin?.name === void 0 ? {} : { pluginName: plugin.name }
3456
+ };
3457
+ }
3458
+ function refusalReason() {
3459
+ return "这个包既不是工作台的模块,也不是 opencli 的插件,装了也用不上";
3460
+ }
3461
+ //#endregion
3462
+ //#region src/shared/api/host/package-content.ts
3463
+ function packedFileFrom(stdout, root) {
3464
+ const parsed = parseJsonLoose(stdout);
3465
+ if (Array.isArray(parsed)) {
3466
+ const first = parsed[0];
3467
+ const filename = typeof first === "object" && first !== null ? first.filename : void 0;
3468
+ if (typeof filename === "string" && filename.trim() !== "") {
3469
+ const at = path.join(root, path.basename(filename.trim()));
3470
+ if (fs.existsSync(at)) return at;
3471
+ }
3472
+ }
3473
+ try {
3474
+ const found = fs.readdirSync(root).filter((name) => name.endsWith(".tgz"));
3475
+ return found.length === 1 ? path.join(root, found[0] ?? "") : void 0;
3476
+ } catch {
3477
+ return;
3478
+ }
3479
+ }
3480
+ async function withPackageDir(options, visit) {
3481
+ const local = localDirOf(options.spec);
3482
+ if (local !== void 0) return visit(local);
3483
+ if (splitNpmSpec(options.spec) === void 0) return void 0;
3484
+ const shell = options.shell;
3485
+ if (shell === void 0) return void 0;
3486
+ let root;
3487
+ try {
3488
+ root = fs.mkdtempSync(path.join(os.tmpdir(), "wb-package-"));
3489
+ } catch {
3490
+ return;
3491
+ }
3492
+ try {
3493
+ const at = options.registry === void 0 ? "" : ` --registry ${quoteShellArg(options.registry)}`;
3494
+ const packed = await runShell(shell, `npm pack ${quoteShellArg(options.spec)} --ignore-scripts --json --pack-destination ${quoteShellArg(root)} --cache ${quoteShellArg(npmCacheDir())}${at}`, { timeoutMs: INSTALL_TIMEOUT_MS }).catch(() => void 0);
3495
+ if (packed === void 0 || packed.exitCode !== 0) return void 0;
3496
+ const file = packedFileFrom(packed.stdout, root);
3497
+ if (file === void 0) return void 0;
3498
+ const unpacked = await runShell(shell, `tar -xzf ${quoteShellArg(file)} -C ${quoteShellArg(root)}`, { timeoutMs: INSTALL_TIMEOUT_MS }).catch(() => void 0);
3499
+ if (unpacked === void 0 || unpacked.exitCode !== 0) return void 0;
3500
+ const dir = path.join(root, "package");
3501
+ if (!fs.existsSync(dir)) return void 0;
3502
+ return visit(dir);
3503
+ } finally {
3504
+ fs.rmSync(root, {
3505
+ recursive: true,
3506
+ force: true
3507
+ });
3508
+ }
3509
+ }
3510
+ //#endregion
3511
+ //#region src/shared/api/host/package-route.ts
3512
+ var LOCAL_PLUGIN_ENV = "DSH_WB_OPENCLI_PLUGIN_PATH";
3513
+ var PLUGIN_REGISTRY_ENV = "DSH_WB_OPENCLI_PLUGIN_REGISTRY";
3514
+ var cache = /* @__PURE__ */ new Map();
3515
+ function routeOfVerdict(verdict, fallback) {
3516
+ const pkg = verdict.pkg ?? fallback;
3517
+ if (verdict.kind === "neither") return {
3518
+ kind: "neither",
3519
+ ...pkg === void 0 ? {} : { pkg }
3520
+ };
3521
+ if (verdict.kind === "dsh-bundle" || verdict.kind === "both") return { kind: "dsh-bundle" };
3522
+ return {
3523
+ kind: "opencli-plugin",
3524
+ ...pkg === void 0 ? {} : { pkg },
3525
+ ...verdict.pluginName === void 0 ? {} : { pluginName: verdict.pluginName }
3526
+ };
3527
+ }
3528
+ async function packageRouteOf(options) {
3529
+ const local = localDirOf(options.spec);
3530
+ if (local !== void 0) {
3531
+ const verdict = verdictOf(local);
3532
+ return verdict === void 0 ? void 0 : routeOfVerdict(verdict, splitNpmSpec(options.spec)?.name);
3533
+ }
3534
+ const ref = splitNpmSpec(options.spec);
3535
+ if (ref === void 0) return void 0;
3536
+ const shell = options.shell;
3537
+ const registry = options.registry;
3538
+ if (shell === void 0 || registry === void 0) return void 0;
3539
+ const key = `${registry}|${options.spec}`;
3540
+ const hit = cache.get(key);
3541
+ if (options.force !== true && hit !== void 0 && Date.now() - hit.at < 6e5) return hit.route ?? void 0;
3542
+ const found = await registryDshOf({
3543
+ shell,
3544
+ name: options.spec,
3545
+ registry
3546
+ });
3547
+ let route;
3548
+ if (found.readable && dshMarkersOf(found.raw)) route = { kind: "dsh-bundle" };
3549
+ else {
3550
+ const verdict = await withPackageDir({
3551
+ shell,
3552
+ spec: options.spec,
3553
+ registry
3554
+ }, (dir) => verdictOf(dir));
3555
+ route = verdict === void 0 ? void 0 : routeOfVerdict(verdict, ref.name);
3556
+ }
3557
+ cache.set(key, {
3558
+ at: Date.now(),
3559
+ route: route ?? null
3560
+ });
3561
+ return route;
3562
+ }
3563
+ function pluginInstallPlan(options) {
3564
+ const local = localDirOf(options.spec);
3565
+ if (local !== void 0) return resolvePluginSource({
3566
+ kind: "local",
3567
+ env: LOCAL_PLUGIN_ENV
3568
+ }, { [LOCAL_PLUGIN_ENV]: local }, { command: options.command });
3569
+ const ref = splitNpmSpec(options.spec);
3570
+ if (ref === void 0) return {
3571
+ ok: false,
3572
+ problem: "malformed"
3573
+ };
3574
+ return resolvePluginSource({
3575
+ kind: "npm",
3576
+ package: ref.name,
3577
+ registryEnv: PLUGIN_REGISTRY_ENV,
3578
+ ...options.registry === void 0 ? {} : { registryDefault: options.registry }
3579
+ }, { [PLUGIN_REGISTRY_ENV]: options.registry }, {
3580
+ command: options.command,
3581
+ ...options.pluginName === void 0 ? {} : { plugin: options.pluginName },
3582
+ ...ref.version === void 0 ? {} : { version: ref.version },
3583
+ mode: "install"
3584
+ });
3585
+ }
3586
+ //#endregion
3587
+ //#region src/modules/panels/model/plugin-name.ts
3588
+ function pluginDirOf(entry) {
3589
+ return entry.local === true ? entry.package : `${vendorPluginsDir()}/${entry.package}`;
3590
+ }
3591
+ function pluginOnDisk(entry) {
3592
+ return pluginAt(pluginDirOf(entry)) !== void 0;
3593
+ }
3594
+ function pluginNameOf(entry) {
3595
+ if (entry.name !== void 0) return entry.name;
3596
+ const name = pluginAt(pluginDirOf(entry))?.name?.trim() ?? "";
3597
+ return name === "" ? void 0 : name;
3598
+ }
3599
+ //#endregion
3600
+ //#region src/modules/panels/model/market-route.ts
2990
3601
  function bundleNameOf(change, spec) {
2991
3602
  if (change.bundle !== void 0 && change.bundle !== "") return change.bundle;
2992
3603
  const at = spec.lastIndexOf("@");
2993
3604
  return at > 0 ? spec.slice(0, at) : spec;
2994
3605
  }
3606
+ function installedPluginName(spec, pkg) {
3607
+ const local = localDirOf(spec);
3608
+ return pluginNameOf(local === void 0 ? { package: pkg } : {
3609
+ package: local,
3610
+ local: true
3611
+ });
3612
+ }
3613
+ function outcomeOf(deps, pkg, change, plugin) {
3614
+ return {
3615
+ change,
3616
+ module: deps.installed?.(pkg) ?? {
3617
+ kind: "unknown",
3618
+ pkg
3619
+ },
3620
+ supply: { state: "no-needs" },
3621
+ ...plugin === void 0 ? {} : { plugin }
3622
+ };
3623
+ }
3624
+ async function installDshBundle(deps, spec, options) {
3625
+ const change = await deps.manager.install({
3626
+ spec,
3627
+ ...options?.registry === void 0 ? {} : { registry: options.registry },
3628
+ ...options?.requestId === void 0 ? {} : { requestId: options.requestId },
3629
+ ...options?.approvedBuilds === void 0 ? {} : { approvedBuilds: options.approvedBuilds }
3630
+ });
3631
+ if (change === null) throw new Error("插件管理器回的安装结果看不懂。");
3632
+ const pkg = bundleNameOf(change, spec);
3633
+ const module = deps.installed?.(pkg) ?? {
3634
+ kind: "unknown",
3635
+ pkg
3636
+ };
3637
+ await deps.rememberSource(deps.manager, pkg, options?.registry);
3638
+ return {
3639
+ change,
3640
+ module,
3641
+ supply: await deps.supplyAfter(module, change)
3642
+ };
3643
+ }
3644
+ async function installRouted(deps) {
3645
+ const { spec, options, registry, shell } = deps;
3646
+ const route = await packageRouteOf({
3647
+ spec,
3648
+ ...shell === void 0 ? {} : { shell },
3649
+ ...registry === void 0 ? {} : { registry }
3650
+ });
3651
+ if (route === void 0 || route.kind === "dsh-bundle") return installDshBundle(deps, spec, options);
3652
+ const pkg = route.pkg ?? splitNpmSpec(spec)?.name ?? spec.trim();
3653
+ if (route.kind === "neither") {
3654
+ deps.logger?.warn?.(`[dsh-workbench] 不装 ${pkg}:${refusalReason()}`);
3655
+ return outcomeOf(deps, pkg, {
3656
+ application: "failed",
3657
+ stage: "install",
3658
+ changed: false,
3659
+ target: pkg,
3660
+ errorCode: "not-a-module-or-plugin",
3661
+ errorDetail: refusalReason()
3662
+ });
3663
+ }
3664
+ const plan = pluginInstallPlan({
3665
+ spec,
3666
+ command: deps.command,
3667
+ ...registry === void 0 ? {} : { registry },
3668
+ ...route.pluginName === void 0 ? {} : { pluginName: route.pluginName }
3669
+ });
3670
+ if (!plan.ok) throw new Error(sourceProblemText(plan.problem, plan.env));
3671
+ deps.ledger?.intend({
3672
+ package: pkg,
3673
+ ...registry === void 0 ? {} : { registry },
3674
+ ...localDirOf(spec) === void 0 ? {} : { local: true }
3675
+ });
3676
+ try {
3677
+ await deps.runAll(plan.commands);
3678
+ } catch (error) {
3679
+ deps.ledger?.drop(pkg);
3680
+ throw error;
3681
+ }
3682
+ deps.ledger?.complete(pkg, installedPluginName(spec, pkg));
3683
+ deps.logger?.info?.(`[dsh-workbench] 把 ${pkg} 装进了 opencli 的插件区`);
3684
+ return outcomeOf(deps, pkg, {
3685
+ application: "applied",
3686
+ stage: "install",
3687
+ changed: true,
3688
+ target: pkg
3689
+ }, {
3690
+ kind: "opencli-plugin",
3691
+ pkg,
3692
+ ...route.pluginName === void 0 ? {} : { name: route.pluginName }
3693
+ });
3694
+ }
3695
+ //#endregion
3696
+ //#region src/modules/panels/model/plugin-actions.ts
3697
+ function pluginKeptOf(retention, detail) {
3698
+ if (retention === "keep-held") return { kind: "held" };
3699
+ if (retention === "keep-unsure") return {
3700
+ kind: "unsure",
3701
+ unreadable: [...detail.unreadable]
3702
+ };
3703
+ return {
3704
+ kind: "claimed",
3705
+ by: [...detail.by]
3706
+ };
3707
+ }
3708
+ function pluginKeptText(kept) {
3709
+ if (kept.kind === "held") return "文件留着:你自己装的那一份,模块放手不动它。";
3710
+ if (kept.kind === "unnamed") return "文件先留着:看不出这是哪个插件,说不准谁在用它。";
3711
+ if (kept.kind === "unsure") return kept.unreadable.length === 0 ? "文件先留着:说不准还有谁在用它。" : `文件先留着:${kept.unreadable.join("、")} 读不出来用了什么,说不准它们在用它。`;
3712
+ return `文件留着:${kept.by.join("、")} 还要用它。`;
3713
+ }
3714
+ function pluginRemovedText(pkg) {
3715
+ return `「${pkg}」卸载干净了:没人再用它,文件也删了。`;
3716
+ }
3717
+ function pluginSweptText(pkg) {
3718
+ return `清掉了 ${pkg}:先前要它的都放手了,也没别人在用,文件跟着删了。`;
3719
+ }
3720
+ function createPluginActions(options) {
3721
+ const { ledger, shell, command, runAll, logger } = options;
3722
+ function requireLedger() {
3723
+ if (ledger === void 0) throw new Error("这个市场没有台账(插件记录),用户装的插件动不了。");
3724
+ return ledger;
3725
+ }
3726
+ function requireHeld(pkg) {
3727
+ const held = requireLedger();
3728
+ const direct = held.read(pkg);
3729
+ if (direct !== void 0) return {
3730
+ entry: direct,
3731
+ key: direct.package
3732
+ };
3733
+ const named = held.heldAsPlugin(pkg);
3734
+ if (named !== void 0) return {
3735
+ entry: named,
3736
+ key: named.package
3737
+ };
3738
+ const known = held.list().map((entry) => entry.name ?? entry.package);
3739
+ throw new Error(`台账里没有「${pkg}」这一笔(包名与插件名都认)—— 你装的插件有:${known.join("、") || "(空)"}`);
3740
+ }
3741
+ function referencesOf() {
3742
+ return options.references?.() ?? {
3743
+ trustworthy: false,
3744
+ unreadable: []
3745
+ };
3746
+ }
3747
+ function decide(entry, held) {
3748
+ const plugin = pluginNameOf(entry);
3749
+ if (plugin === void 0) return {
3750
+ act: "keep",
3751
+ kept: { kind: "unnamed" }
3752
+ };
3753
+ const refs = referencesOf();
3754
+ const by = options.claimedBy?.(plugin) ?? [];
3755
+ const retention = pluginRetentionOf({
3756
+ held,
3757
+ claimedBy: by,
3758
+ trustworthy: refs.trustworthy
3759
+ });
3760
+ if (retention === "remove") return {
3761
+ act: "remove",
3762
+ plugin
3763
+ };
3764
+ return {
3765
+ act: "keep",
3766
+ kept: pluginKeptOf(retention, {
3767
+ by,
3768
+ unreadable: refs.unreadable
3769
+ }),
3770
+ plugin
3771
+ };
3772
+ }
3773
+ async function removeFiles(plugin) {
3774
+ if (shell === void 0) throw new Error("没有执行缝(shell 服务不可用),删不掉插件文件。");
3775
+ await runAll([{ line: `${quoteShellArg(command)} plugin uninstall ${quoteShellArg(plugin)}` }]);
3776
+ }
3777
+ return {
3778
+ async update(pkg) {
3779
+ const { entry } = requireHeld(pkg);
3780
+ if (entry.local === true || entry.registry === void 0) throw new Error(`「${entry.package}」是从本地装的,没有地方查新版。`);
3781
+ if (shell === void 0) throw new Error("没有执行缝(shell 服务不可用),更新做不了。");
3782
+ const latest = await latestVersion({
3783
+ shell,
3784
+ name: entry.package,
3785
+ registry: entry.registry,
3786
+ force: true
3787
+ });
3788
+ if (latest === void 0) throw new Error(`问不到「${entry.package}」的新版本。`);
3789
+ const plan = pluginInstallPlan({
3790
+ spec: `${entry.package}@${latest}`,
3791
+ command,
3792
+ registry: entry.registry,
3793
+ ...entry.name === void 0 ? {} : { pluginName: entry.name }
3794
+ });
3795
+ if (!plan.ok) throw new Error(sourceProblemText(plan.problem, plan.env));
3796
+ await runAll(plan.commands);
3797
+ logger?.info?.(`[dsh-workbench] 把 ${entry.package} 更新到 ${latest}`);
3798
+ return { name: pluginNameOf(entry) ?? entry.name ?? entry.package };
3799
+ },
3800
+ async uninstall(pkg) {
3801
+ const ledger = requireLedger();
3802
+ const { entry, key } = requireHeld(pkg);
3803
+ ledger.release(key);
3804
+ const decision = decide(entry, false);
3805
+ if (decision.act === "keep") {
3806
+ logger?.info?.(`[dsh-workbench] ${pluginKeptText(decision.kept)}`);
3807
+ return {
3808
+ removed: false,
3809
+ ...decision.plugin === void 0 ? {} : { name: decision.plugin },
3810
+ kept: decision.kept
3811
+ };
3812
+ }
3813
+ await removeFiles(decision.plugin);
3814
+ ledger.drop(key);
3815
+ logger?.info?.(`[dsh-workbench] ${pluginRemovedText(key)}`);
3816
+ return {
3817
+ removed: true,
3818
+ name: decision.plugin
3819
+ };
3820
+ },
3821
+ async sweep() {
3822
+ if (ledger === void 0) return [];
3823
+ const removed = [];
3824
+ for (const entry of ledger.all()) {
3825
+ if (entry.releasedAt === void 0) continue;
3826
+ const decision = decide(entry, false);
3827
+ if (decision.act === "keep") continue;
3828
+ try {
3829
+ await removeFiles(decision.plugin);
3830
+ } catch (error) {
3831
+ logger?.warn?.(`[dsh-workbench] 清 ${entry.package} 没成:${String(error)}`);
3832
+ continue;
3833
+ }
3834
+ ledger.drop(entry.package);
3835
+ removed.push(entry.package);
3836
+ logger?.info?.(`[dsh-workbench] ${pluginSweptText(entry.package)}`);
3837
+ }
3838
+ return removed;
3839
+ }
3840
+ };
3841
+ }
3842
+ //#endregion
3843
+ //#region src/modules/panels/model/market-env.ts
3844
+ function createEnvDropper(options) {
3845
+ const { catalog, isReleased, ledger, command, run, logger } = options;
3846
+ function referencesOf() {
3847
+ return options.references?.() ?? {
3848
+ trustworthy: false,
3849
+ unreadable: []
3850
+ };
3851
+ }
3852
+ function usersOf(plugin, except) {
3853
+ return catalog().filter((item) => item.id !== except && !(isReleased?.(item.id) ?? false) && (item.env?.plugins ?? []).some((need) => need.name === plugin)).map((item) => item.id);
3854
+ }
3855
+ function libUsersOf(need, unit) {
3856
+ return catalog().filter((item) => item.id !== unit && !(isReleased?.(item.id) ?? false) && (item.env?.libs ?? []).some((other) => other.name === need.name)).map((item) => item.id);
3857
+ }
3858
+ return {
3859
+ usersOf,
3860
+ async drop(unit, env, before) {
3861
+ const kept = /* @__PURE__ */ new Set();
3862
+ const onMachine = new Set((before.plugins ?? []).filter((state) => state.status !== "missing").map((state) => state.name));
3863
+ const unknown = before.plugins === void 0;
3864
+ for (const need of env?.plugins ?? []) {
3865
+ const users = usersOf(need.name, unit);
3866
+ const retention = pluginRetentionOf({
3867
+ held: ledger?.heldAsPlugin(need.name) !== void 0,
3868
+ claimedBy: users,
3869
+ trustworthy: referencesOf().trustworthy
3870
+ });
3871
+ if (retention !== "remove") {
3872
+ for (const id of users) kept.add(id);
3873
+ const reason = pluginKeptOf(retention, {
3874
+ by: users,
3875
+ unreadable: referencesOf().unreadable
3876
+ });
3877
+ logger?.info?.(`[dsh-workbench] ${pluginKeptText(reason)}`);
3878
+ continue;
3879
+ }
3880
+ if (!unknown && !onMachine.has(need.name)) continue;
3881
+ await run(`${quoteShellArg(command)} plugin uninstall ${quoteShellArg(need.name)}`);
3882
+ }
3883
+ for (const need of env?.libs ?? []) {
3884
+ const keepers = libUsersOf(need, unit);
3885
+ if (keepers.length > 0) {
3886
+ for (const id of keepers) kept.add(id);
3887
+ continue;
3888
+ }
3889
+ const present = (before.libs ?? []).some((state) => state.name === need.name && state.status !== "missing");
3890
+ if (before.libs !== void 0 && !present) continue;
3891
+ await run(`npm uninstall --prefix ${quoteShellArg(libsPrefix())} ${quoteShellArg(need.name)}`, true);
3892
+ }
3893
+ return [...kept];
3894
+ }
3895
+ };
3896
+ }
3897
+ //#endregion
3898
+ //#region src/modules/panels/model/market.ts
2995
3899
  function createMarketService(options) {
2996
- const { manifests, manifestsNow, probe, shell, logger, onInstalled, onUninstalled, isReleased, bundles, installed, modules, registries } = options;
3900
+ const { manifests, manifestsNow, probe, shell, logger, onInstalled, onUninstalled, isReleased, bundles, installed, modules, registries, ledger, references } = options;
2997
3901
  const command = options.command ?? "opencli";
2998
3902
  const catalog = () => (manifestsNow?.() ?? manifests).filter((manifest) => (manifest.env?.plugins ?? []).length > 0 || (manifest.env?.libs ?? []).length > 0);
2999
3903
  function requireEntry(unit) {
@@ -3098,30 +4002,28 @@ function createMarketService(options) {
3098
4002
  if (bundles === void 0) throw new Error("这个 profile 里没有插件管理器,装不了模块包。");
3099
4003
  return bundles;
3100
4004
  }
3101
- async function dropEnv(unit, env, before) {
3102
- const kept = /* @__PURE__ */ new Set();
3103
- const onMachine = new Set((before.plugins ?? []).filter((state) => state.status !== "missing").map((state) => state.name));
3104
- const unknown = before.plugins === void 0;
3105
- for (const need of env?.plugins ?? []) {
3106
- const keepers = catalog().filter((item) => item.id !== unit && (item.env?.plugins ?? []).some((other) => other.name === need.name) && !(isReleased?.(item.id) ?? false));
3107
- if (keepers.length > 0) {
3108
- for (const item of keepers) kept.add(item.id);
3109
- continue;
3110
- }
3111
- if (!unknown && !onMachine.has(need.name)) continue;
3112
- await run(`${quoteShellArg(command)} plugin uninstall ${quoteShellArg(need.name)}`);
3113
- }
3114
- for (const need of env?.libs ?? []) {
3115
- const keepers = catalog().filter((item) => item.id !== unit && (item.env?.libs ?? []).some((other) => other.name === need.name) && !(isReleased?.(item.id) ?? false));
3116
- if (keepers.length > 0) {
3117
- for (const item of keepers) kept.add(item.id);
3118
- continue;
3119
- }
3120
- const present = (before.libs ?? []).some((state) => state.name === need.name && state.status !== "missing");
3121
- if (before.libs !== void 0 && !present) continue;
3122
- await run(`npm uninstall --prefix ${quoteShellArg(libsPrefix())} ${quoteShellArg(need.name)}`, true);
4005
+ const envDropper = createEnvDropper({
4006
+ catalog,
4007
+ ...isReleased === void 0 ? {} : { isReleased },
4008
+ ...ledger === void 0 ? {} : { ledger },
4009
+ ...references === void 0 ? {} : { references },
4010
+ command,
4011
+ run,
4012
+ ...logger === void 0 ? {} : { logger }
4013
+ });
4014
+ function pluginUsersOf(plugin, except) {
4015
+ return envDropper.usersOf(plugin, except);
4016
+ }
4017
+ async function reprobePlugin(pkg, name) {
4018
+ const keys = name === void 0 || name === pkg ? [pkg] : [name, pkg];
4019
+ for (const key of keys) {
4020
+ const id = `plugin:${key}`;
4021
+ if (findDependency(catalog(), id, isReleased) === void 0) continue;
4022
+ await probeSharing({
4023
+ action: "update-dependency",
4024
+ id
4025
+ });
3123
4026
  }
3124
- return [...kept];
3125
4027
  }
3126
4028
  async function supplyAfter(module, change) {
3127
4029
  if (module.kind !== "module") return { state: "not-a-module" };
@@ -3172,6 +4074,38 @@ function createMarketService(options) {
3172
4074
  logger?.warn?.(`[dsh-workbench] 卸 ${pkg} 撞上源:补 ${added.join("、")} 后重试一次`);
3173
4075
  return await manager.remove(pkg).catch(() => null) ?? change;
3174
4076
  }
4077
+ function routeDeps(manager) {
4078
+ return {
4079
+ manager,
4080
+ command,
4081
+ runAll,
4082
+ rememberSource: rememberInstalledSource,
4083
+ supplyAfter,
4084
+ ...shell === void 0 ? {} : { shell },
4085
+ ...installed === void 0 ? {} : { installed },
4086
+ ...ledger === void 0 ? {} : { ledger },
4087
+ ...logger === void 0 ? {} : { logger }
4088
+ };
4089
+ }
4090
+ const plugins = createPluginActions({
4091
+ ...ledger === void 0 ? {} : { ledger },
4092
+ ...shell === void 0 ? {} : { shell },
4093
+ command,
4094
+ runAll,
4095
+ claimedBy: (plugin) => pluginUsersOf(plugin),
4096
+ ...references === void 0 ? {} : { references },
4097
+ ...logger === void 0 ? {} : { logger }
4098
+ });
4099
+ async function installByRoute(spec, options) {
4100
+ const manager = requireBundles();
4101
+ const registry = options?.registry ?? (await manager.registries().catch(() => null))?.resolved ?? void 0;
4102
+ return installRouted({
4103
+ ...routeDeps(manager),
4104
+ spec,
4105
+ ...options === void 0 ? {} : { options },
4106
+ ...registry === void 0 ? {} : { registry }
4107
+ });
4108
+ }
3175
4109
  const service = {
3176
4110
  async list() {
3177
4111
  return snapshot();
@@ -3258,7 +4192,8 @@ function createMarketService(options) {
3258
4192
  };
3259
4193
  }
3260
4194
  const before = await capabilityOf(unit, true);
3261
- const keptFor = await dropEnv(unit, requireEntry(unit).env, before);
4195
+ const keptFor = await envDropper.drop(unit, requireEntry(unit).env, before);
4196
+ await plugins.sweep();
3262
4197
  await probeSharing({
3263
4198
  action: "uninstall",
3264
4199
  unit
@@ -3277,25 +4212,7 @@ function createMarketService(options) {
3277
4212
  };
3278
4213
  },
3279
4214
  async installBundle(spec, options) {
3280
- const manager = requireBundles();
3281
- const change = await manager.install({
3282
- spec,
3283
- ...options?.registry === void 0 ? {} : { registry: options.registry },
3284
- ...options?.requestId === void 0 ? {} : { requestId: options.requestId },
3285
- ...options?.approvedBuilds === void 0 ? {} : { approvedBuilds: options.approvedBuilds }
3286
- });
3287
- if (change === null) throw new Error("插件管理器回的安装结果看不懂。");
3288
- const pkg = bundleNameOf(change, spec);
3289
- const module = installed?.(pkg) ?? {
3290
- kind: "unknown",
3291
- pkg
3292
- };
3293
- await rememberInstalledSource(manager, pkg, options?.registry);
3294
- return {
3295
- change,
3296
- module,
3297
- supply: await supplyAfter(module, change)
3298
- };
4215
+ return installByRoute(spec, options);
3299
4216
  },
3300
4217
  async removeBundle(pkg) {
3301
4218
  const manager = requireBundles();
@@ -3313,7 +4230,8 @@ function createMarketService(options) {
3313
4230
  change,
3314
4231
  keptFor: []
3315
4232
  };
3316
- const keptFor = manifest === void 0 || before === void 0 ? [] : await dropEnv(unit, manifest.env, before);
4233
+ const keptFor = manifest === void 0 || before === void 0 ? [] : await envDropper.drop(unit, manifest.env, before);
4234
+ await plugins.sweep();
3317
4235
  onUninstalled?.(unit);
3318
4236
  return {
3319
4237
  change,
@@ -3338,10 +4256,21 @@ function createMarketService(options) {
3338
4256
  force: true
3339
4257
  });
3340
4258
  const spec = latest === void 0 ? pkg : `${pkg}@${latest}`;
3341
- const outcome = await service.installBundle(spec, { ...registry === void 0 ? {} : { registry } });
4259
+ const outcome = await installDshBundle(routeDeps(requireBundles()), spec, { ...registry === void 0 ? {} : { registry } });
4260
+ await plugins.sweep();
3342
4261
  await service.update(unit).catch(() => void 0);
3343
4262
  return outcome;
3344
4263
  },
4264
+ async updatePlugin(pkg) {
4265
+ const outcome = await plugins.update(pkg);
4266
+ await reprobePlugin(pkg, outcome.name);
4267
+ return outcome;
4268
+ },
4269
+ async uninstallPlugin(pkg) {
4270
+ const outcome = await plugins.uninstall(pkg);
4271
+ await reprobePlugin(pkg, outcome.name);
4272
+ return outcome;
4273
+ },
3345
4274
  updateDependency: updateDependencyRow
3346
4275
  };
3347
4276
  return service;
@@ -3396,6 +4325,22 @@ async function dispatchMarket(market, request) {
3396
4325
  action: "cancel-install",
3397
4326
  cancellation: await market.cancelInstall(request.requestId ?? "")
3398
4327
  };
4328
+ if (request.action === "update-plugin") {
4329
+ await market.updatePlugin(request.name ?? "");
4330
+ return {
4331
+ ok: true,
4332
+ action: "update-plugin"
4333
+ };
4334
+ }
4335
+ if (request.action === "uninstall-plugin") {
4336
+ const outcome = await market.uninstallPlugin(request.name ?? "");
4337
+ return {
4338
+ ok: true,
4339
+ action: "uninstall-plugin",
4340
+ removed: outcome.removed,
4341
+ ...outcome.kept === void 0 ? {} : { kept: outcome.kept }
4342
+ };
4343
+ }
3399
4344
  const unit = request.unit ?? "";
3400
4345
  if (request.action === "uninstall") {
3401
4346
  const { entry, keptFor, change } = await market.uninstall(unit);
@@ -3470,13 +4415,24 @@ var OUTPUT_SCHEMA$1 = {
3470
4415
  },
3471
4416
  additionalProperties: true
3472
4417
  };
4418
+ function describePluginUninstall(pkg, outcome) {
4419
+ if (outcome.removed) return `已卸载 opencli 插件 ${pkg}:没人再用它,文件也删了。`;
4420
+ return `已从「你装的插件」里去掉 ${pkg}。${outcome.kept === void 0 ? "" : pluginKeptText(outcome.kept)}`;
4421
+ }
4422
+ function usersOf(shared, names) {
4423
+ const hit = shared.find((item) => names.some((name) => name !== void 0 && name !== "" && item.id === `plugin:${name}`));
4424
+ if (hit === void 0 || hit.usedBy.length === 0) return "";
4425
+ return `它被 ${hit.usedBy.join("、")} 用着(动的是它们的供给,它们的状态跟着重探了一遍)`;
4426
+ }
3473
4427
  var ACTIONS = [
3474
4428
  "list",
3475
4429
  "install",
3476
4430
  "update",
3477
4431
  "uninstall",
3478
4432
  "install-bundle",
3479
- "update-dependency"
4433
+ "update-dependency",
4434
+ "update-plugin",
4435
+ "uninstall-plugin"
3480
4436
  ];
3481
4437
  function registerMarketTools(ctx, market) {
3482
4438
  const tools = ctx.get("tools");
@@ -3486,13 +4442,13 @@ function registerMarketTools(ctx, market) {
3486
4442
  }
3487
4443
  const tool = {
3488
4444
  name: "workbench_modules",
3489
- description: "看 / 装 / 升 / 卸工作台的面板模块。install-bundle 按 spec(包名,可带版本;git / tarball / 本地路径也行)把模块包装进 profile —— 装完会连带装它声明的 opencli 插件;其余 action 按模块 id 动它要的外部供给。卸载只动工作台运行时,不影响终端那套;共用依赖只有升级。",
4445
+ description: "看 / 装 / 升 / 卸工作台的面板模块,以及用户自己装的 opencli 插件。install-bundle 按 spec(包名,可带版本;git / tarball / 本地路径也行)把模块包装进 profile —— 装完会连带装它声明的 opencli 插件;其余 action 按模块 id 动它要的外部供给;update-plugin / uninstall-plugin 按包名或插件名动用户装的那份插件。卸载只动工作台运行时,不影响终端那套;共用依赖只有升级。",
3490
4446
  parameters: {
3491
4447
  type: "object",
3492
4448
  properties: {
3493
4449
  action: {
3494
4450
  type: "string",
3495
- description: "install-bundle=按 spec 装模块包(要 spec),update-dependency=升级共用依赖,其余=对模块(要 id)。",
4451
+ description: "install-bundle=按 spec 装模块包(要 spec),update-dependency=升级共用依赖(要 id),update-plugin / uninstall-plugin=动用户自己装的 opencli 插件(要 plugin),其余=对模块(要 id)。",
3496
4452
  enum: [...ACTIONS]
3497
4453
  },
3498
4454
  id: {
@@ -3506,6 +4462,10 @@ function registerMarketTools(ctx, market) {
3506
4462
  registry: {
3507
4463
  type: "string",
3508
4464
  description: "install-bundle 用:先问哪个源(不填就用这个 profile 自己配的源)。"
4465
+ },
4466
+ plugin: {
4467
+ type: "string",
4468
+ description: "update-plugin / uninstall-plugin 用:opencli 插件的包名或插件名(就是市场「已安装 opencli 插件」那一段里的名字,如 cmdb 或 opencli-plugin-cmdb)。"
3509
4469
  }
3510
4470
  },
3511
4471
  required: ["action"],
@@ -3544,6 +4504,23 @@ function registerMarketTools(ctx, market) {
3544
4504
  text: describeInstall(spec, outcome.module, outcome.supply, outcome.change)
3545
4505
  };
3546
4506
  }
4507
+ if (action === "update-plugin" || action === "uninstall-plugin") {
4508
+ const plugin = args?.plugin?.trim() ?? "";
4509
+ if (plugin === "") throw new Error(`${action} 需要一个 plugin:opencli 插件的包名或插件名 —— 就是市场「已安装 opencli 插件」那一段里的名字。`);
4510
+ if (action === "update-plugin") {
4511
+ const updated = await market.updatePlugin(plugin);
4512
+ const users = usersOf((await market.list()).shared, [updated.name, plugin]);
4513
+ return { text: `已更新 opencli 插件 ${plugin}${users === "" ? "。" : `;${users}。`}` };
4514
+ }
4515
+ const outcome = await market.uninstallPlugin(plugin);
4516
+ const users = usersOf((await market.list()).shared, [outcome.name, plugin]);
4517
+ return {
4518
+ removed: outcome.removed,
4519
+ ...outcome.name === void 0 ? {} : { name: outcome.name },
4520
+ ...outcome.kept === void 0 ? {} : { kept: outcome.kept },
4521
+ text: `${describePluginUninstall(plugin, outcome)}${users === "" ? "" : `${users}。`}`
4522
+ };
4523
+ }
3547
4524
  const id = args?.id;
3548
4525
  if (id === void 0 || id.trim() === "") {
3549
4526
  const { modules, shared } = await market.list();
@@ -3594,7 +4571,7 @@ function applyPanelsPref(panels, payload) {
3594
4571
  const path = action.path ?? null;
3595
4572
  if (action.action === "reset") return {
3596
4573
  ok: true,
3597
- units: panels.resetWorkspace(path)
4574
+ units: panels.reset(path)
3598
4575
  };
3599
4576
  if (action.id === void 0) throw new Error(`${action.action} 需要一个 id(当前套件:${panels.list(path).map((entry) => entry.id).join(", ")})`);
3600
4577
  if (action.action === "show") return {
@@ -3655,66 +4632,12 @@ function panelsFactsOf(call, manifests) {
3655
4632
  ];
3656
4633
  }
3657
4634
  //#endregion
3658
- //#region src/modules/panels/model/config.ts
3659
- function strings(raw) {
3660
- return Array.isArray(raw) ? raw.filter((item) => typeof item === "string") : [];
3661
- }
3662
- function parseShown(raw) {
3663
- if (typeof raw !== "object" || raw === null) return [];
3664
- return strings(raw.shown);
3665
- }
3666
- function parseReleased(raw) {
3667
- if (typeof raw !== "object" || raw === null) return [];
3668
- return strings(raw.ids);
3669
- }
3670
- function resolvePanels(manifests, panels) {
3671
- const shown = panels ?? [];
3672
- const rank = new Map(shown.map((id, index) => [id, index]));
3673
- return manifests.filter((manifest) => manifest.card !== null).map((manifest) => {
3674
- const isShown = rank.has(manifest.id);
3675
- const order = rank.get(manifest.id) ?? shown.length + (manifest.card?.order ?? 0);
3676
- return {
3677
- id: manifest.id,
3678
- titleKey: manifest.card?.titleKey ?? manifest.id,
3679
- order,
3680
- shown: isShown,
3681
- source: isShown ? "user" : "default",
3682
- hasCard: true
3683
- };
3684
- }).sort((a, b) => a.order - b.order || a.id.localeCompare(b.id));
3685
- }
3686
- function withoutReleased(released, id) {
3687
- return released.filter((item) => item !== id);
3688
- }
3689
- function withReleased(released, id) {
3690
- return [.../* @__PURE__ */ new Set([...released, id])];
3691
- }
3692
- function withShown(panels, id) {
3693
- return panels.includes(id) ? [...panels] : [...panels, id];
3694
- }
3695
- function withHidden(panels, id) {
3696
- return panels.filter((item) => item !== id);
3697
- }
3698
- function withMoved(panels, id, index) {
3699
- const current = [...panels];
3700
- const from = current.indexOf(id);
3701
- const max = from === -1 ? current.length : current.length - 1;
3702
- const to = Math.max(0, Math.min(max, Math.trunc(index)));
3703
- if (from === -1) {
3704
- current.splice(to, 0, id);
3705
- return current;
3706
- }
3707
- if (from === to) return current;
3708
- current.splice(to, 0, ...current.splice(from, 1));
3709
- return current;
3710
- }
3711
- //#endregion
3712
4635
  //#region src/modules/panels/model/service.ts
3713
4636
  var PANELS_MODULE_ID = "panels";
3714
4637
  var PANELS_FILE = "config.json";
3715
4638
  var RELEASED_FILE = "released.json";
3716
4639
  function createPanelsService(options) {
3717
- const { manifests, store, logger } = options;
4640
+ const { manifests, store, owned, logger } = options;
3718
4641
  const listeners = /* @__PURE__ */ new Set();
3719
4642
  const machine = store.machine(PANELS_MODULE_ID);
3720
4643
  const folders = /* @__PURE__ */ new Map();
@@ -3725,12 +4648,36 @@ function createPanelsService(options) {
3725
4648
  folders.set(workspace, folder);
3726
4649
  return folder;
3727
4650
  }
3728
- function readShown(folder) {
3729
- return parseShown(folder.read(PANELS_FILE));
4651
+ function readBoard(folder) {
4652
+ const raw = folder.read(PANELS_FILE);
4653
+ return raw === null ? null : parseBoard(raw);
4654
+ }
4655
+ function boardIn(folder) {
4656
+ return readBoard(folder) ?? EMPTY_BOARD;
4657
+ }
4658
+ function writeBoard(folder, next) {
4659
+ folder.write(PANELS_FILE, {
4660
+ shown: next.shown,
4661
+ hidden: next.hidden
4662
+ });
3730
4663
  }
3731
- function writeShown(folder, next) {
3732
- if (next.length === 0) folder.remove(PANELS_FILE);
3733
- else folder.write(PANELS_FILE, { shown: next });
4664
+ function boardsOf(workspace) {
4665
+ const key = workspace?.trim() ?? "";
4666
+ return {
4667
+ workspace: key === "" ? null : readBoard(folderOf(key)),
4668
+ machine: readBoard(machine)
4669
+ };
4670
+ }
4671
+ function targetOf(target, workspace) {
4672
+ if (target === "machine") return {
4673
+ folder: machine,
4674
+ key: null
4675
+ };
4676
+ const key = requireWorkspace(workspace);
4677
+ return {
4678
+ folder: folderOf(key),
4679
+ key
4680
+ };
3734
4681
  }
3735
4682
  let released;
3736
4683
  function readReleased() {
@@ -3749,10 +4696,10 @@ function createPanelsService(options) {
3749
4696
  logger?.warn?.(`[dsh-workbench] 面板配置订阅者报错:${String(error)}`);
3750
4697
  }
3751
4698
  }
3752
- function withPersisted(workspace, next) {
3753
- writeShown(folderOf(workspace), next);
4699
+ function withPersisted(folder, next, key) {
4700
+ writeBoard(folder, next);
3754
4701
  notify();
3755
- return resolvePanels(manifests(), next);
4702
+ return resolvePanels(manifests(), boardsOf(key));
3756
4703
  }
3757
4704
  function takeBackReleased(id) {
3758
4705
  const current = readReleased();
@@ -3773,16 +4720,19 @@ function createPanelsService(options) {
3773
4720
  return key;
3774
4721
  }
3775
4722
  return {
3776
- list(workspace) {
3777
- if (workspace === null || workspace.trim() === "") return resolvePanels(manifests(), null);
3778
- return resolvePanels(manifests(), readShown(folderOf(workspace.trim())));
4723
+ list(workspace, target) {
4724
+ return resolvePanels(manifests(), target === "machine" ? boardsOf(null) : boardsOf(workspace));
4725
+ },
4726
+ boards(workspace) {
4727
+ return boardsOf(workspace);
3779
4728
  },
3780
4729
  isEnabled(id, workspace) {
3781
- const manifest = manifests().find((item) => item.id === id);
3782
- if (manifest === void 0) return false;
3783
- if (manifest.card === null) return true;
3784
- if (workspace === null || workspace.trim() === "") return false;
3785
- return readShown(folderOf(workspace.trim())).includes(id);
4730
+ if (!manifests().some((item) => item.id === id)) return false;
4731
+ if (owned?.().includes(id) === true) return true;
4732
+ return resolveScope({
4733
+ id,
4734
+ boards: boardsOf(workspace)
4735
+ });
3786
4736
  },
3787
4737
  subscribe(listener) {
3788
4738
  listeners.add(listener);
@@ -3796,36 +4746,40 @@ function createPanelsService(options) {
3796
4746
  const seen = /* @__PURE__ */ new Set();
3797
4747
  const drop = (folder) => {
3798
4748
  seen.add(folder.dir);
3799
- const current = readShown(folder);
3800
- if (current.includes(id)) writeShown(folder, withHidden(current, id));
4749
+ const current = readBoard(folder);
4750
+ if (current === null) return;
4751
+ if (!current.shown.includes(id) && !current.hidden.includes(id)) return;
4752
+ writeBoard(folder, withoutPanel(current, id));
3801
4753
  };
3802
4754
  for (const folder of store.scan(PANELS_MODULE_ID)) drop(folder);
3803
4755
  for (const folder of folders.values()) if (!seen.has(folder.dir)) drop(folder);
4756
+ drop(machine);
3804
4757
  notify();
3805
4758
  },
3806
4759
  accept(id) {
3807
4760
  takeBackReleased(id);
3808
4761
  },
3809
- show(id, workspace) {
4762
+ show(id, workspace, target) {
3810
4763
  requireSwitchable(id);
3811
- const key = requireWorkspace(workspace);
4764
+ const { folder, key } = targetOf(target ?? "workspace", workspace);
3812
4765
  takeBackReleased(id);
3813
- return withPersisted(key, withShown(readShown(folderOf(key)), id));
4766
+ return withPersisted(folder, withShown(boardIn(folder), id), key);
3814
4767
  },
3815
- hide(id, workspace) {
4768
+ hide(id, workspace, target) {
3816
4769
  requireSwitchable(id);
3817
- const key = requireWorkspace(workspace);
3818
- return withPersisted(key, withHidden(readShown(folderOf(key)), id));
4770
+ const { folder, key } = targetOf(target ?? "workspace", workspace);
4771
+ return withPersisted(folder, withHidden(boardIn(folder), id), key);
3819
4772
  },
3820
- move(id, index, workspace) {
4773
+ move(id, index, workspace, target) {
3821
4774
  requireSwitchable(id);
3822
- const key = requireWorkspace(workspace);
3823
- const current = readShown(folderOf(key));
3824
- if (!current.includes(id)) takeBackReleased(id);
3825
- return withPersisted(key, withMoved(current, id, index));
4775
+ const { folder, key } = targetOf(target ?? "workspace", workspace);
4776
+ const current = boardIn(folder);
4777
+ if (!current.shown.includes(id)) takeBackReleased(id);
4778
+ return withPersisted(folder, withMoved(current, id, index), key);
3826
4779
  },
3827
- resetWorkspace(workspace) {
3828
- return withPersisted(requireWorkspace(workspace), []);
4780
+ reset(workspace, target) {
4781
+ const { folder, key } = targetOf(target ?? "workspace", workspace);
4782
+ return withPersisted(folder, EMPTY_BOARD, key);
3829
4783
  }
3830
4784
  };
3831
4785
  }
@@ -3847,11 +4801,119 @@ function panelRoutes(panels, values) {
3847
4801
  } };
3848
4802
  }
3849
4803
  //#endregion
4804
+ //#region src/modules/panels/model/plugin-ledger.ts
4805
+ function createPluginLedger(options) {
4806
+ const folder = options.store.machine(PANELS_MODULE_ID);
4807
+ const home = options.home;
4808
+ const all = () => readPluginLedger(folder);
4809
+ const save = (entries) => writePluginLedger(folder, entries);
4810
+ return {
4811
+ home,
4812
+ list: () => heldEntriesOfHome(all(), home),
4813
+ all: () => pluginEntriesOfHome(all(), home),
4814
+ read: (pkg) => {
4815
+ const entry = pluginEntryOf(all(), home, pkg);
4816
+ return entry !== void 0 && entry.releasedAt === void 0 ? entry : void 0;
4817
+ },
4818
+ held: (pkg) => {
4819
+ const entry = pluginEntryOf(all(), home, pkg);
4820
+ return entry !== void 0 && entry.releasedAt === void 0;
4821
+ },
4822
+ heldAsPlugin: (plugin) => {
4823
+ const key = plugin.trim();
4824
+ if (key === "") return void 0;
4825
+ const known = heldEntryOfPlugin(all(), home, key);
4826
+ if (known !== void 0) return known;
4827
+ return heldEntriesOfHome(all(), home).find((entry) => pluginNameOf(entry) === key);
4828
+ },
4829
+ intend(intent) {
4830
+ const registry = intent.registry?.trim() ?? "";
4831
+ save(withPluginEntry(all(), {
4832
+ opencliHome: home,
4833
+ package: intent.package,
4834
+ installedAt: (/* @__PURE__ */ new Date()).toISOString(),
4835
+ ...registry === "" ? {} : { registry },
4836
+ ...intent.local === true ? { local: true } : {}
4837
+ }));
4838
+ },
4839
+ complete(pkg, name) {
4840
+ const trimmed = name?.trim() ?? "";
4841
+ if (trimmed === "") return;
4842
+ const current = pluginEntryOf(all(), home, pkg);
4843
+ if (current === void 0 || current.releasedAt !== void 0) return;
4844
+ save(withPluginEntry(all(), {
4845
+ ...current,
4846
+ name: trimmed
4847
+ }));
4848
+ },
4849
+ release(pkg) {
4850
+ save(withReleasedEntry(all(), home, pkg, (/* @__PURE__ */ new Date()).toISOString()));
4851
+ },
4852
+ drop(pkg) {
4853
+ save(withoutPluginEntry(all(), home, pkg));
4854
+ }
4855
+ };
4856
+ }
4857
+ //#endregion
4858
+ //#region src/shared/api/installed-plugins.ts
4859
+ var INSTALLED_PLUGINS_ROUTE = "panels/plugins";
4860
+ //#endregion
4861
+ //#region src/modules/panels/model/plugin-section.ts
4862
+ async function installedPlugins(options) {
4863
+ const { ledger, shell, force = false } = options;
4864
+ const rows = [];
4865
+ for (const entry of ledger.list()) {
4866
+ const release = shell === void 0 || entry.local === true || entry.registry === void 0 ? void 0 : await probeRelease({
4867
+ shell,
4868
+ package: entry.package,
4869
+ registry: entry.registry,
4870
+ force
4871
+ });
4872
+ rows.push({
4873
+ pkg: entry.package,
4874
+ ...entry.name === void 0 ? {} : { name: entry.name },
4875
+ ...release === void 0 ? {} : {
4876
+ installed: release.installed,
4877
+ ...release.latest === void 0 ? {} : { latest: release.latest }
4878
+ },
4879
+ ...entry.local === true ? { local: true } : {},
4880
+ ...pluginOnDisk(entry) ? {} : { broken: true }
4881
+ });
4882
+ }
4883
+ return rows;
4884
+ }
4885
+ function pluginRoutes(options) {
4886
+ return { [INSTALLED_PLUGINS_ROUTE]: async (search) => {
4887
+ return {
4888
+ ok: true,
4889
+ plugins: await installedPlugins({
4890
+ ledger: options.ledger,
4891
+ ...options.shell === void 0 ? {} : { shell: options.shell },
4892
+ force: param(search, "force") === "1"
4893
+ })
4894
+ };
4895
+ } };
4896
+ }
4897
+ //#endregion
3850
4898
  //#region src/modules/panels/model/tools.ts
3851
- function render(entries, workspace) {
4899
+ var MACHINE_LABEL = "机器级默认板 —— 还没配过的工作区默认开这些:";
4900
+ var MACHINE_CAVEAT = "机器级默认板只是还没配过的工作区的默认值:已经自己配过的工作区保持自己的选择,不会被它改。";
4901
+ function listed(entries) {
4902
+ return entries.map((entry, index) => `${index + 1}. ${entry.id} · ${entry.shown ? "已启用" : "未启用"}${entry.hasCard ? "" : "(无卡片)"}${entry.source === "user" ? " · 用户改过" : ""}`).join("\n");
4903
+ }
4904
+ function render(entries, workspace, scope) {
4905
+ if (scope === "machine") {
4906
+ if (entries.length === 0) return "机器级默认板是空的(还没配过的工作区默认什么卡片都不开)——用 show 放一个上去。";
4907
+ return `${MACHINE_LABEL}\n${listed(entries)}`;
4908
+ }
3852
4909
  const where = workspace === null ? "(不知道当前会话在哪个工作区)" : workspace;
3853
4910
  if (entries.length === 0) return `这个工作区的面板是空的(${where})——用 show 放一个上去。`;
3854
- return `当前工作区:${where}\n${entries.map((entry, index) => `${index + 1}. ${entry.id} · ${entry.shown ? "已启用" : "未启用"}${entry.hasCard ? "" : "(无卡片)"}${entry.source === "user" ? " · 用户改过" : ""}`).join("\n")}`;
4911
+ return `当前工作区:${where}\n${listed(entries)}`;
4912
+ }
4913
+ function parseScope(raw) {
4914
+ if (raw === void 0) return DEFAULT_BOARD_TARGET;
4915
+ if (typeof raw === "string" && BOARD_TARGETS.includes(raw)) return raw;
4916
+ throw new Error(`不认识的 scope:${String(raw)}(可用:workspace / machine)`);
3855
4917
  }
3856
4918
  var OUTPUT_SCHEMA = {
3857
4919
  type: "object",
@@ -3882,13 +4944,13 @@ function registerPanelTools(ctx, panels, values) {
3882
4944
  }
3883
4945
  const tool = {
3884
4946
  name: "workbench_panels",
3885
- description: "看或调当前工作区的右侧面板:启用哪些套件、什么顺序。启用/收起 = 卡片与 agent 工具一起上下线(别的项目不受影响,不删数据)。",
4947
+ description: "看或调右侧面板:启用哪些套件、什么顺序。启用/收起 = 卡片与 agent 工具一起上下线(别的项目不受影响,不删数据)。收起是\"这个工作区明确不要它\",连那些在机器级默认板上开着的套件也照样在这个工作区收起来,之后 show 能放回来。默认只动当前工作区(scope=workspace);scope=machine 动的是\"机器级默认板\" —— 机器级默认板只是还没配过的工作区的默认值,已经自己配过的工作区保持自己的选择,不会被它改。在那份板上 hide = 明确写下\"不要它\",这一笔比名单更管用:名单里还列着它也算不要,没配过的工作区因此看不到它;show 会把这笔划掉、把它放回去。",
3886
4948
  parameters: {
3887
4949
  type: "object",
3888
4950
  properties: {
3889
4951
  action: {
3890
4952
  type: "string",
3891
- description: "show/hide=启用或收起;move=调顺序;reset=清空这个工作区的面板。",
4953
+ description: "show=这个工作区要它(之前收起的也放回来);hide=这个工作区明确不要它(卡片与工具一起下线,默认在机器上也开着的套件同样收起,别的项目不受影响);move=调顺序;reset=这个工作区不再有自己的选择(之前收起、启用的都忘掉;它从此明确\"什么都不要\",机器级默认板也管不到它,要哪一块用 show 放回来)。",
3892
4954
  enum: [
3893
4955
  "list",
3894
4956
  "show",
@@ -3904,6 +4966,11 @@ function registerPanelTools(ctx, panels, values) {
3904
4966
  index: {
3905
4967
  type: "integer",
3906
4968
  description: "move 的位置,0 基(0=最上面)。"
4969
+ },
4970
+ scope: {
4971
+ type: "string",
4972
+ description: "workspace(默认)=只动当前工作区;machine=动机器级默认板,那是还没配过的工作区的默认值 —— 已经自己配过的工作区保持自己的选择,不受它影响。",
4973
+ enum: ["workspace", "machine"]
3907
4974
  }
3908
4975
  },
3909
4976
  required: ["action"],
@@ -3922,8 +4989,9 @@ function registerPanelTools(ctx, panels, values) {
3922
4989
  async execute(args, exec) {
3923
4990
  const action = args?.action ?? "list";
3924
4991
  const id = args?.id;
4992
+ const scope = parseScope(args?.scope);
3925
4993
  const workspace = sessionWorkspace(exec);
3926
- const list = () => panels.list(workspace);
4994
+ const list = () => panels.list(workspace, scope);
3927
4995
  if (![
3928
4996
  "list",
3929
4997
  "show",
@@ -3933,39 +5001,41 @@ function registerPanelTools(ctx, panels, values) {
3933
5001
  ].includes(action)) throw new Error(`不认识的 action:${action}(可用:list / show / hide / move / reset)`);
3934
5002
  if (action === "list") {
3935
5003
  const units = list();
3936
- const entry = panelsHandoffEntry(workspace === null ? {} : { path: workspace });
3937
- if (entry !== void 0) values?.hand(entry, units);
5004
+ if (scope === "workspace") {
5005
+ const entry = panelsHandoffEntry(workspace === null ? {} : { path: workspace });
5006
+ if (entry !== void 0) values?.hand(entry, units);
5007
+ }
3938
5008
  return {
3939
5009
  units,
3940
- text: render(units, workspace)
5010
+ text: render(units, workspace, scope)
3941
5011
  };
3942
5012
  }
3943
5013
  if (action === "reset") {
3944
- const units = panels.resetWorkspace(workspace);
5014
+ const units = panels.reset(workspace, scope);
3945
5015
  return {
3946
5016
  units,
3947
- text: `这个工作区的面板已清空。\n${render(units, workspace)}`
5017
+ text: scope === "machine" ? `机器级默认板已清空(是"配成空",不是"没配过")——还没配过的工作区从此默认什么都不开。${MACHINE_CAVEAT}\n${render(units, workspace, scope)}` : `这个工作区不再有自己的面板选择(之前收起、启用的都忘掉了;它从此明确"什么都不要"—— 机器级默认板也管不到它,要哪一块用 show 放回来)。\n${render(units, workspace, scope)}`
3948
5018
  };
3949
5019
  }
3950
5020
  if (id === void 0) throw new Error(`${action} 需要一个 id(例如 coding-issues)。当前套件:${list().map((entry) => entry.id).join(", ")}`);
3951
5021
  if (action === "show") {
3952
- const units = panels.show(id, workspace);
5022
+ const units = panels.show(id, workspace, scope);
3953
5023
  return {
3954
5024
  units,
3955
- text: `已启用 ${id} —— 卡片与工具一起上线了。\n${render(units, workspace)}`
5025
+ text: scope === "machine" ? `已把 ${id} 放进机器级默认板 —— 还没配过的工作区会带上它(卡片与工具一起上线);那份板上要是给它写过"不要",这一下也划掉了。${MACHINE_CAVEAT}\n${render(units, workspace, scope)}` : `已启用 ${id} —— 这个工作区要它:之前收起的也放回来了,卡片与工具一起上线。\n${render(units, workspace, scope)}`
3956
5026
  };
3957
5027
  }
3958
5028
  if (action === "hide") {
3959
- const units = panels.hide(id, workspace);
5029
+ const units = panels.hide(id, workspace, scope);
3960
5030
  return {
3961
5031
  units,
3962
- text: `已收起 ${id}(卡片与工具一起下线,别的项目不受影响)。\n${render(units, workspace)}`
5032
+ text: scope === "machine" ? `已把 ${id} 从机器级默认板划掉,并在那份板上明确写下"不要它" —— 还没配过的工作区从此看不到它(卡片与工具一起下线);名单里还列着它也一样不算数,要放回来用 show。${MACHINE_CAVEAT}\n${render(units, workspace, scope)}` : `已收起 ${id} —— 这个工作区明确不要它:卡片与工具一起下线(默认在机器上也开着的套件同样收起),别的项目不受影响;要放回来用 show。\n${render(units, workspace, scope)}`
3963
5033
  };
3964
5034
  }
3965
- const units = panels.move(id, args?.index ?? 0, workspace);
5035
+ const units = panels.move(id, args?.index ?? 0, workspace, scope);
3966
5036
  return {
3967
5037
  units,
3968
- text: `已把 ${id} 放到第 ${(args?.index ?? 0) + 1} 位。\n${render(units, workspace)}`
5038
+ text: scope === "machine" ? `已把 ${id} 放到机器级默认板第 ${(args?.index ?? 0) + 1} 位 —— 只影响还没配过的工作区。${MACHINE_CAVEAT}\n${render(units, workspace, scope)}` : `已把 ${id} 放到第 ${(args?.index ?? 0) + 1} 位。\n${render(units, workspace, scope)}`
3969
5039
  };
3970
5040
  }
3971
5041
  };
@@ -3973,26 +5043,44 @@ function registerPanelTools(ctx, panels, values) {
3973
5043
  }
3974
5044
  //#endregion
3975
5045
  //#region src/app/units/host.ts
3976
- var HOST_UNITS = [{
5046
+ var OWNED = [{
5047
+ unit: panelsUnit.id,
3977
5048
  manifest: panelsUnit,
3978
- mount({ ctx, logger, manifests, opencliCommand, probe, store, values }) {
5049
+ mount({ ctx, logger, manifests, ownedUnits, opencliCommand, probe, store, values }) {
3979
5050
  const panels = createPanelsService({
3980
5051
  manifests,
3981
5052
  store,
5053
+ owned: ownedUnits,
3982
5054
  logger
3983
5055
  });
3984
5056
  ctx.provide(PANELS_SERVICE, panels);
3985
5057
  const profileDir = profileDirOf(ctx);
5058
+ const shell = ctx.get("shell");
5059
+ const ledger = createPluginLedger({
5060
+ store,
5061
+ home: isolatedHome()
5062
+ });
3986
5063
  const market = createMarketService({
3987
5064
  manifests: manifests(),
3988
5065
  manifestsNow: manifests,
3989
5066
  ...probe === void 0 ? {} : { probe },
3990
- shell: ctx.get("shell"),
5067
+ shell,
3991
5068
  ...opencliCommand === void 0 ? {} : { command: opencliCommand },
3992
5069
  bundles: readBundleManager(ctx),
3993
5070
  installed: (pkg) => readInstalledModule(profileDir, pkg),
3994
5071
  modules: () => profileModules(profileDir),
3995
5072
  registries: createRegistryNotebook(profileDir),
5073
+ ledger,
5074
+ references: () => {
5075
+ const gaps = declarationGaps(profileDir);
5076
+ return gaps === void 0 ? {
5077
+ trustworthy: false,
5078
+ unreadable: []
5079
+ } : {
5080
+ trustworthy: gaps.length === 0,
5081
+ unreadable: gaps.map((gap) => gap.pkg)
5082
+ };
5083
+ },
3996
5084
  onUninstalled: (unit) => {
3997
5085
  panels.release(unit);
3998
5086
  },
@@ -4003,7 +5091,13 @@ var HOST_UNITS = [{
4003
5091
  logger
4004
5092
  });
4005
5093
  return {
4006
- routes: panelRoutes(panels, values),
5094
+ routes: {
5095
+ ...panelRoutes(panels, values),
5096
+ ...pluginRoutes({
5097
+ ledger,
5098
+ ...shell === void 0 ? {} : { shell }
5099
+ })
5100
+ },
4007
5101
  registerTools: () => [
4008
5102
  ...registerPanelTools(ctx, panels, values),
4009
5103
  ...registerMarketTools(ctx, market),
@@ -4012,7 +5106,14 @@ var HOST_UNITS = [{
4012
5106
  factsOf: (call) => panelsFactsOf(call, manifests()),
4013
5107
  needsApproval: panelsApproval,
4014
5108
  gate: {
4015
- deniedTools: (workspace) => [...manifests().filter((manifest) => manifest.card !== null).filter((manifest) => !panels.isEnabled(manifest.id, workspace)).flatMap((manifest) => manifest.tools.map((tool) => tool.name)), ...inWorkshopSession(ctx, workspace) ? [] : [DIY_TOOL]],
5109
+ deniedTools: (workspace) => {
5110
+ const boards = panels.boards(workspace);
5111
+ const owned = new Set(ownedUnits());
5112
+ return [...manifests().flatMap((manifest) => owned.has(manifest.id) || resolveScope({
5113
+ id: manifest.id,
5114
+ boards
5115
+ }) ? [] : manifest.tools.map((tool) => tool.name)), ...inWorkshopSession(ctx, workspace) ? [] : [DIY_TOOL]];
5116
+ },
4016
5117
  subscribe: (listener) => panels.subscribe(listener)
4017
5118
  },
4018
5119
  prefs: {
@@ -4027,7 +5128,7 @@ var HOST_UNITS = [{
4027
5128
  }
4028
5129
  }];
4029
5130
  function assembleHostUnits(registry) {
4030
- for (const face of HOST_UNITS) registry.register(face);
5131
+ for (const face of OWNED) registry.registerOwned(face);
4031
5132
  }
4032
5133
  //#endregion
4033
5134
  //#region src/shared/api/host/runtime-seed.ts
@@ -4346,6 +5447,7 @@ function bundleRegistriesHandler(ctx) {
4346
5447
  };
4347
5448
  };
4348
5449
  }
5450
+ var refusalLog = /* @__PURE__ */ new Map();
4349
5451
  function moduleVersionsHandler(ctx, shell) {
4350
5452
  return async (search) => {
4351
5453
  const manager = readBundleManager(ctx);
@@ -4353,7 +5455,9 @@ function moduleVersionsHandler(ctx, shell) {
4353
5455
  ok: true,
4354
5456
  modules: []
4355
5457
  };
4356
- const found = profileModules(profileDirOf(ctx));
5458
+ const dir = profileDirOf(ctx);
5459
+ const found = profileModules(dir);
5460
+ const refusals = new Map((ctx.get("workbenchUnits")?.refusals() ?? []).filter((one) => one.unit !== "").map((one) => [one.unit, one.reasons]));
4357
5461
  const force = param(search, "force") === "1";
4358
5462
  const fallback = (await manager.registries())?.resolved ?? void 0;
4359
5463
  return {
@@ -4366,17 +5470,66 @@ function moduleVersionsHandler(ctx, shell) {
4366
5470
  registry,
4367
5471
  force
4368
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
+ }
4369
5484
  return {
4370
5485
  unit: module.unit,
4371
5486
  pkg: module.pkg,
4372
5487
  installed: module.version,
4373
5488
  ...latest === void 0 ? {} : { latest },
4374
- ...module.local === true ? { local: true } : {}
5489
+ ...module.local === true ? { local: true } : {},
5490
+ ...refused === void 0 ? {} : { refused }
4375
5491
  };
4376
5492
  }))
4377
5493
  };
4378
5494
  };
4379
5495
  }
5496
+ function refusedInspection(problems) {
5497
+ if (problems.length === 0) return void 0;
5498
+ return {
5499
+ status: "refused",
5500
+ problem: "not-a-workbench-module",
5501
+ reason: problems.join(";")
5502
+ };
5503
+ }
5504
+ async function declarationRefusal(spec, inspection, ctx, manager) {
5505
+ if (inspection.status !== "accepted" || inspection.name === void 0) return void 0;
5506
+ if (inspection.kind === "path") return refusedInspection(declarationRefusalOf(inspection.name, pathDeclarationOf(spec)));
5507
+ if (inspection.kind !== "registry") return void 0;
5508
+ const shell = ctx.get("shell");
5509
+ if (shell === void 0) return void 0;
5510
+ const registry = typeof inspection.registry === "string" ? inspection.registry : (await manager.registries())?.resolved ?? void 0;
5511
+ if (registry === void 0) return void 0;
5512
+ const raw = await registryDeclarationOf({
5513
+ shell,
5514
+ name: inspection.name,
5515
+ registry
5516
+ });
5517
+ return refusedInspection(declarationRefusalOf(inspection.name, raw));
5518
+ }
5519
+ async function packageRefusal(spec, inspection, ctx, manager) {
5520
+ if (inspection.status !== "accepted") return void 0;
5521
+ const shell = ctx.get("shell");
5522
+ const registry = typeof inspection.registry === "string" ? inspection.registry : (await manager.registries())?.resolved ?? void 0;
5523
+ return (await packageRouteOf({
5524
+ spec,
5525
+ ...shell === void 0 ? {} : { shell },
5526
+ ...registry === void 0 ? {} : { registry }
5527
+ }))?.kind === "neither" ? {
5528
+ status: "refused",
5529
+ problem: "not-a-module-or-plugin",
5530
+ reason: refusalReason()
5531
+ } : void 0;
5532
+ }
4380
5533
  function bundleInspectHandler(ctx) {
4381
5534
  return async (search) => {
4382
5535
  const spec = param(search, "spec");
@@ -4405,7 +5558,7 @@ function bundleInspectHandler(ctx) {
4405
5558
  };
4406
5559
  return {
4407
5560
  ok: true,
4408
- inspection
5561
+ inspection: await packageRefusal(spec, inspection, ctx, manager) ?? await declarationRefusal(spec, inspection, ctx, manager) ?? inspection
4409
5562
  };
4410
5563
  };
4411
5564
  }
@@ -4416,6 +5569,9 @@ var WORKSHOP_TITLE = "工作台DIY";
4416
5569
  //#region src/shared/api/dsh-plugins.ts
4417
5570
  var DSH_PLUGINS_ROUTE = "dsh-plugins";
4418
5571
  //#endregion
5572
+ //#region src/shared/api/unit-manifests.ts
5573
+ var UNIT_MANIFESTS_ROUTE = "unit-manifests";
5574
+ //#endregion
4419
5575
  //#region src/shared/api/host/context-inject.ts
4420
5576
  var CONTEXT_SOURCE_KIND = "workbench";
4421
5577
  function collectContextRefs(messages) {
@@ -5114,7 +6270,7 @@ function failureReasonOf(error) {
5114
6270
  function fail(ledger, item, reason) {
5115
6271
  ledger.failed.add(item);
5116
6272
  ledger.failures.push({
5117
- id: item.face.manifest.id,
6273
+ id: item.host.manifest.id,
5118
6274
  reason
5119
6275
  });
5120
6276
  }
@@ -5124,7 +6280,7 @@ function collectRoutes(mounted, ledger) {
5124
6280
  const routesOfUnit = /* @__PURE__ */ new Map();
5125
6281
  for (const item of mounted) {
5126
6282
  if (ledger.failed.has(item)) continue;
5127
- const unit = item.face.manifest.id;
6283
+ const unit = item.host.manifest.id;
5128
6284
  const accepted = [];
5129
6285
  let problem;
5130
6286
  for (const [key, handler] of Object.entries(item.result.routes)) {
@@ -5162,7 +6318,7 @@ function collectNamed(mounted, ledger, pickMany, label) {
5162
6318
  const out = [];
5163
6319
  for (const item of mounted) {
5164
6320
  if (ledger.failed.has(item)) continue;
5165
- const unit = item.face.manifest.id;
6321
+ const unit = item.host.manifest.id;
5166
6322
  const entries = pickMany(item);
5167
6323
  if (entries.length === 0) continue;
5168
6324
  const clash = entries.find((entry) => owner.has(entry.resource));
@@ -5185,7 +6341,7 @@ function collectContextRenderers(mounted, ledger) {
5185
6341
  const owner = /* @__PURE__ */ new Map();
5186
6342
  for (const item of mounted) {
5187
6343
  if (ledger.failed.has(item)) continue;
5188
- const unit = item.face.manifest.id;
6344
+ const unit = item.host.manifest.id;
5189
6345
  const context = item.result.context;
5190
6346
  if (context === void 0) continue;
5191
6347
  const clash = context.kinds.find((kind) => owner.has(kind));
@@ -5209,10 +6365,10 @@ function pickProvider(mounted, ledger, pick, label) {
5209
6365
  if (value === void 0) continue;
5210
6366
  if (chosen === void 0) {
5211
6367
  chosen = value;
5212
- chosenBy = item.face.manifest.id;
6368
+ chosenBy = item.host.manifest.id;
5213
6369
  continue;
5214
6370
  }
5215
- fail(ledger, item, `${label}只能有一个提供者,现在是:${chosenBy}、${item.face.manifest.id}`);
6371
+ fail(ledger, item, `${label}只能有一个提供者,现在是:${chosenBy}、${item.host.manifest.id}`);
5216
6372
  }
5217
6373
  return chosen;
5218
6374
  }
@@ -5230,8 +6386,9 @@ function collectWarmReads(mounted) {
5230
6386
  return reads;
5231
6387
  };
5232
6388
  }
5233
- function mountUnits(faces, deps) {
6389
+ function mountUnits(units, deps) {
5234
6390
  const store = deps.store ?? createWorkspaceStore({ root: null });
6391
+ const ownedUnits = deps.ownedUnits ?? (() => []);
5235
6392
  const mounted = [];
5236
6393
  const skipped = [];
5237
6394
  const longTaskDisposers = /* @__PURE__ */ new Map();
@@ -5239,16 +6396,16 @@ function mountUnits(faces, deps) {
5239
6396
  failed: /* @__PURE__ */ new Set(),
5240
6397
  failures: []
5241
6398
  };
5242
- for (const face of faces) {
5243
- const readiness = unitReadiness(face.manifest, (dep) => deps.ctx.get(dep));
6399
+ for (const host of units) {
6400
+ const readiness = unitReadiness(host.manifest, (dep) => deps.ctx.get(dep));
5244
6401
  if (!readiness.canMount) {
5245
6402
  skipped.push({
5246
- id: face.manifest.id,
6403
+ id: host.manifest.id,
5247
6404
  missing: readiness.missingHard
5248
6405
  });
5249
6406
  continue;
5250
6407
  }
5251
- const unit = face.manifest.id;
6408
+ const unit = host.manifest.id;
5252
6409
  const longTasks = deps.longTasks === void 0 ? void 0 : createLongTaskHost({
5253
6410
  scope: deps.longTasks.scope,
5254
6411
  unit,
@@ -5256,10 +6413,12 @@ function mountUnits(faces, deps) {
5256
6413
  });
5257
6414
  let result;
5258
6415
  try {
5259
- result = face.mount({
6416
+ result = host.face.mount({
6417
+ manifest: host.manifest,
5260
6418
  ctx: deps.ctx,
5261
6419
  logger: deps.logger,
5262
6420
  manifests: deps.manifests,
6421
+ ownedUnits,
5263
6422
  ...deps.opencliCommand === void 0 ? {} : { opencliCommand: deps.opencliCommand },
5264
6423
  ...deps.probe === void 0 ? {} : { probe: deps.probe },
5265
6424
  ...deps.values === void 0 ? {} : { values: deps.values },
@@ -5275,7 +6434,7 @@ function mountUnits(faces, deps) {
5275
6434
  continue;
5276
6435
  }
5277
6436
  const item = {
5278
- face,
6437
+ host,
5279
6438
  readiness,
5280
6439
  result: withRunsRoute(result, unit)
5281
6440
  };
@@ -5291,7 +6450,7 @@ function mountUnits(faces, deps) {
5291
6450
  for (const item of mounted) {
5292
6451
  if (ledger.failed.has(item)) continue;
5293
6452
  const declare = item.result.factsOf;
5294
- if (declare !== void 0) factsOf.set(item.face.manifest.id, declare);
6453
+ if (declare !== void 0) factsOf.set(item.host.manifest.id, declare);
5295
6454
  }
5296
6455
  const gate = pickProvider(mounted, ledger, (item) => item.result.gate, "启停闸门");
5297
6456
  const provision = pickProvider(mounted, ledger, (item) => item.result.provision, "机器供给");
@@ -5324,30 +6483,71 @@ function mountUnits(faces, deps) {
5324
6483
  }
5325
6484
  //#endregion
5326
6485
  //#region src/shared/api/host/unit-registry.ts
5327
- function createUnitRegistry() {
5328
- const faces = [];
5329
- const ids = /* @__PURE__ */ new Set();
6486
+ function sourceLabel(source) {
6487
+ return source.kind === "owned" ? "外壳自己" : source.pkg;
6488
+ }
6489
+ function createUnitRegistry(options) {
6490
+ const refusals = [];
6491
+ const hosts = [];
6492
+ const byUnit = /* @__PURE__ */ new Map();
5330
6493
  const listeners = /* @__PURE__ */ new Set();
5331
6494
  function notify() {
5332
6495
  for (const listener of listeners) listener();
5333
6496
  }
6497
+ function add(host) {
6498
+ const taken = byUnit.get(host.unit);
6499
+ if (taken !== void 0) throw new Error(`套件 id 重复:${host.unit}(id 会进持久化配置,不能撞)—— 已被 ${sourceLabel(taken.source)} 占着`);
6500
+ byUnit.set(host.unit, host);
6501
+ hosts.push(host);
6502
+ notify();
6503
+ return () => {
6504
+ if (byUnit.get(host.unit) !== host) return;
6505
+ byUnit.delete(host.unit);
6506
+ const index = hosts.indexOf(host);
6507
+ if (index >= 0) hosts.splice(index, 1);
6508
+ notify();
6509
+ };
6510
+ }
5334
6511
  return {
5335
6512
  register(face) {
5336
- const id = face.manifest.id;
5337
- if (ids.has(id)) throw new Error(`套件 id 重复:${id}(id 会进持久化配置,不能撞)`);
5338
- ids.add(id);
5339
- faces.push(face);
5340
- notify();
5341
- return () => {
5342
- const index = faces.indexOf(face);
5343
- if (index < 0) return;
5344
- faces.splice(index, 1);
5345
- ids.delete(id);
6513
+ const unit = typeof face?.unit === "string" ? face.unit.trim() : "";
6514
+ const refuse = (pkg, reasons) => {
6515
+ refusals.push({
6516
+ unit,
6517
+ ...pkg === void 0 ? {} : { pkg },
6518
+ reasons
6519
+ });
6520
+ options.logger?.warn?.(`[dsh-workbench] ${pkg ?? "这个模块"}没挂上(unit ${unit === "" ? "没写" : unit}):${reasons.join(";")}`);
5346
6521
  notify();
6522
+ return () => {};
5347
6523
  };
6524
+ if (unit === "") return refuse(void 0, ["register({ unit, mount }) 里的 unit 是空的 —— U1b 之前的旧形状是 register({ manifest, mount });现在清单由外壳从**已装包**的 dsh.workbench 读出来,模块里的 id 从 deps.manifest.id 拿(ADR-0038 §2.2)"]);
6525
+ const source = options.declarationOf(unit);
6526
+ if (source === void 0) return refuse(void 0, [`没有哪个已装模块声明了 unit「${unit}」—— 查过这份 profile 的已装清单。要么 package.json 里 dsh.workbench.unit 写错了(改完要重启工作台),要么这个包根本没被装进这份 profile`]);
6527
+ const read = readDeclaration(source.pkg, source.declaration);
6528
+ if (read.manifest === void 0) return refuse(source.pkg, read.problems);
6529
+ return add({
6530
+ unit,
6531
+ manifest: read.manifest,
6532
+ face,
6533
+ source: {
6534
+ kind: "package",
6535
+ pkg: source.pkg
6536
+ }
6537
+ });
5348
6538
  },
5349
- all: () => faces,
5350
- manifests: () => faces.map((face) => face.manifest),
6539
+ registerOwned(face) {
6540
+ return add({
6541
+ unit: face.manifest.id,
6542
+ manifest: face.manifest,
6543
+ face,
6544
+ source: { kind: "owned" }
6545
+ });
6546
+ },
6547
+ all: () => hosts,
6548
+ owned: () => hosts.filter((host) => host.source.kind === "owned").map((host) => host.unit),
6549
+ refusals: () => refusals,
6550
+ manifests: () => hosts.map((host) => host.manifest),
5351
6551
  subscribe(listener) {
5352
6552
  listeners.add(listener);
5353
6553
  return () => listeners.delete(listener);
@@ -5355,6 +6555,20 @@ function createUnitRegistry() {
5355
6555
  };
5356
6556
  }
5357
6557
  //#endregion
6558
+ //#region src/shared/api/host/unit-roster-facts.ts
6559
+ function rosterKeyOf(units) {
6560
+ return units.all().map((host) => host.unit).join("\0");
6561
+ }
6562
+ function bindRosterFacts(options) {
6563
+ let seen = rosterKeyOf(options.units);
6564
+ return options.units.subscribe(() => {
6565
+ const now = rosterKeyOf(options.units);
6566
+ if (now === seen) return;
6567
+ seen = now;
6568
+ options.bump([UNIT_MANIFESTS_ROUTE]);
6569
+ });
6570
+ }
6571
+ //#endregion
5358
6572
  //#region src/shared/api/host/tool-scope.ts
5359
6573
  function agentOf(value) {
5360
6574
  return typeof value === "object" && value !== null ? value : void 0;
@@ -5482,8 +6696,24 @@ function apply(ctx) {
5482
6696
  };
5483
6697
  const renderers = /* @__PURE__ */ new Map();
5484
6698
  const shell = ctx.get("shell");
5485
- const units = createUnitRegistry();
6699
+ let roster;
6700
+ const profileDir = profileDirOf(ctx);
6701
+ const units = createUnitRegistry({
6702
+ declarationOf: (unit) => {
6703
+ if (roster === void 0) roster = new Map(profileModules(profileDir).map((row) => [row.unit, row.pkg]));
6704
+ const pkg = roster.get(unit);
6705
+ return pkg === void 0 ? void 0 : {
6706
+ pkg,
6707
+ declaration: moduleDeclarationOf(profileDir, pkg)
6708
+ };
6709
+ },
6710
+ logger: ctx.logger
6711
+ });
5486
6712
  ctx.provide(UNITS_SERVICE, units);
6713
+ routes[UNIT_MANIFESTS_ROUTE] = () => ({
6714
+ ok: true,
6715
+ manifests: units.manifests()
6716
+ });
5487
6717
  assembleHostUnits(units);
5488
6718
  ctx.inject(["workspaceRegistry"], (scoped) => {
5489
6719
  const registry = scoped.get("workspaceRegistry");
@@ -5548,14 +6778,15 @@ function apply(ctx) {
5548
6778
  for (const unit of item.plan.routesOfUnit.keys()) routesOfUnitMap.delete(unit);
5549
6779
  for (const resource of item.plan.owners.keys()) ownersMap.delete(resource);
5550
6780
  }
5551
- function mountUnit(face) {
5552
- const id = face.manifest.id;
6781
+ function mountUnit(host) {
6782
+ const id = host.unit;
5553
6783
  let one;
5554
6784
  try {
5555
- one = mountUnits([face], {
6785
+ one = mountUnits([host], {
5556
6786
  ctx,
5557
6787
  logger: ctx.logger,
5558
6788
  manifests: liveManifests,
6789
+ ownedUnits: () => units.owned(),
5559
6790
  store,
5560
6791
  opencliCommand: opencli.command,
5561
6792
  probe: (unit, options) => probeOne(unit, options?.force ?? false),
@@ -5568,7 +6799,7 @@ function apply(ctx) {
5568
6799
  } catch (error) {
5569
6800
  mountFailures.set(id, {
5570
6801
  reason: failureReasonOf(error),
5571
- face
6802
+ host
5572
6803
  });
5573
6804
  ctx.logger?.warn?.(`[dsh-workbench] ${id} 套件起不来(装配期):${failureReasonOf(error)}`);
5574
6805
  return;
@@ -5577,7 +6808,7 @@ function apply(ctx) {
5577
6808
  for (const failure of one.failed) {
5578
6809
  mountFailures.set(failure.id, {
5579
6810
  reason: failure.reason,
5580
- face
6811
+ host
5581
6812
  });
5582
6813
  ctx.logger?.warn?.(`[dsh-workbench] ${failure.id} 套件起不来,已摘掉它这一份:${failure.reason}`);
5583
6814
  }
@@ -5586,7 +6817,7 @@ function apply(ctx) {
5586
6817
  one.disposeLongTasks();
5587
6818
  return;
5588
6819
  }
5589
- if (slot.readiness.missingSoft.length > 0) ctx.logger?.warn?.(`[dsh-workbench] ${face.manifest.id} 套件降级:缺 ${slot.readiness.missingSoft.join(", ")}`);
6820
+ if (slot.readiness.missingSoft.length > 0) ctx.logger?.warn?.(`[dsh-workbench] ${host.unit} 套件降级:缺 ${slot.readiness.missingSoft.join(", ")}`);
5590
6821
  const added = [];
5591
6822
  try {
5592
6823
  Object.assign(routes, one.routes);
@@ -5595,7 +6826,7 @@ function apply(ctx) {
5595
6826
  for (const [unit, fn] of one.factsOf) factsOfMap.set(unit, fn);
5596
6827
  for (const [unit, list] of one.routesOfUnit) routesOfUnitMap.set(unit, list);
5597
6828
  for (const [resource, unit] of one.owners) ownersMap.set(resource, unit);
5598
- mounted.set(face.manifest.id, {
6829
+ mounted.set(host.unit, {
5599
6830
  result: slot.result,
5600
6831
  plan: one,
5601
6832
  toolDisposers: slot.result.registerTools(),
@@ -5611,7 +6842,7 @@ function apply(ctx) {
5611
6842
  one.disposeLongTasks();
5612
6843
  mountFailures.set(id, {
5613
6844
  reason: failureReasonOf(error),
5614
- face
6845
+ host
5615
6846
  });
5616
6847
  ctx.logger?.warn?.(`[dsh-workbench] ${id} 套件起不来(注册期):${failureReasonOf(error)}`);
5617
6848
  }
@@ -5635,14 +6866,14 @@ function apply(ctx) {
5635
6866
  if (provision !== void 0) undoPost.push(registerProvision(ctx, provision, facts));
5636
6867
  }
5637
6868
  function sync() {
5638
- const present = new Set(units.all().map((face) => face.manifest.id));
6869
+ const present = new Set(units.all().map((one) => one.unit));
5639
6870
  for (const id of [...mounted.keys()]) if (!present.has(id)) unmountUnit(id);
5640
6871
  for (const id of [...mountFailures.keys()]) if (!present.has(id)) mountFailures.delete(id);
5641
- for (const face of units.all()) {
5642
- if (mounted.has(face.manifest.id)) continue;
5643
- const already = mountFailures.get(face.manifest.id);
5644
- if (already !== void 0 && already.face === face) continue;
5645
- mountUnit(face);
6872
+ for (const host of units.all()) {
6873
+ if (mounted.has(host.unit)) continue;
6874
+ const already = mountFailures.get(host.unit);
6875
+ if (already !== void 0 && already.host === host) continue;
6876
+ mountUnit(host);
5646
6877
  }
5647
6878
  rebindPost();
5648
6879
  }
@@ -5682,6 +6913,10 @@ function apply(ctx) {
5682
6913
  for (const dispose of undoPost) dispose?.();
5683
6914
  }, `${PLUGIN_ID}: 卸载时把单元摘干净`);
5684
6915
  ctx.effect(() => units.subscribe(sync), `${PLUGIN_ID}: 注册表变了就增删(装/卸一个模块 = 挂/卸它的面)`);
6916
+ ctx.effect(() => bindRosterFacts({
6917
+ units,
6918
+ bump: (names) => facts.bump(names)
6919
+ }), `${PLUGIN_ID}: 花名册变了就把清单那条事实记一笔(浏览器据此重读,不用刷新页面)`);
5685
6920
  ctx.effect(() => {
5686
6921
  const off = ctx.on?.("tools/post-execute", (exec, result, next) => {
5687
6922
  const name = exec?.name === void 0 ? void 0 : String(exec.name);