zdashboard 2.7.0 → 2.8.0

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 (46) hide show
  1. package/README.md +1 -0
  2. package/dist/cli.js +551 -1008
  3. package/dist/cli.js.map +1 -1
  4. package/dist/web/assets/BatchView-C7Y83zJ8.js +2 -0
  5. package/dist/web/assets/{CheckpointViewer-DBmTkC_E.js → CheckpointViewer-Dnakl7qw.js} +1 -1
  6. package/dist/web/assets/{FileIcon-tDB2mYGC.js → CodeViewer-DKQhzhtt.js} +31 -31
  7. package/dist/web/assets/DependencyGraph-M-usNrkC.js +1 -0
  8. package/dist/web/assets/FileIcon-AydLkQKG.js +1 -0
  9. package/dist/web/assets/{FilterPills-CvO0pz85.js → FilterPills-X52teHq7.js} +1 -1
  10. package/dist/web/assets/{ProgressBar-DIoIeYBA.js → ProgressBar-C8W_FpI2.js} +1 -1
  11. package/dist/web/assets/Sidebar-BUEb7NX4.js +1 -0
  12. package/dist/web/assets/Sidebar-R9gmd6Ss.js +1 -0
  13. package/dist/web/assets/SingleChangeView-Cfr_Wkzz.js +1 -0
  14. package/dist/web/assets/Workspace-C2bvxp8p.js +1 -0
  15. package/dist/web/assets/Workspace-CkU9yWvw.js +5 -0
  16. package/dist/web/assets/Workspace-CqTemGJp.js +2 -0
  17. package/dist/web/assets/Workspace-Do07KofT.js +1 -0
  18. package/dist/web/assets/Workspace-qzGIvIU-.js +1 -0
  19. package/dist/web/assets/client-Cw4K1Nx3.js +1 -0
  20. package/dist/web/assets/{index-yx7txO50.js → index-B1epVp2c.js} +509 -509
  21. package/dist/web/assets/index-BJDneNeU.css +1 -0
  22. package/dist/web/assets/{index-CtlFZEcM.js → index-DOZp2EHd.js} +2 -2
  23. package/dist/web/assets/usePluginData-jGCocXxu.js +1 -0
  24. package/dist/web/assets/web-BROwAvbQ.js +2 -0
  25. package/dist/web/assets/web-C-_Hr8JN.js +2 -0
  26. package/dist/web/assets/web-C4mUiGNR.js +2 -0
  27. package/dist/web/assets/web-He9MXVk-.js +2 -0
  28. package/dist/web/assets/web-VATakLhV.js +2 -0
  29. package/dist/web/index.html +3 -3
  30. package/package.json +5 -1
  31. package/dist/web/assets/ApprovalPanel-CXQRioSw.js +0 -1
  32. package/dist/web/assets/BatchDashboard-BigPUG8g.js +0 -2
  33. package/dist/web/assets/ConfigField-BqjVlteZ.js +0 -1
  34. package/dist/web/assets/DependencyGraph-DPVneSQh.js +0 -1
  35. package/dist/web/assets/Workspace-B2nKQnQf.js +0 -1
  36. package/dist/web/assets/badge-CmIMTBDj.js +0 -1
  37. package/dist/web/assets/index-BHL7sr_b.css +0 -1
  38. package/dist/web/assets/web-BBv8hkwo.js +0 -1
  39. package/dist/web/assets/web-BNjs9lCy.js +0 -139
  40. package/dist/web/assets/web-BX2cajm6.js +0 -5
  41. package/dist/web/assets/web-BpBEbcYZ.js +0 -1
  42. package/dist/web/assets/web-CGbbhItO.js +0 -2
  43. package/dist/web/assets/web-CcDed_yx.js +0 -1
  44. package/dist/web/assets/web-DsIjKVnn.js +0 -2
  45. package/dist/web/assets/web-Eo4S0U_k.js +0 -2
  46. /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.0",
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: {
@@ -121,6 +115,7 @@ var package_default = {
121
115
  "@types/react": "^18.3.12",
122
116
  "@types/react-dom": "^18.3.1",
123
117
  "@vitejs/plugin-react": "^4.7.0",
118
+ "@vitest/coverage-v8": "1.6.0",
124
119
  autoprefixer: "^10.4.20",
125
120
  jsdom: "^30.0.1",
126
121
  postcss: "^8.4.49",
@@ -166,14 +161,21 @@ function readRecord(root) {
166
161
  }
167
162
  }
168
163
  function writeRecord(root, port) {
169
- fs2.mkdirSync(path2.dirname(recordPath(root)), { recursive: true });
164
+ const fp = recordPath(root);
165
+ fs2.mkdirSync(path2.dirname(fp), { recursive: true });
166
+ const old = readRecord(root);
170
167
  const rec = {
171
168
  pid: process.pid,
172
169
  port,
173
170
  root,
174
171
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
175
172
  };
176
- fs2.writeFileSync(recordPath(root), JSON.stringify(rec, null, 2) + "\n");
173
+ if (old?.plugins && typeof old.plugins === "object") {
174
+ rec.plugins = old.plugins;
175
+ }
176
+ const tmp = fp + ".tmp";
177
+ fs2.writeFileSync(tmp, JSON.stringify(rec, null, 2) + "\n");
178
+ fs2.renameSync(tmp, fp);
177
179
  }
178
180
  function readPluginsConfig(root) {
179
181
  try {
@@ -199,6 +201,44 @@ function writePluginsConfig(root, plugins) {
199
201
  fs2.writeFileSync(tmp, JSON.stringify(rec, null, 2) + "\n");
200
202
  fs2.renameSync(tmp, fp);
201
203
  }
204
+ function stripDeadPluginConfig(root, manifests) {
205
+ try {
206
+ const rec = readRecord(root);
207
+ if (!rec?.plugins || typeof rec.plugins !== "object") return false;
208
+ const byMode = new Map(manifests.map((m) => [m.mode, m]));
209
+ const stored = rec.plugins;
210
+ const plugins = { ...stored };
211
+ let changed = false;
212
+ for (const mode of Object.keys(plugins)) {
213
+ const m = byMode.get(mode);
214
+ if (!m || m.external) continue;
215
+ const section = plugins[mode];
216
+ if (section === null || typeof section !== "object") continue;
217
+ const keys = Object.keys(section);
218
+ if (!m.config || Object.keys(m.config).length === 0) {
219
+ if (keys.length > 0) {
220
+ delete plugins[mode];
221
+ changed = true;
222
+ }
223
+ continue;
224
+ }
225
+ const kept = {};
226
+ for (const key of Object.keys(m.config)) {
227
+ if (key in section) kept[key] = section[key];
228
+ }
229
+ if (Object.keys(kept).length !== keys.length) {
230
+ if (Object.keys(kept).length === 0) delete plugins[mode];
231
+ else plugins[mode] = kept;
232
+ changed = true;
233
+ }
234
+ }
235
+ if (!changed) return false;
236
+ writePluginsConfig(root, plugins);
237
+ return true;
238
+ } catch {
239
+ return false;
240
+ }
241
+ }
202
242
  function clearRecord(root) {
203
243
  try {
204
244
  const rec = readRecord(root);
@@ -330,8 +370,20 @@ var MIME = {
330
370
  ".ttf": "font/woff",
331
371
  ".map": "application/json; charset=utf-8"
332
372
  };
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>`;
373
+ 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
374
  var PLUGIN_STATIC_PREFIX = "/__plugin/";
375
+ function json(res, data, status = 200) {
376
+ res.writeHead(status, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
377
+ res.end(JSON.stringify(data));
378
+ }
379
+ function guarded(req, res, token) {
380
+ if (req.headers["x-stop-token"] !== token) {
381
+ res.writeHead(403);
382
+ res.end("forbidden");
383
+ return false;
384
+ }
385
+ return true;
386
+ }
335
387
  var ServerService = class extends Service {
336
388
  stopToken;
337
389
  root;
@@ -358,28 +410,54 @@ var ServerService = class extends Service {
358
410
  ctx.effect(() => () => this.dispose());
359
411
  this.refreshGitInfo();
360
412
  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 }));
413
+ json(res, { stopToken: this.stopToken, version: VERSION, root: this.root, ...this.gitInfo });
363
414
  });
364
- this.route("/__stop", async (req, res) => {
365
- if (req.headers["x-stop-token"] !== this.stopToken) {
415
+ this.route("/__detect", async (_req, res) => {
416
+ const shape = await detectLiveShape(this.root);
417
+ json(res, shape);
418
+ });
419
+ this.route("/favicon.svg", (req, res) => {
420
+ this.serveFile(path3.join(this.appDir, "favicon.svg"), req, res, false);
421
+ });
422
+ this.route("/__file-content", async (req, res) => {
423
+ const url = new URL(req.url || "/", "http://localhost");
424
+ const rawPath = url.pathname.replace(/^\/__file-content\/?/, "");
425
+ if (!rawPath || rawPath === "/") {
426
+ res.writeHead(400);
427
+ res.end("missing path");
428
+ return;
429
+ }
430
+ const filePath = path3.join(this.root, decodeURIComponent(rawPath));
431
+ if (!filePath.startsWith(this.root + path3.sep) && filePath !== this.root) {
366
432
  res.writeHead(403);
367
- res.end("forbidden");
433
+ res.end("Forbidden");
368
434
  return;
369
435
  }
370
- res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
371
- res.end('{"ok":true}');
436
+ fs3.stat(filePath, (err) => {
437
+ if (err) {
438
+ res.writeHead(404);
439
+ return res.end("Not found");
440
+ }
441
+ const ext = path3.extname(filePath).toLowerCase();
442
+ const ct = MIME[ext] ?? "application/octet-stream";
443
+ res.writeHead(200, { "Content-Type": ct, "Cache-Control": "no-cache" });
444
+ fs3.createReadStream(filePath).pipe(res);
445
+ });
446
+ });
447
+ this.route("/__stop", async (req, res) => {
448
+ if (!guarded(req, res, this.stopToken)) return;
449
+ json(res, { ok: true });
372
450
  this.stop();
373
451
  });
374
452
  this.start(config.port);
375
453
  }
376
- route(path13, handler) {
377
- this.routes.set(path13, handler);
378
- this.ctx.effect(() => () => this.routes.delete(path13));
454
+ route(path12, handler) {
455
+ this.routes.set(path12, handler);
456
+ this.ctx.effect(() => () => this.routes.delete(path12));
379
457
  }
380
- sse(path13, onConnect) {
381
- this.sses.set(path13, onConnect);
382
- this.ctx.effect(() => () => this.sses.delete(path13));
458
+ sse(path12, onConnect) {
459
+ this.sses.set(path12, onConnect);
460
+ this.ctx.effect(() => () => this.sses.delete(path12));
383
461
  }
384
462
  static(prefix, dir) {
385
463
  this.prefixStatic.set(prefix, dir);
@@ -484,6 +562,12 @@ var ServerService = class extends Service {
484
562
  rh(req, res);
485
563
  return;
486
564
  }
565
+ for (const [rp, rh2] of this.routes) {
566
+ if (url.startsWith(rp + "/")) {
567
+ rh2(req, res);
568
+ return;
569
+ }
570
+ }
487
571
  const sh = this.sses.get(url);
488
572
  if (sh) {
489
573
  res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" });
@@ -532,14 +616,16 @@ var ServerService = class extends Service {
532
616
  } else throw err;
533
617
  });
534
618
  this.server.listen(port, "127.0.0.1", () => {
535
- const u = `http://localhost:${port}`;
619
+ const addr = this.server?.address();
620
+ const bound = typeof addr === "object" && addr ? addr.port : port;
621
+ const u = `http://localhost:${bound}`;
536
622
  const target = this.page ? `${u}#${this.page}` : u;
537
623
  console.log(`[zdashboard] v${VERSION} dashboard -> ${u}`);
538
624
  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}`);
625
+ console.log(`[zdashboard] detect -> openspec:${this.det.hasOpenspec} docs:${this.det.hasDocs} just:${this.det.hasJust}`);
540
626
  if (this.dataDir) console.log(`[zdashboard] data -> ${this.dataDir}`);
541
627
  if (this.open) openUrl(target);
542
- this.onListen?.(port);
628
+ this.onListen?.(bound);
543
629
  });
544
630
  }
545
631
  /** 项目 git 信息(分支/脏文件数),启动探测一次,文件变更时刷新 */
@@ -594,6 +680,12 @@ var ServerService = class extends Service {
594
680
  import fs4 from "fs";
595
681
  import { Service as Service2 } from "cordis";
596
682
  var WATCH_DEBOUNCE_MS = 300;
683
+ var IGNORED_DIR_RE = /^(?:\.git|node_modules|dist|\.pnpm)(?:[/\\]|$)/;
684
+ var IGNORED_FILE_RE = /\.(?:swp|tmp|log)$|~$|^\.DS_Store$|^Thumbs\.db$/i;
685
+ function isIgnoredPath(filename) {
686
+ if (!filename) return false;
687
+ return IGNORED_DIR_RE.test(filename) || IGNORED_FILE_RE.test(filename);
688
+ }
597
689
  var ReloadService = class extends Service2 {
598
690
  static inject = ["server"];
599
691
  clients = /* @__PURE__ */ new Set();
@@ -608,8 +700,7 @@ var ReloadService = class extends Service2 {
608
700
  });
609
701
  try {
610
702
  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;
703
+ if (isIgnoredPath(filename)) return;
613
704
  if (this.timer) clearTimeout(this.timer);
614
705
  this.timer = setTimeout(() => {
615
706
  this.ctx.server.refreshGitInfo();
@@ -633,6 +724,10 @@ data: ${JSON.stringify(data == null ? "" : data)}
633
724
  }
634
725
  }
635
726
  }
727
+ /** 插件专用频道:plugin:<mode>:<event>,客户端 usePluginData subscribe 消费 */
728
+ broadcastPlugin(mode, event, data = "") {
729
+ this.broadcast(`plugin:${mode}:${event}`, data);
730
+ }
636
731
  dispose() {
637
732
  if (this.watcher) {
638
733
  try {
@@ -661,7 +756,7 @@ import path5 from "path";
661
756
  import fs5 from "fs";
662
757
  import path4 from "path";
663
758
  function walkDir(root, opts = {}) {
664
- const { skip = /* @__PURE__ */ new Set(), maxDepth, showHidden, onFile, onDir } = opts;
759
+ const { skip = /* @__PURE__ */ new Set(), maxDepth, onFile, onDir } = opts;
665
760
  function walk(dir, rel, depth) {
666
761
  if (typeof maxDepth === "number" && depth > maxDepth) return;
667
762
  let ents;
@@ -671,7 +766,7 @@ function walkDir(root, opts = {}) {
671
766
  return;
672
767
  }
673
768
  for (const ent of ents) {
674
- if (!showHidden && ent.name.startsWith(".")) continue;
769
+ if (ent.name.startsWith(".")) continue;
675
770
  if (skip.has(ent.name)) continue;
676
771
  const abs = path4.join(dir, ent.name);
677
772
  const r = rel ? `${rel}/${ent.name}` : ent.name;
@@ -725,98 +820,53 @@ function buildTree(paths, defaultExpandDepth = 2) {
725
820
  sortNodes(root);
726
821
  return root;
727
822
  }
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 });
823
+ function prefixPaths(nodes, prefix) {
824
+ return nodes.map((n) => {
825
+ if (n.kind === "file" && n.path) {
826
+ return { ...n, path: `${prefix}/${n.path}` };
742
827
  }
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));
828
+ if (n.children) {
829
+ return { ...n, children: prefixPaths(n.children, prefix) };
755
830
  }
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 });
764
- }
765
- }
766
- if (hasDocs && fs6.existsSync(path5.join(root, "docs"))) {
831
+ return n;
832
+ });
833
+ }
834
+ function scanTree(root, scanDirs, opts) {
835
+ const tree = [];
836
+ const defaultExpandDepth = typeof opts?.defaultExpandDepth === "number" ? opts.defaultExpandDepth : 2;
837
+ const dirs = Array.isArray(scanDirs) ? scanDirs.filter((d) => typeof d === "string" && d) : [];
838
+ for (const dir of dirs) {
839
+ const dirPath = path5.join(root, dir);
840
+ if (!fs6.existsSync(dirPath)) continue;
767
841
  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 });
842
+ walkDir(dirPath, { maxDepth: 4, onFile: (_, rel) => paths.push(rel) });
843
+ const prefix = dir;
844
+ const children = prefixPaths(buildTree(paths, defaultExpandDepth), prefix);
845
+ if (children.length) {
846
+ tree.push({ name: `${dir} (${children.length})`, kind: "dir", children });
783
847
  }
784
- } catch (e) {
785
- console.error("[zdashboard] scan root etc failed:", e);
786
848
  }
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
849
  return tree;
790
850
  }
791
851
 
792
852
  // src/core/tree.ts
853
+ var CONVENTION_SCAN_DIRS = ["openspec", "docs"];
793
854
  var apply = {
794
- inject: ["server", "dashboard"],
855
+ inject: ["server"],
795
856
  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
857
  const server = ctx.server;
805
858
  if (server?.route) {
806
- server.route("/__files", async (_req, res) => {
859
+ server.route("/__files", async (req, res) => {
807
860
  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
- });
861
+ const url = new URL(req.url || "/", "http://localhost");
862
+ const wt = url.searchParams.get("wt");
863
+ const scanRoot = wt ? decodeURIComponent(wt) : config.root;
864
+ const tree = scanTree(scanRoot, [...CONVENTION_SCAN_DIRS]);
815
865
  res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
816
- res.end(JSON.stringify({ tree, ...d }));
866
+ res.end(JSON.stringify({ tree }));
817
867
  } catch {
818
868
  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 }));
869
+ res.end(JSON.stringify({ tree: [] }));
820
870
  }
821
871
  });
822
872
  }
@@ -905,7 +955,7 @@ var apply2 = {
905
955
  import { Service as Service3 } from "cordis";
906
956
 
907
957
  // src/core/read-body.ts
908
- async function readBody2(req) {
958
+ async function readBody(req) {
909
959
  return new Promise((resolve) => {
910
960
  let data = "";
911
961
  req.on("data", (chunk) => {
@@ -952,7 +1002,7 @@ var DashboardService = class extends Service3 {
952
1002
  return;
953
1003
  }
954
1004
  try {
955
- const body = JSON.parse(await readBody2(req));
1005
+ const body = JSON.parse(await readBody(req));
956
1006
  const merged = {};
957
1007
  for (const mode of Object.keys(body)) {
958
1008
  merged[mode] = body[mode] ?? {};
@@ -977,9 +1027,9 @@ var DashboardService = class extends Service3 {
977
1027
  res.end('{"error":"method not allowed"}');
978
1028
  });
979
1029
  }
980
- register(manifest) {
981
- this.plugins.set(manifest.mode, manifest);
982
- this.ctx.effect(() => () => this.plugins.delete(manifest.mode));
1030
+ register(manifest6) {
1031
+ this.plugins.set(manifest6.mode, manifest6);
1032
+ this.ctx.effect(() => () => this.plugins.delete(manifest6.mode));
983
1033
  }
984
1034
  list() {
985
1035
  return Array.from(this.plugins.values());
@@ -1131,498 +1181,202 @@ var JustRunner = class {
1131
1181
  }
1132
1182
  };
1133
1183
 
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([]));
1184
+ // src/sdk/server.ts
1185
+ function definePlugin(def) {
1186
+ const { manifest: manifest6, setup } = def;
1187
+ return {
1188
+ inject: ["server", "dashboard", "reload"],
1189
+ apply(ctx) {
1190
+ const disposers = [];
1191
+ ctx.effect(() => () => {
1192
+ for (const fn of disposers.splice(0)) {
1193
+ try {
1194
+ fn();
1195
+ } catch (e) {
1196
+ console.error(`[zdashboard] plugin ${manifest6.mode} dispose error:`, e);
1197
+ }
1151
1198
  }
1152
1199
  });
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;
1200
+ ctx.dashboard.register(manifest6);
1201
+ const respondWith = (handler) => async (req, res) => {
1202
+ if (res.headersSent) return;
1169
1203
  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);
1204
+ const data = await handler(req, res);
1205
+ if (data !== void 0 && !res.writableEnded) json(res, data);
1206
+ } catch (e) {
1207
+ console.error(`[zdashboard] plugin ${manifest6.mode} route error:`, e);
1208
+ if (!res.headersSent) json(res, { error: "internal" }, 500);
1194
1209
  }
1195
- res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
1196
- res.end(JSON.stringify(runner.list()));
1197
1210
  };
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
- }
1211
+ const pctx = {
1212
+ mode: manifest6.mode,
1213
+ config() {
1214
+ const schema = manifest6.config ?? {};
1215
+ const stored = ctx.server.getPluginConfig(manifest6.mode) ?? {};
1216
+ const defaults = Object.fromEntries(
1217
+ Object.entries(schema).map(([key, field]) => [key, field.default])
1218
+ );
1219
+ return { ...defaults, ...stored };
1220
+ },
1221
+ route(path12, handler) {
1222
+ ctx.server.route(path12, respondWith(handler));
1223
+ },
1224
+ guardedRoute(path12, handler) {
1225
+ const token = () => ctx.server.stopToken;
1226
+ ctx.server.route(path12, (req, res) => {
1227
+ if (!guarded(req, res, token())) return;
1228
+ void respondWith(handler)(req, res);
1205
1229
  });
1206
- });
1230
+ },
1231
+ sse(path12, handler) {
1232
+ ctx.server.sse(path12, handler);
1233
+ },
1234
+ static(prefix, dir) {
1235
+ ctx.server.static(prefix, dir);
1236
+ },
1237
+ broadcast(event, data) {
1238
+ ctx.reload?.broadcast(`plugin:${manifest6.mode}:${event}`, data ?? "");
1239
+ },
1240
+ onDispose(fn) {
1241
+ disposers.push(fn);
1242
+ }
1207
1243
  };
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}`);
1244
+ setup(pctx);
1248
1245
  }
1249
- if (e instanceof Error && e.name === "HTTPError") {
1250
- throw new HttpError(e.status ?? 500, e.message);
1251
- }
1252
- throw new NetworkError(`\u8BF7\u6C42\u5931\u8D25: ${url}`, e);
1253
- }
1246
+ };
1254
1247
  }
