@dsh-workbench/core 0.2.5 → 0.2.7

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 (38) hide show
  1. package/README.md +13 -9
  2. package/bin/dsh-workbench +12 -43
  3. package/client.js +914 -350
  4. package/dist/client/client.body.js +914 -350
  5. package/dist/client/index.js +915 -351
  6. package/dist/host/index.js +1314 -667
  7. package/dist/types/app/client.d.ts +216 -6
  8. package/dist/types/app/layouts/config-sync/ConfigSyncSection.d.ts +5 -0
  9. package/dist/types/app/layouts/rightbar/__tests__/market-order.test.d.ts +1 -0
  10. package/dist/types/app/layouts/rightbar/__tests__/market-running-display.test.d.ts +1 -0
  11. package/dist/types/app/layouts/rightbar/__tests__/market-workbench-act.test.d.ts +1 -0
  12. package/dist/types/app/layouts/rightbar/market-act.d.ts +2 -1
  13. package/dist/types/app/layouts/rightbar/market-rows.d.ts +3 -0
  14. package/dist/types/app/layouts/rightbar/market-settle-hold.d.ts +13 -0
  15. package/dist/types/app/layouts/rightbar/market-settle.d.ts +22 -0
  16. package/dist/types/app/layouts/rightbar/market-transient-state.d.ts +22 -0
  17. package/dist/types/app/layouts/rightbar/market-view-text.d.ts +26 -1
  18. package/dist/types/app/layouts/sidebar/workspace-groups.d.ts +4 -1
  19. package/dist/types/modules/panels/__tests__/market-tools-harness.d.ts +3 -0
  20. package/dist/types/modules/panels/model/market-workbench-update.d.ts +19 -0
  21. package/dist/types/modules/panels/model/market.d.ts +4 -0
  22. package/dist/types/modules/sessions/index.d.ts +1 -1
  23. package/dist/types/modules/sessions/model/session-node.d.ts +1 -0
  24. package/dist/types/shared/api/bundle-install.d.ts +1 -0
  25. package/dist/types/shared/api/config-sync.d.ts +35 -0
  26. package/dist/types/shared/api/config-sync.test.d.ts +1 -0
  27. package/dist/types/shared/api/host/__tests__/config-sync.test.d.ts +1 -0
  28. package/dist/types/shared/api/host/__tests__/post-channels.test.d.ts +1 -0
  29. package/dist/types/shared/api/host/__tests__/self-update.test.d.ts +1 -0
  30. package/dist/types/shared/api/host/bundle-manager.d.ts +1 -0
  31. package/dist/types/shared/api/host/bundle-routes.d.ts +1 -1
  32. package/dist/types/shared/api/host/config-sync.d.ts +46 -0
  33. package/dist/types/shared/api/host/post-channels.d.ts +18 -0
  34. package/dist/types/shared/api/host/self-update.d.ts +45 -0
  35. package/docs/HANDOFF.md +19 -14
  36. package/docs/OPERATIONS.md +40 -8
  37. package/package.json +4 -4
  38. package/scripts/seed-profile.sh +21 -85
