@dsh-workbench/core 1.1.5 → 1.1.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 (28) hide show
  1. package/client.js +714 -520
  2. package/dist/client/client.body.js +714 -520
  3. package/dist/client/index.js +585 -391
  4. package/dist/host/index.js +732 -168
  5. package/dist/types/app/layouts/market/market-check.d.ts +7 -0
  6. package/dist/types/app/layouts/market/market-header.d.ts +3 -1
  7. package/dist/types/app/layouts/market/section-check.d.ts +12 -0
  8. package/dist/types/app/layouts/market/use-market-reload.d.ts +14 -0
  9. package/dist/types/app/layouts/market/use-module-versions.d.ts +20 -4
  10. package/dist/types/app/layouts/rightbar/FailureCard.d.ts +3 -3
  11. package/dist/types/app/layouts/rightbar/MarketView.d.ts +5 -3
  12. package/dist/types/app/layouts/rightbar/failure-card-text.d.ts +6 -0
  13. package/dist/types/app/layouts/rightbar/market-row-types.d.ts +2 -0
  14. package/dist/types/app/layouts/rightbar/market-rows.d.ts +2 -0
  15. package/dist/types/app/layouts/rightbar/market-uninstall.d.ts +10 -0
  16. package/dist/types/app/layouts/rightbar/market-update-verdict.d.ts +2 -0
  17. package/dist/types/kernel/contract/market-index.d.ts +1 -1
  18. package/dist/types/modules/panels/model/market-env.d.ts +2 -2
  19. package/dist/types/modules/panels/model/market-swap-stuck.d.ts +20 -0
  20. package/dist/types/modules/panels/model/market-swap-stuck.test.d.ts +1 -0
  21. package/dist/types/modules/panels/model/market-types.d.ts +3 -0
  22. package/dist/types/modules/panels/model/market-update-bundle.d.ts +5 -0
  23. package/dist/types/modules/panels/model/plugin-actions.d.ts +1 -1
  24. package/dist/types/shared/api/host/self-update-marker.d.ts +5 -0
  25. package/dist/types/shared/api/host/self-update-marker.test.d.ts +1 -0
  26. package/package.json +3 -3
  27. package/skills/workbench-module-diy/TOOLS.md +11 -1
  28. package/skills/workbench-module-diy/WIRING.md +2 -1
@@ -1,4 +1,4 @@
1
- import path, { basename, delimiter, dirname, join, resolve } from "node:path";
1
+ import path, { basename, delimiter, dirname, join, relative, resolve, sep } from "node:path";
2
2
  import dayjs from "dayjs";
3
3
  import utc from "dayjs/plugin/utc.js";
4
4
  import { createHash, randomUUID } from "node:crypto";
@@ -144,7 +144,8 @@ var NEED_KEYS = [
144
144
  "sources",
145
145
  "secret",
146
146
  "install",
147
- "docs"
147
+ "docs",
148
+ "runs"
148
149
  ];
149
150
  var SOURCE_KEYS = [
150
151
  "kind",
@@ -251,6 +252,15 @@ function binInstallProblemsOf(raw, at) {
251
252
  }
252
253
  return errors;
253
254
  }