1255
1248
 
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: "" })
1249
+ // src/plugins/builtin.ts
1250
+ function defineBuiltin(def) {
1251
+ let currentRoot = "";
1252
+ const inner = definePlugin({
1253
+ manifest: def.manifest,
1254
+ setup(pctx) {
1255
+ def.setup(pctx, currentRoot);
1256
+ }
1266
1257
  });
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
1258
  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
1259
+ inject: inner.inject,
1260
+ manifest: def.manifest,
1261
+ // cli 启动期死键剥离按 manifest 判定(config 声明/external)
1262
+ apply(ctx, config) {
1263
+ currentRoot = config?.root ?? "";
1264
+ inner.apply(ctx, config);
1265
+ }
1286
1266
  };
1287
1267
  }
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
1268
 
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
- }
1269
+ // src/plugins/just/manifest.ts
1270
+ var manifest = {
1271
+ mode: "just",
1272
+ label: "Just Runner",
1273
+ icon: "\u{1F4DC}",
1274
+ description: "Just \u4EFB\u52A1\u65E5\u5FD7\u4E0E\u6267\u884C",
1275
+ order: 50
1342
1276
  };
1343
1277
 
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: [] };
1278
+ // src/plugins/just/index.ts
1279
+ var apply3 = defineBuiltin({
1280
+ manifest,
1281
+ setup(ctx, root) {
1282
+ const runner = new JustRunner(root);
1283
+ ctx.onDispose(() => runner.stop());
1284
+ ctx.route("/__just/recipes", async () => {
1285
+ try {
1286
+ return await runner.recipes();
1287
+ } catch {
1288
+ return [];
1371
1289
  }
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
1290
  });
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
- }
1291
+ ctx.route("/__just/tasks", async () => runner.list());
1292
+ ctx.sse("/__just/logs", (res) => {
1293
+ const unsub = runner.subscribe((ev) => {
1534
1294
  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
- }
1550
- 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" }));
1295
+ ctx.broadcast(ev.type, ev);
1296
+ } catch {
1558
1297
  }
1298
+ res.write(`data: ${JSON.stringify(ev)}
1299
+
1300
+ `);
1559
1301
  });
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) {
1302
+ return unsub;
1303
+ });
1304
+ const handleAction = async (req, res, act) => {
1305
+ const body = await readBody(req);
1306
+ let recipe;
1307
+ try {
1308
+ recipe = JSON.parse(body || "{}").recipe;
1309
+ } catch {
1310
+ }
1311
+ if (act === "clear") {
1312
+ if (recipe) runner.clear(recipe);
1313
+ return { ok: true };
1314
+ }
1315
+ if (act !== "stop") {
1316
+ if (!recipe) {
1572
1317
  res.writeHead(400, { "Content-Type": "application/json; charset=utf-8" });
1573
- res.end(JSON.stringify({ error: e instanceof Error ? e.message : "unknown error" }));
1318
+ res.end('{"error":"no recipe"}');
1319
+ return void 0;
1574
1320
  }
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" }));
1321
+ const recipes = await runner.recipes();
1322
+ if (!recipes.some((r) => r.name === recipe)) {
1323
+ res.writeHead(403, { "Content-Type": "application/json; charset=utf-8" });
1324
+ res.end('{"error":"unknown recipe"}');
1325
+ return void 0;
1590
1326
  }
1591
- });
1592
- });
1327
+ runner.start(recipe);
1328
+ } else {
1329
+ runner.stop(recipe || void 0);
1330
+ }
1331
+ return runner.list();
1332
+ };
1333
+ const routeAction = (name) => {
1334
+ ctx.guardedRoute(`/__just/${name}`, ((req, res) => handleAction(req, res, name)));
1335
+ };
1336
+ routeAction("start");
1337
+ routeAction("stop");
1338
+ routeAction("restart");
1339
+ routeAction("clear");
1593
1340
  }
