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.
- package/dist/cli.js +400 -280
- package/dist/cli.js.map +1 -1
- package/dist/web/assets/{web-CUFkwTcN.js → CodeViewer-D8YqzmwB.js} +8 -23
- package/dist/web/assets/FilterPills-A_fz1nby.js +1 -0
- package/dist/web/assets/{katex.min-BvZgSYSU.js → MdViewer-P4wmRzy_.js} +24 -24
- package/dist/web/assets/ProgressBar-B4U41etC.js +1 -0
- package/dist/web/assets/Workspace-nm0YyxF7.js +1 -0
- package/dist/web/assets/badge-C92Y2FRG.js +1 -0
- package/dist/web/assets/{file-text-B46O-IoU.js → file-text-BHzQwEJK.js} +1 -1
- package/dist/web/assets/index-ByI0Pghw.css +1 -0
- package/dist/web/assets/index-CNPKNdvp.js +111 -0
- package/dist/web/assets/{index-RpDccu4v.js → index-C_wTjWH0.js} +1 -1
- package/dist/web/assets/web-5tq6UJuk.js +2 -0
- package/dist/web/assets/web-DgYHvMu9.js +16 -0
- package/dist/web/assets/web-DhyfyLB-.js +11 -0
- package/dist/web/assets/web-JxMZZReP.js +2 -0
- package/dist/web/assets/web-ZQ0eDR1y.js +31 -0
- package/dist/web/assets/web-vTYFh6Bo.js +25 -0
- package/dist/web/assets/web-vbs6AnjC.js +26 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/MdViewer-CgSHNqC3.js +0 -1
- package/dist/web/assets/Workspace-BjAcIoe8.js +0 -1
- package/dist/web/assets/index-CEza9SOJ.css +0 -1
- package/dist/web/assets/index-CcIhCGOi.js +0 -90
- package/dist/web/assets/web-BG3uPf-s.js +0 -12
- package/dist/web/assets/web-BH8k8F-d.js +0 -26
- package/dist/web/assets/web-CB5F-y_R.js +0 -2
- package/dist/web/assets/web-CTWlKjBz.js +0 -32
- package/dist/web/assets/web-D6ODdMUf.js +0 -31
- package/dist/web/assets/web-DiGxrTCi.js +0 -11
- /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
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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.
|
|
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
|
-
|
|
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(
|
|
447
|
-
this.routes.set(
|
|
448
|
-
this.ctx.effect(() => () => this.routes.delete(
|
|
462
|
+
route(path12, handler) {
|
|
463
|
+
this.routes.set(path12, handler);
|
|
464
|
+
this.ctx.effect(() => () => this.routes.delete(path12));
|
|
449
465
|
}
|
|
450
|
-
sse(
|
|
451
|
-
this.sses.set(
|
|
452
|
-
this.ctx.effect(() => () => this.sses.delete(
|
|
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.
|
|
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
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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
|
-
|
|
472
|
-
|
|
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
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
ents
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
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
|
-
|
|
679
|
-
|
|
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 &&
|
|
684
|
-
const changesDir =
|
|
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
|
|
800
|
+
for (const ent of fs7.readdirSync(changesDir, { withFileTypes: true })) {
|
|
688
801
|
if (!ent.isDirectory() || ent.name.startsWith(".") || ent.name === "archive") continue;
|
|
689
|
-
|
|
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 =
|
|
693
|
-
if (
|
|
694
|
-
for (const ent of
|
|
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
|
-
|
|
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:
|
|
701
|
-
if (archived.length) tree.push({ name:
|
|
702
|
-
const specsDir =
|
|
703
|
-
if (
|
|
704
|
-
const
|
|
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 &&
|
|
709
|
-
const
|
|
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
|
|
840
|
+
for (const ent of fs7.readdirSync(root, { withFileTypes: true })) {
|
|
716
841
|
if (ent.name.startsWith(".") || skip.has(ent.name)) continue;
|
|
717
|
-
const ext =
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
1036
|
-
import
|
|
1037
|
-
import
|
|
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) =>
|
|
1047
|
-
this.file =
|
|
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
|
|
1255
|
+
return fs8.existsSync(this.file);
|
|
1051
1256
|
}
|
|
1052
1257
|
read() {
|
|
1053
1258
|
try {
|
|
1054
|
-
const parsed =
|
|
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
|
-
|
|
1063
|
-
|
|
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 =
|
|
1088
|
-
if (!
|
|
1089
|
-
return
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1162
|
-
import
|
|
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
|
|
1165
|
-
|
|
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
|
|
1376
|
+
return fs9.readFileSync(p, "utf8");
|
|
1171
1377
|
} catch {
|
|
1172
1378
|
return "";
|
|
1173
1379
|
}
|
|
1174
1380
|
}
|
|
1175
1381
|
function worktreeDir(root, name) {
|
|
1176
|
-
return
|
|
1382
|
+
return path8.join(root, ".zworktree", name);
|
|
1177
1383
|
}
|
|
1178
1384
|
function changeDir(root, name) {
|
|
1179
|
-
return
|
|
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 =
|
|
1194
|
-
if (!
|
|
1399
|
+
const changesDir = path8.join(root, "openspec", "changes");
|
|
1400
|
+
if (!fs9.existsSync(changesDir)) return [];
|
|
1195
1401
|
const out = [];
|
|
1196
|
-
for (const ent of
|
|
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(
|
|
1405
|
+
const tasks = readText(path8.join(wt, "openspec", "changes", ent.name, "tasks.md"));
|
|
1200
1406
|
if (!tasks) {
|
|
1201
|
-
const fallback = readText(
|
|
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 =
|
|
1205
|
-
const mainChange =
|
|
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:
|
|
1212
|
-
hasDesign:
|
|
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:
|
|
1420
|
+
inWorktree: fs9.existsSync(wtChange)
|
|
1215
1421
|
});
|
|
1216
1422
|
}
|
|
1217
1423
|
} else {
|
|
1218
1424
|
const { total, done } = countTasks(tasks);
|
|
1219
|
-
const wtChange =
|
|
1220
|
-
const mainChange =
|
|
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:
|
|
1228
|
-
hasDesign:
|
|
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 =
|
|
1240
|
-
const designFrom =
|
|
1241
|
-
const tasksFrom =
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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(
|
|
1583
|
+
hasJust: fs10.existsSync(path10.join(root, "justfile"))
|
|
1451
1584
|
};
|
|
1452
1585
|
const extMap = /* @__PURE__ */ new Map();
|
|
1453
1586
|
const walk = (dir) => {
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
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 =
|
|
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 =
|
|
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(
|
|
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
|
|
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 =
|
|
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
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
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
|
|
1538
|
-
port: typeof
|
|
1539
|
-
portExplicit,
|
|
1540
|
-
open: !!
|
|
1541
|
-
page: typeof
|
|
1542
|
-
restart: !!
|
|
1543
|
-
plugins: typeof
|
|
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 =
|
|
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 =
|
|
1585
|
-
if (await pathExists(
|
|
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 =
|
|
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 =
|
|
1737
|
+
const appDir = path11.resolve(__dirname2, "web");
|
|
1619
1738
|
const startPort = oldRecord && !args.portExplicit ? oldRecord.port : args.port;
|
|
1620
|
-
const zdevDir =
|
|
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:
|
|
1639
|
-
{ name: "just", apply:
|
|
1640
|
-
{ name: "bugs", apply:
|
|
1641
|
-
{ name: "review", apply:
|
|
1642
|
-
{ name: "apply", apply:
|
|
1643
|
-
{ name: "design", apply:
|
|
1644
|
-
{ name: "view", apply:
|
|
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,
|
|
1774
|
+
await loadExternal(ctx, path11.resolve(args.plugins), root);
|
|
1655
1775
|
}
|
|
1656
1776
|
const shutdown = () => {
|
|
1657
1777
|
try {
|