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