1594
- };
1341
+ });
1595
1342
 
1596
1343
  // src/plugins/apply/scan.ts
1597
- import fs8 from "fs";
1598
- import path7 from "path";
1344
+ import fs7 from "fs";
1345
+ import path6 from "path";
1599
1346
 
1600
1347
  // src/plugins/apply/parse-tasks.ts
1601
1348
  var TASK_RE = /^\s*-\s*\[([ xX])\]\s*(.*)$/;
1349
+ var MANUAL_PREFIX = "\u{1F527}[\u4EBA\u5DE5]";
1602
1350
  function parseTasks(md) {
1603
- return md.split("\n").filter((l) => TASK_RE.test(l)).map((l) => {
1351
+ return md.split(/\r?\n/).filter((l) => TASK_RE.test(l)).map((l) => {
1604
1352
  const m = l.match(TASK_RE);
1605
- return { text: (m?.[2] ?? l).trim(), checked: m?.[1] !== " " };
1353
+ const text = (m?.[2] ?? l).trim();
1354
+ return { text, checked: m?.[1] !== " ", manual: text.startsWith(MANUAL_PREFIX) };
1606
1355
  });
1607
1356
  }
1608
1357
  function countTasks(md) {
1609
1358
  const tasks = parseTasks(md);
1610
- return { total: tasks.length, done: tasks.filter((t) => t.checked).length };
1359
+ const normal = tasks.filter((t) => !t.manual);
1360
+ return {
1361
+ total: normal.length,
1362
+ done: normal.filter((t) => t.checked).length,
1363
+ manual: tasks.length - normal.length
1364
+ };
1611
1365
  }
1612
1366
 
1613
1367
  // src/plugins/apply/scan.ts
1614
1368
  function readText(p) {
1615
1369
  try {
1616
- return fs8.readFileSync(p, "utf8");
1370
+ return fs7.readFileSync(p, "utf8");
1617
1371
  } catch {
1618
1372
  return "";
1619
1373
  }
1620
1374
  }
1621
1375
  function worktreeDir(root, name) {
1622
- return path7.join(root, ".zworktree", name);
1376
+ return path6.join(root, ".zworktree", name);
1623
1377
  }
1624
1378
  function changeDir(root, name) {
1625
- return path7.join(root, "openspec", "changes", name);
1379
+ return path6.join(root, "openspec", "changes", name);
1626
1380
  }
1627
1381
  function parseDependsOn(proposal) {
1628
1382
  const deps = [];
@@ -1636,42 +1390,42 @@ function parseDependsOn(proposal) {
1636
1390
  return deps;
1637
1391
  }
1638
1392
  function scanApplyChanges(root) {
1639
- const changesDir = path7.join(root, "openspec", "changes");
1640
- if (!fs8.existsSync(changesDir)) return [];
1393
+ const changesDir = path6.join(root, "openspec", "changes");
1394
+ if (!fs7.existsSync(changesDir)) return [];
1641
1395
  const out = [];
1642
- for (const ent of fs8.readdirSync(changesDir, { withFileTypes: true })) {
1396
+ for (const ent of fs7.readdirSync(changesDir, { withFileTypes: true })) {
1643
1397
  if (!ent.isDirectory() || ent.name.startsWith(".") || ent.name === "archive") continue;
1644
1398
  const wt = worktreeDir(root, ent.name);
1645
- const tasks = readText(path7.join(wt, "openspec", "changes", ent.name, "tasks.md"));
1399
+ const tasks = readText(path6.join(wt, "openspec", "changes", ent.name, "tasks.md"));
1646
1400
  if (!tasks) {
1647
- const fallback = readText(path7.join(changesDir, ent.name, "tasks.md"));
1401
+ const fallback = readText(path6.join(changesDir, ent.name, "tasks.md"));
1648
1402
  if (fallback) {
1649
1403
  const { total, done } = countTasks(fallback);
1650
- const wtChange = path7.join(wt, "openspec", "changes", ent.name);
1651
- const mainChange = path7.join(changesDir, ent.name);
1404
+ const wtChange = path6.join(wt, "openspec", "changes", ent.name);
1405
+ const mainChange = path6.join(changesDir, ent.name);
1652
1406
  out.push({
1653
1407
  name: ent.name,
1654
1408
  path: `openspec/changes/${ent.name}`,
1655
1409
  total,
1656
1410
  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")),
1411
+ hasProposal: fs7.existsSync(path6.join(wtChange, "proposal.md")) || fs7.existsSync(path6.join(mainChange, "proposal.md")),
1412
+ hasDesign: fs7.existsSync(path6.join(wtChange, "design.md")) || fs7.existsSync(path6.join(mainChange, "design.md")),
1659
1413
  // worktree 目录存在即算执行中(tasks.md 可能尚未落盘)
1660
- inWorktree: fs8.existsSync(wtChange)
1414
+ inWorktree: fs7.existsSync(wtChange)
1661
1415
  });
1662
1416
  }
1663
1417
  } else {
1664
1418
  const { total, done } = countTasks(tasks);
1665
- const wtChange = path7.join(wt, "openspec", "changes", ent.name);
1666
- const mainChange = path7.join(changesDir, ent.name);
1419
+ const wtChange = path6.join(wt, "openspec", "changes", ent.name);
1420
+ const mainChange = path6.join(changesDir, ent.name);
1667
1421
  out.push({
1668
1422
  name: ent.name,
1669
1423
  path: `openspec/changes/${ent.name}`,
1670
1424
  total,
1671
1425
  done,
1672
1426
  // 任一侧存在即算(详情读取是 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")),
1427
+ hasProposal: fs7.existsSync(path6.join(wtChange, "proposal.md")) || fs7.existsSync(path6.join(mainChange, "proposal.md")),
1428
+ hasDesign: fs7.existsSync(path6.join(wtChange, "design.md")) || fs7.existsSync(path6.join(mainChange, "design.md")),
1675
1429
  inWorktree: true
1676
1430
  });
1677
1431
  }
@@ -1682,14 +1436,14 @@ function scanApplyChanges(root) {
1682
1436
  function readApplyChange(root, name) {
1683
1437
  const wt = worktreeDir(root, name);
1684
1438
  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");
1439
+ 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");
1440
+ 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");
1441
+ 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
1442
  const proposal = readText(proposalFrom);
1689
1443
  const design = readText(designFrom);
1690
1444
  const tasks = readText(tasksFrom);
1691
1445
  const { total, done } = countTasks(tasks);
1692
- const inWorktree = fs8.existsSync(path7.join(wt, "openspec", "changes", name));
1446
+ const inWorktree = fs7.existsSync(path6.join(wt, "openspec", "changes", name));
1693
1447
  const dependsOn = parseDependsOn(proposal);
1694
1448
  const hasTestStrategy = /^##\s+测试策略\s*$/m.test(design);
1695
1449
  return {
@@ -1708,43 +1462,106 @@ function readApplyChange(root, name) {
1708
1462
  };
1709
1463
  }
1710
1464
 
1465
+ // src/plugins/apply/batch.ts
1466
+ import fs8 from "fs";
1467
+ import path7 from "path";
1468
+ var RUN_ID_PATTERN = /^[A-Za-z0-9-]+$/;
1469
+ function isPlainObject(v) {
1470
+ return typeof v === "object" && v !== null && !Array.isArray(v);
1471
+ }
1472
+ function readText2(p) {
1473
+ try {
1474
+ return fs8.readFileSync(p, "utf8");
1475
+ } catch {
1476
+ return null;
1477
+ }
1478
+ }
1479
+ function readBatchState(root) {
1480
+ const applyDir = path7.join(root, ".zdev", "apply");
1481
+ const raw = readText2(path7.join(applyDir, "CURRENT"));
1482
+ const runId = raw?.trim() ?? "";
1483
+ if (!runId || !RUN_ID_PATTERN.test(runId)) {
1484
+ return { run: null, state: null };
1485
+ }
1486
+ const parsed = (() => {
1487
+ const text = readText2(path7.join(applyDir, "runs", runId, "state.json"));
1488
+ if (text == null) return null;
1489
+ try {
1490
+ return JSON.parse(text);
1491
+ } catch {
1492
+ return null;
1493
+ }
1494
+ })();
1495
+ if (!isPlainObject(parsed)) {
1496
+ return { run: { id: runId }, state: null };
1497
+ }
1498
+ return { run: { id: runId }, state: parsed };
1499
+ }
1500
+ function projectGraph(state) {
1501
+ if (!state) {
1502
+ return { changes: [], batches: [], conflicts: [] };
1503
+ }
1504
+ return {
1505
+ changes: state.changes.map((c) => ({
1506
+ name: c.name,
1507
+ status: c.status,
1508
+ dependencies: c.dependencies,
1509
+ batchIndex: c.batchIndex
1510
+ })),
1511
+ batches: state.batches,
1512
+ conflicts: state.conflicts
1513
+ };
1514
+ }
1515
+ var BATCH_LOGS_TAIL = 100;
1516
+ function tailLogs(state) {
1517
+ return (state?.logs ?? []).slice(-BATCH_LOGS_TAIL);
1518
+ }
1519
+ function readBatchPlan(root, runId) {
1520
+ return readText2(path7.join(root, ".zdev", "apply", "runs", runId, "plan.md"));
1521
+ }
1522
+
1523
+ // src/plugins/apply/manifest.ts
1524
+ var manifest2 = {
1525
+ mode: "apply",
1526
+ label: "\u6267\u884C\u8FDB\u5EA6",
1527
+ icon: "\u2699\uFE0F",
1528
+ description: "OpenSpec \u6267\u884C\u8FDB\u5EA6 \xB7 zapply \u5355 change \u4E0E\u6279\u91CF",
1529
+ order: 40
1530
+ };
1531
+
1711
1532
  // 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
- });
1533
+ function errorResponse(res, status, msg) {
1534
+ res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
1535
+ res.end(JSON.stringify({ error: msg }));
1536
+ return void 0;
1537
+ }
1538
+ var apply4 = defineBuiltin({
1539
+ manifest: manifest2,
1540
+ setup(ctx, root) {
1541
+ ctx.route("/__apply", async () => scanApplyChanges(root));
1542
+ ctx.route("/__apply/change", async (req, res) => {
1543
+ const url = new URL(req.url || "", "http://x");
1544
+ const name = url.searchParams.get("name");
1545
+ const bad = (msg) => errorResponse(res, 400, msg);
1546
+ if (!name) return bad("missing name");
1547
+ if (name.includes("..") || name.includes("/") || name.includes("\\")) return bad("invalid name");
1548
+ try {
1549
+ return readApplyChange(root, name);
1550
+ } catch (e) {
1551
+ return bad(e instanceof Error ? e.message : "unknown error");
1552
+ }
1553
+ });
1554
+ ctx.route("/__apply/batch", async () => readBatchState(root));
1555
+ ctx.route("/__apply/batch/graph", async () => projectGraph(readBatchState(root).state));
1556
+ ctx.route("/__apply/batch/logs", async () => tailLogs(readBatchState(root).state));
1557
+ ctx.route("/__apply/batch/plan", async (_req, res) => {
1558
+ const snap = readBatchState(root);
1559
+ const plan = snap.run ? readBatchPlan(root, snap.run.id) : null;
1560
+ if (plan == null) return errorResponse(res, 404, "plan not found");
1561
+ return { plan };
1745
1562
  });
1746
1563
  }
1747
- };
1564
+ });
1748
1565
 
1749
1566
  // src/plugins/design/index.ts
1750
1567
  import path9 from "path";
@@ -1788,75 +1605,100 @@ function scanAssets(root, subDir = "") {
1788
1605
  return out;
1789
1606
  }
1790
1607
 
1608
+ // src/plugins/design/manifest.ts
1609
+ var manifest3 = {
1610
+ mode: "design",
1611
+ label: "\u8BBE\u8BA1\u8D44\u4EA7",
1612
+ icon: "\u{1F3A8}",
1613
+ description: "\u8BBE\u8BA1\u8D44\u4EA7\u5206\u7C7B\u6D4F\u89C8 \xB7 zdesign",
1614
+ order: 30
1615
+ };
1616
+
1791
1617
  // src/plugins/design/index.ts
1792
1618
  var ASSET_KEYS = ["page", "component", "icon", "token", "md", "video", "audio", "pdf", "font"];
1793
- function mergeScanResults(a, b) {
1619
+ function emptyScan() {
1794
1620
  const out = {};
1795
- for (const k of ASSET_KEYS) {
1796
- out[k] = [...a[k], ...b[k]];
1797
- }
1621
+ for (const k of ASSET_KEYS) out[k] = [];
1798
1622
  return out;
1799
1623
  }
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;
1624
+ var apply5 = defineBuiltin({
1625
+ manifest: manifest3,
1626
+ setup(ctx, root) {
1627
+ ctx.route("/__design/assets", async () => {
1628
+ const designRoot = path9.join(root, ".zdev", "design");
1629
+ return fs9.existsSync(designRoot) ? scanAssets(designRoot) : emptyScan();
1630
+ });
1631
+ ctx.route("/__design/asset", async (req, res) => {
1632
+ const rel = new URL(req.url || "/", "http://localhost").searchParams.get("path") ?? "";
1633
+ if (!rel || rel.includes("..") || rel.includes("\\") || path9.isAbsolute(rel)) {
1634
+ res.writeHead(400);
1635
+ res.end("bad path");
1636
+ return;
1637
+ }
1638
+ const designRoot = path9.join(root, ".zdev", "design");
1639
+ const filePath = path9.join(designRoot, rel);
1640
+ if (!filePath.startsWith(designRoot + path9.sep)) {
1641
+ res.writeHead(400);
1642
+ res.end("bad path");
1643
+ return;
1644
+ }
1645
+ fs9.stat(filePath, (err) => {
1646
+ if (err) {
1647
+ res.writeHead(404);
1648
+ return res.end("Not found");
1822
1649
  }
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);
1650
+ const ct = MIME[path9.extname(filePath).toLowerCase()] ?? "application/octet-stream";
1651
+ res.writeHead(200, { "Content-Type": ct, "Cache-Control": "no-cache" });
1652
+ const stream = fs9.createReadStream(filePath);
1653
+ stream.on("error", () => {
1654
+ try {
1655
+ res.destroy();
1656
+ } catch {
1830
1657
  }
1831
- }
1832
- res.end(JSON.stringify(merged));
1658
+ });
1659
+ stream.pipe(res);
1833
1660
  });
1834
1661
  });
1835
1662
  }
1663
+ });
1664
+
1665
+ // src/plugins/view/manifest.ts
1666
+ var manifest4 = {
1667
+ mode: "view",
1668
+ label: "\u9879\u76EE\u6D4F\u89C8",
1669
+ icon: "\u{1F441}\uFE0F",
1670
+ description: "openspec / docs / \u6587\u6863\u9884\u89C8",
1671
+ order: 20
1836
1672
  };
1837
1673
 
1838
1674
  // 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
- });
1675
+ var apply6 = defineBuiltin({
1676
+ manifest: manifest4,
1677
+ setup() {
1853
1678
  }
1854
- };
1679
+ });
1855
1680
 
1856
1681
  // src/plugins/stats/index.ts
1857
1682
  import fs10 from "fs";
1858
1683
  import path10 from "path";
1684
+ import { execFile as execFile6 } from "child_process";
1685
+
1686
+ // src/plugins/stats/manifest.ts
1687
+ var manifest5 = {
1688
+ mode: "stats",
1689
+ label: "\u9879\u76EE\u7EDF\u8BA1",
1690
+ icon: "\u{1F4CA}",
1691
+ description: "\u9879\u76EE\u6587\u4EF6\u7EDF\u8BA1 \xB7 \u626B\u63CF\u751F\u6210",
1692
+ order: 10
1693
+ };
1694
+
1695
+ // src/plugins/stats/index.ts
1859
1696
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", ".cache"]);
1697
+ function execFilePromise(file, args, opts) {
1698
+ return new Promise((resolve) => {
1699
+ execFile6(file, args, opts, (err, stdout) => resolve(err ? "" : stdout));
1700
+ });
1701
+ }
1860
1702
  function scan(root) {
1861
1703
  const stats = {
1862
1704
  root: path10.basename(root),
@@ -1866,7 +1708,8 @@ function scan(root) {
1866
1708
  byExt: [],
1867
1709
  markdown: 0,
1868
1710
  openspec: { active: 0, archived: 0 },
1869
- hasJust: fs10.existsSync(path10.join(root, "justfile"))
1711
+ hasJust: fs10.existsSync(path10.join(root, "justfile")),
1712
+ worktrees: 0
1870
1713
  };
1871
1714
  const extMap = /* @__PURE__ */ new Map();
1872
1715
  const walk = (dir) => {
@@ -1899,271 +1742,34 @@ function scan(root) {
1899
1742
  stats.byExt = Array.from(extMap.entries()).map(([ext, count]) => ({ ext, count })).sort((a, b) => b.count - a.count).slice(0, 10);
1900
1743
  return stats;
1901
1744
  }
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"));
1745
+ var apply7 = defineBuiltin({
1746
+ manifest: manifest5,
1747
+ setup(ctx, root) {
1748
+ ctx.route("/__stats/data", async () => {
1749
+ const stats = scan(root);
1750
+ try {
1751
+ const wts = await listWorktrees(root);
1752
+ stats.worktrees = wts.length;
1753
+ } catch {
1754
+ stats.worktrees = 0;
1950
1755
  }
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
- });
1756
+ try {
1757
+ const branchOut = await execFilePromise("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd: root });
1758
+ stats.branch = branchOut.trim() || void 0;
1759
+ } catch {
1760
+ }
1761
+ try {
1762
+ const statusOut = await execFilePromise("git", ["status", "--porcelain"], { cwd: root });
1763
+ stats.dirty = statusOut.trim() ? statusOut.trim().split("\n").length : 0;
1764
+ } catch {
1765
+ }
1766
+ return stats;
2160
1767
  });
2161
1768
  }
2162
- };
1769
+ });
2163
1770
 
2164
1771
  // src/cli.ts
2165
- import YAML2 from "yaml";
2166
- var __dirname2 = path12.dirname(fileURLToPath2(import.meta.url));
1772
+ var __dirname2 = path11.dirname(fileURLToPath2(import.meta.url));
2167
1773
  var DEFAULT_PORT = 4190;
2168
1774
  function parseArgs() {
2169
1775
  const args = process.argv.slice(2);
@@ -2198,62 +1804,6 @@ async function pathExists(p) {
2198
1804
  return false;
2199
1805
  }
2200
1806
  }
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
1807
  async function loadExternal(ctx, dir, root) {
2258
1808
  if (!dir) return;
2259
1809
  let tsxLoaded = false;
@@ -2269,7 +1819,7 @@ async function loadExternal(ctx, dir, root) {
2269
1819
  if (!ent.isDirectory()) continue;
2270
1820
  const candidates = tsxLoaded ? ["index.ts", "index.js", "index.mjs"] : ["index.js", "index.mjs"];
2271
1821
  for (const name of candidates) {
2272
- const p = path12.join(dir, ent.name, name);
1822
+ const p = path11.join(dir, ent.name, name);
2273
1823
  if (await pathExists(p)) {
2274
1824
  try {
2275
1825
  const mod = await import(p);
@@ -2284,8 +1834,8 @@ async function loadExternal(ctx, dir, root) {
2284
1834
  const m = ctx.dashboard.get(ent.name);
2285
1835
  if (m && m.mode === ent.name) {
2286
1836
  const patch = { external: true };
2287
- const webDir = path12.join(dir, ent.name, "web");
2288
- if (await pathExists(path12.join(webDir, "index.html"))) {
1837
+ const webDir = path11.join(dir, ent.name, "web");
1838
+ if (await pathExists(path11.join(webDir, "index.html"))) {
2289
1839
  ctx.server.static(`${PLUGIN_STATIC_PREFIX}${ent.name}/`, webDir);
2290
1840
  if (!m.viewerUrl) patch.viewerUrl = `${PLUGIN_STATIC_PREFIX}${ent.name}/`;
2291
1841
  }
@@ -2304,7 +1854,7 @@ async function loadExternal(ctx, dir, root) {
2304
1854
  }
2305
1855
  async function main() {
2306
1856
  const args = parseArgs();
2307
- const root = path12.resolve(args.dir);
1857
+ const root = path11.resolve(args.dir);
2308
1858
  const existing = await findReusable(root);
2309
1859
  if (existing && !args.restart) {
2310
1860
  const u = `http://localhost:${existing.port}` + (args.page ? `#${args.page}` : "");
@@ -2318,12 +1868,15 @@ async function main() {
2318
1868
  console.log(`[zdashboard] --restart\uFF1A\u505C\u6B62\u65E7\u5B9E\u4F8B pid=${oldRecord.pid}`);
2319
1869
  await stopInstance(oldRecord);
2320
1870
  }
2321
- const appDir = path12.resolve(__dirname2, "web");
1871
+ const appDir = path11.resolve(__dirname2, "web");
1872
+ const BUILTIN_PLUGINS = [apply7, apply3, apply4, apply5, apply6];
1873
+ if (stripDeadPluginConfig(root, BUILTIN_PLUGINS.map((p) => p.manifest))) {
1874
+ console.log("[zdashboard] \u5DF2\u6E05\u7406 dashboard.json \u4E2D\u5185\u7F6E\u63D2\u4EF6\u7684\u8FC7\u65F6\u914D\u7F6E\u952E");
1875
+ }
2322
1876
  const startPort = oldRecord && !args.portExplicit ? oldRecord.port : args.port;
2323
- const zdevDir = path12.join(root, ".zdev");
2324
- const dataDir = fs12.existsSync(zdevDir) ? ".zdev/" : "";
1877
+ const zdevDir = path11.join(root, ".zdev");
1878
+ const dataDir = fs11.existsSync(zdevDir) ? ".zdev/" : "";
2325
1879
  const det = await detect(root);
2326
- migrateLegacyBugsConfig(root);
2327
1880
  const ctx = new Context();
2328
1881
  ctx.plugin(ServerService, {
2329
1882
  root,
@@ -2339,25 +1892,15 @@ async function main() {
2339
1892
  ctx.plugin(apply, { root });
2340
1893
  ctx.plugin(apply2, { root });
2341
1894
  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) {
1895
+ for (const apply8 of BUILTIN_PLUGINS) {
2353
1896
  try {
2354
- ctx.plugin(p.apply, { root });
1897
+ ctx.plugin(apply8, { root });
2355
1898
  } catch (e) {
2356
- console.error(`[zdashboard] plugin ${p.name} failed:`, e);
1899
+ console.error("[zdashboard] builtin plugin failed:", e);
2357
1900
  }
2358
1901
  }
2359
1902
  if (args.plugins) {
2360
- await loadExternal(ctx, path12.resolve(args.plugins), root);
1903
+ await loadExternal(ctx, path11.resolve(args.plugins), root);
2361
1904
  }
2362
1905
  const shutdown = () => {
2363
1906
  try {