@dsh-workbench/core 0.1.14 → 0.1.15

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 (42) hide show
  1. package/bin/dsh-workbench +16 -0
  2. package/client.js +163 -89
  3. package/cordis.patch.yml +18 -0
  4. package/dist/client/client.body.js +163 -89
  5. package/dist/client/index.js +163 -89
  6. package/dist/host/index.js +1300 -93
  7. package/dist/types/app/__tests__/workbench-styles.test.d.ts +1 -0
  8. package/dist/types/app/client.d.ts +39 -15
  9. package/dist/types/app/layouts/rightbar/board-columns.d.ts +8 -0
  10. package/dist/types/app/layouts/rightbar/board-columns.test.d.ts +1 -0
  11. package/dist/types/app/layouts/rightbar/market-rows.d.ts +2 -0
  12. package/dist/types/app/layouts/workbench-styles.d.ts +2 -0
  13. package/dist/types/modules/panels/__tests__/diy-harness.d.ts +51 -0
  14. package/dist/types/modules/panels/__tests__/diy-tools-status.test.d.ts +1 -0
  15. package/dist/types/modules/panels/__tests__/diy-tools.test.d.ts +1 -0
  16. package/dist/types/modules/panels/model/diy-tools.d.ts +4 -0
  17. package/dist/types/modules/panels/unit.d.ts +1 -0
  18. package/dist/types/shared/api/bundle-install.d.ts +1 -0
  19. package/dist/types/shared/api/host/diy-facts.d.ts +9 -0
  20. package/dist/types/shared/api/host/diy-facts.test.d.ts +1 -0
  21. package/dist/types/shared/api/host/diy-recon-fs.d.ts +3 -0
  22. package/dist/types/shared/api/host/diy-recon-fs.test.d.ts +1 -0
  23. package/dist/types/shared/api/host/diy-recon.d.ts +39 -0
  24. package/dist/types/shared/api/host/diy-recon.test.d.ts +1 -0
  25. package/dist/types/shared/api/host/diy-workshop.d.ts +12 -0
  26. package/dist/types/shared/api/host/diy-workshop.test.d.ts +1 -0
  27. package/dist/types/shared/api/host/installed-module.d.ts +2 -0
  28. package/dist/types/shared/api/host/opencli-command.d.ts +10 -0
  29. package/dist/types/shared/api/host/opencli-command.test.d.ts +1 -0
  30. package/package.json +2 -3
  31. package/scripts/seed-profile.sh +4 -113
  32. package/skills/workbench-module-diy/BUILD.md +36 -66
  33. package/skills/workbench-module-diy/DATASOURCE.md +80 -12
  34. package/skills/workbench-module-diy/DESIGN.md +27 -2
  35. package/skills/workbench-module-diy/LINKAGE.md +49 -19
  36. package/skills/workbench-module-diy/PANEL.md +78 -4
  37. package/skills/workbench-module-diy/REVIEW.md +6 -5
  38. package/skills/workbench-module-diy/SKILL.md +32 -12
  39. package/skills/workbench-module-diy/STEPS.md +4 -0
  40. package/skills/workbench-module-diy/TOOLING.md +47 -0
  41. package/skills/workbench-module-diy/TOOLS.md +147 -5
  42. package/skills/workbench-module-diy/WIRING.md +88 -0
