@dsh-workbench/core 1.1.5 → 1.1.6

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 (27) hide show
  1. package/client.js +704 -520
  2. package/dist/client/client.body.js +704 -520
  3. package/dist/client/index.js +575 -391
  4. package/dist/host/index.js +613 -149
  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/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";
@@ -4896,6 +4896,11 @@ function checkVersionRange(contract, version) {
4896
4896
  if (parsed === null) return "malformed";
4897
4897
  return import_semver.default.satisfies(parsed, range) ? "satisfied" : "unsatisfied";
4898
4898
  }
4899
+ //#endregion
4900
+ //#region ../shared/dist/contract/protocol/market-index.js
4901
+ var MARKET_INDEX_DIR = "market";
4902
+ var MARKET_ARTIFACTS_DIR = "artifacts";
4903
+ var MARKET_INDEX_PATH = "market.json";
4899
4904
  var MARKET_DISCOVER_ROUTE = "panels/market-discover";
4900
4905
  var MARKET_DOC_ROUTE = "panels/market-doc";
4901
4906
  var MARKET_GIT_PROBE_ROUTE = "panels/git-probe";
@@ -4917,34 +4922,39 @@ function marketText(value, locale) {
4917
4922
  return locale.toLowerCase().startsWith("zh") ? zh || en : en || zh;
4918
4923
  }
4919
4924
  //#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
- ];
4925
+ //#region ../shared/dist/host/market-index-parse.js
4945
4926
  var NPM_NAME = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
4946
4927
  var GIT_COMMIT = /^[0-9a-f]{40}$/;
4947
4928
  var MAX_ICON_CHARS = 32768;
4929
+ function compareSemver(left, right) {
4930
+ return compareVersions(left, right);
4931
+ }
4932
+ function httpUrlProblem(url) {
4933
+ let parsed;
4934
+ try {
4935
+ parsed = new URL(url);
4936
+ } catch {
4937
+ return "不是合法 URL";
4938
+ }
4939
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return "只收 http / https";
4940
+ if (parsed.username !== "" || parsed.password !== "") return "URL 不能带凭据";
4941
+ if (parsed.search !== "" || parsed.hash !== "") return "URL 不能带 query / fragment";
4942
+ }
4943
+ function gitUrlOk(url) {
4944
+ if (url === "") return false;
4945
+ if (/^[^@\s]+@[^@\s:]+:[^@\s]+$/.test(url)) return true;
4946
+ let parsed;
4947
+ try {
4948
+ parsed = new URL(url);
4949
+ } catch {
4950
+ return false;
4951
+ }
4952
+ if (parsed.protocol !== "https:" && parsed.protocol !== "ssh:" && parsed.protocol !== "http:") return false;
4953
+ if (parsed.password !== "") return false;
4954
+ if (parsed.username !== "" && parsed.protocol !== "ssh:") return false;
4955
+ if (parsed.search !== "" || parsed.hash !== "") return false;
4956
+ return true;
4957
+ }
4948
4958
  function asRecord$4(value) {
4949
4959
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
4950
4960
  }
@@ -4964,22 +4974,19 @@ function asLocalized(value) {
4964
4974
  ...en === "" ? {} : { en }
4965
4975
  };
4966
4976
  }
