zdashboard 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -0
- package/dist/cli.js +313 -86
- package/dist/cli.js.map +1 -1
- package/dist/web/assets/{MdViewer-DHLa9u9L.js → MdViewer-C8ZG_xn1.js} +1 -1
- package/dist/web/assets/Workspace-Cz1A_IXM.js +1 -0
- package/dist/web/assets/{file-text-D4672ckY.js → file-text-DqCOBCjz.js} +1 -1
- package/dist/web/assets/{index-BRLQFQ6k.js → index-B9eiA0v2.js} +1 -1
- package/dist/web/assets/index-D58_bWw5.css +1 -0
- package/dist/web/assets/{index-Deg587va.js → index-DkOVCCZ3.js} +22 -19
- package/dist/web/assets/{katex.min-D64GJ8fh.js → katex.min-DAmb8ZJI.js} +1 -1
- package/dist/web/assets/web-8ZBVzAEO.js +26 -0
- package/dist/web/assets/{web-2je-DAWY.js → web-CEyEoz0i.js} +5 -5
- package/dist/web/assets/{web-DqBNmBY8.js → web-CXHaDUKR.js} +11 -11
- package/dist/web/assets/{web-C0d3eOAl.js → web-CrX-i-rZ.js} +2 -2
- package/dist/web/assets/{web-Bgs6ipDo.js → web-D_b3sVrp.js} +2 -2
- package/dist/web/assets/web-M4KeFgZw.js +2 -0
- package/dist/web/assets/{web-4tpIem4N.js → web-Y4h9oNHC.js} +2 -2
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-Dqj3ddhK.css +0 -1
- package/dist/web/assets/web-DIAM7Pif.js +0 -26
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import
|
|
4
|
+
import path10 from "path";
|
|
5
5
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
6
6
|
import { access, readdir } from "fs/promises";
|
|
7
7
|
|
|
@@ -30,16 +30,15 @@ import { Context } from "cordis";
|
|
|
30
30
|
|
|
31
31
|
// src/core/server.ts
|
|
32
32
|
import http from "http";
|
|
33
|
-
import
|
|
34
|
-
import
|
|
33
|
+
import fs3 from "fs";
|
|
34
|
+
import path3 from "path";
|
|
35
35
|
import crypto from "crypto";
|
|
36
|
-
import { exec } from "child_process";
|
|
37
36
|
import { fileURLToPath } from "url";
|
|
38
37
|
|
|
39
38
|
// package.json
|
|
40
39
|
var package_default = {
|
|
41
40
|
name: "zdashboard",
|
|
42
|
-
version: "2.
|
|
41
|
+
version: "2.1.0",
|
|
43
42
|
description: "ZCode skill dashboard platform \u2014 pluggable viewers for zdesign/zview/zreview/zgoal",
|
|
44
43
|
type: "module",
|
|
45
44
|
bin: {
|
|
@@ -123,8 +122,113 @@ var package_default = {
|
|
|
123
122
|
|
|
124
123
|
// src/core/server.ts
|
|
125
124
|
import { Service } from "cordis";
|
|
125
|
+
|
|
126
|
+
// src/core/instance.ts
|
|
127
|
+
import fs2 from "fs";
|
|
128
|
+
import path2 from "path";
|
|
129
|
+
var RECORD_FILE = ".zdev/dashboard.json";
|
|
130
|
+
var VERIFY_TIMEOUT_MS = 1500;
|
|
131
|
+
var STOP_POLL_MS = 2e3;
|
|
132
|
+
var STOP_POLL_INTERVAL_MS = 100;
|
|
133
|
+
var STOP_FINAL_WAIT_MS = 100;
|
|
134
|
+
function recordPath(root) {
|
|
135
|
+
return path2.join(root, RECORD_FILE);
|
|
136
|
+
}
|
|
137
|
+
function readRecord(root) {
|
|
138
|
+
try {
|
|
139
|
+
const fp = recordPath(root);
|
|
140
|
+
const raw = fs2.readFileSync(fp, "utf-8");
|
|
141
|
+
const rec = JSON.parse(raw);
|
|
142
|
+
if (typeof rec.pid === "number" && typeof rec.port === "number" && typeof rec.root === "string" && typeof rec.startedAt === "string") {
|
|
143
|
+
return rec;
|
|
144
|
+
}
|
|
145
|
+
return null;
|
|
146
|
+
} catch {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function writeRecord(root, port) {
|
|
151
|
+
fs2.mkdirSync(path2.dirname(recordPath(root)), { recursive: true });
|
|
152
|
+
const rec = {
|
|
153
|
+
pid: process.pid,
|
|
154
|
+
port,
|
|
155
|
+
root,
|
|
156
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
157
|
+
};
|
|
158
|
+
fs2.writeFileSync(recordPath(root), JSON.stringify(rec, null, 2) + "\n");
|
|
159
|
+
}
|
|
160
|
+
function clearRecord(root) {
|
|
161
|
+
try {
|
|
162
|
+
const rec = readRecord(root);
|
|
163
|
+
if (rec && rec.pid !== process.pid) return;
|
|
164
|
+
fs2.unlinkSync(recordPath(root));
|
|
165
|
+
} catch {
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function isAlive(pid) {
|
|
169
|
+
try {
|
|
170
|
+
process.kill(pid, 0);
|
|
171
|
+
return true;
|
|
172
|
+
} catch (e) {
|
|
173
|
+
if (e.code === "ESRCH") return false;
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
async function fetchWithTimeout(url) {
|
|
178
|
+
const controller = new AbortController();
|
|
179
|
+
const timer = setTimeout(() => controller.abort(), VERIFY_TIMEOUT_MS);
|
|
180
|
+
try {
|
|
181
|
+
return await fetch(url, { signal: controller.signal });
|
|
182
|
+
} finally {
|
|
183
|
+
clearTimeout(timer);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
async function verifyRoot(port, root) {
|
|
187
|
+
try {
|
|
188
|
+
const res = await fetchWithTimeout(`http://127.0.0.1:${port}/__config`);
|
|
189
|
+
if (res.status !== 200) return false;
|
|
190
|
+
const body = await res.json();
|
|
191
|
+
if (typeof body.root !== "string") return false;
|
|
192
|
+
return body.root === root;
|
|
193
|
+
} catch {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
async function findReusable(root) {
|
|
198
|
+
const rec = readRecord(root);
|
|
199
|
+
if (!rec) return null;
|
|
200
|
+
if (!isAlive(rec.pid)) return null;
|
|
201
|
+
if (!await verifyRoot(rec.port, root)) return null;
|
|
202
|
+
return rec;
|
|
203
|
+
}
|
|
204
|
+
async function stopInstance(record) {
|
|
205
|
+
try {
|
|
206
|
+
process.kill(record.pid, "SIGTERM");
|
|
207
|
+
} catch {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const deadline = Date.now() + STOP_POLL_MS;
|
|
211
|
+
while (Date.now() < deadline) {
|
|
212
|
+
if (!isAlive(record.pid)) return;
|
|
213
|
+
await new Promise((r) => setTimeout(r, STOP_POLL_INTERVAL_MS));
|
|
214
|
+
}
|
|
215
|
+
try {
|
|
216
|
+
process.kill(record.pid, "SIGKILL");
|
|
217
|
+
} catch {
|
|
218
|
+
}
|
|
219
|
+
await new Promise((r) => setTimeout(r, STOP_FINAL_WAIT_MS));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// src/core/open-url.ts
|
|
223
|
+
import { exec } from "child_process";
|
|
224
|
+
function openUrl2(url) {
|
|
225
|
+
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
226
|
+
exec(`${cmd} ${url}`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// src/core/server.ts
|
|
126
230
|
var VERSION = package_default.version;
|
|
127
|
-
var __dirname =
|
|
231
|
+
var __dirname = path3.dirname(fileURLToPath(import.meta.url));
|
|
128
232
|
var MIME = {
|
|
129
233
|
".html": "text/html; charset=utf-8",
|
|
130
234
|
".htm": "text/html; charset=utf-8",
|
|
@@ -186,6 +290,7 @@ var ServerService = class extends Service {
|
|
|
186
290
|
sses = /* @__PURE__ */ new Map();
|
|
187
291
|
prefixStatic = /* @__PURE__ */ new Map();
|
|
188
292
|
server;
|
|
293
|
+
onListen;
|
|
189
294
|
constructor(ctx, config) {
|
|
190
295
|
super(ctx, "server");
|
|
191
296
|
this.stopToken = crypto.randomBytes(12).toString("hex");
|
|
@@ -194,6 +299,7 @@ var ServerService = class extends Service {
|
|
|
194
299
|
this.open = config.open;
|
|
195
300
|
this.page = config.page;
|
|
196
301
|
this.det = config.detect;
|
|
302
|
+
this.onListen = config.onListen;
|
|
197
303
|
ctx.effect(() => () => this.dispose());
|
|
198
304
|
this.route("/__config", (_req, res) => {
|
|
199
305
|
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
|
|
@@ -211,25 +317,25 @@ var ServerService = class extends Service {
|
|
|
211
317
|
});
|
|
212
318
|
this.start(config.port);
|
|
213
319
|
}
|
|
214
|
-
route(
|
|
215
|
-
this.routes.set(
|
|
216
|
-
this.ctx.effect(() => () => this.routes.delete(
|
|
320
|
+
route(path11, handler) {
|
|
321
|
+
this.routes.set(path11, handler);
|
|
322
|
+
this.ctx.effect(() => () => this.routes.delete(path11));
|
|
217
323
|
}
|
|
218
|
-
sse(
|
|
219
|
-
this.sses.set(
|
|
220
|
-
this.ctx.effect(() => () => this.sses.delete(
|
|
324
|
+
sse(path11, onConnect) {
|
|
325
|
+
this.sses.set(path11, onConnect);
|
|
326
|
+
this.ctx.effect(() => () => this.sses.delete(path11));
|
|
221
327
|
}
|
|
222
328
|
static(prefix, dir) {
|
|
223
329
|
this.prefixStatic.set(prefix, dir);
|
|
224
330
|
this.ctx.effect(() => () => this.prefixStatic.delete(prefix));
|
|
225
331
|
}
|
|
226
332
|
serveFile(filePath, res, injectHtml) {
|
|
227
|
-
|
|
333
|
+
fs3.readFile(filePath, (err, data) => {
|
|
228
334
|
if (err) {
|
|
229
335
|
res.writeHead(404);
|
|
230
336
|
return res.end("Not found");
|
|
231
337
|
}
|
|
232
|
-
const ext =
|
|
338
|
+
const ext = path3.extname(filePath).toLowerCase();
|
|
233
339
|
const ct = MIME[ext] ?? "application/octet-stream";
|
|
234
340
|
let body = data;
|
|
235
341
|
if (injectHtml && ext === ".html") {
|
|
@@ -244,9 +350,9 @@ var ServerService = class extends Service {
|
|
|
244
350
|
for (const [prefix, dir] of this.prefixStatic) {
|
|
245
351
|
if (url.indexOf(prefix) === 0) {
|
|
246
352
|
let rel = this.safeDecode(url.slice(prefix.length));
|
|
247
|
-
if (!rel || rel === "/" || !
|
|
248
|
-
const fp =
|
|
249
|
-
if (fp.indexOf(dir +
|
|
353
|
+
if (!rel || rel === "/" || !path3.extname(rel)) rel = path3.join(rel || "", "index.html");
|
|
354
|
+
const fp = path3.join(dir, rel);
|
|
355
|
+
if (fp.indexOf(dir + path3.sep) !== 0) {
|
|
250
356
|
res.writeHead(403);
|
|
251
357
|
return res.end("Forbidden");
|
|
252
358
|
}
|
|
@@ -285,17 +391,17 @@ var ServerService = class extends Service {
|
|
|
285
391
|
if (this.servePrefix(url, res)) return;
|
|
286
392
|
if (url === "/" || url.indexOf("/__app/") === 0 || url.indexOf("/assets/") === 0) {
|
|
287
393
|
let fp2 = this.appDir;
|
|
288
|
-
if (url !== "/") fp2 =
|
|
289
|
-
if (url.indexOf("/__app/") === 0) fp2 =
|
|
290
|
-
if (fp2 !== this.appDir && fp2.indexOf(this.appDir +
|
|
394
|
+
if (url !== "/") fp2 = path3.join(this.appDir, this.safeDecode(url));
|
|
395
|
+
if (url.indexOf("/__app/") === 0) fp2 = path3.join(this.appDir, url.slice(7));
|
|
396
|
+
if (fp2 !== this.appDir && fp2.indexOf(this.appDir + path3.sep) !== 0) {
|
|
291
397
|
res.writeHead(403);
|
|
292
398
|
return res.end("Forbidden");
|
|
293
399
|
}
|
|
294
|
-
if (url === "/") fp2 =
|
|
400
|
+
if (url === "/") fp2 = path3.join(this.appDir, "index.html");
|
|
295
401
|
return this.serveFile(fp2, res, false);
|
|
296
402
|
}
|
|
297
|
-
const fp =
|
|
298
|
-
if (fp !== this.root && fp.indexOf(this.root +
|
|
403
|
+
const fp = path3.join(this.root, this.safeDecode(url));
|
|
404
|
+
if (fp !== this.root && fp.indexOf(this.root + path3.sep) !== 0) {
|
|
299
405
|
res.writeHead(403);
|
|
300
406
|
return res.end("Forbidden");
|
|
301
407
|
}
|
|
@@ -323,7 +429,8 @@ var ServerService = class extends Service {
|
|
|
323
429
|
console.log(`[zdashboard] v${VERSION} dashboard -> ${u}`);
|
|
324
430
|
console.log(`[zdashboard] project -> ${this.root}`);
|
|
325
431
|
console.log(`[zdashboard] detect -> openspec:${this.det.hasOpenspec} docs:${this.det.hasDocs} just:${this.det.hasJust} bugs:${this.det.hasBugs}`);
|
|
326
|
-
if (this.open)
|
|
432
|
+
if (this.open) openUrl2(target);
|
|
433
|
+
this.onListen?.(port);
|
|
327
434
|
});
|
|
328
435
|
}
|
|
329
436
|
stop() {
|
|
@@ -333,6 +440,10 @@ var ServerService = class extends Service {
|
|
|
333
440
|
} catch {
|
|
334
441
|
}
|
|
335
442
|
}
|
|
443
|
+
try {
|
|
444
|
+
clearRecord(this.root);
|
|
445
|
+
} catch {
|
|
446
|
+
}
|
|
336
447
|
setTimeout(() => {
|
|
337
448
|
try {
|
|
338
449
|
this.ctx.root.fiber.dispose();
|
|
@@ -348,11 +459,15 @@ var ServerService = class extends Service {
|
|
|
348
459
|
} catch {
|
|
349
460
|
}
|
|
350
461
|
}
|
|
462
|
+
try {
|
|
463
|
+
clearRecord(this.root);
|
|
464
|
+
} catch {
|
|
465
|
+
}
|
|
351
466
|
}
|
|
352
467
|
};
|
|
353
468
|
|
|
354
469
|
// src/core/reload.ts
|
|
355
|
-
import
|
|
470
|
+
import fs4 from "fs";
|
|
356
471
|
import { Service as Service2 } from "cordis";
|
|
357
472
|
var WATCH_DEBOUNCE_MS = 150;
|
|
358
473
|
var ReloadService = class extends Service2 {
|
|
@@ -368,7 +483,7 @@ var ReloadService = class extends Service2 {
|
|
|
368
483
|
return () => this.clients.delete(res);
|
|
369
484
|
});
|
|
370
485
|
try {
|
|
371
|
-
this.watcher =
|
|
486
|
+
this.watcher = fs4.watch(config.root, { recursive: true }, () => {
|
|
372
487
|
if (this.timer) clearTimeout(this.timer);
|
|
373
488
|
this.timer = setTimeout(() => {
|
|
374
489
|
this.broadcast("reload");
|
|
@@ -411,13 +526,13 @@ data: ${JSON.stringify(data == null ? "" : data)}
|
|
|
411
526
|
};
|
|
412
527
|
|
|
413
528
|
// src/server/spec-scan.ts
|
|
414
|
-
import
|
|
415
|
-
import
|
|
529
|
+
import fs5 from "fs";
|
|
530
|
+
import path4 from "path";
|
|
416
531
|
function walkFiles(absDir, relDir, depth = 0) {
|
|
417
532
|
if (depth > 4) return [];
|
|
418
533
|
let ents;
|
|
419
534
|
try {
|
|
420
|
-
ents =
|
|
535
|
+
ents = fs5.readdirSync(absDir, { withFileTypes: true });
|
|
421
536
|
} catch {
|
|
422
537
|
return [];
|
|
423
538
|
}
|
|
@@ -426,7 +541,7 @@ function walkFiles(absDir, relDir, depth = 0) {
|
|
|
426
541
|
if (ent.name.startsWith(".") || ent.name === "node_modules") continue;
|
|
427
542
|
const rel = relDir ? `${relDir}/${ent.name}` : ent.name;
|
|
428
543
|
if (ent.isDirectory()) {
|
|
429
|
-
nodes.push({ name: ent.name, kind: "dir", children: walkFiles(
|
|
544
|
+
nodes.push({ name: ent.name, kind: "dir", children: walkFiles(path4.join(absDir, ent.name), rel, depth + 1) });
|
|
430
545
|
} else {
|
|
431
546
|
nodes.push({ name: ent.name, kind: "file", path: rel });
|
|
432
547
|
}
|
|
@@ -436,41 +551,41 @@ function walkFiles(absDir, relDir, depth = 0) {
|
|
|
436
551
|
}
|
|
437
552
|
function scanTree(root, hasOpenspec, hasDocs) {
|
|
438
553
|
const tree = [];
|
|
439
|
-
if (hasOpenspec &&
|
|
440
|
-
const changesDir =
|
|
554
|
+
if (hasOpenspec && fs5.existsSync(path4.join(root, "openspec", "changes"))) {
|
|
555
|
+
const changesDir = path4.join(root, "openspec", "changes");
|
|
441
556
|
const active = [];
|
|
442
557
|
const archived = [];
|
|
443
|
-
for (const ent of
|
|
558
|
+
for (const ent of fs5.readdirSync(changesDir, { withFileTypes: true })) {
|
|
444
559
|
if (!ent.isDirectory() || ent.name.startsWith(".") || ent.name === "archive") continue;
|
|
445
|
-
active.push({ name: ent.name, kind: "dir", children: walkFiles(
|
|
560
|
+
active.push({ name: ent.name, kind: "dir", children: walkFiles(path4.join(changesDir, ent.name), `openspec/changes/${ent.name}`) });
|
|
446
561
|
}
|
|
447
562
|
active.sort((a, b) => a.name.localeCompare(b.name));
|
|
448
|
-
const archiveDir =
|
|
449
|
-
if (
|
|
450
|
-
for (const ent of
|
|
563
|
+
const archiveDir = path4.join(changesDir, "archive");
|
|
564
|
+
if (fs5.existsSync(archiveDir)) {
|
|
565
|
+
for (const ent of fs5.readdirSync(archiveDir, { withFileTypes: true })) {
|
|
451
566
|
if (!ent.isDirectory() || ent.name.startsWith(".")) continue;
|
|
452
|
-
archived.push({ name: ent.name, kind: "dir", children: walkFiles(
|
|
567
|
+
archived.push({ name: ent.name, kind: "dir", children: walkFiles(path4.join(archiveDir, ent.name), `openspec/changes/archive/${ent.name}`) });
|
|
453
568
|
}
|
|
454
569
|
archived.sort((a, b) => b.name.localeCompare(a.name));
|
|
455
570
|
}
|
|
456
571
|
if (active.length) tree.push({ name: `\u8FDB\u884C\u4E2D (${active.length})`, kind: "dir", children: active });
|
|
457
572
|
if (archived.length) tree.push({ name: "archive", kind: "dir", defaultCollapsed: true, children: archived });
|
|
458
|
-
const specsDir =
|
|
459
|
-
if (
|
|
573
|
+
const specsDir = path4.join(root, "openspec", "specs");
|
|
574
|
+
if (fs5.existsSync(specsDir)) {
|
|
460
575
|
const specs = walkFiles(specsDir, "openspec/specs");
|
|
461
576
|
if (specs.length) tree.push({ name: "specs", kind: "dir", children: specs });
|
|
462
577
|
}
|
|
463
578
|
}
|
|
464
|
-
if (hasDocs &&
|
|
465
|
-
const docs = walkFiles(
|
|
579
|
+
if (hasDocs && fs5.existsSync(path4.join(root, "docs"))) {
|
|
580
|
+
const docs = walkFiles(path4.join(root, "docs"), "docs");
|
|
466
581
|
if (docs.length) tree.push({ name: "docs", kind: "dir", children: docs });
|
|
467
582
|
}
|
|
468
583
|
const skip = /* @__PURE__ */ new Set(["openspec", "docs", "node_modules", ".git", "dist", "test-server"]);
|
|
469
584
|
const etc = [];
|
|
470
585
|
try {
|
|
471
|
-
for (const ent of
|
|
586
|
+
for (const ent of fs5.readdirSync(root, { withFileTypes: true })) {
|
|
472
587
|
if (ent.name.startsWith(".") || skip.has(ent.name)) continue;
|
|
473
|
-
const ext =
|
|
588
|
+
const ext = path4.extname(ent.name).toLowerCase();
|
|
474
589
|
if (ent.isFile() && (ext === ".md" || ext === ".markdown")) etc.push({ name: ent.name, kind: "file", path: ent.name });
|
|
475
590
|
}
|
|
476
591
|
} catch (e) {
|
|
@@ -771,8 +886,8 @@ async function readBody(req) {
|
|
|
771
886
|
}
|
|
772
887
|
|
|
773
888
|
// src/server/bugs.ts
|
|
774
|
-
import
|
|
775
|
-
import
|
|
889
|
+
import fs6 from "fs";
|
|
890
|
+
import path5 from "path";
|
|
776
891
|
|
|
777
892
|
// src/server/api/fetch.ts
|
|
778
893
|
import ky from "ky";
|
|
@@ -816,10 +931,10 @@ async function fetchJson(url, init) {
|
|
|
816
931
|
|
|
817
932
|
// src/server/bugs.ts
|
|
818
933
|
function loadZgoalConfig(root) {
|
|
819
|
-
const file =
|
|
820
|
-
if (!
|
|
934
|
+
const file = path5.join(root, ".zgoal", "config.yaml");
|
|
935
|
+
if (!fs6.existsSync(file)) return null;
|
|
821
936
|
const kv = {};
|
|
822
|
-
for (const line of
|
|
937
|
+
for (const line of fs6.readFileSync(file, "utf8").split("\n")) {
|
|
823
938
|
const m = line.match(/^\s*([A-Za-z_]\w*)\s*:\s*(.+?)\s*$/);
|
|
824
939
|
if (m && !m[2].startsWith("#")) kv[m[1]] = m[2].replace(/^["']|["']$/g, "");
|
|
825
940
|
}
|
|
@@ -903,8 +1018,8 @@ var apply3 = {
|
|
|
903
1018
|
};
|
|
904
1019
|
|
|
905
1020
|
// src/server/review-store.ts
|
|
906
|
-
import
|
|
907
|
-
import
|
|
1021
|
+
import fs7 from "fs";
|
|
1022
|
+
import path6 from "path";
|
|
908
1023
|
import YAML from "yaml";
|
|
909
1024
|
var REVIEW_FILE = "review.yaml";
|
|
910
1025
|
var ReviewStore = class {
|
|
@@ -913,15 +1028,15 @@ var ReviewStore = class {
|
|
|
913
1028
|
onChange;
|
|
914
1029
|
constructor(root, onChange) {
|
|
915
1030
|
this.root = root;
|
|
916
|
-
this.file =
|
|
1031
|
+
this.file = path6.join(root, REVIEW_FILE);
|
|
917
1032
|
this.onChange = onChange;
|
|
918
1033
|
}
|
|
919
1034
|
exists() {
|
|
920
|
-
return
|
|
1035
|
+
return fs7.existsSync(this.file);
|
|
921
1036
|
}
|
|
922
1037
|
read() {
|
|
923
1038
|
try {
|
|
924
|
-
const parsed = YAML.parse(
|
|
1039
|
+
const parsed = YAML.parse(fs7.readFileSync(this.file, "utf8"));
|
|
925
1040
|
if (!parsed || !Array.isArray(parsed.items)) return { status: "draft", items: [] };
|
|
926
1041
|
return parsed;
|
|
927
1042
|
} catch {
|
|
@@ -929,7 +1044,7 @@ var ReviewStore = class {
|
|
|
929
1044
|
}
|
|
930
1045
|
}
|
|
931
1046
|
write(data) {
|
|
932
|
-
|
|
1047
|
+
fs7.writeFileSync(this.file, YAML.stringify(data), "utf8");
|
|
933
1048
|
this.onChange?.();
|
|
934
1049
|
}
|
|
935
1050
|
updateItem(id, patch) {
|
|
@@ -953,7 +1068,7 @@ var ReviewStore = class {
|
|
|
953
1068
|
}
|
|
954
1069
|
docs() {
|
|
955
1070
|
try {
|
|
956
|
-
return
|
|
1071
|
+
return fs7.readdirSync(this.root).filter((f) => /\.(md|markdown)$/i.test(f) && fs7.statSync(path6.join(this.root, f)).isFile()).sort();
|
|
957
1072
|
} catch {
|
|
958
1073
|
return [];
|
|
959
1074
|
}
|
|
@@ -1025,8 +1140,8 @@ async function readBody2(req) {
|
|
|
1025
1140
|
}
|
|
1026
1141
|
|
|
1027
1142
|
// src/plugins/apply/scan.ts
|
|
1028
|
-
import
|
|
1029
|
-
import
|
|
1143
|
+
import fs8 from "fs";
|
|
1144
|
+
import path7 from "path";
|
|
1030
1145
|
function countTasks(md) {
|
|
1031
1146
|
const all = (md.match(/^\s*-\s*\[[ xX]\]\s*/gm) || []).length;
|
|
1032
1147
|
const done = (md.match(/^\s*-\s*\[[xX]\]\s*/gm) || []).length;
|
|
@@ -1034,37 +1149,37 @@ function countTasks(md) {
|
|
|
1034
1149
|
}
|
|
1035
1150
|
function readText(p) {
|
|
1036
1151
|
try {
|
|
1037
|
-
return
|
|
1152
|
+
return fs8.readFileSync(p, "utf8");
|
|
1038
1153
|
} catch {
|
|
1039
1154
|
return "";
|
|
1040
1155
|
}
|
|
1041
1156
|
}
|
|
1042
1157
|
function scanApplyChanges(root) {
|
|
1043
|
-
const changesDir =
|
|
1044
|
-
if (!
|
|
1158
|
+
const changesDir = path7.join(root, "openspec", "changes");
|
|
1159
|
+
if (!fs8.existsSync(changesDir)) return [];
|
|
1045
1160
|
const out = [];
|
|
1046
|
-
for (const ent of
|
|
1161
|
+
for (const ent of fs8.readdirSync(changesDir, { withFileTypes: true })) {
|
|
1047
1162
|
if (!ent.isDirectory() || ent.name.startsWith(".") || ent.name === "archive") continue;
|
|
1048
|
-
const dir =
|
|
1049
|
-
const tasks = readText(
|
|
1163
|
+
const dir = path7.join(changesDir, ent.name);
|
|
1164
|
+
const tasks = readText(path7.join(dir, "tasks.md"));
|
|
1050
1165
|
const { total, done } = countTasks(tasks);
|
|
1051
1166
|
out.push({
|
|
1052
1167
|
name: ent.name,
|
|
1053
1168
|
path: `openspec/changes/${ent.name}`,
|
|
1054
1169
|
total,
|
|
1055
1170
|
done,
|
|
1056
|
-
hasProposal:
|
|
1057
|
-
hasDesign:
|
|
1171
|
+
hasProposal: fs8.existsSync(path7.join(dir, "proposal.md")),
|
|
1172
|
+
hasDesign: fs8.existsSync(path7.join(dir, "design.md"))
|
|
1058
1173
|
});
|
|
1059
1174
|
}
|
|
1060
1175
|
out.sort((a, b) => a.name.localeCompare(b.name));
|
|
1061
1176
|
return out;
|
|
1062
1177
|
}
|
|
1063
1178
|
function readApplyChange(root, name) {
|
|
1064
|
-
const dir =
|
|
1065
|
-
const proposal = readText(
|
|
1066
|
-
const design = readText(
|
|
1067
|
-
const tasks = readText(
|
|
1179
|
+
const dir = path7.join(root, "openspec", "changes", name);
|
|
1180
|
+
const proposal = readText(path7.join(dir, "proposal.md"));
|
|
1181
|
+
const design = readText(path7.join(dir, "design.md"));
|
|
1182
|
+
const tasks = readText(path7.join(dir, "tasks.md"));
|
|
1068
1183
|
const { total, done } = countTasks(tasks);
|
|
1069
1184
|
return {
|
|
1070
1185
|
name,
|
|
@@ -1118,8 +1233,8 @@ var apply5 = {
|
|
|
1118
1233
|
};
|
|
1119
1234
|
|
|
1120
1235
|
// src/server/design-assets.ts
|
|
1121
|
-
import
|
|
1122
|
-
import
|
|
1236
|
+
import fs9 from "fs";
|
|
1237
|
+
import path8 from "path";
|
|
1123
1238
|
var PAGE_EXTS = [".html", ".htm"];
|
|
1124
1239
|
var ICON_EXTS = [".svg", ".png", ".ico", ".jpg", ".jpeg", ".gif", ".webp"];
|
|
1125
1240
|
var VIDEO_EXTS = [".mp4", ".webm", ".mov", ".ogg", ".ogv"];
|
|
@@ -1136,18 +1251,21 @@ function categorize(rel, ext) {
|
|
|
1136
1251
|
if (FONT_EXTS.includes(ext)) return "font";
|
|
1137
1252
|
if (ICON_EXTS.includes(ext)) return "icon";
|
|
1138
1253
|
if (PAGE_EXTS.includes(ext)) return "page";
|
|
1139
|
-
if (CODE_EXTS.includes(ext))
|
|
1254
|
+
if (CODE_EXTS.includes(ext)) {
|
|
1255
|
+
if (TOKEN_RE.test(rel) && (ext === ".css" || ext === ".json")) return "token";
|
|
1256
|
+
return null;
|
|
1257
|
+
}
|
|
1140
1258
|
return "other";
|
|
1141
1259
|
}
|
|
1142
1260
|
function scanAssets(root) {
|
|
1143
1261
|
const out = {};
|
|
1144
|
-
const keys = ["page", "component", "icon", "token", "md", "video", "audio", "pdf", "
|
|
1262
|
+
const keys = ["page", "component", "icon", "token", "md", "video", "audio", "pdf", "font", "other"];
|
|
1145
1263
|
for (const k of keys) out[k] = [];
|
|
1146
1264
|
const SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "coverage", ".next", ".cache"]);
|
|
1147
1265
|
function walk(dir, rel) {
|
|
1148
1266
|
let ents;
|
|
1149
1267
|
try {
|
|
1150
|
-
ents =
|
|
1268
|
+
ents = fs9.readdirSync(dir, { withFileTypes: true });
|
|
1151
1269
|
} catch {
|
|
1152
1270
|
return;
|
|
1153
1271
|
}
|
|
@@ -1155,12 +1273,12 @@ function scanAssets(root) {
|
|
|
1155
1273
|
if (ent.name.startsWith(".") || SKIP.has(ent.name)) continue;
|
|
1156
1274
|
const r = rel ? `${rel}/${ent.name}` : ent.name;
|
|
1157
1275
|
if (ent.isDirectory()) {
|
|
1158
|
-
walk(
|
|
1276
|
+
walk(path8.join(dir, ent.name), r);
|
|
1159
1277
|
continue;
|
|
1160
1278
|
}
|
|
1161
|
-
const ext =
|
|
1279
|
+
const ext = path8.extname(ent.name).toLowerCase();
|
|
1162
1280
|
const t = categorize(r, ext);
|
|
1163
|
-
out[t].push({ path: r, name: ent.name, ext, type: t });
|
|
1281
|
+
if (t) out[t].push({ path: r, name: ent.name, ext, type: t });
|
|
1164
1282
|
}
|
|
1165
1283
|
}
|
|
1166
1284
|
walk(root, "");
|
|
@@ -1191,8 +1309,86 @@ var apply7 = {
|
|
|
1191
1309
|
}
|
|
1192
1310
|
};
|
|
1193
1311
|
|
|
1312
|
+
// src/plugins/stats/index.ts
|
|
1313
|
+
import fs10 from "fs";
|
|
1314
|
+
import path9 from "path";
|
|
1315
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", ".cache"]);
|
|
1316
|
+
function scan(root) {
|
|
1317
|
+
const stats = {
|
|
1318
|
+
root: path9.basename(root),
|
|
1319
|
+
files: 0,
|
|
1320
|
+
dirs: 0,
|
|
1321
|
+
totalSize: 0,
|
|
1322
|
+
byExt: [],
|
|
1323
|
+
markdown: 0,
|
|
1324
|
+
openspec: { active: 0, archived: 0 },
|
|
1325
|
+
hasJust: fs10.existsSync(path9.join(root, "justfile"))
|
|
1326
|
+
};
|
|
1327
|
+
const extMap = /* @__PURE__ */ new Map();
|
|
1328
|
+
const walk = (dir) => {
|
|
1329
|
+
let ents;
|
|
1330
|
+
try {
|
|
1331
|
+
ents = fs10.readdirSync(dir, { withFileTypes: true });
|
|
1332
|
+
} catch {
|
|
1333
|
+
return;
|
|
1334
|
+
}
|
|
1335
|
+
for (const ent of ents) {
|
|
1336
|
+
if (ent.name.startsWith(".") || SKIP_DIRS.has(ent.name)) continue;
|
|
1337
|
+
const abs = path9.join(dir, ent.name);
|
|
1338
|
+
if (ent.isDirectory()) {
|
|
1339
|
+
stats.dirs++;
|
|
1340
|
+
walk(abs);
|
|
1341
|
+
continue;
|
|
1342
|
+
}
|
|
1343
|
+
stats.files++;
|
|
1344
|
+
try {
|
|
1345
|
+
stats.totalSize += fs10.statSync(abs).size;
|
|
1346
|
+
} catch {
|
|
1347
|
+
}
|
|
1348
|
+
const ext = path9.extname(ent.name).toLowerCase() || "(\u65E0\u6269\u5C55\u540D)";
|
|
1349
|
+
extMap.set(ext, (extMap.get(ext) ?? 0) + 1);
|
|
1350
|
+
if (ext === ".md" || ext === ".markdown") stats.markdown++;
|
|
1351
|
+
}
|
|
1352
|
+
};
|
|
1353
|
+
walk(root);
|
|
1354
|
+
const changesDir = path9.join(root, "openspec", "changes");
|
|
1355
|
+
if (fs10.existsSync(changesDir)) {
|
|
1356
|
+
for (const ent of fs10.readdirSync(changesDir, { withFileTypes: true })) {
|
|
1357
|
+
if (!ent.isDirectory() || ent.name.startsWith(".")) continue;
|
|
1358
|
+
if (ent.name === "archive") {
|
|
1359
|
+
try {
|
|
1360
|
+
stats.openspec.archived = fs10.readdirSync(path9.join(changesDir, "archive"), { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).length;
|
|
1361
|
+
} catch {
|
|
1362
|
+
}
|
|
1363
|
+
} else stats.openspec.active++;
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
stats.byExt = Array.from(extMap.entries()).map(([ext, count]) => ({ ext, count })).sort((a, b) => b.count - a.count).slice(0, 10);
|
|
1367
|
+
return stats;
|
|
1368
|
+
}
|
|
1369
|
+
var apply8 = {
|
|
1370
|
+
inject: ["server", "dashboard"],
|
|
1371
|
+
apply(ctx, config) {
|
|
1372
|
+
ctx.inject(["server", "dashboard"], () => {
|
|
1373
|
+
const server = ctx.server;
|
|
1374
|
+
const dashboard = ctx.dashboard;
|
|
1375
|
+
if (!server?.route || !dashboard?.register) return;
|
|
1376
|
+
dashboard.register({
|
|
1377
|
+
mode: "stats",
|
|
1378
|
+
label: "\u9879\u76EE\u7EDF\u8BA1",
|
|
1379
|
+
icon: "\u{1F4CA}",
|
|
1380
|
+
description: "\u9879\u76EE\u6587\u4EF6\u7EDF\u8BA1 \xB7 \u626B\u63CF\u751F\u6210"
|
|
1381
|
+
});
|
|
1382
|
+
server.route("/__stats/data", (_req, res) => {
|
|
1383
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
|
|
1384
|
+
res.end(JSON.stringify(scan(config.root)));
|
|
1385
|
+
});
|
|
1386
|
+
});
|
|
1387
|
+
}
|
|
1388
|
+
};
|
|
1389
|
+
|
|
1194
1390
|
// src/cli.ts
|
|
1195
|
-
var __dirname2 =
|
|
1391
|
+
var __dirname2 = path10.dirname(fileURLToPath2(import.meta.url));
|
|
1196
1392
|
function parseArgs() {
|
|
1197
1393
|
const args = process.argv.slice(2);
|
|
1198
1394
|
const opts = {};
|
|
@@ -1214,6 +1410,7 @@ function parseArgs() {
|
|
|
1214
1410
|
port: typeof opts.port === "string" ? Number(opts.port) : 4190,
|
|
1215
1411
|
open: !!opts.open,
|
|
1216
1412
|
page: typeof opts.page === "string" ? opts.page : null,
|
|
1413
|
+
restart: !!opts.restart,
|
|
1217
1414
|
plugins: typeof opts.plugins === "string" ? opts.plugins : null
|
|
1218
1415
|
};
|
|
1219
1416
|
}
|
|
@@ -1240,7 +1437,7 @@ async function loadExternal(ctx, dir, root) {
|
|
|
1240
1437
|
if (!ent.isDirectory()) continue;
|
|
1241
1438
|
const candidates = tsxLoaded ? ["index.ts", "index.js", "index.mjs"] : ["index.js", "index.mjs"];
|
|
1242
1439
|
for (const name of candidates) {
|
|
1243
|
-
const p =
|
|
1440
|
+
const p = path10.join(dir, ent.name, name);
|
|
1244
1441
|
if (await pathExists(p)) {
|
|
1245
1442
|
try {
|
|
1246
1443
|
const mod = await import(p);
|
|
@@ -1255,8 +1452,8 @@ async function loadExternal(ctx, dir, root) {
|
|
|
1255
1452
|
const m = ctx.dashboard.get(ent.name);
|
|
1256
1453
|
if (m && m.mode === ent.name) {
|
|
1257
1454
|
const patch = { external: true };
|
|
1258
|
-
const webDir =
|
|
1259
|
-
if (await pathExists(
|
|
1455
|
+
const webDir = path10.join(dir, ent.name, "web");
|
|
1456
|
+
if (await pathExists(path10.join(webDir, "index.html"))) {
|
|
1260
1457
|
ctx.server.static(`${PLUGIN_STATIC_PREFIX}${ent.name}/`, webDir);
|
|
1261
1458
|
if (!m.viewerUrl) patch.viewerUrl = `${PLUGIN_STATIC_PREFIX}${ent.name}/`;
|
|
1262
1459
|
}
|
|
@@ -1275,15 +1472,36 @@ async function loadExternal(ctx, dir, root) {
|
|
|
1275
1472
|
}
|
|
1276
1473
|
async function main() {
|
|
1277
1474
|
const args = parseArgs();
|
|
1278
|
-
const root =
|
|
1279
|
-
const
|
|
1475
|
+
const root = path10.resolve(args.dir);
|
|
1476
|
+
const existing = await findReusable(root);
|
|
1477
|
+
if (existing && !args.restart) {
|
|
1478
|
+
const u = `http://localhost:${existing.port}` + (args.page ? `#${args.page}` : "");
|
|
1479
|
+
if (args.open) openUrl(u);
|
|
1480
|
+
console.log(`[zdashboard] \u5DF2\u590D\u7528\u5B9E\u4F8B ${u}\uFF08--restart \u53EF\u5F3A\u5236\u91CD\u5F00\uFF09`);
|
|
1481
|
+
await new Promise((resolve) => process.stdout.write("", () => resolve()));
|
|
1482
|
+
process.exit(0);
|
|
1483
|
+
}
|
|
1484
|
+
if (existing && args.restart) {
|
|
1485
|
+
console.log(`[zdashboard] --restart\uFF1A\u505C\u6B62\u65E7\u5B9E\u4F8B pid=${existing.pid}`);
|
|
1486
|
+
await stopInstance(existing);
|
|
1487
|
+
}
|
|
1488
|
+
const appDir = path10.resolve(__dirname2, "web");
|
|
1280
1489
|
const det = await detect(root);
|
|
1281
1490
|
const ctx = new Context();
|
|
1282
|
-
ctx.plugin(ServerService, {
|
|
1491
|
+
ctx.plugin(ServerService, {
|
|
1492
|
+
root,
|
|
1493
|
+
appDir,
|
|
1494
|
+
port: args.port,
|
|
1495
|
+
open: args.open,
|
|
1496
|
+
detect: det,
|
|
1497
|
+
page: args.page,
|
|
1498
|
+
onListen: (port) => writeRecord(root, port)
|
|
1499
|
+
});
|
|
1283
1500
|
ctx.plugin(ReloadService, { root });
|
|
1284
1501
|
ctx.plugin(apply, { root });
|
|
1285
1502
|
ctx.plugin(DashboardService);
|
|
1286
1503
|
const plugins = [
|
|
1504
|
+
{ name: "stats", apply: apply8 },
|
|
1287
1505
|
{ name: "just", apply: apply2 },
|
|
1288
1506
|
{ name: "bugs", apply: apply3 },
|
|
1289
1507
|
{ name: "review", apply: apply4 },
|
|
@@ -1299,8 +1517,17 @@ async function main() {
|
|
|
1299
1517
|
}
|
|
1300
1518
|
}
|
|
1301
1519
|
if (args.plugins) {
|
|
1302
|
-
await loadExternal(ctx,
|
|
1520
|
+
await loadExternal(ctx, path10.resolve(args.plugins), root);
|
|
1303
1521
|
}
|
|
1522
|
+
const shutdown = () => {
|
|
1523
|
+
try {
|
|
1524
|
+
clearRecord(root);
|
|
1525
|
+
} catch {
|
|
1526
|
+
}
|
|
1527
|
+
process.exit(0);
|
|
1528
|
+
};
|
|
1529
|
+
process.on("SIGTERM", shutdown);
|
|
1530
|
+
process.on("SIGINT", shutdown);
|
|
1304
1531
|
return ctx;
|
|
1305
1532
|
}
|
|
1306
1533
|
await main().catch((e) => {
|