@@ -1,6 +1,6 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
- import fs, { readFileSync } from "node:fs";
3
- import path, { join } from "node:path";
2
+ import fs, { existsSync, readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
3
+ import path, { join, resolve, sep } from "node:path";
4
4
  //#region ../shared/dist/config/constants.js
5
5
  var PLUGIN_ID = "@dsh-workbench/core";
6
6
  var API_PREFIX = "/workbench/api";
@@ -96,6 +96,516 @@ function workspacesRoot() {
96
96
  return `${workbenchRoot()}/workspaces`;
97
97
  }
98
98
  //#endregion
99
+ //#region ../shared/dist/host/opencli-window.js
100
+ var OPENCLI_WINDOW_ENV = "OPENCLI_WINDOW=background";
101
+ function backgroundPrefix() {
102
+ return [quoteShellArg("env"), quoteShellArg(OPENCLI_WINDOW_ENV)];
103
+ }
104
+ function backgroundLine(command, argv) {
105
+ return [
106
+ ...backgroundPrefix(),
107
+ quoteShellArg(command),
108
+ ...argv.map(quoteShellArg)
109
+ ].join(" ");
110
+ }
111
+ //#endregion
112
+ //#region ../shared/dist/host/shell.js
113
+ var DEFAULT_MAX_BYTES = 8388608;
114
+ async function runShell(shell, command, options = {}) {
115
+ const { workdir, timeoutMs = GIT_TIMEOUT_MS, signal } = options;
116
+ const spec = shell.resolve({
117
+ command,
118
+ ...workdir === void 0 ? {} : { workdir },
119
+ timeoutMs,
120
+ onExpiry: "kill",
121
+ stdoutMaxBytes: DEFAULT_MAX_BYTES,
122
+ ...signal === void 0 ? {} : { signal }
123
+ });
124
+ const result = await (await shell.execute(spec)).result();
125
+ return {
126
+ exitCode: result.exitCode,
127
+ timedOut: result.timedOut === true,
128
+ aborted: result.aborted === true,
129
+ stdout: result.stdout.text,
130
+ stderr: result.stderr.text,
131
+ truncated: result.stdout.truncated || result.stderr.truncated
132
+ };
133
+ }
134
+ var DEFAULT_TIMEOUT_MS$1 = 25e3;
135
+ var DEFAULT_RETRY_MS = 1200;
136
+ var DEFAULT_SETTLE_MS$1 = 900;
137
+ var AUTH_CODES = /* @__PURE__ */ new Set([
138
+ "NOT_LOGGED_IN",
139
+ "NOT_LOGIN",
140
+ "AUTH_FAILED",
141
+ "AUTH_REQUIRED",
142
+ "UNAUTHENTICATED",
143
+ "UNAUTHORIZED",
144
+ "TOKEN_EXPIRED"
145
+ ]);
146
+ var PERMISSION_CODES = /* @__PURE__ */ new Set([
147
+ "FORBIDDEN",
148
+ "PERMISSION_DENIED",
149
+ "NO_PERMISSION",
150
+ "EACCES"
151
+ ]);
152
+ function asRecord$7(value) {
153
+ return typeof value === "object" && value !== null ? value : void 0;
154
+ }
155
+ function textOf$3(value) {
156
+ return typeof value === "string" ? value.trim() : "";
157
+ }
158
+ function codeOf(page) {
159
+ const record = asRecord$7(page);
160
+ return record === void 0 ? "" : textOf$3(record["code"]);
161
+ }
162
+ function messageOf$1(page) {
163
+ const record = asRecord$7(page);
164
+ return record === void 0 ? "" : textOf$3(record["message"]);
165
+ }
166
+ function hintOf$1(page) {
167
+ const record = asRecord$7(page);
168
+ const hint = record === void 0 ? "" : textOf$3(record["hint"]);
169
+ return hint === "" ? void 0 : hint;
170
+ }
171
+ function pageFailed(page) {
172
+ const record = asRecord$7(page);
173
+ return record !== void 0 && record["ok"] === false;
174
+ }
175
+ function blockerFor(code) {
176
+ const normalized = code.trim().toUpperCase().replace(/-/g, "_");
177
+ if (normalized === "") return void 0;
178
+ const matches = (set) => [...set].some((one) => normalized === one || normalized.endsWith(`_${one}`));
179
+ if (matches(PERMISSION_CODES)) return "permission-denied";
180
+ if (matches(AUTH_CODES) || normalized.includes("UNAUTH")) return "auth-required";
181
+ }
182
+ function sleep$1(ms) {
183
+ return new Promise((resolve) => setTimeout(resolve, ms));
184
+ }
185
+ async function shellRun(shell, command, argv, timeoutMs) {
186
+ const result = await runShell(shell, backgroundLine(command, argv), { timeoutMs });
187
+ if (result.timedOut) return {
188
+ kind: "timeout",
189
+ detail: `没在 ${Math.round(timeoutMs / 1e3)} 秒内返回`,
190
+ stderr: result.stderr
191
+ };
192
+ if (result.exitCode !== 0) return {
193
+ kind: "failed",
194
+ detail: tailLines(result.stderr || result.stdout, 4),
195
+ stderr: result.stderr
196
+ };
197
+ return {
198
+ kind: "ok",
199
+ stdout: result.stdout
200
+ };
201
+ }
202
+ async function evalOnce(shell, command, session, script, timeoutMs, retryMs) {
203
+ const run = async () => {
204
+ let attempt;
205
+ try {
206
+ attempt = await shellRun(shell, command, [
207
+ "browser",
208
+ session,
209
+ "eval",
210
+ script,
211
+ "--window",
212
+ "background"
213
+ ], timeoutMs);
214
+ } catch (error) {
215
+ return {
216
+ kind: "failure",
217
+ result: {
218
+ ok: false,
219
+ code: "cli-exec-failed",
220
+ message: "读不了:opencli 起不来",
221
+ hint: "确认这台机器上 opencli 能跑(工作台会自己把它挂好)",
222
+ detail: error instanceof Error ? error.message : String(error)
223
+ }
224
+ };
225
+ }
226
+ if (attempt.kind === "timeout") return {
227
+ kind: "failure",
228
+ result: {
229
+ ok: false,
230
+ code: "cli-timeout",
231
+ message: "这次没读到(等超时了)",
232
+ hint: "稍后重试",
233
+ detail: attempt.detail
234
+ }
235
+ };
236
+ if (attempt.kind === "failed") {
237
+ const normalized = attempt.stderr.trim().toUpperCase().replace(/-/g, "_");
238
+ const forbidden = [...PERMISSION_CODES].some((one) => normalized.includes(one));
239
+ return {
240
+ kind: "failure",
241
+ result: {
242
+ ok: false,
243
+ code: forbidden ? "permission-denied" : "cli-exec-failed",
244
+ message: forbidden ? "没有权限在这台机器上读" : "读不了(命令失败)",
245
+ hint: forbidden ? "换一个有权限的账号,或让用户开权限" : "把下面这段原始输出看一眼再重试",
246
+ detail: attempt.detail
247
+ }
248
+ };
249
+ }
250
+ const value = parseJsonLoose(attempt.stdout);
251
+ if (value === void 0) return {
252
+ kind: "failure",
253
+ result: {
254
+ ok: false,
255
+ code: "cli-exec-failed",
256
+ message: "读到了、但不是 JSON",
257
+ hint: "脚本必须回 JSON",
258
+ detail: tailLines(attempt.stdout, 4)
259
+ }
260
+ };
261
+ return {
262
+ kind: "value",
263
+ value
264
+ };
265
+ };
266
+ const first = await run();
267
+ if (first.kind === "value") return first;
268
+ if (first.result.code !== "cli-exec-failed") return first;
269
+ await sleep$1(retryMs);
270
+ return run();
271
+ }
272
+ async function openOnce$1(shell, command, session, url, timeoutMs) {
273
+ let attempt;
274
+ try {
275
+ attempt = await shellRun(shell, command, [
276
+ "browser",
277
+ session,
278
+ "open",
279
+ url,
280
+ "--window",
281
+ "background"
282
+ ], timeoutMs);
283
+ } catch (error) {
284
+ return {
285
+ ok: false,
286
+ code: "cli-exec-failed",
287
+ message: "打不开那个页面",
288
+ hint: "确认 opencli 能跑起来",
289
+ detail: error instanceof Error ? error.message : String(error),
290
+ helpUrl: url
291
+ };
292
+ }
293
+ if (attempt.kind === "ok") return void 0;
294
+ return {
295
+ ok: false,
296
+ code: attempt.kind === "timeout" ? "cli-timeout" : "cli-exec-failed",
297
+ message: "打不开那个页面",
298
+ hint: "在浏览器里手动打开一次,然后重试",
299
+ detail: attempt.detail,
300
+ helpUrl: url
301
+ };
302
+ }
303
+ async function browserRead(options) {
304
+ const { shell, script, url, loginUrl, open = true } = options;
305
+ const command = options.command ?? "opencli";
306
+ const session = options.session ?? "workbench";
307
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS$1;
308
+ const retryMs = options.retryMs ?? DEFAULT_RETRY_MS;
309
+ const settleMs = options.settleMs ?? DEFAULT_SETTLE_MS$1;
310
+ const at = Date.now();
311
+ const first = await evalOnce(shell, command, session, script, timeoutMs, retryMs);
312
+ if (first.kind === "failure") return {
313
+ ...first.result,
314
+ at
315
+ };
316
+ let page = first.value;
317
+ if (codeOf(page) === "not-on-site") {
318
+ if (!open || url === void 0) return {
319
+ ok: false,
320
+ code: "cli-exec-failed",
321
+ at,
322
+ message: "没停在那套系统上",
323
+ hint: "先在那个浏览器里把它打开,再读一次",
324
+ detail: JSON.stringify(page),
325
+ ...url === void 0 ? {} : { helpUrl: url }
326
+ };
327
+ const opened = await openOnce$1(shell, command, session, url, timeoutMs);
328
+ if (opened !== void 0) return {
329
+ ...opened,
330
+ at
331
+ };
332
+ await sleep$1(settleMs);
333
+ const second = await evalOnce(shell, command, session, script, timeoutMs, retryMs);
334
+ if (second.kind === "failure") return {
335
+ ...second.result,
336
+ at
337
+ };
338
+ page = second.value;
339
+ if (codeOf(page) === "not-on-site") return {
340
+ ok: false,
341
+ code: "cli-exec-failed",
342
+ at,
343
+ message: "打开了页面、又读了一遍,仍然不在那套系统上",
344
+ hint: "确认这个地址就是那套系统(或者它跳到了别的域)",
345
+ detail: JSON.stringify(page),
346
+ helpUrl: url
347
+ };
348
+ }
349
+ if (!pageFailed(page)) return {
350
+ ok: true,
351
+ code: "ok",
352
+ at,
353
+ value: page,
354
+ message: "读到了"
355
+ };
356
+ const pageCode = codeOf(page) || "read-failed";
357
+ const message = messageOf$1(page) || `读不到(${pageCode})`;
358
+ const blocker = blockerFor(pageCode);
359
+ if (blocker === "auth-required") return {
360
+ ok: false,
361
+ code: blocker,
362
+ at,
363
+ pageCode,
364
+ message,
365
+ hint: "去登录一次,再重试",
366
+ detail: JSON.stringify(page),
367
+ ...loginUrl === void 0 && url === void 0 ? {} : { helpUrl: loginUrl ?? url }
368
+ };
369
+ if (blocker === "permission-denied") return {
370
+ ok: false,
371
+ code: blocker,
372
+ at,
373
+ pageCode,
374
+ message,
375
+ hint: "这个账号没有权限",
376
+ detail: JSON.stringify(page),
377
+ ...loginUrl === void 0 && url === void 0 ? {} : { helpUrl: loginUrl ?? url }
378
+ };
379
+ return {
380
+ ok: false,
381
+ code: pageCode,
382
+ at,
383
+ pageCode,
384
+ message,
385
+ detail: JSON.stringify(page),
386
+ ...hintOf$1(page) === void 0 ? {} : { hint: hintOf$1(page) }
387
+ };
388
+ }
389
+ //#endregion
390
+ //#region ../shared/dist/host/browser-find.js
391
+ var DEFAULT_TIMEOUT_MS = 25e3;
392
+ var DEFAULT_SETTLE_MS = 2500;
393
+ var URL_TIMEOUT_MS = 8e3;
394
+ var DEFAULT_LIMIT = 50;
395
+ var DEFAULT_TEXT_MAX = 200;
396
+ function asRecord$6(value) {
397
+ return typeof value === "object" && value !== null ? value : void 0;
398
+ }
399
+ function textOf$2(value) {
400
+ return typeof value === "string" ? value.trim() : "";
401
+ }
402
+ function entriesOf(value) {
403
+ const record = asRecord$6(value);
404
+ const raw = record === void 0 ? void 0 : record["entries"];
405
+ if (!Array.isArray(raw)) return [];
406
+ const entries = [];
407
+ for (const one of raw) {
408
+ const item = asRecord$6(one);
409
+ if (item === void 0) continue;
410
+ const nth = typeof item["nth"] === "number" ? item["nth"] : entries.length;
411
+ entries.push({
412
+ text: textOf$2(item["text"]),
413
+ tag: textOf$2(item["tag"]),
414
+ nth
415
+ });
416
+ }
417
+ return entries;
418
+ }
419
+ function reasonOf(stdout, stderr) {
420
+ const parsed = asRecord$6(parseJsonLoose(stdout));
421
+ const error = parsed === void 0 ? void 0 : asRecord$6(parsed["error"]);
422
+ const message = error === void 0 ? "" : textOf$2(error["message"]);
423
+ if (message !== "") return message;
424
+ const lines = (stderr.trim() === "" ? stdout : stderr).split("\n").map((one) => one.trim()).filter((one) => one !== "");
425
+ return (lines[lines.length - 1] ?? "").slice(-200);
426
+ }
427
+ function hintOf(stdout) {
428
+ const parsed = asRecord$6(parseJsonLoose(stdout));
429
+ const error = parsed === void 0 ? void 0 : asRecord$6(parsed["error"]);
430
+ const hint = error === void 0 ? "" : textOf$2(error["hint"]);
431
+ return hint === "" ? void 0 : hint;
432
+ }
433
+ function sameOrigin(left, right) {
434
+ try {
435
+ return new URL(left).origin === new URL(right).origin;
436
+ } catch {
437
+ return false;
438
+ }
439
+ }
440
+ function urlOf(stdout) {
441
+ const bare = (stdout.split("\n").map((one) => one.trim()).filter((one) => one !== "")[0] ?? "").replace(/^url:\s*/i, "").trim().replace(/^["']+|["']+$/g, "");
442
+ if (bare.startsWith("{")) {
443
+ const parsed = asRecord$6(parseJsonLoose(bare));
444
+ const url = parsed === void 0 ? "" : textOf$2(parsed["url"]);
445
+ if (url !== "") return url;
446
+ }
447
+ return bare;
448
+ }
449
+ async function currentUrl(shell, command, session) {
450
+ const line = backgroundLine(command, [
451
+ "browser",
452
+ session,
453
+ "get",
454
+ "url",
455
+ "--window",
456
+ "background"
457
+ ]);
458
+ try {
459
+ const result = await runShell(shell, line, { timeoutMs: URL_TIMEOUT_MS });
460
+ if (result.exitCode !== 0) return "";
461
+ return urlOf(result.stdout);
462
+ } catch {
463
+ return "";
464
+ }
465
+ }
466
+ function sleep(ms) {
467
+ return new Promise((resolve) => setTimeout(resolve, ms));
468
+ }
469
+ async function findOnce(shell, command, session, css, limit, textMax, timeoutMs) {
470
+ const result = await runShell(shell, backgroundLine(command, [
471
+ "browser",
472
+ session,
473
+ "find",
474
+ "--css",
475
+ css,
476
+ "--limit",
477
+ String(limit),
478
+ "--text-max",
479
+ String(textMax),
480
+ "--window",
481
+ "background"
482
+ ]), { timeoutMs });
483
+ if (result.timedOut) return {
484
+ kind: "timeout",
485
+ detail: `没在 ${Math.round(timeoutMs / 1e3)} 秒内返回`
486
+ };
487
+ if (result.exitCode !== 0) return {
488
+ kind: "failed",
489
+ detail: reasonOf(result.stdout, result.stderr),
490
+ ...hintOf(result.stdout) === void 0 ? {} : { hint: hintOf(result.stdout) }
491
+ };
492
+ return {
493
+ kind: "ok",
494
+ entries: entriesOf(parseJsonLoose(result.stdout)),
495
+ stdout: result.stdout
496
+ };
497
+ }
498
+ async function openOnce(shell, command, session, url, timeoutMs) {
499
+ const line = backgroundLine(command, [
500
+ "browser",
501
+ session,
502
+ "open",
503
+ url,
504
+ "--window",
505
+ "background"
506
+ ]);
507
+ let result;
508
+ try {
509
+ result = await runShell(shell, line, { timeoutMs });
510
+ } catch (error) {
511
+ return error instanceof Error ? error.message : String(error);
512
+ }
513
+ if (result.timedOut) return `没在 ${Math.round(timeoutMs / 1e3)} 秒内打开`;
514
+ if (result.exitCode !== 0) {
515
+ const reason = reasonOf(result.stdout, result.stderr);
516
+ return reason === "" ? `opencli 打开失败(退出码 ${String(result.exitCode ?? "?")})` : reason;
517
+ }
518
+ }
519
+ async function browserFind(options) {
520
+ const { shell, css, url, open = true } = options;
521
+ const command = options.command ?? "opencli";
522
+ const session = options.session ?? "workbench";
523
+ const limit = options.limit ?? DEFAULT_LIMIT;
524
+ const textMax = options.textMax ?? DEFAULT_TEXT_MAX;
525
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
526
+ const settleMs = options.settleMs ?? DEFAULT_SETTLE_MS;
527
+ const at = Date.now();
528
+ const find = async () => {
529
+ try {
530
+ return await findOnce(shell, command, session, css, limit, textMax, timeoutMs);
531
+ } catch (error) {
532
+ return {
533
+ kind: "failed",
534
+ detail: error instanceof Error ? error.message : String(error),
535
+ hint: "确认这台机器上 opencli 能跑(工作台会自己把它挂好)"
536
+ };
537
+ }
538
+ };
539
+ const canMove = open && url !== void 0;
540
+ if (canMove) {
541
+ const here = await currentUrl(shell, command, session);
542
+ if (here !== "" && !sameOrigin(here, url)) {
543
+ const opened = await openOnce(shell, command, session, url, timeoutMs);
544
+ if (opened !== void 0) return {
545
+ ok: false,
546
+ code: "cli-exec-failed",
547
+ at,
548
+ entries: [],
549
+ count: 0,
550
+ message: "打不开那个页面",
551
+ hint: "在浏览器里手动打开一次,然后重试",
552
+ detail: opened,
553
+ helpUrl: url
554
+ };
555
+ await sleep(settleMs);
556
+ }
557
+ }
558
+ let outcome = await find();
559
+ if (outcome === void 0) return {
560
+ ok: false,
561
+ code: "cli-exec-failed",
562
+ at,
563
+ entries: [],
564
+ count: 0,
565
+ message: "没找到元素"
566
+ };
567
+ if (outcome.kind === "failed" && canMove) {
568
+ if (await openOnce(shell, command, session, url, timeoutMs) === void 0) {
569
+ await sleep(settleMs);
570
+ const second = await find();
571
+ if (second !== void 0) outcome = second;
572
+ }
573
+ }
574
+ if (outcome.kind === "timeout") return {
575
+ ok: false,
576
+ code: "cli-timeout",
577
+ at,
578
+ entries: [],
579
+ count: 0,
580
+ message: "这次没找到元素(等超时了)",
581
+ hint: "稍后重试",
582
+ detail: outcome.detail,
583
+ ...url === void 0 ? {} : { helpUrl: url }
584
+ };
585
+ if (outcome.kind === "failed") {
586
+ const detail = outcome.detail;
587
+ return {
588
+ ok: false,
589
+ code: "cli-exec-failed",
590
+ at,
591
+ entries: [],
592
+ count: 0,
593
+ message: detail === "" ? "没找到元素(opencli 命令失败)" : `没找到元素:${detail}`,
594
+ ...outcome.hint === void 0 ? {} : { hint: outcome.hint },
595
+ ...detail === "" ? {} : { detail },
596
+ ...url === void 0 ? {} : { helpUrl: url }
597
+ };
598
+ }
599
+ return {
600
+ ok: true,
601
+ code: "ok",
602
+ at,
603
+ entries: outcome.entries,
604
+ count: outcome.entries.length,
605
+ message: outcome.entries.length === 0 ? "页面上没有匹配的元素" : `找到 ${String(outcome.entries.length)} 个元素`
606
+ };
607
+ }
608
+ //#endregion
99
609
  //#region ../shared/dist/host/facts.js
100
610
  function createFactLog(options = {}) {
101
611
  const epoch = options.epoch ?? `e${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
@@ -145,29 +655,6 @@ function wroteNow(result) {
145
655
  return typeof wrote === "boolean" ? wrote : void 0;
146
656
  }
147
657
  //#endregion
148
- //#region ../shared/dist/host/shell.js
149
- var DEFAULT_MAX_BYTES = 8388608;
150
- async function runShell(shell, command, options = {}) {
151
- const { workdir, timeoutMs = GIT_TIMEOUT_MS, signal } = options;
152
- const spec = shell.resolve({
153
- command,
154
- ...workdir === void 0 ? {} : { workdir },
155
- timeoutMs,
156
- onExpiry: "kill",
157
- stdoutMaxBytes: DEFAULT_MAX_BYTES,
158
- ...signal === void 0 ? {} : { signal }
159
- });
160
- const result = await (await shell.execute(spec)).result();
161
- return {
162
- exitCode: result.exitCode,
163
- timedOut: result.timedOut === true,
164
- aborted: result.aborted === true,
165
- stdout: result.stdout.text,
166
- stderr: result.stderr.text,
167
- truncated: result.stdout.truncated || result.stderr.truncated
168
- };
169
- }
170
- //#endregion
171
658
  //#region ../shared/dist/host/provision.js
172
659
  var PROVISION_ACTIONS = [
173
660
  "install",
@@ -197,10 +684,10 @@ var TARGET_CODE = {
197
684
  name: "workbench/no-name",
198
685
  requestId: "workbench/no-request-id"
199
686
  };
200
- function asRecord$3(value) {
687
+ function asRecord$5(value) {
201
688
  return typeof value === "object" && value !== null ? value : {};
202
689
  }
203
- function stringsOf$1(value) {
690
+ function stringsOf$2(value) {
204
691
  if (!Array.isArray(value)) return void 0;
205
692
  const items = value.filter((item) => typeof item === "string" && item.trim() !== "");
206
693
  return items.length === 0 ? void 0 : items;
@@ -228,7 +715,7 @@ function requestOf$1(action, value) {
228
715
  };
229
716
  }
230
717
  function parseProvisionBody(body) {
231
- const raw = asRecord$3(body);
718
+ const raw = asRecord$5(body);
232
719
  const action = raw.action;
233
720
  if (typeof action !== "string" || !PROVISION_ACTIONS.includes(action)) return {
234
721
  ok: false,
@@ -248,7 +735,7 @@ function parseProvisionBody(body) {
248
735
  request: requestOf$1(wanted, value.trim())
249
736
  };
250
737
  const registry = typeof raw.registry === "string" && raw.registry.trim() !== "" ? raw.registry.trim() : void 0;
251
- const approvedBuilds = stringsOf$1(raw.approvedBuilds);
738
+ const approvedBuilds = stringsOf$2(raw.approvedBuilds);
252
739
  const requestId = typeof raw.requestId === "string" && raw.requestId.trim() !== "" ? raw.requestId.trim() : void 0;
253
740
  return {
254
741
  ok: true,
@@ -281,6 +768,7 @@ var WorkbenchDataError = class extends Error {
281
768
  };
282
769
  function opencliCommandLine(command, argv, session = "persistent") {
283
770
  return [
771
+ ...backgroundPrefix(),
284
772
  quoteShellArg(command),
285
773
  ...argv.map(quoteShellArg),
286
774
  ...session === "persistent" ? SITE_SESSION_ARGS : [],
@@ -578,7 +1066,7 @@ function capabilityHandler(probe) {
578
1066
  };
579
1067
  };
580
1068
  }
581
- function healthHandler(ctx) {
1069
+ function healthHandler(ctx, opencli) {
582
1070
  return () => {
583
1071
  const dir = ctx.get("profileContext")?.dir;
584
1072
  return {
@@ -587,7 +1075,8 @@ function healthHandler(ctx) {
587
1075
  profile: dir === void 0 ? null : dir.split("/").pop(),
588
1076
  profileDir: dir ?? null,
589
1077
  pid: process.pid,
590
- now: (/* @__PURE__ */ new Date()).toISOString()
1078
+ now: (/* @__PURE__ */ new Date()).toISOString(),
1079
+ ...opencli === void 0 ? {} : { opencli }
591
1080
  };
592
1081
  };
593
1082
  }
@@ -778,6 +1267,14 @@ function toolchainOpencliDir() {
778
1267
  function vendorPrefix() {
779
1268
  return `${isolatedRoot()}/plugins-npm`;
780
1269
  }
1270
+ function vendorPluginsDir() {
1271
+ return `${vendorPrefix()}/node_modules`;
1272
+ }
1273
+ var OPENCLI_CATALOG_ENV = "DSH_WB_OPENCLI_CATALOG";
1274
+ function cliCatalogPath() {
1275
+ const override = process.env[OPENCLI_CATALOG_ENV]?.trim();
1276
+ return override === void 0 || override === "" ? `${toolchainOpencliDir()}/cli-manifest.json` : override;
1277
+ }
781
1278
  function vendorPeerLink() {
782
1279
  const base = `${vendorPrefix()}/node_modules`;
783
1280
  const [scope] = OPENCLI_NPM_PACKAGE.split("/");
@@ -1118,10 +1615,10 @@ var CANCELLATIONS = [
1118
1615
  "too-late",
1119
1616
  "not-running"
1120
1617
  ];
1121
- function asRecord$2(value) {
1618
+ function asRecord$4(value) {
1122
1619
  return typeof value === "object" && value !== null ? value : void 0;
1123
1620
  }
1124
- function asText$2(value) {
1621
+ function asText$4(value) {
1125
1622
  return typeof value === "string" ? value : "";
1126
1623
  }
1127
1624
  function pick(allowed, value) {
@@ -1135,7 +1632,7 @@ function asTextArray(value) {
1135
1632
  return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
1136
1633
  }
1137
1634
  function asRegistries(raw) {
1138
- const record = asRecord$2(raw);
1635
+ const record = asRecord$4(raw);
1139
1636
  if (record === void 0) return null;
1140
1637
  const registry = asRegistry(record.registry);
1141
1638
  if (registry === void 0) return null;
@@ -1146,35 +1643,35 @@ function asRegistries(raw) {
1146
1643
  };
1147
1644
  }
1148
1645
  function asPackageResult(raw) {
1149
- const record = asRecord$2(raw);
1646
+ const record = asRecord$4(raw);
1150
1647
  if (record === void 0) return void 0;
1151
1648
  const kind = pick(FAILURE_KINDS, record.kind);
1152
1649
  return {
1153
1650
  exitCode: typeof record.exitCode === "number" ? record.exitCode : -1,
1154
- output: asText$2(record.output),
1651
+ output: asText$4(record.output),
1155
1652
  truncated: record.truncated === true,
1156
- logPath: asText$2(record.logPath),
1653
+ logPath: asText$4(record.logPath),
1157
1654
  ...kind === void 0 ? {} : { kind }
1158
1655
  };
1159
1656
  }
1160
1657
  function asChange(raw) {
1161
- const record = asRecord$2(raw);
1658
+ const record = asRecord$4(raw);
1162
1659
  if (record === void 0) return null;
1163
1660
  const application = pick(APPLICATIONS, record.application);
1164
1661
  const stage = pick(STAGES, record.stage);
1165
1662
  if (application === void 0 || stage === void 0) return null;
1166
- const error = asRecord$2(record.error);
1663
+ const error = asRecord$4(record.error);
1167
1664
  const packageResult = asPackageResult(record.packageResult);
1168
1665
  const pendingBuilds = asTextArray(record.pendingBuilds);
1169
1666
  const failedAt = pick(["registry", "spec-host"], record.failedAt);
1170
- const bundle = asText$2(record.bundle);
1171
- const errorCode = error === void 0 ? "" : asText$2(error.code);
1172
- const errorDetail = error === void 0 ? "" : asText$2(error.diagnostic);
1667
+ const bundle = asText$4(record.bundle);
1668
+ const errorCode = error === void 0 ? "" : asText$4(error.code);
1669
+ const errorDetail = error === void 0 ? "" : asText$4(error.diagnostic);
1173
1670
  return {
1174
1671
  application,
1175
1672
  stage,
1176
1673
  changed: record.changed === true,
1177
- target: asText$2(record.target),
1674
+ target: asText$4(record.target),
1178
1675
  ...bundle === "" ? {} : { bundle },
1179
1676
  ...errorCode === "" ? {} : { errorCode },
1180
1677
  ...errorDetail === "" ? {} : { errorDetail },
@@ -1184,16 +1681,16 @@ function asChange(raw) {
1184
1681
  };
1185
1682
  }
1186
1683
  function asInspection(raw) {
1187
- const record = asRecord$2(raw);
1684
+ const record = asRecord$4(raw);
1188
1685
  if (record === void 0) return null;
1189
- const status = asText$2(record.status);
1686
+ const status = asText$4(record.status);
1190
1687
  if (status === "refused") {
1191
1688
  const problem = pick(PROBLEMS, record.problem) ?? "unknown";
1192
1689
  const registries = Array.isArray(record.registries) ? record.registries.map(asRegistry).filter((item) => item !== void 0) : [];
1193
1690
  return {
1194
1691
  status: "refused",
1195
1692
  problem,
1196
- reason: asText$2(record.reason),
1693
+ reason: asText$4(record.reason),
1197
1694
  ...registries.length === 0 ? {} : { registries }
1198
1695
  };
1199
1696
  }
@@ -1201,10 +1698,10 @@ function asInspection(raw) {
1201
1698
  const kind = pick(SPEC_KINDS, record.kind);
1202
1699
  const registry = asRegistry(record.registry);
1203
1700
  if (kind === void 0 || registry === void 0) return null;
1204
- const name = asText$2(record.name);
1205
- const version = asText$2(record.version);
1206
- const description = asText$2(record.description);
1207
- const host = asText$2(record.host);
1701
+ const name = asText$4(record.name);
1702
+ const version = asText$4(record.version);
1703
+ const description = asText$4(record.description);
1704
+ const host = asText$4(record.host);
1208
1705
  return {
1209
1706
  status: "accepted",
1210
1707
  kind,
@@ -1242,17 +1739,17 @@ function readBundleManager(ctx) {
1242
1739
  },
1243
1740
  async cancel(requestId) {
1244
1741
  if (typeof face.cancelInstall !== "function") return null;
1245
- return pick(CANCELLATIONS, (asRecord$2(await face.cancelInstall(requestId)) ?? {}).status) ?? null;
1742
+ return pick(CANCELLATIONS, (asRecord$4(await face.cancelInstall(requestId)) ?? {}).status) ?? null;
1246
1743
  }
1247
1744
  };
1248
1745
  }
1249
1746
  //#endregion
1250
1747
  //#region src/shared/api/host/installed-module.ts
1251
1748
  var PACKAGE_NAME$1 = /^(?:@[a-z0-9][a-z0-9._~-]*\/)?[a-z0-9][a-z0-9._~-]*$/;
1252
- function asRecord$1(value) {
1749
+ function asRecord$3(value) {
1253
1750
  return typeof value === "object" && value !== null ? value : void 0;
1254
1751
  }
1255
- function asText$1(value) {
1752
+ function asText$3(value) {
1256
1753
  return typeof value === "string" ? value : "";
1257
1754
  }
1258
1755
  function isPackageName(value) {
@@ -1263,7 +1760,7 @@ function profileDirOf(ctx) {
1263
1760
  const dir = typeof profile?.dir === "string" ? profile.dir.trim() : "";
1264
1761
  return dir === "" ? null : dir;
1265
1762
  }
1266
- function stringsOf(value) {
1763
+ function stringsOf$1(value) {
1267
1764
  return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
1268
1765
  }
1269
1766
  function profileModules(profileDir) {
@@ -1274,10 +1771,11 @@ function profileModules(profileDir) {
1274
1771
  } catch {
1275
1772
  return [];
1276
1773
  }
1277
- const record = asRecord$1(manifest);
1774
+ const record = asRecord$3(manifest);
1278
1775
  if (record === void 0) return [];
1279
- const bundles = stringsOf(asRecord$1(asRecord$1(record.dsh)?.profile)?.bundles);
1280
- const names = bundles.length > 0 ? bundles : Object.keys(asRecord$1(record.dependencies) ?? {});
1776
+ const bundles = stringsOf$1(asRecord$3(asRecord$3(record.dsh)?.profile)?.bundles);
1777
+ const dependencies = asRecord$3(record.dependencies) ?? {};
1778
+ const names = bundles.length > 0 ? bundles : Object.keys(dependencies);
1281
1779
  const found = [];
1282
1780
  for (const pkg of names) {
1283
1781
  const read = readInstalledModule(profileDir, pkg);
@@ -1285,7 +1783,8 @@ function profileModules(profileDir) {
1285
1783
  pkg: read.pkg,
1286
1784
  unit: read.unit,
1287
1785
  version: read.version,
1288
- ...read.registry === void 0 ? {} : { registry: read.registry }
1786
+ ...read.registry === void 0 ? {} : { registry: read.registry },
1787
+ local: asText$3(dependencies[read.pkg]).startsWith("link:")
1289
1788
  });
1290
1789
  }
1291
1790
  return found;
@@ -1323,24 +1822,24 @@ function readInstalledModule(profileDir, pkg) {
1323
1822
  pkg
1324
1823
  };
1325
1824
  }
1326
- const record = asRecord$1(manifest);
1825
+ const record = asRecord$3(manifest);
1327
1826
  if (record === void 0) return {
1328
1827
  kind: "unknown",
1329
1828
  pkg
1330
1829
  };
1331
- const declared = asRecord$1(asRecord$1(record.dsh)?.workbench);
1332
- const unit = declared === void 0 ? "" : asText$1(declared.unit);
1830
+ const declared = asRecord$3(asRecord$3(record.dsh)?.workbench);
1831
+ const unit = declared === void 0 ? "" : asText$3(declared.unit);
1333
1832
  if (unit === "") return {
1334
1833
  kind: "not-module",
1335
1834
  pkg
1336
1835
  };
1337
- const declaredRegistry = asText$1(asRecord$1(record.publishConfig)?.registry);
1836
+ const declaredRegistry = asText$3(asRecord$3(record.publishConfig)?.registry);
1338
1837
  const registry = declaredRegistry === "" ? registryFromLockfile(profileDir, pkg) : declaredRegistry;
1339
1838
  return {
1340
1839
  kind: "module",
1341
1840
  pkg,
1342
1841
  unit,
1343
- version: asText$1(record.version),
1842
+ version: asText$3(record.version),
1344
1843
  ...registry === "" ? {} : { registry }
1345
1844
  };
1346
1845
  }
@@ -1456,17 +1955,86 @@ function createRegistryNotebook(dir) {
1456
1955
  }
1457
1956
  };
1458
1957
  }
1958
+ //#endregion
1959
+ //#region src/shared/api/host/diy.ts
1960
+ var WORKSHOP_RELATIVE = "workbench/diy";
1961
+ function workshopPathOf(ctx) {
1962
+ const homePath = ctx.get("dshHomePath");
1963
+ if (typeof homePath !== "function") return null;
1964
+ try {
1965
+ return homePath(WORKSHOP_RELATIVE);
1966
+ } catch {
1967
+ return null;
1968
+ }
1969
+ }
1970
+ //#endregion
1971
+ //#region src/shared/api/host/diy-workshop.ts
1972
+ function asRecord$2(value) {
1973
+ return typeof value === "object" && value !== null ? value : void 0;
1974
+ }
1975
+ function asText$2(value) {
1976
+ return typeof value === "string" ? value.trim() : "";
1977
+ }
1978
+ function workshopPackagesDir(ctx) {
1979
+ const root = workshopPathOf(ctx);
1980
+ return root === null ? null : join(root, "packages");
1981
+ }
1982
+ function insideWorkshop(root, target) {
1983
+ const base = existsSync(root) ? realpathSync(root) : resolve(root);
1984
+ const real = existsSync(target) ? realpathSync(target) : resolve(target);
1985
+ return real === base || real.startsWith(base + sep);
1986
+ }
1987
+ function workshopModules(packagesDir, installed = []) {
1988
+ if (!existsSync(packagesDir)) return [];
1989
+ const already = new Set(installed);
1990
+ return readdirSync(packagesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory() || entry.isSymbolicLink()).flatMap((entry) => {
1991
+ const dir = join(packagesDir, entry.name);
1992
+ let manifest;
1993
+ try {
1994
+ manifest = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
1995
+ } catch {
1996
+ return [];
1997
+ }
1998
+ const record = asRecord$2(manifest);
1999
+ const declared = record === void 0 ? void 0 : asRecord$2(asRecord$2(record.dsh)?.workbench);
2000
+ const unit = declared === void 0 ? "" : asText$2(declared.unit);
2001
+ if (unit === "") return [];
2002
+ return [{
2003
+ unit,
2004
+ name: asText$2(record?.name),
2005
+ description: asText$2(record?.description),
2006
+ dir,
2007
+ installed: already.has(unit)
2008
+ }];
2009
+ }).sort((left, right) => left.unit.localeCompare(right.unit));
2010
+ }
2011
+ function inWorkshopSession(ctx, workspace) {
2012
+ const root = workshopPathOf(ctx);
2013
+ return root !== null && workspace !== null && insideWorkshop(root, workspace);
2014
+ }
2015
+ //#endregion
2016
+ //#region src/modules/panels/unit.ts
2017
+ var PANELS_TOOL = "workbench_panels";
2018
+ var MODULES_TOOL = "workbench_modules";
2019
+ var DIY_TOOL = "workbench_diy";
1459
2020
  var panelsUnit = {
1460
2021
  id: "panels",
1461
2022
  kind: "ui",
1462
2023
  card: null,
1463
- tools: [{
1464
- name: "workbench_panels",
1465
- writes: true
1466
- }, {
1467
- name: "workbench_modules",
1468
- writes: true
1469
- }],
2024
+ tools: [
2025
+ {
2026
+ name: PANELS_TOOL,
2027
+ writes: true
2028
+ },
2029
+ {
2030
+ name: MODULES_TOOL,
2031
+ writes: true
2032
+ },
2033
+ {
2034
+ name: DIY_TOOL,
2035
+ writes: true
2036
+ }
2037
+ ],
1470
2038
  host: {
1471
2039
  inject: [],
1472
2040
  optional: ["tools", "shell"]
@@ -1475,7 +2043,13 @@ var panelsUnit = {
1475
2043
  //#endregion
1476
2044
  //#region src/modules/panels/model/approval.ts
1477
2045
  var READ = { kind: "read" };
1478
- var READ_ONLY_ACTIONS = /* @__PURE__ */ new Set(["list", ""]);
2046
+ var READ_ONLY_ACTIONS = /* @__PURE__ */ new Set([
2047
+ "list",
2048
+ "status",
2049
+ "probe",
2050
+ "recon",
2051
+ ""
2052
+ ]);
1479
2053
  function actionOf$1(call) {
1480
2054
  const raw = call.arguments;
1481
2055
  if (typeof raw !== "object" || raw === null) return {};
@@ -1520,6 +2094,23 @@ var panelsApproval = (call) => {
1520
2094
  zh: `agent 想${verb}模块 ${what}(会改这台机器上的插件)。允许吗?`
1521
2095
  });
1522
2096
  }
2097
+ if (call.name === "workbench_diy") {
2098
+ if (readAction) return READ;
2099
+ if (action === "open") return askOf("workbench_diy open puts an already-installed module on this project panel", {
2100
+ en: `it puts ${what} on this project's panel (nothing new gets installed on this machine). Allow?`,
2101
+ zh: `会把${what}摆到**这个项目**的面板上(这台机器上不装新东西)。允许吗?`
2102
+ }, {
2103
+ en: `The agent wants to show the module ${what} on this project's panel. Allow?`,
2104
+ zh: `agent 想把${what}显示在**这个项目**的面板上。允许吗?`
2105
+ });
2106
+ return askOf(`workbench_diy ${action ?? "(unknown action)"} installs a module built in the workshop and opens it on this project's panel`, {
2107
+ en: `it installs ${what} on this machine and opens it on this project's panel. Allow?`,
2108
+ zh: `会把${what}装到这台机器上,并在**这个项目**的面板上打开。允许吗?`
2109
+ }, {
2110
+ en: `The agent wants to install the module ${what} built in the DIY workshop (it changes what is installed on this machine) and open it here. Allow?`,
2111
+ zh: `agent 想把 DIY 工坊里造好的${what}装上(会改这台机器上装的东西),并在**这个项目**的面板上打开。允许吗?`
2112
+ });
2113
+ }
1523
2114
  if (call.name !== "workbench_panels") return void 0;
1524
2115
  if (readAction) return READ;
1525
2116
  if (action === "hide") return askOf("workbench_panels hide removes a module from the panel", {
@@ -1552,6 +2143,628 @@ var panelsApproval = (call) => {
1552
2143
  });
1553
2144
  };
1554
2145
  //#endregion
2146
+ //#region src/shared/api/host/diy-facts.ts
2147
+ function asExported(dir) {
2148
+ try {
2149
+ const manifest = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
2150
+ const built = (typeof manifest.exports === "object" && manifest.exports !== null ? Object.values(manifest.exports) : []).filter((one) => typeof one === "string");
2151
+ if (built.length > 0) return built;
2152
+ return Object.values(manifest.dsh?.workbench?.faces ?? {}).filter((one) => typeof one === "string");
2153
+ } catch {
2154
+ return [];
2155
+ }
2156
+ }
2157
+ function mtimeOf(file) {
2158
+ try {
2159
+ return statSync(file).mtimeMs;
2160
+ } catch {
2161
+ return;
2162
+ }
2163
+ }
2164
+ function newestMtime(dir) {
2165
+ let newest;
2166
+ const walk = (at) => {
2167
+ let entries;
2168
+ try {
2169
+ entries = readdirSync(at, { withFileTypes: true });
2170
+ } catch {
2171
+ return;
2172
+ }
2173
+ for (const entry of entries) {
2174
+ const full = join(at, entry.name);
2175
+ if (entry.isDirectory()) walk(full);
2176
+ else if (entry.isFile()) {
2177
+ const stamp = mtimeOf(full);
2178
+ if (stamp !== void 0 && (newest === void 0 || stamp > newest)) newest = stamp;
2179
+ }
2180
+ }
2181
+ };
2182
+ walk(dir);
2183
+ return newest;
2184
+ }
2185
+ function moduleBuildFacts(dir) {
2186
+ const artifacts = asExported(dir).map((rel) => ({
2187
+ path: rel,
2188
+ mtime: mtimeOf(join(dir, rel))
2189
+ }));
2190
+ const present = artifacts.filter((one) => one.mtime !== void 0);
2191
+ const oldestBuilt = present.reduce((min, one) => min === void 0 || one.mtime < min ? one.mtime : min, void 0);
2192
+ const newestSource = newestMtime(join(dir, "src"));
2193
+ return {
2194
+ artifacts: artifacts.map((one) => ({
2195
+ path: one.path,
2196
+ at: one.mtime === void 0 ? null : new Date(one.mtime).toISOString()
2197
+ })),
2198
+ builtAll: present.length === artifacts.length && artifacts.length > 0,
2199
+ stale: newestSource !== void 0 && oldestBuilt !== void 0 && newestSource > oldestBuilt
2200
+ };
2201
+ }
2202
+ //#endregion
2203
+ //#region src/shared/api/host/opencli-command.ts
2204
+ var OPENCLI_PATH_FALLBACK = "opencli";
2205
+ function isolatedOpencliPathOf(ctx) {
2206
+ const homePath = ctx.get("dshHomePath");
2207
+ if (typeof homePath !== "function") return null;
2208
+ try {
2209
+ return join(homePath("workbench/opencli/bin/opencli"));
2210
+ } catch {
2211
+ return null;
2212
+ }
2213
+ }
2214
+ function opencliCommandOf(ctx) {
2215
+ const isolated = isolatedOpencliPathOf(ctx);
2216
+ const fromEnv = process.env.DSH_WORKBENCH_OPENCLI;
2217
+ if (fromEnv !== void 0 && fromEnv !== "") return {
2218
+ command: fromEnv,
2219
+ source: "env",
2220
+ isolated
2221
+ };
2222
+ if (isolated !== null && existsSync(isolated)) return {
2223
+ command: isolated,
2224
+ source: "isolated",
2225
+ isolated
2226
+ };
2227
+ return {
2228
+ command: OPENCLI_PATH_FALLBACK,
2229
+ source: "path",
2230
+ isolated
2231
+ };
2232
+ }
2233
+ //#endregion
2234
+ //#region src/shared/api/host/diy-recon.ts
2235
+ var SITE_LIMIT = 6;
2236
+ var COMMAND_LIMIT = 12;
2237
+ var SHOWN_LIMIT = 6;
2238
+ var PLUGIN_PREFIX = "opencli-plugin-";
2239
+ function text(value) {
2240
+ return typeof value === "string" ? value.trim() : "";
2241
+ }
2242
+ function stringsOf(value) {
2243
+ return Array.isArray(value) ? value.filter((one) => typeof one === "string") : [];
2244
+ }
2245
+ function manifestCommandsOf(raw) {
2246
+ let parsed;
2247
+ try {
2248
+ parsed = JSON.parse(raw);
2249
+ } catch {
2250
+ return [];
2251
+ }
2252
+ if (!Array.isArray(parsed)) return [];
2253
+ const commands = [];
2254
+ for (const one of parsed) {
2255
+ if (typeof one !== "object" || one === null) continue;
2256
+ const row = one;
2257
+ const declared = text(row["command"]);
2258
+ const site = text(row["site"]) || declared.split("/")[0] || "";
2259
+ const name = text(row["name"]) || declared.split("/")[1] || "";
2260
+ const command = declared === "" ? site === "" || name === "" ? "" : `${site}/${name}` : declared;
2261
+ if (command === "" || site === "") continue;
2262
+ const access = text(row["access"]);
2263
+ commands.push({
2264
+ command,
2265
+ site,
2266
+ name: name === "" ? command.slice(command.indexOf("/") + 1) : name,
2267
+ domain: text(row["domain"]).toLowerCase(),
2268
+ description: text(row["description"]),
2269
+ access: access === "" ? "read" : access,
2270
+ strategy: text(row["strategy"]).toLowerCase(),
2271
+ browser: row["browser"] === true,
2272
+ example: text(row["example"]),
2273
+ columns: stringsOf(row["columns"])
2274
+ });
2275
+ }
2276
+ return commands;
2277
+ }
2278
+ function pluginNamesOf(entries) {
2279
+ return entries.filter((one) => one.startsWith(PLUGIN_PREFIX)).map((one) => one.slice(15)).filter((one) => one !== "").sort();
2280
+ }
2281
+ function pluginCommandsOf(files) {
2282
+ return files.filter((one) => one.endsWith(".js")).map((one) => one.slice(0, -3)).filter((one) => one !== "index" && !one.startsWith("_")).sort();
2283
+ }
2284
+ function hostOf(url) {
2285
+ const trimmed = url.trim();
2286
+ if (trimmed === "") return "";
2287
+ try {
2288
+ return new URL(trimmed).hostname.toLowerCase().replace(/^www\./, "");
2289
+ } catch {
2290
+ return "";
2291
+ }
2292
+ }
2293
+ function keywordsOf(keyword, url) {
2294
+ const words = /* @__PURE__ */ new Set();
2295
+ const trimmed = keyword.trim().toLowerCase();
2296
+ if (trimmed !== "") words.add(trimmed);
2297
+ const host = hostOf(url);
2298
+ if (host !== "") words.add(host);
2299
+ return [...words];
2300
+ }
2301
+ function scoreOf(command, word) {
2302
+ const site = command.site.toLowerCase();
2303
+ const name = command.name.toLowerCase();
2304
+ if (word === "") return 0;
2305
+ if (site === word) return 100;
2306
+ if (site.includes(word)) return 80;
2307
+ if (command.domain !== "" && command.domain.includes(word)) return 70;
2308
+ if (site.length >= 3 && word.includes(site)) return 45;
2309
+ if (name === word) return 65;
2310
+ if (name.includes(word)) return 55;
2311
+ if (command.command.toLowerCase().includes(word)) return 50;
2312
+ if (command.description.toLowerCase().includes(word)) return 30;
2313
+ return 0;
2314
+ }
2315
+ function reconOf(commands, plugins, keywords) {
2316
+ const keyword = keywords[0] ?? "";
2317
+ const best = /* @__PURE__ */ new Map();
2318
+ for (const command of commands) {
2319
+ let score = 0;
2320
+ for (const word of keywords) score = Math.max(score, scoreOf(command, word));
2321
+ if (score === 0) continue;
2322
+ const current = best.get(command.command);
2323
+ if (current === void 0 || current.score < score) best.set(command.command, {
2324
+ command,
2325
+ score
2326
+ });
2327
+ }
2328
+ const grouped = /* @__PURE__ */ new Map();
2329
+ for (const entry of best.values()) {
2330
+ const group = grouped.get(entry.command.site) ?? {
2331
+ best: 0,
2332
+ rows: []
2333
+ };
2334
+ group.best = Math.max(group.best, entry.score);
2335
+ group.rows.push(entry);
2336
+ grouped.set(entry.command.site, group);
2337
+ }
2338
+ const base = {
2339
+ keyword,
2340
+ sites: [...grouped.entries()].sort((left, right) => right[1].best - left[1].best || left[0].localeCompare(right[0])).slice(0, SITE_LIMIT).map(([site, group]) => {
2341
+ const picked = [...group.rows].sort((left, right) => (left.command.access === "write" ? 1 : 0) - (right.command.access === "write" ? 1 : 0) || right.score - left.score || left.command.command.localeCompare(right.command.command)).slice(0, COMMAND_LIMIT).map((one) => one.command);
2342
+ const first = picked[0];
2343
+ return {
2344
+ site,
2345
+ domain: first?.domain ?? "",
2346
+ strategy: first?.strategy ?? "",
2347
+ browser: first?.browser === true,
2348
+ commands: picked
2349
+ };
2350
+ }),
2351
+ plugins: plugins.filter((plugin) => {
2352
+ const name = plugin.name.toLowerCase();
2353
+ return keywords.some((word) => word !== "" && (name.includes(word) || word.includes(name)));
2354
+ }),
2355
+ next: "",
2356
+ text: ""
2357
+ };
2358
+ const next = nextOf(base);
2359
+ const withNext = {
2360
+ ...base,
2361
+ next
2362
+ };
2363
+ return {
2364
+ ...withNext,
2365
+ text: textOf$1(withNext)
2366
+ };
2367
+ }
2368
+ function nextOf(answer) {
2369
+ const site = answer.sites[0];
2370
+ if (site !== void 0) {
2371
+ const pick = site.commands.find((one) => one.access !== "write") ?? site.commands[0];
2372
+ return `直接复用:${pick === void 0 ? `opencli ${site.site}` : pick.example === "" ? `opencli ${pick.command}` : pick.example}(这台机器上就能跑,不用装、不用点头)`;
2373
+ }
2374
+ const plugin = answer.plugins[0];
2375
+ if (plugin !== void 0) return `这台机器另装的插件 ${plugin.name} 里有匹配:opencli ${plugin.name} --help 看全部命令`;
2376
+ return "这台机器上没有现成的 —— 两条路:① 给这个站点写 adapter:opencli skills read opencli-adapter-author;② 只读一次:workbench_diy action=probe(script 读接口 / css 读 DOM)";
2377
+ }
2378
+ function textOf$1(answer) {
2379
+ const lines = [];
2380
+ if (answer.sites.length > 0) {
2381
+ lines.push(`opencli 自带这 ${String(answer.sites.length)} 组能力(不用装、不用点头):`);
2382
+ for (const site of answer.sites) {
2383
+ const where = site.domain === "" ? "" : `(${site.domain})`;
2384
+ const chrome = site.browser ? "要用你的 Chrome 登录" : "不用浏览器";
2385
+ lines.push(`· ${site.site}${where} · ${site.strategy === "" ? "—" : site.strategy} · ${chrome}`);
2386
+ for (const command of site.commands.slice(0, SHOWN_LIMIT)) {
2387
+ const what = command.description === "" ? "" : ` —— ${command.description.slice(0, 60)}`;
2388
+ lines.push(` ${command.access === "write" ? "写" : "读"} opencli ${command.command}${what}`);
2389
+ }
2390
+ const rest = site.commands.length - SHOWN_LIMIT;
2391
+ if (rest > 0) lines.push(` … 还有 ${String(rest)} 条:opencli ${site.site} --help`);
2392
+ }
2393
+ }
2394
+ if (answer.plugins.length > 0) lines.push(`这台机器另装的插件里匹配到:${answer.plugins.map((one) => `${one.name}(${String(one.commands.length)} 条命令)`).join("、")}`);
2395
+ if (answer.sites.length === 0 && answer.plugins.length === 0) lines.push(`没匹配到「${answer.keyword}」—— 换英文名或域名再试(例:bilibili、arxiv.org);实在没有就走下面那条。`);
2396
+ lines.push(answer.next);
2397
+ return lines.join("\n");
2398
+ }
2399
+ //#endregion
2400
+ //#region src/shared/api/host/diy-recon-fs.ts
2401
+ function filesOf(path) {
2402
+ try {
2403
+ return readdirSync(path);
2404
+ } catch {
2405
+ return [];
2406
+ }
2407
+ }
2408
+ function reconCatalogOf(path = cliCatalogPath()) {
2409
+ try {
2410
+ return manifestCommandsOf(readFileSync(path, "utf8"));
2411
+ } catch {
2412
+ return;
2413
+ }
2414
+ }
2415
+ function installedPluginsOf(dir = vendorPluginsDir()) {
2416
+ return pluginNamesOf(filesOf(dir)).map((name) => ({
2417
+ name,
2418
+ commands: pluginCommandsOf(filesOf(join(dir, `opencli-plugin-${name}`)))
2419
+ }));
2420
+ }
2421
+ //#endregion
2422
+ //#region src/modules/panels/model/diy-tools.ts
2423
+ var ACTIONS$1 = [
2424
+ "list",
2425
+ "status",
2426
+ "probe",
2427
+ "recon",
2428
+ "install",
2429
+ "open"
2430
+ ];
2431
+ var OUTPUT_SCHEMA$2 = {
2432
+ type: "object",
2433
+ properties: {
2434
+ text: { type: "string" },
2435
+ workshop: { type: "string" },
2436
+ modules: {
2437
+ type: "array",
2438
+ items: {
2439
+ type: "object",
2440
+ additionalProperties: true
2441
+ }
2442
+ },
2443
+ unit: { type: "string" },
2444
+ path: { type: "string" },
2445
+ install: {
2446
+ type: "object",
2447
+ additionalProperties: true
2448
+ },
2449
+ alreadyInstalled: { type: "boolean" },
2450
+ panel: {
2451
+ type: "object",
2452
+ additionalProperties: true
2453
+ }
2454
+ },
2455
+ additionalProperties: true
2456
+ };
2457
+ function asRecord$1(value) {
2458
+ return typeof value === "object" && value !== null ? value : void 0;
2459
+ }
2460
+ function asNumber(value) {
2461
+ if (typeof value === "number" && Number.isFinite(value)) return value;
2462
+ if (typeof value === "string" && value.trim() !== "") {
2463
+ const parsed = Number(value);
2464
+ if (Number.isFinite(parsed)) return parsed;
2465
+ }
2466
+ }
2467
+ function asText$1(value) {
2468
+ return typeof value === "string" ? value.trim() : "";
2469
+ }
2470
+ function plainJson(value) {
2471
+ return JSON.parse(JSON.stringify(value));
2472
+ }
2473
+ function verdictOf(change) {
2474
+ if (change.application === "applied") return "装好了,已生效";
2475
+ if (change.application === "restart-required") return "装好了,要重启一次才生效(装新模块、同名重装、替换已有包都走这一支)";
2476
+ if (change.application === "overridden") return "被更高层的声明盖住了,这次没生效";
2477
+ if (change.application === "cancelled") return "安装被取消,没有生效";
2478
+ const tail = (change.packageResult?.output ?? "").trim().split("\n").slice(-3).join(" | ");
2479
+ return `没装上(${change.errorCode ?? "failed"})${tail === "" ? "" : `:${tail}`}`;
2480
+ }
2481
+ function statusOf(options) {
2482
+ const { dir, unit, name, installed, known, open } = options;
2483
+ const facts = moduleBuildFacts(dir);
2484
+ const next = !installed ? "装:action=install(第一次,会让用户点头一次)" : !known ? "面板还不认得这个单元 —— 重启一次工作台(新装的模块要重启才注册进面板),重启后再 action=open" : !open ? "上板:action=open(只上板,不重装)" : !facts.builtAll ? "还没构建:pnpm exec dsh-wb-module build" : facts.stale ? "源码比产物新:pnpm exec dsh-wb-module build(宿主侧秒级热换;改了 client.js 刷新页面)" : "都在生效:改源码 → build(宿主侧秒级热换;改了 client.js 刷新页面),不要再装一遍";
2485
+ return {
2486
+ unit,
2487
+ name,
2488
+ dir,
2489
+ installed,
2490
+ known,
2491
+ open,
2492
+ artifacts: facts.artifacts,
2493
+ stale: facts.stale,
2494
+ next
2495
+ };
2496
+ }
2497
+ function renderStatus(packagesDir, lines) {
2498
+ if (lines.length === 0) return renderList(packagesDir, []);
2499
+ return `工坊:${packagesDir}\n${lines.map((one) => {
2500
+ return [
2501
+ `—— ${one.unit}(${one.name})`,
2502
+ ` 装:${one.installed ? "是" : "没有"} · 面板认得:${one.known ? "是" : "不是"} · 这个项目上开着:${one.open ? "是" : "没有"}`,
2503
+ one.artifacts.length === 0 ? " 产物:没有" : ` 产物:${one.artifacts.map((item) => `${item.path}${item.at === null ? "(缺)" : ` ${item.at.slice(11, 19)}Z`}`).join(" · ")} · 源码比产物新:${one.stale ? "是" : "没有"}`,
2504
+ ` 下一步:${one.next}`
2505
+ ].join("\n");
2506
+ }).join("\n")}`;
2507
+ }
2508
+ function renderProbe(out) {
2509
+ if (out.ok) return `读到了 —— 这就是那条读的返回值,把它固化成模块宿主半侧的那一条:\n${(JSON.stringify(out.value, null, 2) ?? "undefined").slice(0, 2e3)}`;
2510
+ const lines = [`没读到(${out.code}):${out.message}`];
2511
+ if (out.hint !== void 0) lines.push(`照做:${out.hint}`);
2512
+ if (out.helpUrl !== void 0) lines.push(`入口:${out.helpUrl}`);
2513
+ if (out.pageCode !== void 0) lines.push(`页面自己回的码:${out.pageCode}`);
2514
+ return lines.join("\n");
2515
+ }
2516
+ function renderFind(out) {
2517
+ if (!out.ok) {
2518
+ const lines = [`没找到(${out.code}):${out.message}`];
2519
+ if (out.hint !== void 0) lines.push(`照做:${out.hint}`);
2520
+ if (out.helpUrl !== void 0) lines.push(`入口:${out.helpUrl}`);
2521
+ return lines.join("\n");
2522
+ }
2523
+ if (out.count === 0) return "找到了 0 个元素 —— 选择器没匹配到东西。先确认停在哪一页(probe 会自己问页面),再把选择器放宽一点(例如 table tr 而不是某个 id)。";
2524
+ const head = out.entries.slice(0, 20).map((one, index) => `${String(index + 1)}. ${one.text.replace(/\n+/g, " | ").slice(0, 200)}`);
2525
+ const more = out.entries.length > 20 ? `\n… 还有 ${String(out.entries.length - 20)} 个` : "";
2526
+ return `找到了 ${String(out.count)} 个元素 —— 每行的单元格按换行拼在 text 里;把它固化成模块宿主半侧的那一条(host 的 browserFind + 切格 + 按列序取值):\n${head.join("\n")}${more}`;
2527
+ }
2528
+ function renderList(packagesDir, modules) {
2529
+ if (modules.length === 0) return `工坊里还没有模块(${packagesDir})—— 在里面造一个:pnpm exec dsh-wb-module init <名字> --dir <工坊>/packages/module-<名字> --template diy`;
2530
+ const lines = modules.map((one) => `${one.unit} · ${one.installed ? "已装" : "没装"} · ${one.name}` + (one.description === "" ? "" : `\n${" ".repeat(one.unit.length)} ${one.description}`));
2531
+ const footer = modules.filter((one) => !one.installed).length === 0 ? "都已装过 —— **不要再装**:改源码 + dsh-wb-module build(宿主侧秒级热换;改了 client.js 刷新一下页面)。" : "装:workbench_diy(action=install, unit=<上面那个 unit>)";
2532
+ return `工坊:${packagesDir}\n${lines.join("\n")}\n\n${footer}`;
2533
+ }
2534
+ function showOrReason(panels, unit, workspace) {
2535
+ try {
2536
+ panels.show(unit, workspace);
2537
+ return { ok: true };
2538
+ } catch (error) {
2539
+ return {
2540
+ ok: false,
2541
+ reason: error instanceof Error ? error.message : String(error)
2542
+ };
2543
+ }
2544
+ }
2545
+ function registerDiyTools(ctx, market, panels) {
2546
+ const tools = ctx.get("tools");
2547
+ if (tools === void 0) {
2548
+ ctx.logger?.info?.("[dsh-workbench] 没有 tools 服务,跳过 DIY 工坊工具注册");
2549
+ return [];
2550
+ }
2551
+ const tool = {
2552
+ name: "workbench_diy",
2553
+ description: "看 DIY 工坊里的模块现在走到哪一步,或者把它们装上 / 上板。只认工坊里的模块(不是市场 —— 装别的东西用 workbench_modules)。status 会把五件事一次说清(装了没 / 面板认得没 / 这个项目上开着没 / 产物齐不齐 / 源码是否比产物新)并给出下一步,所以别自己去拼这些状态、也别反复 install。recon 是**先看有没有现成的**:拿一个关键词(站点名 / 系统名 / 域名)去 opencli 自带的那份目录里找,回它有哪些命令、要不要用户的 Chrome 登录;都没有就老实说,并给出\"写 adapter\"或\"探一条读\"两条路。用户不知道 opencli 是什么 —— 这一步别问他,自己查。probe 是**探一条读**:用用户浏览器里现有的登录去读一次,把结果与失败码归一(两拍、重试、平台码映射都在里面),探明之后再把它固化成模块宿主半侧的那一条。两种读法二选一:script(页面里跑一段回 JSON 的脚本,读接口)或 css(给一个选择器,读 DOM 那一行 —— 页面里没有接口时用这个,别写 document.querySelectorAll)。装是这台机器上的持久变更,要用户点头;**一辈子只装一次**:之后改源码只需 dsh-wb-module build(宿主侧秒级热换;改了 client.js 刷新一下页面),不要再调它。",
2554
+ parameters: {
2555
+ type: "object",
2556
+ properties: {
2557
+ action: {
2558
+ type: "string",
2559
+ description: "list=工坊里有哪些模块(默认);status=某个模块现在走到哪一步、下一步做什么;probe=在用户浏览器里探一条读;recon=先查 opencli 现成的能力(自带 adapter / 本机插件);open=只把它在这个项目上板(不装);install=装上并上板(第一次、会让用户点头)。",
2560
+ enum: [...ACTIONS$1]
2561
+ },
2562
+ unit: {
2563
+ type: "string",
2564
+ description: "status / open / install 填:工坊里那个模块的单元 id(list 会给)。"
2565
+ },
2566
+ script: {
2567
+ type: "string",
2568
+ description: "probe 要(与 css 二选一):在页面里跑的脚本(必须回 JSON)。不在站点就回 { ok: false, code: \"not-on-site\" };登录过期回 { ok: false, code: \"not-logged-in\" };成功回 { ok: true, ...数据 }。"
2569
+ },
2570
+ css: {
2571
+ type: "string",
2572
+ description: "probe 要(与 script 二选一):一个 CSS 选择器,回页面上匹配到的元素(每行的单元格按换行拼在 text 里)。页面里没有接口时用它 —— 走的是一条固定的 DOM 读(找不到会自己打开 url 再读一遍),别写 document.querySelectorAll。"
2573
+ },
2574
+ limit: {
2575
+ type: "number",
2576
+ description: "probe 配 css 用:最多回几个元素(默认 50)。"
2577
+ },
2578
+ textMax: {
2579
+ type: "number",
2580
+ description: "probe 配 css 用:每个元素文本最多留几个字(默认 200)。"
2581
+ },
2582
+ url: {
2583
+ type: "string",
2584
+ description: "probe / recon 用:那套系统的地址(probe 不在站点上时自己打开它再读一遍;recon 拿它取域名去找现成的 adapter)。"
2585
+ },
2586
+ keyword: {
2587
+ type: "string",
2588
+ description: "recon 要:站点名 / 系统名 / 关键词(英文名或域名最准,例:bilibili、arxiv.org、cmdb)。用户说的是\"我想看 X\",把它翻译成关键词是你的活 —— 别问他用哪个插件。"
2589
+ },
2590
+ loginUrl: {
2591
+ type: "string",
2592
+ description: "probe 用:要登录时那颗\"去登录\"该去哪(默认用 url)。"
2593
+ },
2594
+ session: {
2595
+ type: "string",
2596
+ description: "probe 用:浏览器桥上的会话名(默认 workbench,一般不用填)。"
2597
+ },
2598
+ open: {
2599
+ type: "boolean",
2600
+ description: "probe 用:不在站点上时要不要顺手打开(默认要)。"
2601
+ }
2602
+ },
2603
+ required: ["action"],
2604
+ additionalProperties: false
2605
+ },
2606
+ output: {
2607
+ schema: OUTPUT_SCHEMA$2,
2608
+ render: (_args, value) => {
2609
+ const payload = value;
2610
+ return [{
2611
+ type: "text",
2612
+ text: typeof payload?.text === "string" ? payload.text : JSON.stringify(value)
2613
+ }];
2614
+ }
2615
+ },
2616
+ async execute(raw, exec) {
2617
+ const args = asRecord$1(raw) ?? {};
2618
+ const action = asText$1(args.action) || "list";
2619
+ if (!ACTIONS$1.includes(action)) throw new Error(`不认识的 action:${action}(可用:${ACTIONS$1.join(" / ")})`);
2620
+ const packagesDir = workshopPackagesDir(ctx);
2621
+ if (packagesDir === null) throw new Error("算不出 DIY 工坊在哪(这台机器没给出 dshHomePath)—— 让用户跑一次 dsh-workbench setup");
2622
+ const session = sessionWorkspace(exec);
2623
+ if (!inWorkshopSession(ctx, session)) throw new Error(`这把工具只在 DIY 工坊里用:当前会话不在工坊里(这个会话的工作区 ${session ?? "(不知道)"})—— 普通项目里装模块用 workbench_modules,工坊的入口是左栏那一行「工作台DIY」`);
2624
+ const workspace = session;
2625
+ const modules = workshopModules(packagesDir, profileModules(profileDirOf(ctx)).map((row) => row.unit));
2626
+ if (action === "list") return plainJson({
2627
+ workshop: packagesDir,
2628
+ modules,
2629
+ text: renderList(packagesDir, modules)
2630
+ });
2631
+ const panelEntries = (() => {
2632
+ try {
2633
+ return panels.list(workspace);
2634
+ } catch {
2635
+ return [];
2636
+ }
2637
+ })();
2638
+ const statusOfUnit = (one) => statusOf({
2639
+ dir: one.dir,
2640
+ unit: one.unit,
2641
+ name: one.name,
2642
+ installed: one.installed,
2643
+ known: panelEntries.some((entry) => entry.id === one.unit),
2644
+ open: panels.isEnabled(one.unit, workspace)
2645
+ });
2646
+ if (action === "status") {
2647
+ const wanted = asText$1(args.unit);
2648
+ const picked = wanted === "" ? modules : modules.filter((one) => one.unit === wanted);
2649
+ if (wanted !== "" && picked.length === 0) throw new Error(`工坊里没有单元 id 为「${wanted}」的模块(可用的:${modules.map((one) => one.unit).join(", ") || "(空)"})`);
2650
+ const rows = picked.map(statusOfUnit);
2651
+ return plainJson({
2652
+ workshop: packagesDir,
2653
+ status: rows,
2654
+ text: renderStatus(packagesDir, rows)
2655
+ });
2656
+ }
2657
+ if (action === "recon") {
2658
+ const keywords = keywordsOf(asText$1(args.keyword), asText$1(args.url));
2659
+ if (keywords.length === 0) throw new Error("recon 要一个关键词(站点名 / 系统名 / 域名都行):先拿它去 opencli 自带的那份目录里找有没有现成的能力,别一上来就自己写 adapter;用户不知道 opencli 是什么,这一步别问他。");
2660
+ const catalog = reconCatalogOf();
2661
+ if (catalog === void 0) throw new Error(`读不到 opencli 那份目录(${cliCatalogPath()})—— 隔离运行时还没装好;照做:${isolatedInstallCommand()}`);
2662
+ return plainJson({
2663
+ workshop: packagesDir,
2664
+ ok: true,
2665
+ ...reconOf(catalog, installedPluginsOf(), keywords)
2666
+ });
2667
+ }
2668
+ if (action === "probe") {
2669
+ const script = asText$1(args.script);
2670
+ const css = asText$1(args.css);
2671
+ if (script === "" && css === "") throw new Error("probe 要 script(页面脚本,回 JSON)或 css(读 DOM 那一行的选择器)之一。script 的约定:不在那个站点上就回 { ok: false, code: \"not-on-site\" }(我会自己把它打开再读一遍);登录过期回 { ok: false, code: \"not-logged-in\" };成功回 { ok: true, ...数据 }。页面里压根没有接口时给 css,别写 document.querySelectorAll(宿主产物不许出现 DOM 字样)。");
2672
+ const shell = ctx.get("shell");
2673
+ if (shell === void 0) throw new Error("这台机器上没有 shell 服务,probe 跑不了");
2674
+ const url = asText$1(args.url);
2675
+ const loginUrl = asText$1(args.loginUrl);
2676
+ const session = asText$1(args.session);
2677
+ const open = typeof args.open === "boolean" ? args.open : void 0;
2678
+ if (css !== "") {
2679
+ const limit = asNumber(args.limit);
2680
+ const textMax = asNumber(args.textMax);
2681
+ const found = await browserFind({
2682
+ shell,
2683
+ css,
2684
+ command: opencliCommandOf(ctx).command,
2685
+ ...url === "" ? {} : { url },
2686
+ ...session === "" ? {} : { session },
2687
+ ...open === void 0 ? {} : { open },
2688
+ ...limit === void 0 ? {} : { limit },
2689
+ ...textMax === void 0 ? {} : { textMax }
2690
+ });
2691
+ return plainJson({
2692
+ workshop: packagesDir,
2693
+ ...found,
2694
+ text: renderFind(found)
2695
+ });
2696
+ }
2697
+ const out = await browserRead({
2698
+ shell,
2699
+ script,
2700
+ command: opencliCommandOf(ctx).command,
2701
+ ...url === "" ? {} : { url },
2702
+ ...loginUrl === "" ? {} : { loginUrl },
2703
+ ...session === "" ? {} : { session },
2704
+ ...open === void 0 ? {} : { open }
2705
+ });
2706
+ return plainJson({
2707
+ workshop: packagesDir,
2708
+ ...out,
2709
+ text: renderProbe(out)
2710
+ });
2711
+ }
2712
+ const unit = asText$1(args.unit);
2713
+ if (unit === "") throw new Error(`install 要一个 unit(工坊里:${modules.map((one) => one.unit).join(", ") || "(空)"})`);
2714
+ const found = modules.find((one) => one.unit === unit);
2715
+ if (found === void 0) throw new Error(`工坊里没有单元 id 为「${unit}」的模块(可用的:${modules.map((one) => one.unit).join(", ") || "(空)"})—— 先装上的前提是它在工坊里造好了`);
2716
+ if (!insideWorkshop(packagesDir, found.dir)) throw new Error(`这个模块不在工坊里(${found.dir})—— 只装 ${packagesDir} 之下的模块`);
2717
+ if (action === "open") {
2718
+ if (!found.installed) throw new Error(`${unit} 还没装 —— open 只负责上板。先用 action=install 装上(第一次会让用户点头一次)`);
2719
+ const panel = showOrReason(panels, unit, workspace);
2720
+ const text = panel.ok ? `面板上已经打开它了(${workspace})—— 让用户看一眼` : `面板上还没打开:${panel.reason}。这就是它的下一步(status 会给出同样的判断)`;
2721
+ return plainJson({
2722
+ workshop: packagesDir,
2723
+ unit,
2724
+ path: found.dir,
2725
+ panel,
2726
+ text
2727
+ });
2728
+ }
2729
+ if (found.installed) {
2730
+ const panel = showOrReason(panels, unit, workspace);
2731
+ const text = [`它已经装过了(就是工坊里这一份)—— **不要再装**:改源码 + dsh-wb-module build(宿主侧秒级热换;改了 client.js 刷新一下页面)。`, panel.ok ? `面板上已经打开它了(${workspace})—— 让用户看一眼` : `面板上还没打开:${panel.reason}。调 workbench_panels(action=show, id=${unit})`].join("\n");
2732
+ return plainJson({
2733
+ workshop: packagesDir,
2734
+ unit,
2735
+ path: found.dir,
2736
+ alreadyInstalled: true,
2737
+ panel,
2738
+ text
2739
+ });
2740
+ }
2741
+ const outcome = await market.installBundle(found.dir);
2742
+ const verdict = verdictOf(outcome.change);
2743
+ const restartRequired = outcome.change.application === "restart-required";
2744
+ const panel = outcome.change.application === "applied" ? showOrReason(panels, unit, workspace) : {
2745
+ ok: false,
2746
+ reason: `安装结果是 ${outcome.change.application},还没生效`
2747
+ };
2748
+ const panelText = panel.ok ? `面板上已经打开它了(${workspace})—— 让用户看一眼` : outcome.change.application === "applied" ? `面板上还没打开:${panel.reason}。调 workbench_panels(action=show, id=${unit})试试。` : restartRequired ? `面板上还没打开(${panel.reason}):模块要**重启一次工作台**才注册进面板 —— 把这句话转给用户,重启后再调 workbench_panels(action=show, id=${unit})。` : `面板上还没打开:${panel.reason}。先把上面那句"没装成"处理掉,再谈上板。`;
2749
+ const text = [
2750
+ `${verdict}:${unit}(${found.name})`,
2751
+ panelText,
2752
+ "之后只改源码 + dsh-wb-module build(宿主侧秒级热换;改了 client.js 刷新一下页面):不要再装一遍。"
2753
+ ].join("\n");
2754
+ return plainJson({
2755
+ workshop: packagesDir,
2756
+ unit,
2757
+ path: found.dir,
2758
+ install: outcome.change,
2759
+ module: outcome.module,
2760
+ panel,
2761
+ text
2762
+ });
2763
+ }
2764
+ };
2765
+ return [tools.register(tool)];
2766
+ }
2767
+ //#endregion
1555
2768
  //#region src/shared/api/host/release.ts
1556
2769
  function parseLedgerVersion(stdout, name) {
1557
2770
  const parsed = parseJsonLoose(stdout);
@@ -2428,7 +3641,8 @@ function panelsHandoffEntry(target = {}) {
2428
3641
  }
2429
3642
  function panelsFactsOf(call, manifests) {
2430
3643
  if (call.source === "workbench_panels") return actionOf(call) === "list" ? [PANELS_FACT] : void 0;
2431
- if (call.source !== "workbench_modules") return void 0;
3644
+ if (call.source === "workbench_diy" && actionOf(call) === "list") return [PANELS_FACT];
3645
+ if (call.source !== "workbench_modules" && call.source !== "workbench_diy") return void 0;
2432
3646
  if (actionOf(call) === "list") return [PANELS_FACT, CAPABILITY_FACT];
2433
3647
  const request = requestOf(call);
2434
3648
  if (request === void 0) return void 0;
@@ -2788,11 +4002,15 @@ var HOST_UNITS = [{
2788
4002
  });
2789
4003
  return {
2790
4004
  routes: panelRoutes(panels, values),
2791
- registerTools: () => [...registerPanelTools(ctx, panels, values), ...registerMarketTools(ctx, market)],
4005
+ registerTools: () => [
4006
+ ...registerPanelTools(ctx, panels, values),
4007
+ ...registerMarketTools(ctx, market),
4008
+ ...registerDiyTools(ctx, market, panels)
4009
+ ],
2792
4010
  factsOf: (call) => panelsFactsOf(call, manifests()),
2793
4011
  needsApproval: panelsApproval,
2794
4012
  gate: {
2795
- deniedTools: (workspace) => manifests().filter((manifest) => manifest.card !== null).filter((manifest) => !panels.isEnabled(manifest.id, workspace)).flatMap((manifest) => manifest.tools.map((tool) => tool.name)),
4013
+ deniedTools: (workspace) => [...manifests().filter((manifest) => manifest.card !== null).filter((manifest) => !panels.isEnabled(manifest.id, workspace)).flatMap((manifest) => manifest.tools.map((tool) => tool.name)), ...inWorkshopSession(ctx, workspace) ? [] : [DIY_TOOL]],
2796
4014
  subscribe: (listener) => panels.subscribe(listener)
2797
4015
  },
2798
4016
  prefs: {
@@ -3003,7 +4221,8 @@ function moduleVersionsHandler(ctx, shell) {
3003
4221
  unit: module.unit,
3004
4222
  pkg: module.pkg,
3005
4223
  installed: module.version,
3006
- ...latest === void 0 ? {} : { latest }
4224
+ ...latest === void 0 ? {} : { latest },
4225
+ ...module.local === true ? { local: true } : {}
3007
4226
  };
3008
4227
  }))
3009
4228
  };
@@ -3045,18 +4264,6 @@ function bundleInspectHandler(ctx) {
3045
4264
  //#region src/shared/api/diy.ts
3046
4265
  var WORKSHOP_TITLE = "工作台DIY";
3047
4266
  //#endregion
3048
- //#region src/shared/api/host/diy.ts
3049
- var WORKSHOP_RELATIVE = "workbench/diy";
3050
- function workshopPathOf(ctx) {
3051
- const homePath = ctx.get("dshHomePath");
3052
- if (typeof homePath !== "function") return null;
3053
- try {
3054
- return homePath(WORKSHOP_RELATIVE);
3055
- } catch {
3056
- return null;
3057
- }
3058
- }
3059
- //#endregion
3060
4267
  //#region src/shared/api/dsh-plugins.ts
3061
4268
  var DSH_PLUGINS_ROUTE = "dsh-plugins";
3062
4269
  //#endregion
@@ -4090,8 +5297,9 @@ function asArgs(value) {
4090
5297
  function apply(ctx) {
4091
5298
  const facts = createFactLog();
4092
5299
  const values = createValueHandoff();
5300
+ const opencli = opencliCommandOf(ctx);
4093
5301
  const routes = {
4094
- health: healthHandler(ctx),
5302
+ health: healthHandler(ctx, opencli),
4095
5303
  changes: changesHandler(facts),
4096
5304
  [DSH_PLUGINS_ROUTE]: dshPluginsHandler(ctx),
4097
5305
  [BUNDLE_REGISTRIES_ROUTE]: bundleRegistriesHandler(ctx),
@@ -4111,7 +5319,6 @@ function apply(ctx) {
4111
5319
  ctx.logger?.warn?.(`dsh-workbench: 工坊没能登记成工作区 —— ${String(error)}`);
4112
5320
  });
4113
5321
  });
4114
- const opencliEnv = process.env.DSH_WORKBENCH_OPENCLI;
4115
5322
  const installEnv = process.env.DSH_WORKBENCH_OPENCLI_INSTALL;
4116
5323
  const liveManifests = () => units.manifests();
4117
5324
  const manifestOf = (unit) => liveManifests().find((manifest) => manifest.id === unit);
@@ -4129,7 +5336,7 @@ function apply(ctx) {
4129
5336
  if (manifest === void 0 || !hasEnvNeeds(manifest)) return void 0;
4130
5337
  const found = (await probeUnits({
4131
5338
  shell,
4132
- ...opencliEnv === void 0 ? {} : { command: opencliEnv },
5339
+ command: opencli.command,
4133
5340
  force,
4134
5341
  units: [probeInput(manifest)]
4135
5342
  }))[0];
@@ -4176,7 +5383,7 @@ function apply(ctx) {
4176
5383
  logger: ctx.logger,
4177
5384
  manifests: liveManifests,
4178
5385
  store,
4179
- ...opencliEnv === void 0 ? {} : { opencliCommand: opencliEnv },
5386
+ opencliCommand: opencli.command,
4180
5387
  probe: (unit, options) => probeOne(unit, options?.force ?? false),
4181
5388
  values,
4182
5389
  longTasks: {
@@ -4269,7 +5476,7 @@ function apply(ctx) {
4269
5476
  else {
4270
5477
  routes.preflight = preflightHandler(createPreflight({
4271
5478
  shell,
4272
- ...opencliEnv === void 0 ? {} : { command: opencliEnv },
5479
+ command: opencli.command,
4273
5480
  ...installEnv === void 0 ? {} : { installCommand: installEnv },
4274
5481
  logger: ctx.logger,
4275
5482
  warmReads: () => [...mounted.values()].flatMap((item) => item.plan.warmReads())
@@ -4277,7 +5484,7 @@ function apply(ctx) {
4277
5484
  routes.capability = capabilityHandler(async (unit, force) => {
4278
5485
  if (unit === void 0) return { units: (await probeUnits({
4279
5486
  shell,
4280
- ...opencliEnv === void 0 ? {} : { command: opencliEnv },
5487
+ command: opencli.command,
4281
5488
  force,
4282
5489
  units: declaredNow().map(probeInput)
4283
5490
  })).map((item) => ({