zdashboard 2.1.0 → 2.1.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 +23 -1
- package/dist/cli.js +416 -282
- package/dist/cli.js.map +1 -1
- package/dist/web/assets/{MdViewer-C8ZG_xn1.js → MdViewer-CgSHNqC3.js} +1 -1
- package/dist/web/assets/Workspace-BjAcIoe8.js +1 -0
- package/dist/web/assets/{file-text-DqCOBCjz.js → file-text-B46O-IoU.js} +1 -1
- package/dist/web/assets/index-CEza9SOJ.css +1 -0
- package/dist/web/assets/{index-DkOVCCZ3.js → index-CcIhCGOi.js} +21 -21
- package/dist/web/assets/{index-B9eiA0v2.js → index-RpDccu4v.js} +1 -1
- package/dist/web/assets/{katex.min-DAmb8ZJI.js → katex.min-BvZgSYSU.js} +1 -1
- package/dist/web/assets/web-BG3uPf-s.js +12 -0
- package/dist/web/assets/web-BH8k8F-d.js +26 -0
- package/dist/web/assets/web-CB5F-y_R.js +2 -0
- package/dist/web/assets/web-CTWlKjBz.js +32 -0
- package/dist/web/assets/{web-CXHaDUKR.js → web-CUFkwTcN.js} +11 -11
- package/dist/web/assets/web-D6ODdMUf.js +31 -0
- package/dist/web/assets/{web-Y4h9oNHC.js → web-DiGxrTCi.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/Workspace-Cz1A_IXM.js +0 -1
- package/dist/web/assets/index-D58_bWw5.css +0 -1
- package/dist/web/assets/web-8ZBVzAEO.js +0 -26
- package/dist/web/assets/web-CEyEoz0i.js +0 -30
- package/dist/web/assets/web-CrX-i-rZ.js +0 -7
- package/dist/web/assets/web-D_b3sVrp.js +0 -31
- package/dist/web/assets/web-M4KeFgZw.js +0 -2
package/dist/cli.js
CHANGED
|
@@ -2,13 +2,137 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import path10 from "path";
|
|
5
|
+
import fs11 from "fs";
|
|
5
6
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
6
7
|
import { access, readdir } from "fs/promises";
|
|
7
8
|
|
|
8
9
|
// src/server/detect.ts
|
|
10
|
+
import fs2 from "fs";
|
|
11
|
+
import path2 from "path";
|
|
12
|
+
import { execFile } from "child_process";
|
|
13
|
+
|
|
14
|
+
// src/server/bugs.ts
|
|
9
15
|
import fs from "fs";
|
|
10
16
|
import path from "path";
|
|
11
|
-
|
|
17
|
+
|
|
18
|
+
// src/server/api/fetch.ts
|
|
19
|
+
import ky from "ky";
|
|
20
|
+
|
|
21
|
+
// src/server/errors.ts
|
|
22
|
+
var HttpError = class extends Error {
|
|
23
|
+
constructor(status, message, body) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.status = status;
|
|
26
|
+
this.body = body;
|
|
27
|
+
this.name = "HttpError";
|
|
28
|
+
}
|
|
29
|
+
status;
|
|
30
|
+
body;
|
|
31
|
+
};
|
|
32
|
+
var NetworkError = class extends Error {
|
|
33
|
+
constructor(message, cause) {
|
|
34
|
+
super(message);
|
|
35
|
+
this.cause = cause;
|
|
36
|
+
this.name = "NetworkError";
|
|
37
|
+
}
|
|
38
|
+
cause;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// src/server/api/fetch.ts
|
|
42
|
+
async function fetchJson(url, init) {
|
|
43
|
+
try {
|
|
44
|
+
const res = await ky(url, { ...init, timeout: 8e3, retry: 2 });
|
|
45
|
+
return await res.json();
|
|
46
|
+
} catch (e) {
|
|
47
|
+
if (e instanceof HttpError) throw e;
|
|
48
|
+
if (e instanceof Error && e.name === "TimeoutError") {
|
|
49
|
+
throw new NetworkError(`\u8BF7\u6C42\u8D85\u65F6: ${url}`);
|
|
50
|
+
}
|
|
51
|
+
if (e instanceof Error && e.name === "HTTPError") {
|
|
52
|
+
throw new HttpError(e.status ?? 500, e.message);
|
|
53
|
+
}
|
|
54
|
+
throw new NetworkError(`\u8BF7\u6C42\u5931\u8D25: ${url}`, e);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// src/server/bugs.ts
|
|
59
|
+
var BUGS_CONFIG_CANDIDATES = [".zdev/config.yaml", ".zgoal/config.yaml"];
|
|
60
|
+
function loadConfig(root) {
|
|
61
|
+
for (const rel of BUGS_CONFIG_CANDIDATES) {
|
|
62
|
+
const file = path.join(root, rel);
|
|
63
|
+
if (!fs.existsSync(file)) continue;
|
|
64
|
+
const kv = {};
|
|
65
|
+
for (const line of fs.readFileSync(file, "utf8").split("\n")) {
|
|
66
|
+
const m = line.match(/^\s*([A-Za-z_]\w*)\s*:\s*(.+?)\s*$/);
|
|
67
|
+
if (m && !m[2].startsWith("#")) kv[m[1]] = m[2].replace(/^["']|["']$/g, "");
|
|
68
|
+
}
|
|
69
|
+
const product = Number(kv.product);
|
|
70
|
+
if (!kv.url || !product) return null;
|
|
71
|
+
return {
|
|
72
|
+
url: kv.url.replace(/\/+$/, ""),
|
|
73
|
+
account: kv.account ?? "",
|
|
74
|
+
password: kv.password,
|
|
75
|
+
token: kv.token,
|
|
76
|
+
product
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
var tokenCache = null;
|
|
82
|
+
async function getToken(cfg) {
|
|
83
|
+
if (cfg.token) return cfg.token;
|
|
84
|
+
const key = `${cfg.url}|${cfg.account}|${cfg.password ?? ""}`;
|
|
85
|
+
if (tokenCache && tokenCache.key === key && Date.now() - tokenCache.at < 10 * 6e4) return tokenCache.token;
|
|
86
|
+
const json = await fetchJson(`${cfg.url}/api.php/v1/tokens`, {
|
|
87
|
+
method: "POST",
|
|
88
|
+
headers: { "Content-Type": "application/json" },
|
|
89
|
+
body: JSON.stringify({ account: cfg.account, password: cfg.password })
|
|
90
|
+
});
|
|
91
|
+
const token = typeof json.token === "string" ? json.token : "";
|
|
92
|
+
if (!token) throw new Error("token \u83B7\u53D6\u5931\u8D25:\u68C0\u67E5 account / password");
|
|
93
|
+
tokenCache = { key, token, at: Date.now() };
|
|
94
|
+
return token;
|
|
95
|
+
}
|
|
96
|
+
function normBug(b, account) {
|
|
97
|
+
const assigned = b.assignedTo;
|
|
98
|
+
const assignedTo = typeof assigned === "string" ? assigned : assigned && typeof assigned === "object" && "realname" in assigned ? String(assigned.realname ?? "") : "";
|
|
99
|
+
const assignedAccount = typeof assigned === "string" ? assigned : assigned && typeof assigned === "object" ? String(assigned.account ?? "") : "";
|
|
100
|
+
const mine = !!account && (assignedAccount === account || assignedTo === account);
|
|
101
|
+
return {
|
|
102
|
+
id: Number(b.id),
|
|
103
|
+
title: String(b.title ?? ""),
|
|
104
|
+
severity: b.severity ?? 4,
|
|
105
|
+
pri: b.pri ?? 3,
|
|
106
|
+
status: String(b.status ?? ""),
|
|
107
|
+
assignedTo,
|
|
108
|
+
openedBy: typeof b.openedBy === "string" ? b.openedBy : void 0,
|
|
109
|
+
mine
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
async function fetchBugs(root) {
|
|
113
|
+
const cfg = loadConfig(root);
|
|
114
|
+
if (!cfg) {
|
|
115
|
+
const anyExists = BUGS_CONFIG_CANDIDATES.some((rel) => fs.existsSync(path.join(root, rel)));
|
|
116
|
+
return {
|
|
117
|
+
ok: false,
|
|
118
|
+
error: anyExists ? "\u914D\u7F6E\u5B58\u5728\u4F46\u65E0\u6548(\u7F3A url \u6216 product \u5B57\u6BB5)\u2014\u2014\u68C0\u67E5 .zdev/config.yaml" : ".zdev/config.yaml \u7F3A\u5931(\u7531 zgoal skill \u521B\u5EFA;\u5B58\u91CF .zgoal/config.yaml \u4EA6\u53EF)"
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
const token = await getToken(cfg);
|
|
123
|
+
const json = await fetchJson(
|
|
124
|
+
`${cfg.url}/api.php/v1/products/${cfg.product}/bugs?page=1&limit=100`,
|
|
125
|
+
{ headers: { Token: token } }
|
|
126
|
+
);
|
|
127
|
+
const raw = Array.isArray(json.bugs) ? json.bugs : [];
|
|
128
|
+
return { ok: true, url: cfg.url, total: Number(json.total ?? raw.length), bugs: raw.map((b) => normBug(b, cfg.account)) };
|
|
129
|
+
} catch (e) {
|
|
130
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
131
|
+
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` };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// src/server/detect.ts
|
|
12
136
|
function justAvailable(cwd) {
|
|
13
137
|
return new Promise((resolve) => {
|
|
14
138
|
const child = execFile("just", ["--list", "--unsorted"], { cwd, timeout: 5e3 }, (err) => {
|
|
@@ -18,10 +142,10 @@ function justAvailable(cwd) {
|
|
|
18
142
|
});
|
|
19
143
|
}
|
|
20
144
|
async function detect(root) {
|
|
21
|
-
const hasOpenspec =
|
|
22
|
-
const hasDocs =
|
|
145
|
+
const hasOpenspec = fs2.existsSync(path2.join(root, "openspec"));
|
|
146
|
+
const hasDocs = fs2.existsSync(path2.join(root, "docs"));
|
|
23
147
|
const hasJust = await justAvailable(root);
|
|
24
|
-
const hasBugs =
|
|
148
|
+
const hasBugs = BUGS_CONFIG_CANDIDATES.some((rel) => fs2.existsSync(path2.join(root, rel)));
|
|
25
149
|
return { hasOpenspec, hasDocs, hasJust, hasBugs };
|
|
26
150
|
}
|
|
27
151
|
|
|
@@ -30,15 +154,15 @@ import { Context } from "cordis";
|
|
|
30
154
|
|
|
31
155
|
// src/core/server.ts
|
|
32
156
|
import http from "http";
|
|
33
|
-
import
|
|
34
|
-
import
|
|
157
|
+
import fs4 from "fs";
|
|
158
|
+
import path4 from "path";
|
|
35
159
|
import crypto from "crypto";
|
|
36
160
|
import { fileURLToPath } from "url";
|
|
37
161
|
|
|
38
162
|
// package.json
|
|
39
163
|
var package_default = {
|
|
40
164
|
name: "zdashboard",
|
|
41
|
-
version: "2.1.
|
|
165
|
+
version: "2.1.1",
|
|
42
166
|
description: "ZCode skill dashboard platform \u2014 pluggable viewers for zdesign/zview/zreview/zgoal",
|
|
43
167
|
type: "module",
|
|
44
168
|
bin: {
|
|
@@ -124,20 +248,20 @@ var package_default = {
|
|
|
124
248
|
import { Service } from "cordis";
|
|
125
249
|
|
|
126
250
|
// src/core/instance.ts
|
|
127
|
-
import
|
|
128
|
-
import
|
|
251
|
+
import fs3 from "fs";
|
|
252
|
+
import path3 from "path";
|
|
129
253
|
var RECORD_FILE = ".zdev/dashboard.json";
|
|
130
254
|
var VERIFY_TIMEOUT_MS = 1500;
|
|
131
255
|
var STOP_POLL_MS = 2e3;
|
|
132
256
|
var STOP_POLL_INTERVAL_MS = 100;
|
|
133
257
|
var STOP_FINAL_WAIT_MS = 100;
|
|
134
258
|
function recordPath(root) {
|
|
135
|
-
return
|
|
259
|
+
return path3.join(root, RECORD_FILE);
|
|
136
260
|
}
|
|
137
261
|
function readRecord(root) {
|
|
138
262
|
try {
|
|
139
263
|
const fp = recordPath(root);
|
|
140
|
-
const raw =
|
|
264
|
+
const raw = fs3.readFileSync(fp, "utf-8");
|
|
141
265
|
const rec = JSON.parse(raw);
|
|
142
266
|
if (typeof rec.pid === "number" && typeof rec.port === "number" && typeof rec.root === "string" && typeof rec.startedAt === "string") {
|
|
143
267
|
return rec;
|
|
@@ -148,20 +272,20 @@ function readRecord(root) {
|
|
|
148
272
|
}
|
|
149
273
|
}
|
|
150
274
|
function writeRecord(root, port) {
|
|
151
|
-
|
|
275
|
+
fs3.mkdirSync(path3.dirname(recordPath(root)), { recursive: true });
|
|
152
276
|
const rec = {
|
|
153
277
|
pid: process.pid,
|
|
154
278
|
port,
|
|
155
279
|
root,
|
|
156
280
|
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
157
281
|
};
|
|
158
|
-
|
|
282
|
+
fs3.writeFileSync(recordPath(root), JSON.stringify(rec, null, 2) + "\n");
|
|
159
283
|
}
|
|
160
284
|
function clearRecord(root) {
|
|
161
285
|
try {
|
|
162
286
|
const rec = readRecord(root);
|
|
163
287
|
if (rec && rec.pid !== process.pid) return;
|
|
164
|
-
|
|
288
|
+
fs3.unlinkSync(recordPath(root));
|
|
165
289
|
} catch {
|
|
166
290
|
}
|
|
167
291
|
}
|
|
@@ -221,14 +345,14 @@ async function stopInstance(record) {
|
|
|
221
345
|
|
|
222
346
|
// src/core/open-url.ts
|
|
223
347
|
import { exec } from "child_process";
|
|
224
|
-
function
|
|
348
|
+
function openUrl(url) {
|
|
225
349
|
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
226
350
|
exec(`${cmd} ${url}`);
|
|
227
351
|
}
|
|
228
352
|
|
|
229
353
|
// src/core/server.ts
|
|
230
354
|
var VERSION = package_default.version;
|
|
231
|
-
var __dirname =
|
|
355
|
+
var __dirname = path4.dirname(fileURLToPath(import.meta.url));
|
|
232
356
|
var MIME = {
|
|
233
357
|
".html": "text/html; charset=utf-8",
|
|
234
358
|
".htm": "text/html; charset=utf-8",
|
|
@@ -286,6 +410,7 @@ var ServerService = class extends Service {
|
|
|
286
410
|
open;
|
|
287
411
|
page;
|
|
288
412
|
det;
|
|
413
|
+
dataDir;
|
|
289
414
|
routes = /* @__PURE__ */ new Map();
|
|
290
415
|
sses = /* @__PURE__ */ new Map();
|
|
291
416
|
prefixStatic = /* @__PURE__ */ new Map();
|
|
@@ -299,6 +424,7 @@ var ServerService = class extends Service {
|
|
|
299
424
|
this.open = config.open;
|
|
300
425
|
this.page = config.page;
|
|
301
426
|
this.det = config.detect;
|
|
427
|
+
this.dataDir = config.dataDir;
|
|
302
428
|
this.onListen = config.onListen;
|
|
303
429
|
ctx.effect(() => () => this.dispose());
|
|
304
430
|
this.route("/__config", (_req, res) => {
|
|
@@ -330,12 +456,12 @@ var ServerService = class extends Service {
|
|
|
330
456
|
this.ctx.effect(() => () => this.prefixStatic.delete(prefix));
|
|
331
457
|
}
|
|
332
458
|
serveFile(filePath, res, injectHtml) {
|
|
333
|
-
|
|
459
|
+
fs4.readFile(filePath, (err, data) => {
|
|
334
460
|
if (err) {
|
|
335
461
|
res.writeHead(404);
|
|
336
462
|
return res.end("Not found");
|
|
337
463
|
}
|
|
338
|
-
const ext =
|
|
464
|
+
const ext = path4.extname(filePath).toLowerCase();
|
|
339
465
|
const ct = MIME[ext] ?? "application/octet-stream";
|
|
340
466
|
let body = data;
|
|
341
467
|
if (injectHtml && ext === ".html") {
|
|
@@ -350,9 +476,9 @@ var ServerService = class extends Service {
|
|
|
350
476
|
for (const [prefix, dir] of this.prefixStatic) {
|
|
351
477
|
if (url.indexOf(prefix) === 0) {
|
|
352
478
|
let rel = this.safeDecode(url.slice(prefix.length));
|
|
353
|
-
if (!rel || rel === "/" || !
|
|
354
|
-
const fp =
|
|
355
|
-
if (fp.indexOf(dir +
|
|
479
|
+
if (!rel || rel === "/" || !path4.extname(rel)) rel = path4.join(rel || "", "index.html");
|
|
480
|
+
const fp = path4.join(dir, rel);
|
|
481
|
+
if (fp.indexOf(dir + path4.sep) !== 0) {
|
|
356
482
|
res.writeHead(403);
|
|
357
483
|
return res.end("Forbidden");
|
|
358
484
|
}
|
|
@@ -391,17 +517,17 @@ var ServerService = class extends Service {
|
|
|
391
517
|
if (this.servePrefix(url, res)) return;
|
|
392
518
|
if (url === "/" || url.indexOf("/__app/") === 0 || url.indexOf("/assets/") === 0) {
|
|
393
519
|
let fp2 = this.appDir;
|
|
394
|
-
if (url !== "/") fp2 =
|
|
395
|
-
if (url.indexOf("/__app/") === 0) fp2 =
|
|
396
|
-
if (fp2 !== this.appDir && fp2.indexOf(this.appDir +
|
|
520
|
+
if (url !== "/") fp2 = path4.join(this.appDir, this.safeDecode(url));
|
|
521
|
+
if (url.indexOf("/__app/") === 0) fp2 = path4.join(this.appDir, url.slice(7));
|
|
522
|
+
if (fp2 !== this.appDir && fp2.indexOf(this.appDir + path4.sep) !== 0) {
|
|
397
523
|
res.writeHead(403);
|
|
398
524
|
return res.end("Forbidden");
|
|
399
525
|
}
|
|
400
|
-
if (url === "/") fp2 =
|
|
526
|
+
if (url === "/") fp2 = path4.join(this.appDir, "index.html");
|
|
401
527
|
return this.serveFile(fp2, res, false);
|
|
402
528
|
}
|
|
403
|
-
const fp =
|
|
404
|
-
if (fp !== this.root && fp.indexOf(this.root +
|
|
529
|
+
const fp = path4.join(this.root, this.safeDecode(url));
|
|
530
|
+
if (fp !== this.root && fp.indexOf(this.root + path4.sep) !== 0) {
|
|
405
531
|
res.writeHead(403);
|
|
406
532
|
return res.end("Forbidden");
|
|
407
533
|
}
|
|
@@ -417,6 +543,8 @@ var ServerService = class extends Service {
|
|
|
417
543
|
};
|
|
418
544
|
start(port) {
|
|
419
545
|
this.server = http.createServer(this.handler);
|
|
546
|
+
this.server.keepAliveTimeout = 65e3;
|
|
547
|
+
this.server.headersTimeout = 66e3;
|
|
420
548
|
this.server.on("error", (err) => {
|
|
421
549
|
if (err.code === "EADDRINUSE") {
|
|
422
550
|
console.log(`[zdashboard] port ${port} busy, trying ${port + 1}`);
|
|
@@ -429,7 +557,8 @@ var ServerService = class extends Service {
|
|
|
429
557
|
console.log(`[zdashboard] v${VERSION} dashboard -> ${u}`);
|
|
430
558
|
console.log(`[zdashboard] project -> ${this.root}`);
|
|
431
559
|
console.log(`[zdashboard] detect -> openspec:${this.det.hasOpenspec} docs:${this.det.hasDocs} just:${this.det.hasJust} bugs:${this.det.hasBugs}`);
|
|
432
|
-
if (this.
|
|
560
|
+
if (this.dataDir) console.log(`[zdashboard] data -> ${this.dataDir}`);
|
|
561
|
+
if (this.open) openUrl(target);
|
|
433
562
|
this.onListen?.(port);
|
|
434
563
|
});
|
|
435
564
|
}
|
|
@@ -467,7 +596,7 @@ var ServerService = class extends Service {
|
|
|
467
596
|
};
|
|
468
597
|
|
|
469
598
|
// src/core/reload.ts
|
|
470
|
-
import
|
|
599
|
+
import fs5 from "fs";
|
|
471
600
|
import { Service as Service2 } from "cordis";
|
|
472
601
|
var WATCH_DEBOUNCE_MS = 150;
|
|
473
602
|
var ReloadService = class extends Service2 {
|
|
@@ -483,7 +612,7 @@ var ReloadService = class extends Service2 {
|
|
|
483
612
|
return () => this.clients.delete(res);
|
|
484
613
|
});
|
|
485
614
|
try {
|
|
486
|
-
this.watcher =
|
|
615
|
+
this.watcher = fs5.watch(config.root, { recursive: true }, () => {
|
|
487
616
|
if (this.timer) clearTimeout(this.timer);
|
|
488
617
|
this.timer = setTimeout(() => {
|
|
489
618
|
this.broadcast("reload");
|
|
@@ -526,13 +655,13 @@ data: ${JSON.stringify(data == null ? "" : data)}
|
|
|
526
655
|
};
|
|
527
656
|
|
|
528
657
|
// src/server/spec-scan.ts
|
|
529
|
-
import
|
|
530
|
-
import
|
|
658
|
+
import fs6 from "fs";
|
|
659
|
+
import path5 from "path";
|
|
531
660
|
function walkFiles(absDir, relDir, depth = 0) {
|
|
532
661
|
if (depth > 4) return [];
|
|
533
662
|
let ents;
|
|
534
663
|
try {
|
|
535
|
-
ents =
|
|
664
|
+
ents = fs6.readdirSync(absDir, { withFileTypes: true });
|
|
536
665
|
} catch {
|
|
537
666
|
return [];
|
|
538
667
|
}
|
|
@@ -541,7 +670,7 @@ function walkFiles(absDir, relDir, depth = 0) {
|
|
|
541
670
|
if (ent.name.startsWith(".") || ent.name === "node_modules") continue;
|
|
542
671
|
const rel = relDir ? `${relDir}/${ent.name}` : ent.name;
|
|
543
672
|
if (ent.isDirectory()) {
|
|
544
|
-
nodes.push({ name: ent.name, kind: "dir", children: walkFiles(
|
|
673
|
+
nodes.push({ name: ent.name, kind: "dir", children: walkFiles(path5.join(absDir, ent.name), rel, depth + 1) });
|
|
545
674
|
} else {
|
|
546
675
|
nodes.push({ name: ent.name, kind: "file", path: rel });
|
|
547
676
|
}
|
|
@@ -551,41 +680,41 @@ function walkFiles(absDir, relDir, depth = 0) {
|
|
|
551
680
|
}
|
|
552
681
|
function scanTree(root, hasOpenspec, hasDocs) {
|
|
553
682
|
const tree = [];
|
|
554
|
-
if (hasOpenspec &&
|
|
555
|
-
const changesDir =
|
|
683
|
+
if (hasOpenspec && fs6.existsSync(path5.join(root, "openspec", "changes"))) {
|
|
684
|
+
const changesDir = path5.join(root, "openspec", "changes");
|
|
556
685
|
const active = [];
|
|
557
686
|
const archived = [];
|
|
558
|
-
for (const ent of
|
|
687
|
+
for (const ent of fs6.readdirSync(changesDir, { withFileTypes: true })) {
|
|
559
688
|
if (!ent.isDirectory() || ent.name.startsWith(".") || ent.name === "archive") continue;
|
|
560
|
-
active.push({ name: ent.name, kind: "dir", children: walkFiles(
|
|
689
|
+
active.push({ name: ent.name, kind: "dir", children: walkFiles(path5.join(changesDir, ent.name), `openspec/changes/${ent.name}`) });
|
|
561
690
|
}
|
|
562
691
|
active.sort((a, b) => a.name.localeCompare(b.name));
|
|
563
|
-
const archiveDir =
|
|
564
|
-
if (
|
|
565
|
-
for (const ent of
|
|
692
|
+
const archiveDir = path5.join(changesDir, "archive");
|
|
693
|
+
if (fs6.existsSync(archiveDir)) {
|
|
694
|
+
for (const ent of fs6.readdirSync(archiveDir, { withFileTypes: true })) {
|
|
566
695
|
if (!ent.isDirectory() || ent.name.startsWith(".")) continue;
|
|
567
|
-
archived.push({ name: ent.name, kind: "dir", children: walkFiles(
|
|
696
|
+
archived.push({ name: ent.name, kind: "dir", children: walkFiles(path5.join(archiveDir, ent.name), `openspec/changes/archive/${ent.name}`) });
|
|
568
697
|
}
|
|
569
698
|
archived.sort((a, b) => b.name.localeCompare(a.name));
|
|
570
699
|
}
|
|
571
700
|
if (active.length) tree.push({ name: `\u8FDB\u884C\u4E2D (${active.length})`, kind: "dir", children: active });
|
|
572
701
|
if (archived.length) tree.push({ name: "archive", kind: "dir", defaultCollapsed: true, children: archived });
|
|
573
|
-
const specsDir =
|
|
574
|
-
if (
|
|
702
|
+
const specsDir = path5.join(root, "openspec", "specs");
|
|
703
|
+
if (fs6.existsSync(specsDir)) {
|
|
575
704
|
const specs = walkFiles(specsDir, "openspec/specs");
|
|
576
705
|
if (specs.length) tree.push({ name: "specs", kind: "dir", children: specs });
|
|
577
706
|
}
|
|
578
707
|
}
|
|
579
|
-
if (hasDocs &&
|
|
580
|
-
const docs = walkFiles(
|
|
708
|
+
if (hasDocs && fs6.existsSync(path5.join(root, "docs"))) {
|
|
709
|
+
const docs = walkFiles(path5.join(root, "docs"), "docs");
|
|
581
710
|
if (docs.length) tree.push({ name: "docs", kind: "dir", children: docs });
|
|
582
711
|
}
|
|
583
712
|
const skip = /* @__PURE__ */ new Set(["openspec", "docs", "node_modules", ".git", "dist", "test-server"]);
|
|
584
713
|
const etc = [];
|
|
585
714
|
try {
|
|
586
|
-
for (const ent of
|
|
715
|
+
for (const ent of fs6.readdirSync(root, { withFileTypes: true })) {
|
|
587
716
|
if (ent.name.startsWith(".") || skip.has(ent.name)) continue;
|
|
588
|
-
const ext =
|
|
717
|
+
const ext = path5.extname(ent.name).toLowerCase();
|
|
589
718
|
if (ent.isFile() && (ext === ".md" || ext === ".markdown")) etc.push({ name: ent.name, kind: "file", path: ent.name });
|
|
590
719
|
}
|
|
591
720
|
} catch (e) {
|
|
@@ -655,13 +784,7 @@ import { spawn, execFile as execFile2 } from "child_process";
|
|
|
655
784
|
var MAX_BUFFER = 1e3;
|
|
656
785
|
var JustRunner = class {
|
|
657
786
|
cwd;
|
|
658
|
-
|
|
659
|
-
recipe = null;
|
|
660
|
-
state = "idle";
|
|
661
|
-
code = null;
|
|
662
|
-
buffer = [];
|
|
663
|
-
pending = "";
|
|
664
|
-
// 行缓冲:块缓冲输出(如 maven)的 chunk 会在行中间断开,攒到 \n 才切行
|
|
787
|
+
tasks = /* @__PURE__ */ new Map();
|
|
665
788
|
clients = /* @__PURE__ */ new Set();
|
|
666
789
|
recipesCache = null;
|
|
667
790
|
constructor(cwd) {
|
|
@@ -695,26 +818,25 @@ var JustRunner = class {
|
|
|
695
818
|
}
|
|
696
819
|
subscribe(fn) {
|
|
697
820
|
this.clients.add(fn);
|
|
698
|
-
for (const
|
|
699
|
-
|
|
821
|
+
for (const t of this.tasks.values()) {
|
|
822
|
+
for (const text of t.buffer) fn({ type: "log", recipe: t.recipe, text });
|
|
823
|
+
fn({ type: "state", recipe: t.recipe, state: t.state, code: t.code, startedAt: t.startedAt });
|
|
824
|
+
}
|
|
700
825
|
return () => this.clients.delete(fn);
|
|
701
826
|
}
|
|
702
827
|
emit(ev) {
|
|
703
828
|
for (const fn of this.clients) fn(ev);
|
|
704
829
|
}
|
|
705
|
-
|
|
706
|
-
return {
|
|
830
|
+
list() {
|
|
831
|
+
return Array.from(this.tasks.values(), (t) => ({ recipe: t.recipe, state: t.state, code: t.code, startedAt: t.startedAt }));
|
|
707
832
|
}
|
|
708
|
-
/** 启动 recipe(
|
|
833
|
+
/** 启动 recipe:同名先停旧进程(重启语义),不影响其他任务;调用方须先用 recipes() 校验名字 */
|
|
709
834
|
start(recipe) {
|
|
710
|
-
this.
|
|
711
|
-
|
|
712
|
-
this.
|
|
713
|
-
this.
|
|
714
|
-
this.
|
|
715
|
-
this.pending = "";
|
|
716
|
-
this.emit({ type: "clear" });
|
|
717
|
-
this.emit({ type: "state", state: "running", recipe, code: null });
|
|
835
|
+
this.killOne(recipe);
|
|
836
|
+
const task = { recipe, child: null, state: "running", code: null, startedAt: Date.now(), buffer: [], pending: "" };
|
|
837
|
+
this.tasks.set(recipe, task);
|
|
838
|
+
this.emit({ type: "clear", recipe });
|
|
839
|
+
this.emit({ type: "state", recipe, state: "running", code: null, startedAt: task.startedAt });
|
|
718
840
|
const child = spawn("just", [recipe], {
|
|
719
841
|
cwd: this.cwd,
|
|
720
842
|
shell: true,
|
|
@@ -727,47 +849,62 @@ var JustRunner = class {
|
|
|
727
849
|
CI: ""
|
|
728
850
|
}
|
|
729
851
|
});
|
|
730
|
-
|
|
852
|
+
task.child = child;
|
|
853
|
+
const isStale = () => this.tasks.get(recipe) !== task;
|
|
731
854
|
const push = (d) => {
|
|
732
|
-
|
|
855
|
+
if (isStale()) return;
|
|
856
|
+
task.pending += d.toString();
|
|
733
857
|
let idx;
|
|
734
|
-
while ((idx =
|
|
735
|
-
const line =
|
|
736
|
-
|
|
737
|
-
this.pushLine(line);
|
|
858
|
+
while ((idx = task.pending.indexOf("\n")) >= 0) {
|
|
859
|
+
const line = task.pending.slice(0, idx + 1);
|
|
860
|
+
task.pending = task.pending.slice(idx + 1);
|
|
861
|
+
this.pushLine(task, line);
|
|
738
862
|
}
|
|
739
863
|
};
|
|
740
864
|
child.stdout?.on("data", push);
|
|
741
865
|
child.stderr?.on("data", push);
|
|
742
866
|
child.on("error", (err) => {
|
|
743
|
-
this.pushLine(`[zdashboard] spawn error: ${err.message}
|
|
867
|
+
this.pushLine(task, `[zdashboard] spawn error: ${err.message}
|
|
744
868
|
`);
|
|
745
869
|
});
|
|
746
|
-
child.on("exit", (code) => {
|
|
747
|
-
if (
|
|
748
|
-
this.pushLine(
|
|
749
|
-
this.pending = "";
|
|
870
|
+
child.on("exit", (code, signal) => {
|
|
871
|
+
if (task.pending && !isStale()) {
|
|
872
|
+
this.pushLine(task, task.pending + "\n");
|
|
750
873
|
}
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
874
|
+
task.pending = "";
|
|
875
|
+
task.child = null;
|
|
876
|
+
task.state = "exited";
|
|
877
|
+
task.code = code ?? 0;
|
|
878
|
+
task.signal = signal ?? void 0;
|
|
879
|
+
if (isStale()) return;
|
|
880
|
+
this.emit({ type: "state", recipe, state: "exited", code: task.code, startedAt: task.startedAt, signal: task.signal });
|
|
755
881
|
});
|
|
756
882
|
}
|
|
757
|
-
pushLine(line) {
|
|
758
|
-
|
|
759
|
-
if (
|
|
760
|
-
this.emit({ type: "log", text: line });
|
|
883
|
+
pushLine(task, line) {
|
|
884
|
+
task.buffer.push(line);
|
|
885
|
+
if (task.buffer.length > MAX_BUFFER) task.buffer.shift();
|
|
886
|
+
this.emit({ type: "log", recipe: task.recipe, text: line });
|
|
761
887
|
}
|
|
762
|
-
|
|
763
|
-
|
|
888
|
+
/** 停单个任务;不传 recipe 停全部 */
|
|
889
|
+
stop(recipe) {
|
|
890
|
+
if (recipe === void 0) {
|
|
891
|
+
for (const t of this.tasks.values()) this.killOne(t.recipe);
|
|
892
|
+
} else this.killOne(recipe);
|
|
764
893
|
}
|
|
765
894
|
restart(recipe) {
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
const
|
|
895
|
+
if (this.tasks.has(recipe)) this.start(recipe);
|
|
896
|
+
}
|
|
897
|
+
/** 清空某任务的日志缓冲并广播(真源清除,重连重放不会复活) */
|
|
898
|
+
clear(recipe) {
|
|
899
|
+
const task = this.tasks.get(recipe);
|
|
900
|
+
if (!task) return;
|
|
901
|
+
task.buffer = [];
|
|
902
|
+
task.pending = "";
|
|
903
|
+
this.emit({ type: "clear", recipe });
|
|
904
|
+
}
|
|
905
|
+
killOne(recipe) {
|
|
906
|
+
const task = this.tasks.get(recipe);
|
|
907
|
+
const child = task?.child;
|
|
771
908
|
if (child?.pid) {
|
|
772
909
|
try {
|
|
773
910
|
if (process.platform === "win32") spawn("taskkill", ["/PID", String(child.pid), "/T", "/F"]);
|
|
@@ -775,7 +912,9 @@ var JustRunner = class {
|
|
|
775
912
|
} catch {
|
|
776
913
|
}
|
|
777
914
|
}
|
|
778
|
-
|
|
915
|
+
if (task) {
|
|
916
|
+
task.child = null;
|
|
917
|
+
}
|
|
779
918
|
}
|
|
780
919
|
};
|
|
781
920
|
|
|
@@ -788,6 +927,7 @@ var apply2 = {
|
|
|
788
927
|
if (!ctx.server?.route) return;
|
|
789
928
|
const runner = new JustRunner(root);
|
|
790
929
|
ctx.effect(() => () => runner.stop());
|
|
930
|
+
ctx.dashboard.register({ mode: "just", label: "Just Runner", icon: "\u{1F4DC}", description: "Just \u591A\u4EFB\u52A1\u5E76\u53D1\u6267\u884C\u4E0E\u65E5\u5FD7" });
|
|
791
931
|
ctx.server.route("/__just/recipes", async (_req, res) => {
|
|
792
932
|
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
|
|
793
933
|
try {
|
|
@@ -805,7 +945,7 @@ var apply2 = {
|
|
|
805
945
|
});
|
|
806
946
|
return unsub;
|
|
807
947
|
});
|
|
808
|
-
|
|
948
|
+
const handleAction = async (req, res, act) => {
|
|
809
949
|
if (req.headers["x-stop-token"] !== ctx.server.stopToken) {
|
|
810
950
|
res.writeHead(403);
|
|
811
951
|
res.end("forbidden");
|
|
@@ -817,61 +957,45 @@ var apply2 = {
|
|
|
817
957
|
recipe = JSON.parse(body || "{}").recipe;
|
|
818
958
|
} catch {
|
|
819
959
|
}
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
res.writeHead(
|
|
823
|
-
res.end('{"
|
|
824
|
-
return;
|
|
825
|
-
}
|
|
826
|
-
const recipes = await runner.recipes();
|
|
827
|
-
if (!recipes.some((r) => r.name === target)) {
|
|
828
|
-
res.writeHead(403);
|
|
829
|
-
res.end('{"error":"unknown recipe"}');
|
|
830
|
-
return;
|
|
831
|
-
}
|
|
832
|
-
runner.start(target);
|
|
833
|
-
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
|
|
834
|
-
res.end(JSON.stringify(runner.info()));
|
|
835
|
-
});
|
|
836
|
-
ctx.server.route("/__just/stop", async (req, res) => {
|
|
837
|
-
if (req.headers["x-stop-token"] !== ctx.server.stopToken) {
|
|
838
|
-
res.writeHead(403);
|
|
839
|
-
res.end("forbidden");
|
|
840
|
-
return;
|
|
841
|
-
}
|
|
842
|
-
runner.stop();
|
|
843
|
-
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
|
|
844
|
-
res.end(JSON.stringify(runner.info()));
|
|
845
|
-
});
|
|
846
|
-
ctx.server.route("/__just/restart", async (req, res) => {
|
|
847
|
-
if (req.headers["x-stop-token"] !== ctx.server.stopToken) {
|
|
848
|
-
res.writeHead(403);
|
|
849
|
-
res.end("forbidden");
|
|
960
|
+
if (act === "clear") {
|
|
961
|
+
if (recipe) runner.clear(recipe);
|
|
962
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
|
|
963
|
+
res.end('{"ok":true}');
|
|
850
964
|
return;
|
|
851
965
|
}
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
res.end('{"error":"unknown recipe"}');
|
|
868
|
-
return;
|
|
966
|
+
if (act !== "stop") {
|
|
967
|
+
if (!recipe) {
|
|
968
|
+
res.writeHead(400);
|
|
969
|
+
res.end('{"error":"no recipe"}');
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
const recipes = await runner.recipes();
|
|
973
|
+
if (!recipes.some((r) => r.name === recipe)) {
|
|
974
|
+
res.writeHead(403);
|
|
975
|
+
res.end('{"error":"unknown recipe"}');
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
runner.start(recipe);
|
|
979
|
+
} else {
|
|
980
|
+
runner.stop(recipe || void 0);
|
|
869
981
|
}
|
|
870
|
-
runner.restart(target);
|
|
871
982
|
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
|
|
872
|
-
res.end(JSON.stringify(runner.
|
|
873
|
-
}
|
|
874
|
-
|
|
983
|
+
res.end(JSON.stringify(runner.list()));
|
|
984
|
+
};
|
|
985
|
+
const routeAction = (name) => {
|
|
986
|
+
ctx.server.route(`/__just/${name}`, (req, res) => {
|
|
987
|
+
handleAction(req, res, name).catch(() => {
|
|
988
|
+
if (!res.headersSent) {
|
|
989
|
+
res.writeHead(500);
|
|
990
|
+
res.end('{"error":"internal"}');
|
|
991
|
+
}
|
|
992
|
+
});
|
|
993
|
+
});
|
|
994
|
+
};
|
|
995
|
+
routeAction("start");
|
|
996
|
+
routeAction("stop");
|
|
997
|
+
routeAction("restart");
|
|
998
|
+
routeAction("clear");
|
|
875
999
|
});
|
|
876
1000
|
}
|
|
877
1001
|
};
|
|
@@ -882,120 +1006,10 @@ async function readBody(req) {
|
|
|
882
1006
|
data += c;
|
|
883
1007
|
});
|
|
884
1008
|
req.on("end", () => resolve(data));
|
|
1009
|
+
req.on("error", () => resolve(""));
|
|
885
1010
|
});
|
|
886
1011
|
}
|
|
887
1012
|
|
|
888
|
-
// src/server/bugs.ts
|
|
889
|
-
import fs6 from "fs";
|
|
890
|
-
import path5 from "path";
|
|
891
|
-
|
|
892
|
-
// src/server/api/fetch.ts
|
|
893
|
-
import ky from "ky";
|
|
894
|
-
|
|
895
|
-
// src/server/errors.ts
|
|
896
|
-
var HttpError = class extends Error {
|
|
897
|
-
constructor(status, message, body) {
|
|
898
|
-
super(message);
|
|
899
|
-
this.status = status;
|
|
900
|
-
this.body = body;
|
|
901
|
-
this.name = "HttpError";
|
|
902
|
-
}
|
|
903
|
-
status;
|
|
904
|
-
body;
|
|
905
|
-
};
|
|
906
|
-
var NetworkError = class extends Error {
|
|
907
|
-
constructor(message, cause) {
|
|
908
|
-
super(message);
|
|
909
|
-
this.cause = cause;
|
|
910
|
-
this.name = "NetworkError";
|
|
911
|
-
}
|
|
912
|
-
cause;
|
|
913
|
-
};
|
|
914
|
-
|
|
915
|
-
// src/server/api/fetch.ts
|
|
916
|
-
async function fetchJson(url, init) {
|
|
917
|
-
try {
|
|
918
|
-
const res = await ky(url, { ...init, timeout: 8e3, retry: 2 });
|
|
919
|
-
return await res.json();
|
|
920
|
-
} catch (e) {
|
|
921
|
-
if (e instanceof HttpError) throw e;
|
|
922
|
-
if (e instanceof Error && e.name === "TimeoutError") {
|
|
923
|
-
throw new NetworkError(`\u8BF7\u6C42\u8D85\u65F6: ${url}`);
|
|
924
|
-
}
|
|
925
|
-
if (e instanceof Error && e.name === "HTTPError") {
|
|
926
|
-
throw new HttpError(e.status ?? 500, e.message);
|
|
927
|
-
}
|
|
928
|
-
throw new NetworkError(`\u8BF7\u6C42\u5931\u8D25: ${url}`, e);
|
|
929
|
-
}
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
// src/server/bugs.ts
|
|
933
|
-
function loadZgoalConfig(root) {
|
|
934
|
-
const file = path5.join(root, ".zgoal", "config.yaml");
|
|
935
|
-
if (!fs6.existsSync(file)) return null;
|
|
936
|
-
const kv = {};
|
|
937
|
-
for (const line of fs6.readFileSync(file, "utf8").split("\n")) {
|
|
938
|
-
const m = line.match(/^\s*([A-Za-z_]\w*)\s*:\s*(.+?)\s*$/);
|
|
939
|
-
if (m && !m[2].startsWith("#")) kv[m[1]] = m[2].replace(/^["']|["']$/g, "");
|
|
940
|
-
}
|
|
941
|
-
const product = Number(kv.product);
|
|
942
|
-
if (!kv.url || !product) return null;
|
|
943
|
-
return {
|
|
944
|
-
url: kv.url.replace(/\/+$/, ""),
|
|
945
|
-
account: kv.account ?? "",
|
|
946
|
-
password: kv.password,
|
|
947
|
-
token: kv.token,
|
|
948
|
-
product
|
|
949
|
-
};
|
|
950
|
-
}
|
|
951
|
-
var tokenCache = null;
|
|
952
|
-
async function getToken(cfg) {
|
|
953
|
-
if (cfg.token) return cfg.token;
|
|
954
|
-
const key = `${cfg.url}|${cfg.account}|${cfg.password ?? ""}`;
|
|
955
|
-
if (tokenCache && tokenCache.key === key && Date.now() - tokenCache.at < 10 * 6e4) return tokenCache.token;
|
|
956
|
-
const json = await fetchJson(`${cfg.url}/api.php/v1/tokens`, {
|
|
957
|
-
method: "POST",
|
|
958
|
-
headers: { "Content-Type": "application/json" },
|
|
959
|
-
body: JSON.stringify({ account: cfg.account, password: cfg.password })
|
|
960
|
-
});
|
|
961
|
-
const token = typeof json.token === "string" ? json.token : "";
|
|
962
|
-
if (!token) throw new Error("token \u83B7\u53D6\u5931\u8D25:\u68C0\u67E5 account / password");
|
|
963
|
-
tokenCache = { key, token, at: Date.now() };
|
|
964
|
-
return token;
|
|
965
|
-
}
|
|
966
|
-
function normBug(b, account) {
|
|
967
|
-
const assigned = b.assignedTo;
|
|
968
|
-
const assignedTo = typeof assigned === "string" ? assigned : assigned && typeof assigned === "object" && "realname" in assigned ? String(assigned.realname ?? "") : "";
|
|
969
|
-
const assignedAccount = typeof assigned === "string" ? assigned : assigned && typeof assigned === "object" ? String(assigned.account ?? "") : "";
|
|
970
|
-
const mine = !!account && (assignedAccount === account || assignedTo === account);
|
|
971
|
-
return {
|
|
972
|
-
id: Number(b.id),
|
|
973
|
-
title: String(b.title ?? ""),
|
|
974
|
-
severity: b.severity ?? 4,
|
|
975
|
-
pri: b.pri ?? 3,
|
|
976
|
-
status: String(b.status ?? ""),
|
|
977
|
-
assignedTo,
|
|
978
|
-
openedBy: typeof b.openedBy === "string" ? b.openedBy : void 0,
|
|
979
|
-
mine
|
|
980
|
-
};
|
|
981
|
-
}
|
|
982
|
-
async function fetchBugs(root) {
|
|
983
|
-
const cfg = loadZgoalConfig(root);
|
|
984
|
-
if (!cfg) return { ok: false, error: ".zgoal/config.yaml \u7F3A\u5931\u6216 url/product \u672A\u914D\u7F6E(\u7531 zgoal skill \u521B\u5EFA)" };
|
|
985
|
-
try {
|
|
986
|
-
const token = await getToken(cfg);
|
|
987
|
-
const json = await fetchJson(
|
|
988
|
-
`${cfg.url}/api.php/v1/products/${cfg.product}/bugs?page=1&limit=100`,
|
|
989
|
-
{ headers: { Token: token } }
|
|
990
|
-
);
|
|
991
|
-
const raw = Array.isArray(json.bugs) ? json.bugs : [];
|
|
992
|
-
return { ok: true, url: cfg.url, total: Number(json.total ?? raw.length), bugs: raw.map((b) => normBug(b, cfg.account)) };
|
|
993
|
-
} catch (e) {
|
|
994
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
995
|
-
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` };
|
|
996
|
-
}
|
|
997
|
-
}
|
|
998
|
-
|
|
999
1013
|
// src/plugins/bugs/index.ts
|
|
1000
1014
|
var apply3 = {
|
|
1001
1015
|
inject: ["server", "dashboard"],
|
|
@@ -1021,15 +1035,16 @@ var apply3 = {
|
|
|
1021
1035
|
import fs7 from "fs";
|
|
1022
1036
|
import path6 from "path";
|
|
1023
1037
|
import YAML from "yaml";
|
|
1024
|
-
var
|
|
1038
|
+
var REVIEW_CANDIDATES = [".zdev/review.yaml", "review.yaml"];
|
|
1025
1039
|
var ReviewStore = class {
|
|
1026
1040
|
root;
|
|
1027
1041
|
file;
|
|
1028
1042
|
onChange;
|
|
1029
1043
|
constructor(root, onChange) {
|
|
1030
1044
|
this.root = root;
|
|
1031
|
-
this.file = path6.join(root, REVIEW_FILE);
|
|
1032
1045
|
this.onChange = onChange;
|
|
1046
|
+
const existing = REVIEW_CANDIDATES.find((rel) => fs7.existsSync(path6.join(root, rel)));
|
|
1047
|
+
this.file = path6.join(root, existing ?? REVIEW_CANDIDATES[0]);
|
|
1033
1048
|
}
|
|
1034
1049
|
exists() {
|
|
1035
1050
|
return fs7.existsSync(this.file);
|
|
@@ -1044,6 +1059,7 @@ var ReviewStore = class {
|
|
|
1044
1059
|
}
|
|
1045
1060
|
}
|
|
1046
1061
|
write(data) {
|
|
1062
|
+
fs7.mkdirSync(path6.dirname(this.file), { recursive: true });
|
|
1047
1063
|
fs7.writeFileSync(this.file, YAML.stringify(data), "utf8");
|
|
1048
1064
|
this.onChange?.();
|
|
1049
1065
|
}
|
|
@@ -1068,7 +1084,9 @@ var ReviewStore = class {
|
|
|
1068
1084
|
}
|
|
1069
1085
|
docs() {
|
|
1070
1086
|
try {
|
|
1071
|
-
|
|
1087
|
+
const zdevDir = path6.join(this.root, ".zdev");
|
|
1088
|
+
if (!fs7.existsSync(zdevDir) || !fs7.statSync(zdevDir).isDirectory()) return [];
|
|
1089
|
+
return fs7.readdirSync(zdevDir).filter((f) => /\.(md|markdown)$/i.test(f) && fs7.statSync(path6.join(zdevDir, f)).isFile()).sort();
|
|
1072
1090
|
} catch {
|
|
1073
1091
|
return [];
|
|
1074
1092
|
}
|
|
@@ -1154,33 +1172,80 @@ function readText(p) {
|
|
|
1154
1172
|
return "";
|
|
1155
1173
|
}
|
|
1156
1174
|
}
|
|
1175
|
+
function worktreeDir(root, name) {
|
|
1176
|
+
return path7.join(root, ".zworktree", name);
|
|
1177
|
+
}
|
|
1178
|
+
function changeDir(root, name) {
|
|
1179
|
+
return path7.join(root, "openspec", "changes", name);
|
|
1180
|
+
}
|
|
1181
|
+
function parseDependsOn(proposal) {
|
|
1182
|
+
const deps = [];
|
|
1183
|
+
const m = proposal.match(/^##\s+依赖\s*\n([\s\S]*?)(?:\n#{1,6}\s|\n---\s*\n|$)/i);
|
|
1184
|
+
if (!m) return deps;
|
|
1185
|
+
for (const line of m[1].split("\n")) {
|
|
1186
|
+
if (!/^-\s+/.test(line)) continue;
|
|
1187
|
+
const name = line.replace(/^-\s+/, "").trim();
|
|
1188
|
+
if (name) deps.push(name);
|
|
1189
|
+
}
|
|
1190
|
+
return deps;
|
|
1191
|
+
}
|
|
1157
1192
|
function scanApplyChanges(root) {
|
|
1158
1193
|
const changesDir = path7.join(root, "openspec", "changes");
|
|
1159
1194
|
if (!fs8.existsSync(changesDir)) return [];
|
|
1160
1195
|
const out = [];
|
|
1161
1196
|
for (const ent of fs8.readdirSync(changesDir, { withFileTypes: true })) {
|
|
1162
1197
|
if (!ent.isDirectory() || ent.name.startsWith(".") || ent.name === "archive") continue;
|
|
1163
|
-
const
|
|
1164
|
-
const tasks = readText(path7.join(
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1198
|
+
const wt = worktreeDir(root, ent.name);
|
|
1199
|
+
const tasks = readText(path7.join(wt, "openspec", "changes", ent.name, "tasks.md"));
|
|
1200
|
+
if (!tasks) {
|
|
1201
|
+
const fallback = readText(path7.join(changesDir, ent.name, "tasks.md"));
|
|
1202
|
+
if (fallback) {
|
|
1203
|
+
const { total, done } = countTasks(fallback);
|
|
1204
|
+
const wtChange = path7.join(wt, "openspec", "changes", ent.name);
|
|
1205
|
+
const mainChange = path7.join(changesDir, ent.name);
|
|
1206
|
+
out.push({
|
|
1207
|
+
name: ent.name,
|
|
1208
|
+
path: `openspec/changes/${ent.name}`,
|
|
1209
|
+
total,
|
|
1210
|
+
done,
|
|
1211
|
+
hasProposal: fs8.existsSync(path7.join(wtChange, "proposal.md")) || fs8.existsSync(path7.join(mainChange, "proposal.md")),
|
|
1212
|
+
hasDesign: fs8.existsSync(path7.join(wtChange, "design.md")) || fs8.existsSync(path7.join(mainChange, "design.md")),
|
|
1213
|
+
// worktree 目录存在即算执行中(tasks.md 可能尚未落盘)
|
|
1214
|
+
inWorktree: fs8.existsSync(wtChange)
|
|
1215
|
+
});
|
|
1216
|
+
}
|
|
1217
|
+
} else {
|
|
1218
|
+
const { total, done } = countTasks(tasks);
|
|
1219
|
+
const wtChange = path7.join(wt, "openspec", "changes", ent.name);
|
|
1220
|
+
const mainChange = path7.join(changesDir, ent.name);
|
|
1221
|
+
out.push({
|
|
1222
|
+
name: ent.name,
|
|
1223
|
+
path: `openspec/changes/${ent.name}`,
|
|
1224
|
+
total,
|
|
1225
|
+
done,
|
|
1226
|
+
// 任一侧存在即算(详情读取是 worktree 优先、主目录兜底,列表标记保持一致)
|
|
1227
|
+
hasProposal: fs8.existsSync(path7.join(wtChange, "proposal.md")) || fs8.existsSync(path7.join(mainChange, "proposal.md")),
|
|
1228
|
+
hasDesign: fs8.existsSync(path7.join(wtChange, "design.md")) || fs8.existsSync(path7.join(mainChange, "design.md")),
|
|
1229
|
+
inWorktree: true
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1174
1232
|
}
|
|
1175
1233
|
out.sort((a, b) => a.name.localeCompare(b.name));
|
|
1176
1234
|
return out;
|
|
1177
1235
|
}
|
|
1178
1236
|
function readApplyChange(root, name) {
|
|
1179
|
-
const
|
|
1180
|
-
const
|
|
1181
|
-
const
|
|
1182
|
-
const
|
|
1237
|
+
const wt = worktreeDir(root, name);
|
|
1238
|
+
const mainChangeDir = changeDir(root, name);
|
|
1239
|
+
const proposalFrom = fs8.existsSync(path7.join(wt, "openspec", "changes", name, "proposal.md")) ? path7.join(wt, "openspec", "changes", name, "proposal.md") : path7.join(mainChangeDir, "proposal.md");
|
|
1240
|
+
const designFrom = fs8.existsSync(path7.join(wt, "openspec", "changes", name, "design.md")) ? path7.join(wt, "openspec", "changes", name, "design.md") : path7.join(mainChangeDir, "design.md");
|
|
1241
|
+
const tasksFrom = fs8.existsSync(path7.join(wt, "openspec", "changes", name, "tasks.md")) ? path7.join(wt, "openspec", "changes", name, "tasks.md") : path7.join(mainChangeDir, "tasks.md");
|
|
1242
|
+
const proposal = readText(proposalFrom);
|
|
1243
|
+
const design = readText(designFrom);
|
|
1244
|
+
const tasks = readText(tasksFrom);
|
|
1183
1245
|
const { total, done } = countTasks(tasks);
|
|
1246
|
+
const inWorktree = fs8.existsSync(path7.join(wt, "openspec", "changes", name));
|
|
1247
|
+
const dependsOn = parseDependsOn(proposal);
|
|
1248
|
+
const hasTestStrategy = /^##\s+测试策略\s*$/m.test(design);
|
|
1184
1249
|
return {
|
|
1185
1250
|
name,
|
|
1186
1251
|
path: `openspec/changes/${name}`,
|
|
@@ -1188,13 +1253,63 @@ function readApplyChange(root, name) {
|
|
|
1188
1253
|
done,
|
|
1189
1254
|
hasProposal: !!proposal,
|
|
1190
1255
|
hasDesign: !!design,
|
|
1256
|
+
inWorktree,
|
|
1191
1257
|
proposal,
|
|
1192
1258
|
design,
|
|
1193
|
-
tasks
|
|
1259
|
+
tasks,
|
|
1260
|
+
dependsOn,
|
|
1261
|
+
hasTestStrategy
|
|
1194
1262
|
};
|
|
1195
1263
|
}
|
|
1196
1264
|
|
|
1197
1265
|
// src/plugins/apply/index.ts
|
|
1266
|
+
import { execFile as execFile3 } from "child_process";
|
|
1267
|
+
var GIT_TIMEOUT_MS = 5e3;
|
|
1268
|
+
function gitWorktrees(root) {
|
|
1269
|
+
return new Promise((resolve) => {
|
|
1270
|
+
execFile3(
|
|
1271
|
+
"git",
|
|
1272
|
+
["worktree", "list", "--porcelain"],
|
|
1273
|
+
{ cwd: root, timeout: GIT_TIMEOUT_MS },
|
|
1274
|
+
(err, stdout) => {
|
|
1275
|
+
if (err) return resolve([]);
|
|
1276
|
+
const entries = [];
|
|
1277
|
+
const lines = stdout.split("\n");
|
|
1278
|
+
let current = {};
|
|
1279
|
+
for (const line of lines) {
|
|
1280
|
+
const m = line.match(/^(worktree|HEAD|branch|detached)\s+(.+)$/);
|
|
1281
|
+
if (m) {
|
|
1282
|
+
const [, key, val] = m;
|
|
1283
|
+
if (key === "worktree") {
|
|
1284
|
+
if (current.path && current.path.includes(".zworktree/")) {
|
|
1285
|
+
entries.push({
|
|
1286
|
+
path: current.path,
|
|
1287
|
+
name: current.path.split("/.zworktree/").pop() ?? "",
|
|
1288
|
+
branch: current.branch ?? "",
|
|
1289
|
+
head: current.head ?? ""
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1292
|
+
current = { path: val, head: "", branch: "" };
|
|
1293
|
+
} else if (key === "HEAD") {
|
|
1294
|
+
current.head = val;
|
|
1295
|
+
} else if (key === "branch") {
|
|
1296
|
+
current.branch = val.replace(/^refs\/heads\//, "");
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
if (current.path && current.path.includes(".zworktree/")) {
|
|
1301
|
+
entries.push({
|
|
1302
|
+
path: current.path,
|
|
1303
|
+
name: current.path.split("/.zworktree/").pop() ?? "",
|
|
1304
|
+
branch: current.branch ?? "",
|
|
1305
|
+
head: current.head ?? ""
|
|
1306
|
+
});
|
|
1307
|
+
}
|
|
1308
|
+
resolve(entries);
|
|
1309
|
+
}
|
|
1310
|
+
);
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1198
1313
|
var apply5 = {
|
|
1199
1314
|
inject: ["server", "dashboard"],
|
|
1200
1315
|
apply(ctx, config) {
|
|
@@ -1228,6 +1343,16 @@ var apply5 = {
|
|
|
1228
1343
|
res.end(JSON.stringify({ error: e instanceof Error ? e.message : "unknown error" }));
|
|
1229
1344
|
}
|
|
1230
1345
|
});
|
|
1346
|
+
ctx.server.route("/__worktrees", async (_req, res) => {
|
|
1347
|
+
try {
|
|
1348
|
+
const entries = await gitWorktrees(root);
|
|
1349
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
|
|
1350
|
+
res.end(JSON.stringify(entries));
|
|
1351
|
+
} catch {
|
|
1352
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
|
|
1353
|
+
res.end(JSON.stringify([]));
|
|
1354
|
+
}
|
|
1355
|
+
});
|
|
1231
1356
|
});
|
|
1232
1357
|
}
|
|
1233
1358
|
};
|
|
@@ -1389,9 +1514,11 @@ var apply8 = {
|
|
|
1389
1514
|
|
|
1390
1515
|
// src/cli.ts
|
|
1391
1516
|
var __dirname2 = path10.dirname(fileURLToPath2(import.meta.url));
|
|
1517
|
+
var DEFAULT_PORT = 4190;
|
|
1392
1518
|
function parseArgs() {
|
|
1393
1519
|
const args = process.argv.slice(2);
|
|
1394
1520
|
const opts = {};
|
|
1521
|
+
let portExplicit = false;
|
|
1395
1522
|
for (let i = 0; i < args.length; i++) {
|
|
1396
1523
|
const a = args[i];
|
|
1397
1524
|
if (a.startsWith("--")) {
|
|
@@ -1403,11 +1530,13 @@ function parseArgs() {
|
|
|
1403
1530
|
} else {
|
|
1404
1531
|
opts[key] = true;
|
|
1405
1532
|
}
|
|
1533
|
+
if (key === "port") portExplicit = true;
|
|
1406
1534
|
}
|
|
1407
1535
|
}
|
|
1408
1536
|
return {
|
|
1409
1537
|
dir: typeof opts.dir === "string" ? opts.dir : process.cwd(),
|
|
1410
|
-
port: typeof opts.port === "string" ? Number(opts.port) :
|
|
1538
|
+
port: typeof opts.port === "string" ? Number(opts.port) : DEFAULT_PORT,
|
|
1539
|
+
portExplicit,
|
|
1411
1540
|
open: !!opts.open,
|
|
1412
1541
|
page: typeof opts.page === "string" ? opts.page : null,
|
|
1413
1542
|
restart: !!opts.restart,
|
|
@@ -1481,20 +1610,25 @@ async function main() {
|
|
|
1481
1610
|
await new Promise((resolve) => process.stdout.write("", () => resolve()));
|
|
1482
1611
|
process.exit(0);
|
|
1483
1612
|
}
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1613
|
+
const oldRecord = existing && args.restart ? existing : null;
|
|
1614
|
+
if (oldRecord) {
|
|
1615
|
+
console.log(`[zdashboard] --restart\uFF1A\u505C\u6B62\u65E7\u5B9E\u4F8B pid=${oldRecord.pid}`);
|
|
1616
|
+
await stopInstance(oldRecord);
|
|
1487
1617
|
}
|
|
1488
1618
|
const appDir = path10.resolve(__dirname2, "web");
|
|
1619
|
+
const startPort = oldRecord && !args.portExplicit ? oldRecord.port : args.port;
|
|
1620
|
+
const zdevDir = path10.join(root, ".zdev");
|
|
1621
|
+
const dataDir = fs11.existsSync(zdevDir) ? ".zdev/" : "";
|
|
1489
1622
|
const det = await detect(root);
|
|
1490
1623
|
const ctx = new Context();
|
|
1491
1624
|
ctx.plugin(ServerService, {
|
|
1492
1625
|
root,
|
|
1493
1626
|
appDir,
|
|
1494
|
-
port:
|
|
1627
|
+
port: startPort,
|
|
1495
1628
|
open: args.open,
|
|
1496
1629
|
detect: det,
|
|
1497
1630
|
page: args.page,
|
|
1631
|
+
dataDir: dataDir || void 0,
|
|
1498
1632
|
onListen: (port) => writeRecord(root, port)
|
|
1499
1633
|
});
|
|
1500
1634
|
ctx.plugin(ReloadService, { root });
|