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