@@ -1,6 +1,6 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
- import fs, { chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, readdirSync, readlinkSync, realpathSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
3
- import path, { delimiter, dirname, join, resolve, sep } from "node:path";
2
+ import fs, { chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, statSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
3
+ import path, { basename, delimiter, dirname, join, resolve, sep } from "node:path";
4
4
  import os from "node:os";
5
5
  import { spawn } from "node:child_process";
6
6
  import { fileURLToPath } from "node:url";
@@ -247,7 +247,7 @@ var PHASES = [
247
247
  function asRecord$10(value) {
248
248
  return typeof value === "object" && value !== null ? value : void 0;
249
249
  }
250
- function textOf$6(value, max) {
250
+ function textOf$7(value, max) {
251
251
  if (typeof value !== "string") return "";
252
252
  const trimmed = value.trim();
253
253
  return trimmed.length > max ? trimmed.slice(0, max) : trimmed;
@@ -260,8 +260,8 @@ function atOf(value) {
260
260
  function errorOf(raw) {
261
261
  const record = asRecord$10(raw);
262
262
  if (record === void 0) return void 0;
263
- const code = textOf$6(record["code"], 120);
264
- const message = textOf$6(record["message"], PANEL_STATE_MAX_TEXT);
263
+ const code = textOf$7(record["code"], 120);
264
+ const message = textOf$7(record["message"], PANEL_STATE_MAX_TEXT);
265
265
  if (code === "" && message === "") return void 0;
266
266
  return {
267
267
  code: code === "" ? "unknown" : code,
@@ -274,7 +274,7 @@ function parseCardsReport(payload) {
274
274
  ok: false,
275
275
  message: "请求体要是一个对象"
276
276
  };
277
- const workspace = textOf$6(record["workspace"], PANEL_STATE_MAX_TEXT);
277
+ const workspace = textOf$7(record["workspace"], PANEL_STATE_MAX_TEXT);
278
278
  if (workspace === "") return {
279
279
  ok: false,
280
280
  message: "workspace 不能是空的 —— 面板按工作区存"
@@ -288,9 +288,9 @@ function parseCardsReport(payload) {
288
288
  for (const item of raw.slice(0, 32)) {
289
289
  const card = asRecord$10(item);
290
290
  if (card === void 0) continue;
291
- const unit = textOf$6(card["unit"], 200);
292
- const route = textOf$6(card["route"], 400);
293
- const phase = textOf$6(card["phase"], 20);
291
+ const unit = textOf$7(card["unit"], 200);
292
+ const route = textOf$7(card["route"], 400);
293
+ const phase = textOf$7(card["phase"], 20);
294
294
  const at = atOf(card["at"]);
295
295
  if (unit === "" || route === "" || at === null || !PHASES.includes(phase)) continue;
296
296
  const error = errorOf(card["error"]);
@@ -320,9 +320,9 @@ function parseCrashReport(payload) {
320
320
  ok: false,
321
321
  message: "请求体要是一个对象"
322
322
  };
323
- const workspace = textOf$6(record["workspace"], PANEL_STATE_MAX_TEXT);
324
- const unit = textOf$6(record["unit"], 200);
325
- const reason = textOf$6(record["reason"], PANEL_STATE_MAX_TEXT);
323
+ const workspace = textOf$7(record["workspace"], PANEL_STATE_MAX_TEXT);
324
+ const unit = textOf$7(record["unit"], 200);
325
+ const reason = textOf$7(record["reason"], PANEL_STATE_MAX_TEXT);
326
326
  if (workspace === "") return {
327
327
  ok: false,
328
328
  message: "workspace 不能是空的"
@@ -568,20 +568,20 @@ var PERMISSION_CODES = /* @__PURE__ */ new Set([
568
568
  function asRecord$9(value) {
569
569
  return typeof value === "object" && value !== null ? value : void 0;
570
570
  }
571
- function textOf$5(value) {
571
+ function textOf$6(value) {
572
572
  return typeof value === "string" ? value.trim() : "";
573
573
  }
574
574
  function codeOf(page) {
575
575
  const record = asRecord$9(page);
576
- return record === void 0 ? "" : textOf$5(record["code"]);
576
+ return record === void 0 ? "" : textOf$6(record["code"]);
577
577
  }
578
578
  function messageOf$2(page) {
579
579
  const record = asRecord$9(page);
580
- return record === void 0 ? "" : textOf$5(record["message"]);
580
+ return record === void 0 ? "" : textOf$6(record["message"]);
581
581
  }
582
582
  function hintOf$1(page) {
583
583
  const record = asRecord$9(page);
584
- const hint = record === void 0 ? "" : textOf$5(record["hint"]);
584
+ const hint = record === void 0 ? "" : textOf$6(record["hint"]);
585
585
  return hint === "" ? void 0 : hint;
586
586
  }
587
587
  function pageFailed(page) {
@@ -812,7 +812,7 @@ var DEFAULT_TEXT_MAX = 200;
812
812
  function asRecord$8(value) {
813
813
  return typeof value === "object" && value !== null ? value : void 0;
814
814
  }
815
- function textOf$4(value) {
815
+ function textOf$5(value) {
816
816
  return typeof value === "string" ? value.trim() : "";
817
817
  }
818
818
  function entriesOf(value) {
@@ -825,8 +825,8 @@ function entriesOf(value) {
825
825
  if (item === void 0) continue;
826
826
  const nth = typeof item["nth"] === "number" ? item["nth"] : entries.length;
827
827
  entries.push({
828
- text: textOf$4(item["text"]),
829
- tag: textOf$4(item["tag"]),
828
+ text: textOf$5(item["text"]),
829
+ tag: textOf$5(item["tag"]),
830
830
  nth
831
831
  });
832
832
  }
@@ -835,7 +835,7 @@ function entriesOf(value) {
835
835
  function reasonOf(stdout, stderr) {
836
836
  const parsed = asRecord$8(parseJsonLoose(stdout));
837
837
  const error = parsed === void 0 ? void 0 : asRecord$8(parsed["error"]);
838
- const message = error === void 0 ? "" : textOf$4(error["message"]);
838
+ const message = error === void 0 ? "" : textOf$5(error["message"]);
839
839
  if (message !== "") return message;
840
840
  const lines = (stderr.trim() === "" ? stdout : stderr).split("\n").map((one) => one.trim()).filter((one) => one !== "");
841
841
  return (lines[lines.length - 1] ?? "").slice(-200);
@@ -843,7 +843,7 @@ function reasonOf(stdout, stderr) {
843
843
  function hintOf(stdout) {
844
844
  const parsed = asRecord$8(parseJsonLoose(stdout));
845
845
  const error = parsed === void 0 ? void 0 : asRecord$8(parsed["error"]);
846
- const hint = error === void 0 ? "" : textOf$4(error["hint"]);
846
+ const hint = error === void 0 ? "" : textOf$5(error["hint"]);
847
847
  return hint === "" ? void 0 : hint;
848
848
  }
849
849
  function sameOrigin(left, right) {
@@ -857,7 +857,7 @@ function urlOf(stdout) {
857
857
  const bare = (stdout.split("\n").map((one) => one.trim()).filter((one) => one !== "")[0] ?? "").replace(/^url:\s*/i, "").trim().replace(/^["']+|["']+$/g, "");
858
858
  if (bare.startsWith("{")) {
859
859
  const parsed = asRecord$8(parseJsonLoose(bare));
860
- const url = parsed === void 0 ? "" : textOf$4(parsed["url"]);
860
+ const url = parsed === void 0 ? "" : textOf$5(parsed["url"]);
861
861
  if (url !== "") return url;
862
862
  }
863
863
  return bare;
@@ -1872,7 +1872,7 @@ var PLUGIN_LEDGER_FILE = "plugins.json";
1872
1872
  function ledgerKeyOf(entry) {
1873
1873
  return `${entry.opencliHome}\u0000${entry.package}`;
1874
1874
  }
1875
- function textOf$3(value) {
1875
+ function textOf$4(value) {
1876
1876
  if (typeof value !== "string") return void 0;
1877
1877
  const trimmed = value.trim();
1878
1878
  return trimmed === "" ? void 0 : trimmed;
@@ -1880,13 +1880,13 @@ function textOf$3(value) {
1880
1880
  function entryOf(value) {
1881
1881
  if (typeof value !== "object" || value === null) return void 0;
1882
1882
  const box = value;
1883
- const opencliHome = textOf$3(box["opencliHome"]);
1884
- const pkg = textOf$3(box["package"]);
1885
- const installedAt = textOf$3(box["installedAt"]);
1883
+ const opencliHome = textOf$4(box["opencliHome"]);
1884
+ const pkg = textOf$4(box["package"]);
1885
+ const installedAt = textOf$4(box["installedAt"]);
1886
1886
  if (opencliHome === void 0 || pkg === void 0 || installedAt === void 0) return void 0;
1887
- const name = textOf$3(box["name"]);
1888
- const registry = textOf$3(box["registry"]);
1889
- const releasedAt = textOf$3(box["releasedAt"]);
1887
+ const name = textOf$4(box["name"]);
1888
+ const registry = textOf$4(box["registry"]);
1889
+ const releasedAt = textOf$4(box["releasedAt"]);
1890
1890
  return {
1891
1891
  opencliHome,
1892
1892
  package: pkg,
@@ -2456,6 +2456,10 @@ function readBundleManager(ctx) {
2456
2456
  async remove(name) {
2457
2457
  return asChange(await face.removeBundle?.(name));
2458
2458
  },
2459
+ async setEnabled(name, enabled) {
2460
+ if (typeof face.setBundleEnabled !== "function") return null;
2461
+ return asChange(await face.setBundleEnabled(name, enabled));
2462
+ },
2459
2463
  async cancel(requestId) {
2460
2464
  if (typeof face.cancelInstall !== "function") return null;
2461
2465
  return pick(CANCELLATIONS, (asRecord$6(await face.cancelInstall(requestId)) ?? {}).status) ?? null;
@@ -2849,20 +2853,67 @@ var READ_ONLY_ACTIONS = /* @__PURE__ */ new Set([
2849
2853
  "recon",
2850
2854
  ""
2851
2855
  ]);
2856
+ var SPEC_LIMIT = 80;
2857
+ function missing(id) {
2858
+ return id === void 0 || id === "";
2859
+ }
2860
+ function pad(token) {
2861
+ return token.startsWith("[") ? ` ${token} ` : token;
2862
+ }
2863
+ function workbenchModule(id) {
2864
+ return missing(id) ? {
2865
+ zh: "工作台模块",
2866
+ en: "the workbench module"
2867
+ } : {
2868
+ zh: `工作台${pad(`[${id}]`)}模块`,
2869
+ en: `the workbench [${id}] module`
2870
+ };
2871
+ }
2872
+ function anyWorkbenchModule(id) {
2873
+ return missing(id) ? {
2874
+ zh: "工作台的一个模块",
2875
+ en: "a workbench module"
2876
+ } : workbenchModule(id);
2877
+ }
2878
+ function workbenchPlugin(id) {
2879
+ return missing(id) ? {
2880
+ zh: "工作台的 opencli 插件",
2881
+ en: "a workbench opencli plugin"
2882
+ } : {
2883
+ zh: `工作台${pad(`[${id}]`)}opencli 插件`,
2884
+ en: `the workbench [${id}] opencli plugin`
2885
+ };
2886
+ }
2887
+ function workbenchDependency(id) {
2888
+ return missing(id) ? {
2889
+ zh: "工作台共用依赖",
2890
+ en: "a workbench shared dependency"
2891
+ } : {
2892
+ zh: `工作台共用依赖 [${id}]`,
2893
+ en: `the workbench shared dependency [${id}]`
2894
+ };
2895
+ }
2896
+ function workbenchPanelRow(id) {
2897
+ return missing(id) ? {
2898
+ zh: "工作台在当前项目的面板",
2899
+ en: "the workbench panel for this project"
2900
+ } : {
2901
+ zh: `工作台${pad(`[${id}]`)}模块这一行`,
2902
+ en: `the workbench [${id}] module row`
2903
+ };
2904
+ }
2852
2905
  function actionOf$1(call) {
2853
2906
  const raw = call.arguments;
2854
2907
  if (typeof raw !== "object" || raw === null) return {};
2855
2908
  const value = raw;
2856
2909
  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;
2910
+ const spec = typeof value.spec === "string" ? value.spec.replace(/\s+/g, " ").trim() : "";
2857
2911
  return {
2858
2912
  ...typeof value.action === "string" ? { action: value.action } : {},
2859
- ...target === void 0 ? {} : { id: target }
2913
+ ...target === void 0 ? {} : { id: target },
2914
+ ...spec === "" ? {} : { spec: spec.length > SPEC_LIMIT ? `${spec.slice(0, SPEC_LIMIT)}…` : spec }
2860
2915
  };
2861
2916
  }
2862
- function whatOf(id) {
2863
- if (id === void 0 || id === "") return "一个模块";
2864
- return `「${id}」`;
2865
- }
2866
2917
  function askOf(reason, question, fallback) {
2867
2918
  return {
2868
2919
  kind: "ask",
@@ -2872,92 +2923,151 @@ function askOf(reason, question, fallback) {
2872
2923
  };
2873
2924
  }
2874
2925
  var panelsApproval = (call) => {
2875
- const { action, id } = actionOf$1(call);
2926
+ const { action, id, spec } = actionOf$1(call);
2876
2927
  const readAction = action !== void 0 && action !== "" && READ_ONLY_ACTIONS.has(action);
2877
- const what = whatOf(id);
2878
2928
  if (call.name === "workbench_modules") {
2879
2929
  if (readAction) return READ;
2880
2930
  if (action === "update-plugin" || action === "uninstall-plugin") {
2881
2931
  const verb = action === "update-plugin" ? "更新" : "卸载";
2882
- const verbEn = action === "update-plugin" ? "update" : "remove";
2932
+ const verbEn = action === "update-plugin" ? "update" : "uninstall";
2933
+ const target = workbenchPlugin(id);
2883
2934
  return askOf(`workbench_modules ${action} ${id ?? ""} changes an opencli plugin on this machine`, {
2884
- en: `it ${verbEn}s that opencli plugin on this machine. Allow?`,
2885
- zh: `会${verb}这个 opencli 插件。允许吗?`
2935
+ en: `it ${verbEn}s ${target.en}. Allow?`,
2936
+ zh: `会${verb}${target.zh},允许吗?`
2886
2937
  }, {
2887
- en: `The agent wants to ${verbEn} the opencli plugin ${what} (it changes what is installed on this machine). Allow?`,
2888
- zh: `agent 想${verb} opencli 插件 ${what}(会改这台机器上装的东西)。允许吗?`
2938
+ en: `The agent wants to ${verbEn} ${target.en}. Allow?`,
2939
+ zh: `agent 想${verb}${target.zh},允许吗?`
2889
2940
  });
2890
2941
  }
2891
2942
  if (action === "update-workbench") return askOf("workbench_modules update-workbench replaces the workbench itself with the new version from the registry", {
2892
- en: "it updates the workbench itself on this machine (the panel picks the new version up by itself). Allow?",
2893
- zh: "会把工作台自己更新到源上的新版(面板会自己换新)。允许吗?"
2943
+ en: "it updates the workbench itself to the new version from the registry. Allow?",
2944
+ zh: "会把工作台本身更新到源上的新版,允许吗?"
2894
2945
  }, {
2895
- en: "The agent wants to update the workbench itself to the new version from the registry (it changes what is installed on this machine; the panel picks the new version up by itself). Allow?",
2896
- zh: "agent 想把工作台自己更新到源上的新版(会改这台机器上装的东西,面板会自己换新)。允许吗?"
2946
+ en: "The agent wants to update the workbench itself to the new version from the registry. Allow?",
2947
+ zh: "agent 想把工作台本身更新到源上的新版,允许吗?"
2897
2948
  });
2898
- if (action === "update-dependency") return askOf(`workbench_modules update-dependency ${id ?? ""} upgrades a shared dependency on this machine`, {
2899
- en: "it upgrades that shared dependency on this machine. Allow?",
2900
- zh: `会升级共用依赖 ${what}。允许吗?`
2949
+ if (action === "update-dependency") {
2950
+ const target = workbenchDependency(id);
2951
+ return askOf(`workbench_modules update-dependency ${id ?? ""} upgrades a shared dependency on this machine`, {
2952
+ en: `it upgrades ${target.en}. Allow?`,
2953
+ zh: `会升级${target.zh},允许吗?`
2954
+ }, {
2955
+ en: `The agent wants to upgrade ${target.en}. Allow?`,
2956
+ zh: `agent 想升级${target.zh},允许吗?`
2957
+ });
2958
+ }
2959
+ if (action === "install-bundle" && spec !== void 0) return askOf(`workbench_modules install-bundle ${spec} installs a workbench module from a spec`, {
2960
+ en: `it installs the workbench module from [${spec}]. Allow?`,
2961
+ zh: `会安装工作台模块 [${spec}],允许吗?`
2901
2962
  }, {
2902
- en: `The agent wants to upgrade the shared dependency ${what} (it changes what is installed on this machine). Allow?`,
2903
- zh: `agent 想升级共用依赖 ${what}(会改这台机器上装的东西)。允许吗?`
2963
+ en: `The agent wants to install the workbench module from [${spec}]. Allow?`,
2964
+ zh: `agent 想安装工作台模块 [${spec}],允许吗?`
2904
2965
  });
2905
- const verb = action === "install" ? "装" : action === "uninstall" ? "卸载" : action === "update" ? "更新" : "装 / 更新 / 卸载";
2906
- const verbEn = verb === "装" ? "install" : verb === "卸载" ? "remove" : verb === "更新" ? "update" : "install/update/remove";
2966
+ if (action === "install") {
2967
+ const target = workbenchModule(id);
2968
+ return askOf(`workbench_modules install ${id ?? ""} installs a workbench module on this machine`, {
2969
+ en: `it installs ${target.en}. Allow?`,
2970
+ zh: `会安装${target.zh},允许吗?`
2971
+ }, {
2972
+ en: `The agent wants to install ${target.en}. Allow?`,
2973
+ zh: `agent 想安装${target.zh},允许吗?`
2974
+ });
2975
+ }
2976
+ if (action === "update") {
2977
+ const target = workbenchModule(id);
2978
+ return askOf(`workbench_modules update ${id ?? ""} updates a workbench module on this machine`, {
2979
+ en: `it updates ${target.en}. Allow?`,
2980
+ zh: `会更新${target.zh},允许吗?`
2981
+ }, {
2982
+ en: `The agent wants to update ${target.en}. Allow?`,
2983
+ zh: `agent 想更新${target.zh},允许吗?`
2984
+ });
2985
+ }
2986
+ if (action === "uninstall") {
2987
+ const target = workbenchModule(id);
2988
+ return askOf(`workbench_modules uninstall ${id ?? ""} removes a workbench module from this machine`, {
2989
+ en: `it uninstalls ${target.en}. Allow?`,
2990
+ zh: `会卸载${target.zh},允许吗?`
2991
+ }, {
2992
+ en: `The agent wants to uninstall ${target.en}. Allow?`,
2993
+ zh: `agent 想卸载${target.zh},允许吗?`
2994
+ });
2995
+ }
2907
2996
  return askOf(`workbench_modules ${action ?? "(unknown action)"} ${id ?? ""} changes plugins on this machine`.trim(), {
2908
- en: "it changes plugins on this machine. Allow?",
2909
- zh: "会改这台机器上的插件。允许吗?"
2997
+ en: "it changes the plugins installed in the workbench. Allow?",
2998
+ zh: "会改动工作台装的插件,允许吗?"
2910
2999
  }, {
2911
- en: `The agent wants to ${verbEn} the module ${what} (it changes plugins on this machine). Allow?`,
2912
- zh: `agent 想${verb}模块 ${what}(会改这台机器上的插件)。允许吗?`
3000
+ en: "The agent wants to change the plugins installed in the workbench. Allow?",
3001
+ zh: "agent 想改动工作台装的插件,允许吗?"
2913
3002
  });
2914
3003
  }
2915
3004
  if (call.name === "workbench_diy") {
2916
3005
  if (readAction) return READ;
2917
- if (action === "open") return askOf("workbench_diy open puts an already-installed module on this project panel", {
2918
- en: `it puts ${what} on this project's panel (nothing new gets installed on this machine). Allow?`,
2919
- zh: `会把${what}摆到**这个项目**的面板上(这台机器上不装新东西)。允许吗?`
2920
- }, {
2921
- en: `The agent wants to show the module ${what} on this project's panel. Allow?`,
2922
- zh: `agent 想把${what}显示在**这个项目**的面板上。允许吗?`
2923
- });
3006
+ if (action === "open") {
3007
+ const target = anyWorkbenchModule(id);
3008
+ return askOf("workbench_diy open puts an already-installed module on this project panel", {
3009
+ en: `it shows ${target.en} on this project's panel. Allow?`,
3010
+ zh: `会把${target.zh}显示在当前项目的面板上,允许吗?`
3011
+ }, {
3012
+ en: `The agent wants to show ${target.en} on this project's panel. Allow?`,
3013
+ zh: `agent 想把${target.zh}显示在当前项目的面板上,允许吗?`
3014
+ });
3015
+ }
3016
+ const target = workbenchModule(id);
2924
3017
  return askOf(`workbench_diy ${action ?? "(unknown action)"} installs a module built in the workshop and opens it on this project's panel`, {
2925
- en: `it installs ${what} on this machine and opens it on this project's panel. Allow?`,
2926
- zh: `会把${what}装到这台机器上,并在**这个项目**的面板上打开。允许吗?`
3018
+ en: `it installs ${target.en} and shows it on this project's panel. Allow?`,
3019
+ zh: `会安装${target.zh},并显示在当前项目的面板上,允许吗?`
2927
3020
  }, {
2928
- en: `The agent wants to install the module ${what} built in the DIY workshop (it changes what is installed on this machine) and open it here. Allow?`,
2929
- zh: `agent 想把 DIY 工坊里造好的${what}装上(会改这台机器上装的东西),并在**这个项目**的面板上打开。允许吗?`
3021
+ en: `The agent wants to install ${target.en} built in the DIY workshop and show it on this project's panel. Allow?`,
3022
+ zh: `agent 想安装${target.zh},并显示在当前项目的面板上,允许吗?`
2930
3023
  });
2931
3024
  }
2932
3025
  if (call.name !== "workbench_panels") return void 0;
2933
3026
  if (readAction) return READ;
2934
- if (action === "hide") return askOf("workbench_panels hide removes a module from the panel", {
2935
- en: "it takes that module off this panel (its agent tools go offline here too). Allow?",
2936
- zh: `${what}会从这个项目的面板上收起来(这个项目里它的 agent 工具也一起下线)。允许吗?`
2937
- }, {
2938
- en: `The agent wants to remove ${what} from this project's panel (its agent tools go offline here too). Allow?`,
2939
- zh: `agent 想把 ${what} 从这个项目的面板上收起来(这个项目里它的 agent 工具也一起下线)。允许吗?`
2940
- });
2941
- if (action === "move") return askOf("workbench_panels move reorders the panel", {
2942
- en: `it reorders this panel (${what} goes first). Allow?`,
2943
- zh: `会调整这个项目面板的顺序(${what}放前面)。允许吗?`
2944
- }, {
2945
- en: `The agent wants to reorder this project's panel (put ${what} first). Allow?`,
2946
- zh: `agent 想调整这个项目面板的顺序(把 ${what} 放前面)。允许吗?`
2947
- });
3027
+ if (action === "hide") {
3028
+ const target = anyWorkbenchModule(id);
3029
+ return askOf("workbench_panels hide removes a module from the panel", {
3030
+ en: `it takes ${target.en} off this project's panel. Allow?`,
3031
+ zh: `会把${target.zh}从当前项目的面板上收起来,允许吗?`
3032
+ }, {
3033
+ en: `The agent wants to take ${target.en} off this project's panel. Allow?`,
3034
+ zh: `agent 想把${target.zh}从当前项目的面板上收起来,允许吗?`
3035
+ });
3036
+ }
3037
+ if (action === "move") {
3038
+ const target = anyWorkbenchModule(id);
3039
+ return askOf("workbench_panels move reorders the panel", {
3040
+ en: `it moves ${target.en} to the front of this project's panel. Allow?`,
3041
+ zh: `会把${target.zh}移到当前项目面板的最前面,允许吗?`
3042
+ }, {
3043
+ en: `The agent wants to move ${target.en} to the front of this project's panel. Allow?`,
3044
+ zh: `agent 想把${target.zh}移到当前项目面板的最前面,允许吗?`
3045
+ });
3046
+ }
2948
3047
  if (action === "reset") return askOf("workbench_panels reset clears the panel", {
2949
- en: "it clears this panel (every module goes offline here). Allow?",
2950
- zh: "会清空这个项目的面板(所有模块在这个项目里都下线)。允许吗?"
3048
+ en: `it clears the workbench panel for this project. Allow?`,
3049
+ zh: "会清空工作台在当前项目的面板,允许吗?"
2951
3050
  }, {
2952
- en: "The agent wants to clear this project's panel (every module goes offline here). Allow?",
2953
- zh: "agent 想清空这个项目的面板(所有模块在这个项目里都下线)。允许吗?"
3051
+ en: "The agent wants to clear the workbench panel for this project. Allow?",
3052
+ zh: "agent 想清空工作台在当前项目的面板,允许吗?"
2954
3053
  });
3054
+ if (action === "follow") {
3055
+ const target = workbenchPanelRow(id);
3056
+ return askOf("workbench_panels follow puts a row back to following the machine-level board", {
3057
+ en: `it puts ${target.en} back to following the machine-level board. Allow?`,
3058
+ zh: `会把${target.zh}改回跟随机器级默认板,允许吗?`
3059
+ }, {
3060
+ en: `The agent wants to put ${target.en} back to following the machine-level board. Allow?`,
3061
+ zh: `agent 想把${target.zh}改回跟随机器级默认板,允许吗?`
3062
+ });
3063
+ }
3064
+ const target = anyWorkbenchModule(id);
2955
3065
  return askOf(`workbench_panels ${action ?? "(unknown action)"} changes the user's panel`, {
2956
- en: `it adds ${what} to this project's panel. Allow?`,
2957
- zh: `会把 ${what} 加到**这个项目**的面板上。允许吗?`
3066
+ en: `it adds ${target.en} to this project's panel. Allow?`,
3067
+ zh: `会把${target.zh}加到当前项目的面板上,允许吗?`
2958
3068
  }, {
2959
- en: `The agent wants to add ${what} to this project's panel. Allow?`,
2960
- zh: `agent 想把 ${what} 加到**这个项目**的面板上。允许吗?`
3069
+ en: `The agent wants to add ${target.en} to this project's panel. Allow?`,
3070
+ zh: `agent 想把${target.zh}加到当前项目的面板上,允许吗?`
2961
3071
  });
2962
3072
  };
2963
3073
  //#endregion
@@ -3272,7 +3382,7 @@ function reconOf(commands, plugins, keywords) {
3272
3382
  };
3273
3383
  return {
3274
3384
  ...withNext,
3275
- text: textOf$2(withNext)
3385
+ text: textOf$3(withNext)
3276
3386
  };
3277
3387
  }
3278
3388
  function nextOf(answer) {
@@ -3285,7 +3395,7 @@ function nextOf(answer) {
3285
3395
  if (plugin !== void 0) return `这台机器另装的插件 ${plugin.name} 里有匹配:opencli ${plugin.name} --help 看全部命令`;
3286
3396
  return "这台机器上没有现成的 —— 两条路:① 给这个站点写 adapter:opencli skills read opencli-adapter-author;② 只读一次:workbench_diy action=probe(script 读接口 / css 读 DOM)";
3287
3397
  }
3288
- function textOf$2(answer) {
3398
+ function textOf$3(answer) {
3289
3399
  const lines = [];
3290
3400
  if (answer.sites.length > 0) {
3291
3401
  lines.push(`opencli 自带这 ${String(answer.sites.length)} 组能力(不用装、不用点头):`);
@@ -3586,26 +3696,26 @@ function registerDiyTools(ctx, market, panels) {
3586
3696
  }
3587
3697
  const tool = {
3588
3698
  name: "workbench_diy",
3589
- description: "看 DIY 工坊里的模块现在走到哪一步,或者把它们装上 / 上板。只认工坊里的模块(不是市场 —— 装别的东西用 workbench_modules)。status 会把六件事一次说清(装了没 / 面板认得没 / 这个项目上开着没 / **起得来没** / 产物齐不齐 / 源码是否比产物新)并给出下一步,所以别自己去拼这些状态、也别反复 install。panel 是**面板上现在究竟是什么样**:这个工作区开了哪几张卡、每张最后是读到了还是读不到(带宿主回的原文)、有没有崩过 —— 用户说\"卡片不对 / 一片空白 / 一直转圈\"就先问它,别靠猜、也别让他截图给你。recon 是**先看有没有现成的**:拿一个关键词(站点名 / 系统名 / 域名)去 opencli 自带的那份目录里找,回它有哪些命令、要不要用户的 Chrome 登录;都没有就老实说,并给出\"写 adapter\"或\"探一条读\"两条路。用户不知道 opencli 是什么 —— 这一步别问他,自己查。probe 是**探一条读**:用用户浏览器里现有的登录去读一次,把结果与失败码归一(两拍、重试、平台码映射都在里面),探明之后再把它固化成模块宿主半侧的那一条。两种读法二选一:script(页面里跑一段回 JSON 的脚本,读接口)或 css(给一个选择器,读 DOM 那一行 —— 页面里没有接口时用这个,别写 document.querySelectorAll)。装是这台机器上的持久变更,要用户点头;**一辈子只装一次**:之后改源码只需 dsh-wb-module build(宿主侧秒级热换;改了 client.js 刷新一下页面),不要再调它。",
3699
+ description: "DIY 工坊里的模块:看它走到哪一步,或把它装上 / 上板。只认工坊里造好的模块 —— 普通项目里装模块用 workbench_modules。status 是入口,照它给的\"下一步\"走。install 是这台机器上的持久变更,一个模块一辈子只装一次:之后改源码只需 dsh-wb-module build(宿主侧秒级热换;改了 client.js 刷新一下页面)。",
3590
3700
  parameters: withApprovalNote({
3591
3701
  type: "object",
3592
3702
  properties: {
3593
3703
  action: {
3594
3704
  type: "string",
3595
- description: "list=工坊里有哪些模块(默认);status=某个模块现在走到哪一步、下一步做什么;panel=这个工作区的面板上现在是什么样(谁读到了 / 谁读不到 / 谁崩过);probe=在用户浏览器里探一条读;recon=先查 opencli 现成的能力(自带 adapter / 本机插件);open=只把它在这个项目上板(不装);install=装上并上板(第一次、会让用户点头)。",
3705
+ description: "list=工坊里有哪些模块(默认);status=某个模块走到哪一步、下一步做什么;panel=面板此刻的真实样子(宿主回的原文都在)—— 用户说\"卡片不对 / 一片空白 / 一直转圈\"就先调它;recon=先查 opencli 有没有现成的(自带 adapter / 本机插件)—— opencli 是什么自己查,别问用户;probe=用用户浏览器里现有的登录探一条读(script 或 css);open=只上板(不装);install=装上并上板(第一次,会让用户点头)。",
3596
3706
  enum: [...ACTIONS$2]
3597
3707
  },
3598
3708
  unit: {
3599
3709
  type: "string",
3600
- description: "status / panel / open / install 填:工坊里那个模块的单元 id(list 会给)。"
3710
+ description: "status / panel / open / install 填:单元 id(list 会给)。"
3601
3711
  },
3602
3712
  script: {
3603
3713
  type: "string",
3604
- description: "probe 要(与 css 二选一):在页面里跑的脚本(必须回 JSON)。不在站点就回 { ok: false, code: \"not-on-site\" };登录过期回 { ok: false, code: \"not-logged-in\" };成功回 { ok: true, ...数据 }。"
3714
+ description: "probe 要(与 css 二选一):读接口用这个 —— 在页面里跑一段回 JSON 的脚本。不在站点就回 { ok: false, code: \"not-on-site\" };登录过期回 { ok: false, code: \"not-logged-in\" };成功回 { ok: true, ...数据 }。"
3605
3715
  },
3606
3716
  css: {
3607
3717
  type: "string",
3608
- description: "probe 要(与 script 二选一):一个 CSS 选择器,回页面上匹配到的元素(每行的单元格按换行拼在 text 里)。页面里没有接口时用它 —— 走的是一条固定的 DOM 读(找不到会自己打开 url 再读一遍),别写 document.querySelectorAll。"
3718
+ description: "probe 要(与 script 二选一):读 DOM 用这个 —— 给一个 CSS 选择器,宿主回匹配到的元素文本(每行的单元格按换行拼在 text 里);不在站点会自己打开 url 再读一遍。"
3609
3719
  },
3610
3720
  limit: {
3611
3721
  type: "number",
@@ -3617,11 +3727,11 @@ function registerDiyTools(ctx, market, panels) {
3617
3727
  },
3618
3728
  url: {
3619
3729
  type: "string",
3620
- description: "probe / recon 用:那套系统的地址(probe 不在站点上时自己打开它再读一遍;recon 拿它取域名去找现成的 adapter)。"
3730
+ description: "probe / recon 用:那套系统的地址(probe 不在站点上时自己打开它再读一遍;recon 从它取域名)。"
3621
3731
  },
3622
3732
  keyword: {
3623
3733
  type: "string",
3624
- description: "recon 要:站点名 / 系统名 / 关键词(英文名或域名最准,例:bilibili、arxiv.org、cmdb)。用户说的是\"我想看 X\",把它翻译成关键词是你的活 —— 别问他用哪个插件。"
3734
+ description: "recon 要:站点名 / 系统名 / 域名(英文名或域名最准,例:bilibili、arxiv.org、cmdb)。"
3625
3735
  },
3626
3736
  loginUrl: {
3627
3737
  type: "string",
@@ -3629,7 +3739,7 @@ function registerDiyTools(ctx, market, panels) {
3629
3739
  },
3630
3740
  session: {
3631
3741
  type: "string",
3632
- description: "probe 用:浏览器桥上的会话名(默认 workbench,一般不用填)。"
3742
+ description: "probe 用:会话名(默认 workbench,一般不用填)。"
3633
3743
  },
3634
3744
  open: {
3635
3745
  type: "boolean",
@@ -4482,6 +4592,330 @@ async function installRouted(deps) {
4482
4592
  });
4483
4593
  }
4484
4594
  //#endregion
4595
+ //#region src/shared/api/host/runtime-seed.ts
4596
+ var WORKSHOP_SKILLS_DIR = "workbench-module-diy";
4597
+ function runtimeSeedPaths(root) {
4598
+ const opencliRoot = join(root, "opencli");
4599
+ const opencliBinDir = join(opencliRoot, "bin");
4600
+ const workshop = join(root, "diy");
4601
+ return {
4602
+ root,
4603
+ entryDir: join(root, "bin"),
4604
+ opencliRoot,
4605
+ opencliBinDir,
4606
+ opencliBin: join(opencliBinDir, "opencli"),
4607
+ opencliHome: join(opencliRoot, "home"),
4608
+ workshop,
4609
+ workshopPackages: join(workshop, "packages"),
4610
+ workshopManifest: join(workshop, "package.json"),
4611
+ skillsLink: join(workshop, ".agents", "skills", WORKSHOP_SKILLS_DIR)
4612
+ };
4613
+ }
4614
+ function runtimeSeedRootOf(ctx) {
4615
+ const homePath = ctx.get("dshHomePath");
4616
+ if (typeof homePath !== "function") return null;
4617
+ try {
4618
+ return homePath("workbench");
4619
+ } catch {
4620
+ return null;
4621
+ }
4622
+ }
4623
+ function packageRootOf(moduleUrl) {
4624
+ return dirname(dirname(dirname(fileURLToPath(moduleUrl))));
4625
+ }
4626
+ function isInstalledCopy(packageRoot) {
4627
+ return packageRoot.split(sep).includes("node_modules");
4628
+ }
4629
+ function opencliWrapperScript(packageRoot, home) {
4630
+ const script = join(packageRoot, "bin", "opencli-isolated");
4631
+ return `#!/bin/sh\nexec env DSH_HOME=${quoteShellArg(home)} bash ${quoteShellArg(script)} "$@"\n`;
4632
+ }
4633
+ function workshopSeedPlan(packageRoot, home, root) {
4634
+ const script = join(packageRoot, "scripts", "seed-workshop.sh");
4635
+ const env = { DSH_HOME: home };
4636
+ const log = join(root, "seed.log");
4637
+ return {
4638
+ entry: {
4639
+ file: "bash",
4640
+ args: [script, "entry"],
4641
+ env,
4642
+ log
4643
+ },
4644
+ full: {
4645
+ file: "bash",
4646
+ args: [script],
4647
+ env,
4648
+ log
4649
+ }
4650
+ };
4651
+ }
4652
+ function runDetached(command, onError, spawnImpl = spawn) {
4653
+ let fd = null;
4654
+ try {
4655
+ fd = openSync(command.log, "a");
4656
+ const child = spawnImpl(command.file, [...command.args], {
4657
+ env: {
4658
+ ...process.env,
4659
+ ...command.env
4660
+ },
4661
+ detached: true,
4662
+ stdio: [
4663
+ "ignore",
4664
+ fd,
4665
+ fd
4666
+ ]
4667
+ });
4668
+ child.on("error", (error) => onError(`自举脚本没跑起来(${command.file}):${String(error)}`));
4669
+ child.unref();
4670
+ } catch (error) {
4671
+ onError(`自举脚本没跑起来(${command.file}):${String(error)}`);
4672
+ } finally {
4673
+ if (fd !== null) closeQuietly(fd);
4674
+ }
4675
+ }
4676
+ function closeQuietly(fd) {
4677
+ try {
4678
+ closeSync(fd);
4679
+ } catch {
4680
+ return;
4681
+ }
4682
+ }
4683
+ function prependPath(entryDir, env = process.env) {
4684
+ const parts = (env.PATH ?? "").split(delimiter).filter((part) => part !== "");
4685
+ if (parts.includes(entryDir)) return false;
4686
+ env.PATH = [entryDir, ...parts].join(delimiter);
4687
+ return true;
4688
+ }
4689
+ function seedRuntimeFiles(options) {
4690
+ const paths = runtimeSeedPaths(options.root);
4691
+ const done = [];
4692
+ try {
4693
+ for (const dir of [
4694
+ paths.entryDir,
4695
+ paths.opencliBinDir,
4696
+ paths.opencliHome,
4697
+ paths.workshopPackages
4698
+ ]) mkdirSync(dir, { recursive: true });
4699
+ const wanted = opencliWrapperScript(options.packageRoot, options.home);
4700
+ if (readText(paths.opencliBin) !== wanted) {
4701
+ rmSync(paths.opencliBin, { force: true });
4702
+ writeFileSync(paths.opencliBin, wanted, "utf8");
4703
+ chmodSync(paths.opencliBin, 493);
4704
+ done.push(paths.opencliBin);
4705
+ }
4706
+ if (linkSkills(paths.skillsLink, join(options.packageRoot, "skills", "workbench-module-diy"), options.log)) done.push(paths.skillsLink);
4707
+ } catch (error) {
4708
+ options.log?.(`自举没做完:${String(error)}`);
4709
+ }
4710
+ return done;
4711
+ }
4712
+ function readText(file) {
4713
+ try {
4714
+ return readFileSync(file, "utf8");
4715
+ } catch {
4716
+ return null;
4717
+ }
4718
+ }
4719
+ function readLink(link) {
4720
+ try {
4721
+ return readlinkSync(link);
4722
+ } catch {
4723
+ return null;
4724
+ }
4725
+ }
4726
+ function linkSkills(link, source, log) {
4727
+ if (!existsSync(source) || readLink(link) === source) return false;
4728
+ try {
4729
+ mkdirSync(dirname(link), { recursive: true });
4730
+ rmSync(link, {
4731
+ force: true,
4732
+ recursive: true
4733
+ });
4734
+ symlinkSync(source, link, "dir");
4735
+ return true;
4736
+ } catch (error) {
4737
+ log?.(`DIY 指引没挂上(${link}):${String(error)}`);
4738
+ return false;
4739
+ }
4740
+ }
4741
+ //#endregion
4742
+ //#region src/shared/api/host/self-update.ts
4743
+ function runningVersionOf(moduleUrl) {
4744
+ try {
4745
+ const read = JSON.parse(readFileSync(new URL("../../package.json", moduleUrl), "utf8"));
4746
+ return typeof read.version === "string" && read.version !== "" ? read.version : void 0;
4747
+ } catch {
4748
+ return;
4749
+ }
4750
+ }
4751
+ function selfUpdatePaths(dshHome) {
4752
+ const dir = join(dshHome, "workbench");
4753
+ return {
4754
+ dir,
4755
+ script: join(dir, "self-update.sh"),
4756
+ marker: join(dir, "self-update.json"),
4757
+ log: join(dir, "self-update.log")
4758
+ };
4759
+ }
4760
+ function recoveryCommand(input) {
4761
+ return `dsh plugin --profile ${input.profile} add ${input.pkg}@${input.version}`;
4762
+ }
4763
+ function selfUpdateMarker(request, state, version, at) {
4764
+ return {
4765
+ ...request,
4766
+ state,
4767
+ version,
4768
+ at
4769
+ };
4770
+ }
4771
+ function quoted(value) {
4772
+ return `'${value.replaceAll("'", "'\\''")}'`;
4773
+ }
4774
+ function selfUpdateScript(request, paths) {
4775
+ const rescue = recoveryCommand({
4776
+ profile: request.profile,
4777
+ pkg: request.pkg,
4778
+ version: request.from
4779
+ });
4780
+ const lastsOne = recoveryCommand({
4781
+ profile: request.profile,
4782
+ pkg: request.pkg,
4783
+ version: request.to === "" ? "latest" : request.to
4784
+ });
4785
+ const target = request.to === "" ? "latest" : request.to;
4786
+ const landed = request.to === "" ? "is_other" : "is_to";
4787
+ return `#!/usr/bin/env bash
4788
+ set -uo pipefail
4789
+ DSH_HOME=${quoted(request.dshHome)}
4790
+ PKG=${quoted(request.pkg)}
4791
+ PROFILE=${quoted(request.profile)}
4792
+ FROM=${quoted(request.from)}
4793
+ TO=${quoted(target)}
4794
+ MARKER=${quoted(paths.marker)}
4795
+ LOG=${quoted(paths.log)}
4796
+ PROFILE_DIR="$DSH_HOME/profiles/$PROFILE"
4797
+ say() { printf '%s %s\\n' "$(date '+%F %T')" "$*" >>"$LOG"; }
4798
+ on_disk() {
4799
+ node -e 'const p=process.argv[1];try{process.stdout.write(require(p).version)}catch(e){}' \\
4800
+ "$PROFILE_DIR/node_modules/$PKG/package.json" 2>/dev/null
4801
+ }
4802
+ write_marker() {
4803
+ printf '{"pkg":"%s","profile":"%s","from":"%s","to":"%s","state":"%s","version":"%s","at":"%s"}\\n' \\
4804
+ "$PKG" "$PROFILE" "$FROM" "$TO" "$1" "$2" "$(date -u '+%FT%TZ')" >"$MARKER"
4805
+ }
4806
+ install() { dsh plugin --profile "$PROFILE" add "$PKG@$1" >>"$LOG" 2>&1; }
4807
+ is_to() { [ "$(on_disk)" = "$TO" ]; }
4808
+ is_other() { [ -n "$(on_disk)" ] && [ "$(on_disk)" != "$FROM" ]; }
4809
+ if ! command -v dsh >/dev/null 2>&1; then
4810
+ say "PATH 上没有 dsh —— 这一跳没跑。救法:${rescue}"
4811
+ write_marker failed "$FROM"
4812
+ exit 1
4813
+ fi
4814
+ say "开始 \${FROM} -> \${TO}(不卸任何一行)"
4815
+ write_marker running "$FROM"
4816
+ if install "$TO" && ${landed}; then
4817
+ say "换好了:$(on_disk) 在盘上(目标 \${TO}),重启一次 dsh 才用得上"
4818
+ write_marker ok "$(on_disk)"
4819
+ exit 0
4820
+ fi
4821
+ say "换版没成(盘上 $(on_disk))—— 把 \${FROM} 装回去"
4822
+ if install "$FROM" && [ "$(on_disk)" = "$FROM" ]; then
4823
+ say "已回滚:\${FROM} 还在盘上,工作台这一行没被动过"
4824
+ write_marker rolled-back "$FROM"
4825
+ exit 0
4826
+ fi
4827
+ say "回滚也没成。救法:${rescue}(或 ${lastsOne})"
4828
+ write_marker failed "$(on_disk)"
4829
+ exit 1
4830
+ `;
4831
+ }
4832
+ function planSelfUpdate(request, now = /* @__PURE__ */ new Date()) {
4833
+ const paths = selfUpdatePaths(request.dshHome);
4834
+ return {
4835
+ paths,
4836
+ script: selfUpdateScript(request, paths),
4837
+ marker: selfUpdateMarker(request, "running", request.from, now.toISOString()),
4838
+ command: {
4839
+ file: "bash",
4840
+ args: [paths.script],
4841
+ env: {},
4842
+ log: paths.log
4843
+ }
4844
+ };
4845
+ }
4846
+ function startSelfUpdate(request, onError, spawnImpl = spawn) {
4847
+ const plan = planSelfUpdate(request);
4848
+ mkdirSync(plan.paths.dir, { recursive: true });
4849
+ writeFileSync(plan.paths.script, plan.script, { mode: 493 });
4850
+ chmodSync(plan.paths.script, 493);
4851
+ writeFileSync(plan.paths.marker, `${JSON.stringify(plan.marker)}\n`);
4852
+ runDetached(plan.command, onError, spawnImpl);
4853
+ const forward = recoveryCommand({
4854
+ profile: request.profile,
4855
+ pkg: request.pkg,
4856
+ version: request.to === "" ? "latest" : request.to
4857
+ });
4858
+ return {
4859
+ started: true,
4860
+ paths: plan.paths,
4861
+ forward
4862
+ };
4863
+ }
4864
+ //#endregion
4865
+ //#region src/modules/panels/model/market-workbench-update.ts
4866
+ async function updateWorkbenchOnce(deps) {
4867
+ const asked = deps.asked.trim();
4868
+ const target = asked === "" ? PLUGIN_ID : asked;
4869
+ if (target !== "@dsh-workbench/core") throw new Error(`这条路只更新工作台自己那一份(${PLUGIN_ID}):${target}`);
4870
+ const read = deps.workbench?.(target);
4871
+ if (read === void 0) throw new Error(`读不到 profile 里装的 ${target} —— 没法判断装的是哪一份,更新做不了。`);
4872
+ if (read.local === true) throw new Error("这份工作台是从本仓装进来的(开发副本),不从源上更新。");
4873
+ const shell = deps.shell;
4874
+ const registry = read.registry ?? await deps.profileRegistry?.();
4875
+ const askedLatest = shell === void 0 || registry === void 0 ? void 0 : await latestVersion({
4876
+ shell,
4877
+ name: read.pkg,
4878
+ registry,
4879
+ force: true
4880
+ });
4881
+ if (shell !== void 0 && registry !== void 0 && askedLatest !== void 0) {
4882
+ const missing = await missingReleaseParts({
4883
+ shell,
4884
+ registry,
4885
+ version: askedLatest,
4886
+ force: true
4887
+ });
4888
+ if (missing.length > 0) throw new Error(`源上已经有 ${read.pkg}@${askedLatest},但这一份发布还没铺齐:${missing.join("、")} 在这一版上还拉不到。装上会缺依赖,稍等再试 —— 市场页那颗「重新检查」会重新探一次。`);
4889
+ }
4890
+ const home = deps.dshHome?.();
4891
+ const profile = deps.profileName?.();
4892
+ if (home === void 0 || profile === void 0 || profile === null) throw new Error("拿不到 DSH_HOME 或 profile 名字 —— 自更新要多一个进程在外头跑,这一步做不了。");
4893
+ const spec = askedLatest === void 0 ? `${read.pkg}@latest` : `${read.pkg}@${askedLatest}`;
4894
+ const started = (deps.selfUpdate ?? startSelfUpdate)({
4895
+ dshHome: home,
4896
+ profile,
4897
+ pkg: read.pkg,
4898
+ from: read.version,
4899
+ to: askedLatest ?? "",
4900
+ pinned: askedLatest !== void 0
4901
+ }, (message) => deps.logger?.warn?.(`[dsh-workbench] ${message}`));
4902
+ deps.logger?.info?.(`[dsh-workbench] 自更新交给外部执行器(${started.paths.script}):${read.version} → ${askedLatest ?? "最新版"},这一跳不掉自己那一行`);
4903
+ return {
4904
+ change: {
4905
+ application: "restart-required",
4906
+ stage: "install",
4907
+ changed: false,
4908
+ target: spec,
4909
+ bundle: read.pkg
4910
+ },
4911
+ module: deps.installed?.(read.pkg) ?? {
4912
+ kind: "unknown",
4913
+ pkg: read.pkg
4914
+ },
4915
+ supply: { state: "no-needs" }
4916
+ };
4917
+ }
4918
+ //#endregion
4485
4919
  //#region src/modules/panels/model/plugin-actions.ts
4486
4920
  function pluginKeptOf(retention, detail) {
4487
4921
  if (retention === "keep-held") return { kind: "held" };
@@ -4722,7 +5156,7 @@ async function retryAfterRegistryFix(deps, manager, pkg, change) {
4722
5156
  //#endregion
4723
5157
  //#region src/modules/panels/model/market.ts
4724
5158
  function createMarketService(options) {
4725
- const { manifests, manifestsNow, probe, shell, logger, onInstalled, onUninstalled, isReleased, bundles, installed, modules, workbench, registries, ledger, references } = options;
5159
+ const { manifests, manifestsNow, probe, shell, logger, onInstalled, onUninstalled, isReleased, bundles, installed, modules, workbench, registries, ledger, references, dshHome, profileName } = options;
4726
5160
  const command = options.command ?? "opencli";
4727
5161
  const catalog = () => manifestsNow?.() ?? manifests;
4728
5162
  function requireEntry(unit) {
@@ -4874,7 +5308,11 @@ function createMarketService(options) {
4874
5308
  async function removeInstalledBundle(manager, pkg) {
4875
5309
  const first = await manager.remove(pkg);
4876
5310
  if (first === null) throw new Error("插件管理器回的卸载结果看不懂。");
4877
- return retryAfterRegistryFix(registryFix, manager, pkg, first);
5311
+ const settled = await retryAfterRegistryFix(registryFix, manager, pkg, first);
5312
+ if (settled.application !== "failed" && settled.application !== "cancelled") return settled;
5313
+ const put = await manager.setEnabled?.(pkg, true);
5314
+ logger?.warn?.(`[dsh-workbench] ${pkg} 没卸成(${settled.errorCode ?? settled.application})—— 已把它那一行放回名单,别让一次失败留在"装着但关着"`);
5315
+ return put ?? settled;
4878
5316
  }
4879
5317
  function routeDeps(manager) {
4880
5318
  return {
@@ -5065,31 +5503,23 @@ function createMarketService(options) {
5065
5503
  return outcome;
5066
5504
  },
5067
5505
  async updateWorkbench(pkg) {
5068
- const target = pkg.trim() === "" ? PLUGIN_ID : pkg.trim();
5069
- if (target !== "@dsh-workbench/core") throw new Error(`这条路只更新工作台自己那一份(${PLUGIN_ID}):${target}`);
5070
- const read = workbench?.(target);
5071
- if (read === void 0) throw new Error(`读不到 profile 里装的 ${target} —— 没法判断装的是哪一份,更新做不了。`);
5072
- if (read.local === true) throw new Error("这份工作台是从本仓装进来的(开发副本),不从源上更新。");
5073
- const registry = read.registry;
5074
- const latest = shell === void 0 || registry === void 0 ? void 0 : await latestVersion({
5075
- shell,
5076
- name: read.pkg,
5077
- registry,
5078
- force: true
5506
+ return updateWorkbenchOnce({
5507
+ asked: pkg,
5508
+ ...workbench === void 0 ? {} : { workbench },
5509
+ ...shell === void 0 ? {} : { shell },
5510
+ ...installed === void 0 ? {} : { installed },
5511
+ ...dshHome === void 0 ? {} : { dshHome },
5512
+ ...profileName === void 0 ? {} : { profileName },
5513
+ profileRegistry: async () => {
5514
+ try {
5515
+ return (await requireBundles().registries())?.resolved ?? void 0;
5516
+ } catch {
5517
+ return;
5518
+ }
5519
+ },
5520
+ ...options.selfUpdate === void 0 ? {} : { selfUpdate: options.selfUpdate },
5521
+ ...logger === void 0 ? {} : { logger }
5079
5522
  });
5080
- if (shell !== void 0 && registry !== void 0 && latest !== void 0) {
5081
- const missing = await missingReleaseParts({
5082
- shell,
5083
- registry,
5084
- version: latest,
5085
- force: true
5086
- });
5087
- if (missing.length > 0) throw new Error(`源上已经有 ${read.pkg}@${latest},但这一份发布还没铺齐:${missing.join("、")} 在这一版上还拉不到。装上会缺依赖,稍等再试 —— 市场页那颗「重新检查」会重新探一次。`);
5088
- }
5089
- const spec = latest === void 0 ? `${read.pkg}@latest` : `${read.pkg}@${latest}`;
5090
- const outcome = await replaceDshBundle(routeDeps(requireBundles()), spec, { ...registry === void 0 ? {} : { registry } });
5091
- logger?.info?.(`[dsh-workbench] 已更新工作台 ${read.version} → ${latest ?? "最新版"}`);
5092
- return outcome;
5093
5523
  },
5094
5524
  async updatePlugin(pkg) {
5095
5525
  const outcome = await plugins.update(pkg);
@@ -5293,30 +5723,30 @@ function registerMarketTools(ctx, market) {
5293
5723
  }
5294
5724
  const tool = {
5295
5725
  name: "workbench_modules",
5296
- description: "看 / 装 / 升 / 卸工作台的面板模块,以及用户自己装的 opencli 插件。install-bundle 按 spec(包名,可带版本;git / tarball / 本地路径也行)把模块包装进 profile —— 装完会连带装它声明的 opencli 插件;其余 action 按模块 id 动它要的外部供给;update-workbench 把工作台自己更新到源上的新版(客户端与宿主一般会自己换上;真要重启时结果里会说,重启由用户来,别自己重启)。卸载只动工作台运行时,不影响终端那套;共用依赖只有升级。",
5726
+ description: "看 / 装 / 升 / 卸工作台的面板模块,以及用户自己装的 opencli 插件。install-bundle 按 spec 把模块包装进 profile;按模块 id 的 install / update 只装、升它声明的那份外部供给(opencli 插件与库),不换模块包本身;uninstall 连模块包一起卸。update-workbench 把工作台自己更新到源上的新版(一般自己换上,要重启时结果里会说;重启由用户来)。卸模块只动工作台运行时,不影响终端那套 dsh;共用依赖只有升级。",
5297
5727
  parameters: withApprovalNote({
5298
5728
  type: "object",
5299
5729
  properties: {
5300
5730
  action: {
5301
5731
  type: "string",
5302
- description: "install-bundle=按 spec 装模块包(要 spec),update-dependency=升级共用依赖(要 id),update-plugin / uninstall-plugin=动用户自己装的 opencli 插件(要 plugin),update-workbench=把工作台自己更新到新版(不用别的参数),其余=对模块(要 id)。",
5732
+ description: "list=看状态(默认);install / update / uninstall 要 id;install-bundle 要 spec(换源给 registry);update-dependency 要 id;update-plugin / uninstall-plugin 要 plugin。",
5303
5733
  enum: [...ACTIONS$1]
5304
5734
  },
5305
5735
  id: {
5306
5736
  type: "string",
5307
- description: "模块 id(如 coding-issues);update-dependency 填共用依赖 id(如 plugin:coding)。"
5737
+ description: "模块 id(如 coding-issues);update-dependency 用共用依赖 id(如 plugin:coding)。"
5308
5738
  },
5309
5739
  spec: {
5310
5740
  type: "string",
5311
- description: "install-bundle 用:包名(可带 @版本),也可以是 git 地址 / tarball / 本地绝对路径。"
5741
+ description: "包名(可带 @版本),也可以是 git 地址 / tarball / 本地绝对路径。"
5312
5742
  },
5313
5743
  registry: {
5314
5744
  type: "string",
5315
- description: "install-bundle 用:先问哪个源(不填就用这个 profile 自己配的源)。"
5745
+ description: "去哪个源装(不填就用这份 profile 自己配的源)。"
5316
5746
  },
5317
5747
  plugin: {
5318
5748
  type: "string",
5319
- description: "update-plugin / uninstall-plugin 用:opencli 插件的包名或插件名(就是市场「已安装 opencli 插件」那一段里的名字,如 cmdb 或 opencli-plugin-cmdb)。"
5749
+ description: "opencli 插件的包名或插件名(如 cmdb 或 opencli-plugin-cmdb)。"
5320
5750
  }
5321
5751
  },
5322
5752
  required: ["action"],
@@ -5466,16 +5896,16 @@ function applyPanelsPref(panels, payload) {
5466
5896
  //#region src/modules/panels/model/handoff.ts
5467
5897
  var PANELS_FACT = "panels";
5468
5898
  var CAPABILITY_FACT = "capability";
5469
- function textOf$1(value) {
5899
+ function textOf$2(value) {
5470
5900
  return typeof value === "string" ? value.trim() : "";
5471
5901
  }
5472
5902
  function actionOf(call) {
5473
- return textOf$1(call.args["action"]) || "list";
5903
+ return textOf$2(call.args["action"]) || "list";
5474
5904
  }
5475
5905
  function requestOf(call) {
5476
5906
  const action = actionOf(call);
5477
5907
  if (!PROVISION_ACTIONS.includes(action)) return void 0;
5478
- const target = textOf$1(call.args["unit"]) || textOf$1(call.args["id"]);
5908
+ const target = textOf$2(call.args["unit"]) || textOf$2(call.args["id"]);
5479
5909
  if (target === "") return void 0;
5480
5910
  const wanted = action;
5481
5911
  return isDependencyAction(wanted) ? {
@@ -5487,7 +5917,7 @@ function requestOf(call) {
5487
5917
  };
5488
5918
  }
5489
5919
  function panelsHandoffEntry(target = {}) {
5490
- const path = textOf$1(target.path);
5920
+ const path = textOf$2(target.path);
5491
5921
  return path === "" ? void 0 : {
5492
5922
  fact: PANELS_FACT,
5493
5923
  key: `path:${path}`
@@ -5872,13 +6302,13 @@ function registerPanelTools(ctx, panels, values) {
5872
6302
  }
5873
6303
  const tool = {
5874
6304
  name: "workbench_panels",
5875
- description: "看或调右侧面板:启用哪些套件、什么顺序。启用/收起 = 卡片与 agent 工具一起上下线(别的项目不受影响,不删数据)。收起是\"这个工作区明确不要它\",连那些在机器级默认板上开着的套件也照样在这个工作区收起来,之后 show 能放回来。顺序只属于工作区:没排过的按各模块自己声明的次序(开着的在前、关着的跟在后面);show 或 move 过就以那次为准。机器级默认板只回答\"默认开哪几行\",没有顺序这一说。scope=machine 动的是\"机器级默认板\" —— 它只是还没配过的工作区的默认值,已经自己配过的工作区保持自己的选择,不会被它改。在那份板上 hide = 明确写下\"不要它\",这一笔比名单更管用:名单里还列着它也算不要,没提过它的工作区因此看不到它;show 会把这笔划掉、把它放回去。本工作区那份板只记**改过的那几行**(逐行覆盖):没提到的行跟着机器级默认板走,所以那份板以后改了,没改过的工作区会跟着变。",
6305
+ description: "看或调右侧面板:哪些套件开着、什么顺序。两份板:本工作区那份优先,没提过的行看机器级默认板 —— 那只是还没配过的工作区的默认值(已经自己配过的工作区保持自己的选择);本工作区只记改过的那几行,所以那份板以后改了,没改过的工作区跟着变。收起 = 这个工作区明确不要它:卡片与 agent 工具一起在这个工作区下线,连机器级默认板上开着的套件也照样收起;别的项目不受影响、不删数据,要放回来用 show。在机器级默认板上 hide 是写下\"不要它\"这一笔 —— 比名单更管用,show 会把这一笔划掉。顺序只属于工作区:没排过的按各模块自己声明的次序(开着的在前、关着的在后),show 或 move 过就以那次为准。",
5876
6306
  parameters: withApprovalNote({
5877
6307
  type: "object",
5878
6308
  properties: {
5879
6309
  action: {
5880
6310
  type: "string",
5881
- description: "show=这个工作区要它(之前收起的也放回来);hide=这个工作区明确不要它(卡片与工具一起下线,默认在机器上也开着的套件同样收起,别的项目不受影响);move=调这个工作区的顺序(新开一行时它排在最后);reset=清掉这个工作区改过的那些行(改完就全部回到跟着机器级默认板走);follow=把某一行放回跟随(带 id = 只放回那一行;不带 id = 全部放回)。",
6311
+ description: "list=看现在这份板(默认);show=这个工作区要它(之前收起的也放回来);hide=这个工作区明确不要它;move=调顺序(新开一行时排最后);reset=清掉这个工作区改过的行(改完全部回到跟着机器级默认板走);follow=某一行放回跟随(带 id = 只那一行,不带 = 全部)。",
5882
6312
  enum: [
5883
6313
  "list",
5884
6314
  "show",
@@ -5890,7 +6320,7 @@ function registerPanelTools(ctx, panels, values) {
5890
6320
  },
5891
6321
  id: {
5892
6322
  type: "string",
5893
- description: "show / hide / move 必填;follow 可选(带 id = 只把那一行放回跟随)。"
6323
+ description: "show / hide / move 必填:要动的套件 id。"
5894
6324
  },
5895
6325
  index: {
5896
6326
  type: "integer",
@@ -5898,7 +6328,7 @@ function registerPanelTools(ctx, panels, values) {
5898
6328
  },
5899
6329
  scope: {
5900
6330
  type: "string",
5901
- description: "workspace(默认)=只动当前工作区;machine=动机器级默认板,那是还没配过的工作区的默认值 —— 已经自己配过的工作区保持自己的选择,不受它影响。机器级板没有顺序这一说(move 会报错)。",
6331
+ description: "workspace(默认)=只动当前工作区;machine=动那份机器级默认板(只影响还没配过的工作区;没有顺序一说,move 会报错)。",
5902
6332
  enum: ["workspace", "machine"]
5903
6333
  }
5904
6334
  },
@@ -6034,6 +6464,8 @@ var OWNED = [{
6034
6464
  panels.accept(unit);
6035
6465
  },
6036
6466
  isReleased: (unit) => panels.isReleased(unit),
6467
+ dshHome: () => dshHome(),
6468
+ profileName: () => profileDir === null ? null : basename(profileDir),
6037
6469
  logger
6038
6470
  });
6039
6471
  return {
@@ -6077,151 +6509,307 @@ function assembleHostUnits(registry) {
6077
6509
  for (const face of OWNED) registry.registerOwned(face);
6078
6510
  }
6079
6511
  //#endregion
6080
- //#region src/shared/api/host/runtime-seed.ts
6081
- var WORKSHOP_SKILLS_DIR = "workbench-module-diy";
6082
- function runtimeSeedPaths(root) {
6083
- const opencliRoot = join(root, "opencli");
6084
- const opencliBinDir = join(opencliRoot, "bin");
6085
- const workshop = join(root, "diy");
6512
+ //#region src/shared/api/host/unit-readiness.ts
6513
+ function unitReadiness(manifest, lookup) {
6514
+ const missingHard = manifest.host.inject.filter((name) => lookup(name) === void 0);
6515
+ const missingSoft = (manifest.host.optional ?? []).filter((name) => lookup(name) === void 0);
6086
6516
  return {
6087
- root,
6088
- entryDir: join(root, "bin"),
6089
- opencliRoot,
6090
- opencliBinDir,
6091
- opencliBin: join(opencliBinDir, "opencli"),
6092
- opencliHome: join(opencliRoot, "home"),
6093
- workshop,
6094
- workshopPackages: join(workshop, "packages"),
6095
- workshopManifest: join(workshop, "package.json"),
6096
- skillsLink: join(workshop, ".agents", "skills", WORKSHOP_SKILLS_DIR)
6517
+ canMount: missingHard.length === 0,
6518
+ missingHard,
6519
+ missingSoft
6097
6520
  };
6098
6521
  }
6099
- function runtimeSeedRootOf(ctx) {
6100
- const homePath = ctx.get("dshHomePath");
6101
- if (typeof homePath !== "function") return null;
6102
- try {
6103
- return homePath("workbench");
6104
- } catch {
6105
- return null;
6106
- }
6107
- }
6108
- function packageRootOf(moduleUrl) {
6109
- return dirname(dirname(dirname(fileURLToPath(moduleUrl))));
6110
- }
6111
- function isInstalledCopy(packageRoot) {
6112
- return packageRoot.split(sep).includes("node_modules");
6113
- }
6114
- function opencliWrapperScript(packageRoot, home) {
6115
- const script = join(packageRoot, "bin", "opencli-isolated");
6116
- return `#!/bin/sh\nexec env DSH_HOME=${quoteShellArg(home)} bash ${quoteShellArg(script)} "$@"\n`;
6117
- }
6118
- function workshopSeedPlan(packageRoot, home, root) {
6119
- const script = join(packageRoot, "scripts", "seed-workshop.sh");
6120
- const env = { DSH_HOME: home };
6121
- const log = join(root, "seed.log");
6522
+ //#endregion
6523
+ //#region src/shared/api/host/mount-units.ts
6524
+ function withRunsRoute(result, unit) {
6525
+ const runs = result.longTasks;
6526
+ if (runs === void 0) return result;
6122
6527
  return {
6123
- entry: {
6124
- file: "bash",
6125
- args: [script, "entry"],
6126
- env,
6127
- log
6128
- },
6129
- full: {
6130
- file: "bash",
6131
- args: [script],
6132
- env,
6133
- log
6528
+ ...result,
6529
+ routes: {
6530
+ ...result.routes,
6531
+ [runsRouteOf(unit)]: (search) => {
6532
+ const path = (param(search, "path") ?? "").trim();
6533
+ return {
6534
+ ok: true,
6535
+ runs: path === "" ? [] : runs.current(path)
6536
+ };
6537
+ }
6134
6538
  }
6135
6539
  };
6136
6540
  }
6137
- function runDetached(command, onError, spawnImpl = spawn) {
6138
- let fd = null;
6139
- try {
6140
- fd = openSync(command.log, "a");
6141
- const child = spawnImpl(command.file, [...command.args], {
6142
- env: {
6143
- ...process.env,
6144
- ...command.env
6145
- },
6146
- detached: true,
6147
- stdio: [
6148
- "ignore",
6149
- fd,
6150
- fd
6151
- ]
6541
+ function notedRegistry(registry, writesOf) {
6542
+ return { register(definition) {
6543
+ if (!writesOf(definition.name)) return registry.register(definition);
6544
+ const noted = withApprovalNote(definition.parameters, { required: true });
6545
+ return registry.register(noted === definition.parameters ? definition : {
6546
+ ...definition,
6547
+ parameters: noted
6152
6548
  });
6153
- child.on("error", (error) => onError(`自举脚本没跑起来(${command.file}):${String(error)}`));
6154
- child.unref();
6155
- } catch (error) {
6156
- onError(`自举脚本没跑起来(${command.file}):${String(error)}`);
6157
- } finally {
6158
- if (fd !== null) closeQuietly(fd);
6159
- }
6549
+ } };
6160
6550
  }
6161
- function closeQuietly(fd) {
6162
- try {
6163
- closeSync(fd);
6164
- } catch {
6165
- return;
6166
- }
6551
+ function scopedMountCtx(ctx, registry, writesOf) {
6552
+ const tools = notedRegistry(registry, writesOf);
6553
+ const get = (name) => {
6554
+ const value = ctx.get(name);
6555
+ return name === "tools" && value !== void 0 ? tools : value;
6556
+ };
6557
+ return new Proxy(ctx, { get(target, key) {
6558
+ if (key === "get") return get;
6559
+ const value = Reflect.get(target, key, target);
6560
+ return typeof value === "function" ? value.bind(target) : value;
6561
+ } });
6167
6562
  }
6168
- function prependPath(entryDir, env = process.env) {
6169
- const parts = (env.PATH ?? "").split(delimiter).filter((part) => part !== "");
6170
- if (parts.includes(entryDir)) return false;
6171
- env.PATH = [entryDir, ...parts].join(delimiter);
6172
- return true;
6563
+ function failureReasonOf(error) {
6564
+ if (error instanceof Error) return error.message;
6565
+ return typeof error === "string" ? error : String(error);
6173
6566
  }
6174
- function seedRuntimeFiles(options) {
6175
- const paths = runtimeSeedPaths(options.root);
6176
- const done = [];
6177
- try {
6178
- for (const dir of [
6179
- paths.entryDir,
6180
- paths.opencliBinDir,
6181
- paths.opencliHome,
6182
- paths.workshopPackages
6183
- ]) mkdirSync(dir, { recursive: true });
6184
- const wanted = opencliWrapperScript(options.packageRoot, options.home);
6185
- if (readText(paths.opencliBin) !== wanted) {
6186
- rmSync(paths.opencliBin, { force: true });
6187
- writeFileSync(paths.opencliBin, wanted, "utf8");
6188
- chmodSync(paths.opencliBin, 493);
6189
- done.push(paths.opencliBin);
6567
+ function fail(ledger, item, reason) {
6568
+ ledger.failed.add(item);
6569
+ ledger.failures.push({
6570
+ id: item.host.manifest.id,
6571
+ reason
6572
+ });
6573
+ }
6574
+ function collectRoutes(mounted, ledger) {
6575
+ const routes = {};
6576
+ const owner = /* @__PURE__ */ new Map();
6577
+ const routesOfUnit = /* @__PURE__ */ new Map();
6578
+ for (const item of mounted) {
6579
+ if (ledger.failed.has(item)) continue;
6580
+ const unit = item.host.manifest.id;
6581
+ const accepted = [];
6582
+ let problem;
6583
+ for (const [key, handler] of Object.entries(item.result.routes)) {
6584
+ if (key !== unit && !key.startsWith(`${unit}/`)) {
6585
+ problem = `路由名要住在自己那一格的命名空间里(${unit} 或 ${unit}/…):${key} 不属于 ${unit} —— 装 / 升 / 卸时点名不到它,那个模块的数据会停在旧值`;
6586
+ break;
6587
+ }
6588
+ const previous = owner.get(key);
6589
+ if (previous !== void 0) {
6590
+ problem = `路由名冲突:${key} 由套件 ${previous} 与 ${unit} 同时声明`;
6591
+ break;
6592
+ }
6593
+ accepted.push([key, handler]);
6594
+ }
6595
+ if (problem !== void 0) {
6596
+ fail(ledger, item, problem);
6597
+ continue;
6598
+ }
6599
+ for (const [key, handler] of accepted) {
6600
+ owner.set(key, unit);
6601
+ routes[key] = handler;
6602
+ const own = routesOfUnit.get(unit);
6603
+ if (own === void 0) routesOfUnit.set(unit, [key]);
6604
+ else own.push(key);
6190
6605
  }
6191
- if (linkSkills(paths.skillsLink, join(options.packageRoot, "skills", "workbench-module-diy"), options.log)) done.push(paths.skillsLink);
6192
- } catch (error) {
6193
- options.log?.(`自举没做完:${String(error)}`);
6194
6606
  }
6195
- return done;
6607
+ return {
6608
+ routes,
6609
+ owners: owner,
6610
+ routesOfUnit
6611
+ };
6196
6612
  }
6197
- function readText(file) {
6198
- try {
6199
- return readFileSync(file, "utf8");
6200
- } catch {
6201
- return null;
6613
+ function collectNamed(mounted, ledger, pickMany, label) {
6614
+ const owner = /* @__PURE__ */ new Map();
6615
+ const out = [];
6616
+ for (const item of mounted) {
6617
+ if (ledger.failed.has(item)) continue;
6618
+ const unit = item.host.manifest.id;
6619
+ const entries = pickMany(item);
6620
+ if (entries.length === 0) continue;
6621
+ const clash = entries.find((entry) => owner.has(entry.resource));
6622
+ if (clash !== void 0) {
6623
+ fail(ledger, item, `${label}名冲突:${clash.resource} 由套件 ${owner.get(clash.resource)} 与 ${unit} 同时声明`);
6624
+ continue;
6625
+ }
6626
+ for (const entry of entries) {
6627
+ owner.set(entry.resource, unit);
6628
+ out.push({
6629
+ ...entry,
6630
+ unit
6631
+ });
6632
+ }
6202
6633
  }
6634
+ return out;
6203
6635
  }
6204
- function readLink(link) {
6205
- try {
6206
- return readlinkSync(link);
6207
- } catch {
6208
- return null;
6636
+ function collectContextRenderers(mounted, ledger) {
6637
+ const table = /* @__PURE__ */ new Map();
6638
+ const owner = /* @__PURE__ */ new Map();
6639
+ for (const item of mounted) {
6640
+ if (ledger.failed.has(item)) continue;
6641
+ const unit = item.host.manifest.id;
6642
+ const context = item.result.context;
6643
+ if (context === void 0) continue;
6644
+ const clash = context.kinds.find((kind) => owner.has(kind));
6645
+ if (clash !== void 0) {
6646
+ fail(ledger, item, `对话引用的 kind 冲突:${clash} 由套件 ${owner.get(clash)} 与 ${unit} 同时认领`);
6647
+ continue;
6648
+ }
6649
+ for (const kind of context.kinds) {
6650
+ owner.set(kind, unit);
6651
+ table.set(kind, (ref) => context.render(ref));
6652
+ }
6209
6653
  }
6654
+ return table;
6210
6655
  }
6211
- function linkSkills(link, source, log) {
6212
- if (!existsSync(source) || readLink(link) === source) return false;
6213
- try {
6214
- mkdirSync(dirname(link), { recursive: true });
6215
- rmSync(link, {
6216
- force: true,
6217
- recursive: true
6218
- });
6219
- symlinkSync(source, link, "dir");
6220
- return true;
6221
- } catch (error) {
6222
- log?.(`DIY 指引没挂上(${link}):${String(error)}`);
6223
- return false;
6656
+ function pickProvider(mounted, ledger, pick, label) {
6657
+ let chosen;
6658
+ let chosenBy;
6659
+ for (const item of mounted) {
6660
+ if (ledger.failed.has(item)) continue;
6661
+ const value = pick(item);
6662
+ if (value === void 0) continue;
6663
+ if (chosen === void 0) {
6664
+ chosen = value;
6665
+ chosenBy = item.host.manifest.id;
6666
+ continue;
6667
+ }
6668
+ fail(ledger, item, `${label}只能有一个提供者,现在是:${chosenBy}、${item.host.manifest.id}`);
6669
+ }
6670
+ return chosen;
6671
+ }
6672
+ function collectWarmReads(mounted) {
6673
+ const declarations = mounted.map((item) => item.result.warmReads).filter((declare) => declare !== void 0);
6674
+ return () => {
6675
+ const seen = /* @__PURE__ */ new Set();
6676
+ const reads = [];
6677
+ for (const read of declarations.flatMap((declare) => declare())) {
6678
+ const key = read.join(" ");
6679
+ if (seen.has(key)) continue;
6680
+ seen.add(key);
6681
+ reads.push(read);
6682
+ }
6683
+ return reads;
6684
+ };
6685
+ }
6686
+ function mountUnits(units, deps) {
6687
+ const store = deps.store ?? createWorkspaceStore({ root: null });
6688
+ const ownedUnits = deps.ownedUnits ?? (() => []);
6689
+ const mounted = [];
6690
+ const skipped = [];
6691
+ const longTaskDisposers = /* @__PURE__ */ new Map();
6692
+ const ledger = {
6693
+ failed: /* @__PURE__ */ new Set(),
6694
+ failures: []
6695
+ };
6696
+ for (const host of units) {
6697
+ const readiness = unitReadiness(host.manifest, (dep) => deps.ctx.get(dep));
6698
+ if (!readiness.canMount) {
6699
+ skipped.push({
6700
+ id: host.manifest.id,
6701
+ missing: readiness.missingHard
6702
+ });
6703
+ continue;
6704
+ }
6705
+ const unit = host.manifest.id;
6706
+ const tools = deps.ctx.get("tools");
6707
+ const writes = toolUnits(deps.manifests());
6708
+ const mountCtx = tools === void 0 ? deps.ctx : scopedMountCtx(deps.ctx, tools, (name) => writes.get(name)?.writes === true);
6709
+ const longTasks = deps.longTasks === void 0 ? void 0 : createLongTaskHost({
6710
+ scope: deps.longTasks.scope,
6711
+ unit,
6712
+ bump: deps.longTasks.bump
6713
+ });
6714
+ let result;
6715
+ try {
6716
+ result = host.face.mount({
6717
+ manifest: host.manifest,
6718
+ ctx: mountCtx,
6719
+ logger: deps.logger,
6720
+ manifests: deps.manifests,
6721
+ ownedUnits,
6722
+ ...deps.installable === void 0 ? {} : { installable: deps.installable },
6723
+ ...deps.opencliCommand === void 0 ? {} : { opencliCommand: deps.opencliCommand },
6724
+ ...deps.probe === void 0 ? {} : { probe: deps.probe },
6725
+ ...deps.values === void 0 ? {} : { values: deps.values },
6726
+ ...longTasks === void 0 ? {} : { longTasks: longTasks.host },
6727
+ store
6728
+ });
6729
+ } catch (error) {
6730
+ longTasks?.dispose();
6731
+ ledger.failures.push({
6732
+ id: unit,
6733
+ reason: failureReasonOf(error)
6734
+ });
6735
+ continue;
6736
+ }
6737
+ const item = {
6738
+ host,
6739
+ readiness,
6740
+ result: withRunsRoute(result, unit)
6741
+ };
6742
+ mounted.push(item);
6743
+ if (longTasks !== void 0) longTaskDisposers.set(item, longTasks.dispose);
6744
+ }
6745
+ const collected = collectRoutes(mounted, ledger);
6746
+ const prefs = collectNamed(mounted, ledger, (item) => item.result.prefs === void 0 ? [] : [item.result.prefs], "偏好资源");
6747
+ const actions = collectNamed(mounted, ledger, (item) => item.result.actions ?? [], "动作资源");
6748
+ const owners = new Map(collected.owners);
6749
+ for (const entry of [...prefs, ...actions]) owners.set(entry.resource, entry.unit);
6750
+ const factsOf = /* @__PURE__ */ new Map();
6751
+ for (const item of mounted) {
6752
+ if (ledger.failed.has(item)) continue;
6753
+ const declare = item.result.factsOf;
6754
+ if (declare !== void 0) factsOf.set(item.host.manifest.id, declare);
6755
+ }
6756
+ const gate = pickProvider(mounted, ledger, (item) => item.result.gate, "启停闸门");
6757
+ const provision = pickProvider(mounted, ledger, (item) => item.result.provision, "机器供给");
6758
+ const contextRenderers = collectContextRenderers(mounted, ledger);
6759
+ const alive = mounted.filter((item) => !ledger.failed.has(item));
6760
+ for (const item of mounted) {
6761
+ if (!ledger.failed.has(item)) continue;
6762
+ longTaskDisposers.get(item)?.();
6763
+ longTaskDisposers.delete(item);
6224
6764
  }
6765
+ return {
6766
+ mounted: alive,
6767
+ routes: collected.routes,
6768
+ owners,
6769
+ routesOfUnit: collected.routesOfUnit,
6770
+ factsOf,
6771
+ gate,
6772
+ prefs,
6773
+ actions,
6774
+ provision,
6775
+ contextRenderers,
6776
+ skipped,
6777
+ failed: ledger.failures,
6778
+ warmReads: collectWarmReads(alive),
6779
+ disposeLongTasks: () => {
6780
+ for (const dispose of [...longTaskDisposers.values()]) dispose();
6781
+ longTaskDisposers.clear();
6782
+ }
6783
+ };
6784
+ }
6785
+ //#endregion
6786
+ //#region src/shared/api/host/post-channels.ts
6787
+ function createPostChannels(deps) {
6788
+ const { ctx, mounted, facts, shellActions } = deps;
6789
+ let undo = [];
6790
+ const bind = (prefix, register) => {
6791
+ try {
6792
+ undo.push(register());
6793
+ } catch (error) {
6794
+ ctx.logger?.warn?.(`[dsh-workbench] ${prefix} 这条写通道没接上:${failureReasonOf(error)}`);
6795
+ }
6796
+ };
6797
+ return {
6798
+ rebind(wrote) {
6799
+ for (const dispose of undo.reverse()) dispose?.();
6800
+ undo = [];
6801
+ const prefs = [...mounted.values()].flatMap((item) => item.plan.prefs);
6802
+ const actions = [...shellActions, ...[...mounted.values()].flatMap((item) => item.plan.actions)];
6803
+ const provision = [...mounted.values()].find((item) => item.plan.provision !== void 0)?.plan.provision;
6804
+ if (prefs.length > 0) bind(PREFS_PREFIX, () => registerPrefs(ctx, prefs, wrote));
6805
+ if (actions.length > 0) bind(ACTIONS_PREFIX, () => registerActions(ctx, actions, wrote));
6806
+ if (provision !== void 0) bind(PROVISION_PREFIX, () => registerProvision(ctx, provision, facts));
6807
+ },
6808
+ dispose() {
6809
+ for (const dispose of undo) dispose?.();
6810
+ undo = [];
6811
+ }
6812
+ };
6225
6813
  }
6226
6814
  //#endregion
6227
6815
  //#region src/shared/api/host/events.ts
@@ -6435,10 +7023,11 @@ async function workbenchVersion(input) {
6435
7023
  pkg: read.pkg,
6436
7024
  installed: read.version,
6437
7025
  ...latest === void 0 ? {} : { latest },
7026
+ ...input.running === void 0 ? {} : { running: input.running },
6438
7027
  ...read.local === true ? { local: true } : {}
6439
7028
  };
6440
7029
  }
6441
- function moduleVersionsHandler(ctx, shell) {
7030
+ function moduleVersionsHandler(ctx, shell, running) {
6442
7031
  return async (search) => {
6443
7032
  const manager = readBundleManager(ctx);
6444
7033
  if (manager === void 0) return {
@@ -6483,6 +7072,7 @@ function moduleVersionsHandler(ctx, shell) {
6483
7072
  shell,
6484
7073
  fallback,
6485
7074
  force,
7075
+ ...running === void 0 ? {} : { running },
6486
7076
  logger: ctx.logger
6487
7077
  });
6488
7078
  return {
@@ -7242,377 +7832,103 @@ function libsOfUnit(unit, libs) {
7242
7832
  }
7243
7833
  var unitCache = createTimedMemo(PROBE_TTL_MS);
7244
7834
  //#endregion
7245
- //#region src/shared/api/host/unit-readiness.ts
7246
- function unitReadiness(manifest, lookup) {
7247
- const missingHard = manifest.host.inject.filter((name) => lookup(name) === void 0);
7248
- const missingSoft = (manifest.host.optional ?? []).filter((name) => lookup(name) === void 0);
7835
+ //#region src/shared/api/host/unit-registry.ts
7836
+ function installableUnitsOf(registry) {
7837
+ return registry.all().filter((host) => host.source.kind === "package").map((host) => host.unit);
7838
+ }
7839
+ function sourceLabel(source) {
7840
+ if (source.kind === "owned") return "外壳自己";
7841
+ if (source.kind === "plugin") return "按已装 opencli 插件合成";
7842
+ return source.pkg;
7843
+ }
7844
+ function createUnitRegistry(options) {
7845
+ const refusals = [];
7846
+ const hosts = [];
7847
+ const byUnit = /* @__PURE__ */ new Map();
7848
+ const failures = /* @__PURE__ */ new Map();
7849
+ const listeners = /* @__PURE__ */ new Set();
7850
+ function notify() {
7851
+ for (const listener of listeners) listener();
7852
+ }
7853
+ function add(host) {
7854
+ const taken = byUnit.get(host.unit);
7855
+ if (taken !== void 0) throw new Error(`套件 id 重复:${host.unit}(id 会进持久化配置,不能撞)—— 已被 ${sourceLabel(taken.source)} 占着`);
7856
+ byUnit.set(host.unit, host);
7857
+ hosts.push(host);
7858
+ notify();
7859
+ return () => {
7860
+ if (byUnit.get(host.unit) !== host) return;
7861
+ byUnit.delete(host.unit);
7862
+ const index = hosts.indexOf(host);
7863
+ if (index >= 0) hosts.splice(index, 1);
7864
+ notify();
7865
+ };
7866
+ }
7249
7867
  return {
7250
- canMount: missingHard.length === 0,
7251
- missingHard,
7252
- missingSoft
7868
+ register(face) {
7869
+ const unit = typeof face?.unit === "string" ? face.unit.trim() : "";
7870
+ const refuse = (pkg, reasons) => {
7871
+ refusals.push({
7872
+ unit,
7873
+ ...pkg === void 0 ? {} : { pkg },
7874
+ reasons
7875
+ });
7876
+ options.logger?.warn?.(`[dsh-workbench] ${pkg ?? "这个模块"}没挂上(unit ${unit === "" ? "没写" : unit}):${reasons.join(";")}`);
7877
+ notify();
7878
+ return () => {};
7879
+ };
7880
+ if (unit === "") return refuse(void 0, ["register({ unit, mount }) 里的 unit 是空的 —— U1b 之前的旧形状是 register({ manifest, mount });现在清单由外壳从**已装包**的 dsh.workbench 读出来,模块里的 id 从 deps.manifest.id 拿(ADR-0038 §2.2)"]);
7881
+ const source = options.declarationOf(unit);
7882
+ if (source === void 0) return refuse(void 0, [`没有哪个已装模块声明了 unit「${unit}」—— 查过这份 profile 的已装清单。要么 package.json 里 dsh.workbench.unit 写错了(改完要重启工作台),要么这个包根本没被装进这份 profile`]);
7883
+ const read = readDeclaration(source.pkg, source.declaration);
7884
+ if (read.manifest === void 0) return refuse(source.pkg, read.problems);
7885
+ return add({
7886
+ unit,
7887
+ manifest: read.manifest,
7888
+ face,
7889
+ source: {
7890
+ kind: "package",
7891
+ pkg: source.pkg
7892
+ }
7893
+ });
7894
+ },
7895
+ registerOwned(face) {
7896
+ return add({
7897
+ unit: face.manifest.id,
7898
+ manifest: face.manifest,
7899
+ face,
7900
+ source: { kind: "owned" }
7901
+ });
7902
+ },
7903
+ registerPlugin(face) {
7904
+ return add({
7905
+ unit: face.manifest.id,
7906
+ manifest: face.manifest,
7907
+ face,
7908
+ source: { kind: "plugin" }
7909
+ });
7910
+ },
7911
+ all: () => hosts,
7912
+ owned: () => hosts.filter((host) => host.source.kind === "owned").map((host) => host.unit),
7913
+ refusals: () => refusals,
7914
+ manifests: () => hosts.map((host) => host.manifest),
7915
+ subscribe(listener) {
7916
+ listeners.add(listener);
7917
+ return () => listeners.delete(listener);
7918
+ },
7919
+ fail(unit, reason) {
7920
+ failures.set(unit, reason);
7921
+ },
7922
+ recovered(unit) {
7923
+ failures.delete(unit);
7924
+ },
7925
+ failureOf: (unit) => failures.get(unit)
7253
7926
  };
7254
7927
  }
7255
7928
  //#endregion
7256
- //#region src/shared/api/host/mount-units.ts
7257
- function withRunsRoute(result, unit) {
7258
- const runs = result.longTasks;
7259
- if (runs === void 0) return result;
7260
- return {
7261
- ...result,
7262
- routes: {
7263
- ...result.routes,
7264
- [runsRouteOf(unit)]: (search) => {
7265
- const path = (param(search, "path") ?? "").trim();
7266
- return {
7267
- ok: true,
7268
- runs: path === "" ? [] : runs.current(path)
7269
- };
7270
- }
7271
- }
7272
- };
7273
- }
7274
- function notedRegistry(registry, writesOf) {
7275
- return { register(definition) {
7276
- if (!writesOf(definition.name)) return registry.register(definition);
7277
- const noted = withApprovalNote(definition.parameters, { required: true });
7278
- return registry.register(noted === definition.parameters ? definition : {
7279
- ...definition,
7280
- parameters: noted
7281
- });
7282
- } };
7283
- }
7284
- function scopedMountCtx(ctx, registry, writesOf) {
7285
- const tools = notedRegistry(registry, writesOf);
7286
- const get = (name) => {
7287
- const value = ctx.get(name);
7288
- return name === "tools" && value !== void 0 ? tools : value;
7289
- };
7290
- return new Proxy(ctx, { get(target, key) {
7291
- if (key === "get") return get;
7292
- const value = Reflect.get(target, key, target);
7293
- return typeof value === "function" ? value.bind(target) : value;
7294
- } });
7295
- }
7296
- function failureReasonOf(error) {
7297
- if (error instanceof Error) return error.message;
7298
- return typeof error === "string" ? error : String(error);
7299
- }
7300
- function fail(ledger, item, reason) {
7301
- ledger.failed.add(item);
7302
- ledger.failures.push({
7303
- id: item.host.manifest.id,
7304
- reason
7305
- });
7306
- }
7307
- function collectRoutes(mounted, ledger) {
7308
- const routes = {};
7309
- const owner = /* @__PURE__ */ new Map();
7310
- const routesOfUnit = /* @__PURE__ */ new Map();
7311
- for (const item of mounted) {
7312
- if (ledger.failed.has(item)) continue;
7313
- const unit = item.host.manifest.id;
7314
- const accepted = [];
7315
- let problem;
7316
- for (const [key, handler] of Object.entries(item.result.routes)) {
7317
- if (key !== unit && !key.startsWith(`${unit}/`)) {
7318
- problem = `路由名要住在自己那一格的命名空间里(${unit} 或 ${unit}/…):${key} 不属于 ${unit} —— 装 / 升 / 卸时点名不到它,那个模块的数据会停在旧值`;
7319
- break;
7320
- }
7321
- const previous = owner.get(key);
7322
- if (previous !== void 0) {
7323
- problem = `路由名冲突:${key} 由套件 ${previous} 与 ${unit} 同时声明`;
7324
- break;
7325
- }
7326
- accepted.push([key, handler]);
7327
- }
7328
- if (problem !== void 0) {
7329
- fail(ledger, item, problem);
7330
- continue;
7331
- }
7332
- for (const [key, handler] of accepted) {
7333
- owner.set(key, unit);
7334
- routes[key] = handler;
7335
- const own = routesOfUnit.get(unit);
7336
- if (own === void 0) routesOfUnit.set(unit, [key]);
7337
- else own.push(key);
7338
- }
7339
- }
7340
- return {
7341
- routes,
7342
- owners: owner,
7343
- routesOfUnit
7344
- };
7345
- }
7346
- function collectNamed(mounted, ledger, pickMany, label) {
7347
- const owner = /* @__PURE__ */ new Map();
7348
- const out = [];
7349
- for (const item of mounted) {
7350
- if (ledger.failed.has(item)) continue;
7351
- const unit = item.host.manifest.id;
7352
- const entries = pickMany(item);
7353
- if (entries.length === 0) continue;
7354
- const clash = entries.find((entry) => owner.has(entry.resource));
7355
- if (clash !== void 0) {
7356
- fail(ledger, item, `${label}名冲突:${clash.resource} 由套件 ${owner.get(clash.resource)} 与 ${unit} 同时声明`);
7357
- continue;
7358
- }
7359
- for (const entry of entries) {
7360
- owner.set(entry.resource, unit);
7361
- out.push({
7362
- ...entry,
7363
- unit
7364
- });
7365
- }
7366
- }
7367
- return out;
7368
- }
7369
- function collectContextRenderers(mounted, ledger) {
7370
- const table = /* @__PURE__ */ new Map();
7371
- const owner = /* @__PURE__ */ new Map();
7372
- for (const item of mounted) {
7373
- if (ledger.failed.has(item)) continue;
7374
- const unit = item.host.manifest.id;
7375
- const context = item.result.context;
7376
- if (context === void 0) continue;
7377
- const clash = context.kinds.find((kind) => owner.has(kind));
7378
- if (clash !== void 0) {
7379
- fail(ledger, item, `对话引用的 kind 冲突:${clash} 由套件 ${owner.get(clash)} 与 ${unit} 同时认领`);
7380
- continue;
7381
- }
7382
- for (const kind of context.kinds) {
7383
- owner.set(kind, unit);
7384
- table.set(kind, (ref) => context.render(ref));
7385
- }
7386
- }
7387
- return table;
7388
- }
7389
- function pickProvider(mounted, ledger, pick, label) {
7390
- let chosen;
7391
- let chosenBy;
7392
- for (const item of mounted) {
7393
- if (ledger.failed.has(item)) continue;
7394
- const value = pick(item);
7395
- if (value === void 0) continue;
7396
- if (chosen === void 0) {
7397
- chosen = value;
7398
- chosenBy = item.host.manifest.id;
7399
- continue;
7400
- }
7401
- fail(ledger, item, `${label}只能有一个提供者,现在是:${chosenBy}、${item.host.manifest.id}`);
7402
- }
7403
- return chosen;
7404
- }
7405
- function collectWarmReads(mounted) {
7406
- const declarations = mounted.map((item) => item.result.warmReads).filter((declare) => declare !== void 0);
7407
- return () => {
7408
- const seen = /* @__PURE__ */ new Set();
7409
- const reads = [];
7410
- for (const read of declarations.flatMap((declare) => declare())) {
7411
- const key = read.join(" ");
7412
- if (seen.has(key)) continue;
7413
- seen.add(key);
7414
- reads.push(read);
7415
- }
7416
- return reads;
7417
- };
7418
- }
7419
- function mountUnits(units, deps) {
7420
- const store = deps.store ?? createWorkspaceStore({ root: null });
7421
- const ownedUnits = deps.ownedUnits ?? (() => []);
7422
- const mounted = [];
7423
- const skipped = [];
7424
- const longTaskDisposers = /* @__PURE__ */ new Map();
7425
- const ledger = {
7426
- failed: /* @__PURE__ */ new Set(),
7427
- failures: []
7428
- };
7429
- for (const host of units) {
7430
- const readiness = unitReadiness(host.manifest, (dep) => deps.ctx.get(dep));
7431
- if (!readiness.canMount) {
7432
- skipped.push({
7433
- id: host.manifest.id,
7434
- missing: readiness.missingHard
7435
- });
7436
- continue;
7437
- }
7438
- const unit = host.manifest.id;
7439
- const tools = deps.ctx.get("tools");
7440
- const writes = toolUnits(deps.manifests());
7441
- const mountCtx = tools === void 0 ? deps.ctx : scopedMountCtx(deps.ctx, tools, (name) => writes.get(name)?.writes === true);
7442
- const longTasks = deps.longTasks === void 0 ? void 0 : createLongTaskHost({
7443
- scope: deps.longTasks.scope,
7444
- unit,
7445
- bump: deps.longTasks.bump
7446
- });
7447
- let result;
7448
- try {
7449
- result = host.face.mount({
7450
- manifest: host.manifest,
7451
- ctx: mountCtx,
7452
- logger: deps.logger,
7453
- manifests: deps.manifests,
7454
- ownedUnits,
7455
- ...deps.installable === void 0 ? {} : { installable: deps.installable },
7456
- ...deps.opencliCommand === void 0 ? {} : { opencliCommand: deps.opencliCommand },
7457
- ...deps.probe === void 0 ? {} : { probe: deps.probe },
7458
- ...deps.values === void 0 ? {} : { values: deps.values },
7459
- ...longTasks === void 0 ? {} : { longTasks: longTasks.host },
7460
- store
7461
- });
7462
- } catch (error) {
7463
- longTasks?.dispose();
7464
- ledger.failures.push({
7465
- id: unit,
7466
- reason: failureReasonOf(error)
7467
- });
7468
- continue;
7469
- }
7470
- const item = {
7471
- host,
7472
- readiness,
7473
- result: withRunsRoute(result, unit)
7474
- };
7475
- mounted.push(item);
7476
- if (longTasks !== void 0) longTaskDisposers.set(item, longTasks.dispose);
7477
- }
7478
- const collected = collectRoutes(mounted, ledger);
7479
- const prefs = collectNamed(mounted, ledger, (item) => item.result.prefs === void 0 ? [] : [item.result.prefs], "偏好资源");
7480
- const actions = collectNamed(mounted, ledger, (item) => item.result.actions ?? [], "动作资源");
7481
- const owners = new Map(collected.owners);
7482
- for (const entry of [...prefs, ...actions]) owners.set(entry.resource, entry.unit);
7483
- const factsOf = /* @__PURE__ */ new Map();
7484
- for (const item of mounted) {
7485
- if (ledger.failed.has(item)) continue;
7486
- const declare = item.result.factsOf;
7487
- if (declare !== void 0) factsOf.set(item.host.manifest.id, declare);
7488
- }
7489
- const gate = pickProvider(mounted, ledger, (item) => item.result.gate, "启停闸门");
7490
- const provision = pickProvider(mounted, ledger, (item) => item.result.provision, "机器供给");
7491
- const contextRenderers = collectContextRenderers(mounted, ledger);
7492
- const alive = mounted.filter((item) => !ledger.failed.has(item));
7493
- for (const item of mounted) {
7494
- if (!ledger.failed.has(item)) continue;
7495
- longTaskDisposers.get(item)?.();
7496
- longTaskDisposers.delete(item);
7497
- }
7498
- return {
7499
- mounted: alive,
7500
- routes: collected.routes,
7501
- owners,
7502
- routesOfUnit: collected.routesOfUnit,
7503
- factsOf,
7504
- gate,
7505
- prefs,
7506
- actions,
7507
- provision,
7508
- contextRenderers,
7509
- skipped,
7510
- failed: ledger.failures,
7511
- warmReads: collectWarmReads(alive),
7512
- disposeLongTasks: () => {
7513
- for (const dispose of [...longTaskDisposers.values()]) dispose();
7514
- longTaskDisposers.clear();
7515
- }
7516
- };
7517
- }
7518
- //#endregion
7519
- //#region src/shared/api/host/unit-registry.ts
7520
- function installableUnitsOf(registry) {
7521
- return registry.all().filter((host) => host.source.kind === "package").map((host) => host.unit);
7522
- }
7523
- function sourceLabel(source) {
7524
- if (source.kind === "owned") return "外壳自己";
7525
- if (source.kind === "plugin") return "按已装 opencli 插件合成";
7526
- return source.pkg;
7527
- }
7528
- function createUnitRegistry(options) {
7529
- const refusals = [];
7530
- const hosts = [];
7531
- const byUnit = /* @__PURE__ */ new Map();
7532
- const failures = /* @__PURE__ */ new Map();
7533
- const listeners = /* @__PURE__ */ new Set();
7534
- function notify() {
7535
- for (const listener of listeners) listener();
7536
- }
7537
- function add(host) {
7538
- const taken = byUnit.get(host.unit);
7539
- if (taken !== void 0) throw new Error(`套件 id 重复:${host.unit}(id 会进持久化配置,不能撞)—— 已被 ${sourceLabel(taken.source)} 占着`);
7540
- byUnit.set(host.unit, host);
7541
- hosts.push(host);
7542
- notify();
7543
- return () => {
7544
- if (byUnit.get(host.unit) !== host) return;
7545
- byUnit.delete(host.unit);
7546
- const index = hosts.indexOf(host);
7547
- if (index >= 0) hosts.splice(index, 1);
7548
- notify();
7549
- };
7550
- }
7551
- return {
7552
- register(face) {
7553
- const unit = typeof face?.unit === "string" ? face.unit.trim() : "";
7554
- const refuse = (pkg, reasons) => {
7555
- refusals.push({
7556
- unit,
7557
- ...pkg === void 0 ? {} : { pkg },
7558
- reasons
7559
- });
7560
- options.logger?.warn?.(`[dsh-workbench] ${pkg ?? "这个模块"}没挂上(unit ${unit === "" ? "没写" : unit}):${reasons.join(";")}`);
7561
- notify();
7562
- return () => {};
7563
- };
7564
- if (unit === "") return refuse(void 0, ["register({ unit, mount }) 里的 unit 是空的 —— U1b 之前的旧形状是 register({ manifest, mount });现在清单由外壳从**已装包**的 dsh.workbench 读出来,模块里的 id 从 deps.manifest.id 拿(ADR-0038 §2.2)"]);
7565
- const source = options.declarationOf(unit);
7566
- if (source === void 0) return refuse(void 0, [`没有哪个已装模块声明了 unit「${unit}」—— 查过这份 profile 的已装清单。要么 package.json 里 dsh.workbench.unit 写错了(改完要重启工作台),要么这个包根本没被装进这份 profile`]);
7567
- const read = readDeclaration(source.pkg, source.declaration);
7568
- if (read.manifest === void 0) return refuse(source.pkg, read.problems);
7569
- return add({
7570
- unit,
7571
- manifest: read.manifest,
7572
- face,
7573
- source: {
7574
- kind: "package",
7575
- pkg: source.pkg
7576
- }
7577
- });
7578
- },
7579
- registerOwned(face) {
7580
- return add({
7581
- unit: face.manifest.id,
7582
- manifest: face.manifest,
7583
- face,
7584
- source: { kind: "owned" }
7585
- });
7586
- },
7587
- registerPlugin(face) {
7588
- return add({
7589
- unit: face.manifest.id,
7590
- manifest: face.manifest,
7591
- face,
7592
- source: { kind: "plugin" }
7593
- });
7594
- },
7595
- all: () => hosts,
7596
- owned: () => hosts.filter((host) => host.source.kind === "owned").map((host) => host.unit),
7597
- refusals: () => refusals,
7598
- manifests: () => hosts.map((host) => host.manifest),
7599
- subscribe(listener) {
7600
- listeners.add(listener);
7601
- return () => listeners.delete(listener);
7602
- },
7603
- fail(unit, reason) {
7604
- failures.set(unit, reason);
7605
- },
7606
- recovered(unit) {
7607
- failures.delete(unit);
7608
- },
7609
- failureOf: (unit) => failures.get(unit)
7610
- };
7611
- }
7612
- //#endregion
7613
- //#region src/shared/api/host/unit-roster-facts.ts
7614
- function rosterKeyOf(units) {
7615
- return units.all().map((host) => host.unit).join("\0");
7929
+ //#region src/shared/api/host/unit-roster-facts.ts
7930
+ function rosterKeyOf(units) {
7931
+ return units.all().map((host) => host.unit).join("\0");
7616
7932
  }
7617
7933
  function bindRosterFacts(options) {
7618
7934
  let seen = rosterKeyOf(options.units);
@@ -7628,7 +7944,7 @@ function bindRosterFacts(options) {
7628
7944
  function recordOf(value) {
7629
7945
  return typeof value === "object" && value !== null ? value : void 0;
7630
7946
  }
7631
- function textOf(value) {
7947
+ function textOf$1(value) {
7632
7948
  return typeof value === "string" ? value : "";
7633
7949
  }
7634
7950
  function stringsOf(value) {
@@ -7639,18 +7955,18 @@ function argsOf(raw) {
7639
7955
  return raw.flatMap((item) => {
7640
7956
  const record = recordOf(item);
7641
7957
  if (record === void 0) return [];
7642
- const name = textOf(record["name"]).trim();
7958
+ const name = textOf$1(record["name"]).trim();
7643
7959
  if (name === "") return [];
7644
7960
  const fallback = record["default"];
7645
7961
  return [{
7646
7962
  name,
7647
- type: textOf(record["type"]) || "string",
7963
+ type: textOf$1(record["type"]) || "string",
7648
7964
  required: record["required"] === true,
7649
7965
  positional: record["positional"] === true,
7650
7966
  valueRequired: record["valueRequired"] === true,
7651
7967
  choices: stringsOf(record["choices"]),
7652
7968
  ...fallback === void 0 || fallback === null ? {} : { defaultValue: fallback },
7653
- help: textOf(record["help"])
7969
+ help: textOf$1(record["help"])
7654
7970
  }];
7655
7971
  });
7656
7972
  }
@@ -7661,11 +7977,11 @@ function parsePluginRows(stdout) {
7661
7977
  for (const item of parsed) {
7662
7978
  const record = recordOf(item);
7663
7979
  if (record === void 0) return void 0;
7664
- const name = textOf(record["name"]).trim();
7980
+ const name = textOf$1(record["name"]).trim();
7665
7981
  if (name === "") return void 0;
7666
7982
  rows.push({
7667
7983
  name,
7668
- version: textOf(record["version"]),
7984
+ version: textOf$1(record["version"]),
7669
7985
  commands: stringsOf(record["commands"])
7670
7986
  });
7671
7987
  }
@@ -7678,17 +7994,17 @@ function parseCommandRows(stdout) {
7678
7994
  for (const item of parsed) {
7679
7995
  const record = recordOf(item);
7680
7996
  if (record === void 0) return void 0;
7681
- const site = textOf(record["site"]).trim();
7682
- const name = textOf(record["name"]).trim();
7997
+ const site = textOf$1(record["site"]).trim();
7998
+ const name = textOf$1(record["name"]).trim();
7683
7999
  if (site === "" || name === "") return void 0;
7684
8000
  rows.push({
7685
8001
  site,
7686
8002
  name,
7687
- description: textOf(record["description"]),
7688
- access: textOf(record["access"]),
8003
+ description: textOf$1(record["description"]),
8004
+ access: textOf$1(record["access"]),
7689
8005
  browser: record["browser"] === true,
7690
- siteSession: textOf(record["siteSession"]),
7691
- ...textOf(record["domain"]) === "" ? {} : { domain: textOf(record["domain"]) },
8006
+ siteSession: textOf$1(record["siteSession"]),
8007
+ ...textOf$1(record["domain"]) === "" ? {} : { domain: textOf$1(record["domain"]) },
7692
8008
  args: argsOf(record["args"])
7693
8009
  });
7694
8010
  }
@@ -7876,6 +8192,7 @@ var SCHEMA_TYPE = {
7876
8192
  string: "string"
7877
8193
  };
7878
8194
  var KEY_ARG_LIMIT = 3;
8195
+ var NO_KEY_ARGS = "没有带参数";
7879
8196
  var RUN_CODE_TOOL = "run_code";
7880
8197
  var PLUGIN_PROMPT_BUDGET = 4e3;
7881
8198
  function pluginToolName(plugin, command) {
@@ -7953,13 +8270,15 @@ function pluginKeyArgs(command, raw) {
7953
8270
  parts.push(`${arg.name}=${String(value)}`);
7954
8271
  if (parts.length === KEY_ARG_LIMIT) break;
7955
8272
  }
7956
- return parts.length === 0 ? "没有带参数" : parts.join("、");
8273
+ return parts.length === 0 ? NO_KEY_ARGS : parts.join("、");
7957
8274
  }
7958
8275
  function pluginApprovalQuestion(command, raw) {
7959
8276
  const key = pluginKeyArgs(command, raw);
8277
+ const zhKey = key === NO_KEY_ARGS ? "这次没带参数" : key;
8278
+ const enKey = key === NO_KEY_ARGS ? "no arguments" : key;
7960
8279
  return {
7961
- zh: `允许执行 ${command.plugin} 的 ${command.name}(${key})吗?`,
7962
- en: `Run ${command.plugin} ${command.name} (${key})?`
8280
+ zh: `agent 想执行工作台 [${command.plugin}] opencli 插件的 [${command.name}] 命令(${zhKey}),允许吗?`,
8281
+ en: `The agent wants to run the [${command.name}] command of the workbench opencli plugin [${command.plugin}] (${enKey}). Allow?`
7963
8282
  };
7964
8283
  }
7965
8284
  function pluginApprovalFor(command) {
@@ -8221,6 +8540,330 @@ function createPluginUnits(options) {
8221
8540
  };
8222
8541
  }
8223
8542
  //#endregion
8543
+ //#region src/shared/api/config-sync.ts
8544
+ var CONFIG_SYNC_ROUTE = "config-sync";
8545
+ var CONFIG_SYNC_RESOURCE = "configSync";
8546
+ //#endregion
8547
+ //#region src/shared/api/host/config-sync.ts
8548
+ var ID_LINE = /^- id:[ \t]*"?([^"\s#]+)/;
8549
+ var INSERT_LINE = /^- insert:[ \t]*$/;
8550
+ var NESTED_ID = /^[ \t]+-[ \t]*id:[ \t]*"?([^"\s#]+)/;
8551
+ var NAME_LINE = /^[ \t]*-?[ \t]*name:[ \t]*['"]?([^'"\s#]+)/;
8552
+ var COMMENT_LINE = /^[ \t]*#/;
8553
+ function trimmedTail(lines) {
8554
+ let end = lines.length;
8555
+ while (end > 0 && (lines[end - 1] ?? "").trim() === "") end -= 1;
8556
+ return lines.slice(0, end);
8557
+ }
8558
+ function matched(text, pattern) {
8559
+ const found = pattern.exec(text);
8560
+ return found === null ? void 0 : found[1];
8561
+ }
8562
+ function blockOf(body, index) {
8563
+ const lines = body.split("\n");
8564
+ const first = lines[0] ?? "";
8565
+ const id = matched(first, ID_LINE);
8566
+ const nested = lines.flatMap((line) => {
8567
+ const one = matched(line, NESTED_ID);
8568
+ return one === void 0 ? [] : [one];
8569
+ });
8570
+ const insert = id === void 0 && INSERT_LINE.test(first);
8571
+ const label = id ?? nested.join(", ");
8572
+ const key = id !== void 0 ? `id:${id}` : insert ? `insert:${nested.join(", ")}` : `block:${String(index)}`;
8573
+ const names = lines.flatMap((line) => {
8574
+ const one = matched(line, NAME_LINE);
8575
+ return one === void 0 ? [] : [one];
8576
+ });
8577
+ return {
8578
+ key,
8579
+ id: label === "" ? key : label,
8580
+ insert,
8581
+ body,
8582
+ names
8583
+ };
8584
+ }
8585
+ function spannedBlocks(text) {
8586
+ const lines = text.split("\n");
8587
+ const starts = [];
8588
+ lines.forEach((line, index) => {
8589
+ if (/^- /.test(line) || line === "-") starts.push(index);
8590
+ });
8591
+ const blocks = [];
8592
+ for (let i = 0; i < starts.length; i += 1) {
8593
+ const from = starts[i] ?? 0;
8594
+ const to = i + 1 < starts.length ? starts[i + 1] ?? lines.length : lines.length;
8595
+ const body = trimmedTail(lines.slice(from, to)).join("\n");
8596
+ if (body.trim() === "") continue;
8597
+ blocks.push({
8598
+ ...blockOf(body, blocks.length),
8599
+ from,
8600
+ to
8601
+ });
8602
+ }
8603
+ return blocks;
8604
+ }
8605
+ function patchBlocks(text) {
8606
+ return spannedBlocks(text).map(({ from: _from, to: _to, ...block }) => block);
8607
+ }
8608
+ function normalizeBlock(body) {
8609
+ return body.split("\n").map((line) => line.replace(/\s+$/, "")).filter((line) => line.trim() !== "" && !COMMENT_LINE.test(line)).join("\n");
8610
+ }
8611
+ function syncRows(source, own, missing) {
8612
+ const mine = new Map(own.map((block) => [block.key, block]));
8613
+ const rows = [];
8614
+ for (const block of source) {
8615
+ const here = mine.get(block.key);
8616
+ if (here !== void 0 && normalizeBlock(here.body) === normalizeBlock(block.body)) continue;
8617
+ rows.push({
8618
+ key: block.key,
8619
+ id: block.id,
8620
+ insert: block.insert,
8621
+ state: here === void 0 ? "new" : "differs",
8622
+ body: block.body,
8623
+ ownBody: here === void 0 ? null : here.body,
8624
+ missing: missing(block)
8625
+ });
8626
+ }
8627
+ return rows;
8628
+ }
8629
+ function replaceBlocks(text, blocks) {
8630
+ const wanted = new Map(blocks.map((block) => [block.key, block]));
8631
+ const spans = spannedBlocks(text).filter((span) => wanted.has(span.key));
8632
+ const lines = text.split("\n");
8633
+ for (const span of [...spans].reverse()) {
8634
+ const replacement = (wanted.get(span.key)?.body ?? "").split("\n");
8635
+ lines.splice(span.from, span.to - span.from, ...replacement);
8636
+ }
8637
+ return lines.join("\n");
8638
+ }
8639
+ function appendBlocks(text, blocks) {
8640
+ const kept = text.split("\n").filter((line) => line.trim() !== "[]").join("\n").replace(/\s+$/, "");
8641
+ let out = kept === "" ? "" : `${kept}\n`;
8642
+ for (const block of blocks) out += `\n${block.body}\n`;
8643
+ return out;
8644
+ }
8645
+ function mergeBlocks(ownText, incoming, overwrite) {
8646
+ const own = patchBlocks(ownText);
8647
+ const mine = new Set(own.map((block) => block.key));
8648
+ const written = [];
8649
+ const skipped = [];
8650
+ const take = [];
8651
+ for (const block of incoming) {
8652
+ if (!mine.has(block.key) || overwrite.has(block.key)) {
8653
+ take.push(block);
8654
+ written.push(block.id);
8655
+ continue;
8656
+ }
8657
+ skipped.push(block.id);
8658
+ }
8659
+ if (take.length === 0) return {
8660
+ ok: true,
8661
+ text: ownText,
8662
+ written,
8663
+ skipped
8664
+ };
8665
+ const text = appendBlocks(replaceBlocks(ownText, take.filter((block) => mine.has(block.key))), take.filter((block) => !mine.has(block.key)));
8666
+ const check = patchBlocks(text);
8667
+ const expected = /* @__PURE__ */ new Set([...mine, ...take.map((block) => block.key)]);
8668
+ if (check.length !== expected.size || new Set(check.map((block) => block.key)).size !== expected.size) return {
8669
+ ok: false,
8670
+ reason: "写入前的自检没过:行的 id 会重复或丢行"
8671
+ };
8672
+ return {
8673
+ ok: true,
8674
+ text,
8675
+ written,
8676
+ skipped
8677
+ };
8678
+ }
8679
+ function readPatchBlocks(path) {
8680
+ try {
8681
+ const text = readFileSync(path, "utf8");
8682
+ return {
8683
+ ok: true,
8684
+ text,
8685
+ blocks: patchBlocks(text)
8686
+ };
8687
+ } catch (error) {
8688
+ return {
8689
+ ok: false,
8690
+ reason: error instanceof Error ? error.message : String(error)
8691
+ };
8692
+ }
8693
+ }
8694
+ function patchMode(path) {
8695
+ try {
8696
+ return statSync(path).mode & 511;
8697
+ } catch {
8698
+ return 420;
8699
+ }
8700
+ }
8701
+ function writePatchFile(path, text) {
8702
+ const mode = patchMode(path);
8703
+ const tmp = `${path}.wb-sync-${String(process.pid)}`;
8704
+ writeFileSync(tmp, text, { mode });
8705
+ try {
8706
+ renameSync(tmp, path);
8707
+ } catch (error) {
8708
+ try {
8709
+ unlinkSync(tmp);
8710
+ } catch {
8711
+ throw error;
8712
+ }
8713
+ throw error;
8714
+ }
8715
+ }
8716
+ function configSyncHandler(deps) {
8717
+ return (search) => ({
8718
+ ok: true,
8719
+ ...configSyncSnapshot(deps, search.get("source") ?? void 0)
8720
+ });
8721
+ }
8722
+ function textOf(value) {
8723
+ return typeof value === "string" ? value : "";
8724
+ }
8725
+ function profileDependencies(dir) {
8726
+ try {
8727
+ const manifest = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
8728
+ return new Set(Object.keys(manifest.dependencies ?? {}));
8729
+ } catch {
8730
+ return /* @__PURE__ */ new Set();
8731
+ }
8732
+ }
8733
+ function profileNames(profilesDir) {
8734
+ try {
8735
+ return readdirSync(profilesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory() || entry.isSymbolicLink()).map((entry) => entry.name).filter((name) => existsSync(join(profilesDir, name, "package.json"))).sort((a, b) => a.localeCompare(b));
8736
+ } catch {
8737
+ return [];
8738
+ }
8739
+ }
8740
+ function configSyncSnapshot(deps, asked) {
8741
+ const profilesDir = join(deps.home, "profiles");
8742
+ const others = profileNames(profilesDir).filter((name) => name !== deps.self);
8743
+ const base = {
8744
+ self: deps.self,
8745
+ home: deps.home
8746
+ };
8747
+ if (deps.profileDir === null) return {
8748
+ ...base,
8749
+ source: "",
8750
+ profiles: [],
8751
+ rows: [],
8752
+ emptyProviders: false,
8753
+ reason: "拿不到当前 profile 目录"
8754
+ };
8755
+ const own = readPatchBlocks(join(deps.profileDir, "cordis.patch.yml"));
8756
+ const emptyProviders = own.ok && !own.blocks.some((block) => block.key.startsWith("id:llm-"));
8757
+ const ownBlocks = own.ok ? own.blocks : [];
8758
+ const profiles = others.map((name) => {
8759
+ const read = readPatchBlocks(join(profilesDir, name, "cordis.patch.yml"));
8760
+ return {
8761
+ name,
8762
+ readable: read.ok,
8763
+ candidates: read.ok ? syncRows(read.blocks, ownBlocks, () => []).length : 0
8764
+ };
8765
+ });
8766
+ if (others.length === 0) return {
8767
+ ...base,
8768
+ source: "",
8769
+ profiles,
8770
+ rows: [],
8771
+ emptyProviders,
8772
+ reason: "这台机器上只有当前这个 profile,没有可同步的来源"
8773
+ };
8774
+ const source = others.includes(asked ?? "") ? asked ?? "" : others.includes("web") ? "web" : others[0] ?? "";
8775
+ const sourceDir = join(profilesDir, source);
8776
+ const read = readPatchBlocks(join(sourceDir, "cordis.patch.yml"));
8777
+ if (!read.ok) return {
8778
+ ...base,
8779
+ source,
8780
+ profiles,
8781
+ rows: [],
8782
+ emptyProviders,
8783
+ reason: read.reason
8784
+ };
8785
+ if (!own.ok) return {
8786
+ ...base,
8787
+ source,
8788
+ profiles,
8789
+ rows: [],
8790
+ emptyProviders,
8791
+ reason: own.reason
8792
+ };
8793
+ const declared = profileDependencies(sourceDir);
8794
+ const modulesDir = join(deps.profileDir, "node_modules");
8795
+ const rows = syncRows(read.blocks, own.blocks, (block) => block.names.filter((name) => declared.has(name) && !existsSync(join(modulesDir, name))));
8796
+ return {
8797
+ ...base,
8798
+ source,
8799
+ profiles,
8800
+ rows,
8801
+ emptyProviders
8802
+ };
8803
+ }
8804
+ function configSyncApply(deps, payload) {
8805
+ if (deps.profileDir === null) return {
8806
+ ok: false,
8807
+ text: "拿不到当前 profile 目录,这次没写任何东西"
8808
+ };
8809
+ const body = typeof payload === "object" && payload !== null ? payload : {};
8810
+ const source = textOf(body["source"]);
8811
+ const keys = new Set((Array.isArray(body["keys"]) ? body["keys"] : []).map((item) => textOf(item)).filter((item) => item !== ""));
8812
+ const overwrite = new Set((Array.isArray(body["overwrite"]) ? body["overwrite"] : []).map((item) => textOf(item)));
8813
+ if (source === "" || keys.size === 0) return {
8814
+ ok: false,
8815
+ text: "没说清从哪个 profile 搬哪几行"
8816
+ };
8817
+ const profilesDir = join(deps.home, "profiles");
8818
+ if (!profileNames(profilesDir).includes(source)) return {
8819
+ ok: false,
8820
+ text: `没有这个 profile:${source}`
8821
+ };
8822
+ if (source === deps.self) return {
8823
+ ok: false,
8824
+ text: "来源不能是工作台自己"
8825
+ };
8826
+ const read = readPatchBlocks(join(profilesDir, source, "cordis.patch.yml"));
8827
+ if (!read.ok) return {
8828
+ ok: false,
8829
+ text: `读不到 ${source} 的配置:${read.reason}`
8830
+ };
8831
+ const own = readPatchBlocks(join(deps.profileDir, "cordis.patch.yml"));
8832
+ if (!own.ok) return {
8833
+ ok: false,
8834
+ text: `读不到工作台自己的配置:${own.reason}`
8835
+ };
8836
+ const picked = read.blocks.filter((block) => keys.has(block.key));
8837
+ if (picked.length === 0) return {
8838
+ ok: false,
8839
+ text: "选中的行在来源里已经找不到了,重新读一遍再试"
8840
+ };
8841
+ const merged = mergeBlocks(own.text, picked, overwrite);
8842
+ if (!merged.ok) return {
8843
+ ok: false,
8844
+ text: merged.reason
8845
+ };
8846
+ try {
8847
+ writePatchFile(join(deps.profileDir, "cordis.patch.yml"), merged.text);
8848
+ } catch (error) {
8849
+ return {
8850
+ ok: false,
8851
+ text: `写不进去:${error instanceof Error ? error.message : String(error)}`
8852
+ };
8853
+ }
8854
+ return {
8855
+ ok: true,
8856
+ written: merged.written,
8857
+ skipped: merged.skipped
8858
+ };
8859
+ }
8860
+ function configSyncAction(deps) {
8861
+ return {
8862
+ resource: CONFIG_SYNC_RESOURCE,
8863
+ handle: (payload) => configSyncApply(deps, payload)
8864
+ };
8865
+ }
8866
+ //#endregion
8224
8867
  //#region src/shared/api/host/tool-scope.ts
8225
8868
  function agentOf(value) {
8226
8869
  return typeof value === "object" && value !== null ? value : void 0;
@@ -8345,11 +8988,17 @@ function apply(ctx) {
8345
8988
  [DSH_PLUGINS_ROUTE]: dshPluginsHandler(ctx),
8346
8989
  [BUNDLE_REGISTRIES_ROUTE]: bundleRegistriesHandler(ctx),
8347
8990
  [BUNDLE_INSPECT_ROUTE]: bundleInspectHandler(ctx),
8348
- [MODULE_VERSIONS_ROUTE]: moduleVersionsHandler(ctx, ctx.get("shell"))
8991
+ [MODULE_VERSIONS_ROUTE]: moduleVersionsHandler(ctx, ctx.get("shell"), runningVersionOf(import.meta.url))
8349
8992
  };
8350
8993
  const renderers = /* @__PURE__ */ new Map();
8351
8994
  const shell = ctx.get("shell");
8352
8995
  const profileDir = profileDirOf(ctx);
8996
+ const configSync = {
8997
+ home: dshHome(),
8998
+ self: profileDir === null ? "" : basename(profileDir),
8999
+ profileDir
9000
+ };
9001
+ routes[CONFIG_SYNC_ROUTE] = configSyncHandler(configSync);
8353
9002
  const units = createUnitRegistry({
8354
9003
  declarationOf: createDeclarationLookup(profileDir),
8355
9004
  logger: ctx.logger
@@ -8424,10 +9073,23 @@ function apply(ctx) {
8424
9073
  const factsOfMap = /* @__PURE__ */ new Map();
8425
9074
  const routesOfUnitMap = /* @__PURE__ */ new Map();
8426
9075
  const ownersMap = /* @__PURE__ */ new Map();
8427
- let undoPost = [];
8428
9076
  const factsOfUnit = (unit) => [...routesOfUnitMap.get(unit) ?? [], "capability"];
8429
9077
  const callFacts = (unit, call) => factsOfMap.get(unit)?.(call) ?? factsOfUnit(unit);
8430
9078
  const deniedNow = (workspace) => [...new Set([...mounted.values()].flatMap((item) => item.plan.gate?.deniedTools(workspace) ?? []))];
9079
+ const wrote = (resource, write) => {
9080
+ const unit = ownersMap.get(resource);
9081
+ if (unit !== void 0) facts.bump(callFacts(unit, {
9082
+ source: resource,
9083
+ args: asArgs(write.args),
9084
+ result: write.result
9085
+ }));
9086
+ };
9087
+ const postChannels = createPostChannels({
9088
+ ctx,
9089
+ mounted,
9090
+ facts,
9091
+ shellActions: [configSyncAction(configSync)]
9092
+ });
8431
9093
  function unmountUnit(id) {
8432
9094
  const item = mounted.get(id);
8433
9095
  if (item === void 0) return;
@@ -8517,22 +9179,7 @@ function apply(ctx) {
8517
9179
  }
8518
9180
  }
8519
9181
  function rebindPost() {
8520
- for (const dispose of undoPost.reverse()) dispose?.();
8521
- undoPost = [];
8522
- const wrote = (resource, write) => {
8523
- const unit = ownersMap.get(resource);
8524
- if (unit !== void 0) facts.bump(callFacts(unit, {
8525
- source: resource,
8526
- args: asArgs(write.args),
8527
- result: write.result
8528
- }));
8529
- };
8530
- const prefs = [...mounted.values()].flatMap((item) => item.plan.prefs);
8531
- const actions = [...mounted.values()].flatMap((item) => item.plan.actions);
8532
- const provision = [...mounted.values()].find((item) => item.plan.provision !== void 0)?.plan.provision;
8533
- if (prefs.length > 0) undoPost.push(registerPrefs(ctx, prefs, wrote));
8534
- if (actions.length > 0) undoPost.push(registerActions(ctx, actions, wrote));
8535
- if (provision !== void 0) undoPost.push(registerProvision(ctx, provision, facts));
9182
+ postChannels.rebind(wrote);
8536
9183
  }
8537
9184
  function sync() {
8538
9185
  const present = new Set(units.all().map((one) => one.unit));
@@ -8599,7 +9246,7 @@ function apply(ctx) {
8599
9246
  }
8600
9247
  ctx.effect(() => () => {
8601
9248
  for (const id of [...mounted.keys()]) unmountUnit(id);
8602
- for (const dispose of undoPost) dispose?.();
9249
+ postChannels.dispose();
8603
9250
  }, `${PLUGIN_ID}: 卸载时把单元摘干净`);
8604
9251
  ctx.effect(() => units.subscribe(sync), `${PLUGIN_ID}: 注册表变了就增删(装/卸一个模块 = 挂/卸它的面)`);
8605
9252
  ctx.effect(() => bindRosterFacts({