zdashboard 2.0.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 +42 -1
- package/dist/cli.js +665 -304
- package/dist/cli.js.map +1 -1
- package/dist/web/assets/{MdViewer-DHLa9u9L.js → MdViewer-CgSHNqC3.js} +1 -1
- package/dist/web/assets/Workspace-BjAcIoe8.js +1 -0
- package/dist/web/assets/{file-text-D4672ckY.js → file-text-B46O-IoU.js} +1 -1
- package/dist/web/assets/index-CEza9SOJ.css +1 -0
- package/dist/web/assets/index-CcIhCGOi.js +90 -0
- package/dist/web/assets/{index-BRLQFQ6k.js → index-RpDccu4v.js} +1 -1
- package/dist/web/assets/{katex.min-D64GJ8fh.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-DqBNmBY8.js → web-CUFkwTcN.js} +11 -11
- package/dist/web/assets/web-D6ODdMUf.js +31 -0
- package/dist/web/assets/{web-4tpIem4N.js → web-DiGxrTCi.js} +2 -2
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-Deg587va.js +0 -87
- package/dist/web/assets/index-Dqj3ddhK.css +0 -1
- package/dist/web/assets/web-2je-DAWY.js +0 -30
- package/dist/web/assets/web-Bgs6ipDo.js +0 -31
- package/dist/web/assets/web-C0d3eOAl.js +0 -7
- package/dist/web/assets/web-DIAM7Pif.js +0 -26
package/dist/cli.js
CHANGED
|
@@ -1,14 +1,138 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import
|
|
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,16 +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
|
-
import { exec } from "child_process";
|
|
37
160
|
import { fileURLToPath } from "url";
|
|
38
161
|
|
|
39
162
|
// package.json
|
|
40
163
|
var package_default = {
|
|
41
164
|
name: "zdashboard",
|
|
42
|
-
version: "2.
|
|
165
|
+
version: "2.1.1",
|
|
43
166
|
description: "ZCode skill dashboard platform \u2014 pluggable viewers for zdesign/zview/zreview/zgoal",
|
|
44
167
|
type: "module",
|
|
45
168
|
bin: {
|
|
@@ -123,8 +246,113 @@ var package_default = {
|
|
|
123
246
|
|
|
124
247
|
// src/core/server.ts
|
|
125
248
|
import { Service } from "cordis";
|
|
249
|
+
|
|
250
|
+
// src/core/instance.ts
|
|
251
|
+
import fs3 from "fs";
|
|
252
|
+
import path3 from "path";
|
|
253
|
+
var RECORD_FILE = ".zdev/dashboard.json";
|
|
254
|
+
var VERIFY_TIMEOUT_MS = 1500;
|
|
255
|
+
var STOP_POLL_MS = 2e3;
|
|
256
|
+
var STOP_POLL_INTERVAL_MS = 100;
|
|
257
|
+
var STOP_FINAL_WAIT_MS = 100;
|
|
258
|
+
function recordPath(root) {
|
|
259
|
+
return path3.join(root, RECORD_FILE);
|
|
260
|
+
}
|
|
261
|
+
function readRecord(root) {
|
|
262
|
+
try {
|
|
263
|
+
const fp = recordPath(root);
|
|
264
|
+
const raw = fs3.readFileSync(fp, "utf-8");
|
|
265
|
+
const rec = JSON.parse(raw);
|
|
266
|
+
if (typeof rec.pid === "number" && typeof rec.port === "number" && typeof rec.root === "string" && typeof rec.startedAt === "string") {
|
|
267
|
+
return rec;
|
|
268
|
+
}
|
|
269
|
+
return null;
|
|
270
|
+
} catch {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
function writeRecord(root, port) {
|
|
275
|
+
fs3.mkdirSync(path3.dirname(recordPath(root)), { recursive: true });
|
|
276
|
+
const rec = {
|
|
277
|
+
pid: process.pid,
|
|
278
|
+
port,
|
|
279
|
+
root,
|
|
280
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
281
|
+
};
|
|
282
|
+
fs3.writeFileSync(recordPath(root), JSON.stringify(rec, null, 2) + "\n");
|
|
283
|
+
}
|
|
284
|
+
function clearRecord(root) {
|
|
285
|
+
try {
|
|
286
|
+
const rec = readRecord(root);
|
|
287
|
+
if (rec && rec.pid !== process.pid) return;
|
|
288
|
+
fs3.unlinkSync(recordPath(root));
|
|
289
|
+
} catch {
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
function isAlive(pid) {
|
|
293
|
+
try {
|
|
294
|
+
process.kill(pid, 0);
|
|
295
|
+
return true;
|
|
296
|
+
} catch (e) {
|
|
297
|
+
if (e.code === "ESRCH") return false;
|
|
298
|
+
return true;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
async function fetchWithTimeout(url) {
|
|
302
|
+
const controller = new AbortController();
|
|
303
|
+
const timer = setTimeout(() => controller.abort(), VERIFY_TIMEOUT_MS);
|
|
304
|
+
try {
|
|
305
|
+
return await fetch(url, { signal: controller.signal });
|
|
306
|
+
} finally {
|
|
307
|
+
clearTimeout(timer);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
async function verifyRoot(port, root) {
|
|
311
|
+
try {
|
|
312
|
+
const res = await fetchWithTimeout(`http://127.0.0.1:${port}/__config`);
|
|
313
|
+
if (res.status !== 200) return false;
|
|
314
|
+
const body = await res.json();
|
|
315
|
+
if (typeof body.root !== "string") return false;
|
|
316
|
+
return body.root === root;
|
|
317
|
+
} catch {
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
async function findReusable(root) {
|
|
322
|
+
const rec = readRecord(root);
|
|
323
|
+
if (!rec) return null;
|
|
324
|
+
if (!isAlive(rec.pid)) return null;
|
|
325
|
+
if (!await verifyRoot(rec.port, root)) return null;
|
|
326
|
+
return rec;
|
|
327
|
+
}
|
|
328
|
+
async function stopInstance(record) {
|
|
329
|
+
try {
|
|
330
|
+
process.kill(record.pid, "SIGTERM");
|
|
331
|
+
} catch {
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
const deadline = Date.now() + STOP_POLL_MS;
|
|
335
|
+
while (Date.now() < deadline) {
|
|
336
|
+
if (!isAlive(record.pid)) return;
|
|
337
|
+
await new Promise((r) => setTimeout(r, STOP_POLL_INTERVAL_MS));
|
|
338
|
+
}
|
|
339
|
+
try {
|
|
340
|
+
process.kill(record.pid, "SIGKILL");
|
|
341
|
+
} catch {
|
|
342
|
+
}
|
|
343
|
+
await new Promise((r) => setTimeout(r, STOP_FINAL_WAIT_MS));
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// src/core/open-url.ts
|
|
347
|
+
import { exec } from "child_process";
|
|
348
|
+
function openUrl(url) {
|
|
349
|
+
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
350
|
+
exec(`${cmd} ${url}`);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// src/core/server.ts
|
|
126
354
|
var VERSION = package_default.version;
|
|
127
|
-
var __dirname =
|
|
355
|
+
var __dirname = path4.dirname(fileURLToPath(import.meta.url));
|
|
128
356
|
var MIME = {
|
|
129
357
|
".html": "text/html; charset=utf-8",
|
|
130
358
|
".htm": "text/html; charset=utf-8",
|
|
@@ -182,10 +410,12 @@ var ServerService = class extends Service {
|
|
|
182
410
|
open;
|
|
183
411
|
page;
|
|
184
412
|
det;
|
|
413
|
+
dataDir;
|
|
185
414
|
routes = /* @__PURE__ */ new Map();
|
|
186
415
|
sses = /* @__PURE__ */ new Map();
|
|
187
416
|
prefixStatic = /* @__PURE__ */ new Map();
|
|
188
417
|
server;
|
|
418
|
+
onListen;
|
|
189
419
|
constructor(ctx, config) {
|
|
190
420
|
super(ctx, "server");
|
|
191
421
|
this.stopToken = crypto.randomBytes(12).toString("hex");
|
|
@@ -194,6 +424,8 @@ var ServerService = class extends Service {
|
|
|
194
424
|
this.open = config.open;
|
|
195
425
|
this.page = config.page;
|
|
196
426
|
this.det = config.detect;
|
|
427
|
+
this.dataDir = config.dataDir;
|
|
428
|
+
this.onListen = config.onListen;
|
|
197
429
|
ctx.effect(() => () => this.dispose());
|
|
198
430
|
this.route("/__config", (_req, res) => {
|
|
199
431
|
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
|
|
@@ -211,25 +443,25 @@ var ServerService = class extends Service {
|
|
|
211
443
|
});
|
|
212
444
|
this.start(config.port);
|
|
213
445
|
}
|
|
214
|
-
route(
|
|
215
|
-
this.routes.set(
|
|
216
|
-
this.ctx.effect(() => () => this.routes.delete(
|
|
446
|
+
route(path11, handler) {
|
|
447
|
+
this.routes.set(path11, handler);
|
|
448
|
+
this.ctx.effect(() => () => this.routes.delete(path11));
|
|
217
449
|
}
|
|
218
|
-
sse(
|
|
219
|
-
this.sses.set(
|
|
220
|
-
this.ctx.effect(() => () => this.sses.delete(
|
|
450
|
+
sse(path11, onConnect) {
|
|
451
|
+
this.sses.set(path11, onConnect);
|
|
452
|
+
this.ctx.effect(() => () => this.sses.delete(path11));
|
|
221
453
|
}
|
|
222
454
|
static(prefix, dir) {
|
|
223
455
|
this.prefixStatic.set(prefix, dir);
|
|
224
456
|
this.ctx.effect(() => () => this.prefixStatic.delete(prefix));
|
|
225
457
|
}
|
|
226
458
|
serveFile(filePath, res, injectHtml) {
|
|
227
|
-
|
|
459
|
+
fs4.readFile(filePath, (err, data) => {
|
|
228
460
|
if (err) {
|
|
229
461
|
res.writeHead(404);
|
|
230
462
|
return res.end("Not found");
|
|
231
463
|
}
|
|
232
|
-
const ext =
|
|
464
|
+
const ext = path4.extname(filePath).toLowerCase();
|
|
233
465
|
const ct = MIME[ext] ?? "application/octet-stream";
|
|
234
466
|
let body = data;
|
|
235
467
|
if (injectHtml && ext === ".html") {
|
|
@@ -244,9 +476,9 @@ var ServerService = class extends Service {
|
|
|
244
476
|
for (const [prefix, dir] of this.prefixStatic) {
|
|
245
477
|
if (url.indexOf(prefix) === 0) {
|
|
246
478
|
let rel = this.safeDecode(url.slice(prefix.length));
|
|
247
|
-
if (!rel || rel === "/" || !
|
|
248
|
-
const fp =
|
|
249
|
-
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) {
|
|
250
482
|
res.writeHead(403);
|
|
251
483
|
return res.end("Forbidden");
|
|
252
484
|
}
|
|
@@ -285,17 +517,17 @@ var ServerService = class extends Service {
|
|
|
285
517
|
if (this.servePrefix(url, res)) return;
|
|
286
518
|
if (url === "/" || url.indexOf("/__app/") === 0 || url.indexOf("/assets/") === 0) {
|
|
287
519
|
let fp2 = this.appDir;
|
|
288
|
-
if (url !== "/") fp2 =
|
|
289
|
-
if (url.indexOf("/__app/") === 0) fp2 =
|
|
290
|
-
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) {
|
|
291
523
|
res.writeHead(403);
|
|
292
524
|
return res.end("Forbidden");
|
|
293
525
|
}
|
|
294
|
-
if (url === "/") fp2 =
|
|
526
|
+
if (url === "/") fp2 = path4.join(this.appDir, "index.html");
|
|
295
527
|
return this.serveFile(fp2, res, false);
|
|
296
528
|
}
|
|
297
|
-
const fp =
|
|
298
|
-
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) {
|
|
299
531
|
res.writeHead(403);
|
|
300
532
|
return res.end("Forbidden");
|
|
301
533
|
}
|
|
@@ -311,6 +543,8 @@ var ServerService = class extends Service {
|
|
|
311
543
|
};
|
|
312
544
|
start(port) {
|
|
313
545
|
this.server = http.createServer(this.handler);
|
|
546
|
+
this.server.keepAliveTimeout = 65e3;
|
|
547
|
+
this.server.headersTimeout = 66e3;
|
|
314
548
|
this.server.on("error", (err) => {
|
|
315
549
|
if (err.code === "EADDRINUSE") {
|
|
316
550
|
console.log(`[zdashboard] port ${port} busy, trying ${port + 1}`);
|
|
@@ -323,7 +557,9 @@ var ServerService = class extends Service {
|
|
|
323
557
|
console.log(`[zdashboard] v${VERSION} dashboard -> ${u}`);
|
|
324
558
|
console.log(`[zdashboard] project -> ${this.root}`);
|
|
325
559
|
console.log(`[zdashboard] detect -> openspec:${this.det.hasOpenspec} docs:${this.det.hasDocs} just:${this.det.hasJust} bugs:${this.det.hasBugs}`);
|
|
326
|
-
if (this.
|
|
560
|
+
if (this.dataDir) console.log(`[zdashboard] data -> ${this.dataDir}`);
|
|
561
|
+
if (this.open) openUrl(target);
|
|
562
|
+
this.onListen?.(port);
|
|
327
563
|
});
|
|
328
564
|
}
|
|
329
565
|
stop() {
|
|
@@ -333,6 +569,10 @@ var ServerService = class extends Service {
|
|
|
333
569
|
} catch {
|
|
334
570
|
}
|
|
335
571
|
}
|
|
572
|
+
try {
|
|
573
|
+
clearRecord(this.root);
|
|
574
|
+
} catch {
|
|
575
|
+
}
|
|
336
576
|
setTimeout(() => {
|
|
337
577
|
try {
|
|
338
578
|
this.ctx.root.fiber.dispose();
|
|
@@ -348,11 +588,15 @@ var ServerService = class extends Service {
|
|
|
348
588
|
} catch {
|
|
349
589
|
}
|
|
350
590
|
}
|
|
591
|
+
try {
|
|
592
|
+
clearRecord(this.root);
|
|
593
|
+
} catch {
|
|
594
|
+
}
|
|
351
595
|
}
|
|
352
596
|
};
|
|
353
597
|
|
|
354
598
|
// src/core/reload.ts
|
|
355
|
-
import
|
|
599
|
+
import fs5 from "fs";
|
|
356
600
|
import { Service as Service2 } from "cordis";
|
|
357
601
|
var WATCH_DEBOUNCE_MS = 150;
|
|
358
602
|
var ReloadService = class extends Service2 {
|
|
@@ -368,7 +612,7 @@ var ReloadService = class extends Service2 {
|
|
|
368
612
|
return () => this.clients.delete(res);
|
|
369
613
|
});
|
|
370
614
|
try {
|
|
371
|
-
this.watcher =
|
|
615
|
+
this.watcher = fs5.watch(config.root, { recursive: true }, () => {
|
|
372
616
|
if (this.timer) clearTimeout(this.timer);
|
|
373
617
|
this.timer = setTimeout(() => {
|
|
374
618
|
this.broadcast("reload");
|
|
@@ -411,13 +655,13 @@ data: ${JSON.stringify(data == null ? "" : data)}
|
|
|
411
655
|
};
|
|
412
656
|
|
|
413
657
|
// src/server/spec-scan.ts
|
|
414
|
-
import
|
|
415
|
-
import
|
|
658
|
+
import fs6 from "fs";
|
|
659
|
+
import path5 from "path";
|
|
416
660
|
function walkFiles(absDir, relDir, depth = 0) {
|
|
417
661
|
if (depth > 4) return [];
|
|
418
662
|
let ents;
|
|
419
663
|
try {
|
|
420
|
-
ents =
|
|
664
|
+
ents = fs6.readdirSync(absDir, { withFileTypes: true });
|
|
421
665
|
} catch {
|
|
422
666
|
return [];
|
|
423
667
|
}
|
|
@@ -426,7 +670,7 @@ function walkFiles(absDir, relDir, depth = 0) {
|
|
|
426
670
|
if (ent.name.startsWith(".") || ent.name === "node_modules") continue;
|
|
427
671
|
const rel = relDir ? `${relDir}/${ent.name}` : ent.name;
|
|
428
672
|
if (ent.isDirectory()) {
|
|
429
|
-
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) });
|
|
430
674
|
} else {
|
|
431
675
|
nodes.push({ name: ent.name, kind: "file", path: rel });
|
|
432
676
|
}
|
|
@@ -436,41 +680,41 @@ function walkFiles(absDir, relDir, depth = 0) {
|
|
|
436
680
|
}
|
|
437
681
|
function scanTree(root, hasOpenspec, hasDocs) {
|
|
438
682
|
const tree = [];
|
|
439
|
-
if (hasOpenspec &&
|
|
440
|
-
const changesDir =
|
|
683
|
+
if (hasOpenspec && fs6.existsSync(path5.join(root, "openspec", "changes"))) {
|
|
684
|
+
const changesDir = path5.join(root, "openspec", "changes");
|
|
441
685
|
const active = [];
|
|
442
686
|
const archived = [];
|
|
443
|
-
for (const ent of
|
|
687
|
+
for (const ent of fs6.readdirSync(changesDir, { withFileTypes: true })) {
|
|
444
688
|
if (!ent.isDirectory() || ent.name.startsWith(".") || ent.name === "archive") continue;
|
|
445
|
-
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}`) });
|
|
446
690
|
}
|
|
447
691
|
active.sort((a, b) => a.name.localeCompare(b.name));
|
|
448
|
-
const archiveDir =
|
|
449
|
-
if (
|
|
450
|
-
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 })) {
|
|
451
695
|
if (!ent.isDirectory() || ent.name.startsWith(".")) continue;
|
|
452
|
-
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}`) });
|
|
453
697
|
}
|
|
454
698
|
archived.sort((a, b) => b.name.localeCompare(a.name));
|
|
455
699
|
}
|
|
456
700
|
if (active.length) tree.push({ name: `\u8FDB\u884C\u4E2D (${active.length})`, kind: "dir", children: active });
|
|
457
701
|
if (archived.length) tree.push({ name: "archive", kind: "dir", defaultCollapsed: true, children: archived });
|
|
458
|
-
const specsDir =
|
|
459
|
-
if (
|
|
702
|
+
const specsDir = path5.join(root, "openspec", "specs");
|
|
703
|
+
if (fs6.existsSync(specsDir)) {
|
|
460
704
|
const specs = walkFiles(specsDir, "openspec/specs");
|
|
461
705
|
if (specs.length) tree.push({ name: "specs", kind: "dir", children: specs });
|
|
462
706
|
}
|
|
463
707
|
}
|
|
464
|
-
if (hasDocs &&
|
|
465
|
-
const docs = walkFiles(
|
|
708
|
+
if (hasDocs && fs6.existsSync(path5.join(root, "docs"))) {
|
|
709
|
+
const docs = walkFiles(path5.join(root, "docs"), "docs");
|
|
466
710
|
if (docs.length) tree.push({ name: "docs", kind: "dir", children: docs });
|
|
467
711
|
}
|
|
468
712
|
const skip = /* @__PURE__ */ new Set(["openspec", "docs", "node_modules", ".git", "dist", "test-server"]);
|
|
469
713
|
const etc = [];
|
|
470
714
|
try {
|
|
471
|
-
for (const ent of
|
|
715
|
+
for (const ent of fs6.readdirSync(root, { withFileTypes: true })) {
|
|
472
716
|
if (ent.name.startsWith(".") || skip.has(ent.name)) continue;
|
|
473
|
-
const ext =
|
|
717
|
+
const ext = path5.extname(ent.name).toLowerCase();
|
|
474
718
|
if (ent.isFile() && (ext === ".md" || ext === ".markdown")) etc.push({ name: ent.name, kind: "file", path: ent.name });
|
|
475
719
|
}
|
|
476
720
|
} catch (e) {
|
|
@@ -540,13 +784,7 @@ import { spawn, execFile as execFile2 } from "child_process";
|
|
|
540
784
|
var MAX_BUFFER = 1e3;
|
|
541
785
|
var JustRunner = class {
|
|
542
786
|
cwd;
|
|
543
|
-
|
|
544
|
-
recipe = null;
|
|
545
|
-
state = "idle";
|
|
546
|
-
code = null;
|
|
547
|
-
buffer = [];
|
|
548
|
-
pending = "";
|
|
549
|
-
// 行缓冲:块缓冲输出(如 maven)的 chunk 会在行中间断开,攒到 \n 才切行
|
|
787
|
+
tasks = /* @__PURE__ */ new Map();
|
|
550
788
|
clients = /* @__PURE__ */ new Set();
|
|
551
789
|
recipesCache = null;
|
|
552
790
|
constructor(cwd) {
|
|
@@ -580,26 +818,25 @@ var JustRunner = class {
|
|
|
580
818
|
}
|
|
581
819
|
subscribe(fn) {
|
|
582
820
|
this.clients.add(fn);
|
|
583
|
-
for (const
|
|
584
|
-
|
|
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
|
+
}
|
|
585
825
|
return () => this.clients.delete(fn);
|
|
586
826
|
}
|
|
587
827
|
emit(ev) {
|
|
588
828
|
for (const fn of this.clients) fn(ev);
|
|
589
829
|
}
|
|
590
|
-
|
|
591
|
-
return {
|
|
830
|
+
list() {
|
|
831
|
+
return Array.from(this.tasks.values(), (t) => ({ recipe: t.recipe, state: t.state, code: t.code, startedAt: t.startedAt }));
|
|
592
832
|
}
|
|
593
|
-
/** 启动 recipe(
|
|
833
|
+
/** 启动 recipe:同名先停旧进程(重启语义),不影响其他任务;调用方须先用 recipes() 校验名字 */
|
|
594
834
|
start(recipe) {
|
|
595
|
-
this.
|
|
596
|
-
|
|
597
|
-
this.
|
|
598
|
-
this.
|
|
599
|
-
this.
|
|
600
|
-
this.pending = "";
|
|
601
|
-
this.emit({ type: "clear" });
|
|
602
|
-
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 });
|
|
603
840
|
const child = spawn("just", [recipe], {
|
|
604
841
|
cwd: this.cwd,
|
|
605
842
|
shell: true,
|
|
@@ -612,47 +849,62 @@ var JustRunner = class {
|
|
|
612
849
|
CI: ""
|
|
613
850
|
}
|
|
614
851
|
});
|
|
615
|
-
|
|
852
|
+
task.child = child;
|
|
853
|
+
const isStale = () => this.tasks.get(recipe) !== task;
|
|
616
854
|
const push = (d) => {
|
|
617
|
-
|
|
855
|
+
if (isStale()) return;
|
|
856
|
+
task.pending += d.toString();
|
|
618
857
|
let idx;
|
|
619
|
-
while ((idx =
|
|
620
|
-
const line =
|
|
621
|
-
|
|
622
|
-
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);
|
|
623
862
|
}
|
|
624
863
|
};
|
|
625
864
|
child.stdout?.on("data", push);
|
|
626
865
|
child.stderr?.on("data", push);
|
|
627
866
|
child.on("error", (err) => {
|
|
628
|
-
this.pushLine(`[zdashboard] spawn error: ${err.message}
|
|
867
|
+
this.pushLine(task, `[zdashboard] spawn error: ${err.message}
|
|
629
868
|
`);
|
|
630
869
|
});
|
|
631
|
-
child.on("exit", (code) => {
|
|
632
|
-
if (
|
|
633
|
-
this.pushLine(
|
|
634
|
-
this.pending = "";
|
|
870
|
+
child.on("exit", (code, signal) => {
|
|
871
|
+
if (task.pending && !isStale()) {
|
|
872
|
+
this.pushLine(task, task.pending + "\n");
|
|
635
873
|
}
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
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 });
|
|
640
881
|
});
|
|
641
882
|
}
|
|
642
|
-
pushLine(line) {
|
|
643
|
-
|
|
644
|
-
if (
|
|
645
|
-
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 });
|
|
646
887
|
}
|
|
647
|
-
|
|
648
|
-
|
|
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);
|
|
649
893
|
}
|
|
650
894
|
restart(recipe) {
|
|
651
|
-
|
|
652
|
-
|
|
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 });
|
|
653
904
|
}
|
|
654
|
-
|
|
655
|
-
const
|
|
905
|
+
killOne(recipe) {
|
|
906
|
+
const task = this.tasks.get(recipe);
|
|
907
|
+
const child = task?.child;
|
|
656
908
|
if (child?.pid) {
|
|
657
909
|
try {
|
|
658
910
|
if (process.platform === "win32") spawn("taskkill", ["/PID", String(child.pid), "/T", "/F"]);
|
|
@@ -660,7 +912,9 @@ var JustRunner = class {
|
|
|
660
912
|
} catch {
|
|
661
913
|
}
|
|
662
914
|
}
|
|
663
|
-
|
|
915
|
+
if (task) {
|
|
916
|
+
task.child = null;
|
|
917
|
+
}
|
|
664
918
|
}
|
|
665
919
|
};
|
|
666
920
|
|
|
@@ -673,6 +927,7 @@ var apply2 = {
|
|
|
673
927
|
if (!ctx.server?.route) return;
|
|
674
928
|
const runner = new JustRunner(root);
|
|
675
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" });
|
|
676
931
|
ctx.server.route("/__just/recipes", async (_req, res) => {
|
|
677
932
|
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
|
|
678
933
|
try {
|
|
@@ -690,45 +945,7 @@ var apply2 = {
|
|
|
690
945
|
});
|
|
691
946
|
return unsub;
|
|
692
947
|
});
|
|
693
|
-
|
|
694
|
-
if (req.headers["x-stop-token"] !== ctx.server.stopToken) {
|
|
695
|
-
res.writeHead(403);
|
|
696
|
-
res.end("forbidden");
|
|
697
|
-
return;
|
|
698
|
-
}
|
|
699
|
-
const body = await readBody(req);
|
|
700
|
-
let recipe;
|
|
701
|
-
try {
|
|
702
|
-
recipe = JSON.parse(body || "{}").recipe;
|
|
703
|
-
} catch {
|
|
704
|
-
}
|
|
705
|
-
const target = recipe ?? runner.info().recipe;
|
|
706
|
-
if (!target) {
|
|
707
|
-
res.writeHead(400);
|
|
708
|
-
res.end('{"error":"no recipe"}');
|
|
709
|
-
return;
|
|
710
|
-
}
|
|
711
|
-
const recipes = await runner.recipes();
|
|
712
|
-
if (!recipes.some((r) => r.name === target)) {
|
|
713
|
-
res.writeHead(403);
|
|
714
|
-
res.end('{"error":"unknown recipe"}');
|
|
715
|
-
return;
|
|
716
|
-
}
|
|
717
|
-
runner.start(target);
|
|
718
|
-
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
|
|
719
|
-
res.end(JSON.stringify(runner.info()));
|
|
720
|
-
});
|
|
721
|
-
ctx.server.route("/__just/stop", async (req, res) => {
|
|
722
|
-
if (req.headers["x-stop-token"] !== ctx.server.stopToken) {
|
|
723
|
-
res.writeHead(403);
|
|
724
|
-
res.end("forbidden");
|
|
725
|
-
return;
|
|
726
|
-
}
|
|
727
|
-
runner.stop();
|
|
728
|
-
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
|
|
729
|
-
res.end(JSON.stringify(runner.info()));
|
|
730
|
-
});
|
|
731
|
-
ctx.server.route("/__just/restart", async (req, res) => {
|
|
948
|
+
const handleAction = async (req, res, act) => {
|
|
732
949
|
if (req.headers["x-stop-token"] !== ctx.server.stopToken) {
|
|
733
950
|
res.writeHead(403);
|
|
734
951
|
res.end("forbidden");
|
|
@@ -740,23 +957,45 @@ var apply2 = {
|
|
|
740
957
|
recipe = JSON.parse(body || "{}").recipe;
|
|
741
958
|
} catch {
|
|
742
959
|
}
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
res.writeHead(
|
|
746
|
-
res.end('{"
|
|
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}');
|
|
747
964
|
return;
|
|
748
965
|
}
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
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);
|
|
754
981
|
}
|
|
755
|
-
runner.restart(target);
|
|
756
982
|
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
|
|
757
|
-
res.end(JSON.stringify(runner.
|
|
758
|
-
}
|
|
759
|
-
|
|
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");
|
|
760
999
|
});
|
|
761
1000
|
}
|
|
762
1001
|
};
|
|
@@ -767,120 +1006,10 @@ async function readBody(req) {
|
|
|
767
1006
|
data += c;
|
|
768
1007
|
});
|
|
769
1008
|
req.on("end", () => resolve(data));
|
|
1009
|
+
req.on("error", () => resolve(""));
|
|
770
1010
|
});
|
|
771
1011
|
}
|
|
772
1012
|
|
|
773
|
-
// src/server/bugs.ts
|
|
774
|
-
import fs5 from "fs";
|
|
775
|
-
import path4 from "path";
|
|
776
|
-
|
|
777
|
-
// src/server/api/fetch.ts
|
|
778
|
-
import ky from "ky";
|
|
779
|
-
|
|
780
|
-
// src/server/errors.ts
|
|
781
|
-
var HttpError = class extends Error {
|
|
782
|
-
constructor(status, message, body) {
|
|
783
|
-
super(message);
|
|
784
|
-
this.status = status;
|
|
785
|
-
this.body = body;
|
|
786
|
-
this.name = "HttpError";
|
|
787
|
-
}
|
|
788
|
-
status;
|
|
789
|
-
body;
|
|
790
|
-
};
|
|
791
|
-
var NetworkError = class extends Error {
|
|
792
|
-
constructor(message, cause) {
|
|
793
|
-
super(message);
|
|
794
|
-
this.cause = cause;
|
|
795
|
-
this.name = "NetworkError";
|
|
796
|
-
}
|
|
797
|
-
cause;
|
|
798
|
-
};
|
|
799
|
-
|
|
800
|
-
// src/server/api/fetch.ts
|
|
801
|
-
async function fetchJson(url, init) {
|
|
802
|
-
try {
|
|
803
|
-
const res = await ky(url, { ...init, timeout: 8e3, retry: 2 });
|
|
804
|
-
return await res.json();
|
|
805
|
-
} catch (e) {
|
|
806
|
-
if (e instanceof HttpError) throw e;
|
|
807
|
-
if (e instanceof Error && e.name === "TimeoutError") {
|
|
808
|
-
throw new NetworkError(`\u8BF7\u6C42\u8D85\u65F6: ${url}`);
|
|
809
|
-
}
|
|
810
|
-
if (e instanceof Error && e.name === "HTTPError") {
|
|
811
|
-
throw new HttpError(e.status ?? 500, e.message);
|
|
812
|
-
}
|
|
813
|
-
throw new NetworkError(`\u8BF7\u6C42\u5931\u8D25: ${url}`, e);
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
// src/server/bugs.ts
|
|
818
|
-
function loadZgoalConfig(root) {
|
|
819
|
-
const file = path4.join(root, ".zgoal", "config.yaml");
|
|
820
|
-
if (!fs5.existsSync(file)) return null;
|
|
821
|
-
const kv = {};
|
|
822
|
-
for (const line of fs5.readFileSync(file, "utf8").split("\n")) {
|
|
823
|
-
const m = line.match(/^\s*([A-Za-z_]\w*)\s*:\s*(.+?)\s*$/);
|
|
824
|
-
if (m && !m[2].startsWith("#")) kv[m[1]] = m[2].replace(/^["']|["']$/g, "");
|
|
825
|
-
}
|
|
826
|
-
const product = Number(kv.product);
|
|
827
|
-
if (!kv.url || !product) return null;
|
|
828
|
-
return {
|
|
829
|
-
url: kv.url.replace(/\/+$/, ""),
|
|
830
|
-
account: kv.account ?? "",
|
|
831
|
-
password: kv.password,
|
|
832
|
-
token: kv.token,
|
|
833
|
-
product
|
|
834
|
-
};
|
|
835
|
-
}
|
|
836
|
-
var tokenCache = null;
|
|
837
|
-
async function getToken(cfg) {
|
|
838
|
-
if (cfg.token) return cfg.token;
|
|
839
|
-
const key = `${cfg.url}|${cfg.account}|${cfg.password ?? ""}`;
|
|
840
|
-
if (tokenCache && tokenCache.key === key && Date.now() - tokenCache.at < 10 * 6e4) return tokenCache.token;
|
|
841
|
-
const json = await fetchJson(`${cfg.url}/api.php/v1/tokens`, {
|
|
842
|
-
method: "POST",
|
|
843
|
-
headers: { "Content-Type": "application/json" },
|
|
844
|
-
body: JSON.stringify({ account: cfg.account, password: cfg.password })
|
|
845
|
-
});
|
|
846
|
-
const token = typeof json.token === "string" ? json.token : "";
|
|
847
|
-
if (!token) throw new Error("token \u83B7\u53D6\u5931\u8D25:\u68C0\u67E5 account / password");
|
|
848
|
-
tokenCache = { key, token, at: Date.now() };
|
|
849
|
-
return token;
|
|
850
|
-
}
|
|
851
|
-
function normBug(b, account) {
|
|
852
|
-
const assigned = b.assignedTo;
|
|
853
|
-
const assignedTo = typeof assigned === "string" ? assigned : assigned && typeof assigned === "object" && "realname" in assigned ? String(assigned.realname ?? "") : "";
|
|
854
|
-
const assignedAccount = typeof assigned === "string" ? assigned : assigned && typeof assigned === "object" ? String(assigned.account ?? "") : "";
|
|
855
|
-
const mine = !!account && (assignedAccount === account || assignedTo === account);
|
|
856
|
-
return {
|
|
857
|
-
id: Number(b.id),
|
|
858
|
-
title: String(b.title ?? ""),
|
|
859
|
-
severity: b.severity ?? 4,
|
|
860
|
-
pri: b.pri ?? 3,
|
|
861
|
-
status: String(b.status ?? ""),
|
|
862
|
-
assignedTo,
|
|
863
|
-
openedBy: typeof b.openedBy === "string" ? b.openedBy : void 0,
|
|
864
|
-
mine
|
|
865
|
-
};
|
|
866
|
-
}
|
|
867
|
-
async function fetchBugs(root) {
|
|
868
|
-
const cfg = loadZgoalConfig(root);
|
|
869
|
-
if (!cfg) return { ok: false, error: ".zgoal/config.yaml \u7F3A\u5931\u6216 url/product \u672A\u914D\u7F6E(\u7531 zgoal skill \u521B\u5EFA)" };
|
|
870
|
-
try {
|
|
871
|
-
const token = await getToken(cfg);
|
|
872
|
-
const json = await fetchJson(
|
|
873
|
-
`${cfg.url}/api.php/v1/products/${cfg.product}/bugs?page=1&limit=100`,
|
|
874
|
-
{ headers: { Token: token } }
|
|
875
|
-
);
|
|
876
|
-
const raw = Array.isArray(json.bugs) ? json.bugs : [];
|
|
877
|
-
return { ok: true, url: cfg.url, total: Number(json.total ?? raw.length), bugs: raw.map((b) => normBug(b, cfg.account)) };
|
|
878
|
-
} catch (e) {
|
|
879
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
880
|
-
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` };
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
|
|
884
1013
|
// src/plugins/bugs/index.ts
|
|
885
1014
|
var apply3 = {
|
|
886
1015
|
inject: ["server", "dashboard"],
|
|
@@ -903,25 +1032,26 @@ var apply3 = {
|
|
|
903
1032
|
};
|
|
904
1033
|
|
|
905
1034
|
// src/server/review-store.ts
|
|
906
|
-
import
|
|
907
|
-
import
|
|
1035
|
+
import fs7 from "fs";
|
|
1036
|
+
import path6 from "path";
|
|
908
1037
|
import YAML from "yaml";
|
|
909
|
-
var
|
|
1038
|
+
var REVIEW_CANDIDATES = [".zdev/review.yaml", "review.yaml"];
|
|
910
1039
|
var ReviewStore = class {
|
|
911
1040
|
root;
|
|
912
1041
|
file;
|
|
913
1042
|
onChange;
|
|
914
1043
|
constructor(root, onChange) {
|
|
915
1044
|
this.root = root;
|
|
916
|
-
this.file = path5.join(root, REVIEW_FILE);
|
|
917
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]);
|
|
918
1048
|
}
|
|
919
1049
|
exists() {
|
|
920
|
-
return
|
|
1050
|
+
return fs7.existsSync(this.file);
|
|
921
1051
|
}
|
|
922
1052
|
read() {
|
|
923
1053
|
try {
|
|
924
|
-
const parsed = YAML.parse(
|
|
1054
|
+
const parsed = YAML.parse(fs7.readFileSync(this.file, "utf8"));
|
|
925
1055
|
if (!parsed || !Array.isArray(parsed.items)) return { status: "draft", items: [] };
|
|
926
1056
|
return parsed;
|
|
927
1057
|
} catch {
|
|
@@ -929,7 +1059,8 @@ var ReviewStore = class {
|
|
|
929
1059
|
}
|
|
930
1060
|
}
|
|
931
1061
|
write(data) {
|
|
932
|
-
|
|
1062
|
+
fs7.mkdirSync(path6.dirname(this.file), { recursive: true });
|
|
1063
|
+
fs7.writeFileSync(this.file, YAML.stringify(data), "utf8");
|
|
933
1064
|
this.onChange?.();
|
|
934
1065
|
}
|
|
935
1066
|
updateItem(id, patch) {
|
|
@@ -953,7 +1084,9 @@ var ReviewStore = class {
|
|
|
953
1084
|
}
|
|
954
1085
|
docs() {
|
|
955
1086
|
try {
|
|
956
|
-
|
|
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();
|
|
957
1090
|
} catch {
|
|
958
1091
|
return [];
|
|
959
1092
|
}
|
|
@@ -1025,8 +1158,8 @@ async function readBody2(req) {
|
|
|
1025
1158
|
}
|
|
1026
1159
|
|
|
1027
1160
|
// src/plugins/apply/scan.ts
|
|
1028
|
-
import
|
|
1029
|
-
import
|
|
1161
|
+
import fs8 from "fs";
|
|
1162
|
+
import path7 from "path";
|
|
1030
1163
|
function countTasks(md) {
|
|
1031
1164
|
const all = (md.match(/^\s*-\s*\[[ xX]\]\s*/gm) || []).length;
|
|
1032
1165
|
const done = (md.match(/^\s*-\s*\[[xX]\]\s*/gm) || []).length;
|
|
@@ -1034,38 +1167,85 @@ function countTasks(md) {
|
|
|
1034
1167
|
}
|
|
1035
1168
|
function readText(p) {
|
|
1036
1169
|
try {
|
|
1037
|
-
return
|
|
1170
|
+
return fs8.readFileSync(p, "utf8");
|
|
1038
1171
|
} catch {
|
|
1039
1172
|
return "";
|
|
1040
1173
|
}
|
|
1041
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
|
+
}
|
|
1042
1192
|
function scanApplyChanges(root) {
|
|
1043
|
-
const changesDir =
|
|
1044
|
-
if (!
|
|
1193
|
+
const changesDir = path7.join(root, "openspec", "changes");
|
|
1194
|
+
if (!fs8.existsSync(changesDir)) return [];
|
|
1045
1195
|
const out = [];
|
|
1046
|
-
for (const ent of
|
|
1196
|
+
for (const ent of fs8.readdirSync(changesDir, { withFileTypes: true })) {
|
|
1047
1197
|
if (!ent.isDirectory() || ent.name.startsWith(".") || ent.name === "archive") continue;
|
|
1048
|
-
const
|
|
1049
|
-
const tasks = readText(
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
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
|
+
}
|
|
1059
1232
|
}
|
|
1060
1233
|
out.sort((a, b) => a.name.localeCompare(b.name));
|
|
1061
1234
|
return out;
|
|
1062
1235
|
}
|
|
1063
1236
|
function readApplyChange(root, name) {
|
|
1064
|
-
const
|
|
1065
|
-
const
|
|
1066
|
-
const
|
|
1067
|
-
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);
|
|
1068
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);
|
|
1069
1249
|
return {
|
|
1070
1250
|
name,
|
|
1071
1251
|
path: `openspec/changes/${name}`,
|
|
@@ -1073,13 +1253,63 @@ function readApplyChange(root, name) {
|
|
|
1073
1253
|
done,
|
|
1074
1254
|
hasProposal: !!proposal,
|
|
1075
1255
|
hasDesign: !!design,
|
|
1256
|
+
inWorktree,
|
|
1076
1257
|
proposal,
|
|
1077
1258
|
design,
|
|
1078
|
-
tasks
|
|
1259
|
+
tasks,
|
|
1260
|
+
dependsOn,
|
|
1261
|
+
hasTestStrategy
|
|
1079
1262
|
};
|
|
1080
1263
|
}
|
|
1081
1264
|
|
|
1082
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
|
+
}
|
|
1083
1313
|
var apply5 = {
|
|
1084
1314
|
inject: ["server", "dashboard"],
|
|
1085
1315
|
apply(ctx, config) {
|
|
@@ -1113,13 +1343,23 @@ var apply5 = {
|
|
|
1113
1343
|
res.end(JSON.stringify({ error: e instanceof Error ? e.message : "unknown error" }));
|
|
1114
1344
|
}
|
|
1115
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
|
+
});
|
|
1116
1356
|
});
|
|
1117
1357
|
}
|
|
1118
1358
|
};
|
|
1119
1359
|
|
|
1120
1360
|
// src/server/design-assets.ts
|
|
1121
|
-
import
|
|
1122
|
-
import
|
|
1361
|
+
import fs9 from "fs";
|
|
1362
|
+
import path8 from "path";
|
|
1123
1363
|
var PAGE_EXTS = [".html", ".htm"];
|
|
1124
1364
|
var ICON_EXTS = [".svg", ".png", ".ico", ".jpg", ".jpeg", ".gif", ".webp"];
|
|
1125
1365
|
var VIDEO_EXTS = [".mp4", ".webm", ".mov", ".ogg", ".ogv"];
|
|
@@ -1136,18 +1376,21 @@ function categorize(rel, ext) {
|
|
|
1136
1376
|
if (FONT_EXTS.includes(ext)) return "font";
|
|
1137
1377
|
if (ICON_EXTS.includes(ext)) return "icon";
|
|
1138
1378
|
if (PAGE_EXTS.includes(ext)) return "page";
|
|
1139
|
-
if (CODE_EXTS.includes(ext))
|
|
1379
|
+
if (CODE_EXTS.includes(ext)) {
|
|
1380
|
+
if (TOKEN_RE.test(rel) && (ext === ".css" || ext === ".json")) return "token";
|
|
1381
|
+
return null;
|
|
1382
|
+
}
|
|
1140
1383
|
return "other";
|
|
1141
1384
|
}
|
|
1142
1385
|
function scanAssets(root) {
|
|
1143
1386
|
const out = {};
|
|
1144
|
-
const keys = ["page", "component", "icon", "token", "md", "video", "audio", "pdf", "
|
|
1387
|
+
const keys = ["page", "component", "icon", "token", "md", "video", "audio", "pdf", "font", "other"];
|
|
1145
1388
|
for (const k of keys) out[k] = [];
|
|
1146
1389
|
const SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "coverage", ".next", ".cache"]);
|
|
1147
1390
|
function walk(dir, rel) {
|
|
1148
1391
|
let ents;
|
|
1149
1392
|
try {
|
|
1150
|
-
ents =
|
|
1393
|
+
ents = fs9.readdirSync(dir, { withFileTypes: true });
|
|
1151
1394
|
} catch {
|
|
1152
1395
|
return;
|
|
1153
1396
|
}
|
|
@@ -1155,12 +1398,12 @@ function scanAssets(root) {
|
|
|
1155
1398
|
if (ent.name.startsWith(".") || SKIP.has(ent.name)) continue;
|
|
1156
1399
|
const r = rel ? `${rel}/${ent.name}` : ent.name;
|
|
1157
1400
|
if (ent.isDirectory()) {
|
|
1158
|
-
walk(
|
|
1401
|
+
walk(path8.join(dir, ent.name), r);
|
|
1159
1402
|
continue;
|
|
1160
1403
|
}
|
|
1161
|
-
const ext =
|
|
1404
|
+
const ext = path8.extname(ent.name).toLowerCase();
|
|
1162
1405
|
const t = categorize(r, ext);
|
|
1163
|
-
out[t].push({ path: r, name: ent.name, ext, type: t });
|
|
1406
|
+
if (t) out[t].push({ path: r, name: ent.name, ext, type: t });
|
|
1164
1407
|
}
|
|
1165
1408
|
}
|
|
1166
1409
|
walk(root, "");
|
|
@@ -1191,11 +1434,91 @@ var apply7 = {
|
|
|
1191
1434
|
}
|
|
1192
1435
|
};
|
|
1193
1436
|
|
|
1437
|
+
// src/plugins/stats/index.ts
|
|
1438
|
+
import fs10 from "fs";
|
|
1439
|
+
import path9 from "path";
|
|
1440
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", ".cache"]);
|
|
1441
|
+
function scan(root) {
|
|
1442
|
+
const stats = {
|
|
1443
|
+
root: path9.basename(root),
|
|
1444
|
+
files: 0,
|
|
1445
|
+
dirs: 0,
|
|
1446
|
+
totalSize: 0,
|
|
1447
|
+
byExt: [],
|
|
1448
|
+
markdown: 0,
|
|
1449
|
+
openspec: { active: 0, archived: 0 },
|
|
1450
|
+
hasJust: fs10.existsSync(path9.join(root, "justfile"))
|
|
1451
|
+
};
|
|
1452
|
+
const extMap = /* @__PURE__ */ new Map();
|
|
1453
|
+
const walk = (dir) => {
|
|
1454
|
+
let ents;
|
|
1455
|
+
try {
|
|
1456
|
+
ents = fs10.readdirSync(dir, { withFileTypes: true });
|
|
1457
|
+
} catch {
|
|
1458
|
+
return;
|
|
1459
|
+
}
|
|
1460
|
+
for (const ent of ents) {
|
|
1461
|
+
if (ent.name.startsWith(".") || SKIP_DIRS.has(ent.name)) continue;
|
|
1462
|
+
const abs = path9.join(dir, ent.name);
|
|
1463
|
+
if (ent.isDirectory()) {
|
|
1464
|
+
stats.dirs++;
|
|
1465
|
+
walk(abs);
|
|
1466
|
+
continue;
|
|
1467
|
+
}
|
|
1468
|
+
stats.files++;
|
|
1469
|
+
try {
|
|
1470
|
+
stats.totalSize += fs10.statSync(abs).size;
|
|
1471
|
+
} catch {
|
|
1472
|
+
}
|
|
1473
|
+
const ext = path9.extname(ent.name).toLowerCase() || "(\u65E0\u6269\u5C55\u540D)";
|
|
1474
|
+
extMap.set(ext, (extMap.get(ext) ?? 0) + 1);
|
|
1475
|
+
if (ext === ".md" || ext === ".markdown") stats.markdown++;
|
|
1476
|
+
}
|
|
1477
|
+
};
|
|
1478
|
+
walk(root);
|
|
1479
|
+
const changesDir = path9.join(root, "openspec", "changes");
|
|
1480
|
+
if (fs10.existsSync(changesDir)) {
|
|
1481
|
+
for (const ent of fs10.readdirSync(changesDir, { withFileTypes: true })) {
|
|
1482
|
+
if (!ent.isDirectory() || ent.name.startsWith(".")) continue;
|
|
1483
|
+
if (ent.name === "archive") {
|
|
1484
|
+
try {
|
|
1485
|
+
stats.openspec.archived = fs10.readdirSync(path9.join(changesDir, "archive"), { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).length;
|
|
1486
|
+
} catch {
|
|
1487
|
+
}
|
|
1488
|
+
} else stats.openspec.active++;
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
stats.byExt = Array.from(extMap.entries()).map(([ext, count]) => ({ ext, count })).sort((a, b) => b.count - a.count).slice(0, 10);
|
|
1492
|
+
return stats;
|
|
1493
|
+
}
|
|
1494
|
+
var apply8 = {
|
|
1495
|
+
inject: ["server", "dashboard"],
|
|
1496
|
+
apply(ctx, config) {
|
|
1497
|
+
ctx.inject(["server", "dashboard"], () => {
|
|
1498
|
+
const server = ctx.server;
|
|
1499
|
+
const dashboard = ctx.dashboard;
|
|
1500
|
+
if (!server?.route || !dashboard?.register) return;
|
|
1501
|
+
dashboard.register({
|
|
1502
|
+
mode: "stats",
|
|
1503
|
+
label: "\u9879\u76EE\u7EDF\u8BA1",
|
|
1504
|
+
icon: "\u{1F4CA}",
|
|
1505
|
+
description: "\u9879\u76EE\u6587\u4EF6\u7EDF\u8BA1 \xB7 \u626B\u63CF\u751F\u6210"
|
|
1506
|
+
});
|
|
1507
|
+
server.route("/__stats/data", (_req, res) => {
|
|
1508
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
|
|
1509
|
+
res.end(JSON.stringify(scan(config.root)));
|
|
1510
|
+
});
|
|
1511
|
+
});
|
|
1512
|
+
}
|
|
1513
|
+
};
|
|
1514
|
+
|
|
1194
1515
|
// src/cli.ts
|
|
1195
|
-
var __dirname2 =
|
|
1516
|
+
var __dirname2 = path10.dirname(fileURLToPath2(import.meta.url));
|
|
1517
|
+
var DEFAULT_PORT = 4190;
|
|
1196
1518
|
function parseArgs() {
|
|
1197
1519
|
const args = process.argv.slice(2);
|
|
1198
1520
|
const opts = {};
|
|
1521
|
+
let portExplicit = false;
|
|
1199
1522
|
for (let i = 0; i < args.length; i++) {
|
|
1200
1523
|
const a = args[i];
|
|
1201
1524
|
if (a.startsWith("--")) {
|
|
@@ -1207,13 +1530,16 @@ function parseArgs() {
|
|
|
1207
1530
|
} else {
|
|
1208
1531
|
opts[key] = true;
|
|
1209
1532
|
}
|
|
1533
|
+
if (key === "port") portExplicit = true;
|
|
1210
1534
|
}
|
|
1211
1535
|
}
|
|
1212
1536
|
return {
|
|
1213
1537
|
dir: typeof opts.dir === "string" ? opts.dir : process.cwd(),
|
|
1214
|
-
port: typeof opts.port === "string" ? Number(opts.port) :
|
|
1538
|
+
port: typeof opts.port === "string" ? Number(opts.port) : DEFAULT_PORT,
|
|
1539
|
+
portExplicit,
|
|
1215
1540
|
open: !!opts.open,
|
|
1216
1541
|
page: typeof opts.page === "string" ? opts.page : null,
|
|
1542
|
+
restart: !!opts.restart,
|
|
1217
1543
|
plugins: typeof opts.plugins === "string" ? opts.plugins : null
|
|
1218
1544
|
};
|
|
1219
1545
|
}
|
|
@@ -1240,7 +1566,7 @@ async function loadExternal(ctx, dir, root) {
|
|
|
1240
1566
|
if (!ent.isDirectory()) continue;
|
|
1241
1567
|
const candidates = tsxLoaded ? ["index.ts", "index.js", "index.mjs"] : ["index.js", "index.mjs"];
|
|
1242
1568
|
for (const name of candidates) {
|
|
1243
|
-
const p =
|
|
1569
|
+
const p = path10.join(dir, ent.name, name);
|
|
1244
1570
|
if (await pathExists(p)) {
|
|
1245
1571
|
try {
|
|
1246
1572
|
const mod = await import(p);
|
|
@@ -1255,8 +1581,8 @@ async function loadExternal(ctx, dir, root) {
|
|
|
1255
1581
|
const m = ctx.dashboard.get(ent.name);
|
|
1256
1582
|
if (m && m.mode === ent.name) {
|
|
1257
1583
|
const patch = { external: true };
|
|
1258
|
-
const webDir =
|
|
1259
|
-
if (await pathExists(
|
|
1584
|
+
const webDir = path10.join(dir, ent.name, "web");
|
|
1585
|
+
if (await pathExists(path10.join(webDir, "index.html"))) {
|
|
1260
1586
|
ctx.server.static(`${PLUGIN_STATIC_PREFIX}${ent.name}/`, webDir);
|
|
1261
1587
|
if (!m.viewerUrl) patch.viewerUrl = `${PLUGIN_STATIC_PREFIX}${ent.name}/`;
|
|
1262
1588
|
}
|
|
@@ -1275,15 +1601,41 @@ async function loadExternal(ctx, dir, root) {
|
|
|
1275
1601
|
}
|
|
1276
1602
|
async function main() {
|
|
1277
1603
|
const args = parseArgs();
|
|
1278
|
-
const root =
|
|
1279
|
-
const
|
|
1604
|
+
const root = path10.resolve(args.dir);
|
|
1605
|
+
const existing = await findReusable(root);
|
|
1606
|
+
if (existing && !args.restart) {
|
|
1607
|
+
const u = `http://localhost:${existing.port}` + (args.page ? `#${args.page}` : "");
|
|
1608
|
+
if (args.open) openUrl(u);
|
|
1609
|
+
console.log(`[zdashboard] \u5DF2\u590D\u7528\u5B9E\u4F8B ${u}\uFF08--restart \u53EF\u5F3A\u5236\u91CD\u5F00\uFF09`);
|
|
1610
|
+
await new Promise((resolve) => process.stdout.write("", () => resolve()));
|
|
1611
|
+
process.exit(0);
|
|
1612
|
+
}
|
|
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);
|
|
1617
|
+
}
|
|
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/" : "";
|
|
1280
1622
|
const det = await detect(root);
|
|
1281
1623
|
const ctx = new Context();
|
|
1282
|
-
ctx.plugin(ServerService, {
|
|
1624
|
+
ctx.plugin(ServerService, {
|
|
1625
|
+
root,
|
|
1626
|
+
appDir,
|
|
1627
|
+
port: startPort,
|
|
1628
|
+
open: args.open,
|
|
1629
|
+
detect: det,
|
|
1630
|
+
page: args.page,
|
|
1631
|
+
dataDir: dataDir || void 0,
|
|
1632
|
+
onListen: (port) => writeRecord(root, port)
|
|
1633
|
+
});
|
|
1283
1634
|
ctx.plugin(ReloadService, { root });
|
|
1284
1635
|
ctx.plugin(apply, { root });
|
|
1285
1636
|
ctx.plugin(DashboardService);
|
|
1286
1637
|
const plugins = [
|
|
1638
|
+
{ name: "stats", apply: apply8 },
|
|
1287
1639
|
{ name: "just", apply: apply2 },
|
|
1288
1640
|
{ name: "bugs", apply: apply3 },
|
|
1289
1641
|
{ name: "review", apply: apply4 },
|
|
@@ -1299,8 +1651,17 @@ async function main() {
|
|
|
1299
1651
|
}
|
|
1300
1652
|
}
|
|
1301
1653
|
if (args.plugins) {
|
|
1302
|
-
await loadExternal(ctx,
|
|
1654
|
+
await loadExternal(ctx, path10.resolve(args.plugins), root);
|
|
1303
1655
|
}
|
|
1656
|
+
const shutdown = () => {
|
|
1657
|
+
try {
|
|
1658
|
+
clearRecord(root);
|
|
1659
|
+
} catch {
|
|
1660
|
+
}
|
|
1661
|
+
process.exit(0);
|
|
1662
|
+
};
|
|
1663
|
+
process.on("SIGTERM", shutdown);
|
|
1664
|
+
process.on("SIGINT", shutdown);
|
|
1304
1665
|
return ctx;
|
|
1305
1666
|
}
|
|
1306
1667
|
await main().catch((e) => {
|