zdashboard 2.1.1 → 2.3.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 (32) hide show
  1. package/dist/cli.js +400 -280
  2. package/dist/cli.js.map +1 -1
  3. package/dist/web/assets/{web-CUFkwTcN.js → CodeViewer-D8YqzmwB.js} +8 -23
  4. package/dist/web/assets/FilterPills-A_fz1nby.js +1 -0
  5. package/dist/web/assets/{katex.min-BvZgSYSU.js → MdViewer-P4wmRzy_.js} +24 -24
  6. package/dist/web/assets/ProgressBar-B4U41etC.js +1 -0
  7. package/dist/web/assets/Workspace-nm0YyxF7.js +1 -0
  8. package/dist/web/assets/badge-C92Y2FRG.js +1 -0
  9. package/dist/web/assets/{file-text-B46O-IoU.js → file-text-BHzQwEJK.js} +1 -1
  10. package/dist/web/assets/index-ByI0Pghw.css +1 -0
  11. package/dist/web/assets/index-CNPKNdvp.js +111 -0
  12. package/dist/web/assets/{index-RpDccu4v.js → index-C_wTjWH0.js} +1 -1
  13. package/dist/web/assets/web-5tq6UJuk.js +2 -0
  14. package/dist/web/assets/web-DgYHvMu9.js +16 -0
  15. package/dist/web/assets/web-DhyfyLB-.js +11 -0
  16. package/dist/web/assets/web-JxMZZReP.js +2 -0
  17. package/dist/web/assets/web-ZQ0eDR1y.js +31 -0
  18. package/dist/web/assets/web-vTYFh6Bo.js +25 -0
  19. package/dist/web/assets/web-vbs6AnjC.js +26 -0
  20. package/dist/web/index.html +2 -2
  21. package/package.json +1 -1
  22. package/dist/web/assets/MdViewer-CgSHNqC3.js +0 -1
  23. package/dist/web/assets/Workspace-BjAcIoe8.js +0 -1
  24. package/dist/web/assets/index-CEza9SOJ.css +0 -1
  25. package/dist/web/assets/index-CcIhCGOi.js +0 -90
  26. package/dist/web/assets/web-BG3uPf-s.js +0 -12
  27. package/dist/web/assets/web-BH8k8F-d.js +0 -26
  28. package/dist/web/assets/web-CB5F-y_R.js +0 -2
  29. package/dist/web/assets/web-CTWlKjBz.js +0 -32
  30. package/dist/web/assets/web-D6ODdMUf.js +0 -31
  31. package/dist/web/assets/web-DiGxrTCi.js +0 -11
  32. /package/dist/web/assets/{katex-BTcE7LSd.css → MdViewer-BTcE7LSd.css} +0 -0
package/dist/cli.js CHANGED
@@ -1,10 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import path10 from "path";
4
+ import path11 from "path";
5
5
  import fs11 from "fs";
6
6
  import { fileURLToPath as fileURLToPath2 } from "url";
7
7
  import { access, readdir } from "fs/promises";
8
+ import { parseArgs as utilParseArgs } from "util";
8
9
 
9
10
  // src/server/detect.ts
10
11
  import fs2 from "fs";
@@ -14,6 +15,7 @@ import { execFile } from "child_process";
14
15
  // src/server/bugs.ts
15
16
  import fs from "fs";
16
17
  import path from "path";
18
+ import YAML from "yaml";
17
19
 
18
20
  // src/server/api/fetch.ts
19
21
  import ky from "ky";