255
+ function libRunsProblemsOf(raw, at) {
256
+ if (raw === void 0) return [];
257
+ if (!Array.isArray(raw)) return [`${at}.runs 要是数组`];
258
+ if (raw.length === 0) return [`${at}.runs 是空数组 —— 不自己跑命令就整条删掉`];
259
+ const names = raw.map((one) => text$3(one));
260
+ if (names.some((one) => one === "")) return [`${at}.runs 里有空字符串`];
261
+ if (new Set(names).size !== names.length) return [`${at}.runs 里有重复的运行时`];
262
+ return [];
263
+ }
254
264
  function perKindProblemsOf(kind, raw, at) {
255
265
  const errors = [];
256
266
  if (kind === "bin" || kind === "lib" || kind === "env") {
@@ -258,6 +268,7 @@ function perKindProblemsOf(kind, raw, at) {
258
268
  }
259
269
  if (kind === "bin") errors.push(...binInstallProblemsOf(raw["install"], at));
260
270
  if (raw["docs"] !== void 0 && text$3(raw["docs"]) === "") errors.push(`${at}.docs 是空串`);
271
+ if (kind === "lib") errors.push(...libRunsProblemsOf(raw["runs"], at));
261
272
  if (kind === "extension") {
262
273
  if (text$3(raw["runtime"]) === "") errors.push(`${at}.runtime 没写 —— 扩展属于哪个运行时(如 opencli)`);
263
274
  if (text$3(raw["name"]) === "") errors.push(`${at}.name 没写`);
@@ -1134,7 +1145,7 @@ var PHASES = [
1134
1145
  function asRecord$7(value) {
1135
1146
  return typeof value === "object" && value !== null ? value : void 0;
1136
1147
  }
1137
- function textOf$5(value, max) {
1148
+ function textOf$6(value, max) {
1138
1149
  if (typeof value !== "string") return "";
1139
1150
  const trimmed = value.trim();
1140
1151
  return trimmed.length > max ? trimmed.slice(0, max) : trimmed;
@@ -1147,8 +1158,8 @@ function atOf(value) {
1147
1158
  function errorOf(raw) {
1148
1159
  const record = asRecord$7(raw);
1149
1160
  if (record === void 0) return void 0;
1150
- const code = textOf$5(record["code"], 120);
1151
- const message = textOf$5(record["message"], PANEL_STATE_MAX_TEXT);
1161
+ const code = textOf$6(record["code"], 120);
1162
+ const message = textOf$6(record["message"], PANEL_STATE_MAX_TEXT);
1152
1163
  if (code === "" && message === "") return void 0;
1153
1164
  return {
1154
1165
  code: code === "" ? "unknown" : code,
@@ -1167,7 +1178,7 @@ function parseCardsReport(payload) {
1167
1178
  ok: false,
1168
1179
  message: "请求体要是一个对象"
1169
1180
  };
1170
- const workspace = textOf$5(record["workspace"], PANEL_STATE_MAX_TEXT);
1181
+ const workspace = textOf$6(record["workspace"], PANEL_STATE_MAX_TEXT);
1171
1182
  if (workspace === "") return {
1172
1183
  ok: false,
1173
1184
  message: "workspace 不能是空的 —— 面板按工作区存"
@@ -1181,9 +1192,9 @@ function parseCardsReport(payload) {
1181
1192
  for (const item of raw.slice(0, 32)) {
1182
1193
  const card = asRecord$7(item);
1183
1194
  if (card === void 0) continue;
1184
- const unit = textOf$5(card["unit"], 200);
1185
- const route = textOf$5(card["route"], 400);
1186
- const phase = textOf$5(card["phase"], 20);
1195
+ const unit = textOf$6(card["unit"], 200);
1196
+ const route = textOf$6(card["route"], 400);
1197
+ const phase = textOf$6(card["phase"], 20);
1187
1198
  const at = atOf(card["at"]);
1188
1199
  if (unit === "" || route === "" || at === null || !PHASES.includes(phase)) continue;
1189
1200
  const error = errorOf(card["error"]);
@@ -1218,9 +1229,9 @@ function parseCrashReport(payload) {
1218
1229
  ok: false,
1219
1230
  message: "请求体要是一个对象"
1220
1231
  };
1221
- const workspace = textOf$5(record["workspace"], PANEL_STATE_MAX_TEXT);
1222
- const unit = textOf$5(record["unit"], 200);
1223
- const reason = textOf$5(record["reason"], PANEL_STATE_MAX_TEXT);
1232
+ const workspace = textOf$6(record["workspace"], PANEL_STATE_MAX_TEXT);
1233
+ const unit = textOf$6(record["unit"], 200);
1234
+ const reason = textOf$6(record["reason"], PANEL_STATE_MAX_TEXT);
1224
1235
  if (workspace === "") return {
1225
1236
  ok: false,
1226
1237
  message: "workspace 不能是空的"
@@ -2033,7 +2044,7 @@ var PLUGIN_LEDGER_FILE = "plugins.json";
2033
2044
  function ledgerKeyOf(entry) {
2034
2045
  return `${entry.opencliHome}\u0000${entry.package}`;
2035
2046
  }
2036
- function textOf$4(value) {
2047
+ function textOf$5(value) {
2037
2048
  if (typeof value !== "string") return void 0;
2038
2049
  const trimmed = value.trim();
2039
2050
  return trimmed === "" ? void 0 : trimmed;
@@ -2041,13 +2052,13 @@ function textOf$4(value) {
2041
2052
  function entryOf$3(value) {
2042
2053
  if (typeof value !== "object" || value === null) return void 0;
2043
2054
  const box = value;
2044
- const opencliHome = textOf$4(box["opencliHome"]);
2045
- const pkg = textOf$4(box["package"]);
2046
- const installedAt = textOf$4(box["installedAt"]);
2055
+ const opencliHome = textOf$5(box["opencliHome"]);
2056
+ const pkg = textOf$5(box["package"]);
2057
+ const installedAt = textOf$5(box["installedAt"]);
2047
2058
  if (opencliHome === void 0 || pkg === void 0 || installedAt === void 0) return void 0;
2048
- const name = textOf$4(box["name"]);
2049
- const registry = textOf$4(box["registry"]);
2050
- const releasedAt = textOf$4(box["releasedAt"]);
2059
+ const name = textOf$5(box["name"]);
2060
+ const registry = textOf$5(box["registry"]);
2061
+ const releasedAt = textOf$5(box["releasedAt"]);
2051
2062
  return {
2052
2063
  opencliHome,
2053
2064
  package: pkg,
@@ -2350,6 +2361,93 @@ function sessionIdOf(exec) {
2350
2361
  return typeof id === "string" && id.trim() !== "" ? id.trim() : null;
2351
2362
  }
2352
2363
  //#endregion
2364
+ //#region ../shared/dist/host/opencli-runner.js
2365
+ var AUTH_REQUIRED_EXIT_CODE = 77;
2366
+ function textOf$4(value) {
2367
+ return typeof value === "string" ? value : "";
2368
+ }
2369
+ function failedOf(args, error) {
2370
+ const head = `Command failed: opencli ${args.join(" ")}`;
2371
+ const auth = error?.pageCode === "auth-required";
2372
+ const helpUrl = textOf$4(error?.helpUrl);
2373
+ const detail = textOf$4(error?.detail);
2374
+ const hint = textOf$4(error?.hint);
2375
+ const failed = new Error([
2376
+ head,
2377
+ ...auth ? ["code: AUTH_REQUIRED", `message: ${textOf$4(error?.message)}`] : [textOf$4(error?.message) || "opencli 这条命令没跑通"],
2378
+ ...detail === "" ? [] : [detail],
2379
+ ...hint === "" ? [] : [hint],
2380
+ ...auth && helpUrl !== "" ? [`help: Please open Chrome or Chromium and log in to ${helpUrl}`] : []
2381
+ ].join("\n"));
2382
+ if (auth) failed.code = AUTH_REQUIRED_EXIT_CODE;
2383
+ return failed;
2384
+ }
2385
+ /**
2386
+ * 把读缝包成一条 opencli 命令的执行器:给还按 `execFile('opencli', args)` 收结果的编排库用,
2387
+ * 让它们跑在面板用的那份 opencli 上(同一套插件、同一份登录态)。
2388
+ *
2389
+ * 失败按 opencli 自己的约定还原:未登录是 `code: AUTH_REQUIRED` + 退出码 77,编排库的
2390
+ * 「等登录再重试」才接得上;其它失败保留 `Command failed: opencli …` 那句外壳。
2391
+ * @param read 模块手里的读缝(`data.read`)。
2392
+ * @param options `timeoutMs` 透传给读缝;不传就用读缝自己的默认值。
2393
+ * @returns 执行器;直接交给编排库的 `setOpencliRunner`。
2394
+ */
2395
+ function opencliRunnerOf(read, options = {}) {
2396
+ return async (args) => {
2397
+ const result = await read({
2398
+ kind: "command",
2399
+ adapter: "opencli",
2400
+ argv: [...args],
2401
+ ...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs }
2402
+ });
2403
+ if (result.ok) return { stdout: JSON.stringify(result.data) };
2404
+ throw failedOf(args, result.error);
2405
+ };
2406
+ }
2407
+ //#endregion
2408
+ //#region ../shared/dist/host/lib-runners.js
2409
+ function runnerOf(runtimes, runtime) {
2410
+ return runtime === "opencli" ? runtimes.opencli : void 0;
2411
+ }
2412
+ function setterNameOf(runtime) {
2413
+ return `set${runtime.charAt(0).toUpperCase()}${runtime.slice(1)}Runner`;
2414
+ }
2415
+ /**
2416
+ * 给声明了 `runs` 的 lib 注入「跑这个运行时」的执行器:库侧按约定暴露 `setOpencliRunner`
2417
+ * (运行时名字首字母大写 + `Runner`),外壳在每次 `load` 之后把执行器交给它。
2418
+ *
2419
+ * 没暴露的(版本旧)就保持它自己的行为(按 PATH 找),只记一条 warn;不熟悉的运行时同样只 warn。
2420
+ * @param libs 原来的读库口。
2421
+ * @param runtimes 外壳手上有的执行器,按运行时名字给。
2422
+ * @param logger 记 warn 的地方(默认不记)。
2423
+ * @returns 包好的读库口;`load` 的签名与返回不变。
2424
+ */
2425
+ function withLibRunners(libs, runtimes, logger) {
2426
+ const warned = /* @__PURE__ */ new Set();
2427
+ const warnOnce = (key, message) => {
2428
+ if (warned.has(key)) return;
2429
+ warned.add(key);
2430
+ logger?.warn?.(message);
2431
+ };
2432
+ return { async load(need, entry) {
2433
+ const module = await libs.load(need, entry);
2434
+ for (const runtime of need.runs ?? []) {
2435
+ const runner = runnerOf(runtimes, runtime);
2436
+ if (runner === void 0) {
2437
+ warnOnce(`${need.name}:${runtime}:unsupported`, `[dsh-workbench] ${need.name} 声明了自己会跑 ${runtime},但工作台没有这个执行器 —— 它会按自己的方式找`);
2438
+ continue;
2439
+ }
2440
+ const setter = typeof module === "object" && module !== null ? module[setterNameOf(runtime)] : void 0;
2441
+ if (typeof setter !== "function") {
2442
+ warnOnce(`${need.name}:${entry}:${runtime}:missing`, `[dsh-workbench] ${need.name} 声明了自己会跑 ${runtime},但这一版没有 ${setterNameOf(runtime)} —— 它会按 PATH 找`);
2443
+ continue;
2444
+ }
2445
+ setter.call(module, runner);
2446
+ }
2447
+ return module;
2448
+ } };
2449
+ }
2450
+ //#endregion
2353
2451
  //#region ../shared/dist/host/value-handoff.js
2354
2452
  function handoffId(entry) {
2355
2453
  return `${String(entry.fact.length)}:${entry.fact}${entry.key}`;
@@ -4896,6 +4994,11 @@ function checkVersionRange(contract, version) {
4896
4994
  if (parsed === null) return "malformed";
4897
4995
  return import_semver.default.satisfies(parsed, range) ? "satisfied" : "unsatisfied";
4898
4996
  }
4997
+ //#endregion
4998
+ //#region ../shared/dist/contract/protocol/market-index.js
4999
+ var MARKET_INDEX_DIR = "market";
5000
+ var MARKET_ARTIFACTS_DIR = "artifacts";
5001
+ var MARKET_INDEX_PATH = "market.json";
4899
5002
  var MARKET_DISCOVER_ROUTE = "panels/market-discover";
4900
5003
  var MARKET_DOC_ROUTE = "panels/market-doc";
4901
5004
  var MARKET_GIT_PROBE_ROUTE = "panels/git-probe";
@@ -4917,34 +5020,39 @@ function marketText(value, locale) {
4917
5020
  return locale.toLowerCase().startsWith("zh") ? zh || en : en || zh;
4918
5021
  }
4919
5022
  //#endregion
4920
- //#region ../shared/dist/host/market-index.js
4921
- var MARKET_INDEX_SEED = "{\n \"format\": 1,\n \"plugins\": []\n}\n";
4922
- var MARKET_INDEX_README_LINES = [
4923
- "# 模块市场索引",
4924
- "",
4925
- "这个仓库给 DeepSeek Harness 工作台当模块市场索引:`market.json`(format 1)列出可安装的模块与版本。",
4926
- "",
4927
- "## 发布模块",
4928
- "",
4929
- "在模块包里运行:",
4930
- "",
4931
- "```bash",
4932
- "dsh-wb-module release --index <本仓库地址> --index-policy artifacts-in-repo",
4933
- "```",
4934
- "",
4935
- "产物会提交到 `packages/<包名>/`(带 scope 时是 `packages/@scope/名字/`),并更新 `market.json`。",
4936
- "",
4937
- "一个包名归第一次发布的人(`market.json` 里的 `owner`)。换一个署名发同名会被拒;要换人,先把这一条的 `owner` 改成新的人。",
4938
- "同一个市场里,一个模块 id(`metadata.unit`)只能被一个条目使用;另一个包名想用同一个 id / 产物目录也会被拒。",
4939
- "",
4940
- "## 使用这个索引",
4941
- "",
4942
- "在工作台的「模块市场 → 发现」里添加本仓库地址,即可浏览与安装。",
4943
- ""
4944
- ];
5023
+ //#region ../shared/dist/host/market-index-parse.js
4945
5024
  var NPM_NAME = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
4946
5025
  var GIT_COMMIT = /^[0-9a-f]{40}$/;
4947
5026
  var MAX_ICON_CHARS = 32768;
5027
+ function compareSemver(left, right) {
5028
+ return compareVersions(left, right);
5029
+ }
5030
+ function httpUrlProblem(url) {
5031
+ let parsed;
5032
+ try {
5033
+ parsed = new URL(url);
5034
+ } catch {
5035
+ return "不是合法 URL";
5036
+ }
5037
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return "只收 http / https";
5038
+ if (parsed.username !== "" || parsed.password !== "") return "URL 不能带凭据";
5039
+ if (parsed.search !== "" || parsed.hash !== "") return "URL 不能带 query / fragment";
5040
+ }
5041
+ function gitUrlOk(url) {
5042
+ if (url === "") return false;
5043
+ if (/^[^@\s]+@[^@\s:]+:[^@\s]+$/.test(url)) return true;
5044
+ let parsed;
5045
+ try {
5046
+ parsed = new URL(url);
5047
+ } catch {
5048
+ return false;
5049
+ }
5050
+ if (parsed.protocol !== "https:" && parsed.protocol !== "ssh:" && parsed.protocol !== "http:") return false;
5051
+ if (parsed.password !== "") return false;
5052
+ if (parsed.username !== "" && parsed.protocol !== "ssh:") return false;
5053
+ if (parsed.search !== "" || parsed.hash !== "") return false;
5054
+ return true;
5055
+ }
4948
5056
  function asRecord$4(value) {
4949
5057
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
4950
5058
  }
@@ -4964,22 +5072,19 @@ function asLocalized(value) {
4964
5072
  ...en === "" ? {} : { en }
4965
5073
  };
4966
5074
  }
4967
- function problem(kind, detail, at) {
5075
+ function problem$1(kind, detail, at) {
4968
5076
  return {
4969
5077
  kind,
4970
5078
  detail,
4971
5079
  ...at === void 0 ? {} : { at }
4972
5080
  };
4973
5081
  }
4974
- function compareSemver(left, right) {
4975
- return compareVersions(left, right);
4976
- }
4977
5082
  function sourceOf(raw, at) {
4978
5083
  const record = asRecord$4(raw);
4979
- if (record === void 0) return problem("source", "source 必须是一个对象", at);
5084
+ if (record === void 0) return problem$1("source", "source 必须是一个对象", at);
4980
5085
  if (record["type"] === "npm") {
4981
5086
  const registry = record["registry"];
4982
- if (registry !== void 0 && asText$4(registry) === "") return problem("source", "npm source 的 registry 必须是非空字符串", at);
5087
+ if (registry !== void 0 && asText$4(registry) === "") return problem$1("source", "npm source 的 registry 必须是非空字符串", at);
4983
5088
  return {
4984
5089
  type: "npm",
4985
5090
  ...registry === void 0 ? {} : { registry: asText$4(registry) }
@@ -4988,8 +5093,8 @@ function sourceOf(raw, at) {
4988
5093
  if (record["type"] === "git") {
4989
5094
  const url = asText$4(record["url"]);
4990
5095
  const commit = asText$4(record["commit"]);
4991
- if (url === "") return problem("source", "git source 缺 url", at);
4992
- if (!GIT_COMMIT.test(commit)) return problem("git-commit", `git source 的 commit 必须是 40 位小写 SHA(现在是 ${JSON.stringify(commit)})`, at);
5096
+ if (url === "") return problem$1("source", "git source 缺 url", at);
5097
+ if (!GIT_COMMIT.test(commit)) return problem$1("git-commit", `git source 的 commit 必须是 40 位小写 SHA(现在是 ${JSON.stringify(commit)})`, at);
4993
5098
  const path = record["path"];
4994
5099
  return {
4995
5100
  type: "git",
@@ -4998,17 +5103,17 @@ function sourceOf(raw, at) {
4998
5103
  ...path === void 0 || asText$4(path) === "" ? {} : { path: asText$4(path) }
4999
5104
  };
5000
5105
  }
5001
- return problem("source", "source.type 只能是 npm 或 git", at);
5106
+ return problem$1("source", "source.type 只能是 npm 或 git", at);
5002
5107
  }
5003
5108
  function versionOf(raw, at) {
5004
5109
  const record = asRecord$4(raw);
5005
- if (record === void 0) return problem("version", "版本必须是一个对象", at);
5110
+ if (record === void 0) return problem$1("version", "版本必须是一个对象", at);
5006
5111
  const version = asText$4(record["version"]);
5007
- if (!isValidVersion(version)) return problem("version", `版本号不是合法 SemVer:${JSON.stringify(version)}`, at);
5112
+ if (!isValidVersion(version)) return problem$1("version", `版本号不是合法 SemVer:${JSON.stringify(version)}`, at);
5008
5113
  const source = sourceOf(record["source"], `${at}.source`);
5009
5114
  if ("kind" in source) return source;
5010
5115
  const timestamp = record["publishTimestamp"];
5011
- if (timestamp !== void 0 && (!Number.isInteger(timestamp) || timestamp < 0)) return problem("timestamp", "publishTimestamp 必须是 Unix 秒整数", at);
5116
+ if (timestamp !== void 0 && (!Number.isInteger(timestamp) || timestamp < 0)) return problem$1("timestamp", "publishTimestamp 必须是 Unix 秒整数", at);
5012
5117
  const engines = asRecord$4(record["engines"]);
5013
5118
  const dsh = engines === void 0 ? "" : asText$4(engines["dsh"]);
5014
5119
  return {
@@ -5019,33 +5124,33 @@ function versionOf(raw, at) {
5019
5124
  };
5020
5125
  }
5021
5126
  function versionsOf$1(raw, at) {
5022
- if (!Array.isArray(raw) || raw.length === 0) return problem("versions", "versions 必须是非空数组", at);
5127
+ if (!Array.isArray(raw) || raw.length === 0) return problem$1("versions", "versions 必须是非空数组", at);
5023
5128
  const versions = [];
5024
5129
  const seen = /* @__PURE__ */ new Set();
5025
5130
  for (const [index, item] of raw.entries()) {
5026
5131
  const parsed = versionOf(item, `${at}[${String(index)}]`);
5027
5132
  if ("kind" in parsed) return parsed;
5028
- if (seen.has(parsed.version)) return problem("versions", `版本重复:${parsed.version}`, at);
5133
+ if (seen.has(parsed.version)) return problem$1("versions", `版本重复:${parsed.version}`, at);
5029
5134
  seen.add(parsed.version);
5030
5135
  versions.push(parsed);
5031
5136
  }
5032
5137
  for (let index = 1; index < versions.length; index += 1) {
5033
5138
  const order = compareSemver(versions[index - 1]?.version ?? "", versions[index]?.version ?? "");
5034
- if (order === void 0 || order < 0) return problem("version-order", "versions 必须按 SemVer 降序", at);
5139
+ if (order === void 0 || order < 0) return problem$1("version-order", "versions 必须按 SemVer 降序", at);
5035
5140
  }
5036
5141
  return versions;
5037
5142
  }
5038
5143
  function iconProblem(icon) {
5039
- if (icon.length > MAX_ICON_CHARS) return problem("icon", "icon 超过 32 KiB");
5040
- if (icon.startsWith("data:image/")) return icon.includes(";base64,") ? void 0 : problem("icon", "data URL 必须是 base64");
5144
+ if (icon.length > MAX_ICON_CHARS) return problem$1("icon", "icon 超过 32 KiB");
5145
+ if (icon.startsWith("data:image/")) return icon.includes(";base64,") ? void 0 : problem$1("icon", "data URL 必须是 base64");
5041
5146
  if (/^https?:\/\//i.test(icon)) return void 0;
5042
- return problem("icon", "icon 只能是 base64 data URL 或 http(s) URL");
5147
+ return problem$1("icon", "icon 只能是 base64 data URL 或 http(s) URL");
5043
5148
  }
5044
5149
  var MAX_OWNER_CHARS = 200;
5045
5150
  var UNIT_ID = /^[a-z][a-z0-9-]*$/;
5046
5151
  function ownerProblem(owner) {
5047
- if (/[\r\n]/u.test(owner)) return problem("owner", "owner 只能是一行署名");
5048
- if (owner.length > MAX_OWNER_CHARS) return problem("owner", "owner 超过 200 字");
5152
+ if (/[\r\n]/u.test(owner)) return problem$1("owner", "owner 只能是一行署名");
5153
+ if (owner.length > MAX_OWNER_CHARS) return problem$1("owner", "owner 超过 200 字");
5049
5154
  }
5050
5155
  function metadataOf(raw) {
5051
5156
  const record = asRecord$4(raw);
@@ -5068,9 +5173,9 @@ function metadataOf(raw) {
5068
5173
  }
5069
5174
  function entryOf$1(raw, at) {
5070
5175
  const record = asRecord$4(raw);
5071
- if (record === void 0) return problem("plugin", "插件条目必须是对象", at);
5176
+ if (record === void 0) return problem$1("plugin", "插件条目必须是对象", at);
5072
5177
  const name = asText$4(record["name"]);
5073
- if (name === "" || name.length > 214 || !NPM_NAME.test(name)) return problem("name", `不是合法 npm 包名:${JSON.stringify(name)}`, at);
5178
+ if (name === "" || name.length > 214 || !NPM_NAME.test(name)) return problem$1("name", `不是合法 npm 包名:${JSON.stringify(name)}`, at);
5074
5179
  const versions = versionsOf$1(record["versions"], `${at}.versions`);
5075
5180
  if ("kind" in versions) return versions;
5076
5181
  const icon = asText$4(record["icon"]);
@@ -5088,7 +5193,7 @@ function entryOf$1(raw, at) {
5088
5193
  }
5089
5194
  const homepage = asText$4(record["homepage"]);
5090
5195
  const metadata = metadataOf(record["metadata"]);
5091
- if (metadata?.unit !== void 0 && !UNIT_ID.test(metadata.unit)) return problem("unit", `metadata.unit 只能是小写字母开头的 a-z0-9-(现在是 ${JSON.stringify(metadata.unit)})`, at);
5196
+ if (metadata?.unit !== void 0 && !UNIT_ID.test(metadata.unit)) return problem$1("unit", `metadata.unit 只能是小写字母开头的 a-z0-9-(现在是 ${JSON.stringify(metadata.unit)})`, at);
5092
5197
  return {
5093
5198
  name,
5094
5199
  versions,
@@ -5105,16 +5210,16 @@ function parseMarketIndex(raw) {
5105
5210
  const record = asRecord$4(raw);
5106
5211
  if (record === void 0) return {
5107
5212
  ok: false,
5108
- problems: [problem("not-object", "索引必须是一个 JSON 对象")]
5213
+ problems: [problem$1("not-object", "索引必须是一个 JSON 对象")]
5109
5214
  };
5110
5215
  if (record["format"] !== 1) return {
5111
5216
  ok: false,
5112
- problems: [problem("format", `format 必须是 ${String(1)}(现在是 ${JSON.stringify(record["format"])})`)]
5217
+ problems: [problem$1("format", `format 必须是 ${String(1)}(现在是 ${JSON.stringify(record["format"])})`)]
5113
5218
  };
5114
5219
  const plugins = record["plugins"];
5115
5220
  if (!Array.isArray(plugins)) return {
5116
5221
  ok: false,
5117
- problems: [problem("plugins", "plugins 必须是数组")]
5222
+ problems: [problem$1("plugins", "plugins 必须是数组")]
5118
5223
  };
5119
5224
  const entries = [];
5120
5225
  const problems = [];
@@ -5126,7 +5231,7 @@ function parseMarketIndex(raw) {
5126
5231
  continue;
5127
5232
  }
5128
5233
  if (seen.has(parsed.name)) {
5129
- problems.push(problem("duplicate-name", `插件名重复:${parsed.name}`, `plugins[${String(index)}]`));
5234
+ problems.push(problem$1("duplicate-name", `插件名重复:${parsed.name}`, `plugins[${String(index)}]`));
5130
5235
  continue;
5131
5236
  }
5132
5237
  seen.add(parsed.name);
@@ -5156,10 +5261,13 @@ function parseMarketIndexJson(text) {
5156
5261
  } catch (error) {
5157
5262
  return {
5158
5263
  ok: false,
5159
- problems: [problem("not-json", `JSON 解析失败:${error instanceof Error ? error.message : String(error)}`)]
5264
+ problems: [problem$1("not-json", `JSON 解析失败:${error instanceof Error ? error.message : String(error)}`)]
5160
5265
  };
5161
5266
  }
5162
5267
  }
5268
+ function sortMarketVersions(versions) {
5269
+ return [...versions].sort((left, right) => compareSemver(right.version, left.version) ?? 0);
5270
+ }
5163
5271
  function marketEntryLabels(entry) {
5164
5272
  return entry.metadata?.labels ?? [];
5165
5273
  }
@@ -5195,7 +5303,7 @@ function parseSourceConfig(raw) {
5195
5303
  const record = asRecord$4(raw);
5196
5304
  if (record === void 0) return {
5197
5305
  ok: false,
5198
- problem: problem("source", "索引源必须是对象")
5306
+ problem: problem$1("source", "索引源必须是对象")
5199
5307
  };
5200
5308
  const id = asText$4(record["id"]);
5201
5309
  const label = asText$4(record["label"]);
@@ -5203,21 +5311,21 @@ function parseSourceConfig(raw) {
5203
5311
  const url = asText$4(record["url"]);
5204
5312
  if (id === "") return {
5205
5313
  ok: false,
5206
- problem: problem("source", "索引源缺 id")
5314
+ problem: problem$1("source", "索引源缺 id")
5207
5315
  };
5208
5316
  if (label === "") return {
5209
5317
  ok: false,
5210
- problem: problem("source", "索引源缺 label")
5318
+ problem: problem$1("source", "索引源缺 label")
5211
5319
  };
5212
5320
  if (kind !== "http" && kind !== "git") return {
5213
5321
  ok: false,
5214
- problem: problem("source", "kind 只能是 http 或 git")
5322
+ problem: problem$1("source", "kind 只能是 http 或 git")
5215
5323
  };
5216
5324
  if (kind === "http") {
5217
5325
  const bad = httpUrlProblem(url);
5218
5326
  if (bad !== void 0) return {
5219
5327
  ok: false,
5220
- problem: problem("source", bad)
5328
+ problem: problem$1("source", bad)
5221
5329
  };
5222
5330
  return {
5223
5331
  ok: true,
@@ -5231,7 +5339,7 @@ function parseSourceConfig(raw) {
5231
5339
  }
5232
5340
  if (!gitUrlOk(url)) return {
5233
5341
  ok: false,
5234
- problem: problem("source", "git 地址不合格(只收 https / ssh / scp 形式,且不能带凭据)")
5342
+ problem: problem$1("source", "git 地址不合格(只收 https / ssh / scp 形式,且不能带凭据)")
5235
5343
  };
5236
5344
  const ref = asText$4(record["ref"]);
5237
5345
  const path = asText$4(record["path"]);
@@ -5239,7 +5347,7 @@ function parseSourceConfig(raw) {
5239
5347
  const policy = record["policy"];
5240
5348
  if (policy !== void 0 && policy !== "artifacts-in-repo" && policy !== "catalog-only") return {
5241
5349
  ok: false,
5242
- problem: problem("source", "policy 只能是 artifacts-in-repo 或 catalog-only")
5350
+ problem: problem$1("source", "policy 只能是 artifacts-in-repo 或 catalog-only")
5243
5351
  };
5244
5352
  return {
5245
5353
  ok: true,
@@ -5255,31 +5363,270 @@ function parseSourceConfig(raw) {
5255
5363
  }
5256
5364
  };
5257
5365
  }
5258
- function httpUrlProblem(url) {
5259
- let parsed;
5260
- try {
5261
- parsed = new URL(url);
5262
- } catch {
5263
- return "不是合法 URL";
5366
+ function docFieldsOf(record, at) {
5367
+ const name = asText$4(record["name"]);
5368
+ if (name === "" || name.length > 214 || !NPM_NAME.test(name)) return problem$1("name", `不是合法 npm 包名:${JSON.stringify(name)}`, at);
5369
+ const icon = asText$4(record["icon"]);
5370
+ if (icon !== "") {
5371
+ const badIcon = iconProblem(icon);
5372
+ if (badIcon !== void 0) return badIcon;
5264
5373
  }
5265
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return "只收 http / https";
5266
- if (parsed.username !== "" || parsed.password !== "") return "URL 不能带凭据";
5267
- if (parsed.search !== "" || parsed.hash !== "") return "URL 不能带 query / fragment";
5374
+ const owner = asText$4(record["owner"]);
5375
+ if (owner !== "") {
5376
+ const badOwner = ownerProblem(owner);
5377
+ if (badOwner !== void 0) return badOwner;
5378
+ }
5379
+ const metadata = metadataOf(record["metadata"]);
5380
+ if (metadata?.unit !== void 0 && !UNIT_ID.test(metadata.unit)) return problem$1("unit", `metadata.unit 只能是小写字母开头的 a-z0-9-(现在是 ${JSON.stringify(metadata.unit)})`, at);
5381
+ const title = asLocalized(record["title"]);
5382
+ const description = asLocalized(record["description"]);
5383
+ const author = asText$4(record["author"]);
5384
+ const homepage = asText$4(record["homepage"]);
5385
+ return {
5386
+ name,
5387
+ ...title === void 0 ? {} : { title },
5388
+ ...description === void 0 ? {} : { description },
5389
+ ...author === "" ? {} : { author },
5390
+ ...owner === "" ? {} : { owner },
5391
+ ...homepage === "" ? {} : { homepage },
5392
+ ...icon === "" ? {} : { icon },
5393
+ ...metadata === void 0 ? {} : { metadata }
5394
+ };
5268
5395
  }
5269
- function gitUrlOk(url) {
5270
- if (url === "") return false;
5271
- if (/^[^@\s]+@[^@\s:]+:[^@\s]+$/.test(url)) return true;
5272
- let parsed;
5273
- try {
5274
- parsed = new URL(url);
5275
- } catch {
5276
- return false;
5396
+ function parseMarketEntryDoc(raw) {
5397
+ const record = asRecord$4(raw);
5398
+ if (record === void 0) return {
5399
+ ok: false,
5400
+ problems: [problem$1("entry", "条目文件必须是一个 JSON 对象")]
5401
+ };
5402
+ const fields = docFieldsOf(record, "entry");
5403
+ if ("kind" in fields) return {
5404
+ ok: false,
5405
+ problems: [fields]
5406
+ };
5407
+ const version = asText$4(record["version"]);
5408
+ const timestamp = record["publishTimestamp"];
5409
+ if (timestamp !== void 0 && (!Number.isInteger(timestamp) || timestamp < 0)) return {
5410
+ ok: false,
5411
+ problems: [problem$1("timestamp", "publishTimestamp 必须是 Unix 秒整数", "entry")]
5412
+ };
5413
+ if (version === "") {
5414
+ if (timestamp === void 0) return {
5415
+ ok: false,
5416
+ problems: [problem$1("timestamp", "元数据文件要有 publishTimestamp", "entry")]
5417
+ };
5418
+ const epoch = record["epoch"];
5419
+ if (epoch !== void 0 && (!Number.isInteger(epoch) || epoch < 0)) return {
5420
+ ok: false,
5421
+ problems: [problem$1("timestamp", "epoch 必须是非负整数", "entry")]
5422
+ };
5423
+ return {
5424
+ ok: true,
5425
+ doc: {
5426
+ ...fields,
5427
+ publishTimestamp: timestamp,
5428
+ ...epoch === void 0 ? {} : { epoch }
5429
+ }
5430
+ };
5277
5431
  }
5278
- if (parsed.protocol !== "https:" && parsed.protocol !== "ssh:" && parsed.protocol !== "http:") return false;
5279
- if (parsed.password !== "") return false;
5280
- if (parsed.username !== "" && parsed.protocol !== "ssh:") return false;
5281
- if (parsed.search !== "" || parsed.hash !== "") return false;
5282
- return true;
5432
+ if (!isValidVersion(version)) return {
5433
+ ok: false,
5434
+ problems: [problem$1("version", `版本号不是合法 SemVer:${JSON.stringify(version)}`, "entry")]
5435
+ };
5436
+ const source = sourceOf(record["source"], "entry.source");
5437
+ if ("kind" in source) return {
5438
+ ok: false,
5439
+ problems: [source]
5440
+ };
5441
+ const engines = asRecord$4(record["engines"]);
5442
+ const dsh = engines === void 0 ? "" : asText$4(engines["dsh"]);
5443
+ return {
5444
+ ok: true,
5445
+ doc: {
5446
+ ...fields,
5447
+ version,
5448
+ source,
5449
+ ...timestamp === void 0 ? {} : { publishTimestamp: timestamp },
5450
+ ...dsh === "" ? {} : { engines: { dsh } }
5451
+ }
5452
+ };
5453
+ }
5454
+ function indexPathBase(dir) {
5455
+ return dir.replace(/^\/+|\/+$/g, "") || "market";
5456
+ }
5457
+ function marketEntryPath(dir, name, version) {
5458
+ if (!NPM_NAME.test(name)) throw new Error(`不是合法包名:${JSON.stringify(name)}`);
5459
+ if (!isValidVersion(version)) throw new Error(`不是合法版本号:${JSON.stringify(version)}`);
5460
+ return `${indexPathBase(dir)}/${name}/${version}.json`;
5461
+ }
5462
+ //#endregion
5463
+ //#region ../shared/dist/host/market-index.js
5464
+ var MARKET_ENTRIES_README_LINES = [
5465
+ "# 索引目录",
5466
+ "",
5467
+ "一个包一个目录,一个版本一个文件:`market/<包名>/<版本>.json`;产物在 `artifacts/<包名>/<版本>/`。",
5468
+ "",
5469
+ "`meta-<epoch>-<随机后缀>.json` 是只改标题 / 署名 / 说明时追加的元数据快照,读取时按 epoch 取最新的一份。"
5470
+ ];
5471
+ var MARKET_INDEX_README_LINES = [
5472
+ "# 模块市场索引",
5473
+ "",
5474
+ "这个仓库给 DeepSeek Harness 工作台当模块市场索引:条目在 `market/<包名>/<版本>.json`,产物在 `artifacts/<包名>/<版本>/`。",
5475
+ "",
5476
+ "## 发布模块",
5477
+ "",
5478
+ "在模块包里运行:",
5479
+ "",
5480
+ "```bash",
5481
+ "dsh-wb-module release --index <本仓库地址> --index-policy artifacts-in-repo",
5482
+ "```",
5483
+ "",
5484
+ "产物会提交到 `artifacts/<包名>/<版本>/`,条目提交到 `market/<包名>/<版本>.json`。",
5485
+ "发布只新增文件、不改已有文件,两台机器同时发也不会冲突;老条目的 `packages/<包名>/` 路径继续可用。",
5486
+ "",
5487
+ "一个包名归第一次发布的人(条目文件里的 `owner`)。换一个署名发同名会被拒;要换人,先把这一条的 `owner` 改成新的人。",
5488
+ "同一个市场里,一个模块 id(`metadata.unit`)只能被一个条目使用;另一个包名想用同一个 id / 产物目录也会被拒。",
5489
+ "",
5490
+ "## 使用这个索引",
5491
+ "",
5492
+ "在工作台的「模块市场 → 发现」里添加本仓库地址,即可浏览与安装。",
5493
+ ""
5494
+ ];
5495
+ function problem(kind, detail, at) {
5496
+ return {
5497
+ kind,
5498
+ detail,
5499
+ ...at === void 0 ? {} : { at }
5500
+ };
5501
+ }
5502
+ function docFieldsFromEntry(entry) {
5503
+ return {
5504
+ name: entry.name,
5505
+ ...entry.title === void 0 ? {} : { title: entry.title },
5506
+ ...entry.description === void 0 ? {} : { description: entry.description },
5507
+ ...entry.author === void 0 ? {} : { author: entry.author },
5508
+ ...entry.owner === void 0 ? {} : { owner: entry.owner },
5509
+ ...entry.homepage === void 0 ? {} : { homepage: entry.homepage },
5510
+ ...entry.icon === void 0 ? {} : { icon: entry.icon },
5511
+ ...entry.metadata === void 0 ? {} : { metadata: entry.metadata }
5512
+ };
5513
+ }
5514
+ function marketVersionDocOf(entry, version) {
5515
+ return {
5516
+ ...docFieldsFromEntry(entry),
5517
+ ...version
5518
+ };
5519
+ }
5520
+ function assembleMarketIndex(files) {
5521
+ const problems = [];
5522
+ const groups = /* @__PURE__ */ new Map();
5523
+ const ordered = [...files].sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0);
5524
+ for (const file of ordered) {
5525
+ let raw;
5526
+ try {
5527
+ raw = JSON.parse(file.text);
5528
+ } catch (error) {
5529
+ problems.push(problem("not-json", `JSON 解析失败:${error instanceof Error ? error.message : String(error)}`, file.path));
5530
+ continue;
5531
+ }
5532
+ const parsed = parseMarketEntryDoc(raw);
5533
+ if (!parsed.ok) {
5534
+ for (const one of parsed.problems) problems.push({
5535
+ ...one,
5536
+ at: file.path
5537
+ });
5538
+ continue;
5539
+ }
5540
+ const doc = parsed.doc;
5541
+ const group = groups.get(doc.name) ?? {
5542
+ versions: /* @__PURE__ */ new Map(),
5543
+ meta: void 0,
5544
+ fallback: void 0
5545
+ };
5546
+ const at = doc.publishTimestamp ?? 0;
5547
+ if ("version" in doc) {
5548
+ if (group.versions.has(doc.version)) problems.push(problem("duplicate-version", `版本重复:${doc.version}`, file.path));
5549
+ group.versions.set(doc.version, {
5550
+ version: doc.version,
5551
+ source: doc.source,
5552
+ ...doc.publishTimestamp === void 0 ? {} : { publishTimestamp: doc.publishTimestamp },
5553
+ ...doc.engines === void 0 ? {} : { engines: doc.engines }
5554
+ });
5555
+ if (group.fallback === void 0 || at > group.fallback.at || at === group.fallback.at && file.path > group.fallback.path) group.fallback = {
5556
+ at,
5557
+ path: file.path,
5558
+ doc
5559
+ };
5560
+ } else {
5561
+ const epoch = doc.epoch ?? 0;
5562
+ if (group.meta === void 0 || epoch > group.meta.epoch || epoch === group.meta.epoch && at > group.meta.at || epoch === group.meta.epoch && at === group.meta.at && file.path > group.meta.path) group.meta = {
5563
+ epoch,
5564
+ at,
5565
+ path: file.path,
5566
+ doc
5567
+ };
5568
+ }
5569
+ groups.set(doc.name, group);
5570
+ }
5571
+ if (problems.length > 0) return {
5572
+ ok: false,
5573
+ problems
5574
+ };
5575
+ const plugins = [...groups.entries()].map(([name, group]) => {
5576
+ const meta = group.meta?.doc ?? group.fallback?.doc;
5577
+ return {
5578
+ name,
5579
+ versions: sortMarketVersions([...group.versions.values()]),
5580
+ ...meta?.title === void 0 ? {} : { title: meta.title },
5581
+ ...meta?.description === void 0 ? {} : { description: meta.description },
5582
+ ...meta?.author === void 0 ? {} : { author: meta.author },
5583
+ ...meta?.owner === void 0 ? {} : { owner: meta.owner },
5584
+ ...meta?.homepage === void 0 ? {} : { homepage: meta.homepage },
5585
+ ...meta?.icon === void 0 ? {} : { icon: meta.icon },
5586
+ ...meta?.metadata === void 0 ? {} : { metadata: meta.metadata }
5587
+ };
5588
+ });
5589
+ plugins.sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
5590
+ return {
5591
+ ok: true,
5592
+ index: {
5593
+ format: 1,
5594
+ plugins
5595
+ }
5596
+ };
5597
+ }
5598
+ function marketEntryFilesOf(index, dir = MARKET_INDEX_DIR) {
5599
+ const files = [];
5600
+ for (const entry of index.plugins) for (const version of entry.versions) files.push({
5601
+ path: marketEntryPath(dir, entry.name, version.version),
5602
+ text: `${JSON.stringify(marketVersionDocOf(entry, version), null, 2)}\n`
5603
+ });
5604
+ return files;
5605
+ }
5606
+ function collectMarketEntryFiles(dir) {
5607
+ if (!existsSync(dir)) return [];
5608
+ const files = [];
5609
+ const walk = (current) => {
5610
+ for (const item of readdirSync(current, { withFileTypes: true })) {
5611
+ const full = join(current, item.name);
5612
+ if (item.isDirectory()) {
5613
+ walk(full);
5614
+ continue;
5615
+ }
5616
+ if (!item.name.endsWith(".json")) continue;
5617
+ files.push({
5618
+ path: relative(dir, full).split(sep).join("/"),
5619
+ text: readFileSync(full, "utf8")
5620
+ });
5621
+ }
5622
+ };
5623
+ walk(dir);
5624
+ return files;
5625
+ }
5626
+ function marketEntryFilesOfLegacy(text, dir = MARKET_INDEX_DIR) {
5627
+ const parsed = parseMarketIndexJson(text);
5628
+ if (!parsed.ok) return void 0;
5629
+ return marketEntryFilesOf(parsed.index, dir);
5283
5630
  }
5284
5631
  //#endregion
5285
5632
  //#region ../shared/dist/host/market-index-claim.js
@@ -5324,8 +5671,11 @@ function claimOfEntry(entry, publisher) {
5324
5671
  owner
5325
5672
  };
5326
5673
  }
5327
- function marketArtifactPath(name) {
5328
- return `packages/${name}`;
5674
+ function marketArtifactRoot(name) {
5675
+ return `${MARKET_ARTIFACTS_DIR}/${name}`;
5676
+ }
5677
+ function marketArtifactPathFor(name, version) {
5678
+ return `${MARKET_ARTIFACTS_DIR}/${name}/${version}`;
5329
5679
  }
5330
5680
  function entryUnitOf(entry) {
5331
5681
  const unit = entry.metadata?.unit?.trim();
@@ -6470,7 +6820,7 @@ function publishClaimDenyOf(claim, name) {
6470
6820
  if (claim === void 0) return void 0;
6471
6821
  if (claim.state === "unit-taken") return `市场里 ${claim.entry} 已经用了这个模块 id —— 两个模块不能共用一个,装在一起会撞。给这个模块换一个 unit(dsh.workbench.unit)再发。`;
6472
6822
  if (claim.state === "path-taken") return `市场仓库里的产物目录已经被 ${claim.entry} 占了 —— 换一个包名再发。`;
6473
- if (claim.state === "conflict") return `市场里 ${name} 已经有主了(${claim.owner})—— 同一个名字不能换人发。换一个带团队 scope 的包名;你就是原来的作者(只是换了机器或署名),用 author 参数带上原来的署名;确实是接手,让索引维护者改 market.json 里这一条的 owner。`;
6823
+ if (claim.state === "conflict") return `市场里 ${name} 已经有主了(${claim.owner})—— 同一个名字不能换人发。换一个带团队 scope 的包名;你就是原来的作者(只是换了机器或署名),用 author 参数带上原来的署名;确实是接手,让索引维护者改 market/<包名>/ 条目文件里的 owner。`;
6474
6824
  }
6475
6825
  //#endregion
6476
6826
  //#region src/modules/panels/model/approval.ts
@@ -10148,10 +10498,16 @@ var EMPTY_INDEX = {
10148
10498
  plugins: []
10149
10499
  };
10150
10500
  function publishedIndexOf(source) {
10151
- const file = join(workbenchRoot(), "market-index", marketSourceKey(source), "worktree", source.path ?? "market.json");
10152
- if (!existsSync(file)) return void 0;
10501
+ const root = join(workbenchRoot(), "market-index", marketSourceKey(source), "worktree");
10153
10502
  try {
10154
- const parsed = parseMarketIndexJson(readFileSync(file, "utf8"));
10503
+ const files = collectMarketEntryFiles(join(root, source.path ?? "market"));
10504
+ if (files.length > 0) {
10505
+ const assembled = assembleMarketIndex(files);
10506
+ return assembled.ok ? assembled.index : void 0;
10507
+ }
10508
+ const legacy = join(root, "market.json");
10509
+ if (!existsSync(legacy)) return void 0;
10510
+ const parsed = parseMarketIndexJson(readFileSync(legacy, "utf8"));
10155
10511
  return parsed.ok ? parsed.index : void 0;
10156
10512
  } catch {
10157
10513
  return;
@@ -10192,8 +10548,8 @@ function marketOf(dir, name, version) {
10192
10548
  market: {
10193
10549
  url: source.url,
10194
10550
  ...source.ref === void 0 ? {} : { ref: source.ref },
10195
- indexPath: source.path ?? "market.json",
10196
- artifactPath: marketArtifactPath(name),
10551
+ indexPath: source.path ?? "market",
10552
+ artifactPath: marketArtifactPathFor(name, nextVersion ?? version),
10197
10553
  files: ARTIFACTS.filter((one) => existsSync(join(dir, one))),
10198
10554
  ...nextVersion === void 0 ? {} : { nextVersion }
10199
10555
  },
@@ -10290,7 +10646,7 @@ function publishPreviewOf(root, unit, options = {}) {
10290
10646
  name,
10291
10647
  publisher: author,
10292
10648
  unit,
10293
- artifactPath: marketArtifactPath(name)
10649
+ artifactPath: marketArtifactRoot(name)
10294
10650
  });
10295
10651
  return {
10296
10652
  name,
@@ -12391,7 +12747,82 @@ function dependencyStillBehind(target, capabilities) {
12391
12747
  }
12392
12748
  }
12393
12749
  //#endregion
12750
+ //#region src/shared/api/host/self-update-marker.ts
12751
+ function readSelfUpdateMarker(file) {
12752
+ try {
12753
+ const raw = JSON.parse(readFileSync(file, "utf8"));
12754
+ return {
12755
+ ...typeof raw.pkg === "string" ? { pkg: raw.pkg } : {},
12756
+ ...typeof raw.state === "string" ? { state: raw.state } : {}
12757
+ };
12758
+ } catch {
12759
+ return {};
12760
+ }
12761
+ }
12762
+ //#endregion
12763
+ //#region src/modules/panels/model/market-swap-stuck.ts
12764
+ var DEFAULT_POLL_MS = 400;
12765
+ var DEFAULT_WAIT_MS = 6e4;
12766
+ function sleep(ms) {
12767
+ return new Promise((resolve) => {
12768
+ setTimeout(resolve, ms);
12769
+ });
12770
+ }
12771
+ function landedIn(after, input) {
12772
+ if (after.kind !== "module") return false;
12773
+ return input.to === "" ? after.version !== input.from : after.version === input.to;
12774
+ }
12775
+ async function swapExternally(deps, input) {
12776
+ let marker;
12777
+ try {
12778
+ marker = deps.supply.startSelfUpdate({
12779
+ dshHome: deps.dshHome,
12780
+ profile: deps.profile,
12781
+ pkg: input.pkg,
12782
+ from: input.from,
12783
+ to: input.to,
12784
+ pinned: input.pinned
12785
+ }, (message) => deps.logger?.warn?.(`[dsh-workbench] ${message}`)).paths.marker;
12786
+ } catch (error) {
12787
+ deps.logger?.warn?.(`[dsh-workbench] 外部换版起不来(${input.pkg}):${error instanceof Error ? error.message : String(error)}`);
12788
+ return;
12789
+ }
12790
+ const pollMs = deps.pollMs ?? DEFAULT_POLL_MS;
12791
+ const deadline = Date.now() + (deps.waitMs ?? DEFAULT_WAIT_MS);
12792
+ let state = "timeout";
12793
+ while (Date.now() < deadline) {
12794
+ const read = readSelfUpdateMarker(marker);
12795
+ if (read.pkg === input.pkg && read.state !== void 0 && read.state !== "running") {
12796
+ state = read.state;
12797
+ break;
12798
+ }
12799
+ await sleep(pollMs);
12800
+ }
12801
+ const after = deps.installed?.(input.pkg) ?? {
12802
+ kind: "unknown",
12803
+ pkg: input.pkg
12804
+ };
12805
+ const version = after.kind === "module" ? after.version : "读不到";
12806
+ deps.logger?.info?.(`[dsh-workbench] 外部换版 ${input.pkg}:${state},盘上 ${version}`);
12807
+ return landedIn(after, input) ? after : void 0;
12808
+ }
12809
+ //#endregion
12394
12810
  //#region src/modules/panels/model/market-update-bundle.ts
12811
+ var SHIPPED_APPLICATIONS = /* @__PURE__ */ new Set([
12812
+ "applied",
12813
+ "restart-required",
12814
+ "overridden"
12815
+ ]);
12816
+ function stillStuck(change, after, before) {
12817
+ if (!SHIPPED_APPLICATIONS.has(change.application)) return true;
12818
+ return before !== void 0 && after.kind === "module" && after.version === before;
12819
+ }
12820
+ function remedyOf(deps, change, before, afterVersion, spec) {
12821
+ const name = deps.profileName ?? deps.profile;
12822
+ if (name === void 0) return void 0;
12823
+ if (!!SHIPPED_APPLICATIONS.has(change.application) && !(before !== void 0 && afterVersion === before)) return void 0;
12824
+ return { command: `dsh plugin --profile ${name} add ${quoteShellArg(spec)}` };
12825
+ }
12395
12826
  async function restoreRemovedBundle(pkg, before, registry, deps) {
12396
12827
  if (before === void 0) return void 0;
12397
12828
  try {
@@ -12446,7 +12877,7 @@ async function updateModuleBundle(unit, deps) {
12446
12877
  kind: "unknown",
12447
12878
  pkg
12448
12879
  }).kind === "module" ? void 0 : await restoreRemovedBundle(pkg, before, registry, deps);
12449
- const after = deps.installed?.(pkg) ?? {
12880
+ let after = deps.installed?.(pkg) ?? {
12450
12881
  kind: "unknown",
12451
12882
  pkg
12452
12883
  };
@@ -12456,12 +12887,43 @@ async function updateModuleBundle(unit, deps) {
12456
12887
  after,
12457
12888
  ...restored === void 0 ? {} : { restored }
12458
12889
  });
12890
+ let change = verified.change;
12891
+ let restoredVersion = verified.restored;
12892
+ const swap = stillStuck(change, after, before) && before !== void 0 && deps.dshHome !== void 0 && deps.profileName !== void 0 ? await swapExternally({
12893
+ supply,
12894
+ installed: deps.installed,
12895
+ dshHome: deps.dshHome,
12896
+ profile: deps.profileName,
12897
+ ...deps.logger === void 0 ? {} : { logger: deps.logger },
12898
+ ...deps.swapWaitMs === void 0 ? {} : { waitMs: deps.swapWaitMs },
12899
+ ...deps.swapPollMs === void 0 ? {} : { pollMs: deps.swapPollMs }
12900
+ }, {
12901
+ pkg,
12902
+ from: before,
12903
+ to: latest ?? "",
12904
+ pinned: latest !== void 0
12905
+ }) : void 0;
12906
+ if (swap !== void 0) {
12907
+ const swapVersion = swap.kind === "module" ? swap.version : "";
12908
+ after = swap;
12909
+ restoredVersion = void 0;
12910
+ change = {
12911
+ application: "restart-required",
12912
+ stage: "install",
12913
+ changed: true,
12914
+ target: pkg,
12915
+ bundle: pkg,
12916
+ ...swapVersion === "" ? {} : { version: swapVersion }
12917
+ };
12918
+ }
12459
12919
  await deps.refreshSupply(unit).catch(() => void 0);
12920
+ const remedy = stillStuck(change, after, before) ? remedyOf(deps, change, before, after.kind === "module" ? after.version : void 0, spec) : void 0;
12460
12921
  return {
12461
12922
  ...outcome,
12462
- change: verified.change,
12923
+ change,
12463
12924
  module: after,
12464
- ...verified.restored === void 0 ? {} : { restored: verified.restored }
12925
+ ...restoredVersion === void 0 ? {} : { restored: restoredVersion },
12926
+ ...remedy === void 0 ? {} : { remedy }
12465
12927
  };
12466
12928
  }
12467
12929
  //#endregion
@@ -12778,14 +13240,14 @@ function createPluginActions(options) {
12778
13240
  unreadable: []
12779
13241
  };
12780
13242
  }
12781
- function decide(entry, held) {
13243
+ function decide(entry, held, claimedBy) {
12782
13244
  const plugin = pluginNameOf(entry);
12783
13245
  if (plugin === void 0) return {
12784
13246
  act: "keep",
12785
13247
  kept: { kind: "unnamed" }
12786
13248
  };
12787
13249
  const refs = referencesOf();
12788
- const by = options.claimedBy?.(plugin) ?? [];
13250
+ const by = claimedBy?.(plugin) ?? options.claimedBy?.(plugin) ?? [];
12789
13251
  const retention = pluginRetentionOf({
12790
13252
  held,
12791
13253
  claimedBy: by,
@@ -12852,12 +13314,12 @@ function createPluginActions(options) {
12852
13314
  name: decision.plugin
12853
13315
  };
12854
13316
  },
12855
- async sweep() {
13317
+ async sweep(claimedBy) {
12856
13318
  if (ledger === void 0) return [];
12857
13319
  const removed = [];
12858
13320
  for (const entry of ledger.all()) {
12859
13321
  if (entry.releasedAt === void 0) continue;
12860
- const decision = decide(entry, false);
13322
+ const decision = decide(entry, false, claimedBy);
12861
13323
  if (decision.act === "keep") continue;
12862
13324
  try {
12863
13325
  await removeFiles(decision.plugin);
@@ -12886,17 +13348,18 @@ function createEnvDropper(options) {
12886
13348
  unreadable: []
12887
13349
  };
12888
13350
  }
12889
- function usersOf(plugin, except) {
12890
- return catalog().filter((item) => item.id !== except && !(isReleased?.(item.id) ?? false) && needsOf(item).some((need) => need.kind === "extension" && need.name === plugin)).map((item) => item.id);
13351
+ function usersOf(plugin, except, roster) {
13352
+ return (roster ?? catalog()).filter((item) => item.id !== except && !(isReleased?.(item.id) ?? false) && needsOf(item).some((need) => need.kind === "extension" && need.name === plugin)).map((item) => item.id);
12891
13353
  }
12892
- function libUsersOf(name, unit) {
12893
- return catalog().filter((item) => item.id !== unit && !(isReleased?.(item.id) ?? false) && needsOf(item).some((need) => need.kind === "lib" && need.name === name)).map((item) => item.id);
13354
+ function libUsersOf(name, unit, roster) {
13355
+ return roster.filter((item) => item.id !== unit && !(isReleased?.(item.id) ?? false) && needsOf(item).some((need) => need.kind === "lib" && need.name === name)).map((item) => item.id);
12894
13356
  }
12895
13357
  return {
12896
13358
  usersOf,
12897
- async drop(unit, needs, before) {
13359
+ async drop(unit, needs, before, roster) {
12898
13360
  const kept = /* @__PURE__ */ new Set();
12899
- const index = buildSupplyIndex(catalog().filter((item) => options.isReleased?.(item.id) !== true).map((item) => ({
13361
+ const known = roster ?? catalog();
13362
+ const index = buildSupplyIndex(known.filter((item) => options.isReleased?.(item.id) !== true).map((item) => ({
12900
13363
  id: item.id,
12901
13364
  needs: needsOf(item)
12902
13365
  })), { env: process.env });
@@ -12905,7 +13368,7 @@ function createEnvDropper(options) {
12905
13368
  const onMachine = new Set(before.needs.filter((state) => state.kind === "extension" && state.status !== "missing").map((state) => state.name));
12906
13369
  const unknown = !before.needs.some((state) => state.kind === "extension");
12907
13370
  for (const need of extensions) {
12908
- const users = usersOf(need.name, unit);
13371
+ const users = usersOf(need.name, unit, known);
12909
13372
  const retention = pluginRetentionOf({
12910
13373
  held: ledger?.heldAsPlugin(need.name) !== void 0,
12911
13374
  claimedBy: users,
@@ -12925,7 +13388,7 @@ function createEnvDropper(options) {
12925
13388
  await run(`${quoteShellArg(command)} plugin uninstall ${quoteShellArg(need.name)}`);
12926
13389
  }
12927
13390
  for (const need of libs) {
12928
- const keepers = libUsersOf(need.name, unit);
13391
+ const keepers = libUsersOf(need.name, unit, known);
12929
13392
  if (keepers.length > 0) {
12930
13393
  for (const id of keepers) kept.add(id);
12931
13394
  continue;
@@ -13234,7 +13697,8 @@ function createMarketService(options) {
13234
13697
  pkg
13235
13698
  };
13236
13699
  const unit = module.kind === "module" ? module.unit : void 0;
13237
- const manifest = unit === void 0 ? void 0 : catalog().find((item) => item.id === unit);
13700
+ const roster = catalog();
13701
+ const manifest = unit === void 0 ? void 0 : roster.find((item) => item.id === unit);
13238
13702
  const before = unit === void 0 ? void 0 : await capabilityOf(unit, true);
13239
13703
  const change = await removeDshBundle({
13240
13704
  manager,
@@ -13247,8 +13711,8 @@ function createMarketService(options) {
13247
13711
  change,
13248
13712
  keptFor: []
13249
13713
  };
13250
- const keptFor = manifest === void 0 || before === void 0 ? [] : await envDropper.drop(unit, needsOf(manifest), before);
13251
- await plugins.sweep();
13714
+ const keptFor = manifest === void 0 || before === void 0 ? [] : await envDropper.drop(unit, needsOf(manifest), before, roster);
13715
+ await plugins.sweep((plugin) => envDropper.usersOf(plugin, unit, roster));
13252
13716
  onUninstalled?.(unit);
13253
13717
  return {
13254
13718
  change,
@@ -13260,15 +13724,22 @@ function createMarketService(options) {
13260
13724
  },
13261
13725
  async updateBundle(unit) {
13262
13726
  const manager = requireBundles();
13727
+ const home = dshHome?.();
13728
+ const profile = profileName?.() ?? (profileDir === void 0 || profileDir === null ? void 0 : basename(profileDir));
13263
13729
  return updateModuleBundle(unit, {
13264
13730
  supply,
13265
13731
  modules,
13266
13732
  installed,
13267
13733
  shell,
13268
13734
  manager,
13735
+ ...profile === void 0 ? {} : {
13736
+ profile,
13737
+ profileName: profile
13738
+ },
13739
+ ...home === void 0 ? {} : { dshHome: home },
13269
13740
  routeDeps: routeDeps(manager),
13270
13741
  sweep: async () => {
13271
- await plugins.sweep();
13742
+ await plugins.sweep((plugin) => envDropper.usersOf(plugin, void 0, catalog()));
13272
13743
  },
13273
13744
  refreshSupply: async (id) => {
13274
13745
  await service.update(id);
@@ -14000,7 +14471,7 @@ function createMarketIndexReader(options) {
14000
14471
  }
14001
14472
  };
14002
14473
  const repo = repoOf(options.root, source);
14003
- const path = (source.path ?? "market.json").replace(/^\/+/, "");
14474
+ const path = ((source.path ?? "market").replace(/^\/+/, "") || "market").replace(/\/+$/, "");
14004
14475
  const ref = (source.ref ?? "").trim();
14005
14476
  let revision = "FETCH_HEAD";
14006
14477
  mkdirSync(directoryOf(options.root, source), { recursive: true });
@@ -14025,17 +14496,6 @@ function createMarketIndexReader(options) {
14025
14496
  detail: "仓库还没有任何提交"
14026
14497
  }
14027
14498
  }, repo);
14028
- const shown = await run(`git -C ${quoteShellArg(repo)} show ${revision}:${quoteShellArg(path)}`);
14029
- if (!shown.ok) {
14030
- if (shown.problem.kind === "git-file") return withBranch({
14031
- ok: false,
14032
- problem: {
14033
- kind: "git-missing-index",
14034
- detail: `索引文件 ${path} 不在仓库里`
14035
- }
14036
- }, repo);
14037
- return shown;
14038
- }
14039
14499
  const head = await run(`git -C ${quoteShellArg(repo)} rev-parse ${revision}`);
14040
14500
  if (!head.ok) return head;
14041
14501
  const commit = head.stdout.trim();
@@ -14046,26 +14506,105 @@ function createMarketIndexReader(options) {
14046
14506
  detail: `读到的 commit 不是 40 位:${commit}`
14047
14507
  }
14048
14508
  };
14049
- const parsed = parseMarketIndexJson(shown.stdout);
14050
- if (!parsed.ok) return {
14509
+ const listed = await run(`git -C ${quoteShellArg(repo)} ls-tree -r ${quoteShellArg(commit)} -- ${quoteShellArg(path)}`);
14510
+ if (!listed.ok) {
14511
+ if (listed.problem.kind === "git-file") return withBranch({
14512
+ ok: false,
14513
+ problem: {
14514
+ kind: "git-missing-index",
14515
+ detail: `索引 ${path} 不在仓库里`
14516
+ }
14517
+ }, repo);
14518
+ return listed;
14519
+ }
14520
+ const rows = listed.stdout.split("\n").map((line) => line.trim()).filter((line) => line !== "").flatMap((line) => {
14521
+ const [meta, file] = line.split(" ");
14522
+ const type = (meta ?? "").split(" ")[1] ?? "";
14523
+ return file === void 0 ? [] : [{
14524
+ type,
14525
+ file
14526
+ }];
14527
+ });
14528
+ const legacy = rows.find((one) => one.type === "blob" && one.file === path);
14529
+ const entries = rows.filter((one) => one.type === "blob" && one.file !== path && one.file.startsWith(`${path}/`) && one.file.endsWith(".json"));
14530
+ const finish = async (index, text) => {
14531
+ const fetchedAt = now();
14532
+ await writeFileAtomic(indexFileOf(options.root, source), text);
14533
+ await writeMeta(metaFileOf(options.root, source), {
14534
+ fetchedAt,
14535
+ commit
14536
+ });
14537
+ return {
14538
+ ok: true,
14539
+ index,
14540
+ fetchedAt,
14541
+ commit
14542
+ };
14543
+ };
14544
+ const readLegacy = async (file) => {
14545
+ const shown = await run(`git -C ${quoteShellArg(repo)} show ${quoteShellArg(commit)}:${quoteShellArg(file)}`);
14546
+ if (!shown.ok) return void 0;
14547
+ const parsed = parseMarketIndexJson(shown.stdout);
14548
+ if (!parsed.ok) return {
14549
+ ok: false,
14550
+ problem: {
14551
+ kind: "format",
14552
+ detail: parsed.problems.map((one) => one.detail).join(";")
14553
+ }
14554
+ };
14555
+ return finish(parsed.index, shown.stdout);
14556
+ };
14557
+ if (legacy !== void 0) {
14558
+ const outcome = await readLegacy(path);
14559
+ if (outcome !== void 0) return outcome;
14560
+ }
14561
+ if (entries.length > 0) {
14562
+ const parts = [];
14563
+ let failed;
14564
+ const queue = [...entries];
14565
+ const worker = async () => {
14566
+ for (;;) {
14567
+ const next = queue.shift();
14568
+ if (next === void 0 || failed !== void 0) return;
14569
+ const shown = await run(`git -C ${quoteShellArg(repo)} show ${quoteShellArg(commit)}:${quoteShellArg(next.file)}`);
14570
+ if (!shown.ok) {
14571
+ failed = shown;
14572
+ return;
14573
+ }
14574
+ parts.push({
14575
+ path: next.file,
14576
+ text: shown.stdout
14577
+ });
14578
+ }
14579
+ };
14580
+ await Promise.all(Array.from({ length: Math.min(6, entries.length) }, () => worker()));
14581
+ if (failed !== void 0) return failed;
14582
+ const assembled = assembleMarketIndex(parts);
14583
+ if (!assembled.ok) return {
14584
+ ok: false,
14585
+ problem: {
14586
+ kind: "format",
14587
+ detail: assembled.problems.map((one) => one.detail).join(";")
14588
+ }
14589
+ };
14590
+ return finish(assembled.index, `${JSON.stringify(assembled.index, null, 2)}\n`);
14591
+ }
14592
+ if (listed.stdout.trim() !== "") {
14593
+ const empty = {
14594
+ format: 1,
14595
+ plugins: []
14596
+ };
14597
+ return finish(empty, `${JSON.stringify(empty, null, 2)}\n`);
14598
+ }
14599
+ const legacyText = await readLegacy(MARKET_INDEX_PATH);
14600
+ if (legacyText !== void 0) return legacyText;
14601
+ return withBranch({
14051
14602
  ok: false,
14052
14603
  problem: {
14053
- kind: "format",
14054
- detail: parsed.problems.map((one) => one.detail).join(";")
14604
+ kind: "git-missing-index",
14605
+ detail: `索引 ${path} 不在仓库里`
14055
14606
  }
14056
- };
14057
- const fetchedAt = now();
14058
- await writeFileAtomic(indexFileOf(options.root, source), shown.stdout);
14059
- await writeMeta(metaFileOf(options.root, source), {
14060
- fetchedAt,
14061
- commit
14062
- });
14063
- return {
14064
- ok: true,
14065
- index: parsed.index,
14066
- fetchedAt,
14067
- commit
14068
- };
14607
+ }, repo);
14069
14608
  }
14070
14609
  async function read(source, callOptions = {}) {
14071
14610
  const key = marketSourceKey(source);
@@ -14413,16 +14952,31 @@ function createMarketSourceInitializer(options) {
14413
14952
  ], workdir);
14414
14953
  if (created.exitCode !== 0) throw new Error(`建分支 ${branch} 没过:${tailLines(created.stderr, 4)}`);
14415
14954
  }
14416
- const indexPath = (source.path ?? "market.json").replace(/^\/+/, "") || "market.json";
14417
- const indexFile = join(workdir, indexPath);
14955
+ const indexPath = ((source.path ?? "market").replace(/^\/+/, "") || "market").replace(/\/+$/, "");
14956
+ const indexDir = join(workdir, indexPath);
14957
+ const indexMarker = join(indexDir, "README.md");
14418
14958
  const stampOnly = request.stampOnly === true;
14419
- if (stampOnly && !existsSync(indexFile)) throw new Error(`仓库里还没有 ${indexPath} —— 先选中这个源,按发现页的提示把它初始化成索引`);
14959
+ if (stampOnly && !existsSync(indexDir)) throw new Error(`仓库里还没有 ${indexPath}/ —— 先选中这个源,按发现页的提示把它初始化成索引`);
14420
14960
  const written = [];
14421
14961
  if (!stampOnly) {
14422
- if (!existsSync(indexFile)) {
14423
- mkdirSync(dirname(indexFile), { recursive: true });
14424
- writeFileSync(indexFile, MARKET_INDEX_SEED);
14425
- written.push(indexPath);
14962
+ const legacyFile = join(workdir, "market.json");
14963
+ if (indexPath !== "market.json" && existsSync(legacyFile)) {
14964
+ const migrated = marketEntryFilesOfLegacy(readFileSync(legacyFile, "utf8"), indexPath);
14965
+ if (migrated !== void 0) {
14966
+ for (const file of migrated) {
14967
+ const target = join(workdir, ...file.path.split("/"));
14968
+ mkdirSync(dirname(target), { recursive: true });
14969
+ writeFileSync(target, file.text);
14970
+ written.push(file.path);
14971
+ }
14972
+ rmSync(legacyFile);
14973
+ written.push("market.json");
14974
+ }
14975
+ }
14976
+ if (!existsSync(indexMarker)) {
14977
+ mkdirSync(indexDir, { recursive: true });
14978
+ writeFileSync(indexMarker, MARKET_ENTRIES_README_LINES.join("\n"));
14979
+ written.push(`${indexPath}/README.md`);
14426
14980
  }
14427
14981
  const readmeFile = join(workdir, "README.md");
14428
14982
  if (!existsSync(readmeFile)) {
@@ -14691,7 +15245,7 @@ function failureText(change) {
14691
15245
  const code = change.errorCode ?? change.application;
14692
15246
  if (code === "already-installed") return "它已经在这份 profile 里,而这次没能把它卸下来(这一版 dsh 不肯就地覆盖装,得先卸再装)";
14693
15247
  const surface = hostSurface();
14694
- return REMOVE_BLOCKED.has(code) ? `旧版卸不掉,而这一版 dsh 又不许就地覆盖装 —— 让用户${surface.restartHint},或者${surface.deployment === "desktop" ? "从 Desktop 的插件页更新" : "从终端更新"}` : code;
15248
+ return REMOVE_BLOCKED.has(code) ? `旧版卸不掉(dsh 不肯就地覆盖正在用的模块),自动从外部换也没成 —— 让用户从终端跑 dsh plugin --profile <名字> add <包>@<版本>,跑完${surface.restartHint}` : code;
14695
15249
  }
14696
15250
  function describeInstall(spec, module, supply, change) {
14697
15251
  return `${change.application === "applied" ? "装好了,已生效" : change.application === "restart-required" ? `装好了,要${hostSurface().restartHint} 才生效` : `没成功:${failureText(change)}`}:${module.kind === "module" ? `${module.unit}(${module.pkg})` : module.pkg}${supply.state === "installed" ? ";它要的 opencli 插件也装上了" : supply.state === "pending-restart" ? ";重启后再装它要的 opencli 插件" : supply.state === "not-a-module" ? ";但它不是工作台模块(没有 dsh.workbench 声明,不会出现在面板上)" : supply.state === "failed" ? `;但它要的插件没装上:${supply.reason}` : ""}(spec:${spec})`;
@@ -14825,6 +15379,14 @@ function registerMarketTools(ctx, market, sources) {
14825
15379
  async execute(raw) {
14826
15380
  const id = await idOrList(raw, MODULES_UNINSTALL_TOOL);
14827
15381
  const outcome = await market.uninstall(id);
15382
+ const change = outcome.change;
15383
+ if (change !== void 0 && change.application !== "applied") {
15384
+ const code = change.errorCode ?? change.application;
15385
+ return {
15386
+ module: outcome.entry,
15387
+ text: `没卸下来:${id} 还在这份 profile 里(${code})—— 这一版 dsh 不肯卸正在加载的模块。让用户从终端跑 dsh plugin --profile <profile> remove ${id} 的包,跑完重启一次工作台;别再调这个工具重试。`
15388
+ };
15389
+ }
14828
15390
  const kept = outcome.keptFor.length === 0 ? "" : `(还有模块在面板上要用其中一份安装,所以那份留着:${outcome.keptFor.join("、")})`;
14829
15391
  return {
14830
15392
  module: outcome.entry,
@@ -14980,7 +15542,7 @@ function registerMarketTools(ctx, market, sources) {
14980
15542
  branch: result.branch,
14981
15543
  commit: result.commit,
14982
15544
  changed: true,
14983
- text: `已把「${source.label}」设为可发布(${source.url},分支 ${result.branch})—— 仓库本身没动。之后发布可以落到它的 ${source.path ?? "market.json"}。`
15545
+ text: `已把「${source.label}」设为可发布(${source.url},分支 ${result.branch})—— 仓库本身没动。之后发布可以落到它的 ${source.path ?? "market"}/。`
14984
15546
  };
14985
15547
  }
14986
15548
  };
@@ -17022,6 +17584,7 @@ function notifyLongTaskStop(agents, unit, change) {
17022
17584
  //#endregion
17023
17585
  //#region src/app/host.ts
17024
17586
  var name = "dsh-workbench";
17587
+ var LIB_OPENCLI_TIMEOUT_MS = 18e5;
17025
17588
  var inject = ["webServer"];
17026
17589
  async function defaultAdapterSet(ctx) {
17027
17590
  return (await import("@dsh-workbench/adapters-default")).createAdapterSet(ctx);
@@ -17042,6 +17605,7 @@ async function apply(ctx, config) {
17042
17605
  else if (loaded.failure !== "not-configured") ctx.logger?.warn?.(`[dsh-workbench] 适配器集合没生效(${loaded.failure}):${loaded.detail};先用默认集合`);
17043
17606
  ctx.provide(WORKBENCH_ADAPTERS, adapters);
17044
17607
  const opencli = adapters.runtime.opencliCommand(ctx);
17608
+ const libRunners = adapters.libs === void 0 ? void 0 : withLibRunners(adapters.libs, { opencli: opencliRunnerOf((request) => adapters.data.read(request), { timeoutMs: LIB_OPENCLI_TIMEOUT_MS }) }, ctx.logger);
17045
17609
  const shell = ctx.get("shell");
17046
17610
  const routes = createHostRoutes({
17047
17611
  ctx,
@@ -17218,7 +17782,7 @@ async function apply(ctx, config) {
17218
17782
  installable: () => installableUnitsOf(units),
17219
17783
  store,
17220
17784
  data: adapters.data,
17221
- libs: adapters.libs,
17785
+ libs: libRunners,
17222
17786
  git: adapters.git,
17223
17787
  commands: adapters.commands,
17224
17788
  probe: (unit, options) => probeOne(unit, options?.force ?? false),