@dsh-workbench/core 0.2.4 → 0.2.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.
@@ -1,6 +1,6 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
- import fs, { chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, readdirSync, readlinkSync, realpathSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
3
- import path, { delimiter, dirname, join, resolve, sep } from "node:path";
2
+ import fs, { chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, statSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
3
+ import path, { basename, delimiter, dirname, join, resolve, sep } from "node:path";
4
4
  import os from "node:os";
5
5
  import { spawn } from "node:child_process";
6
6
  import { fileURLToPath } from "node:url";
@@ -234,6 +234,9 @@ function parseJsonLoose(text) {
234
234
  return;
235
235
  }
236
236
  }
237
+ var DAY = 864e5;
238
+ 30 * DAY;
239
+ 365 * DAY;
237
240
  var PANEL_STATE_MAX_TEXT = 2e3;
238
241
  var PHASES = [
239
242
  "idle",
@@ -244,7 +247,7 @@ var PHASES = [
244
247
  function asRecord$10(value) {
245
248
  return typeof value === "object" && value !== null ? value : void 0;
246
249
  }
247
- function textOf$6(value, max) {
250
+ function textOf$7(value, max) {
248
251
  if (typeof value !== "string") return "";
249
252
  const trimmed = value.trim();
250
253
  return trimmed.length > max ? trimmed.slice(0, max) : trimmed;
@@ -257,8 +260,8 @@ function atOf(value) {
257
260
  function errorOf(raw) {
258
261
  const record = asRecord$10(raw);
259
262
  if (record === void 0) return void 0;
260
- const code = textOf$6(record["code"], 120);
261
- const message = textOf$6(record["message"], PANEL_STATE_MAX_TEXT);
263
+ const code = textOf$7(record["code"], 120);
264
+ const message = textOf$7(record["message"], PANEL_STATE_MAX_TEXT);
262
265
  if (code === "" && message === "") return void 0;
263
266
  return {
264
267
  code: code === "" ? "unknown" : code,
@@ -271,7 +274,7 @@ function parseCardsReport(payload) {
271
274
  ok: false,
272
275
  message: "请求体要是一个对象"
273
276
  };
274
- const workspace = textOf$6(record["workspace"], PANEL_STATE_MAX_TEXT);
277
+ const workspace = textOf$7(record["workspace"], PANEL_STATE_MAX_TEXT);
275
278
  if (workspace === "") return {
276
279
  ok: false,
277
280
  message: "workspace 不能是空的 —— 面板按工作区存"
@@ -285,9 +288,9 @@ function parseCardsReport(payload) {
285
288
  for (const item of raw.slice(0, 32)) {
286
289
  const card = asRecord$10(item);
287
290
  if (card === void 0) continue;
288
- const unit = textOf$6(card["unit"], 200);
289
- const route = textOf$6(card["route"], 400);
290
- const phase = textOf$6(card["phase"], 20);
291
+ const unit = textOf$7(card["unit"], 200);
292
+ const route = textOf$7(card["route"], 400);
293
+ const phase = textOf$7(card["phase"], 20);
291
294
  const at = atOf(card["at"]);
292
295
  if (unit === "" || route === "" || at === null || !PHASES.includes(phase)) continue;
293
296
  const error = errorOf(card["error"]);
@@ -317,9 +320,9 @@ function parseCrashReport(payload) {
317
320
  ok: false,
318
321
  message: "请求体要是一个对象"
319
322
  };
320
- const workspace = textOf$6(record["workspace"], PANEL_STATE_MAX_TEXT);
321
- const unit = textOf$6(record["unit"], 200);
322
- const reason = textOf$6(record["reason"], PANEL_STATE_MAX_TEXT);
323
+ const workspace = textOf$7(record["workspace"], PANEL_STATE_MAX_TEXT);
324
+ const unit = textOf$7(record["unit"], 200);
325
+ const reason = textOf$7(record["reason"], PANEL_STATE_MAX_TEXT);
323
326
  if (workspace === "") return {
324
327
  ok: false,
325
328
  message: "workspace 不能是空的"
@@ -565,20 +568,20 @@ var PERMISSION_CODES = /* @__PURE__ */ new Set([
565
568
  function asRecord$9(value) {
566
569
  return typeof value === "object" && value !== null ? value : void 0;
567
570
  }
568
- function textOf$5(value) {
571
+ function textOf$6(value) {
569
572
  return typeof value === "string" ? value.trim() : "";
570
573
  }
571
574
  function codeOf(page) {
572
575
  const record = asRecord$9(page);
573
- return record === void 0 ? "" : textOf$5(record["code"]);
576
+ return record === void 0 ? "" : textOf$6(record["code"]);
574
577
  }
575
578
  function messageOf$2(page) {
576
579
  const record = asRecord$9(page);
577
- return record === void 0 ? "" : textOf$5(record["message"]);
580
+ return record === void 0 ? "" : textOf$6(record["message"]);
578
581
  }
579
582
  function hintOf$1(page) {
580
583
  const record = asRecord$9(page);
581
- const hint = record === void 0 ? "" : textOf$5(record["hint"]);
584
+ const hint = record === void 0 ? "" : textOf$6(record["hint"]);
582
585
  return hint === "" ? void 0 : hint;
583
586
  }
584
587
  function pageFailed(page) {
@@ -809,7 +812,7 @@ var DEFAULT_TEXT_MAX = 200;
809
812
  function asRecord$8(value) {
810
813
  return typeof value === "object" && value !== null ? value : void 0;
811
814
  }
812
- function textOf$4(value) {
815
+ function textOf$5(value) {
813
816
  return typeof value === "string" ? value.trim() : "";
814
817
  }
815
818
  function entriesOf(value) {
@@ -822,8 +825,8 @@ function entriesOf(value) {
822
825
  if (item === void 0) continue;
823
826
  const nth = typeof item["nth"] === "number" ? item["nth"] : entries.length;
824
827
  entries.push({
825
- text: textOf$4(item["text"]),
826
- tag: textOf$4(item["tag"]),
828
+ text: textOf$5(item["text"]),
829
+ tag: textOf$5(item["tag"]),
827
830
  nth
828
831
  });
829
832
  }
@@ -832,7 +835,7 @@ function entriesOf(value) {
832
835
  function reasonOf(stdout, stderr) {
833
836
  const parsed = asRecord$8(parseJsonLoose(stdout));
834
837
  const error = parsed === void 0 ? void 0 : asRecord$8(parsed["error"]);
835
- const message = error === void 0 ? "" : textOf$4(error["message"]);
838
+ const message = error === void 0 ? "" : textOf$5(error["message"]);
836
839
  if (message !== "") return message;
837
840
  const lines = (stderr.trim() === "" ? stdout : stderr).split("\n").map((one) => one.trim()).filter((one) => one !== "");
838
841
  return (lines[lines.length - 1] ?? "").slice(-200);
@@ -840,7 +843,7 @@ function reasonOf(stdout, stderr) {
840
843
  function hintOf(stdout) {
841
844
  const parsed = asRecord$8(parseJsonLoose(stdout));
842
845
  const error = parsed === void 0 ? void 0 : asRecord$8(parsed["error"]);
843
- const hint = error === void 0 ? "" : textOf$4(error["hint"]);
846
+ const hint = error === void 0 ? "" : textOf$5(error["hint"]);
844
847
  return hint === "" ? void 0 : hint;
845
848
  }
846
849
  function sameOrigin(left, right) {
@@ -854,7 +857,7 @@ function urlOf(stdout) {
854
857
  const bare = (stdout.split("\n").map((one) => one.trim()).filter((one) => one !== "")[0] ?? "").replace(/^url:\s*/i, "").trim().replace(/^["']+|["']+$/g, "");
855
858
  if (bare.startsWith("{")) {
856
859
  const parsed = asRecord$8(parseJsonLoose(bare));
857
- const url = parsed === void 0 ? "" : textOf$4(parsed["url"]);
860
+ const url = parsed === void 0 ? "" : textOf$5(parsed["url"]);
858
861
  if (url !== "") return url;
859
862
  }
860
863
  return bare;
@@ -1869,7 +1872,7 @@ var PLUGIN_LEDGER_FILE = "plugins.json";
1869
1872
  function ledgerKeyOf(entry) {
1870
1873
  return `${entry.opencliHome}\u0000${entry.package}`;
1871
1874
  }
1872
- function textOf$3(value) {
1875
+ function textOf$4(value) {
1873
1876
  if (typeof value !== "string") return void 0;
1874
1877
  const trimmed = value.trim();
1875
1878
  return trimmed === "" ? void 0 : trimmed;
@@ -1877,13 +1880,13 @@ function textOf$3(value) {
1877
1880
  function entryOf(value) {
1878
1881
  if (typeof value !== "object" || value === null) return void 0;
1879
1882
  const box = value;
1880
- const opencliHome = textOf$3(box["opencliHome"]);
1881
- const pkg = textOf$3(box["package"]);
1882
- const installedAt = textOf$3(box["installedAt"]);
1883
+ const opencliHome = textOf$4(box["opencliHome"]);
1884
+ const pkg = textOf$4(box["package"]);
1885
+ const installedAt = textOf$4(box["installedAt"]);
1883
1886
  if (opencliHome === void 0 || pkg === void 0 || installedAt === void 0) return void 0;
1884
- const name = textOf$3(box["name"]);
1885
- const registry = textOf$3(box["registry"]);
1886
- const releasedAt = textOf$3(box["releasedAt"]);
1887
+ const name = textOf$4(box["name"]);
1888
+ const registry = textOf$4(box["registry"]);
1889
+ const releasedAt = textOf$4(box["releasedAt"]);
1887
1890
  return {
1888
1891
  opencliHome,
1889
1892
  package: pkg,
@@ -3269,7 +3272,7 @@ function reconOf(commands, plugins, keywords) {
3269
3272
  };
3270
3273
  return {
3271
3274
  ...withNext,
3272
- text: textOf$2(withNext)
3275
+ text: textOf$3(withNext)
3273
3276
  };
3274
3277
  }
3275
3278
  function nextOf(answer) {
@@ -3282,7 +3285,7 @@ function nextOf(answer) {
3282
3285
  if (plugin !== void 0) return `这台机器另装的插件 ${plugin.name} 里有匹配:opencli ${plugin.name} --help 看全部命令`;
3283
3286
  return "这台机器上没有现成的 —— 两条路:① 给这个站点写 adapter:opencli skills read opencli-adapter-author;② 只读一次:workbench_diy action=probe(script 读接口 / css 读 DOM)";
3284
3287
  }
3285
- function textOf$2(answer) {
3288
+ function textOf$3(answer) {
3286
3289
  const lines = [];
3287
3290
  if (answer.sites.length > 0) {
3288
3291
  lines.push(`opencli 自带这 ${String(answer.sites.length)} 组能力(不用装、不用点头):`);
@@ -5463,16 +5466,16 @@ function applyPanelsPref(panels, payload) {
5463
5466
  //#region src/modules/panels/model/handoff.ts
5464
5467
  var PANELS_FACT = "panels";
5465
5468
  var CAPABILITY_FACT = "capability";
5466
- function textOf$1(value) {
5469
+ function textOf$2(value) {
5467
5470
  return typeof value === "string" ? value.trim() : "";
5468
5471
  }
5469
5472
  function actionOf(call) {
5470
- return textOf$1(call.args["action"]) || "list";
5473
+ return textOf$2(call.args["action"]) || "list";
5471
5474
  }
5472
5475
  function requestOf(call) {
5473
5476
  const action = actionOf(call);
5474
5477
  if (!PROVISION_ACTIONS.includes(action)) return void 0;
5475
- const target = textOf$1(call.args["unit"]) || textOf$1(call.args["id"]);
5478
+ const target = textOf$2(call.args["unit"]) || textOf$2(call.args["id"]);
5476
5479
  if (target === "") return void 0;
5477
5480
  const wanted = action;
5478
5481
  return isDependencyAction(wanted) ? {
@@ -5484,7 +5487,7 @@ function requestOf(call) {
5484
5487
  };
5485
5488
  }
5486
5489
  function panelsHandoffEntry(target = {}) {
5487
- const path = textOf$1(target.path);
5490
+ const path = textOf$2(target.path);
5488
5491
  return path === "" ? void 0 : {
5489
5492
  fact: PANELS_FACT,
5490
5493
  key: `path:${path}`
@@ -7625,7 +7628,7 @@ function bindRosterFacts(options) {
7625
7628
  function recordOf(value) {
7626
7629
  return typeof value === "object" && value !== null ? value : void 0;
7627
7630
  }
7628
- function textOf(value) {
7631
+ function textOf$1(value) {
7629
7632
  return typeof value === "string" ? value : "";
7630
7633
  }
7631
7634
  function stringsOf(value) {
@@ -7636,18 +7639,18 @@ function argsOf(raw) {
7636
7639
  return raw.flatMap((item) => {
7637
7640
  const record = recordOf(item);
7638
7641
  if (record === void 0) return [];
7639
- const name = textOf(record["name"]).trim();
7642
+ const name = textOf$1(record["name"]).trim();
7640
7643
  if (name === "") return [];
7641
7644
  const fallback = record["default"];
7642
7645
  return [{
7643
7646
  name,
7644
- type: textOf(record["type"]) || "string",
7647
+ type: textOf$1(record["type"]) || "string",
7645
7648
  required: record["required"] === true,
7646
7649
  positional: record["positional"] === true,
7647
7650
  valueRequired: record["valueRequired"] === true,
7648
7651
  choices: stringsOf(record["choices"]),
7649
7652
  ...fallback === void 0 || fallback === null ? {} : { defaultValue: fallback },
7650
- help: textOf(record["help"])
7653
+ help: textOf$1(record["help"])
7651
7654
  }];
7652
7655
  });
7653
7656
  }
@@ -7658,11 +7661,11 @@ function parsePluginRows(stdout) {
7658
7661
  for (const item of parsed) {
7659
7662
  const record = recordOf(item);
7660
7663
  if (record === void 0) return void 0;
7661
- const name = textOf(record["name"]).trim();
7664
+ const name = textOf$1(record["name"]).trim();
7662
7665
  if (name === "") return void 0;
7663
7666
  rows.push({
7664
7667
  name,
7665
- version: textOf(record["version"]),
7668
+ version: textOf$1(record["version"]),
7666
7669
  commands: stringsOf(record["commands"])
7667
7670
  });
7668
7671
  }
@@ -7675,17 +7678,17 @@ function parseCommandRows(stdout) {
7675
7678
  for (const item of parsed) {
7676
7679
  const record = recordOf(item);
7677
7680
  if (record === void 0) return void 0;
7678
- const site = textOf(record["site"]).trim();
7679
- const name = textOf(record["name"]).trim();
7681
+ const site = textOf$1(record["site"]).trim();
7682
+ const name = textOf$1(record["name"]).trim();
7680
7683
  if (site === "" || name === "") return void 0;
7681
7684
  rows.push({
7682
7685
  site,
7683
7686
  name,
7684
- description: textOf(record["description"]),
7685
- access: textOf(record["access"]),
7687
+ description: textOf$1(record["description"]),
7688
+ access: textOf$1(record["access"]),
7686
7689
  browser: record["browser"] === true,
7687
- siteSession: textOf(record["siteSession"]),
7688
- ...textOf(record["domain"]) === "" ? {} : { domain: textOf(record["domain"]) },
7690
+ siteSession: textOf$1(record["siteSession"]),
7691
+ ...textOf$1(record["domain"]) === "" ? {} : { domain: textOf$1(record["domain"]) },
7689
7692
  args: argsOf(record["args"])
7690
7693
  });
7691
7694
  }
@@ -8218,6 +8221,326 @@ function createPluginUnits(options) {
8218
8221
  };
8219
8222
  }
8220
8223
  //#endregion
8224
+ //#region src/shared/api/config-sync.ts
8225
+ var CONFIG_SYNC_ROUTE = "config-sync";
8226
+ var CONFIG_SYNC_RESOURCE = "configSync";
8227
+ //#endregion
8228
+ //#region src/shared/api/host/config-sync.ts
8229
+ var ID_LINE = /^- id:[ \t]*"?([^"\s#]+)/;
8230
+ var INSERT_LINE = /^- insert:[ \t]*$/;
8231
+ var NESTED_ID = /^[ \t]+-[ \t]*id:[ \t]*"?([^"\s#]+)/;
8232
+ var NAME_LINE = /^[ \t]*-?[ \t]*name:[ \t]*['"]?([^'"\s#]+)/;
8233
+ var COMMENT_LINE = /^[ \t]*#/;
8234
+ function trimmedTail(lines) {
8235
+ let end = lines.length;
8236
+ while (end > 0 && (lines[end - 1] ?? "").trim() === "") end -= 1;
8237
+ return lines.slice(0, end);
8238
+ }
8239
+ function matched(text, pattern) {
8240
+ const found = pattern.exec(text);
8241
+ return found === null ? void 0 : found[1];
8242
+ }
8243
+ function blockOf(body, index) {
8244
+ const lines = body.split("\n");
8245
+ const first = lines[0] ?? "";
8246
+ const id = matched(first, ID_LINE);
8247
+ const nested = lines.flatMap((line) => {
8248
+ const one = matched(line, NESTED_ID);
8249
+ return one === void 0 ? [] : [one];
8250
+ });
8251
+ const insert = id === void 0 && INSERT_LINE.test(first);
8252
+ const label = id ?? nested.join(", ");
8253
+ const key = id !== void 0 ? `id:${id}` : insert ? `insert:${nested.join(", ")}` : `block:${String(index)}`;
8254
+ const names = lines.flatMap((line) => {
8255
+ const one = matched(line, NAME_LINE);
8256
+ return one === void 0 ? [] : [one];
8257
+ });
8258
+ return {
8259
+ key,
8260
+ id: label === "" ? key : label,
8261
+ insert,
8262
+ body,
8263
+ names
8264
+ };
8265
+ }
8266
+ function spannedBlocks(text) {
8267
+ const lines = text.split("\n");
8268
+ const starts = [];
8269
+ lines.forEach((line, index) => {
8270
+ if (/^- /.test(line) || line === "-") starts.push(index);
8271
+ });
8272
+ const blocks = [];
8273
+ for (let i = 0; i < starts.length; i += 1) {
8274
+ const from = starts[i] ?? 0;
8275
+ const to = i + 1 < starts.length ? starts[i + 1] ?? lines.length : lines.length;
8276
+ const body = trimmedTail(lines.slice(from, to)).join("\n");
8277
+ if (body.trim() === "") continue;
8278
+ blocks.push({
8279
+ ...blockOf(body, blocks.length),
8280
+ from,
8281
+ to
8282
+ });
8283
+ }
8284
+ return blocks;
8285
+ }
8286
+ function patchBlocks(text) {
8287
+ return spannedBlocks(text).map(({ from: _from, to: _to, ...block }) => block);
8288
+ }
8289
+ function normalizeBlock(body) {
8290
+ return body.split("\n").map((line) => line.replace(/\s+$/, "")).filter((line) => line.trim() !== "" && !COMMENT_LINE.test(line)).join("\n");
8291
+ }
8292
+ function syncRows(source, own, missing) {
8293
+ const mine = new Map(own.map((block) => [block.key, block]));
8294
+ const rows = [];
8295
+ for (const block of source) {
8296
+ const here = mine.get(block.key);
8297
+ if (here !== void 0 && normalizeBlock(here.body) === normalizeBlock(block.body)) continue;
8298
+ rows.push({
8299
+ key: block.key,
8300
+ id: block.id,
8301
+ insert: block.insert,
8302
+ state: here === void 0 ? "new" : "differs",
8303
+ body: block.body,
8304
+ ownBody: here === void 0 ? null : here.body,
8305
+ missing: missing(block)
8306
+ });
8307
+ }
8308
+ return rows;
8309
+ }
8310
+ function replaceBlocks(text, blocks) {
8311
+ const wanted = new Map(blocks.map((block) => [block.key, block]));
8312
+ const spans = spannedBlocks(text).filter((span) => wanted.has(span.key));
8313
+ const lines = text.split("\n");
8314
+ for (const span of [...spans].reverse()) {
8315
+ const replacement = (wanted.get(span.key)?.body ?? "").split("\n");
8316
+ lines.splice(span.from, span.to - span.from, ...replacement);
8317
+ }
8318
+ return lines.join("\n");
8319
+ }
8320
+ function appendBlocks(text, blocks) {
8321
+ const kept = text.split("\n").filter((line) => line.trim() !== "[]").join("\n").replace(/\s+$/, "");
8322
+ let out = kept === "" ? "" : `${kept}\n`;
8323
+ for (const block of blocks) out += `\n${block.body}\n`;
8324
+ return out;
8325
+ }
8326
+ function mergeBlocks(ownText, incoming, overwrite) {
8327
+ const own = patchBlocks(ownText);
8328
+ const mine = new Set(own.map((block) => block.key));
8329
+ const written = [];
8330
+ const skipped = [];
8331
+ const take = [];
8332
+ for (const block of incoming) {
8333
+ if (!mine.has(block.key) || overwrite.has(block.key)) {
8334
+ take.push(block);
8335
+ written.push(block.id);
8336
+ continue;
8337
+ }
8338
+ skipped.push(block.id);
8339
+ }
8340
+ if (take.length === 0) return {
8341
+ ok: true,
8342
+ text: ownText,
8343
+ written,
8344
+ skipped
8345
+ };
8346
+ const text = appendBlocks(replaceBlocks(ownText, take.filter((block) => mine.has(block.key))), take.filter((block) => !mine.has(block.key)));
8347
+ const check = patchBlocks(text);
8348
+ const expected = /* @__PURE__ */ new Set([...mine, ...take.map((block) => block.key)]);
8349
+ if (check.length !== expected.size || new Set(check.map((block) => block.key)).size !== expected.size) return {
8350
+ ok: false,
8351
+ reason: "写入前的自检没过:行的 id 会重复或丢行"
8352
+ };
8353
+ return {
8354
+ ok: true,
8355
+ text,
8356
+ written,
8357
+ skipped
8358
+ };
8359
+ }
8360
+ function readPatchBlocks(path) {
8361
+ try {
8362
+ const text = readFileSync(path, "utf8");
8363
+ return {
8364
+ ok: true,
8365
+ text,
8366
+ blocks: patchBlocks(text)
8367
+ };
8368
+ } catch (error) {
8369
+ return {
8370
+ ok: false,
8371
+ reason: error instanceof Error ? error.message : String(error)
8372
+ };
8373
+ }
8374
+ }
8375
+ function patchMode(path) {
8376
+ try {
8377
+ return statSync(path).mode & 511;
8378
+ } catch {
8379
+ return 420;
8380
+ }
8381
+ }
8382
+ function writePatchFile(path, text) {
8383
+ const mode = patchMode(path);
8384
+ const tmp = `${path}.wb-sync-${String(process.pid)}`;
8385
+ writeFileSync(tmp, text, { mode });
8386
+ try {
8387
+ renameSync(tmp, path);
8388
+ } catch (error) {
8389
+ try {
8390
+ unlinkSync(tmp);
8391
+ } catch {
8392
+ throw error;
8393
+ }
8394
+ throw error;
8395
+ }
8396
+ }
8397
+ function configSyncHandler(deps) {
8398
+ return (search) => ({
8399
+ ok: true,
8400
+ ...configSyncSnapshot(deps, search.get("source") ?? void 0)
8401
+ });
8402
+ }
8403
+ function textOf(value) {
8404
+ return typeof value === "string" ? value : "";
8405
+ }
8406
+ function profileDependencies(dir) {
8407
+ try {
8408
+ const manifest = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
8409
+ return new Set(Object.keys(manifest.dependencies ?? {}));
8410
+ } catch {
8411
+ return /* @__PURE__ */ new Set();
8412
+ }
8413
+ }
8414
+ function profileNames(profilesDir) {
8415
+ try {
8416
+ return readdirSync(profilesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory() || entry.isSymbolicLink()).map((entry) => entry.name).filter((name) => existsSync(join(profilesDir, name, "package.json"))).sort((a, b) => a.localeCompare(b));
8417
+ } catch {
8418
+ return [];
8419
+ }
8420
+ }
8421
+ function configSyncSnapshot(deps, asked) {
8422
+ const profilesDir = join(deps.home, "profiles");
8423
+ const others = profileNames(profilesDir).filter((name) => name !== deps.self);
8424
+ const base = {
8425
+ self: deps.self,
8426
+ home: deps.home,
8427
+ restart: true
8428
+ };
8429
+ if (deps.profileDir === null) return {
8430
+ ...base,
8431
+ source: "",
8432
+ profiles: [],
8433
+ rows: [],
8434
+ emptyProviders: false,
8435
+ reason: "拿不到当前 profile 目录"
8436
+ };
8437
+ const own = readPatchBlocks(join(deps.profileDir, "cordis.patch.yml"));
8438
+ const emptyProviders = own.ok && !own.blocks.some((block) => block.key.startsWith("id:llm-"));
8439
+ const ownBlocks = own.ok ? own.blocks : [];
8440
+ const profiles = others.map((name) => {
8441
+ const read = readPatchBlocks(join(profilesDir, name, "cordis.patch.yml"));
8442
+ return {
8443
+ name,
8444
+ readable: read.ok,
8445
+ candidates: read.ok ? syncRows(read.blocks, ownBlocks, () => []).length : 0
8446
+ };
8447
+ });
8448
+ if (others.length === 0) return {
8449
+ ...base,
8450
+ source: "",
8451
+ profiles,
8452
+ rows: [],
8453
+ emptyProviders,
8454
+ reason: "这台机器上只有当前这个 profile,没有可同步的来源"
8455
+ };
8456
+ const source = others.includes(asked ?? "") ? asked ?? "" : others.includes("web") ? "web" : others[0] ?? "";
8457
+ const sourceDir = join(profilesDir, source);
8458
+ const read = readPatchBlocks(join(sourceDir, "cordis.patch.yml"));
8459
+ if (!read.ok) return {
8460
+ ...base,
8461
+ source,
8462
+ profiles,
8463
+ rows: [],
8464
+ emptyProviders,
8465
+ reason: read.reason
8466
+ };
8467
+ if (!own.ok) return {
8468
+ ...base,
8469
+ source,
8470
+ profiles,
8471
+ rows: [],
8472
+ emptyProviders,
8473
+ reason: own.reason
8474
+ };
8475
+ const declared = profileDependencies(sourceDir);
8476
+ const modulesDir = join(deps.profileDir, "node_modules");
8477
+ const rows = syncRows(read.blocks, own.blocks, (block) => block.names.filter((name) => declared.has(name) && !existsSync(join(modulesDir, name))));
8478
+ return {
8479
+ ...base,
8480
+ source,
8481
+ profiles,
8482
+ rows,
8483
+ emptyProviders
8484
+ };
8485
+ }
8486
+ function configSyncApply(deps, payload) {
8487
+ if (deps.profileDir === null) return {
8488
+ ok: false,
8489
+ text: "拿不到当前 profile 目录,这次没写任何东西"
8490
+ };
8491
+ const body = typeof payload === "object" && payload !== null ? payload : {};
8492
+ const source = textOf(body["source"]);
8493
+ const keys = new Set((Array.isArray(body["keys"]) ? body["keys"] : []).map((item) => textOf(item)).filter((item) => item !== ""));
8494
+ const overwrite = new Set((Array.isArray(body["overwrite"]) ? body["overwrite"] : []).map((item) => textOf(item)));
8495
+ if (source === "" || keys.size === 0) return {
8496
+ ok: false,
8497
+ text: "没说清从哪个 profile 搬哪几行"
8498
+ };
8499
+ const profilesDir = join(deps.home, "profiles");
8500
+ if (!profileNames(profilesDir).includes(source)) return {
8501
+ ok: false,
8502
+ text: `没有这个 profile:${source}`
8503
+ };
8504
+ if (source === deps.self) return {
8505
+ ok: false,
8506
+ text: "来源不能是工作台自己"
8507
+ };
8508
+ const read = readPatchBlocks(join(profilesDir, source, "cordis.patch.yml"));
8509
+ if (!read.ok) return {
8510
+ ok: false,
8511
+ text: `读不到 ${source} 的配置:${read.reason}`
8512
+ };
8513
+ const own = readPatchBlocks(join(deps.profileDir, "cordis.patch.yml"));
8514
+ if (!own.ok) return {
8515
+ ok: false,
8516
+ text: `读不到工作台自己的配置:${own.reason}`
8517
+ };
8518
+ const picked = read.blocks.filter((block) => keys.has(block.key));
8519
+ if (picked.length === 0) return {
8520
+ ok: false,
8521
+ text: "选中的行在来源里已经找不到了,重新读一遍再试"
8522
+ };
8523
+ const merged = mergeBlocks(own.text, picked, overwrite);
8524
+ if (!merged.ok) return {
8525
+ ok: false,
8526
+ text: merged.reason
8527
+ };
8528
+ try {
8529
+ writePatchFile(join(deps.profileDir, "cordis.patch.yml"), merged.text);
8530
+ } catch (error) {
8531
+ return {
8532
+ ok: false,
8533
+ text: `写不进去:${error instanceof Error ? error.message : String(error)}`
8534
+ };
8535
+ }
8536
+ return {
8537
+ ok: true,
8538
+ written: merged.written,
8539
+ skipped: merged.skipped,
8540
+ restart: true
8541
+ };
8542
+ }
8543
+ //#endregion
8221
8544
  //#region src/shared/api/host/tool-scope.ts
8222
8545
  function agentOf(value) {
8223
8546
  return typeof value === "object" && value !== null ? value : void 0;
@@ -8347,6 +8670,12 @@ function apply(ctx) {
8347
8670
  const renderers = /* @__PURE__ */ new Map();
8348
8671
  const shell = ctx.get("shell");
8349
8672
  const profileDir = profileDirOf(ctx);
8673
+ const configSync = {
8674
+ home: dshHome(),
8675
+ self: profileDir === null ? "" : basename(profileDir),
8676
+ profileDir
8677
+ };
8678
+ routes[CONFIG_SYNC_ROUTE] = configSyncHandler(configSync);
8350
8679
  const units = createUnitRegistry({
8351
8680
  declarationOf: createDeclarationLookup(profileDir),
8352
8681
  logger: ctx.logger
@@ -8684,6 +9013,10 @@ function apply(ctx) {
8684
9013
  return () => off?.();
8685
9014
  }, `${PLUGIN_ID}: 对话引用注入`);
8686
9015
  ctx.effect(() => registerFactStream(ctx, facts), `${PLUGIN_ID}: 变更流`);
9016
+ ctx.effect(() => registerActions(ctx, [{
9017
+ resource: CONFIG_SYNC_RESOURCE,
9018
+ handle: (payload) => configSyncApply(configSync, payload)
9019
+ }]), `${PLUGIN_ID}: 配置同步那一跳(把来源 profile 的行写进工作台自己的 patch)`);
8687
9020
  ctx.effect(() => registerPanelState(ctx, panelState), `${PLUGIN_ID}: 面板运行态上报(浏览器半侧报自己的取数与渲染收尾)`);
8688
9021
  ctx.effect(() => registerApi(ctx, routes), `${PLUGIN_ID}: HTTP 投影`);
8689
9022
  }