4967
- function problem(kind, detail, at) {
4977
+ function problem$1(kind, detail, at) {
4968
4978
  return {
4969
4979
  kind,
4970
4980
  detail,
4971
4981
  ...at === void 0 ? {} : { at }
4972
4982
  };
4973
4983
  }
4974
- function compareSemver(left, right) {
4975
- return compareVersions(left, right);
4976
- }
4977
4984
  function sourceOf(raw, at) {
4978
4985
  const record = asRecord$4(raw);
4979
- if (record === void 0) return problem("source", "source 必须是一个对象", at);
4986
+ if (record === void 0) return problem$1("source", "source 必须是一个对象", at);
4980
4987
  if (record["type"] === "npm") {
4981
4988
  const registry = record["registry"];
4982
- if (registry !== void 0 && asText$4(registry) === "") return problem("source", "npm source 的 registry 必须是非空字符串", at);
4989
+ if (registry !== void 0 && asText$4(registry) === "") return problem$1("source", "npm source 的 registry 必须是非空字符串", at);
4983
4990
  return {
4984
4991
  type: "npm",
4985
4992
  ...registry === void 0 ? {} : { registry: asText$4(registry) }
@@ -4988,8 +4995,8 @@ function sourceOf(raw, at) {
4988
4995
  if (record["type"] === "git") {
4989
4996
  const url = asText$4(record["url"]);
4990
4997
  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);
4998
+ if (url === "") return problem$1("source", "git source 缺 url", at);
4999
+ if (!GIT_COMMIT.test(commit)) return problem$1("git-commit", `git source 的 commit 必须是 40 位小写 SHA(现在是 ${JSON.stringify(commit)})`, at);
4993
5000
  const path = record["path"];
4994
5001
  return {
4995
5002
  type: "git",
@@ -4998,17 +5005,17 @@ function sourceOf(raw, at) {
4998
5005
  ...path === void 0 || asText$4(path) === "" ? {} : { path: asText$4(path) }
4999
5006
  };
5000
5007
  }
5001
- return problem("source", "source.type 只能是 npm 或 git", at);
5008
+ return problem$1("source", "source.type 只能是 npm 或 git", at);
5002
5009
  }
5003
5010
  function versionOf(raw, at) {
5004
5011
  const record = asRecord$4(raw);
5005
- if (record === void 0) return problem("version", "版本必须是一个对象", at);
5012
+ if (record === void 0) return problem$1("version", "版本必须是一个对象", at);
5006
5013
  const version = asText$4(record["version"]);
5007
- if (!isValidVersion(version)) return problem("version", `版本号不是合法 SemVer:${JSON.stringify(version)}`, at);
5014
+ if (!isValidVersion(version)) return problem$1("version", `版本号不是合法 SemVer:${JSON.stringify(version)}`, at);
5008
5015
  const source = sourceOf(record["source"], `${at}.source`);
5009
5016
  if ("kind" in source) return source;
5010
5017
  const timestamp = record["publishTimestamp"];
5011
- if (timestamp !== void 0 && (!Number.isInteger(timestamp) || timestamp < 0)) return problem("timestamp", "publishTimestamp 必须是 Unix 秒整数", at);
5018
+ if (timestamp !== void 0 && (!Number.isInteger(timestamp) || timestamp < 0)) return problem$1("timestamp", "publishTimestamp 必须是 Unix 秒整数", at);
5012
5019
  const engines = asRecord$4(record["engines"]);
5013
5020
  const dsh = engines === void 0 ? "" : asText$4(engines["dsh"]);
5014
5021
  return {
@@ -5019,33 +5026,33 @@ function versionOf(raw, at) {
5019
5026
  };
5020
5027
  }
5021
5028
  function versionsOf$1(raw, at) {
5022
- if (!Array.isArray(raw) || raw.length === 0) return problem("versions", "versions 必须是非空数组", at);
5029
+ if (!Array.isArray(raw) || raw.length === 0) return problem$1("versions", "versions 必须是非空数组", at);
5023
5030
  const versions = [];
5024
5031
  const seen = /* @__PURE__ */ new Set();
5025
5032
  for (const [index, item] of raw.entries()) {
5026
5033
  const parsed = versionOf(item, `${at}[${String(index)}]`);
5027
5034
  if ("kind" in parsed) return parsed;
5028
- if (seen.has(parsed.version)) return problem("versions", `版本重复:${parsed.version}`, at);
5035
+ if (seen.has(parsed.version)) return problem$1("versions", `版本重复:${parsed.version}`, at);
5029
5036
  seen.add(parsed.version);
5030
5037
  versions.push(parsed);
5031
5038
  }
5032
5039
  for (let index = 1; index < versions.length; index += 1) {
5033
5040
  const order = compareSemver(versions[index - 1]?.version ?? "", versions[index]?.version ?? "");
5034
- if (order === void 0 || order < 0) return problem("version-order", "versions 必须按 SemVer 降序", at);
5041
+ if (order === void 0 || order < 0) return problem$1("version-order", "versions 必须按 SemVer 降序", at);
5035
5042
  }
5036
5043
  return versions;
5037
5044
  }
5038
5045
  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");
5046
+ if (icon.length > MAX_ICON_CHARS) return problem$1("icon", "icon 超过 32 KiB");
5047
+ if (icon.startsWith("data:image/")) return icon.includes(";base64,") ? void 0 : problem$1("icon", "data URL 必须是 base64");
5041
5048
  if (/^https?:\/\//i.test(icon)) return void 0;
5042
- return problem("icon", "icon 只能是 base64 data URL 或 http(s) URL");
5049
+ return problem$1("icon", "icon 只能是 base64 data URL 或 http(s) URL");
5043
5050
  }
5044
5051
  var MAX_OWNER_CHARS = 200;
5045
5052
  var UNIT_ID = /^[a-z][a-z0-9-]*$/;
5046
5053
  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 字");
5054
+ if (/[\r\n]/u.test(owner)) return problem$1("owner", "owner 只能是一行署名");
5055
+ if (owner.length > MAX_OWNER_CHARS) return problem$1("owner", "owner 超过 200 字");
5049
5056
  }
5050
5057
  function metadataOf(raw) {
5051
5058
  const record = asRecord$4(raw);
@@ -5068,9 +5075,9 @@ function metadataOf(raw) {
5068
5075
  }
5069
5076
  function entryOf$1(raw, at) {
5070
5077
  const record = asRecord$4(raw);
5071
- if (record === void 0) return problem("plugin", "插件条目必须是对象", at);
5078
+ if (record === void 0) return problem$1("plugin", "插件条目必须是对象", at);
5072
5079
  const name = asText$4(record["name"]);
5073
- if (name === "" || name.length > 214 || !NPM_NAME.test(name)) return problem("name", `不是合法 npm 包名:${JSON.stringify(name)}`, at);
5080
+ if (name === "" || name.length > 214 || !NPM_NAME.test(name)) return problem$1("name", `不是合法 npm 包名:${JSON.stringify(name)}`, at);
5074
5081
  const versions = versionsOf$1(record["versions"], `${at}.versions`);
5075
5082
  if ("kind" in versions) return versions;
5076
5083
  const icon = asText$4(record["icon"]);
@@ -5088,7 +5095,7 @@ function entryOf$1(raw, at) {
5088
5095
  }
5089
5096
  const homepage = asText$4(record["homepage"]);
5090
5097
  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);
5098
+ 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
5099
  return {
5093
5100
  name,
5094
5101
  versions,
@@ -5105,16 +5112,16 @@ function parseMarketIndex(raw) {
5105
5112
  const record = asRecord$4(raw);
5106
5113
  if (record === void 0) return {
5107
5114
  ok: false,
5108
- problems: [problem("not-object", "索引必须是一个 JSON 对象")]
5115
+ problems: [problem$1("not-object", "索引必须是一个 JSON 对象")]
5109
5116
  };
5110
5117
  if (record["format"] !== 1) return {
5111
5118
  ok: false,
5112
- problems: [problem("format", `format 必须是 ${String(1)}(现在是 ${JSON.stringify(record["format"])})`)]
5119
+ problems: [problem$1("format", `format 必须是 ${String(1)}(现在是 ${JSON.stringify(record["format"])})`)]
5113
5120
  };
5114
5121
  const plugins = record["plugins"];
5115
5122
  if (!Array.isArray(plugins)) return {
5116
5123
  ok: false,
5117
- problems: [problem("plugins", "plugins 必须是数组")]
5124
+ problems: [problem$1("plugins", "plugins 必须是数组")]
5118
5125
  };
5119
5126
  const entries = [];
5120
5127
  const problems = [];
@@ -5126,7 +5133,7 @@ function parseMarketIndex(raw) {
5126
5133
  continue;
5127
5134
  }
5128
5135
  if (seen.has(parsed.name)) {
5129
- problems.push(problem("duplicate-name", `插件名重复:${parsed.name}`, `plugins[${String(index)}]`));
5136
+ problems.push(problem$1("duplicate-name", `插件名重复:${parsed.name}`, `plugins[${String(index)}]`));
5130
5137
  continue;
5131
5138
  }
5132
5139
  seen.add(parsed.name);
@@ -5156,10 +5163,13 @@ function parseMarketIndexJson(text) {
5156
5163
  } catch (error) {
5157
5164
  return {
5158
5165
  ok: false,
5159
- problems: [problem("not-json", `JSON 解析失败:${error instanceof Error ? error.message : String(error)}`)]
5166
+ problems: [problem$1("not-json", `JSON 解析失败:${error instanceof Error ? error.message : String(error)}`)]
5160
5167
  };
5161
5168
  }
5162
5169
  }
5170
+ function sortMarketVersions(versions) {
5171
+ return [...versions].sort((left, right) => compareSemver(right.version, left.version) ?? 0);
5172
+ }
5163
5173
  function marketEntryLabels(entry) {
5164
5174
  return entry.metadata?.labels ?? [];
5165
5175
  }
@@ -5195,7 +5205,7 @@ function parseSourceConfig(raw) {
5195
5205
  const record = asRecord$4(raw);
5196
5206
  if (record === void 0) return {
5197
5207
  ok: false,
5198
- problem: problem("source", "索引源必须是对象")
5208
+ problem: problem$1("source", "索引源必须是对象")
5199
5209
  };
5200
5210
  const id = asText$4(record["id"]);
5201
5211
  const label = asText$4(record["label"]);
@@ -5203,21 +5213,21 @@ function parseSourceConfig(raw) {
5203
5213
  const url = asText$4(record["url"]);
5204
5214
  if (id === "") return {
5205
5215
  ok: false,
5206
- problem: problem("source", "索引源缺 id")
5216
+ problem: problem$1("source", "索引源缺 id")
5207
5217
  };
5208
5218
  if (label === "") return {
5209
5219
  ok: false,
5210
- problem: problem("source", "索引源缺 label")
5220
+ problem: problem$1("source", "索引源缺 label")
5211
5221
  };
5212
5222
  if (kind !== "http" && kind !== "git") return {
5213
5223
  ok: false,
5214
- problem: problem("source", "kind 只能是 http 或 git")
5224
+ problem: problem$1("source", "kind 只能是 http 或 git")
5215
5225
  };
5216
5226
  if (kind === "http") {
5217
5227
  const bad = httpUrlProblem(url);
5218
5228
  if (bad !== void 0) return {
5219
5229
  ok: false,
5220
- problem: problem("source", bad)
5230
+ problem: problem$1("source", bad)
5221
5231
  };
5222
5232
  return {
5223
5233
  ok: true,
@@ -5231,7 +5241,7 @@ function parseSourceConfig(raw) {
5231
5241
  }
5232
5242
  if (!gitUrlOk(url)) return {
5233
5243
  ok: false,
5234
- problem: problem("source", "git 地址不合格(只收 https / ssh / scp 形式,且不能带凭据)")
5244
+ problem: problem$1("source", "git 地址不合格(只收 https / ssh / scp 形式,且不能带凭据)")
5235
5245
  };
5236
5246
  const ref = asText$4(record["ref"]);
5237
5247
  const path = asText$4(record["path"]);
@@ -5239,7 +5249,7 @@ function parseSourceConfig(raw) {
5239
5249
  const policy = record["policy"];
5240
5250
  if (policy !== void 0 && policy !== "artifacts-in-repo" && policy !== "catalog-only") return {
5241
5251
  ok: false,
5242
- problem: problem("source", "policy 只能是 artifacts-in-repo 或 catalog-only")
5252
+ problem: problem$1("source", "policy 只能是 artifacts-in-repo 或 catalog-only")
5243
5253
  };
5244
5254
  return {
5245
5255
  ok: true,
@@ -5255,31 +5265,270 @@ function parseSourceConfig(raw) {
5255
5265
  }
5256
5266
  };
5257
5267
  }
5258
- function httpUrlProblem(url) {
5259
- let parsed;
5260
- try {
5261
- parsed = new URL(url);
5262
- } catch {
5263
- return "不是合法 URL";
5268
+ function docFieldsOf(record, at) {
5269
+ const name = asText$4(record["name"]);
5270
+ if (name === "" || name.length > 214 || !NPM_NAME.test(name)) return problem$1("name", `不是合法 npm 包名:${JSON.stringify(name)}`, at);
5271
+ const icon = asText$4(record["icon"]);
5272
+ if (icon !== "") {
5273
+ const badIcon = iconProblem(icon);
5274
+ if (badIcon !== void 0) return badIcon;
5264
5275
  }
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";
5276
+ const owner = asText$4(record["owner"]);
5277
+ if (owner !== "") {
5278
+ const badOwner = ownerProblem(owner);
5279
+ if (badOwner !== void 0) return badOwner;
5280
+ }
5281
+ const metadata = metadataOf(record["metadata"]);
5282
+ if (metadata?.unit !== void 0 && !UNIT_ID.test(metadata.unit)) return problem$1("unit", `metadata.unit 只能是小写字母开头的 a-z0-9-(现在是 ${JSON.stringify(metadata.unit)})`, at);
5283
+ const title = asLocalized(record["title"]);
5284
+ const description = asLocalized(record["description"]);
5285
+ const author = asText$4(record["author"]);
5286
+ const homepage = asText$4(record["homepage"]);
5287
+ return {
5288
+ name,
5289
+ ...title === void 0 ? {} : { title },
5290
+ ...description === void 0 ? {} : { description },
5291
+ ...author === "" ? {} : { author },
5292
+ ...owner === "" ? {} : { owner },
5293
+ ...homepage === "" ? {} : { homepage },
5294
+ ...icon === "" ? {} : { icon },
5295
+ ...metadata === void 0 ? {} : { metadata }
5296
+ };
5268
5297
  }
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;
5298
+ function parseMarketEntryDoc(raw) {
5299
+ const record = asRecord$4(raw);
5300
+ if (record === void 0) return {
5301
+ ok: false,
5302
+ problems: [problem$1("entry", "条目文件必须是一个 JSON 对象")]
5303
+ };
5304
+ const fields = docFieldsOf(record, "entry");
5305
+ if ("kind" in fields) return {
5306
+ ok: false,
5307
+ problems: [fields]
5308
+ };
5309
+ const version = asText$4(record["version"]);
5310
+ const timestamp = record["publishTimestamp"];
5311
+ if (timestamp !== void 0 && (!Number.isInteger(timestamp) || timestamp < 0)) return {
5312
+ ok: false,
5313
+ problems: [problem$1("timestamp", "publishTimestamp 必须是 Unix 秒整数", "entry")]
5314
+ };
5315
+ if (version === "") {
5316
+ if (timestamp === void 0) return {
5317
+ ok: false,
5318
+ problems: [problem$1("timestamp", "元数据文件要有 publishTimestamp", "entry")]
5319
+ };
5320
+ const epoch = record["epoch"];
5321
+ if (epoch !== void 0 && (!Number.isInteger(epoch) || epoch < 0)) return {
5322
+ ok: false,
5323
+ problems: [problem$1("timestamp", "epoch 必须是非负整数", "entry")]
5324
+ };
5325
+ return {
5326
+ ok: true,
5327
+ doc: {
5328
+ ...fields,
5329
+ publishTimestamp: timestamp,
5330
+ ...epoch === void 0 ? {} : { epoch }
5331
+ }
5332
+ };
5277
5333
  }
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;
5334
+ if (!isValidVersion(version)) return {
5335
+ ok: false,
5336
+ problems: [problem$1("version", `版本号不是合法 SemVer:${JSON.stringify(version)}`, "entry")]
5337
+ };
5338
+ const source = sourceOf(record["source"], "entry.source");
5339
+ if ("kind" in source) return {
5340
+ ok: false,
5341
+ problems: [source]
5342
+ };
5343
+ const engines = asRecord$4(record["engines"]);
5344
+ const dsh = engines === void 0 ? "" : asText$4(engines["dsh"]);
5345
+ return {
5346
+ ok: true,
5347
+ doc: {
5348
+ ...fields,
5349
+ version,
5350
+ source,
5351
+ ...timestamp === void 0 ? {} : { publishTimestamp: timestamp },
5352
+ ...dsh === "" ? {} : { engines: { dsh } }
5353
+ }
5354
+ };
5355
+ }
5356
+ function indexPathBase(dir) {
5357
+ return dir.replace(/^\/+|\/+$/g, "") || "market";
5358
+ }
5359
+ function marketEntryPath(dir, name, version) {
5360
+ if (!NPM_NAME.test(name)) throw new Error(`不是合法包名:${JSON.stringify(name)}`);
5361
+ if (!isValidVersion(version)) throw new Error(`不是合法版本号:${JSON.stringify(version)}`);
5362
+ return `${indexPathBase(dir)}/${name}/${version}.json`;
5363
+ }
5364
+ //#endregion
5365
+ //#region ../shared/dist/host/market-index.js
5366
+ var MARKET_ENTRIES_README_LINES = [
5367
+ "# 索引目录",
5368
+ "",
5369
+ "一个包一个目录,一个版本一个文件:`market/<包名>/<版本>.json`;产物在 `artifacts/<包名>/<版本>/`。",
5370
+ "",
5371
+ "`meta-<epoch>-<随机后缀>.json` 是只改标题 / 署名 / 说明时追加的元数据快照,读取时按 epoch 取最新的一份。"
5372
+ ];
5373
+ var MARKET_INDEX_README_LINES = [
5374
+ "# 模块市场索引",
5375
+ "",
5376
+ "这个仓库给 DeepSeek Harness 工作台当模块市场索引:条目在 `market/<包名>/<版本>.json`,产物在 `artifacts/<包名>/<版本>/`。",
5377
+ "",
5378
+ "## 发布模块",
5379
+ "",
5380
+ "在模块包里运行:",
5381
+ "",
5382
+ "```bash",
5383
+ "dsh-wb-module release --index <本仓库地址> --index-policy artifacts-in-repo",
5384
+ "```",
5385
+ "",
5386
+ "产物会提交到 `artifacts/<包名>/<版本>/`,条目提交到 `market/<包名>/<版本>.json`。",
5387
+ "发布只新增文件、不改已有文件,两台机器同时发也不会冲突;老条目的 `packages/<包名>/` 路径继续可用。",
5388
+ "",
5389
+ "一个包名归第一次发布的人(条目文件里的 `owner`)。换一个署名发同名会被拒;要换人,先把这一条的 `owner` 改成新的人。",
5390
+ "同一个市场里,一个模块 id(`metadata.unit`)只能被一个条目使用;另一个包名想用同一个 id / 产物目录也会被拒。",
5391
+ "",
5392
+ "## 使用这个索引",
5393
+ "",
5394
+ "在工作台的「模块市场 → 发现」里添加本仓库地址,即可浏览与安装。",
5395
+ ""
5396
+ ];
5397
+ function problem(kind, detail, at) {
5398
+ return {
5399
+ kind,
5400
+ detail,
5401
+ ...at === void 0 ? {} : { at }
5402
+ };
5403
+ }
5404
+ function docFieldsFromEntry(entry) {
5405
+ return {
5406
+ name: entry.name,
5407
+ ...entry.title === void 0 ? {} : { title: entry.title },
5408
+ ...entry.description === void 0 ? {} : { description: entry.description },
5409
+ ...entry.author === void 0 ? {} : { author: entry.author },
5410
+ ...entry.owner === void 0 ? {} : { owner: entry.owner },
5411
+ ...entry.homepage === void 0 ? {} : { homepage: entry.homepage },
5412
+ ...entry.icon === void 0 ? {} : { icon: entry.icon },
5413
+ ...entry.metadata === void 0 ? {} : { metadata: entry.metadata }
5414
+ };
5415
+ }
5416
+ function marketVersionDocOf(entry, version) {
5417
+ return {
5418
+ ...docFieldsFromEntry(entry),
5419
+ ...version
5420
+ };
5421
+ }
5422
+ function assembleMarketIndex(files) {
5423
+ const problems = [];
5424
+ const groups = /* @__PURE__ */ new Map();
5425
+ const ordered = [...files].sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0);
5426
+ for (const file of ordered) {
5427
+ let raw;
5428
+ try {
5429
+ raw = JSON.parse(file.text);
5430
+ } catch (error) {
5431
+ problems.push(problem("not-json", `JSON 解析失败:${error instanceof Error ? error.message : String(error)}`, file.path));
5432
+ continue;
5433
+ }
5434
+ const parsed = parseMarketEntryDoc(raw);
5435
+ if (!parsed.ok) {
5436
+ for (const one of parsed.problems) problems.push({
5437
+ ...one,
5438
+ at: file.path
5439
+ });
5440
+ continue;
5441
+ }
5442
+ const doc = parsed.doc;
5443
+ const group = groups.get(doc.name) ?? {
5444
+ versions: /* @__PURE__ */ new Map(),
5445
+ meta: void 0,
5446
+ fallback: void 0
5447
+ };
5448
+ const at = doc.publishTimestamp ?? 0;
5449
+ if ("version" in doc) {
5450
+ if (group.versions.has(doc.version)) problems.push(problem("duplicate-version", `版本重复:${doc.version}`, file.path));
5451
+ group.versions.set(doc.version, {
5452
+ version: doc.version,
5453
+ source: doc.source,
5454
+ ...doc.publishTimestamp === void 0 ? {} : { publishTimestamp: doc.publishTimestamp },
5455
+ ...doc.engines === void 0 ? {} : { engines: doc.engines }
5456
+ });
5457
+ if (group.fallback === void 0 || at > group.fallback.at || at === group.fallback.at && file.path > group.fallback.path) group.fallback = {
5458
+ at,
5459
+ path: file.path,
5460
+ doc
5461
+ };
5462
+ } else {
5463
+ const epoch = doc.epoch ?? 0;
5464
+ 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 = {
5465
+ epoch,
5466
+ at,
5467
+ path: file.path,
5468
+ doc
5469
+ };
5470
+ }
5471
+ groups.set(doc.name, group);
5472
+ }
5473
+ if (problems.length > 0) return {
5474
+ ok: false,
5475
+ problems
5476
+ };
5477
+ const plugins = [...groups.entries()].map(([name, group]) => {
5478
+ const meta = group.meta?.doc ?? group.fallback?.doc;
5479
+ return {
5480
+ name,
5481
+ versions: sortMarketVersions([...group.versions.values()]),
5482
+ ...meta?.title === void 0 ? {} : { title: meta.title },
5483
+ ...meta?.description === void 0 ? {} : { description: meta.description },
5484
+ ...meta?.author === void 0 ? {} : { author: meta.author },
5485
+ ...meta?.owner === void 0 ? {} : { owner: meta.owner },
5486
+ ...meta?.homepage === void 0 ? {} : { homepage: meta.homepage },
5487
+ ...meta?.icon === void 0 ? {} : { icon: meta.icon },
5488
+ ...meta?.metadata === void 0 ? {} : { metadata: meta.metadata }
5489
+ };
5490
+ });
5491
+ plugins.sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
5492
+ return {
5493
+ ok: true,
5494
+ index: {
5495
+ format: 1,
5496
+ plugins
5497
+ }
5498
+ };
5499
+ }
5500
+ function marketEntryFilesOf(index, dir = MARKET_INDEX_DIR) {
5501
+ const files = [];
5502
+ for (const entry of index.plugins) for (const version of entry.versions) files.push({
5503
+ path: marketEntryPath(dir, entry.name, version.version),
5504
+ text: `${JSON.stringify(marketVersionDocOf(entry, version), null, 2)}\n`
5505
+ });
5506
+ return files;
5507
+ }
5508
+ function collectMarketEntryFiles(dir) {
5509
+ if (!existsSync(dir)) return [];
5510
+ const files = [];
5511
+ const walk = (current) => {
5512
+ for (const item of readdirSync(current, { withFileTypes: true })) {
5513
+ const full = join(current, item.name);
5514
+ if (item.isDirectory()) {
5515
+ walk(full);
5516
+ continue;
5517
+ }
5518
+ if (!item.name.endsWith(".json")) continue;
5519
+ files.push({
5520
+ path: relative(dir, full).split(sep).join("/"),
5521
+ text: readFileSync(full, "utf8")
5522
+ });
5523
+ }
5524
+ };
5525
+ walk(dir);
5526
+ return files;
5527
+ }
5528
+ function marketEntryFilesOfLegacy(text, dir = MARKET_INDEX_DIR) {
5529
+ const parsed = parseMarketIndexJson(text);
5530
+ if (!parsed.ok) return void 0;
5531
+ return marketEntryFilesOf(parsed.index, dir);
5283
5532
  }
5284
5533
  //#endregion
5285
5534
  //#region ../shared/dist/host/market-index-claim.js
@@ -5324,8 +5573,11 @@ function claimOfEntry(entry, publisher) {
5324
5573
  owner
5325
5574
  };
5326
5575
  }
5327
- function marketArtifactPath(name) {
5328
- return `packages/${name}`;
5576
+ function marketArtifactRoot(name) {
5577
+ return `${MARKET_ARTIFACTS_DIR}/${name}`;
5578
+ }
5579
+ function marketArtifactPathFor(name, version) {
5580
+ return `${MARKET_ARTIFACTS_DIR}/${name}/${version}`;
5329
5581
  }
5330
5582
  function entryUnitOf(entry) {
5331
5583
  const unit = entry.metadata?.unit?.trim();
@@ -6470,7 +6722,7 @@ function publishClaimDenyOf(claim, name) {
6470
6722
  if (claim === void 0) return void 0;
6471
6723
  if (claim.state === "unit-taken") return `市场里 ${claim.entry} 已经用了这个模块 id —— 两个模块不能共用一个,装在一起会撞。给这个模块换一个 unit(dsh.workbench.unit)再发。`;
6472
6724
  if (claim.state === "path-taken") return `市场仓库里的产物目录已经被 ${claim.entry} 占了 —— 换一个包名再发。`;
6473
- if (claim.state === "conflict") return `市场里 ${name} 已经有主了(${claim.owner})—— 同一个名字不能换人发。换一个带团队 scope 的包名;你就是原来的作者(只是换了机器或署名),用 author 参数带上原来的署名;确实是接手,让索引维护者改 market.json 里这一条的 owner。`;
6725
+ if (claim.state === "conflict") return `市场里 ${name} 已经有主了(${claim.owner})—— 同一个名字不能换人发。换一个带团队 scope 的包名;你就是原来的作者(只是换了机器或署名),用 author 参数带上原来的署名;确实是接手,让索引维护者改 market/<包名>/ 条目文件里的 owner。`;
6474
6726
  }
6475
6727
  //#endregion
6476
6728
  //#region src/modules/panels/model/approval.ts
@@ -10148,10 +10400,16 @@ var EMPTY_INDEX = {
10148
10400
  plugins: []
10149
10401
  };
10150
10402
  function publishedIndexOf(source) {
10151
- const file = join(workbenchRoot(), "market-index", marketSourceKey(source), "worktree", source.path ?? "market.json");
10152
- if (!existsSync(file)) return void 0;
10403
+ const root = join(workbenchRoot(), "market-index", marketSourceKey(source), "worktree");
10153
10404
  try {
10154
- const parsed = parseMarketIndexJson(readFileSync(file, "utf8"));
10405
+ const files = collectMarketEntryFiles(join(root, source.path ?? "market"));
10406
+ if (files.length > 0) {
10407
+ const assembled = assembleMarketIndex(files);
10408
+ return assembled.ok ? assembled.index : void 0;
10409
+ }
10410
+ const legacy = join(root, "market.json");
10411
+ if (!existsSync(legacy)) return void 0;
10412
+ const parsed = parseMarketIndexJson(readFileSync(legacy, "utf8"));
10155
10413
  return parsed.ok ? parsed.index : void 0;
10156
10414
  } catch {
10157
10415
  return;
@@ -10192,8 +10450,8 @@ function marketOf(dir, name, version) {
10192
10450
  market: {
10193
10451
  url: source.url,
10194
10452
  ...source.ref === void 0 ? {} : { ref: source.ref },
10195
- indexPath: source.path ?? "market.json",
10196
- artifactPath: marketArtifactPath(name),
10453
+ indexPath: source.path ?? "market",
10454
+ artifactPath: marketArtifactPathFor(name, nextVersion ?? version),
10197
10455
  files: ARTIFACTS.filter((one) => existsSync(join(dir, one))),
10198
10456
  ...nextVersion === void 0 ? {} : { nextVersion }
10199
10457
  },
@@ -10290,7 +10548,7 @@ function publishPreviewOf(root, unit, options = {}) {
10290
10548
  name,
10291
10549
  publisher: author,
10292
10550
  unit,
10293
- artifactPath: marketArtifactPath(name)
10551
+ artifactPath: marketArtifactRoot(name)
10294
10552
  });
10295
10553
  return {
10296
10554
  name,
@@ -12391,7 +12649,82 @@ function dependencyStillBehind(target, capabilities) {
12391
12649
  }
12392
12650
  }
12393
12651
  //#endregion
12652
+ //#region src/shared/api/host/self-update-marker.ts
12653
+ function readSelfUpdateMarker(file) {
12654
+ try {
12655
+ const raw = JSON.parse(readFileSync(file, "utf8"));
12656
+ return {
12657
+ ...typeof raw.pkg === "string" ? { pkg: raw.pkg } : {},
12658
+ ...typeof raw.state === "string" ? { state: raw.state } : {}
12659
+ };
12660
+ } catch {
12661
+ return {};
12662
+ }
12663
+ }
12664
+ //#endregion
12665
+ //#region src/modules/panels/model/market-swap-stuck.ts
12666
+ var DEFAULT_POLL_MS = 400;
12667
+ var DEFAULT_WAIT_MS = 6e4;
12668
+ function sleep(ms) {
12669
+ return new Promise((resolve) => {
12670
+ setTimeout(resolve, ms);
12671
+ });
12672
+ }
12673
+ function landedIn(after, input) {
12674
+ if (after.kind !== "module") return false;
12675
+ return input.to === "" ? after.version !== input.from : after.version === input.to;
12676
+ }
12677
+ async function swapExternally(deps, input) {
12678
+ let marker;
12679
+ try {
12680
+ marker = deps.supply.startSelfUpdate({
12681
+ dshHome: deps.dshHome,
12682
+ profile: deps.profile,
12683
+ pkg: input.pkg,
12684
+ from: input.from,
12685
+ to: input.to,
12686
+ pinned: input.pinned
12687
+ }, (message) => deps.logger?.warn?.(`[dsh-workbench] ${message}`)).paths.marker;
12688
+ } catch (error) {
12689
+ deps.logger?.warn?.(`[dsh-workbench] 外部换版起不来(${input.pkg}):${error instanceof Error ? error.message : String(error)}`);
12690
+ return;
12691
+ }
12692
+ const pollMs = deps.pollMs ?? DEFAULT_POLL_MS;
12693
+ const deadline = Date.now() + (deps.waitMs ?? DEFAULT_WAIT_MS);
12694
+ let state = "timeout";
12695
+ while (Date.now() < deadline) {
12696
+ const read = readSelfUpdateMarker(marker);
12697
+ if (read.pkg === input.pkg && read.state !== void 0 && read.state !== "running") {
12698
+ state = read.state;
12699
+ break;
12700
+ }
12701
+ await sleep(pollMs);
12702
+ }
12703
+ const after = deps.installed?.(input.pkg) ?? {
12704
+ kind: "unknown",
12705
+ pkg: input.pkg
12706
+ };
12707
+ const version = after.kind === "module" ? after.version : "读不到";
12708
+ deps.logger?.info?.(`[dsh-workbench] 外部换版 ${input.pkg}:${state},盘上 ${version}`);
12709
+ return landedIn(after, input) ? after : void 0;
12710
+ }
12711
+ //#endregion
12394
12712
  //#region src/modules/panels/model/market-update-bundle.ts
12713
+ var SHIPPED_APPLICATIONS = /* @__PURE__ */ new Set([
12714
+ "applied",
12715
+ "restart-required",
12716
+ "overridden"
12717
+ ]);
12718
+ function stillStuck(change, after, before) {
12719
+ if (!SHIPPED_APPLICATIONS.has(change.application)) return true;
12720
+ return before !== void 0 && after.kind === "module" && after.version === before;
12721
+ }
12722
+ function remedyOf(deps, change, before, afterVersion, spec) {
12723
+ const name = deps.profileName ?? deps.profile;
12724
+ if (name === void 0) return void 0;
12725
+ if (!!SHIPPED_APPLICATIONS.has(change.application) && !(before !== void 0 && afterVersion === before)) return void 0;
12726
+ return { command: `dsh plugin --profile ${name} add ${quoteShellArg(spec)}` };
12727
+ }
12395
12728
  async function restoreRemovedBundle(pkg, before, registry, deps) {
12396
12729
  if (before === void 0) return void 0;
12397
12730
  try {
@@ -12446,7 +12779,7 @@ async function updateModuleBundle(unit, deps) {
12446
12779
  kind: "unknown",
12447
12780
  pkg
12448
12781
  }).kind === "module" ? void 0 : await restoreRemovedBundle(pkg, before, registry, deps);
12449
- const after = deps.installed?.(pkg) ?? {
12782
+ let after = deps.installed?.(pkg) ?? {
12450
12783
  kind: "unknown",
12451
12784
  pkg
12452
12785
  };
@@ -12456,12 +12789,43 @@ async function updateModuleBundle(unit, deps) {
12456
12789
  after,
12457
12790
  ...restored === void 0 ? {} : { restored }
12458
12791
  });
12792
+ let change = verified.change;
12793
+ let restoredVersion = verified.restored;
12794
+ const swap = stillStuck(change, after, before) && before !== void 0 && deps.dshHome !== void 0 && deps.profileName !== void 0 ? await swapExternally({
12795
+ supply,
12796
+ installed: deps.installed,
12797
+ dshHome: deps.dshHome,
12798
+ profile: deps.profileName,
12799
+ ...deps.logger === void 0 ? {} : { logger: deps.logger },
12800
+ ...deps.swapWaitMs === void 0 ? {} : { waitMs: deps.swapWaitMs },
12801
+ ...deps.swapPollMs === void 0 ? {} : { pollMs: deps.swapPollMs }
12802
+ }, {
12803
+ pkg,
12804
+ from: before,
12805
+ to: latest ?? "",
12806
+ pinned: latest !== void 0
12807
+ }) : void 0;
12808
+ if (swap !== void 0) {
12809
+ const swapVersion = swap.kind === "module" ? swap.version : "";
12810
+ after = swap;
12811
+ restoredVersion = void 0;
12812
+ change = {
12813
+ application: "restart-required",
12814
+ stage: "install",
12815
+ changed: true,
12816
+ target: pkg,
12817
+ bundle: pkg,
12818
+ ...swapVersion === "" ? {} : { version: swapVersion }
12819
+ };
12820
+ }
12459
12821
  await deps.refreshSupply(unit).catch(() => void 0);
12822
+ const remedy = stillStuck(change, after, before) ? remedyOf(deps, change, before, after.kind === "module" ? after.version : void 0, spec) : void 0;
12460
12823
  return {
12461
12824
  ...outcome,
12462
- change: verified.change,
12825
+ change,
12463
12826
  module: after,
12464
- ...verified.restored === void 0 ? {} : { restored: verified.restored }
12827
+ ...restoredVersion === void 0 ? {} : { restored: restoredVersion },
12828
+ ...remedy === void 0 ? {} : { remedy }
12465
12829
  };
12466
12830
  }
12467
12831
  //#endregion
@@ -12778,14 +13142,14 @@ function createPluginActions(options) {
12778
13142
  unreadable: []
12779
13143
  };
12780
13144
  }
12781
- function decide(entry, held) {
13145
+ function decide(entry, held, claimedBy) {
12782
13146
  const plugin = pluginNameOf(entry);
12783
13147
  if (plugin === void 0) return {
12784
13148
  act: "keep",
12785
13149
  kept: { kind: "unnamed" }
12786
13150
  };
12787
13151
  const refs = referencesOf();
12788
- const by = options.claimedBy?.(plugin) ?? [];
13152
+ const by = claimedBy?.(plugin) ?? options.claimedBy?.(plugin) ?? [];
12789
13153
  const retention = pluginRetentionOf({
12790
13154
  held,
12791
13155
  claimedBy: by,
@@ -12852,12 +13216,12 @@ function createPluginActions(options) {
12852
13216
  name: decision.plugin
12853
13217
  };
12854
13218
  },
12855
- async sweep() {
13219
+ async sweep(claimedBy) {
12856
13220
  if (ledger === void 0) return [];
12857
13221
  const removed = [];
12858
13222
  for (const entry of ledger.all()) {
12859
13223
  if (entry.releasedAt === void 0) continue;
12860
- const decision = decide(entry, false);
13224
+ const decision = decide(entry, false, claimedBy);
12861
13225
  if (decision.act === "keep") continue;
12862
13226
  try {
12863
13227
  await removeFiles(decision.plugin);
@@ -12886,17 +13250,18 @@ function createEnvDropper(options) {
12886
13250
  unreadable: []
12887
13251
  };
12888
13252
  }
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);
13253
+ function usersOf(plugin, except, roster) {
13254
+ 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
13255
  }
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);
13256
+ function libUsersOf(name, unit, roster) {
13257
+ 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
13258
  }
12895
13259
  return {
12896
13260
  usersOf,
12897
- async drop(unit, needs, before) {
13261
+ async drop(unit, needs, before, roster) {
12898
13262
  const kept = /* @__PURE__ */ new Set();
12899
- const index = buildSupplyIndex(catalog().filter((item) => options.isReleased?.(item.id) !== true).map((item) => ({
13263
+ const known = roster ?? catalog();
13264
+ const index = buildSupplyIndex(known.filter((item) => options.isReleased?.(item.id) !== true).map((item) => ({
12900
13265
  id: item.id,
12901
13266
  needs: needsOf(item)
12902
13267
  })), { env: process.env });
@@ -12905,7 +13270,7 @@ function createEnvDropper(options) {
12905
13270
  const onMachine = new Set(before.needs.filter((state) => state.kind === "extension" && state.status !== "missing").map((state) => state.name));
12906
13271
  const unknown = !before.needs.some((state) => state.kind === "extension");
12907
13272
  for (const need of extensions) {
12908
- const users = usersOf(need.name, unit);
13273
+ const users = usersOf(need.name, unit, known);
12909
13274
  const retention = pluginRetentionOf({
12910
13275
  held: ledger?.heldAsPlugin(need.name) !== void 0,
12911
13276
  claimedBy: users,
@@ -12925,7 +13290,7 @@ function createEnvDropper(options) {
12925
13290
  await run(`${quoteShellArg(command)} plugin uninstall ${quoteShellArg(need.name)}`);
12926
13291
  }
12927
13292
  for (const need of libs) {
12928
- const keepers = libUsersOf(need.name, unit);
13293
+ const keepers = libUsersOf(need.name, unit, known);
12929
13294
  if (keepers.length > 0) {
12930
13295
  for (const id of keepers) kept.add(id);
12931
13296
  continue;
@@ -13234,7 +13599,8 @@ function createMarketService(options) {
13234
13599
  pkg
13235
13600
  };
13236
13601
  const unit = module.kind === "module" ? module.unit : void 0;
13237
- const manifest = unit === void 0 ? void 0 : catalog().find((item) => item.id === unit);
13602
+ const roster = catalog();
13603
+ const manifest = unit === void 0 ? void 0 : roster.find((item) => item.id === unit);
13238
13604
  const before = unit === void 0 ? void 0 : await capabilityOf(unit, true);
13239
13605
  const change = await removeDshBundle({
13240
13606
  manager,
@@ -13247,8 +13613,8 @@ function createMarketService(options) {
13247
13613
  change,
13248
13614
  keptFor: []
13249
13615
  };
13250
- const keptFor = manifest === void 0 || before === void 0 ? [] : await envDropper.drop(unit, needsOf(manifest), before);
13251
- await plugins.sweep();
13616
+ const keptFor = manifest === void 0 || before === void 0 ? [] : await envDropper.drop(unit, needsOf(manifest), before, roster);
13617
+ await plugins.sweep((plugin) => envDropper.usersOf(plugin, unit, roster));
13252
13618
  onUninstalled?.(unit);
13253
13619
  return {
13254
13620
  change,
@@ -13260,15 +13626,22 @@ function createMarketService(options) {
13260
13626
  },
13261
13627
  async updateBundle(unit) {
13262
13628
  const manager = requireBundles();
13629
+ const home = dshHome?.();
13630
+ const profile = profileName?.() ?? (profileDir === void 0 || profileDir === null ? void 0 : basename(profileDir));
13263
13631
  return updateModuleBundle(unit, {
13264
13632
  supply,
13265
13633
  modules,
13266
13634
  installed,
13267
13635
  shell,
13268
13636
  manager,
13637
+ ...profile === void 0 ? {} : {
13638
+ profile,
13639
+ profileName: profile
13640
+ },
13641
+ ...home === void 0 ? {} : { dshHome: home },
13269
13642
  routeDeps: routeDeps(manager),
13270
13643
  sweep: async () => {
13271
- await plugins.sweep();
13644
+ await plugins.sweep((plugin) => envDropper.usersOf(plugin, void 0, catalog()));
13272
13645
  },
13273
13646
  refreshSupply: async (id) => {
13274
13647
  await service.update(id);
@@ -14000,7 +14373,7 @@ function createMarketIndexReader(options) {
14000
14373
  }
14001
14374
  };
14002
14375
  const repo = repoOf(options.root, source);
14003
- const path = (source.path ?? "market.json").replace(/^\/+/, "");
14376
+ const path = ((source.path ?? "market").replace(/^\/+/, "") || "market").replace(/\/+$/, "");
14004
14377
  const ref = (source.ref ?? "").trim();
14005
14378
  let revision = "FETCH_HEAD";
14006
14379
  mkdirSync(directoryOf(options.root, source), { recursive: true });
@@ -14025,17 +14398,6 @@ function createMarketIndexReader(options) {
14025
14398
  detail: "仓库还没有任何提交"
14026
14399
  }
14027
14400
  }, 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
14401
  const head = await run(`git -C ${quoteShellArg(repo)} rev-parse ${revision}`);
14040
14402
  if (!head.ok) return head;
14041
14403
  const commit = head.stdout.trim();
@@ -14046,26 +14408,105 @@ function createMarketIndexReader(options) {
14046
14408
  detail: `读到的 commit 不是 40 位:${commit}`
14047
14409
  }
14048
14410
  };
14049
- const parsed = parseMarketIndexJson(shown.stdout);
14050
- if (!parsed.ok) return {
14411
+ const listed = await run(`git -C ${quoteShellArg(repo)} ls-tree -r ${quoteShellArg(commit)} -- ${quoteShellArg(path)}`);
14412
+ if (!listed.ok) {
14413
+ if (listed.problem.kind === "git-file") return withBranch({
14414
+ ok: false,
14415
+ problem: {
14416
+ kind: "git-missing-index",
14417
+ detail: `索引 ${path} 不在仓库里`
14418
+ }
14419
+ }, repo);
14420
+ return listed;
14421
+ }
14422
+ const rows = listed.stdout.split("\n").map((line) => line.trim()).filter((line) => line !== "").flatMap((line) => {
14423
+ const [meta, file] = line.split(" ");
14424
+ const type = (meta ?? "").split(" ")[1] ?? "";
14425
+ return file === void 0 ? [] : [{
14426
+ type,
14427
+ file
14428
+ }];
14429
+ });
14430
+ const legacy = rows.find((one) => one.type === "blob" && one.file === path);
14431
+ const entries = rows.filter((one) => one.type === "blob" && one.file !== path && one.file.startsWith(`${path}/`) && one.file.endsWith(".json"));
14432
+ const finish = async (index, text) => {
14433
+ const fetchedAt = now();
14434
+ await writeFileAtomic(indexFileOf(options.root, source), text);
14435
+ await writeMeta(metaFileOf(options.root, source), {
14436
+ fetchedAt,
14437
+ commit
14438
+ });
14439
+ return {
14440
+ ok: true,
14441
+ index,
14442
+ fetchedAt,
14443
+ commit
14444
+ };
14445
+ };
14446
+ const readLegacy = async (file) => {
14447
+ const shown = await run(`git -C ${quoteShellArg(repo)} show ${quoteShellArg(commit)}:${quoteShellArg(file)}`);
14448
+ if (!shown.ok) return void 0;
14449
+ const parsed = parseMarketIndexJson(shown.stdout);
14450
+ if (!parsed.ok) return {
14451
+ ok: false,
14452
+ problem: {
14453
+ kind: "format",
14454
+ detail: parsed.problems.map((one) => one.detail).join(";")
14455
+ }
14456
+ };
14457
+ return finish(parsed.index, shown.stdout);
14458
+ };
14459
+ if (legacy !== void 0) {
14460
+ const outcome = await readLegacy(path);
14461
+ if (outcome !== void 0) return outcome;
14462
+ }
14463
+ if (entries.length > 0) {
14464
+ const parts = [];
14465
+ let failed;
14466
+ const queue = [...entries];
14467
+ const worker = async () => {
14468
+ for (;;) {
14469
+ const next = queue.shift();
14470
+ if (next === void 0 || failed !== void 0) return;
14471
+ const shown = await run(`git -C ${quoteShellArg(repo)} show ${quoteShellArg(commit)}:${quoteShellArg(next.file)}`);
14472
+ if (!shown.ok) {
14473
+ failed = shown;
14474
+ return;
14475
+ }
14476
+ parts.push({
14477
+ path: next.file,
14478
+ text: shown.stdout
14479
+ });
14480
+ }
14481
+ };
14482
+ await Promise.all(Array.from({ length: Math.min(6, entries.length) }, () => worker()));
14483
+ if (failed !== void 0) return failed;
14484
+ const assembled = assembleMarketIndex(parts);
14485
+ if (!assembled.ok) return {
14486
+ ok: false,
14487
+ problem: {
14488
+ kind: "format",
14489
+ detail: assembled.problems.map((one) => one.detail).join(";")
14490
+ }
14491
+ };
14492
+ return finish(assembled.index, `${JSON.stringify(assembled.index, null, 2)}\n`);
14493
+ }
14494
+ if (listed.stdout.trim() !== "") {
14495
+ const empty = {
14496
+ format: 1,
14497
+ plugins: []
14498
+ };
14499
+ return finish(empty, `${JSON.stringify(empty, null, 2)}\n`);
14500
+ }
14501
+ const legacyText = await readLegacy(MARKET_INDEX_PATH);
14502
+ if (legacyText !== void 0) return legacyText;
14503
+ return withBranch({
14051
14504
  ok: false,
14052
14505
  problem: {
14053
- kind: "format",
14054
- detail: parsed.problems.map((one) => one.detail).join(";")
14506
+ kind: "git-missing-index",
14507
+ detail: `索引 ${path} 不在仓库里`
14055
14508
  }
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
- };
14509
+ }, repo);
14069
14510
  }
14070
14511
  async function read(source, callOptions = {}) {
14071
14512
  const key = marketSourceKey(source);
@@ -14413,16 +14854,31 @@ function createMarketSourceInitializer(options) {
14413
14854
  ], workdir);
14414
14855
  if (created.exitCode !== 0) throw new Error(`建分支 ${branch} 没过:${tailLines(created.stderr, 4)}`);
14415
14856
  }
14416
- const indexPath = (source.path ?? "market.json").replace(/^\/+/, "") || "market.json";
14417
- const indexFile = join(workdir, indexPath);
14857
+ const indexPath = ((source.path ?? "market").replace(/^\/+/, "") || "market").replace(/\/+$/, "");
14858
+ const indexDir = join(workdir, indexPath);
14859
+ const indexMarker = join(indexDir, "README.md");
14418
14860
  const stampOnly = request.stampOnly === true;
14419
- if (stampOnly && !existsSync(indexFile)) throw new Error(`仓库里还没有 ${indexPath} —— 先选中这个源,按发现页的提示把它初始化成索引`);
14861
+ if (stampOnly && !existsSync(indexDir)) throw new Error(`仓库里还没有 ${indexPath}/ —— 先选中这个源,按发现页的提示把它初始化成索引`);
14420
14862
  const written = [];
14421
14863
  if (!stampOnly) {
14422
- if (!existsSync(indexFile)) {
14423
- mkdirSync(dirname(indexFile), { recursive: true });
14424
- writeFileSync(indexFile, MARKET_INDEX_SEED);
14425
- written.push(indexPath);
14864
+ const legacyFile = join(workdir, "market.json");
14865
+ if (indexPath !== "market.json" && existsSync(legacyFile)) {
14866
+ const migrated = marketEntryFilesOfLegacy(readFileSync(legacyFile, "utf8"), indexPath);
14867
+ if (migrated !== void 0) {
14868
+ for (const file of migrated) {
14869
+ const target = join(workdir, ...file.path.split("/"));
14870
+ mkdirSync(dirname(target), { recursive: true });
14871
+ writeFileSync(target, file.text);
14872
+ written.push(file.path);
14873
+ }
14874
+ rmSync(legacyFile);
14875
+ written.push("market.json");
14876
+ }
14877
+ }
14878
+ if (!existsSync(indexMarker)) {
14879
+ mkdirSync(indexDir, { recursive: true });
14880
+ writeFileSync(indexMarker, MARKET_ENTRIES_README_LINES.join("\n"));
14881
+ written.push(`${indexPath}/README.md`);
14426
14882
  }
14427
14883
  const readmeFile = join(workdir, "README.md");
14428
14884
  if (!existsSync(readmeFile)) {
@@ -14691,7 +15147,7 @@ function failureText(change) {
14691
15147
  const code = change.errorCode ?? change.application;
14692
15148
  if (code === "already-installed") return "它已经在这份 profile 里,而这次没能把它卸下来(这一版 dsh 不肯就地覆盖装,得先卸再装)";
14693
15149
  const surface = hostSurface();
14694
- return REMOVE_BLOCKED.has(code) ? `旧版卸不掉,而这一版 dsh 又不许就地覆盖装 —— 让用户${surface.restartHint},或者${surface.deployment === "desktop" ? "从 Desktop 的插件页更新" : "从终端更新"}` : code;
15150
+ return REMOVE_BLOCKED.has(code) ? `旧版卸不掉(dsh 不肯就地覆盖正在用的模块),自动从外部换也没成 —— 让用户从终端跑 dsh plugin --profile <名字> add <包>@<版本>,跑完${surface.restartHint}` : code;
14695
15151
  }
14696
15152
  function describeInstall(spec, module, supply, change) {
14697
15153
  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 +15281,14 @@ function registerMarketTools(ctx, market, sources) {
14825
15281
  async execute(raw) {
14826
15282
  const id = await idOrList(raw, MODULES_UNINSTALL_TOOL);
14827
15283
  const outcome = await market.uninstall(id);
15284
+ const change = outcome.change;
15285
+ if (change !== void 0 && change.application !== "applied") {
15286
+ const code = change.errorCode ?? change.application;
15287
+ return {
15288
+ module: outcome.entry,
15289
+ text: `没卸下来:${id} 还在这份 profile 里(${code})—— 这一版 dsh 不肯卸正在加载的模块。让用户从终端跑 dsh plugin --profile <profile> remove ${id} 的包,跑完重启一次工作台;别再调这个工具重试。`
15290
+ };
15291
+ }
14828
15292
  const kept = outcome.keptFor.length === 0 ? "" : `(还有模块在面板上要用其中一份安装,所以那份留着:${outcome.keptFor.join("、")})`;
14829
15293
  return {
14830
15294
  module: outcome.entry,
@@ -14980,7 +15444,7 @@ function registerMarketTools(ctx, market, sources) {
14980
15444
  branch: result.branch,
14981
15445
  commit: result.commit,
14982
15446
  changed: true,
14983
- text: `已把「${source.label}」设为可发布(${source.url},分支 ${result.branch})—— 仓库本身没动。之后发布可以落到它的 ${source.path ?? "market.json"}。`
15447
+ text: `已把「${source.label}」设为可发布(${source.url},分支 ${result.branch})—— 仓库本身没动。之后发布可以落到它的 ${source.path ?? "market"}/。`
14984
15448
  };
14985
15449
  }
14986
15450
  };