zdashboard 2.7.0 → 2.8.1

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 (47) hide show
  1. package/README.md +1 -0
  2. package/dist/cli.js +571 -1018
  3. package/dist/cli.js.map +1 -1
  4. package/dist/web/assets/BatchView-BcYVTq5m.js +2 -0
  5. package/dist/web/assets/{CheckpointViewer-DBmTkC_E.js → CheckpointViewer-aVIaKKvz.js} +1 -1
  6. package/dist/web/assets/{FileIcon-tDB2mYGC.js → CodeViewer-DFzMfmdB.js} +31 -31
  7. package/dist/web/assets/DependencyGraph-NqqCB6Vt.js +1 -0
  8. package/dist/web/assets/FileIcon-Bk6QVgj6.js +1 -0
  9. package/dist/web/assets/{FilterPills-CvO0pz85.js → FilterPills-CiDfp_8h.js} +1 -1
  10. package/dist/web/assets/{ProgressBar-DIoIeYBA.js → ProgressBar-DIVNOeTV.js} +1 -1
  11. package/dist/web/assets/Sidebar-DbPvTOr4.js +1 -0
  12. package/dist/web/assets/Sidebar-prXqMrLa.js +1 -0
  13. package/dist/web/assets/SingleChangeView-e7LU1eVP.js +1 -0
  14. package/dist/web/assets/ViewHeader-1hW25T7x.js +1 -0
  15. package/dist/web/assets/Workspace-BgZLrrMa.js +1 -0
  16. package/dist/web/assets/Workspace-BjE2HRuu.js +1 -0
  17. package/dist/web/assets/Workspace-CCD9dE4r.js +2 -0
  18. package/dist/web/assets/Workspace-CthIPSy_.js +1 -0
  19. package/dist/web/assets/Workspace-DHQUpHN1.js +5 -0
  20. package/dist/web/assets/client-Cw4K1Nx3.js +1 -0
  21. package/dist/web/assets/{index-yx7txO50.js → index-Cq5UOBJN.js} +509 -509
  22. package/dist/web/assets/index-DcspTMh9.css +1 -0
  23. package/dist/web/assets/{index-CtlFZEcM.js → index-PcP6ebRP.js} +2 -2
  24. package/dist/web/assets/usePluginData-Cpm5C7LU.js +1 -0
  25. package/dist/web/assets/web-BnL7vuwB.js +2 -0
  26. package/dist/web/assets/web-CdCGfZIi.js +2 -0
  27. package/dist/web/assets/web-Ch8IK7pu.js +2 -0
  28. package/dist/web/assets/web-CjjS2dbX.js +2 -0
  29. package/dist/web/assets/web-Jaf9aZCx.js +2 -0
  30. package/dist/web/index.html +3 -3
  31. package/package.json +6 -1
  32. package/dist/web/assets/ApprovalPanel-CXQRioSw.js +0 -1
  33. package/dist/web/assets/BatchDashboard-BigPUG8g.js +0 -2
  34. package/dist/web/assets/ConfigField-BqjVlteZ.js +0 -1
  35. package/dist/web/assets/DependencyGraph-DPVneSQh.js +0 -1
  36. package/dist/web/assets/Workspace-B2nKQnQf.js +0 -1
  37. package/dist/web/assets/badge-CmIMTBDj.js +0 -1
  38. package/dist/web/assets/index-BHL7sr_b.css +0 -1
  39. package/dist/web/assets/web-BBv8hkwo.js +0 -1
  40. package/dist/web/assets/web-BNjs9lCy.js +0 -139
  41. package/dist/web/assets/web-BX2cajm6.js +0 -5
  42. package/dist/web/assets/web-BpBEbcYZ.js +0 -1
  43. package/dist/web/assets/web-CGbbhItO.js +0 -2
  44. package/dist/web/assets/web-CcDed_yx.js +0 -1
  45. package/dist/web/assets/web-DsIjKVnn.js +0 -2
  46. package/dist/web/assets/web-Eo4S0U_k.js +0 -2
  47. /package/dist/web/assets/{FileIcon-BTcE7LSd.css → CodeViewer-BTcE7LSd.css} +0 -0
package/dist/cli.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import path12 from "path";
5
- import fs12 from "fs";
4
+ import path11 from "path";
5
+ import fs11 from "fs";
6
6
  import { fileURLToPath as fileURLToPath2 } from "url";
7
7
  import { access, readdir } from "fs/promises";
8
8
  import { parseArgs as utilParseArgs } from "util";
@@ -11,32 +11,23 @@ import { parseArgs as utilParseArgs } from "util";
11
11
  import fs from "fs";
12
12
  import path from "path";
13
13
  import { execFile } from "child_process";