@@ -61,20 +63,22 @@ function loadConfig(root) {
61
63
  for (const rel of BUGS_CONFIG_CANDIDATES) {
62
64
  const file = path.join(root, rel);
63
65
  if (!fs.existsSync(file)) continue;
64
- const kv = {};
65
- for (const line of fs.readFileSync(file, "utf8").split("\n")) {
66
- const m = line.match(/^\s*([A-Za-z_]\w*)\s*:\s*(.+?)\s*$/);
67
- if (m && !m[2].startsWith("#")) kv[m[1]] = m[2].replace(/^["']|["']$/g, "");
66
+ try {
67
+ const kv = YAML.parse(fs.readFileSync(file, "utf8"));
68
+ if (!kv || typeof kv !== "object") return null;
69
+ const record = kv;
70
+ const product = Number(record.product);
71
+ if (!record.url || !product) return null;
72
+ return {
73
+ url: String(record.url).replace(/\/+$/, ""),
74
+ account: record.account ?? "",
75
+ password: record.password,
76
+ token: record.token,
77
+ product
78
+ };
79
+ } catch {
80
+ return null;
68
81
  }
69
- const product = Number(kv.product);
70
- if (!kv.url || !product) return null;
71
- return {
72
- url: kv.url.replace(/\/+$/, ""),
73
- account: kv.account ?? "",
74
- password: kv.password,
75
- token: kv.token,
76
- product
77
- };
78
82
  }
79
83
  return null;
80
84
  }
@@ -162,7 +166,7 @@ import { fileURLToPath } from "url";
162
166
  // package.json
163
167
  var package_default = {
164
168
  name: "zdashboard",
165
- version: "2.1.1",
169
+ version: "2.3.0",
166
170
  description: "ZCode skill dashboard platform \u2014 pluggable viewers for zdesign/zview/zreview/zgoal",
167
171
  type: "module",
168
172
  bin: {
@@ -344,13 +348,18 @@ async function stopInstance(record) {
344
348
  }
345
349
 
346
350
  // src/core/open-url.ts
347
- import { exec } from "child_process";
351
+ import { exec, execFile as execFile2 } from "child_process";
348
352
  function openUrl(url) {
349
353
  const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
350
- exec(`${cmd} ${url}`);
354
+ if (process.platform === "win32") {
355
+ exec(`start "" "${url}"`);
356
+ } else {
357
+ execFile2(cmd, [url]);
358
+ }
351
359
  }
352
360
 
353
361
  // src/core/server.ts
362
+ import { execFile as execFile3 } from "child_process";
354
363
  var VERSION = package_default.version;
355
364
  var __dirname = path4.dirname(fileURLToPath(import.meta.url));
356
365
  var MIME = {
@@ -367,6 +376,7 @@ var MIME = {
367
376
  ".jpeg": "image/jpeg",
368
377
  ".gif": "image/gif",
369
378
  ".webp": "image/webp",
379
+ ".avif": "image/avif",
370
380
  ".md": "text/markdown; charset=utf-8",
371
381
  ".txt": "text/plain; charset=utf-8",
372
382
  ".sql": "text/plain; charset=utf-8",
@@ -396,6 +406,11 @@ var MIME = {
396
406
  ".env": "text/plain; charset=utf-8",
397
407
  ".gitignore": "text/plain; charset=utf-8",
398
408
  ".dockerfile": "text/plain; charset=utf-8",
409
+ ".mp4": "video/mp4",
410
+ ".webm": "video/webm",
411
+ ".mov": "video/quicktime",
412
+ ".m4a": "audio/mp4",
413
+ ".aac": "audio/aac",
399
414
  ".woff": "font/woff",
400
415
  ".woff2": "font/woff2",
401
416
  ".ttf": "font/woff",
@@ -427,9 +442,10 @@ var ServerService = class extends Service {
427
442
  this.dataDir = config.dataDir;
428
443
  this.onListen = config.onListen;
429
444
  ctx.effect(() => () => this.dispose());
445
+ this.refreshGitInfo();
430
446
  this.route("/__config", (_req, res) => {
431
447
  res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
432
- res.end(JSON.stringify({ stopToken: this.stopToken, version: VERSION, root: this.root }));
448
+ res.end(JSON.stringify({ stopToken: this.stopToken, version: VERSION, root: this.root, ...this.gitInfo }));
433
449
  });
434
450
  this.route("/__stop", async (req, res) => {
435
451
  if (req.headers["x-stop-token"] !== this.stopToken) {
@@ -443,36 +459,74 @@ var ServerService = class extends Service {
443
459
  });
444
460
  this.start(config.port);
445
461
  }
446
- route(path11, handler) {
447
- this.routes.set(path11, handler);
448
- this.ctx.effect(() => () => this.routes.delete(path11));
462
+ route(path12, handler) {
463
+ this.routes.set(path12, handler);
464
+ this.ctx.effect(() => () => this.routes.delete(path12));
449
465
  }
450
- sse(path11, onConnect) {
451
- this.sses.set(path11, onConnect);
452
- this.ctx.effect(() => () => this.sses.delete(path11));
466
+ sse(path12, onConnect) {
467
+ this.sses.set(path12, onConnect);
468
+ this.ctx.effect(() => () => this.sses.delete(path12));
453
469
  }
454
470
  static(prefix, dir) {
455
471
  this.prefixStatic.set(prefix, dir);
456
472
  this.ctx.effect(() => () => this.prefixStatic.delete(prefix));
457
473
  }
458
- serveFile(filePath, res, injectHtml) {
459
- fs4.readFile(filePath, (err, data) => {
474
+ serveFile(filePath, req, res, injectHtml) {
475
+ fs4.stat(filePath, (err, stat) => {
460
476
  if (err) {
461
477
  res.writeHead(404);
462
478
  return res.end("Not found");
463
479
  }
464
480
  const ext = path4.extname(filePath).toLowerCase();
465
481
  const ct = MIME[ext] ?? "application/octet-stream";
466
- let body = data;
467
- if (injectHtml && ext === ".html") {
468
- const s = data.toString("utf8");
469
- body = Buffer.from(s.indexOf("</body>") >= 0 ? s.replace("</body>", INJECT + "</body>") : s + INJECT);
482
+ const rangeHeader = req.headers["range"];
483
+ let range = null;
484
+ if (rangeHeader) {
485
+ const m = rangeHeader.match(/^bytes=(\d+)-(\d*)$/);
486
+ if (m) {
487
+ const start = Number(m[1]);
488
+ const end = m[2] !== "" ? Number(m[2]) : stat.size - 1;
489
+ if (!Number.isNaN(start) && !Number.isNaN(end) && start <= end && start < stat.size) {
490
+ range = { start, end: Math.min(end, stat.size - 1) };
491
+ }
492
+ }
470
493
  }
471
- res.writeHead(200, { "Content-Type": ct, "Cache-Control": "no-cache" });
472
- res.end(body);
494
+ if (range) {
495
+ const { start, end } = range;
496
+ const chunkSize = end - start + 1;
497
+ res.writeHead(206, {
498
+ "Content-Type": ct,
499
+ "Content-Length": String(chunkSize),
500
+ "Content-Range": `bytes ${start}-${end}/${stat.size}`,
501
+ "Accept-Ranges": "bytes",
502
+ "Cache-Control": "no-cache"
503
+ });
504
+ const stream = fs4.createReadStream(filePath, { start, end });
505
+ stream.on("error", () => {
506
+ try {
507
+ res.destroy();
508
+ } catch {
509
+ }
510
+ });
511
+ stream.pipe(res);
512
+ return;
513
+ }
514
+ fs4.readFile(filePath, (err2, data) => {
515
+ if (err2) {
516
+ res.writeHead(404);
517
+ return res.end("Not found");
518
+ }
519
+ let body = data;
520
+ if (injectHtml && ext === ".html") {
521
+ const s = data.toString("utf8");
522
+ body = Buffer.from(s.indexOf("</body>") >= 0 ? s.replace("</body>", INJECT + "</body>") : s + INJECT);
523
+ }
524
+ res.writeHead(200, { "Content-Type": ct, "Accept-Ranges": "bytes", "Cache-Control": "no-cache" });
525
+ res.end(body);
526
+ });
473
527
  });
474
528
  }
475
- servePrefix(url, res) {
529
+ servePrefix(url, req, res) {
476
530
  for (const [prefix, dir] of this.prefixStatic) {
477
531
  if (url.indexOf(prefix) === 0) {
478
532
  let rel = this.safeDecode(url.slice(prefix.length));
@@ -482,7 +536,7 @@ var ServerService = class extends Service {
482
536
  res.writeHead(403);
483
537
  return res.end("Forbidden");
484
538
  }
485
- this.serveFile(fp, res, true);
539
+ this.serveFile(fp, req, res, true);
486
540
  return true;
487
541
  }
488
542
  }
@@ -514,7 +568,7 @@ var ServerService = class extends Service {
514
568
  });
515
569
  return;
516
570
  }
517
- if (this.servePrefix(url, res)) return;
571
+ if (this.servePrefix(url, req, res)) return;
518
572
  if (url === "/" || url.indexOf("/__app/") === 0 || url.indexOf("/assets/") === 0) {
519
573
  let fp2 = this.appDir;
520
574
  if (url !== "/") fp2 = path4.join(this.appDir, this.safeDecode(url));
@@ -524,14 +578,14 @@ var ServerService = class extends Service {
524
578
  return res.end("Forbidden");
525
579
  }
526
580
  if (url === "/") fp2 = path4.join(this.appDir, "index.html");
527
- return this.serveFile(fp2, res, false);
581
+ return this.serveFile(fp2, req, res, false);
528
582
  }
529
583
  const fp = path4.join(this.root, this.safeDecode(url));
530
584
  if (fp !== this.root && fp.indexOf(this.root + path4.sep) !== 0) {
531
585
  res.writeHead(403);
532
586
  return res.end("Forbidden");
533
587
  }
534
- return this.serveFile(fp, res, true);
588
+ return this.serveFile(fp, req, res, true);
535
589
  } catch (e) {
536
590
  try {
537
591
  res.writeHead(400);
@@ -562,6 +616,21 @@ var ServerService = class extends Service {
562
616
  this.onListen?.(port);
563
617
  });
564
618
  }
619
+ /** 项目 git 信息(分支/脏文件数),启动探测一次,文件变更时刷新 */
620
+ gitInfo = {};
621
+ refreshGitInfo() {
622
+ const run = (args) => new Promise((resolve) => {
623
+ execFile3("git", args, { cwd: this.root, timeout: 3e3 }, (err, stdout) => resolve(err ? "" : stdout));
624
+ });
625
+ void (async () => {
626
+ const [branch, status] = await Promise.all([
627
+ run(["rev-parse", "--abbrev-ref", "HEAD"]),
628
+ run(["status", "--porcelain"])
629
+ ]);
630
+ const b = branch.trim();
631
+ this.gitInfo = b ? { branch: b, dirty: status ? status.split("\n").filter(Boolean).length : 0 } : {};
632
+ })();
633
+ }
565
634
  stop() {
566
635
  if (this.server) {
567
636
  try {
@@ -615,6 +684,7 @@ var ReloadService = class extends Service2 {
615
684
  this.watcher = fs5.watch(config.root, { recursive: true }, () => {
616
685
  if (this.timer) clearTimeout(this.timer);
617
686
  this.timer = setTimeout(() => {
687
+ this.ctx.server.refreshGitInfo();
618
688
  this.broadcast("reload");
619
689
  this.broadcast("files");
620
690
  }, WATCH_DEBOUNCE_MS);
@@ -655,73 +725,128 @@ data: ${JSON.stringify(data == null ? "" : data)}
655
725
  };
656
726
 
657
727
  // src/server/spec-scan.ts
728
+ import fs7 from "fs";
729
+ import path6 from "path";
730
+
731
+ // src/server/walk.ts
658
732
  import fs6 from "fs";
659
733
  import path5 from "path";
660
- function walkFiles(absDir, relDir, depth = 0) {
661
- if (depth > 4) return [];
662
- let ents;
663
- try {
664
- ents = fs6.readdirSync(absDir, { withFileTypes: true });
665
- } catch {
666
- return [];
667
- }
668
- const nodes = [];
669
- for (const ent of ents) {
670
- if (ent.name.startsWith(".") || ent.name === "node_modules") continue;
671
- const rel = relDir ? `${relDir}/${ent.name}` : ent.name;
672
- if (ent.isDirectory()) {
673
- nodes.push({ name: ent.name, kind: "dir", children: walkFiles(path5.join(absDir, ent.name), rel, depth + 1) });
674
- } else {
675
- nodes.push({ name: ent.name, kind: "file", path: rel });
734
+ function walkDir(root, opts = {}) {
735
+ const { skip = /* @__PURE__ */ new Set(), maxDepth, onFile, onDir } = opts;
736
+ function walk(dir, rel, depth) {
737
+ if (typeof maxDepth === "number" && depth > maxDepth) return;
738
+ let ents;
739
+ try {
740
+ ents = fs6.readdirSync(dir, { withFileTypes: true });
741
+ } catch {
742
+ return;
743
+ }
744
+ for (const ent of ents) {
745
+ if (ent.name.startsWith(".") || skip.has(ent.name)) continue;
746
+ const abs = path5.join(dir, ent.name);
747
+ const r = rel ? `${rel}/${ent.name}` : ent.name;
748
+ if (ent.isDirectory()) {
749
+ onDir?.(abs, r);
750
+ walk(abs, r, depth + 1);
751
+ } else if (onFile) {
752
+ onFile(abs, r);
753
+ }
676
754
  }
677
755
  }
678
- nodes.sort((a, b) => a.kind === b.kind ? a.name.localeCompare(b.name) : a.kind === "dir" ? -1 : 1);
679
- return nodes;
756
+ walk(root, "", 0);
757
+ }
758
+
759
+ // src/server/spec-scan.ts
760
+ function buildTree(paths) {
761
+ const root = [];
762
+ const map = /* @__PURE__ */ new Map();
763
+ for (const p of paths) {
764
+ const parts = p.split("/");
765
+ let current = "";
766
+ let parent = root;
767
+ for (let i = 0; i < parts.length; i++) {
768
+ const part = parts[i];
769
+ current = current ? `${current}/${part}` : part;
770
+ const isLast = i === parts.length - 1;
771
+ if (!map.has(current)) {
772
+ const node = {
773
+ name: part,
774
+ kind: isLast ? "file" : "dir",
775
+ ...isLast ? { path: p } : { children: [] }
776
+ };
777
+ map.set(current, node);
778
+ parent.push(node);
779
+ }
780
+ if (!isLast) {
781
+ parent = map.get(current).children;
782
+ }
783
+ }
784
+ }
785
+ const sortNodes = (nodes) => {
786
+ nodes.sort((a, b) => a.kind === b.kind ? a.name.localeCompare(b.name) : a.kind === "dir" ? -1 : 1);
787
+ for (const n of nodes) {
788
+ if (n.children) sortNodes(n.children);
789
+ }
790
+ };
791
+ sortNodes(root);
792
+ return root;
680
793
  }
681
794
  function scanTree(root, hasOpenspec, hasDocs) {
682
795
  const tree = [];
683
- if (hasOpenspec && fs6.existsSync(path5.join(root, "openspec", "changes"))) {
684
- const changesDir = path5.join(root, "openspec", "changes");
796
+ if (hasOpenspec && fs7.existsSync(path6.join(root, "openspec", "changes"))) {
797
+ const changesDir = path6.join(root, "openspec", "changes");
685
798
  const active = [];
686
799
  const archived = [];
687
- for (const ent of fs6.readdirSync(changesDir, { withFileTypes: true })) {
800
+ for (const ent of fs7.readdirSync(changesDir, { withFileTypes: true })) {
688
801
  if (!ent.isDirectory() || ent.name.startsWith(".") || ent.name === "archive") continue;
689
- active.push({ name: ent.name, kind: "dir", children: walkFiles(path5.join(changesDir, ent.name), `openspec/changes/${ent.name}`) });
802
+ const relBase = `openspec/changes/${ent.name}`;
803
+ const paths = [];
804
+ walkDir(path6.join(changesDir, ent.name), { maxDepth: 4, onFile: (_, rel) => paths.push(rel ? `${relBase}/${rel}` : relBase) });
805
+ const children = buildTree(paths).map((n) => ({ ...n, path: n.path ? `${relBase}/${n.path}` : void 0 }));
806
+ active.push({ name: ent.name, kind: "dir", children });
690
807
  }
691
808
  active.sort((a, b) => a.name.localeCompare(b.name));
692
- const archiveDir = path5.join(changesDir, "archive");
693
- if (fs6.existsSync(archiveDir)) {
694
- for (const ent of fs6.readdirSync(archiveDir, { withFileTypes: true })) {
809
+ const archiveDir = path6.join(changesDir, "archive");
810
+ if (fs7.existsSync(archiveDir)) {
811
+ for (const ent of fs7.readdirSync(archiveDir, { withFileTypes: true })) {
695
812
  if (!ent.isDirectory() || ent.name.startsWith(".")) continue;
696
- archived.push({ name: ent.name, kind: "dir", children: walkFiles(path5.join(archiveDir, ent.name), `openspec/changes/archive/${ent.name}`) });
813
+ const relBase = `openspec/changes/archive/${ent.name}`;
814
+ const paths = [];
815
+ walkDir(path6.join(archiveDir, ent.name), { maxDepth: 4, onFile: (_, rel) => paths.push(rel ? `${relBase}/${rel}` : relBase) });
816
+ const children = buildTree(paths).map((n) => ({ ...n, path: n.path ? `${relBase}/${n.path}` : void 0 }));
817
+ archived.push({ name: ent.name, kind: "dir", children });
697
818
  }
698
819
  archived.sort((a, b) => b.name.localeCompare(a.name));
699
820
  }
700
- if (active.length) tree.push({ name: `\u8FDB\u884C\u4E2D (${active.length})`, kind: "dir", children: active });
701
- if (archived.length) tree.push({ name: "archive", kind: "dir", defaultCollapsed: true, children: archived });
702
- const specsDir = path5.join(root, "openspec", "specs");
703
- if (fs6.existsSync(specsDir)) {
704
- const specs = walkFiles(specsDir, "openspec/specs");
821
+ if (active.length) tree.push({ name: `changes (${active.length})`, kind: "dir", children: active });
822
+ if (archived.length) tree.push({ name: `archive (${archived.length})`, kind: "dir", defaultCollapsed: true, children: archived });
823
+ const specsDir = path6.join(root, "openspec", "specs");
824
+ if (fs7.existsSync(specsDir)) {
825
+ const paths = [];
826
+ walkDir(specsDir, { maxDepth: 4, onFile: (_, rel) => paths.push(`openspec/specs/${rel}`) });
827
+ const specs = buildTree(paths).map((n) => ({ ...n, path: n.path ? `openspec/specs/${n.path}` : void 0 }));
705
828
  if (specs.length) tree.push({ name: "specs", kind: "dir", children: specs });
706
829
  }
707
830
  }
708
- if (hasDocs && fs6.existsSync(path5.join(root, "docs"))) {
709
- const docs = walkFiles(path5.join(root, "docs"), "docs");
831
+ if (hasDocs && fs7.existsSync(path6.join(root, "docs"))) {
832
+ const paths = [];
833
+ walkDir(path6.join(root, "docs"), { maxDepth: 4, onFile: (_, rel) => paths.push(`docs/${rel}`) });
834
+ const docs = buildTree(paths).map((n) => ({ ...n, path: n.path ? `docs/${n.path}` : void 0 }));
710
835
  if (docs.length) tree.push({ name: "docs", kind: "dir", children: docs });
711
836
  }
712
837
  const skip = /* @__PURE__ */ new Set(["openspec", "docs", "node_modules", ".git", "dist", "test-server"]);
713
838
  const etc = [];
714
839
  try {
715
- for (const ent of fs6.readdirSync(root, { withFileTypes: true })) {
840
+ for (const ent of fs7.readdirSync(root, { withFileTypes: true })) {
716
841
  if (ent.name.startsWith(".") || skip.has(ent.name)) continue;
717
- const ext = path5.extname(ent.name).toLowerCase();
842
+ const ext = path6.extname(ent.name).toLowerCase();
718
843
  if (ent.isFile() && (ext === ".md" || ext === ".markdown")) etc.push({ name: ent.name, kind: "file", path: ent.name });
719
844
  }
720
845
  } catch (e) {
721
846
  console.error("[zdashboard] scan root etc failed:", e);
722
847
  }
723
848
  etc.sort((a, b) => a.name.localeCompare(b.name));
724
- if (etc.length) tree.push({ name: `\u5176\u4ED6 (${etc.length})`, kind: "dir", children: etc });
849
+ if (etc.length) tree.push({ name: `other (${etc.length})`, kind: "dir", children: etc });
725
850
  return tree;
726
851
  }
727
852
 
@@ -754,6 +879,84 @@ var apply = {
754
879
  }
755
880
  };
756
881
 
882
+ // src/core/worktrees.ts
883
+ import { execFile as execFile4 } from "child_process";
884
+ var GIT_TIMEOUT_MS = 5e3;
885
+ var ZWORKTREE_SEGMENT = ".zworktree/";
886
+ function runGit(args, cwd) {
887
+ return new Promise((resolve) => {
888
+ execFile4("git", args, { cwd, timeout: GIT_TIMEOUT_MS }, (err, stdout) => {
889
+ if (err) return resolve("");
890
+ resolve(stdout);
891
+ });
892
+ });
893
+ }
894
+ async function listWorktrees(root) {
895
+ const raw = await runGit(["worktree", "list", "--porcelain"], root);
896
+ if (!raw) return [];
897
+ const entries = [];
898
+ const lines = raw.split("\n");
899
+ let current = {};
900
+ for (const line of lines) {
901
+ const m = line.match(/^(worktree|HEAD|branch|detached)\s+(.+)$/);
902
+ if (!m) continue;
903
+ const [, key, val] = m;
904
+ if (key === "worktree") {
905
+ if (current.path && current.path.includes(ZWORKTREE_SEGMENT)) {
906
+ entries.push({
907
+ path: current.path,
908
+ name: current.path.split(ZWORKTREE_SEGMENT).pop() ?? "",
909
+ branch: current.branch ?? "",
910
+ head: current.head ?? "",
911
+ dirty: false
912
+ });
913
+ }
914
+ current = { path: val, head: "", branch: "" };
915
+ } else if (key === "HEAD") {
916
+ current.head = val;
917
+ } else if (key === "branch") {
918
+ current.branch = val.replace(/^refs\/heads\//, "");
919
+ }
920
+ }
921
+ if (current.path && current.path.includes(ZWORKTREE_SEGMENT)) {
922
+ entries.push({
923
+ path: current.path,
924
+ name: current.path.split(ZWORKTREE_SEGMENT).pop() ?? "",
925
+ branch: current.branch ?? "",
926
+ head: current.head ?? "",
927
+ dirty: false
928
+ });
929
+ }
930
+ await Promise.all(
931
+ entries.map(async (entry) => {
932
+ try {
933
+ const statusOut = await runGit(["status", "--porcelain"], entry.path);
934
+ entry.dirty = statusOut.trim().length > 0;
935
+ } catch {
936
+ entry.dirty = false;
937
+ }
938
+ })
939
+ );
940
+ return entries;
941
+ }
942
+ var apply2 = {
943
+ inject: ["server"],
944
+ apply(ctx, config) {
945
+ const server = ctx.server;
946
+ if (!server?.route) return;
947
+ server.route("/__worktrees", async (_req, res) => {
948
+ try {
949
+ const entries = await listWorktrees(config.root);
950
+ res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
951
+ res.end(JSON.stringify(entries));
952
+ } catch {
953
+ res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
954
+ res.end(JSON.stringify([]));
955
+ }
956
+ });
957
+ }
958
+ };
959
+
757
960
  // src/core/manifest.ts
758
961
  import { Service as Service3 } from "cordis";
759
962
  var DashboardService = class extends Service3 {
@@ -780,7 +983,7 @@ var DashboardService = class extends Service3 {
780
983
  };
781
984
 
782
985
  // src/server/just-runner.ts
783
- import { spawn, execFile as execFile2 } from "child_process";
986
+ import { spawn, execFile as execFile5 } from "child_process";
784
987
  var MAX_BUFFER = 1e3;
785
988
  var JustRunner = class {
786
989
  cwd;
@@ -793,7 +996,7 @@ var JustRunner = class {
793
996
  recipes() {
794
997
  if (this.recipesCache) return Promise.resolve(this.recipesCache);
795
998
  return new Promise((resolve) => {
796
- execFile2("just", ["--list", "--unsorted"], { cwd: this.cwd, maxBuffer: 1 << 20, timeout: 8e3 }, (err, stdout) => {
999
+ execFile5("just", ["--list", "--unsorted"], { cwd: this.cwd, maxBuffer: 1 << 20, timeout: 8e3 }, (err, stdout) => {
797
1000
  if (err) {
798
1001
  resolve([]);
799
1002
  return;
@@ -918,8 +1121,20 @@ var JustRunner = class {
918
1121
  }
919
1122
  };
920
1123
 
1124
+ // src/core/read-body.ts
1125
+ async function readBody(req) {
1126
+ return new Promise((resolve) => {
1127
+ let data = "";
1128
+ req.on("data", (chunk) => {
1129
+ data += chunk;
1130
+ });
1131
+ req.on("end", () => resolve(data));
1132
+ req.on("error", () => resolve(""));
1133
+ });
1134
+ }
1135
+
921
1136
  // src/plugins/just/index.ts
922
- var apply2 = {
1137
+ var apply3 = {
923
1138
  inject: ["server", "dashboard"],
924
1139
  apply(ctx, config) {
925
1140
  const root = config.root;
@@ -999,19 +1214,9 @@ var apply2 = {
999
1214
  });
1000
1215
  }
1001
1216
  };
1002
- async function readBody(req) {
1003
- return new Promise((resolve) => {
1004
- let data = "";
1005
- req.on("data", (c) => {
1006
- data += c;
1007
- });
1008
- req.on("end", () => resolve(data));
1009
- req.on("error", () => resolve(""));
1010
- });
1011
- }
1012
1217
 
1013
1218
  // src/plugins/bugs/index.ts
1014
- var apply3 = {
1219
+ var apply4 = {
1015
1220
  inject: ["server", "dashboard"],
1016
1221
  apply(ctx, config) {
1017
1222
  const root = config.root;
@@ -1032,9 +1237,9 @@ var apply3 = {
1032
1237
  };
1033
1238
 
1034
1239
  // src/server/review-store.ts
1035
- import fs7 from "fs";
1036
- import path6 from "path";
1037
- import YAML from "yaml";
1240
+ import fs8 from "fs";
1241
+ import path7 from "path";
1242
+ import YAML2 from "yaml";
1038
1243
  var REVIEW_CANDIDATES = [".zdev/review.yaml", "review.yaml"];
1039
1244
  var ReviewStore = class {
1040
1245
  root;
@@ -1043,15 +1248,15 @@ var ReviewStore = class {
1043
1248
  constructor(root, onChange) {
1044
1249
  this.root = root;
1045
1250
  this.onChange = onChange;
1046
- const existing = REVIEW_CANDIDATES.find((rel) => fs7.existsSync(path6.join(root, rel)));
1047
- this.file = path6.join(root, existing ?? REVIEW_CANDIDATES[0]);
1251
+ const existing = REVIEW_CANDIDATES.find((rel) => fs8.existsSync(path7.join(root, rel)));
1252
+ this.file = path7.join(root, existing ?? REVIEW_CANDIDATES[0]);
1048
1253
  }
1049
1254
  exists() {
1050
- return fs7.existsSync(this.file);
1255
+ return fs8.existsSync(this.file);
1051
1256
  }
1052
1257
  read() {
1053
1258
  try {
1054
- const parsed = YAML.parse(fs7.readFileSync(this.file, "utf8"));
1259
+ const parsed = YAML2.parse(fs8.readFileSync(this.file, "utf8"));
1055
1260
  if (!parsed || !Array.isArray(parsed.items)) return { status: "draft", items: [] };
1056
1261
  return parsed;
1057
1262
  } catch {
@@ -1059,8 +1264,8 @@ var ReviewStore = class {
1059
1264
  }
1060
1265
  }
1061
1266
  write(data) {
1062
- fs7.mkdirSync(path6.dirname(this.file), { recursive: true });
1063
- fs7.writeFileSync(this.file, YAML.stringify(data), "utf8");
1267
+ fs8.mkdirSync(path7.dirname(this.file), { recursive: true });
1268
+ fs8.writeFileSync(this.file, YAML2.stringify(data), "utf8");
1064
1269
  this.onChange?.();
1065
1270
  }
1066
1271
  updateItem(id, patch) {
@@ -1084,9 +1289,9 @@ var ReviewStore = class {
1084
1289
  }
1085
1290
  docs() {
1086
1291
  try {
1087
- const zdevDir = path6.join(this.root, ".zdev");
1088
- if (!fs7.existsSync(zdevDir) || !fs7.statSync(zdevDir).isDirectory()) return [];
1089
- return fs7.readdirSync(zdevDir).filter((f) => /\.(md|markdown)$/i.test(f) && fs7.statSync(path6.join(zdevDir, f)).isFile()).sort();
1292
+ const zdevDir = path7.join(this.root, ".zdev");
1293
+ if (!fs8.existsSync(zdevDir) || !fs8.statSync(zdevDir).isDirectory()) return [];
1294
+ return fs8.readdirSync(zdevDir).filter((f) => /\.(md|markdown)$/i.test(f) && fs8.statSync(path7.join(zdevDir, f)).isFile()).sort();
1090
1295
  } catch {
1091
1296
  return [];
1092
1297
  }
@@ -1094,7 +1299,7 @@ var ReviewStore = class {
1094
1299
  };
1095
1300
 
1096
1301
  // src/plugins/review/index.ts
1097
- var apply4 = {
1302
+ var apply5 = {
1098
1303
  inject: ["server", "dashboard", "reload"],
1099
1304
  apply(ctx, config) {
1100
1305
  const root = config.root;
@@ -1115,7 +1320,7 @@ var apply4 = {
1115
1320
  return;
1116
1321
  }
1117
1322
  try {
1118
- const body = JSON.parse(await readBody2(req) || "{}");
1323
+ const body = JSON.parse(await readBody(req) || "{}");
1119
1324
  const data = reviewStore.updateItem(body.id, { answer: body.answer, state: body.state });
1120
1325
  res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
1121
1326
  res.end(JSON.stringify(data));
@@ -1131,7 +1336,7 @@ var apply4 = {
1131
1336
  return;
1132
1337
  }
1133
1338
  try {
1134
- const body = JSON.parse(await readBody2(req) || "{}");
1339
+ const body = JSON.parse(await readBody(req) || "{}");
1135
1340
  const data = reviewStore.setStatus(body.status);
1136
1341
  res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
1137
1342
  res.end(JSON.stringify(data));
@@ -1147,36 +1352,37 @@ var apply4 = {
1147
1352
  });
1148
1353
  }
1149
1354
  };
1150
- async function readBody2(req) {
1151
- return new Promise((resolve) => {
1152
- let data = "";
1153
- req.on("data", (c) => {
1154
- data += c;
1155
- });
1156
- req.on("end", () => resolve(data));
1157
- });
1158
- }
1159
1355
 
1160
1356
  // src/plugins/apply/scan.ts
1161
- import fs8 from "fs";
1162
- import path7 from "path";
1357
+ import fs9 from "fs";
1358
+ import path8 from "path";
1359
+
1360
+ // src/plugins/apply/parse-tasks.ts
1361
+ var TASK_RE = /^\s*-\s*\[([ xX])\]\s*(.*)$/;
1362
+ function parseTasks(md) {
1363
+ return md.split("\n").filter((l) => TASK_RE.test(l)).map((l) => {
1364
+ const m = l.match(TASK_RE);
1365
+ return { text: (m?.[2] ?? l).trim(), checked: m?.[1] !== " " };
1366
+ });
1367
+ }
1163
1368
  function countTasks(md) {
1164
- const all = (md.match(/^\s*-\s*\[[ xX]\]\s*/gm) || []).length;
1165
- const done = (md.match(/^\s*-\s*\[[xX]\]\s*/gm) || []).length;
1166
- return { total: all, done };
1369
+ const tasks = parseTasks(md);
1370
+ return { total: tasks.length, done: tasks.filter((t) => t.checked).length };
1167
1371
  }
1372
+
1373
+ // src/plugins/apply/scan.ts
1168
1374
  function readText(p) {
1169
1375
  try {
1170
- return fs8.readFileSync(p, "utf8");
1376
+ return fs9.readFileSync(p, "utf8");
1171
1377
  } catch {
1172
1378
  return "";
1173
1379
  }
1174
1380
  }
1175
1381
  function worktreeDir(root, name) {
1176
- return path7.join(root, ".zworktree", name);
1382
+ return path8.join(root, ".zworktree", name);
1177
1383
  }
1178
1384
  function changeDir(root, name) {
1179
- return path7.join(root, "openspec", "changes", name);
1385
+ return path8.join(root, "openspec", "changes", name);
1180
1386
  }
1181
1387
  function parseDependsOn(proposal) {
1182
1388
  const deps = [];
@@ -1190,42 +1396,42 @@ function parseDependsOn(proposal) {
1190
1396
  return deps;
1191
1397
  }
1192
1398
  function scanApplyChanges(root) {
1193
- const changesDir = path7.join(root, "openspec", "changes");
1194
- if (!fs8.existsSync(changesDir)) return [];
1399
+ const changesDir = path8.join(root, "openspec", "changes");
1400
+ if (!fs9.existsSync(changesDir)) return [];
1195
1401
  const out = [];
1196
- for (const ent of fs8.readdirSync(changesDir, { withFileTypes: true })) {
1402
+ for (const ent of fs9.readdirSync(changesDir, { withFileTypes: true })) {
1197
1403
  if (!ent.isDirectory() || ent.name.startsWith(".") || ent.name === "archive") continue;
1198
1404
  const wt = worktreeDir(root, ent.name);
1199
- const tasks = readText(path7.join(wt, "openspec", "changes", ent.name, "tasks.md"));
1405
+ const tasks = readText(path8.join(wt, "openspec", "changes", ent.name, "tasks.md"));
1200
1406
  if (!tasks) {
1201
- const fallback = readText(path7.join(changesDir, ent.name, "tasks.md"));
1407
+ const fallback = readText(path8.join(changesDir, ent.name, "tasks.md"));
1202
1408
  if (fallback) {
1203
1409
  const { total, done } = countTasks(fallback);
1204
- const wtChange = path7.join(wt, "openspec", "changes", ent.name);
1205
- const mainChange = path7.join(changesDir, ent.name);
1410
+ const wtChange = path8.join(wt, "openspec", "changes", ent.name);
1411
+ const mainChange = path8.join(changesDir, ent.name);
1206
1412
  out.push({
1207
1413
  name: ent.name,
1208
1414
  path: `openspec/changes/${ent.name}`,
1209
1415
  total,
1210
1416
  done,
1211
- hasProposal: fs8.existsSync(path7.join(wtChange, "proposal.md")) || fs8.existsSync(path7.join(mainChange, "proposal.md")),
1212
- hasDesign: fs8.existsSync(path7.join(wtChange, "design.md")) || fs8.existsSync(path7.join(mainChange, "design.md")),
1417
+ hasProposal: fs9.existsSync(path8.join(wtChange, "proposal.md")) || fs9.existsSync(path8.join(mainChange, "proposal.md")),
1418
+ hasDesign: fs9.existsSync(path8.join(wtChange, "design.md")) || fs9.existsSync(path8.join(mainChange, "design.md")),
1213
1419
  // worktree 目录存在即算执行中(tasks.md 可能尚未落盘)
1214
- inWorktree: fs8.existsSync(wtChange)
1420
+ inWorktree: fs9.existsSync(wtChange)
1215
1421
  });
1216
1422
  }
1217
1423
  } else {
1218
1424
  const { total, done } = countTasks(tasks);
1219
- const wtChange = path7.join(wt, "openspec", "changes", ent.name);
1220
- const mainChange = path7.join(changesDir, ent.name);
1425
+ const wtChange = path8.join(wt, "openspec", "changes", ent.name);
1426
+ const mainChange = path8.join(changesDir, ent.name);
1221
1427
  out.push({
1222
1428
  name: ent.name,
1223
1429
  path: `openspec/changes/${ent.name}`,
1224
1430
  total,
1225
1431
  done,
1226
1432
  // 任一侧存在即算(详情读取是 worktree 优先、主目录兜底,列表标记保持一致)
1227
- hasProposal: fs8.existsSync(path7.join(wtChange, "proposal.md")) || fs8.existsSync(path7.join(mainChange, "proposal.md")),
1228
- hasDesign: fs8.existsSync(path7.join(wtChange, "design.md")) || fs8.existsSync(path7.join(mainChange, "design.md")),
1433
+ hasProposal: fs9.existsSync(path8.join(wtChange, "proposal.md")) || fs9.existsSync(path8.join(mainChange, "proposal.md")),
1434
+ hasDesign: fs9.existsSync(path8.join(wtChange, "design.md")) || fs9.existsSync(path8.join(mainChange, "design.md")),
1229
1435
  inWorktree: true
1230
1436
  });
1231
1437
  }
@@ -1236,14 +1442,14 @@ function scanApplyChanges(root) {
1236
1442
  function readApplyChange(root, name) {
1237
1443
  const wt = worktreeDir(root, name);
1238
1444
  const mainChangeDir = changeDir(root, name);
1239
- 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");
1240
- 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");
1241
- 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");
1445
+ const proposalFrom = fs9.existsSync(path8.join(wt, "openspec", "changes", name, "proposal.md")) ? path8.join(wt, "openspec", "changes", name, "proposal.md") : path8.join(mainChangeDir, "proposal.md");
1446
+ const designFrom = fs9.existsSync(path8.join(wt, "openspec", "changes", name, "design.md")) ? path8.join(wt, "openspec", "changes", name, "design.md") : path8.join(mainChangeDir, "design.md");
1447
+ const tasksFrom = fs9.existsSync(path8.join(wt, "openspec", "changes", name, "tasks.md")) ? path8.join(wt, "openspec", "changes", name, "tasks.md") : path8.join(mainChangeDir, "tasks.md");
1242
1448
  const proposal = readText(proposalFrom);
1243
1449
  const design = readText(designFrom);
1244
1450
  const tasks = readText(tasksFrom);
1245
1451
  const { total, done } = countTasks(tasks);
1246
- const inWorktree = fs8.existsSync(path7.join(wt, "openspec", "changes", name));
1452
+ const inWorktree = fs9.existsSync(path8.join(wt, "openspec", "changes", name));
1247
1453
  const dependsOn = parseDependsOn(proposal);
1248
1454
  const hasTestStrategy = /^##\s+测试策略\s*$/m.test(design);
1249
1455
  return {
@@ -1263,54 +1469,7 @@ function readApplyChange(root, name) {
1263
1469
  }
1264
1470
 
1265
1471
  // src/plugins/apply/index.ts
1266
- import { execFile as execFile3 } from "child_process";
1267
- var GIT_TIMEOUT_MS = 5e3;
1268
- function gitWorktrees(root) {
1269
- return new Promise((resolve) => {
1270
- execFile3(
1271
- "git",
1272
- ["worktree", "list", "--porcelain"],
1273
- { cwd: root, timeout: GIT_TIMEOUT_MS },
1274
- (err, stdout) => {
1275
- if (err) return resolve([]);
1276
- const entries = [];
1277
- const lines = stdout.split("\n");
1278
- let current = {};
1279
- for (const line of lines) {
1280
- const m = line.match(/^(worktree|HEAD|branch|detached)\s+(.+)$/);
1281
- if (m) {
1282
- const [, key, val] = m;
1283
- if (key === "worktree") {
1284
- if (current.path && current.path.includes(".zworktree/")) {
1285
- entries.push({
1286
- path: current.path,
1287
- name: current.path.split("/.zworktree/").pop() ?? "",
1288
- branch: current.branch ?? "",
1289
- head: current.head ?? ""
1290
- });
1291
- }
1292
- current = { path: val, head: "", branch: "" };
1293
- } else if (key === "HEAD") {
1294
- current.head = val;
1295
- } else if (key === "branch") {
1296
- current.branch = val.replace(/^refs\/heads\//, "");
1297
- }
1298
- }
1299
- }
1300
- if (current.path && current.path.includes(".zworktree/")) {
1301
- entries.push({
1302
- path: current.path,
1303
- name: current.path.split("/.zworktree/").pop() ?? "",
1304
- branch: current.branch ?? "",
1305
- head: current.head ?? ""
1306
- });
1307
- }
1308
- resolve(entries);
1309
- }
1310
- );
1311
- });
1312
- }
1313
- var apply5 = {
1472
+ var apply6 = {
1314
1473
  inject: ["server", "dashboard"],
1315
1474
  apply(ctx, config) {
1316
1475
  const root = config.root;
@@ -1343,28 +1502,17 @@ var apply5 = {
1343
1502
  res.end(JSON.stringify({ error: e instanceof Error ? e.message : "unknown error" }));
1344
1503
  }
1345
1504
  });
1346
- ctx.server.route("/__worktrees", async (_req, res) => {
1347
- try {
1348
- const entries = await gitWorktrees(root);
1349
- res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
1350
- res.end(JSON.stringify(entries));
1351
- } catch {
1352
- res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
1353
- res.end(JSON.stringify([]));
1354
- }
1355
- });
1356
1505
  });
1357
1506
  }
1358
1507
  };
1359
1508
 
1360
1509
  // src/server/design-assets.ts
1361
- import fs9 from "fs";
1362
- import path8 from "path";
1510
+ import path9 from "path";
1363
1511
  var PAGE_EXTS = [".html", ".htm"];
1364
1512
  var ICON_EXTS = [".svg", ".png", ".ico", ".jpg", ".jpeg", ".gif", ".webp"];
1365
1513
  var VIDEO_EXTS = [".mp4", ".webm", ".mov", ".ogg", ".ogv"];
1366
1514
  var AUDIO_EXTS = [".mp3", ".wav", ".flac", ".aac", ".m4a"];
1367
- var CODE_EXTS = [".js", ".mjs", ".ts", ".tsx", ".jsx", ".css", ".json", ".txt", ".xml", ".yml", ".yaml", ".sh", ".md"];
1515
+ var CODE_EXTS = [".js", ".mjs", ".cjs", ".mts", ".cts", ".ts", ".tsx", ".jsx", ".css", ".json", ".txt", ".xml", ".yml", ".yaml", ".sh", ".md"];
1368
1516
  var FONT_EXTS = [".woff", ".woff2", ".ttf", ".otf"];
1369
1517
  var TOKEN_RE = /token|theme|design|color|palette|typograph/i;
1370
1518
  function categorize(rel, ext) {
@@ -1387,31 +1535,16 @@ function scanAssets(root) {
1387
1535
  const keys = ["page", "component", "icon", "token", "md", "video", "audio", "pdf", "font", "other"];
1388
1536
  for (const k of keys) out[k] = [];
1389
1537
  const SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "coverage", ".next", ".cache"]);
1390
- function walk(dir, rel) {
1391
- let ents;
1392
- try {
1393
- ents = fs9.readdirSync(dir, { withFileTypes: true });
1394
- } catch {
1395
- return;
1396
- }
1397
- for (const ent of ents) {
1398
- if (ent.name.startsWith(".") || SKIP.has(ent.name)) continue;
1399
- const r = rel ? `${rel}/${ent.name}` : ent.name;
1400
- if (ent.isDirectory()) {
1401
- walk(path8.join(dir, ent.name), r);
1402
- continue;
1403
- }
1404
- const ext = path8.extname(ent.name).toLowerCase();
1405
- const t = categorize(r, ext);
1406
- if (t) out[t].push({ path: r, name: ent.name, ext, type: t });
1407
- }
1408
- }
1409
- walk(root, "");
1538
+ walkDir(root, { skip: SKIP, onFile: (abs, rel) => {
1539
+ const ext = path9.extname(abs).toLowerCase();
1540
+ const t = categorize(rel, ext);
1541
+ if (t) out[t].push({ path: rel, name: path9.basename(abs), ext, type: t });
1542
+ } });
1410
1543
  return out;
1411
1544
  }
1412
1545
 
1413
1546
  // src/plugins/design/index.ts
1414
- var apply6 = {
1547
+ var apply7 = {
1415
1548
  inject: ["server", "dashboard"],
1416
1549
  apply(ctx, config) {
1417
1550
  const root = config.root;
@@ -1427,7 +1560,7 @@ var apply6 = {
1427
1560
  };
1428
1561
 
1429
1562
  // src/plugins/view/index.ts
1430
- var apply7 = {
1563
+ var apply8 = {
1431
1564
  inject: ["dashboard"],
1432
1565
  apply(ctx) {
1433
1566
  ctx.dashboard.register({ mode: "view", label: "\u9879\u76EE\u6D4F\u89C8", icon: "\u{1F441}\uFE0F", description: "openspec / docs / \u6587\u6863\u9884\u89C8" });
@@ -1436,53 +1569,42 @@ var apply7 = {
1436
1569
 
1437
1570
  // src/plugins/stats/index.ts
1438
1571
  import fs10 from "fs";
1439
- import path9 from "path";
1572
+ import path10 from "path";
1440
1573
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", ".cache"]);
1441
1574
  function scan(root) {
1442
1575
  const stats = {
1443
- root: path9.basename(root),
1576
+ root: path10.basename(root),
1444
1577
  files: 0,
1445
1578
  dirs: 0,
1446
1579
  totalSize: 0,
1447
1580
  byExt: [],
1448
1581
  markdown: 0,
1449
1582
  openspec: { active: 0, archived: 0 },
1450
- hasJust: fs10.existsSync(path9.join(root, "justfile"))
1583
+ hasJust: fs10.existsSync(path10.join(root, "justfile"))
1451
1584
  };
1452
1585
  const extMap = /* @__PURE__ */ new Map();
1453
1586
  const walk = (dir) => {
1454
- let ents;
1455
- try {
1456
- ents = fs10.readdirSync(dir, { withFileTypes: true });
1457
- } catch {
1458
- return;
1459
- }
1460
- for (const ent of ents) {
1461
- if (ent.name.startsWith(".") || SKIP_DIRS.has(ent.name)) continue;
1462
- const abs = path9.join(dir, ent.name);
1463
- if (ent.isDirectory()) {
1464
- stats.dirs++;
1465
- walk(abs);
1466
- continue;
1467
- }
1587
+ walkDir(dir, { skip: SKIP_DIRS, onDir: () => {
1588
+ stats.dirs++;
1589
+ }, onFile: (abs) => {
1468
1590
  stats.files++;
1469
1591
  try {
1470
1592
  stats.totalSize += fs10.statSync(abs).size;
1471
1593
  } catch {
1472
1594
  }
1473
- const ext = path9.extname(ent.name).toLowerCase() || "(\u65E0\u6269\u5C55\u540D)";
1595
+ const ext = path10.extname(abs).toLowerCase() || "(\u65E0\u6269\u5C55\u540D)";
1474
1596
  extMap.set(ext, (extMap.get(ext) ?? 0) + 1);
1475
1597
  if (ext === ".md" || ext === ".markdown") stats.markdown++;
1476
- }
1598
+ } });
1477
1599
  };
1478
1600
  walk(root);
1479
- const changesDir = path9.join(root, "openspec", "changes");
1601
+ const changesDir = path10.join(root, "openspec", "changes");
1480
1602
  if (fs10.existsSync(changesDir)) {
1481
1603
  for (const ent of fs10.readdirSync(changesDir, { withFileTypes: true })) {
1482
1604
  if (!ent.isDirectory() || ent.name.startsWith(".")) continue;
1483
1605
  if (ent.name === "archive") {
1484
1606
  try {
1485
- stats.openspec.archived = fs10.readdirSync(path9.join(changesDir, "archive"), { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).length;
1607
+ stats.openspec.archived = fs10.readdirSync(path10.join(changesDir, "archive"), { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).length;
1486
1608
  } catch {
1487
1609
  }
1488
1610
  } else stats.openspec.active++;
@@ -1491,7 +1613,7 @@ function scan(root) {
1491
1613
  stats.byExt = Array.from(extMap.entries()).map(([ext, count]) => ({ ext, count })).sort((a, b) => b.count - a.count).slice(0, 10);
1492
1614
  return stats;
1493
1615
  }
1494
- var apply8 = {
1616
+ var apply9 = {
1495
1617
  inject: ["server", "dashboard"],
1496
1618
  apply(ctx, config) {
1497
1619
  ctx.inject(["server", "dashboard"], () => {
@@ -1513,34 +1635,31 @@ var apply8 = {
1513
1635
  };
1514
1636
 
1515
1637
  // src/cli.ts
1516
- var __dirname2 = path10.dirname(fileURLToPath2(import.meta.url));
1638
+ var __dirname2 = path11.dirname(fileURLToPath2(import.meta.url));
1517
1639
  var DEFAULT_PORT = 4190;
1518
1640
  function parseArgs() {
1519
1641
  const args = process.argv.slice(2);
1520
- const opts = {};
1521
- let portExplicit = false;
1522
- for (let i = 0; i < args.length; i++) {
1523
- const a = args[i];
1524
- if (a.startsWith("--")) {
1525
- const key = a.slice(2);
1526
- const next = args[i + 1];
1527
- if (next && !next.startsWith("--")) {
1528
- opts[key] = next;
1529
- i++;
1530
- } else {
1531
- opts[key] = true;
1532
- }
1533
- if (key === "port") portExplicit = true;
1534
- }
1535
- }
1642
+ const { values } = utilParseArgs({
1643
+ options: {
1644
+ dir: { type: "string" },
1645
+ port: { type: "string" },
1646
+ open: { type: "boolean", default: false },
1647
+ page: { type: "string" },
1648
+ restart: { type: "boolean", default: false },
1649
+ plugins: { type: "string" }
1650
+ },
1651
+ args,
1652
+ strict: false,
1653
+ allowPositionals: false
1654
+ });
1536
1655
  return {
1537
- dir: typeof opts.dir === "string" ? opts.dir : process.cwd(),
1538
- port: typeof opts.port === "string" ? Number(opts.port) : DEFAULT_PORT,
1539
- portExplicit,
1540
- open: !!opts.open,
1541
- page: typeof opts.page === "string" ? opts.page : null,
1542
- restart: !!opts.restart,
1543
- plugins: typeof opts.plugins === "string" ? opts.plugins : null
1656
+ dir: typeof values.dir === "string" ? values.dir : process.cwd(),
1657
+ port: typeof values.port === "string" ? Number(values.port) : DEFAULT_PORT,
1658
+ portExplicit: args.some((a) => a === "--port" || a.startsWith("--port=")),
1659
+ open: !!values.open,
1660
+ page: typeof values.page === "string" ? values.page : null,
1661
+ restart: !!values.restart,
1662
+ plugins: typeof values.plugins === "string" ? values.plugins : null
1544
1663
  };
1545
1664
  }
1546
1665
  async function pathExists(p) {
@@ -1566,7 +1685,7 @@ async function loadExternal(ctx, dir, root) {
1566
1685
  if (!ent.isDirectory()) continue;
1567
1686
  const candidates = tsxLoaded ? ["index.ts", "index.js", "index.mjs"] : ["index.js", "index.mjs"];
1568
1687
  for (const name of candidates) {
1569
- const p = path10.join(dir, ent.name, name);
1688
+ const p = path11.join(dir, ent.name, name);
1570
1689
  if (await pathExists(p)) {
1571
1690
  try {
1572
1691
  const mod = await import(p);
@@ -1581,8 +1700,8 @@ async function loadExternal(ctx, dir, root) {
1581
1700
  const m = ctx.dashboard.get(ent.name);
1582
1701
  if (m && m.mode === ent.name) {
1583
1702
  const patch = { external: true };
1584
- const webDir = path10.join(dir, ent.name, "web");
1585
- if (await pathExists(path10.join(webDir, "index.html"))) {
1703
+ const webDir = path11.join(dir, ent.name, "web");
1704
+ if (await pathExists(path11.join(webDir, "index.html"))) {
1586
1705
  ctx.server.static(`${PLUGIN_STATIC_PREFIX}${ent.name}/`, webDir);
1587
1706
  if (!m.viewerUrl) patch.viewerUrl = `${PLUGIN_STATIC_PREFIX}${ent.name}/`;
1588
1707
  }
@@ -1601,7 +1720,7 @@ async function loadExternal(ctx, dir, root) {
1601
1720
  }
1602
1721
  async function main() {
1603
1722
  const args = parseArgs();
1604
- const root = path10.resolve(args.dir);
1723
+ const root = path11.resolve(args.dir);
1605
1724
  const existing = await findReusable(root);
1606
1725
  if (existing && !args.restart) {
1607
1726
  const u = `http://localhost:${existing.port}` + (args.page ? `#${args.page}` : "");
@@ -1615,9 +1734,9 @@ async function main() {
1615
1734
  console.log(`[zdashboard] --restart\uFF1A\u505C\u6B62\u65E7\u5B9E\u4F8B pid=${oldRecord.pid}`);
1616
1735
  await stopInstance(oldRecord);
1617
1736
  }
1618
- const appDir = path10.resolve(__dirname2, "web");
1737
+ const appDir = path11.resolve(__dirname2, "web");
1619
1738
  const startPort = oldRecord && !args.portExplicit ? oldRecord.port : args.port;
1620
- const zdevDir = path10.join(root, ".zdev");
1739
+ const zdevDir = path11.join(root, ".zdev");
1621
1740
  const dataDir = fs11.existsSync(zdevDir) ? ".zdev/" : "";
1622
1741
  const det = await detect(root);
1623
1742
  const ctx = new Context();
@@ -1633,15 +1752,16 @@ async function main() {
1633
1752
  });
1634
1753
  ctx.plugin(ReloadService, { root });
1635
1754
  ctx.plugin(apply, { root });
1755
+ ctx.plugin(apply2, { root });
1636
1756
  ctx.plugin(DashboardService);
1637
1757
  const plugins = [
1638
- { name: "stats", apply: apply8 },
1639
- { name: "just", apply: apply2 },
1640
- { name: "bugs", apply: apply3 },
1641
- { name: "review", apply: apply4 },
1642
- { name: "apply", apply: apply5 },
1643
- { name: "design", apply: apply6 },
1644
- { name: "view", apply: apply7 }
1758
+ { name: "stats", apply: apply9 },
1759
+ { name: "just", apply: apply3 },
1760
+ { name: "bugs", apply: apply4 },
1761
+ { name: "review", apply: apply5 },
1762
+ { name: "apply", apply: apply6 },
1763
+ { name: "design", apply: apply7 },
1764
+ { name: "view", apply: apply8 }
1645
1765
  ];
1646
1766
  for (const p of plugins) {
1647
1767
  try {
@@ -1651,7 +1771,7 @@ async function main() {
1651
1771
  }
1652
1772
  }
1653
1773
  if (args.plugins) {
1654
- await loadExternal(ctx, path10.resolve(args.plugins), root);
1774
+ await loadExternal(ctx, path11.resolve(args.plugins), root);
1655
1775
  }
1656
1776
  const shutdown = () => {
1657
1777
  try {