14
+ var JUST_PROBE_TIMEOUT_MS = 5e3;
15
+ var JUST_FALLBACK_TIMEOUT_MS = 3e3;
14
16
  function justAvailable(cwd) {
15
- return new Promise((resolve) => {
16
- const child = execFile("just", ["--list", "--unsupported"], { cwd, timeout: 5e3 }, (err) => {
17
- resolve(!err);
18
- });
17
+ const probe = (args, timeout) => new Promise((resolve) => {
18
+ const child = execFile("just", args, { cwd, timeout }, (err) => resolve(!err));
19
19
  if (child.killed) resolve(false);
20
20
  });
21
- }
22
- function hasBugsConfig(root) {
23
- const dashboardPath = path.join(root, ".zdev", "dashboard.json");
24
- if (!fs.existsSync(dashboardPath)) return false;
25
- try {
26
- const raw = fs.readFileSync(dashboardPath, "utf-8");
27
- const rec = JSON.parse(raw);
28
- const bugs = rec.plugins?.bugs;
29
- if (bugs && typeof bugs === "object" && bugs.url && bugs.product) return true;
30
- } catch {
31
- }
32
- return false;
21
+ return probe(["--list", "--unsupported"], JUST_PROBE_TIMEOUT_MS).then((ok) => ok ? true : probe(["--version"], JUST_FALLBACK_TIMEOUT_MS));
33
22
  }
34
23
  async function detect(root) {
35
24
  const hasOpenspec = fs.existsSync(path.join(root, "openspec"));
36
25
  const hasDocs = fs.existsSync(path.join(root, "docs"));
37
26
  const hasJust = await justAvailable(root);
38
- const hasBugs = hasBugsConfig(root);
39
- return { hasOpenspec, hasDocs, hasJust, hasBugs };
27
+ return { hasOpenspec, hasDocs, hasJust };
28
+ }
29
+ async function detectLiveShape(root) {
30
+ return detect(root);
40
31
  }
41
32
 
42
33
  // src/cli.ts
@@ -52,7 +43,7 @@ import { fileURLToPath } from "url";
52
43
  // package.json
53
44
  var package_default = {
54
45
  name: "zdashboard",
55
- version: "2.7.0",
46
+ version: "2.8.1",
56
47
  description: "ZCode skill dashboard platform \u2014 pluggable viewers for zdesign/zview/zreview/zgoal",
57
48
  type: "module",
58
49
  bin: {
@@ -70,6 +61,9 @@ var package_default = {
70
61
  "build:web": "vite build",
71
62
  "build:node": "tsup",
72
63
  start: "node dist/cli.js",
64
+ test: "vitest run",
65
+ typecheck: "tsc --noEmit",
66
+ "test:coverage": "vitest run --coverage",
73
67
  preview: "vite preview"
74
68
  },
75
69
  dependencies: {
@@ -89,6 +83,7 @@ var package_default = {
89
83
  "date-fns": "^3.6.0",
90
84
  filesize: "^11.0.0",
91
85
  "highlight.js": "^11.11.1",
86
+ "iconv-lite": "0.7.3",
92
87
  katex: "^0.16.11",
93
88
  ky: "^1.7.0",
94
89
  "lodash-es": "^4.17.21",
@@ -121,6 +116,7 @@ var package_default = {
121
116
  "@types/react": "^18.3.12",
122
117
  "@types/react-dom": "^18.3.1",
123
118
  "@vitejs/plugin-react": "^4.7.0",
119
+ "@vitest/coverage-v8": "1.6.0",
124
120
  autoprefixer: "^10.4.20",
125
121
  jsdom: "^30.0.1",
126
122
  postcss: "^8.4.49",
@@ -166,14 +162,21 @@ function readRecord(root) {
166
162
  }
167
163
  }
168
164
  function writeRecord(root, port) {
169
- fs2.mkdirSync(path2.dirname(recordPath(root)), { recursive: true });
165
+ const fp = recordPath(root);
166
+ fs2.mkdirSync(path2.dirname(fp), { recursive: true });
167
+ const old = readRecord(root);
170
168
  const rec = {
171
169
  pid: process.pid,
172
170
  port,
173
171
  root,
174
172
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
175
173
  };
176
- fs2.writeFileSync(recordPath(root), JSON.stringify(rec, null, 2) + "\n");
174
+ if (old?.plugins && typeof old.plugins === "object") {
175
+ rec.plugins = old.plugins;
176
+ }
177
+ const tmp = fp + ".tmp";
178
+ fs2.writeFileSync(tmp, JSON.stringify(rec, null, 2) + "\n");
179
+ fs2.renameSync(tmp, fp);
177
180
  }
178
181
  function readPluginsConfig(root) {
179
182
  try {
@@ -199,6 +202,44 @@ function writePluginsConfig(root, plugins) {
199
202
  fs2.writeFileSync(tmp, JSON.stringify(rec, null, 2) + "\n");
200
203
  fs2.renameSync(tmp, fp);
201
204
  }
205
+ function stripDeadPluginConfig(root, manifests) {
206
+ try {
207
+ const rec = readRecord(root);
208
+ if (!rec?.plugins || typeof rec.plugins !== "object") return false;
209
+ const byMode = new Map(manifests.map((m) => [m.mode, m]));
210
+ const stored = rec.plugins;
211
+ const plugins = { ...stored };
212
+ let changed = false;
213
+ for (const mode of Object.keys(plugins)) {
214
+ const m = byMode.get(mode);
215
+ if (!m || m.external) continue;
216
+ const section = plugins[mode];
217
+ if (section === null || typeof section !== "object") continue;
218
+ const keys = Object.keys(section);
219
+ if (!m.config || Object.keys(m.config).length === 0) {
220
+ if (keys.length > 0) {
221
+ delete plugins[mode];
222
+ changed = true;
223
+ }
224
+ continue;
225
+ }
226
+ const kept = {};
227
+ for (const key of Object.keys(m.config)) {
228
+ if (key in section) kept[key] = section[key];
229
+ }
230
+ if (Object.keys(kept).length !== keys.length) {
231
+ if (Object.keys(kept).length === 0) delete plugins[mode];
232
+ else plugins[mode] = kept;
233
+ changed = true;
234
+ }
235
+ }
236
+ if (!changed) return false;
237
+ writePluginsConfig(root, plugins);
238
+ return true;
239
+ } catch {
240
+ return false;
241
+ }
242
+ }
202
243
  function clearRecord(root) {
203
244
  try {
204
245
  const rec = readRecord(root);
@@ -330,8 +371,20 @@ var MIME = {
330
371
  ".ttf": "font/woff",
331
372
  ".map": "application/json; charset=utf-8"
332
373
  };
333
- var INJECT = `<script>(function(){try{var es=new EventSource('/__reload');es.addEventListener('files',function(){/* plugin \u5C40\u90E8\u5237\u65B0\u7531 useSSE \u6D88\u8D39,\u6B64\u5904\u4E0D\u518D\u6574\u9875 reload */});es.onerror=function(){/* EventSource \u539F\u751F\u91CD\u8FDE,\u9759\u9ED8\u5904\u7406 */};}catch(e){}document.addEventListener('click',function(e){var t=e.target;if(t&&t.closest){var a=t.closest('a[target]');if(a&&a.target!=='_self'){a.target='_self';}}},true);})();</script>`;
374
+ var INJECT = `<script>(function(){document.addEventListener('click',function(e){var t=e.target;if(t&&t.closest){var a=t.closest('a[target]');if(a&&a.target!=='_self'){a.target='_self';}}},true);})();</script>`;
334
375
  var PLUGIN_STATIC_PREFIX = "/__plugin/";
376
+ function json(res, data, status = 200) {
377
+ res.writeHead(status, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
378
+ res.end(JSON.stringify(data));
379
+ }
380
+ function guarded(req, res, token) {
381
+ if (req.headers["x-stop-token"] !== token) {
382
+ res.writeHead(403);
383
+ res.end("forbidden");
384
+ return false;
385
+ }
386
+ return true;
387
+ }
335
388
  var ServerService = class extends Service {
336
389
  stopToken;
337
390
  root;
@@ -358,28 +411,54 @@ var ServerService = class extends Service {
358
411
  ctx.effect(() => () => this.dispose());
359
412
  this.refreshGitInfo();
360
413
  this.route("/__config", (_req, res) => {
361
- res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
362
- res.end(JSON.stringify({ stopToken: this.stopToken, version: VERSION, root: this.root, ...this.gitInfo }));
414
+ json(res, { stopToken: this.stopToken, version: VERSION, root: this.root, ...this.gitInfo });
363
415
  });
364
- this.route("/__stop", async (req, res) => {
365
- if (req.headers["x-stop-token"] !== this.stopToken) {
416
+ this.route("/__detect", async (_req, res) => {
417
+ const shape = await detectLiveShape(this.root);
418
+ json(res, shape);
419
+ });
420
+ this.route("/favicon.svg", (req, res) => {
421
+ this.serveFile(path3.join(this.appDir, "favicon.svg"), req, res, false);
422
+ });
423
+ this.route("/__file-content", async (req, res) => {
424
+ const url = new URL(req.url || "/", "http://localhost");
425
+ const rawPath = url.pathname.replace(/^\/__file-content\/?/, "");
426
+ if (!rawPath || rawPath === "/") {
427
+ res.writeHead(400);
428
+ res.end("missing path");
429
+ return;
430
+ }
431
+ const filePath = path3.join(this.root, decodeURIComponent(rawPath));
432
+ if (!filePath.startsWith(this.root + path3.sep) && filePath !== this.root) {
366
433
  res.writeHead(403);
367
- res.end("forbidden");
434
+ res.end("Forbidden");
368
435
  return;
369
436
  }
370
- res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
371
- res.end('{"ok":true}');
437
+ fs3.stat(filePath, (err) => {
438
+ if (err) {
439
+ res.writeHead(404);
440
+ return res.end("Not found");
441
+ }
442
+ const ext = path3.extname(filePath).toLowerCase();
443
+ const ct = MIME[ext] ?? "application/octet-stream";
444
+ res.writeHead(200, { "Content-Type": ct, "Cache-Control": "no-cache" });
445
+ fs3.createReadStream(filePath).pipe(res);
446
+ });
447
+ });
448
+ this.route("/__stop", async (req, res) => {
449
+ if (!guarded(req, res, this.stopToken)) return;
450
+ json(res, { ok: true });
372
451
  this.stop();
373
452
  });
374
453
  this.start(config.port);
375
454
  }
376
- route(path13, handler) {
377
- this.routes.set(path13, handler);
378
- this.ctx.effect(() => () => this.routes.delete(path13));
455
+ route(path12, handler) {
456
+ this.routes.set(path12, handler);
457
+ this.ctx.effect(() => () => this.routes.delete(path12));
379
458
  }
380
- sse(path13, onConnect) {
381
- this.sses.set(path13, onConnect);
382
- this.ctx.effect(() => () => this.sses.delete(path13));
459
+ sse(path12, onConnect) {
460
+ this.sses.set(path12, onConnect);
461
+ this.ctx.effect(() => () => this.sses.delete(path12));
383
462
  }
384
463
  static(prefix, dir) {
385
464
  this.prefixStatic.set(prefix, dir);
@@ -484,6 +563,12 @@ var ServerService = class extends Service {
484
563
  rh(req, res);
485
564
  return;
486
565
  }
566
+ for (const [rp, rh2] of this.routes) {
567
+ if (url.startsWith(rp + "/")) {
568
+ rh2(req, res);
569
+ return;
570
+ }
571
+ }
487
572
  const sh = this.sses.get(url);
488
573
  if (sh) {
489
574
  res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" });
@@ -532,14 +617,16 @@ var ServerService = class extends Service {
532
617
  } else throw err;
533
618
  });
534
619
  this.server.listen(port, "127.0.0.1", () => {
535
- const u = `http://localhost:${port}`;
620
+ const addr = this.server?.address();
621
+ const bound = typeof addr === "object" && addr ? addr.port : port;
622
+ const u = `http://localhost:${bound}`;
536
623
  const target = this.page ? `${u}#${this.page}` : u;
537
624
  console.log(`[zdashboard] v${VERSION} dashboard -> ${u}`);
538
625
  console.log(`[zdashboard] project -> ${this.root}`);
539
- console.log(`[zdashboard] detect -> openspec:${this.det.hasOpenspec} docs:${this.det.hasDocs} just:${this.det.hasJust} bugs:${this.det.hasBugs}`);
626
+ console.log(`[zdashboard] detect -> openspec:${this.det.hasOpenspec} docs:${this.det.hasDocs} just:${this.det.hasJust}`);
540
627
  if (this.dataDir) console.log(`[zdashboard] data -> ${this.dataDir}`);
541
628
  if (this.open) openUrl(target);
542
- this.onListen?.(port);
629
+ this.onListen?.(bound);
543
630
  });
544
631
  }
545
632
  /** 项目 git 信息(分支/脏文件数),启动探测一次,文件变更时刷新 */
@@ -594,6 +681,12 @@ var ServerService = class extends Service {
594
681
  import fs4 from "fs";
595
682
  import { Service as Service2 } from "cordis";
596
683
  var WATCH_DEBOUNCE_MS = 300;
684
+ var IGNORED_DIR_RE = /^(?:\.git|node_modules|dist|\.pnpm)(?:[/\\]|$)/;
685
+ var IGNORED_FILE_RE = /\.(?:swp|tmp|log)$|~$|^\.DS_Store$|^Thumbs\.db$/i;
686
+ function isIgnoredPath(filename) {
687
+ if (!filename) return false;
688
+ return IGNORED_DIR_RE.test(filename) || IGNORED_FILE_RE.test(filename);
689
+ }
597
690
  var ReloadService = class extends Service2 {
598
691
  static inject = ["server"];
599
692
  clients = /* @__PURE__ */ new Set();
@@ -608,8 +701,7 @@ var ReloadService = class extends Service2 {
608
701
  });
609
702
  try {
610
703
  this.watcher = fs4.watch(config.root, { recursive: true }, (eventType, filename) => {
611
- if (filename && /^(?:\.git|node_modules|dist|\.pnpm)(?:[/\\]|$)/.test(filename)) return;
612
- if (filename && /\.(?:swp|tmp)$|~$|^\.DS_Store$|^Thumbs\.db$/i.test(filename)) return;
704
+ if (isIgnoredPath(filename)) return;
613
705
  if (this.timer) clearTimeout(this.timer);
614
706
  this.timer = setTimeout(() => {
615
707
  this.ctx.server.refreshGitInfo();
@@ -633,6 +725,10 @@ data: ${JSON.stringify(data == null ? "" : data)}
633
725
  }
634
726
  }
635
727
  }
728
+ /** 插件专用频道:plugin:<mode>:<event>,客户端 usePluginData subscribe 消费 */
729
+ broadcastPlugin(mode, event, data = "") {
730
+ this.broadcast(`plugin:${mode}:${event}`, data);
731
+ }
636
732
  dispose() {
637
733
  if (this.watcher) {
638
734
  try {
@@ -661,7 +757,7 @@ import path5 from "path";
661
757
  import fs5 from "fs";
662
758
  import path4 from "path";
663
759
  function walkDir(root, opts = {}) {
664
- const { skip = /* @__PURE__ */ new Set(), maxDepth, showHidden, onFile, onDir } = opts;
760
+ const { skip = /* @__PURE__ */ new Set(), maxDepth, onFile, onDir } = opts;
665
761
  function walk(dir, rel, depth) {
666
762
  if (typeof maxDepth === "number" && depth > maxDepth) return;
667
763
  let ents;
@@ -671,7 +767,7 @@ function walkDir(root, opts = {}) {
671
767
  return;
672
768
  }
673
769
  for (const ent of ents) {
674
- if (!showHidden && ent.name.startsWith(".")) continue;
770
+ if (ent.name.startsWith(".")) continue;
675
771
  if (skip.has(ent.name)) continue;
676
772
  const abs = path4.join(dir, ent.name);
677
773
  const r = rel ? `${rel}/${ent.name}` : ent.name;
@@ -725,98 +821,53 @@ function buildTree(paths, defaultExpandDepth = 2) {
725
821
  sortNodes(root);
726
822
  return root;
727
823
  }
728
- function scanTree(root, hasOpenspec, hasDocs, opts) {
729
- const tree = [];
730
- const defaultExpandDepth = typeof opts?.defaultExpandDepth === "number" ? opts.defaultExpandDepth : 2;
731
- if (hasOpenspec && fs6.existsSync(path5.join(root, "openspec", "changes"))) {
732
- const changesDir = path5.join(root, "openspec", "changes");
733
- const active = [];
734
- const archived = [];
735
- for (const ent of fs6.readdirSync(changesDir, { withFileTypes: true })) {
736
- if (!ent.isDirectory() || ent.name.startsWith(".") || ent.name === "archive") continue;
737
- const relBase = `openspec/changes/${ent.name}`;
738
- const paths = [];
739
- walkDir(path5.join(changesDir, ent.name), { maxDepth: 4, showHidden: opts?.showHidden, onFile: (_, rel) => paths.push(rel ? `${relBase}/${rel}` : relBase) });
740
- const children = buildTree(paths, defaultExpandDepth).map((n) => ({ ...n, path: n.path ? `${relBase}/${n.path}` : void 0 }));
741
- active.push({ name: ent.name, kind: "dir", children });
742
- }
743
- active.sort((a, b) => a.name.localeCompare(b.name));
744
- const archiveDir = path5.join(changesDir, "archive");
745
- if (fs6.existsSync(archiveDir)) {
746
- for (const ent of fs6.readdirSync(archiveDir, { withFileTypes: true })) {
747
- if (!ent.isDirectory() || ent.name.startsWith(".")) continue;
748
- const relBase = `openspec/changes/archive/${ent.name}`;
749
- const paths = [];
750
- walkDir(path5.join(archiveDir, ent.name), { maxDepth: 4, showHidden: opts?.showHidden, onFile: (_, rel) => paths.push(rel ? `${relBase}/${rel}` : relBase) });
751
- const children = buildTree(paths, defaultExpandDepth).map((n) => ({ ...n, path: n.path ? `${relBase}/${n.path}` : void 0 }));
752
- archived.push({ name: ent.name, kind: "dir", children });
753
- }
754
- archived.sort((a, b) => b.name.localeCompare(a.name));
824
+ function prefixPaths(nodes, prefix) {
825
+ return nodes.map((n) => {
826
+ if (n.kind === "file" && n.path) {
827
+ return { ...n, path: `${prefix}/${n.path}` };
755
828
  }
756
- if (active.length) tree.push({ name: `changes (${active.length})`, kind: "dir", children: active });
757
- if (archived.length) tree.push({ name: `archive (${archived.length})`, kind: "dir", defaultCollapsed: true, children: archived });
758
- const specsDir = path5.join(root, "openspec", "specs");
759
- if (fs6.existsSync(specsDir)) {
760
- const paths = [];
761
- walkDir(specsDir, { maxDepth: 4, showHidden: opts?.showHidden, onFile: (_, rel) => paths.push(`openspec/specs/${rel}`) });
762
- const specs = buildTree(paths, defaultExpandDepth).map((n) => ({ ...n, path: n.path ? `openspec/specs/${n.path}` : void 0 }));
763
- if (specs.length) tree.push({ name: "specs", kind: "dir", children: specs });
829
+ if (n.children) {
830
+ return { ...n, children: prefixPaths(n.children, prefix) };
764
831
  }
765
- }
766
- if (hasDocs && fs6.existsSync(path5.join(root, "docs"))) {
832
+ return n;
833
+ });
834
+ }
835
+ function scanTree(root, scanDirs, opts) {
836
+ const tree = [];
837
+ const defaultExpandDepth = typeof opts?.defaultExpandDepth === "number" ? opts.defaultExpandDepth : 2;
838
+ const dirs = Array.isArray(scanDirs) ? scanDirs.filter((d) => typeof d === "string" && d) : [];
839
+ for (const dir of dirs) {
840
+ const dirPath = path5.join(root, dir);
841
+ if (!fs6.existsSync(dirPath)) continue;
767
842
  const paths = [];
768
- walkDir(path5.join(root, "docs"), { maxDepth: 4, showHidden: opts?.showHidden, onFile: (_, rel) => paths.push(`docs/${rel}`) });
769
- const docs = buildTree(paths, defaultExpandDepth).map((n) => ({ ...n, path: n.path ? `docs/${n.path}` : void 0 }));
770
- if (docs.length) tree.push({ name: "docs", kind: "dir", children: docs });
771
- }
772
- const skip = /* @__PURE__ */ new Set(["openspec", "docs", "node_modules", ".git", "dist", "playground", ".zreview"]);
773
- if (Array.isArray(opts?.hiddenDirs)) {
774
- for (const d of opts.hiddenDirs) skip.add(d);
775
- }
776
- const etc = [];
777
- try {
778
- for (const ent of fs6.readdirSync(root, { withFileTypes: true })) {
779
- if (!opts?.showHidden && ent.name.startsWith(".")) continue;
780
- if (skip.has(ent.name)) continue;
781
- const ext = path5.extname(ent.name).toLowerCase();
782
- if (ent.isFile() && (ext === ".md" || ext === ".markdown")) etc.push({ name: ent.name, kind: "file", path: ent.name });
843
+ walkDir(dirPath, { maxDepth: 4, onFile: (_, rel) => paths.push(rel) });
844
+ const prefix = dir;
845
+ const children = prefixPaths(buildTree(paths, defaultExpandDepth), prefix);
846
+ if (children.length) {
847
+ tree.push({ name: `${dir} (${children.length})`, kind: "dir", children });
783
848
  }
784
- } catch (e) {
785
- console.error("[zdashboard] scan root etc failed:", e);
786
849
  }
787
- etc.sort((a, b) => a.name.localeCompare(b.name));
788
- if (etc.length) tree.push({ name: `other (${etc.length})`, kind: "dir", children: etc });
789
850
  return tree;
790
851
  }
791
852
 
792
853
  // src/core/tree.ts
854
+ var CONVENTION_SCAN_DIRS = ["openspec", "docs"];
793
855
  var apply = {
794
- inject: ["server", "dashboard"],
856
+ inject: ["server"],
795
857
  apply(ctx, config) {
796
- let cached = null;
797
- const getDet = async () => {
798
- if (cached?.v) return cached.v;
799
- if (!cached) cached = { p: detect(config.root).catch(() => ({ hasOpenspec: false, hasDocs: false, hasJust: false, hasBugs: false })) };
800
- const d = await cached.p;
801
- cached.v = d;
802
- return d;
803
- };
804
858
  const server = ctx.server;
805
859
  if (server?.route) {
806
- server.route("/__files", async (_req, res) => {
860
+ server.route("/__files", async (req, res) => {
807
861
  try {
808
- const d = await getDet();
809
- const viewCfg = ctx.dashboard.getConfig("view");
810
- const tree = scanTree(config.root, d.hasOpenspec, d.hasDocs, {
811
- hiddenDirs: Array.isArray(viewCfg?.hiddenDirs) ? viewCfg.hiddenDirs : void 0,
812
- defaultExpandDepth: typeof viewCfg?.defaultExpandDepth === "number" ? viewCfg.defaultExpandDepth : void 0,
813
- showHidden: typeof viewCfg?.showHidden === "boolean" ? viewCfg.showHidden : void 0
814
- });
862
+ const url = new URL(req.url || "/", "http://localhost");
863
+ const wt = url.searchParams.get("wt");
864
+ const scanRoot = wt ? decodeURIComponent(wt) : config.root;
865
+ const tree = scanTree(scanRoot, [...CONVENTION_SCAN_DIRS]);
815
866
  res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
816
- res.end(JSON.stringify({ tree, ...d }));
867
+ res.end(JSON.stringify({ tree }));
817
868
  } catch {
818
869
  res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
819
- res.end(JSON.stringify({ tree: [], hasOpenspec: false, hasDocs: false, hasJust: false, hasBugs: false }));
870
+ res.end(JSON.stringify({ tree: [] }));
820
871
  }
821
872
  });
822
873
  }
@@ -905,7 +956,7 @@ var apply2 = {
905
956
  import { Service as Service3 } from "cordis";
906
957
 
907
958
  // src/core/read-body.ts
908
- async function readBody2(req) {
959
+ async function readBody(req) {
909
960
  return new Promise((resolve) => {
910
961
  let data = "";
911
962
  req.on("data", (chunk) => {
@@ -952,7 +1003,7 @@ var DashboardService = class extends Service3 {
952
1003
  return;
953
1004
  }
954
1005
  try {
955
- const body = JSON.parse(await readBody2(req));
1006
+ const body = JSON.parse(await readBody(req));
956
1007
  const merged = {};
957
1008
  for (const mode of Object.keys(body)) {
958
1009
  merged[mode] = body[mode] ?? {};
@@ -977,9 +1028,9 @@ var DashboardService = class extends Service3 {
977
1028
  res.end('{"error":"method not allowed"}');
978
1029
  });
979
1030
  }
980
- register(manifest) {
981
- this.plugins.set(manifest.mode, manifest);
982
- this.ctx.effect(() => () => this.plugins.delete(manifest.mode));
1031
+ register(manifest6) {
1032
+ this.plugins.set(manifest6.mode, manifest6);
1033
+ this.ctx.effect(() => () => this.plugins.delete(manifest6.mode));
983
1034
  }
984
1035
  list() {
985
1036
  return Array.from(this.plugins.values());
@@ -994,7 +1045,16 @@ var DashboardService = class extends Service3 {
994
1045
 
995
1046
  // src/server/just-runner.ts
996
1047
  import { spawn, execFile as execFile5 } from "child_process";
1048
+ import iconv from "iconv-lite";
997
1049
  var MAX_BUFFER = 1e3;
1050
+ var utf8Strict = new TextDecoder("utf-8", { fatal: true });
1051
+ function decodeLine(buf) {
1052
+ try {
1053
+ return utf8Strict.decode(buf);
1054
+ } catch {
1055
+ return iconv.decode(buf, "gbk");
1056
+ }
1057
+ }
998
1058
  var JustRunner = class {
999
1059
  cwd;
1000
1060
  tasks = /* @__PURE__ */ new Map();
@@ -1046,7 +1106,7 @@ var JustRunner = class {
1046
1106
  /** 启动 recipe:同名先停旧进程(重启语义),不影响其他任务;调用方须先用 recipes() 校验名字 */
1047
1107
  start(recipe) {
1048
1108
  this.killOne(recipe);
1049
- const task = { recipe, child: null, state: "running", code: null, startedAt: Date.now(), buffer: [], pending: "" };
1109
+ const task = { recipe, child: null, state: "running", code: null, startedAt: Date.now(), buffer: [], pending: Buffer.alloc(0) };
1050
1110
  this.tasks.set(recipe, task);
1051
1111
  this.emit({ type: "clear", recipe });
1052
1112
  this.emit({ type: "state", recipe, state: "running", code: null, startedAt: task.startedAt });
@@ -1066,12 +1126,12 @@ var JustRunner = class {
1066
1126
  const isStale = () => this.tasks.get(recipe) !== task;
1067
1127
  const push = (d) => {
1068
1128
  if (isStale()) return;
1069
- task.pending += d.toString();
1129
+ task.pending = Buffer.concat([task.pending, d]);
1070
1130
  let idx;
1071
- while ((idx = task.pending.indexOf("\n")) >= 0) {
1072
- const line = task.pending.slice(0, idx + 1);
1073
- task.pending = task.pending.slice(idx + 1);
1074
- this.pushLine(task, line);
1131
+ while ((idx = task.pending.indexOf(10)) >= 0) {
1132
+ const line = task.pending.subarray(0, idx + 1);
1133
+ task.pending = task.pending.subarray(idx + 1);
1134
+ this.pushLine(task, decodeLine(line));
1075
1135
  }
1076
1136
  };
1077
1137
  child.stdout?.on("data", push);
@@ -1081,10 +1141,10 @@ var JustRunner = class {
1081
1141
  `);
1082
1142
  });
1083
1143
  child.on("exit", (code, signal) => {
1084
- if (task.pending && !isStale()) {
1085
- this.pushLine(task, task.pending + "\n");
1144
+ if (task.pending.length > 0 && !isStale()) {
1145
+ this.pushLine(task, decodeLine(task.pending) + "\n");
1086
1146
  }
1087
- task.pending = "";
1147
+ task.pending = Buffer.alloc(0);
1088
1148
  task.child = null;
1089
1149
  task.state = "exited";
1090
1150
  task.code = code ?? 0;
@@ -1112,7 +1172,7 @@ var JustRunner = class {
1112
1172
  const task = this.tasks.get(recipe);
1113
1173
  if (!task) return;
1114
1174
  task.buffer = [];
1115
- task.pending = "";
1175
+ task.pending = Buffer.alloc(0);
1116
1176
  this.emit({ type: "clear", recipe });
1117
1177
  }
1118
1178
  killOne(recipe) {
@@ -1131,498 +1191,202 @@ var JustRunner = class {
1131
1191
  }
1132
1192
  };
1133
1193
 
1134
- // src/plugins/just/index.ts
1135
- var apply3 = {
1136
- inject: ["server", "dashboard"],
1137
- apply(ctx, config) {
1138
- const root = config.root;
1139
- ctx.inject(["server"], () => {
1140
- if (!ctx.server?.route) return;
1141
- const runner = new JustRunner(root);
1142
- ctx.effect(() => () => runner.stop());
1143
- ctx.dashboard.register({ mode: "just", label: "Just Runner", icon: "\u{1F4DC}", description: "Just \u591A\u4EFB\u52A1\u5E76\u53D1\u6267\u884C\u4E0E\u65E5\u5FD7" });
1144
- ctx.server.route("/__just/recipes", async (_req, res) => {
1145
- res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
1146
- try {
1147
- const recipes = await runner.recipes();
1148
- res.end(JSON.stringify(recipes));
1149
- } catch {
1150
- res.end(JSON.stringify([]));
1194
+ // src/sdk/server.ts
1195
+ function definePlugin(def) {
1196
+ const { manifest: manifest6, setup } = def;
1197
+ return {
1198
+ inject: ["server", "dashboard", "reload"],
1199
+ apply(ctx) {
1200
+ const disposers = [];
1201
+ ctx.effect(() => () => {
1202
+ for (const fn of disposers.splice(0)) {
1203
+ try {
1204
+ fn();
1205
+ } catch (e) {
1206
+ console.error(`[zdashboard] plugin ${manifest6.mode} dispose error:`, e);
1207
+ }
1151
1208
  }
1152
1209
  });
1153
- ctx.server.sse("/__just/logs", (res) => {
1154
- const unsub = runner.subscribe((ev) => {
1155
- res.write(`data: ${JSON.stringify(ev)}
1156
-
1157
- `);
1158
- });
1159
- return unsub;
1160
- });
1161
- const handleAction = async (req, res, act) => {
1162
- if (req.headers["x-stop-token"] !== ctx.server.stopToken) {
1163
- res.writeHead(403);
1164
- res.end("forbidden");
1165
- return;
1166
- }
1167
- const body = await readBody2(req);
1168
- let recipe;
1210
+ ctx.dashboard.register(manifest6);
1211
+ const respondWith = (handler) => async (req, res) => {
1212
+ if (res.headersSent) return;
1169
1213
  try {
1170
- recipe = JSON.parse(body || "{}").recipe;
1171
- } catch {
1172
- }
1173
- if (act === "clear") {
1174
- if (recipe) runner.clear(recipe);
1175
- res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
1176
- res.end('{"ok":true}');
1177
- return;
1178
- }
1179
- if (act !== "stop") {
1180
- if (!recipe) {
1181
- res.writeHead(400);
1182
- res.end('{"error":"no recipe"}');
1183
- return;
1184
- }
1185
- const recipes = await runner.recipes();
1186
- if (!recipes.some((r) => r.name === recipe)) {
1187
- res.writeHead(403);
1188
- res.end('{"error":"unknown recipe"}');
1189
- return;
1190
- }
1191
- runner.start(recipe);
1192
- } else {
1193
- runner.stop(recipe || void 0);
1214
+ const data = await handler(req, res);
1215
+ if (data !== void 0 && !res.writableEnded) json(res, data);
1216
+ } catch (e) {
1217
+ console.error(`[zdashboard] plugin ${manifest6.mode} route error:`, e);
1218
+ if (!res.headersSent) json(res, { error: "internal" }, 500);
1194
1219
  }
1195
- res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
1196
- res.end(JSON.stringify(runner.list()));
1197
1220
  };
1198
- const routeAction = (name) => {
1199
- ctx.server.route(`/__just/${name}`, (req, res) => {
1200
- handleAction(req, res, name).catch(() => {
1201
- if (!res.headersSent) {
1202
- res.writeHead(500);
1203
- res.end('{"error":"internal"}');
1204
- }
1221
+ const pctx = {
1222
+ mode: manifest6.mode,
1223
+ config() {
1224
+ const schema = manifest6.config ?? {};
1225
+ const stored = ctx.server.getPluginConfig(manifest6.mode) ?? {};
1226
+ const defaults = Object.fromEntries(
1227
+ Object.entries(schema).map(([key, field]) => [key, field.default])
1228
+ );
1229
+ return { ...defaults, ...stored };
1230
+ },
1231
+ route(path12, handler) {
1232
+ ctx.server.route(path12, respondWith(handler));
1233
+ },
1234
+ guardedRoute(path12, handler) {
1235
+ const token = () => ctx.server.stopToken;
1236
+ ctx.server.route(path12, (req, res) => {
1237
+ if (!guarded(req, res, token())) return;
1238
+ void respondWith(handler)(req, res);
1205
1239
  });
1206
- });
1240
+ },
1241
+ sse(path12, handler) {
1242
+ ctx.server.sse(path12, handler);
1243
+ },
1244
+ static(prefix, dir) {
1245
+ ctx.server.static(prefix, dir);
1246
+ },
1247
+ broadcast(event, data) {
1248
+ ctx.reload?.broadcast(`plugin:${manifest6.mode}:${event}`, data ?? "");
1249
+ },
1250
+ onDispose(fn) {
1251
+ disposers.push(fn);
1252
+ }
1207
1253
  };
1208
- routeAction("start");
1209
- routeAction("stop");
1210
- routeAction("restart");
1211
- routeAction("clear");
1212
- });
1213
- }
1214
- };
1215
-
1216
- // src/server/api/fetch.ts
1217
- import ky from "ky";
1218
-
1219
- // src/server/errors.ts
1220
- var HttpError = class extends Error {
1221
- constructor(status, message, body) {
1222
- super(message);
1223
- this.status = status;
1224
- this.body = body;
1225
- this.name = "HttpError";
1226
- }
1227
- status;
1228
- body;
1229
- };
1230
- var NetworkError = class extends Error {
1231
- constructor(message, cause) {
1232
- super(message);
1233
- this.cause = cause;
1234
- this.name = "NetworkError";
1235
- }
1236
- cause;
1237
- };
1238
-
1239
- // src/server/api/fetch.ts
1240
- async function fetchJson(url, init) {
1241
- try {
1242
- const res = await ky(url, { ...init, timeout: 8e3, retry: 2 });
1243
- return await res.json();
1244
- } catch (e) {
1245
- if (e instanceof HttpError) throw e;
1246
- if (e instanceof Error && e.name === "TimeoutError") {
1247
- throw new NetworkError(`\u8BF7\u6C42\u8D85\u65F6: ${url}`);
1248
- }
1249
- if (e instanceof Error && e.name === "HTTPError") {
1250
- throw new HttpError(e.status ?? 500, e.message);
1254
+ setup(pctx);
1251
1255
  }
1252
- throw new NetworkError(`\u8BF7\u6C42\u5931\u8D25: ${url}`, e);
1253
- }
1256
+ };
1254
1257
  }
1255
1258
 
1256
- // src/server/bugs.ts
1257
- var tokenCache = null;
1258
- async function getToken(cfg) {
1259
- if (cfg.token) return cfg.token;
1260
- const key = `${cfg.url}|${cfg.account}`;
1261
- if (tokenCache && tokenCache.key === key && Date.now() - tokenCache.at < 10 * 6e4) return tokenCache.token;
1262
- const json = await fetchJson(`${cfg.url}/api.php/v1/tokens`, {
1263
- method: "POST",
1264
- headers: { "Content-Type": "application/json" },
1265
- body: JSON.stringify({ account: cfg.account, password: "" })
1259
+ // src/plugins/builtin.ts
1260
+ function defineBuiltin(def) {
1261
+ let currentRoot = "";
1262
+ const inner = definePlugin({
1263
+ manifest: def.manifest,
1264
+ setup(pctx) {
1265
+ def.setup(pctx, currentRoot);
1266
+ }
1266
1267
  });
1267
- const token = typeof json.token === "string" ? json.token : "";
1268
- if (!token) throw new Error("token \u83B7\u53D6\u5931\u8D25:\u68C0\u67E5 account / password");
1269
- tokenCache = { key, token, at: Date.now() };
1270
- return token;
1271
- }
1272
- function normBug(b, account) {
1273
- const assigned = b.assignedTo;
1274
- const assignedTo = typeof assigned === "string" ? assigned : assigned && typeof assigned === "object" && "realname" in assigned ? String(assigned.realname ?? "") : "";
1275
- const assignedAccount = typeof assigned === "string" ? assigned : assigned && typeof assigned === "object" ? String(assigned.account ?? "") : "";
1276
- const mine = !!account && (assignedAccount === account || assignedTo === account);
1277
1268
  return {
1278
- id: Number(b.id),
1279
- title: String(b.title ?? ""),
1280
- severity: b.severity ?? 4,
1281
- pri: b.pri ?? 3,
1282
- status: String(b.status ?? ""),
1283
- assignedTo,
1284
- openedBy: typeof b.openedBy === "string" ? b.openedBy : void 0,
1285
- mine
1269
+ inject: inner.inject,
1270
+ manifest: def.manifest,
1271
+ // cli 启动期死键剥离按 manifest 判定(config 声明/external)
1272
+ apply(ctx, config) {
1273
+ currentRoot = config?.root ?? "";
1274
+ inner.apply(ctx, config);
1275
+ }
1286
1276
  };
1287
1277
  }
1288
- async function fetchBugs(config) {
1289
- if (!config.url || !config.product) {
1290
- return { ok: false, error: "\u914D\u7F6E\u65E0\u6548\uFF1A\u8BF7\u8BBE\u7F6E\u670D\u52A1\u5668 URL \u548C\u4EA7\u54C1 ID" };
1291
- }
1292
- try {
1293
- const token = await getToken(config);
1294
- const json = await fetchJson(
1295
- `${config.url}/api.php/v1/products/${config.product}/bugs?page=1&limit=100`,
1296
- { headers: { Token: token } }
1297
- );
1298
- const raw = Array.isArray(json.bugs) ? json.bugs : [];
1299
- return { ok: true, url: config.url, total: Number(json.total ?? raw.length), bugs: raw.map((b) => normBug(b, config.account)) };
1300
- } catch (e) {
1301
- const msg = e instanceof Error ? e.message : String(e);
1302
- return { ok: false, error: `\u7985\u9053\u8BF7\u6C42\u5931\u8D25(${msg})\u2014\u2014\u68C0\u67E5 url / \u51ED\u636E / \u662F\u5426\u5F00\u542F RESTful API v1` };
1303
- }
1304
- }
1305
1278
 
1306
- // src/plugins/bugs/index.ts
1307
- var apply4 = {
1308
- inject: ["server", "dashboard"],
1309
- apply(ctx, config) {
1310
- const root = config.root;
1311
- ctx.inject(["server"], () => {
1312
- if (!ctx.server?.route) return;
1313
- ctx.dashboard.register({
1314
- mode: "bugs",
1315
- label: "\u7985\u9053 Bugs",
1316
- icon: "\u{1F3AF}",
1317
- description: "\u53EA\u8BFB bug \u5217\u8868",
1318
- config: {
1319
- url: { type: "string", label: "\u670D\u52A1\u5668 URL", default: "" },
1320
- account: { type: "string", label: "\u8D26\u53F7", default: "" },
1321
- token: { type: "string", label: "Token", default: "" },
1322
- product: { type: "number", label: "\u4EA7\u54C1 ID", default: 0 }
1323
- }
1324
- });
1325
- ctx.server.route("/__bugs", async (_req, res) => {
1326
- res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
1327
- try {
1328
- const bugConfig = ctx.dashboard.getConfig("bugs");
1329
- const result = await fetchBugs({
1330
- url: String(bugConfig?.url ?? ""),
1331
- account: String(bugConfig?.account ?? ""),
1332
- token: String(bugConfig?.token ?? ""),
1333
- product: Number(bugConfig?.product ?? 0)
1334
- });
1335
- res.end(JSON.stringify(result));
1336
- } catch (e) {
1337
- res.end(JSON.stringify({ ok: false, error: e instanceof Error ? e.message : "unknown error" }));
1338
- }
1339
- });
1340
- });
1341
- }
1279
+ // src/plugins/just/manifest.ts
1280
+ var manifest = {
1281
+ mode: "just",
1282
+ label: "Just Runner",
1283
+ icon: "\u{1F4DC}",
1284
+ description: "Just \u4EFB\u52A1\u65E5\u5FD7\u4E0E\u6267\u884C",
1285
+ order: 50
1342
1286
  };
1343
1287
 
1344
- // src/server/review-store.ts
1345
- import fs7 from "fs";
1346
- import path6 from "path";
1347
- import YAML from "yaml";
1348
- var REVIEW_DIR = ".zreview";
1349
- var REVIEW_FILE = path6.join(REVIEW_DIR, "review.yaml");
1350
- var DOCS_DIR = path6.join(REVIEW_DIR, "docs");
1351
- var DIAGRAMS_DIR = path6.join(REVIEW_DIR, "diagrams");
1352
- var ReviewStore = class {
1353
- root;
1354
- onChange;
1355
- constructor(root, onChange) {
1356
- this.root = root;
1357
- this.onChange = onChange;
1358
- }
1359
- exists() {
1360
- return fs7.existsSync(path6.join(this.root, REVIEW_FILE));
1361
- }
1362
- read() {
1363
- const fullPath = path6.join(this.root, REVIEW_FILE);
1364
- if (!fs7.existsSync(fullPath)) {
1365
- return { status: "draft", summary: "", documents: [], codebases: [], diagrams: [], items: [] };
1366
- }
1367
- try {
1368
- const parsed = YAML.parse(fs7.readFileSync(fullPath, "utf8"));
1369
- if (!parsed || !Array.isArray(parsed.items)) {
1370
- return { status: "draft", summary: parsed?.summary ?? "", documents: parsed?.documents ?? [], codebases: parsed?.codebases ?? [], diagrams: parsed?.diagrams ?? [], items: [] };
1288
+ // src/plugins/just/index.ts
1289
+ var apply3 = defineBuiltin({
1290
+ manifest,
1291
+ setup(ctx, root) {
1292
+ const runner = new JustRunner(root);
1293
+ ctx.onDispose(() => runner.stop());
1294
+ ctx.route("/__just/recipes", async () => {
1295
+ try {
1296
+ return await runner.recipes();
1297
+ } catch {
1298
+ return [];
1371
1299
  }
1372
- return {
1373
- status: parsed.status ?? "draft",
1374
- summary: parsed.summary ?? "",
1375
- documents: parsed.documents ?? [],
1376
- codebases: parsed.codebases ?? [],
1377
- diagrams: parsed.diagrams ?? [],
1378
- items: parsed.items
1379
- };
1380
- } catch {
1381
- return { status: "draft", summary: "", documents: [], codebases: [], diagrams: [], items: [] };
1382
- }
1383
- }
1384
- write(data) {
1385
- fs7.mkdirSync(path6.join(this.root, REVIEW_DIR), { recursive: true });
1386
- fs7.writeFileSync(path6.join(this.root, REVIEW_FILE), YAML.stringify(data), "utf8");
1387
- this.onChange?.();
1388
- }
1389
- updateItem(id, patch) {
1390
- const data = this.read();
1391
- const item = this.findItem(data.items, id);
1392
- if (!item) throw new Error(`item ${id} not found`);
1393
- if (patch.answer !== void 0) item.answer = patch.answer;
1394
- if (patch.state !== void 0) item.state = patch.state;
1395
- if (patch.priority !== void 0) item.priority = patch.priority;
1396
- if (patch.title !== void 0) item.title = patch.title;
1397
- if (patch.state === "answered" && !patch.answer && !item.answer) item.answer = "";
1398
- this.write(data);
1399
- return data;
1400
- }
1401
- addChild(parentId, title, priority = "medium") {
1402
- const data = this.read();
1403
- const parent = this.findItem(data.items, parentId);
1404
- if (!parent) throw new Error(`parent ${parentId} not found`);
1405
- if (!parent.children) parent.children = [];
1406
- const newId = `${parentId}-${parent.children.length + 1}`;
1407
- parent.children.push({
1408
- id: newId,
1409
- type: "decomposition",
1410
- state: "open",
1411
- title,
1412
- priority
1413
1300
  });
1414
- this.write(data);
1415
- return data;
1416
- }
1417
- removeItem(id) {
1418
- const data = this.read();
1419
- const removed = this.removeItemFromList(data.items, id);
1420
- if (!removed) throw new Error(`item ${id} not found`);
1421
- this.write(data);
1422
- return data;
1423
- }
1424
- setStatus(status) {
1425
- const data = this.read();
1426
- if (status === "passed" && this.hasOpenItems(data.items)) {
1427
- throw new Error("\u5B58\u5728\u672A\u5904\u7406\u7684\u8BC4\u5BA1\u9879(open),\u4E0D\u80FD\u901A\u8FC7");
1428
- }
1429
- data.status = status;
1430
- this.write(data);
1431
- return data;
1432
- }
1433
- listDocs() {
1434
- const docsDir = path6.join(this.root, DOCS_DIR);
1435
- if (!fs7.existsSync(docsDir)) return [];
1436
- try {
1437
- return fs7.readdirSync(docsDir).filter((f) => /\.(md|markdown|txt)$/i.test(f)).sort();
1438
- } catch {
1439
- return [];
1440
- }
1441
- }
1442
- readDoc(name) {
1443
- const fullPath = path6.join(this.root, DOCS_DIR, name);
1444
- if (!fs7.existsSync(fullPath)) throw new Error(`doc ${name} not found`);
1445
- return fs7.readFileSync(fullPath, "utf8");
1446
- }
1447
- listDiagrams() {
1448
- const dir = path6.join(this.root, DIAGRAMS_DIR);
1449
- if (!fs7.existsSync(dir)) return [];
1450
- try {
1451
- return fs7.readdirSync(dir).filter((f) => /\.html$/i.test(f)).sort();
1452
- } catch {
1453
- return [];
1454
- }
1455
- }
1456
- readDiagram(name) {
1457
- const fullPath = path6.join(this.root, DIAGRAMS_DIR, name);
1458
- if (!fs7.existsSync(fullPath)) throw new Error(`diagram ${name} not found`);
1459
- return fs7.readFileSync(fullPath, "utf8");
1460
- }
1461
- findItem(items, id) {
1462
- for (const item of items) {
1463
- if (item.id === id) return item;
1464
- if (item.children) {
1465
- const found = this.findItem(item.children, id);
1466
- if (found) return found;
1467
- }
1468
- }
1469
- return null;
1470
- }
1471
- removeItemFromList(items, id) {
1472
- for (let i = 0; i < items.length; i++) {
1473
- if (items[i].id === id) {
1474
- items.splice(i, 1);
1475
- return true;
1476
- }
1477
- if (items[i].children && this.removeItemFromList(items[i].children, id)) {
1478
- return true;
1479
- }
1480
- }
1481
- return false;
1482
- }
1483
- hasOpenItems(items) {
1484
- for (const item of items) {
1485
- if (item.state === "open") return true;
1486
- if (item.children && this.hasOpenItems(item.children)) return true;
1487
- }
1488
- return false;
1489
- }
1490
- };
1491
-
1492
- // src/plugins/review/index.ts
1493
- var apply5 = {
1494
- inject: ["server", "dashboard", "reload"],
1495
- apply(ctx, config) {
1496
- const root = config.root;
1497
- ctx.inject(["server"], () => {
1498
- if (!ctx.server?.route) return;
1499
- ctx.dashboard.register({ mode: "review", label: "\u6587\u6863\u8BC4\u5BA1", icon: "\u2705", description: "\u591A\u6587\u6863\u9700\u6C42\u8BC4\u5BA1\u4E0E\u62C6\u89E3 \xB7 zreview" });
1500
- const reviewStore = new ReviewStore(root, () => {
1501
- ctx.reload.broadcast("files");
1502
- });
1503
- ctx.server.route("/__review", async (_req, res) => {
1504
- res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
1505
- res.end(JSON.stringify(reviewStore.read()));
1506
- });
1507
- ctx.server.route("/__review/item", async (req, res) => {
1508
- if (req.headers["x-stop-token"] !== ctx.server.stopToken) {
1509
- res.writeHead(403);
1510
- res.end("forbidden");
1511
- return;
1512
- }
1513
- try {
1514
- const body = JSON.parse(await readBody2(req) || "{}");
1515
- const data = reviewStore.updateItem(body.id, {
1516
- answer: body.answer,
1517
- state: body.state,
1518
- priority: body.priority,
1519
- title: body.title
1520
- });
1521
- res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
1522
- res.end(JSON.stringify(data));
1523
- } catch (e) {
1524
- res.writeHead(400, { "Content-Type": "application/json; charset=utf-8" });
1525
- res.end(JSON.stringify({ error: e instanceof Error ? e.message : "unknown error" }));
1526
- }
1527
- });
1528
- ctx.server.route("/__review/item/add-child", async (req, res) => {
1529
- if (req.headers["x-stop-token"] !== ctx.server.stopToken) {
1530
- res.writeHead(403);
1531
- res.end("forbidden");
1532
- return;
1533
- }
1534
- try {
1535
- const body = JSON.parse(await readBody2(req) || "{}");
1536
- const data = reviewStore.addChild(body.parentId, body.title, body.priority);
1537
- res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
1538
- res.end(JSON.stringify(data));
1539
- } catch (e) {
1540
- res.writeHead(400, { "Content-Type": "application/json; charset=utf-8" });
1541
- res.end(JSON.stringify({ error: e instanceof Error ? e.message : "unknown error" }));
1542
- }
1543
- });
1544
- ctx.server.route("/__review/item/remove", async (req, res) => {
1545
- if (req.headers["x-stop-token"] !== ctx.server.stopToken) {
1546
- res.writeHead(403);
1547
- res.end("forbidden");
1548
- return;
1549
- }
1301
+ ctx.route("/__just/tasks", async () => runner.list());
1302
+ ctx.sse("/__just/logs", (res) => {
1303
+ const unsub = runner.subscribe((ev) => {
1550
1304
  try {
1551
- const body = JSON.parse(await readBody2(req) || "{}");
1552
- const data = reviewStore.removeItem(body.id);
1553
- res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
1554
- res.end(JSON.stringify(data));
1555
- } catch (e) {
1556
- res.writeHead(400, { "Content-Type": "application/json; charset=utf-8" });
1557
- res.end(JSON.stringify({ error: e instanceof Error ? e.message : "unknown error" }));
1305
+ ctx.broadcast(ev.type, ev);
1306
+ } catch {
1558
1307
  }
1308
+ res.write(`data: ${JSON.stringify(ev)}
1309
+
1310
+ `);
1559
1311
  });
1560
- ctx.server.route("/__review/status", async (req, res) => {
1561
- if (req.headers["x-stop-token"] !== ctx.server.stopToken) {
1562
- res.writeHead(403);
1563
- res.end("forbidden");
1564
- return;
1565
- }
1566
- try {
1567
- const body = JSON.parse(await readBody2(req) || "{}");
1568
- const data = reviewStore.setStatus(body.status);
1569
- res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
1570
- res.end(JSON.stringify(data));
1571
- } catch (e) {
1312
+ return unsub;
1313
+ });
1314
+ const handleAction = async (req, res, act) => {
1315
+ const body = await readBody(req);
1316
+ let recipe;
1317
+ try {
1318
+ recipe = JSON.parse(body || "{}").recipe;
1319
+ } catch {
1320
+ }
1321
+ if (act === "clear") {
1322
+ if (recipe) runner.clear(recipe);
1323
+ return { ok: true };
1324
+ }
1325
+ if (act !== "stop") {
1326
+ if (!recipe) {
1572
1327
  res.writeHead(400, { "Content-Type": "application/json; charset=utf-8" });
1573
- res.end(JSON.stringify({ error: e instanceof Error ? e.message : "unknown error" }));
1328
+ res.end('{"error":"no recipe"}');
1329
+ return void 0;
1574
1330
  }
1575
- });
1576
- ctx.server.route("/__docs", async (_req, res) => {
1577
- res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
1578
- res.end(JSON.stringify(reviewStore.listDocs()));
1579
- });
1580
- ctx.server.route("/__docs/:name", async (req, res) => {
1581
- try {
1582
- const name = req.params?.name;
1583
- if (!name) throw new Error("missing doc name");
1584
- const content = reviewStore.readDoc(name);
1585
- res.writeHead(200, { "Content-Type": "text/markdown; charset=utf-8", "Cache-Control": "no-cache" });
1586
- res.end(content);
1587
- } catch (e) {
1588
- res.writeHead(404, { "Content-Type": "application/json; charset=utf-8" });
1589
- res.end(JSON.stringify({ error: e instanceof Error ? e.message : "not found" }));
1331
+ const recipes = await runner.recipes();
1332
+ if (!recipes.some((r) => r.name === recipe)) {
1333
+ res.writeHead(403, { "Content-Type": "application/json; charset=utf-8" });
1334
+ res.end('{"error":"unknown recipe"}');
1335
+ return void 0;
1590
1336
  }
1591
- });
1592
- });
1337
+ runner.start(recipe);
1338
+ } else {
1339
+ runner.stop(recipe || void 0);
1340
+ }
1341
+ return runner.list();
1342
+ };
1343
+ const routeAction = (name) => {
1344
+ ctx.guardedRoute(`/__just/${name}`, ((req, res) => handleAction(req, res, name)));
1345
+ };
1346
+ routeAction("start");
1347
+ routeAction("stop");
1348
+ routeAction("restart");
1349
+ routeAction("clear");
1593
1350
  }
1594
- };
1351
+ });
1595
1352
 
1596
1353
  // src/plugins/apply/scan.ts
1597
- import fs8 from "fs";
1598
- import path7 from "path";
1354
+ import fs7 from "fs";
1355
+ import path6 from "path";
1599
1356
 
1600
1357
  // src/plugins/apply/parse-tasks.ts
1601
1358
  var TASK_RE = /^\s*-\s*\[([ xX])\]\s*(.*)$/;
1359
+ var MANUAL_PREFIX = "\u{1F527}[\u4EBA\u5DE5]";
1602
1360
  function parseTasks(md) {
1603
- return md.split("\n").filter((l) => TASK_RE.test(l)).map((l) => {
1361
+ return md.split(/\r?\n/).filter((l) => TASK_RE.test(l)).map((l) => {
1604
1362
  const m = l.match(TASK_RE);
1605
- return { text: (m?.[2] ?? l).trim(), checked: m?.[1] !== " " };
1363
+ const text = (m?.[2] ?? l).trim();
1364
+ return { text, checked: m?.[1] !== " ", manual: text.startsWith(MANUAL_PREFIX) };
1606
1365
  });
1607
1366
  }
1608
1367
  function countTasks(md) {
1609
1368
  const tasks = parseTasks(md);
1610
- return { total: tasks.length, done: tasks.filter((t) => t.checked).length };
1369
+ const normal = tasks.filter((t) => !t.manual);
1370
+ return {
1371
+ total: normal.length,
1372
+ done: normal.filter((t) => t.checked).length,
1373
+ manual: tasks.length - normal.length
1374
+ };
1611
1375
  }
1612
1376
 
1613
1377
  // src/plugins/apply/scan.ts
1614
1378
  function readText(p) {
1615
1379
  try {
1616
- return fs8.readFileSync(p, "utf8");
1380
+ return fs7.readFileSync(p, "utf8");
1617
1381
  } catch {
1618
1382
  return "";
1619
1383
  }
1620
1384
  }
1621
1385
  function worktreeDir(root, name) {
1622
- return path7.join(root, ".zworktree", name);
1386
+ return path6.join(root, ".zworktree", name);
1623
1387
  }
1624
1388
  function changeDir(root, name) {
1625
- return path7.join(root, "openspec", "changes", name);
1389
+ return path6.join(root, "openspec", "changes", name);
1626
1390
  }
1627
1391
  function parseDependsOn(proposal) {
1628
1392
  const deps = [];
@@ -1636,42 +1400,42 @@ function parseDependsOn(proposal) {
1636
1400
  return deps;
1637
1401
  }
1638
1402
  function scanApplyChanges(root) {
1639
- const changesDir = path7.join(root, "openspec", "changes");
1640
- if (!fs8.existsSync(changesDir)) return [];
1403
+ const changesDir = path6.join(root, "openspec", "changes");
1404
+ if (!fs7.existsSync(changesDir)) return [];
1641
1405
  const out = [];
1642
- for (const ent of fs8.readdirSync(changesDir, { withFileTypes: true })) {
1406
+ for (const ent of fs7.readdirSync(changesDir, { withFileTypes: true })) {
1643
1407
  if (!ent.isDirectory() || ent.name.startsWith(".") || ent.name === "archive") continue;
1644
1408
  const wt = worktreeDir(root, ent.name);
1645
- const tasks = readText(path7.join(wt, "openspec", "changes", ent.name, "tasks.md"));
1409
+ const tasks = readText(path6.join(wt, "openspec", "changes", ent.name, "tasks.md"));
1646
1410
  if (!tasks) {
1647
- const fallback = readText(path7.join(changesDir, ent.name, "tasks.md"));
1411
+ const fallback = readText(path6.join(changesDir, ent.name, "tasks.md"));
1648
1412
  if (fallback) {
1649
1413
  const { total, done } = countTasks(fallback);
1650
- const wtChange = path7.join(wt, "openspec", "changes", ent.name);
1651
- const mainChange = path7.join(changesDir, ent.name);
1414
+ const wtChange = path6.join(wt, "openspec", "changes", ent.name);
1415
+ const mainChange = path6.join(changesDir, ent.name);
1652
1416
  out.push({
1653
1417
  name: ent.name,
1654
1418
  path: `openspec/changes/${ent.name}`,
1655
1419
  total,
1656
1420
  done,
1657
- hasProposal: fs8.existsSync(path7.join(wtChange, "proposal.md")) || fs8.existsSync(path7.join(mainChange, "proposal.md")),
1658
- hasDesign: fs8.existsSync(path7.join(wtChange, "design.md")) || fs8.existsSync(path7.join(mainChange, "design.md")),
1421
+ hasProposal: fs7.existsSync(path6.join(wtChange, "proposal.md")) || fs7.existsSync(path6.join(mainChange, "proposal.md")),
1422
+ hasDesign: fs7.existsSync(path6.join(wtChange, "design.md")) || fs7.existsSync(path6.join(mainChange, "design.md")),
1659
1423
  // worktree 目录存在即算执行中(tasks.md 可能尚未落盘)
1660
- inWorktree: fs8.existsSync(wtChange)
1424
+ inWorktree: fs7.existsSync(wtChange)
1661
1425
  });
1662
1426
  }
1663
1427
  } else {
1664
1428
  const { total, done } = countTasks(tasks);
1665
- const wtChange = path7.join(wt, "openspec", "changes", ent.name);
1666
- const mainChange = path7.join(changesDir, ent.name);
1429
+ const wtChange = path6.join(wt, "openspec", "changes", ent.name);
1430
+ const mainChange = path6.join(changesDir, ent.name);
1667
1431
  out.push({
1668
1432
  name: ent.name,
1669
1433
  path: `openspec/changes/${ent.name}`,
1670
1434
  total,
1671
1435
  done,
1672
1436
  // 任一侧存在即算(详情读取是 worktree 优先、主目录兜底,列表标记保持一致)
1673
- hasProposal: fs8.existsSync(path7.join(wtChange, "proposal.md")) || fs8.existsSync(path7.join(mainChange, "proposal.md")),
1674
- hasDesign: fs8.existsSync(path7.join(wtChange, "design.md")) || fs8.existsSync(path7.join(mainChange, "design.md")),
1437
+ hasProposal: fs7.existsSync(path6.join(wtChange, "proposal.md")) || fs7.existsSync(path6.join(mainChange, "proposal.md")),
1438
+ hasDesign: fs7.existsSync(path6.join(wtChange, "design.md")) || fs7.existsSync(path6.join(mainChange, "design.md")),
1675
1439
  inWorktree: true
1676
1440
  });
1677
1441
  }
@@ -1682,14 +1446,14 @@ function scanApplyChanges(root) {
1682
1446
  function readApplyChange(root, name) {
1683
1447
  const wt = worktreeDir(root, name);
1684
1448
  const mainChangeDir = changeDir(root, name);
1685
- const proposalFrom = fs8.existsSync(path7.join(wt, "openspec", "changes", name, "proposal.md")) ? path7.join(wt, "openspec", "changes", name, "proposal.md") : path7.join(mainChangeDir, "proposal.md");
1686
- const designFrom = fs8.existsSync(path7.join(wt, "openspec", "changes", name, "design.md")) ? path7.join(wt, "openspec", "changes", name, "design.md") : path7.join(mainChangeDir, "design.md");
1687
- const tasksFrom = fs8.existsSync(path7.join(wt, "openspec", "changes", name, "tasks.md")) ? path7.join(wt, "openspec", "changes", name, "tasks.md") : path7.join(mainChangeDir, "tasks.md");
1449
+ const proposalFrom = fs7.existsSync(path6.join(wt, "openspec", "changes", name, "proposal.md")) ? path6.join(wt, "openspec", "changes", name, "proposal.md") : path6.join(mainChangeDir, "proposal.md");
1450
+ const designFrom = fs7.existsSync(path6.join(wt, "openspec", "changes", name, "design.md")) ? path6.join(wt, "openspec", "changes", name, "design.md") : path6.join(mainChangeDir, "design.md");
1451
+ const tasksFrom = fs7.existsSync(path6.join(wt, "openspec", "changes", name, "tasks.md")) ? path6.join(wt, "openspec", "changes", name, "tasks.md") : path6.join(mainChangeDir, "tasks.md");
1688
1452
  const proposal = readText(proposalFrom);
1689
1453
  const design = readText(designFrom);
1690
1454
  const tasks = readText(tasksFrom);
1691
1455
  const { total, done } = countTasks(tasks);
1692
- const inWorktree = fs8.existsSync(path7.join(wt, "openspec", "changes", name));
1456
+ const inWorktree = fs7.existsSync(path6.join(wt, "openspec", "changes", name));
1693
1457
  const dependsOn = parseDependsOn(proposal);
1694
1458
  const hasTestStrategy = /^##\s+测试策略\s*$/m.test(design);
1695
1459
  return {
@@ -1708,43 +1472,106 @@ function readApplyChange(root, name) {
1708
1472
  };
1709
1473
  }
1710
1474
 
1475
+ // src/plugins/apply/batch.ts
1476
+ import fs8 from "fs";
1477
+ import path7 from "path";
1478
+ var RUN_ID_PATTERN = /^[A-Za-z0-9-]+$/;
1479
+ function isPlainObject(v) {
1480
+ return typeof v === "object" && v !== null && !Array.isArray(v);
1481
+ }
1482
+ function readText2(p) {
1483
+ try {
1484
+ return fs8.readFileSync(p, "utf8");
1485
+ } catch {
1486
+ return null;
1487
+ }
1488
+ }
1489
+ function readBatchState(root) {
1490
+ const applyDir = path7.join(root, ".zdev", "apply");
1491
+ const raw = readText2(path7.join(applyDir, "CURRENT"));
1492
+ const runId = raw?.trim() ?? "";
1493
+ if (!runId || !RUN_ID_PATTERN.test(runId)) {
1494
+ return { run: null, state: null };
1495
+ }
1496
+ const parsed = (() => {
1497
+ const text = readText2(path7.join(applyDir, "runs", runId, "state.json"));
1498
+ if (text == null) return null;
1499
+ try {
1500
+ return JSON.parse(text);
1501
+ } catch {
1502
+ return null;
1503
+ }
1504
+ })();
1505
+ if (!isPlainObject(parsed)) {
1506
+ return { run: { id: runId }, state: null };
1507
+ }
1508
+ return { run: { id: runId }, state: parsed };
1509
+ }
1510
+ function projectGraph(state) {
1511
+ if (!state) {
1512
+ return { changes: [], batches: [], conflicts: [] };
1513
+ }
1514
+ return {
1515
+ changes: state.changes.map((c) => ({
1516
+ name: c.name,
1517
+ status: c.status,
1518
+ dependencies: c.dependencies,
1519
+ batchIndex: c.batchIndex
1520
+ })),
1521
+ batches: state.batches,
1522
+ conflicts: state.conflicts
1523
+ };
1524
+ }
1525
+ var BATCH_LOGS_TAIL = 100;
1526
+ function tailLogs(state) {
1527
+ return (state?.logs ?? []).slice(-BATCH_LOGS_TAIL);
1528
+ }
1529
+ function readBatchPlan(root, runId) {
1530
+ return readText2(path7.join(root, ".zdev", "apply", "runs", runId, "plan.md"));
1531
+ }
1532
+
1533
+ // src/plugins/apply/manifest.ts
1534
+ var manifest2 = {
1535
+ mode: "apply",
1536
+ label: "\u6267\u884C\u8FDB\u5EA6",
1537
+ icon: "\u2699\uFE0F",
1538
+ description: "OpenSpec \u6267\u884C\u8FDB\u5EA6 \xB7 zapply \u5355 change \u4E0E\u6279\u91CF",
1539
+ order: 40
1540
+ };
1541
+
1711
1542
  // src/plugins/apply/index.ts
1712
- var apply6 = {
1713
- inject: ["server", "dashboard"],
1714
- apply(ctx, config) {
1715
- const root = config.root;
1716
- ctx.inject(["server"], () => {
1717
- if (!ctx.server?.route) return;
1718
- ctx.dashboard.register({ mode: "apply", label: "\u6267\u884C\u8FDB\u5EA6", icon: "\u2699\uFE0F", description: "OpenSpec change \u4EFB\u52A1\u8FDB\u5EA6" });
1719
- ctx.server.route("/__apply", async (_req, res) => {
1720
- res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
1721
- res.end(JSON.stringify(scanApplyChanges(root)));
1722
- });
1723
- ctx.server.route("/__apply/change", async (req, res) => {
1724
- const url = new URL(req.url || "", "http://x");
1725
- const name = url.searchParams.get("name");
1726
- if (!name) {
1727
- res.writeHead(400);
1728
- res.end(JSON.stringify({ error: "missing name" }));
1729
- return;
1730
- }
1731
- if (name.includes("..") || name.includes("/") || name.includes("\\")) {
1732
- res.writeHead(400);
1733
- res.end(JSON.stringify({ error: "invalid name" }));
1734
- return;
1735
- }
1736
- try {
1737
- const data = readApplyChange(root, name);
1738
- res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
1739
- res.end(JSON.stringify(data));
1740
- } catch (e) {
1741
- res.writeHead(400);
1742
- res.end(JSON.stringify({ error: e instanceof Error ? e.message : "unknown error" }));
1743
- }
1744
- });
1543
+ function errorResponse(res, status, msg) {
1544
+ res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
1545
+ res.end(JSON.stringify({ error: msg }));
1546
+ return void 0;
1547
+ }
1548
+ var apply4 = defineBuiltin({
1549
+ manifest: manifest2,
1550
+ setup(ctx, root) {
1551
+ ctx.route("/__apply", async () => scanApplyChanges(root));
1552
+ ctx.route("/__apply/change", async (req, res) => {
1553
+ const url = new URL(req.url || "", "http://x");
1554
+ const name = url.searchParams.get("name");
1555
+ const bad = (msg) => errorResponse(res, 400, msg);
1556
+ if (!name) return bad("missing name");
1557
+ if (name.includes("..") || name.includes("/") || name.includes("\\")) return bad("invalid name");
1558
+ try {
1559
+ return readApplyChange(root, name);
1560
+ } catch (e) {
1561
+ return bad(e instanceof Error ? e.message : "unknown error");
1562
+ }
1563
+ });
1564
+ ctx.route("/__apply/batch", async () => readBatchState(root));
1565
+ ctx.route("/__apply/batch/graph", async () => projectGraph(readBatchState(root).state));
1566
+ ctx.route("/__apply/batch/logs", async () => tailLogs(readBatchState(root).state));
1567
+ ctx.route("/__apply/batch/plan", async (_req, res) => {
1568
+ const snap = readBatchState(root);
1569
+ const plan = snap.run ? readBatchPlan(root, snap.run.id) : null;
1570
+ if (plan == null) return errorResponse(res, 404, "plan not found");
1571
+ return { plan };
1745
1572
  });
1746
1573
  }
1747
- };
1574
+ });
1748
1575
 
1749
1576
  // src/plugins/design/index.ts
1750
1577
  import path9 from "path";
@@ -1788,75 +1615,100 @@ function scanAssets(root, subDir = "") {
1788
1615
  return out;
1789
1616
  }
1790
1617
 
1618
+ // src/plugins/design/manifest.ts
1619
+ var manifest3 = {
1620
+ mode: "design",
1621
+ label: "\u8BBE\u8BA1\u8D44\u4EA7",
1622
+ icon: "\u{1F3A8}",
1623
+ description: "\u8BBE\u8BA1\u8D44\u4EA7\u5206\u7C7B\u6D4F\u89C8 \xB7 zdesign",
1624
+ order: 30
1625
+ };
1626
+
1791
1627
  // src/plugins/design/index.ts
1792
1628
  var ASSET_KEYS = ["page", "component", "icon", "token", "md", "video", "audio", "pdf", "font"];
1793
- function mergeScanResults(a, b) {
1629
+ function emptyScan() {
1794
1630
  const out = {};
1795
- for (const k of ASSET_KEYS) {
1796
- out[k] = [...a[k], ...b[k]];
1797
- }
1631
+ for (const k of ASSET_KEYS) out[k] = [];
1798
1632
  return out;
1799
1633
  }
1800
- var apply7 = {
1801
- inject: ["server", "dashboard"],
1802
- apply(ctx, config) {
1803
- const root = config.root;
1804
- ctx.inject(["server"], () => {
1805
- if (!ctx.server?.route) return;
1806
- ctx.dashboard.register({
1807
- mode: "design",
1808
- label: "\u8BBE\u8BA1\u8D44\u4EA7",
1809
- icon: "\u{1F3A8}",
1810
- description: "\u9875\u9762/\u7EC4\u4EF6/\u56FE\u6807/Token \u5206\u7C7B\u9884\u89C8",
1811
- config: {
1812
- folders: { type: "string[]", label: "\u626B\u63CF\u6587\u4EF6\u5939", default: [] }
1813
- }
1814
- });
1815
- ctx.server.route("/__design/assets", async (_req, res) => {
1816
- res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
1817
- const designConfig = ctx.dashboard.getConfig("design");
1818
- const folders = Array.isArray(designConfig?.folders) ? designConfig.folders : [];
1819
- if (folders.length === 0) {
1820
- res.end(JSON.stringify(scanAssets(root)));
1821
- return;
1634
+ var apply5 = defineBuiltin({
1635
+ manifest: manifest3,
1636
+ setup(ctx, root) {
1637
+ ctx.route("/__design/assets", async () => {
1638
+ const designRoot = path9.join(root, ".zdev", "design");
1639
+ return fs9.existsSync(designRoot) ? scanAssets(designRoot) : emptyScan();
1640
+ });
1641
+ ctx.route("/__design/asset", async (req, res) => {
1642
+ const rel = new URL(req.url || "/", "http://localhost").searchParams.get("path") ?? "";
1643
+ if (!rel || rel.includes("..") || rel.includes("\\") || path9.isAbsolute(rel)) {
1644
+ res.writeHead(400);
1645
+ res.end("bad path");
1646
+ return;
1647
+ }
1648
+ const designRoot = path9.join(root, ".zdev", "design");
1649
+ const filePath = path9.join(designRoot, rel);
1650
+ if (!filePath.startsWith(designRoot + path9.sep)) {
1651
+ res.writeHead(400);
1652
+ res.end("bad path");
1653
+ return;
1654
+ }
1655
+ fs9.stat(filePath, (err) => {
1656
+ if (err) {
1657
+ res.writeHead(404);
1658
+ return res.end("Not found");
1822
1659
  }
1823
- let merged = {};
1824
- for (const k of ASSET_KEYS) merged[k] = [];
1825
- for (const folder of folders) {
1826
- const folderPath = path9.resolve(root, folder);
1827
- if (fs9.existsSync(folderPath) && fs9.statSync(folderPath).isDirectory()) {
1828
- const result = scanAssets(folderPath);
1829
- merged = mergeScanResults(merged, result);
1660
+ const ct = MIME[path9.extname(filePath).toLowerCase()] ?? "application/octet-stream";
1661
+ res.writeHead(200, { "Content-Type": ct, "Cache-Control": "no-cache" });
1662
+ const stream = fs9.createReadStream(filePath);
1663
+ stream.on("error", () => {
1664
+ try {
1665
+ res.destroy();
1666
+ } catch {
1830
1667
  }
1831
- }
1832
- res.end(JSON.stringify(merged));
1668
+ });
1669
+ stream.pipe(res);
1833
1670
  });
1834
1671
  });
1835
1672
  }
1673
+ });
1674
+
1675
+ // src/plugins/view/manifest.ts
1676
+ var manifest4 = {
1677
+ mode: "view",
1678
+ label: "\u9879\u76EE\u6D4F\u89C8",
1679
+ icon: "\u{1F441}\uFE0F",
1680
+ description: "openspec / docs / \u6587\u6863\u9884\u89C8",
1681
+ order: 20
1836
1682
  };
1837
1683
 
1838
1684
  // src/plugins/view/index.ts
1839
- var apply8 = {
1840
- inject: ["dashboard"],
1841
- apply(ctx) {
1842
- ctx.dashboard.register({
1843
- mode: "view",
1844
- label: "\u9879\u76EE\u6D4F\u89C8",
1845
- icon: "\u{1F441}\uFE0F",
1846
- description: "openspec / docs / \u6587\u6863\u9884\u89C8",
1847
- config: {
1848
- hiddenDirs: { type: "string[]", label: "\u9690\u85CF\u76EE\u5F55", default: [".git", "node_modules", "dist", "build"] },
1849
- defaultExpandDepth: { type: "number", label: "\u9ED8\u8BA4\u5C55\u5F00\u6DF1\u5EA6", default: 2 },
1850
- showHidden: { type: "boolean", label: "\u663E\u793A\u9690\u85CF\u6587\u4EF6", default: false }
1851
- }
1852
- });
1685
+ var apply6 = defineBuiltin({
1686
+ manifest: manifest4,
1687
+ setup() {
1853
1688
  }
1854
- };
1689
+ });
1855
1690
 
1856
1691
  // src/plugins/stats/index.ts
1857
1692
  import fs10 from "fs";
1858
1693
  import path10 from "path";
1694
+ import { execFile as execFile6 } from "child_process";
1695
+
1696
+ // src/plugins/stats/manifest.ts
1697
+ var manifest5 = {
1698
+ mode: "stats",
1699
+ label: "\u9879\u76EE\u7EDF\u8BA1",
1700
+ icon: "\u{1F4CA}",
1701
+ description: "\u9879\u76EE\u6587\u4EF6\u7EDF\u8BA1 \xB7 \u626B\u63CF\u751F\u6210",
1702
+ order: 10
1703
+ };
1704
+
1705
+ // src/plugins/stats/index.ts
1859
1706
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", ".cache"]);
1707
+ function execFilePromise(file, args, opts) {
1708
+ return new Promise((resolve) => {
1709
+ execFile6(file, args, opts, (err, stdout) => resolve(err ? "" : stdout));
1710
+ });
1711
+ }
1860
1712
  function scan(root) {
1861
1713
  const stats = {
1862
1714
  root: path10.basename(root),
@@ -1866,7 +1718,8 @@ function scan(root) {
1866
1718
  byExt: [],
1867
1719
  markdown: 0,
1868
1720
  openspec: { active: 0, archived: 0 },
1869
- hasJust: fs10.existsSync(path10.join(root, "justfile"))
1721
+ hasJust: fs10.existsSync(path10.join(root, "justfile")),
1722
+ worktrees: 0
1870
1723
  };
1871
1724
  const extMap = /* @__PURE__ */ new Map();
1872
1725
  const walk = (dir) => {
@@ -1899,271 +1752,34 @@ function scan(root) {
1899
1752
  stats.byExt = Array.from(extMap.entries()).map(([ext, count]) => ({ ext, count })).sort((a, b) => b.count - a.count).slice(0, 10);
1900
1753
  return stats;
1901
1754
  }
1902
- var apply9 = {
1903
- inject: ["server", "dashboard"],
1904
- apply(ctx, config) {
1905
- ctx.inject(["server", "dashboard"], () => {
1906
- const server = ctx.server;
1907
- const dashboard = ctx.dashboard;
1908
- if (!server?.route || !dashboard?.register) return;
1909
- dashboard.register({
1910
- mode: "stats",
1911
- label: "\u9879\u76EE\u7EDF\u8BA1",
1912
- icon: "\u{1F4CA}",
1913
- description: "\u9879\u76EE\u6587\u4EF6\u7EDF\u8BA1 \xB7 \u626B\u63CF\u751F\u6210"
1914
- });
1915
- server.route("/__stats/data", (_req, res) => {
1916
- res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
1917
- res.end(JSON.stringify(scan(config.root)));
1918
- });
1919
- });
1920
- }
1921
- };
1922
-
1923
- // src/server/apply-batch-store.ts
1924
- import fs11 from "fs";
1925
- import path11 from "path";
1926
- var DEFAULT_STATE = {
1927
- version: "1",
1928
- status: "idle",
1929
- changes: [],
1930
- batches: [],
1931
- currentBatchIndex: -1,
1932
- parallelism: 2,
1933
- logs: [],
1934
- conflicts: []
1935
- };
1936
- var ApplyBatchStore = class {
1937
- root;
1938
- statePath;
1939
- state;
1940
- listeners = /* @__PURE__ */ new Set();
1941
- constructor(root, onBroadcast) {
1942
- this.root = root;
1943
- this.statePath = path11.join(root, ".zapply", "batch-state.json");
1944
- this.state = this.load();
1945
- }
1946
- load() {
1947
- try {
1948
- if (fs11.existsSync(this.statePath)) {
1949
- return JSON.parse(fs11.readFileSync(this.statePath, "utf8"));
1755
+ var apply7 = defineBuiltin({
1756
+ manifest: manifest5,
1757
+ setup(ctx, root) {
1758
+ ctx.route("/__stats/data", async () => {
1759
+ const stats = scan(root);
1760
+ try {
1761
+ const wts = await listWorktrees(root);
1762
+ stats.worktrees = wts.length;
1763
+ } catch {
1764
+ stats.worktrees = 0;
1950
1765
  }
1951
- } catch {
1952
- }
1953
- return { ...DEFAULT_STATE, createdAt: (/* @__PURE__ */ new Date()).toISOString(), updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
1954
- }
1955
- save() {
1956
- this.state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1957
- fs11.mkdirSync(path11.dirname(this.statePath), { recursive: true });
1958
- fs11.writeFileSync(this.statePath, JSON.stringify(this.state, null, 2));
1959
- this.listeners.forEach((fn) => fn());
1960
- }
1961
- subscribe(fn) {
1962
- this.listeners.add(fn);
1963
- return () => this.listeners.delete(fn);
1964
- }
1965
- read() {
1966
- return this.state;
1967
- }
1968
- updateStatus(status) {
1969
- this.state.status = status;
1970
- this.save();
1971
- }
1972
- updateChanges(changes) {
1973
- this.state.changes = changes;
1974
- this.save();
1975
- }
1976
- updateBatches(batches) {
1977
- this.state.batches = batches;
1978
- this.save();
1979
- }
1980
- updateParallelism(parallelism) {
1981
- this.state.parallelism = Math.max(1, Math.min(8, parallelism));
1982
- this.save();
1983
- }
1984
- addLog(log) {
1985
- this.state.logs.push(log);
1986
- if (this.state.logs.length > 1e3) {
1987
- this.state.logs = this.state.logs.slice(-1e3);
1988
- }
1989
- this.save();
1990
- }
1991
- updateConflicts(conflicts) {
1992
- this.state.conflicts = conflicts;
1993
- this.save();
1994
- }
1995
- updateChange(name, patch) {
1996
- const idx = this.state.changes.findIndex((c) => c.name === name);
1997
- if (idx !== -1) {
1998
- this.state.changes[idx] = { ...this.state.changes[idx], ...patch };
1999
- this.save();
2000
- }
2001
- }
2002
- updateCheckpoint(name, checkpoint) {
2003
- const idx = this.state.changes.findIndex((c) => c.name === name);
2004
- if (idx !== -1) {
2005
- this.state.changes[idx].checkpoint = checkpoint;
2006
- this.save();
2007
- }
2008
- }
2009
- reset() {
2010
- this.state = { ...DEFAULT_STATE, createdAt: (/* @__PURE__ */ new Date()).toISOString(), updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
2011
- this.save();
2012
- }
2013
- };
2014
-
2015
- // src/plugins/apply-batch/index.ts
2016
- var apply10 = {
2017
- inject: ["server", "dashboard", "reload"],
2018
- apply(ctx, config) {
2019
- const root = config.root;
2020
- ctx.inject(["server"], () => {
2021
- if (!ctx.server?.route) return;
2022
- ctx.dashboard.register({
2023
- mode: "apply-batch",
2024
- label: "\u6279\u91CF\u6267\u884C",
2025
- icon: "\u26A1",
2026
- description: "zapply \u6279\u91CF\u5E76\u884C\u6267\u884C\u9A7E\u9A76\u8231"
2027
- });
2028
- const store = new ApplyBatchStore(root, () => {
2029
- try {
2030
- ctx.reload.broadcast("files");
2031
- } catch {
2032
- }
2033
- });
2034
- ctx.server.route("/__apply-batch", async (_req, res) => {
2035
- res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
2036
- res.end(JSON.stringify(store.read()));
2037
- });
2038
- ctx.server.route("/__apply-batch/status", async (req, res) => {
2039
- try {
2040
- const body = JSON.parse(await readBody(req) || "{}");
2041
- store.updateStatus(body.status);
2042
- res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
2043
- res.end(JSON.stringify(store.read()));
2044
- } catch (e) {
2045
- res.writeHead(400, { "Content-Type": "application/json; charset=utf-8" });
2046
- res.end(JSON.stringify({ error: e instanceof Error ? e.message : "unknown error" }));
2047
- }
2048
- });
2049
- ctx.server.route("/__apply-batch/changes", async (_req, res) => {
2050
- res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
2051
- res.end(JSON.stringify(store.read().changes));
2052
- });
2053
- ctx.server.route("/__apply-batch/graph", async (_req, res) => {
2054
- const state = store.read();
2055
- const graph = {
2056
- changes: state.changes.map((c) => ({
2057
- name: c.name,
2058
- status: c.status,
2059
- dependencies: c.dependencies,
2060
- batchIndex: c.batchIndex
2061
- })),
2062
- batches: state.batches,
2063
- conflicts: state.conflicts
2064
- };
2065
- res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
2066
- res.end(JSON.stringify(graph));
2067
- });
2068
- ctx.server.route("/__apply-batch/logs", async (_req, res) => {
2069
- res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
2070
- res.end(JSON.stringify(store.read().logs.slice(-100)));
2071
- });
2072
- ctx.server.route("/__apply-batch/approve", async (req, res) => {
2073
- try {
2074
- const body = JSON.parse(await readBody(req) || "{}");
2075
- store.updateStatus("running");
2076
- store.updateParallelism(body.parallelism ?? 2);
2077
- if (body.skipChanges?.length) {
2078
- for (const name of body.skipChanges) {
2079
- store.updateChange(name, { status: "skipped" });
2080
- }
2081
- }
2082
- store.addLog({
2083
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2084
- level: "info",
2085
- message: `\u7528\u6237\u786E\u8BA4\u6267\u884C\u8BA1\u5212\uFF0C\u5E76\u884C\u5EA6=${body.parallelism ?? 2}`
2086
- });
2087
- res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
2088
- res.end(JSON.stringify(store.read()));
2089
- } catch (e) {
2090
- res.writeHead(400, { "Content-Type": "application/json; charset=utf-8" });
2091
- res.end(JSON.stringify({ error: e instanceof Error ? e.message : "unknown error" }));
2092
- }
2093
- });
2094
- ctx.server.route("/__apply-batch/adjust", async (req, res) => {
2095
- try {
2096
- const body = JSON.parse(await readBody(req) || "{}");
2097
- if (body.parallelism) store.updateParallelism(body.parallelism);
2098
- if (body.skipChanges?.length) {
2099
- for (const name of body.skipChanges) {
2100
- store.updateChange(name, { status: "skipped" });
2101
- }
2102
- }
2103
- res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
2104
- res.end(JSON.stringify(store.read()));
2105
- } catch (e) {
2106
- res.writeHead(400, { "Content-Type": "application/json; charset=utf-8" });
2107
- res.end(JSON.stringify({ error: e instanceof Error ? e.message : "unknown error" }));
2108
- }
2109
- });
2110
- ctx.server.route("/__apply-batch/retry", async (req, res) => {
2111
- try {
2112
- const body = JSON.parse(await readBody(req) || "{}");
2113
- const name = body.name;
2114
- if (!name) throw new Error("missing name");
2115
- const change = store.read().changes.find((c) => c.name === name);
2116
- if (!change) throw new Error("change not found");
2117
- store.updateChange(name, {
2118
- status: "pending",
2119
- error: void 0,
2120
- retryCount: (change.retryCount ?? 0) + 1
2121
- });
2122
- store.addLog({
2123
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2124
- level: "info",
2125
- message: `\u91CD\u8BD5 change: ${name}`,
2126
- changeName: name
2127
- });
2128
- res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
2129
- res.end(JSON.stringify(store.read()));
2130
- } catch (e) {
2131
- res.writeHead(400, { "Content-Type": "application/json; charset=utf-8" });
2132
- res.end(JSON.stringify({ error: e instanceof Error ? e.message : "unknown error" }));
2133
- }
2134
- });
2135
- ctx.server.route("/__apply-batch/pause", async (req, res) => {
2136
- store.updateStatus("paused");
2137
- store.addLog({
2138
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2139
- level: "warn",
2140
- message: "\u7528\u6237\u6682\u505C\u6279\u91CF\u6267\u884C"
2141
- });
2142
- res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
2143
- res.end(JSON.stringify(store.read()));
2144
- });
2145
- ctx.server.route("/__apply-batch/resume", async (req, res) => {
2146
- store.updateStatus("running");
2147
- store.addLog({
2148
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2149
- level: "info",
2150
- message: "\u7528\u6237\u6062\u590D\u6279\u91CF\u6267\u884C"
2151
- });
2152
- res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
2153
- res.end(JSON.stringify(store.read()));
2154
- });
2155
- ctx.server.route("/__apply-batch/reset", async (req, res) => {
2156
- store.reset();
2157
- res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
2158
- res.end(JSON.stringify(store.read()));
2159
- });
1766
+ try {
1767
+ const branchOut = await execFilePromise("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd: root });
1768
+ stats.branch = branchOut.trim() || void 0;
1769
+ } catch {
1770
+ }
1771
+ try {
1772
+ const statusOut = await execFilePromise("git", ["status", "--porcelain"], { cwd: root });
1773
+ stats.dirty = statusOut.trim() ? statusOut.trim().split("\n").length : 0;
1774
+ } catch {
1775
+ }
1776
+ return stats;
2160
1777
  });
2161
1778
  }
2162
- };
1779
+ });
2163
1780
 
2164
1781
  // src/cli.ts
2165
- import YAML2 from "yaml";
2166
- var __dirname2 = path12.dirname(fileURLToPath2(import.meta.url));
1782
+ var __dirname2 = path11.dirname(fileURLToPath2(import.meta.url));
2167
1783
  var DEFAULT_PORT = 4190;
2168
1784
  function parseArgs() {
2169
1785
  const args = process.argv.slice(2);
@@ -2198,62 +1814,6 @@ async function pathExists(p) {
2198
1814
  return false;
2199
1815
  }
2200
1816
  }
2201
- function migrateLegacyBugsConfig(root) {
2202
- const zgoalConfigYaml = path12.join(root, ".zgoal", "config.yaml");
2203
- const zdevConfigYaml = path12.join(root, ".zdev", "config.yaml");
2204
- if (fs12.existsSync(zgoalConfigYaml)) {
2205
- try {
2206
- fs12.unlinkSync(zgoalConfigYaml);
2207
- } catch {
2208
- }
2209
- }
2210
- if (!fs12.existsSync(zdevConfigYaml)) return;
2211
- const pluginsConfig = readPluginsConfig(root);
2212
- if ("bugs" in pluginsConfig) {
2213
- try {
2214
- fs12.unlinkSync(zdevConfigYaml);
2215
- } catch {
2216
- }
2217
- return;
2218
- }
2219
- let yamlContent;
2220
- try {
2221
- yamlContent = fs12.readFileSync(zdevConfigYaml, "utf8");
2222
- } catch {
2223
- return;
2224
- }
2225
- let parsed;
2226
- try {
2227
- parsed = YAML2.parse(yamlContent);
2228
- } catch {
2229
- console.warn("[zdashboard] \u65E0\u6548\u7684 .zdev/config.yaml\uFF0C\u5DF2\u5FFD\u7565\u5E76\u5220\u9664");
2230
- try {
2231
- fs12.unlinkSync(zdevConfigYaml);
2232
- } catch {
2233
- }
2234
- return;
2235
- }
2236
- if (!parsed || typeof parsed !== "object" || !parsed.url || !parsed.product) {
2237
- console.warn("[zdashboard] .zdev/config.yaml \u7F3A\u5C11\u5FC5\u8981\u5B57\u6BB5\uFF0C\u5DF2\u5FFD\u7565\u5E76\u5220\u9664");
2238
- try {
2239
- fs12.unlinkSync(zdevConfigYaml);
2240
- } catch {
2241
- }
2242
- return;
2243
- }
2244
- const bugsConfig = {
2245
- url: String(parsed.url).replace(/\/+$/, ""),
2246
- account: typeof parsed.account === "string" ? parsed.account : "",
2247
- token: typeof parsed.token === "string" ? parsed.token : typeof parsed.password === "string" ? parsed.password : "",
2248
- product: Number(parsed.product)
2249
- };
2250
- pluginsConfig.bugs = bugsConfig;
2251
- writePluginsConfig(root, pluginsConfig);
2252
- try {
2253
- fs12.unlinkSync(zdevConfigYaml);
2254
- } catch {
2255
- }
2256
- }
2257
1817
  async function loadExternal(ctx, dir, root) {
2258
1818
  if (!dir) return;
2259
1819
  let tsxLoaded = false;
@@ -2269,7 +1829,7 @@ async function loadExternal(ctx, dir, root) {
2269
1829
  if (!ent.isDirectory()) continue;
2270
1830
  const candidates = tsxLoaded ? ["index.ts", "index.js", "index.mjs"] : ["index.js", "index.mjs"];
2271
1831
  for (const name of candidates) {
2272
- const p = path12.join(dir, ent.name, name);
1832
+ const p = path11.join(dir, ent.name, name);
2273
1833
  if (await pathExists(p)) {
2274
1834
  try {
2275
1835
  const mod = await import(p);
@@ -2284,8 +1844,8 @@ async function loadExternal(ctx, dir, root) {
2284
1844
  const m = ctx.dashboard.get(ent.name);
2285
1845
  if (m && m.mode === ent.name) {
2286
1846
  const patch = { external: true };
2287
- const webDir = path12.join(dir, ent.name, "web");
2288
- if (await pathExists(path12.join(webDir, "index.html"))) {
1847
+ const webDir = path11.join(dir, ent.name, "web");
1848
+ if (await pathExists(path11.join(webDir, "index.html"))) {
2289
1849
  ctx.server.static(`${PLUGIN_STATIC_PREFIX}${ent.name}/`, webDir);
2290
1850
  if (!m.viewerUrl) patch.viewerUrl = `${PLUGIN_STATIC_PREFIX}${ent.name}/`;
2291
1851
  }
@@ -2304,7 +1864,7 @@ async function loadExternal(ctx, dir, root) {
2304
1864
  }
2305
1865
  async function main() {
2306
1866
  const args = parseArgs();
2307
- const root = path12.resolve(args.dir);
1867
+ const root = path11.resolve(args.dir);
2308
1868
  const existing = await findReusable(root);
2309
1869
  if (existing && !args.restart) {
2310
1870
  const u = `http://localhost:${existing.port}` + (args.page ? `#${args.page}` : "");
@@ -2318,12 +1878,15 @@ async function main() {
2318
1878
  console.log(`[zdashboard] --restart\uFF1A\u505C\u6B62\u65E7\u5B9E\u4F8B pid=${oldRecord.pid}`);
2319
1879
  await stopInstance(oldRecord);
2320
1880
  }
2321
- const appDir = path12.resolve(__dirname2, "web");
1881
+ const appDir = path11.resolve(__dirname2, "web");
1882
+ const BUILTIN_PLUGINS = [apply7, apply3, apply4, apply5, apply6];
1883
+ if (stripDeadPluginConfig(root, BUILTIN_PLUGINS.map((p) => p.manifest))) {
1884
+ console.log("[zdashboard] \u5DF2\u6E05\u7406 dashboard.json \u4E2D\u5185\u7F6E\u63D2\u4EF6\u7684\u8FC7\u65F6\u914D\u7F6E\u952E");
1885
+ }
2322
1886
  const startPort = oldRecord && !args.portExplicit ? oldRecord.port : args.port;
2323
- const zdevDir = path12.join(root, ".zdev");
2324
- const dataDir = fs12.existsSync(zdevDir) ? ".zdev/" : "";
1887
+ const zdevDir = path11.join(root, ".zdev");
1888
+ const dataDir = fs11.existsSync(zdevDir) ? ".zdev/" : "";
2325
1889
  const det = await detect(root);
2326
- migrateLegacyBugsConfig(root);
2327
1890
  const ctx = new Context();
2328
1891
  ctx.plugin(ServerService, {
2329
1892
  root,
@@ -2339,25 +1902,15 @@ async function main() {
2339
1902
  ctx.plugin(apply, { root });
2340
1903
  ctx.plugin(apply2, { root });
2341
1904
  ctx.plugin(DashboardService);
2342
- const plugins = [
2343
- { name: "stats", apply: apply9 },
2344
- { name: "just", apply: apply3 },
2345
- { name: "bugs", apply: apply4 },
2346
- { name: "review", apply: apply5 },
2347
- { name: "apply", apply: apply6 },
2348
- { name: "apply-batch", apply: apply10 },
2349
- { name: "design", apply: apply7 },
2350
- { name: "view", apply: apply8 }
2351
- ];
2352
- for (const p of plugins) {
1905
+ for (const apply8 of BUILTIN_PLUGINS) {
2353
1906
  try {
2354
- ctx.plugin(p.apply, { root });
1907
+ ctx.plugin(apply8, { root });
2355
1908
  } catch (e) {
2356
- console.error(`[zdashboard] plugin ${p.name} failed:`, e);
1909
+ console.error("[zdashboard] builtin plugin failed:", e);
2357
1910
  }
2358
1911
  }
2359
1912
  if (args.plugins) {
2360
- await loadExternal(ctx, path12.resolve(args.plugins), root);
1913
+ await loadExternal(ctx, path11.resolve(args.plugins), root);
2361
1914
  }
2362
1915
  const shutdown = () => {
2363
1916
  try {