zdashboard 1.6.2 → 2.0.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/dist/cli.js CHANGED
@@ -1,138 +1,594 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // src/server/index.ts
4
- import http from "http";
5
- import fs6 from "fs";
6
- import path6 from "path";
7
- import crypto from "crypto";
8
- import { exec } from "child_process";
3
+ // src/cli.ts
4
+ import path8 from "path";
9
5
  import { fileURLToPath as fileURLToPath2 } from "url";
6
+ import { access, readdir } from "fs/promises";
10
7
 
11
- // src/server/spec-scan.ts
8
+ // src/server/detect.ts
12
9
  import fs from "fs";
13
10
  import path from "path";
14
- function walkFiles(absDir, relDir, depth = 0) {
15
- if (depth > 4) return [];
16
- let ents;
17
- try {
18
- ents = fs.readdirSync(absDir, { withFileTypes: true });
19
- } catch {
20
- return [];
21
- }
22
- const nodes = [];
23
- for (const ent of ents) {
24
- if (ent.name.startsWith(".") || ent.name === "node_modules") continue;
25
- const rel = relDir ? `${relDir}/${ent.name}` : ent.name;
26
- if (ent.isDirectory()) {
27
- nodes.push({ name: ent.name, kind: "dir", children: walkFiles(path.join(absDir, ent.name), rel, depth + 1) });
28
- } else {
29
- nodes.push({ name: ent.name, kind: "file", path: rel });
30
- }
31
- }
32
- nodes.sort((a, b) => a.kind === b.kind ? a.name.localeCompare(b.name) : a.kind === "dir" ? -1 : 1);
33
- return nodes;
11
+ import { execFile } from "child_process";
12
+ function justAvailable(cwd) {
13
+ return new Promise((resolve) => {
14
+ const child = execFile("just", ["--list", "--unsorted"], { cwd, timeout: 5e3 }, (err) => {
15
+ resolve(!err);
16
+ });
17
+ if (child.killed) resolve(false);
18
+ });
34
19
  }
35
- function scanTree(root2, hasOpenspec, hasDocs) {
36
- const tree = [];
37
- if (hasOpenspec && fs.existsSync(path.join(root2, "openspec", "changes"))) {
38
- const changesDir = path.join(root2, "openspec", "changes");
39
- const active = [];
40
- const archived = [];
41
- for (const ent of fs.readdirSync(changesDir, { withFileTypes: true })) {
42
- if (!ent.isDirectory() || ent.name.startsWith(".") || ent.name === "archive") continue;
43
- active.push({ name: ent.name, kind: "dir", children: walkFiles(path.join(changesDir, ent.name), `openspec/changes/${ent.name}`) });
44
- }
45
- active.sort((a, b) => a.name.localeCompare(b.name));
46
- const archiveDir = path.join(changesDir, "archive");
47
- if (fs.existsSync(archiveDir)) {
48
- for (const ent of fs.readdirSync(archiveDir, { withFileTypes: true })) {
49
- if (!ent.isDirectory() || ent.name.startsWith(".")) continue;
50
- archived.push({ name: ent.name, kind: "dir", children: walkFiles(path.join(archiveDir, ent.name), `openspec/changes/archive/${ent.name}`) });
51
- }
52
- archived.sort((a, b) => b.name.localeCompare(a.name));
53
- }
54
- if (active.length) tree.push({ name: `\u8FDB\u884C\u4E2D (${active.length})`, kind: "dir", children: active });
55
- if (archived.length) tree.push({ name: "archive", kind: "dir", defaultCollapsed: true, children: archived });
56
- const specsDir = path.join(root2, "openspec", "specs");
57
- if (fs.existsSync(specsDir)) {
58
- const specs = walkFiles(specsDir, "openspec/specs");
59
- if (specs.length) tree.push({ name: "specs", kind: "dir", children: specs });
60
- }
61
- }
62
- if (hasDocs && fs.existsSync(path.join(root2, "docs"))) {
63
- const docs = walkFiles(path.join(root2, "docs"), "docs");
64
- if (docs.length) tree.push({ name: "docs", kind: "dir", children: docs });
65
- }
66
- const skip = /* @__PURE__ */ new Set(["openspec", "docs", "node_modules", ".git", "dist", "test-server"]);
67
- const etc = [];
68
- try {
69
- for (const ent of fs.readdirSync(root2, { withFileTypes: true })) {
70
- if (ent.name.startsWith(".") || skip.has(ent.name)) continue;
71
- const ext = path.extname(ent.name).toLowerCase();
72
- if (ent.isFile() && (ext === ".md" || ext === ".markdown")) etc.push({ name: ent.name, kind: "file", path: ent.name });
73
- }
74
- } catch (e) {
75
- console.error("[zdashboard] scan root etc failed:", e);
76
- }
77
- etc.sort((a, b) => a.name.localeCompare(b.name));
78
- if (etc.length) tree.push({ name: `\u5176\u4ED6 (${etc.length})`, kind: "dir", children: etc });
79
- return tree;
20
+ async function detect(root) {
21
+ const hasOpenspec = fs.existsSync(path.join(root, "openspec"));
22
+ const hasDocs = fs.existsSync(path.join(root, "docs"));
23
+ const hasJust = await justAvailable(root);
24
+ const hasBugs = fs.existsSync(path.join(root, ".zgoal", "config.yaml"));
25
+ return { hasOpenspec, hasDocs, hasJust, hasBugs };
80
26
  }
81
27
 
82
- // src/server/just-runner.ts
83
- import { spawn, execFile } from "child_process";
84
- var MAX_BUFFER = 1e3;
85
- var JustRunner = class {
86
- cwd;
87
- child = null;
88
- recipe = null;
89
- state = "idle";
90
- code = null;
91
- buffer = [];
92
- pending = "";
93
- // 行缓冲:块缓冲输出(如 maven)的 chunk 会在行中间断开,攒到 \n 才切行
94
- clients = /* @__PURE__ */ new Set();
95
- recipesCache = null;
96
- constructor(cwd) {
97
- this.cwd = cwd;
28
+ // src/cli.ts
29
+ import { Context } from "cordis";
30
+
31
+ // src/core/server.ts
32
+ import http from "http";
33
+ import fs2 from "fs";
34
+ import path2 from "path";
35
+ import crypto from "crypto";
36
+ import { exec } from "child_process";
37
+ import { fileURLToPath } from "url";
38
+
39
+ // package.json
40
+ var package_default = {
41
+ name: "zdashboard",
42
+ version: "2.0.0",
43
+ description: "ZCode skill dashboard platform \u2014 pluggable viewers for zdesign/zview/zreview/zgoal",
44
+ type: "module",
45
+ bin: {
46
+ zdashboard: "./dist/cli.js"
47
+ },
48
+ files: [
49
+ "dist"
50
+ ],
51
+ publishConfig: {
52
+ access: "public"
53
+ },
54
+ scripts: {
55
+ dev: "vite",
56
+ build: "tsup && vite build",
57
+ "build:web": "vite build",
58
+ "build:node": "tsup",
59
+ start: "node dist/cli.js",
60
+ preview: "vite preview"
61
+ },
62
+ dependencies: {
63
+ "@radix-ui/react-scroll-area": "^1.2.0",
64
+ "@radix-ui/react-separator": "^1.1.0",
65
+ "@radix-ui/react-slot": "^1.1.0",
66
+ "@radix-ui/react-tooltip": "^1.1.2",
67
+ "@uidotdev/usehooks": "^2.4.0",
68
+ "ansi-to-react": "^6.1.6",
69
+ "class-variance-authority": "^0.7.0",
70
+ clsx: "^2.1.1",
71
+ cordis: "4.0.0-rc.8",
72
+ cosmokit: "^1.8.1",
73
+ "date-fns": "^3.6.0",
74
+ filesize: "^11.0.0",
75
+ "highlight.js": "^11.11.1",
76
+ katex: "^0.16.11",
77
+ ky: "^1.7.0",
78
+ "lodash-es": "^4.17.21",
79
+ "lucide-react": "^0.460.0",
80
+ react: "^18.3.1",
81
+ "react-dom": "^18.3.1",
82
+ "react-error-boundary": "^5.0.0",
83
+ "react-markdown": "^9.0.1",
84
+ "rehype-autolink-headings": "^7.1.0",
85
+ "rehype-highlight": "^7.0.1",
86
+ "rehype-katex": "^7.0.1",
87
+ "rehype-raw": "^7.0.0",
88
+ "rehype-slug": "^6.0.0",
89
+ "remark-frontmatter": "^5.0.0",
90
+ "remark-gfm": "^4.0.0",
91
+ "remark-math": "^6.0.0",
92
+ sonner: "^1.5.0",
93
+ "tailwind-merge": "^2.5.4",
94
+ tsx: "^4.7.0",
95
+ "use-debounce": "^10.0.0",
96
+ yaml: "^2.9.0"
97
+ },
98
+ devDependencies: {
99
+ "@tailwindcss/typography": "^0.5.20",
100
+ "@testing-library/jest-dom": "^7.0.1",
101
+ "@testing-library/react": "^16.3.2",
102
+ "@types/lodash-es": "^4.17.12",
103
+ "@types/node": "^22.9.0",
104
+ "@types/react": "^18.3.12",
105
+ "@types/react-dom": "^18.3.1",
106
+ "@vitejs/plugin-react": "^4.7.0",
107
+ autoprefixer: "^10.4.20",
108
+ jsdom: "^30.0.1",
109
+ postcss: "^8.4.49",
110
+ tailwindcss: "^3.4.14",
111
+ "tailwindcss-animate": "^1.0.7",
112
+ tsup: "^8.3.5",
113
+ typescript: "^5.6.3",
114
+ vite: "^5.4.10",
115
+ vitest: "^1.6.0"
116
+ },
117
+ pnpm: {
118
+ onlyBuiltDependencies: [
119
+ "esbuild"
120
+ ]
98
121
  }
99
- recipes() {
100
- if (this.recipesCache) return Promise.resolve(this.recipesCache);
101
- return new Promise((resolve) => {
102
- execFile("just", ["--list", "--unsorted"], { cwd: this.cwd, maxBuffer: 1 << 20, timeout: 8e3 }, (err, stdout) => {
103
- if (err) {
104
- resolve([]);
105
- return;
106
- }
107
- const out = [];
108
- const seen = /* @__PURE__ */ new Set();
109
- for (const line of stdout.split(/\r?\n/).slice(1)) {
110
- const trimmed = line.trim();
111
- if (!trimmed) continue;
112
- const hashIdx = trimmed.indexOf("#");
113
- const sig = (hashIdx >= 0 ? trimmed.slice(0, hashIdx) : trimmed).trim();
114
- if (!sig) continue;
115
- const name = sig.split(/\s+/)[0];
116
- if (seen.has(name)) continue;
117
- seen.add(name);
118
- out.push({ name, description: hashIdx >= 0 ? trimmed.slice(hashIdx + 1).trim() : "" });
119
- }
120
- this.recipesCache = out;
121
- resolve(out);
122
- });
122
+ };
123
+
124
+ // src/core/server.ts
125
+ import { Service } from "cordis";
126
+ var VERSION = package_default.version;
127
+ var __dirname = path2.dirname(fileURLToPath(import.meta.url));
128
+ var MIME = {
129
+ ".html": "text/html; charset=utf-8",
130
+ ".htm": "text/html; charset=utf-8",
131
+ ".css": "text/css; charset=utf-8",
132
+ ".js": "application/javascript; charset=utf-8",
133
+ ".mjs": "application/javascript; charset=utf-8",
134
+ ".json": "application/json; charset=utf-8",
135
+ ".svg": "image/svg+xml",
136
+ ".png": "image/png",
137
+ ".ico": "image/x-icon",
138
+ ".jpg": "image/jpeg",
139
+ ".jpeg": "image/jpeg",
140
+ ".gif": "image/gif",
141
+ ".webp": "image/webp",
142
+ ".md": "text/markdown; charset=utf-8",
143
+ ".txt": "text/plain; charset=utf-8",
144
+ ".sql": "text/plain; charset=utf-8",
145
+ ".csv": "text/plain; charset=utf-8",
146
+ ".tsv": "text/plain; charset=utf-8",
147
+ ".yaml": "text/yaml; charset=utf-8",
148
+ ".yml": "text/yaml; charset=utf-8",
149
+ ".xml": "application/xml; charset=utf-8",
150
+ ".py": "text/plain; charset=utf-8",
151
+ ".ts": "text/plain; charset=utf-8",
152
+ ".java": "text/plain; charset=utf-8",
153
+ ".go": "text/plain; charset=utf-8",
154
+ ".rs": "text/plain; charset=utf-8",
155
+ ".rb": "text/plain; charset=utf-8",
156
+ ".php": "text/plain; charset=utf-8",
157
+ ".c": "text/plain; charset=utf-8",
158
+ ".cpp": "text/plain; charset=utf-8",
159
+ ".h": "text/plain; charset=utf-8",
160
+ ".cs": "text/plain; charset=utf-8",
161
+ ".swift": "text/plain; charset=utf-8",
162
+ ".kt": "text/plain; charset=utf-8",
163
+ ".scala": "text/plain; charset=utf-8",
164
+ ".sh": "text/plain; charset=utf-8",
165
+ ".bash": "text/plain; charset=utf-8",
166
+ ".zsh": "text/plain; charset=utf-8",
167
+ ".fish": "text/plain; charset=utf-8",
168
+ ".env": "text/plain; charset=utf-8",
169
+ ".gitignore": "text/plain; charset=utf-8",
170
+ ".dockerfile": "text/plain; charset=utf-8",
171
+ ".woff": "font/woff",
172
+ ".woff2": "font/woff2",
173
+ ".ttf": "font/woff",
174
+ ".map": "application/json; charset=utf-8"
175
+ };
176
+ var INJECT = `<script>(function(){try{var es=new EventSource('/__reload');es.addEventListener('reload',function(){location.reload();});es.onerror=function(){es.close();};}catch(e){}document.addEventListener('click',function(e){var t=e.target;if(t&&t.closest){var a=t.closest('a[target]');if(a&&a.target!=='_self'){a.target='_self';}}},true);})();</script>`;
177
+ var PLUGIN_STATIC_PREFIX = "/__plugin/";
178
+ var ServerService = class extends Service {
179
+ stopToken;
180
+ root;
181
+ appDir;
182
+ open;
183
+ page;
184
+ det;
185
+ routes = /* @__PURE__ */ new Map();
186
+ sses = /* @__PURE__ */ new Map();
187
+ prefixStatic = /* @__PURE__ */ new Map();
188
+ server;
189
+ constructor(ctx, config) {
190
+ super(ctx, "server");
191
+ this.stopToken = crypto.randomBytes(12).toString("hex");
192
+ this.root = config.root;
193
+ this.appDir = config.appDir;
194
+ this.open = config.open;
195
+ this.page = config.page;
196
+ this.det = config.detect;
197
+ ctx.effect(() => () => this.dispose());
198
+ this.route("/__config", (_req, res) => {
199
+ res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
200
+ res.end(JSON.stringify({ stopToken: this.stopToken, version: VERSION, root: this.root }));
201
+ });
202
+ this.route("/__stop", async (req, res) => {
203
+ if (req.headers["x-stop-token"] !== this.stopToken) {
204
+ res.writeHead(403);
205
+ res.end("forbidden");
206
+ return;
207
+ }
208
+ res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
209
+ res.end('{"ok":true}');
210
+ this.stop();
123
211
  });
212
+ this.start(config.port);
124
213
  }
125
- subscribe(fn) {
126
- this.clients.add(fn);
127
- for (const text of this.buffer) fn({ type: "log", text });
128
- fn({ type: "state", state: this.state, recipe: this.recipe, code: this.code });
129
- return () => this.clients.delete(fn);
214
+ route(path9, handler) {
215
+ this.routes.set(path9, handler);
216
+ this.ctx.effect(() => () => this.routes.delete(path9));
130
217
  }
131
- emit(ev) {
132
- for (const fn of this.clients) fn(ev);
218
+ sse(path9, onConnect) {
219
+ this.sses.set(path9, onConnect);
220
+ this.ctx.effect(() => () => this.sses.delete(path9));
133
221
  }
134
- info() {
135
- return { state: this.state, recipe: this.recipe, code: this.code };
222
+ static(prefix, dir) {
223
+ this.prefixStatic.set(prefix, dir);
224
+ this.ctx.effect(() => () => this.prefixStatic.delete(prefix));
225
+ }
226
+ serveFile(filePath, res, injectHtml) {
227
+ fs2.readFile(filePath, (err, data) => {
228
+ if (err) {
229
+ res.writeHead(404);
230
+ return res.end("Not found");
231
+ }
232
+ const ext = path2.extname(filePath).toLowerCase();
233
+ const ct = MIME[ext] ?? "application/octet-stream";
234
+ let body = data;
235
+ if (injectHtml && ext === ".html") {
236
+ const s = data.toString("utf8");
237
+ body = Buffer.from(s.indexOf("</body>") >= 0 ? s.replace("</body>", INJECT + "</body>") : s + INJECT);
238
+ }
239
+ res.writeHead(200, { "Content-Type": ct, "Cache-Control": "no-cache" });
240
+ res.end(body);
241
+ });
242
+ }
243
+ servePrefix(url, res) {
244
+ for (const [prefix, dir] of this.prefixStatic) {
245
+ if (url.indexOf(prefix) === 0) {
246
+ let rel = this.safeDecode(url.slice(prefix.length));
247
+ if (!rel || rel === "/" || !path2.extname(rel)) rel = path2.join(rel || "", "index.html");
248
+ const fp = path2.join(dir, rel);
249
+ if (fp.indexOf(dir + path2.sep) !== 0) {
250
+ res.writeHead(403);
251
+ return res.end("Forbidden");
252
+ }
253
+ this.serveFile(fp, res, true);
254
+ return true;
255
+ }
256
+ }
257
+ return false;
258
+ }
259
+ safeDecode(raw) {
260
+ try {
261
+ return decodeURIComponent(raw);
262
+ } catch {
263
+ return raw;
264
+ }
265
+ }
266
+ handler = (req, res) => {
267
+ try {
268
+ const rawUrl = req.url || "/";
269
+ const url = rawUrl.split("?")[0];
270
+ const rh = this.routes.get(url);
271
+ if (rh) {
272
+ rh(req, res);
273
+ return;
274
+ }
275
+ const sh = this.sses.get(url);
276
+ if (sh) {
277
+ res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" });
278
+ res.write(": connected\n\n");
279
+ const cleanup = sh(res);
280
+ req.on("close", () => {
281
+ cleanup?.();
282
+ });
283
+ return;
284
+ }
285
+ if (this.servePrefix(url, res)) return;
286
+ if (url === "/" || url.indexOf("/__app/") === 0 || url.indexOf("/assets/") === 0) {
287
+ let fp2 = this.appDir;
288
+ if (url !== "/") fp2 = path2.join(this.appDir, this.safeDecode(url));
289
+ if (url.indexOf("/__app/") === 0) fp2 = path2.join(this.appDir, url.slice(7));
290
+ if (fp2 !== this.appDir && fp2.indexOf(this.appDir + path2.sep) !== 0) {
291
+ res.writeHead(403);
292
+ return res.end("Forbidden");
293
+ }
294
+ if (url === "/") fp2 = path2.join(this.appDir, "index.html");
295
+ return this.serveFile(fp2, res, false);
296
+ }
297
+ const fp = path2.join(this.root, this.safeDecode(url));
298
+ if (fp !== this.root && fp.indexOf(this.root + path2.sep) !== 0) {
299
+ res.writeHead(403);
300
+ return res.end("Forbidden");
301
+ }
302
+ return this.serveFile(fp, res, true);
303
+ } catch (e) {
304
+ try {
305
+ res.writeHead(400);
306
+ res.end("bad request");
307
+ } catch {
308
+ }
309
+ console.error("[zdashboard] handler error", e);
310
+ }
311
+ };
312
+ start(port) {
313
+ this.server = http.createServer(this.handler);
314
+ this.server.on("error", (err) => {
315
+ if (err.code === "EADDRINUSE") {
316
+ console.log(`[zdashboard] port ${port} busy, trying ${port + 1}`);
317
+ this.start(port + 1);
318
+ } else throw err;
319
+ });
320
+ this.server.listen(port, "127.0.0.1", () => {
321
+ const u = `http://localhost:${port}`;
322
+ const target = this.page ? `${u}#${this.page}` : u;
323
+ console.log(`[zdashboard] v${VERSION} dashboard -> ${u}`);
324
+ console.log(`[zdashboard] project -> ${this.root}`);
325
+ console.log(`[zdashboard] detect -> openspec:${this.det.hasOpenspec} docs:${this.det.hasDocs} just:${this.det.hasJust} bugs:${this.det.hasBugs}`);
326
+ if (this.open) exec(process.platform === "darwin" ? `open ${target}` : `start ${target}`);
327
+ });
328
+ }
329
+ stop() {
330
+ if (this.server) {
331
+ try {
332
+ this.server.close();
333
+ } catch {
334
+ }
335
+ }
336
+ setTimeout(() => {
337
+ try {
338
+ this.ctx.root.fiber.dispose();
339
+ } catch {
340
+ }
341
+ process.exit(0);
342
+ }, 50);
343
+ }
344
+ dispose() {
345
+ if (this.server) {
346
+ try {
347
+ this.server.close();
348
+ } catch {
349
+ }
350
+ }
351
+ }
352
+ };
353
+
354
+ // src/core/reload.ts
355
+ import fs3 from "fs";
356
+ import { Service as Service2 } from "cordis";
357
+ var WATCH_DEBOUNCE_MS = 150;
358
+ var ReloadService = class extends Service2 {
359
+ static inject = ["server"];
360
+ clients = /* @__PURE__ */ new Set();
361
+ watcher;
362
+ timer;
363
+ constructor(ctx, config) {
364
+ super(ctx, "reload");
365
+ this.ctx.effect(() => () => this.dispose());
366
+ this.ctx.server.sse("/__reload", (res) => {
367
+ this.clients.add(res);
368
+ return () => this.clients.delete(res);
369
+ });
370
+ try {
371
+ this.watcher = fs3.watch(config.root, { recursive: true }, () => {
372
+ if (this.timer) clearTimeout(this.timer);
373
+ this.timer = setTimeout(() => {
374
+ this.broadcast("reload");
375
+ this.broadcast("files");
376
+ }, WATCH_DEBOUNCE_MS);
377
+ });
378
+ } catch {
379
+ }
380
+ }
381
+ broadcast(ev, data = "") {
382
+ const payload = `event: ${ev}
383
+ data: ${JSON.stringify(data == null ? "" : data)}
384
+
385
+ `;
386
+ for (const c of this.clients) {
387
+ try {
388
+ c.write(payload);
389
+ } catch {
390
+ }
391
+ }
392
+ }
393
+ dispose() {
394
+ if (this.watcher) {
395
+ try {
396
+ this.watcher.close();
397
+ } catch {
398
+ }
399
+ }
400
+ if (this.timer) {
401
+ clearTimeout(this.timer);
402
+ }
403
+ for (const c of this.clients) {
404
+ try {
405
+ c.end();
406
+ } catch {
407
+ }
408
+ }
409
+ this.clients.clear();
410
+ }
411
+ };
412
+
413
+ // src/server/spec-scan.ts
414
+ import fs4 from "fs";
415
+ import path3 from "path";
416
+ function walkFiles(absDir, relDir, depth = 0) {
417
+ if (depth > 4) return [];
418
+ let ents;
419
+ try {
420
+ ents = fs4.readdirSync(absDir, { withFileTypes: true });
421
+ } catch {
422
+ return [];
423
+ }
424
+ const nodes = [];
425
+ for (const ent of ents) {
426
+ if (ent.name.startsWith(".") || ent.name === "node_modules") continue;
427
+ const rel = relDir ? `${relDir}/${ent.name}` : ent.name;
428
+ if (ent.isDirectory()) {
429
+ nodes.push({ name: ent.name, kind: "dir", children: walkFiles(path3.join(absDir, ent.name), rel, depth + 1) });
430
+ } else {
431
+ nodes.push({ name: ent.name, kind: "file", path: rel });
432
+ }
433
+ }
434
+ nodes.sort((a, b) => a.kind === b.kind ? a.name.localeCompare(b.name) : a.kind === "dir" ? -1 : 1);
435
+ return nodes;
436
+ }
437
+ function scanTree(root, hasOpenspec, hasDocs) {
438
+ const tree = [];
439
+ if (hasOpenspec && fs4.existsSync(path3.join(root, "openspec", "changes"))) {
440
+ const changesDir = path3.join(root, "openspec", "changes");
441
+ const active = [];
442
+ const archived = [];
443
+ for (const ent of fs4.readdirSync(changesDir, { withFileTypes: true })) {
444
+ if (!ent.isDirectory() || ent.name.startsWith(".") || ent.name === "archive") continue;
445
+ active.push({ name: ent.name, kind: "dir", children: walkFiles(path3.join(changesDir, ent.name), `openspec/changes/${ent.name}`) });
446
+ }
447
+ active.sort((a, b) => a.name.localeCompare(b.name));
448
+ const archiveDir = path3.join(changesDir, "archive");
449
+ if (fs4.existsSync(archiveDir)) {
450
+ for (const ent of fs4.readdirSync(archiveDir, { withFileTypes: true })) {
451
+ if (!ent.isDirectory() || ent.name.startsWith(".")) continue;
452
+ archived.push({ name: ent.name, kind: "dir", children: walkFiles(path3.join(archiveDir, ent.name), `openspec/changes/archive/${ent.name}`) });
453
+ }
454
+ archived.sort((a, b) => b.name.localeCompare(a.name));
455
+ }
456
+ if (active.length) tree.push({ name: `\u8FDB\u884C\u4E2D (${active.length})`, kind: "dir", children: active });
457
+ if (archived.length) tree.push({ name: "archive", kind: "dir", defaultCollapsed: true, children: archived });
458
+ const specsDir = path3.join(root, "openspec", "specs");
459
+ if (fs4.existsSync(specsDir)) {
460
+ const specs = walkFiles(specsDir, "openspec/specs");
461
+ if (specs.length) tree.push({ name: "specs", kind: "dir", children: specs });
462
+ }
463
+ }
464
+ if (hasDocs && fs4.existsSync(path3.join(root, "docs"))) {
465
+ const docs = walkFiles(path3.join(root, "docs"), "docs");
466
+ if (docs.length) tree.push({ name: "docs", kind: "dir", children: docs });
467
+ }
468
+ const skip = /* @__PURE__ */ new Set(["openspec", "docs", "node_modules", ".git", "dist", "test-server"]);
469
+ const etc = [];
470
+ try {
471
+ for (const ent of fs4.readdirSync(root, { withFileTypes: true })) {
472
+ if (ent.name.startsWith(".") || skip.has(ent.name)) continue;
473
+ const ext = path3.extname(ent.name).toLowerCase();
474
+ if (ent.isFile() && (ext === ".md" || ext === ".markdown")) etc.push({ name: ent.name, kind: "file", path: ent.name });
475
+ }
476
+ } catch (e) {
477
+ console.error("[zdashboard] scan root etc failed:", e);
478
+ }
479
+ etc.sort((a, b) => a.name.localeCompare(b.name));
480
+ if (etc.length) tree.push({ name: `\u5176\u4ED6 (${etc.length})`, kind: "dir", children: etc });
481
+ return tree;
482
+ }
483
+
484
+ // src/core/tree.ts
485
+ var apply = {
486
+ inject: ["server"],
487
+ apply(ctx, config) {
488
+ let cached = null;
489
+ const getDet = async () => {
490
+ if (cached?.v) return cached.v;
491
+ if (!cached) cached = { p: detect(config.root).catch(() => ({ hasOpenspec: false, hasDocs: false, hasJust: false, hasBugs: false })) };
492
+ const d = await cached.p;
493
+ cached.v = d;
494
+ return d;
495
+ };
496
+ const server = ctx.server;
497
+ if (server?.route) {
498
+ server.route("/__files", async (_req, res) => {
499
+ try {
500
+ const d = await getDet();
501
+ const tree = scanTree(config.root, d.hasOpenspec, d.hasDocs);
502
+ res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
503
+ res.end(JSON.stringify({ tree, ...d }));
504
+ } catch {
505
+ res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
506
+ res.end(JSON.stringify({ tree: [], hasOpenspec: false, hasDocs: false, hasJust: false, hasBugs: false }));
507
+ }
508
+ });
509
+ }
510
+ }
511
+ };
512
+
513
+ // src/core/manifest.ts
514
+ import { Service as Service3 } from "cordis";
515
+ var DashboardService = class extends Service3 {
516
+ static inject = ["server"];
517
+ plugins = /* @__PURE__ */ new Map();
518
+ constructor(ctx) {
519
+ super(ctx, "dashboard");
520
+ this.ctx.effect(() => () => this.plugins.clear());
521
+ this.ctx.server.route("/__plugins", (_req, res) => {
522
+ res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
523
+ res.end(JSON.stringify({ plugins: Array.from(this.plugins.values()) }));
524
+ });
525
+ }
526
+ register(manifest) {
527
+ this.plugins.set(manifest.mode, manifest);
528
+ this.ctx.effect(() => () => this.plugins.delete(manifest.mode));
529
+ }
530
+ list() {
531
+ return Array.from(this.plugins.values());
532
+ }
533
+ get(mode) {
534
+ return this.plugins.get(mode);
535
+ }
536
+ };
537
+
538
+ // src/server/just-runner.ts
539
+ import { spawn, execFile as execFile2 } from "child_process";
540
+ var MAX_BUFFER = 1e3;
541
+ var JustRunner = class {
542
+ cwd;
543
+ child = null;
544
+ recipe = null;
545
+ state = "idle";
546
+ code = null;
547
+ buffer = [];
548
+ pending = "";
549
+ // 行缓冲:块缓冲输出(如 maven)的 chunk 会在行中间断开,攒到 \n 才切行
550
+ clients = /* @__PURE__ */ new Set();
551
+ recipesCache = null;
552
+ constructor(cwd) {
553
+ this.cwd = cwd;
554
+ }
555
+ recipes() {
556
+ if (this.recipesCache) return Promise.resolve(this.recipesCache);
557
+ return new Promise((resolve) => {
558
+ execFile2("just", ["--list", "--unsorted"], { cwd: this.cwd, maxBuffer: 1 << 20, timeout: 8e3 }, (err, stdout) => {
559
+ if (err) {
560
+ resolve([]);
561
+ return;
562
+ }
563
+ const out = [];
564
+ const seen = /* @__PURE__ */ new Set();
565
+ for (const line of stdout.split(/\r?\n/).slice(1)) {
566
+ const trimmed = line.trim();
567
+ if (!trimmed) continue;
568
+ const hashIdx = trimmed.indexOf("#");
569
+ const sig = (hashIdx >= 0 ? trimmed.slice(0, hashIdx) : trimmed).trim();
570
+ if (!sig) continue;
571
+ const name = sig.split(/\s+/)[0];
572
+ if (seen.has(name)) continue;
573
+ seen.add(name);
574
+ out.push({ name, description: hashIdx >= 0 ? trimmed.slice(hashIdx + 1).trim() : "" });
575
+ }
576
+ this.recipesCache = out;
577
+ resolve(out);
578
+ });
579
+ });
580
+ }
581
+ subscribe(fn) {
582
+ this.clients.add(fn);
583
+ for (const text of this.buffer) fn({ type: "log", text });
584
+ fn({ type: "state", state: this.state, recipe: this.recipe, code: this.code });
585
+ return () => this.clients.delete(fn);
586
+ }
587
+ emit(ev) {
588
+ for (const fn of this.clients) fn(ev);
589
+ }
590
+ info() {
591
+ return { state: this.state, recipe: this.recipe, code: this.code };
136
592
  }
137
593
  /** 启动 recipe(调用方须先用 recipes() 校验名字);自动停旧进程 */
138
594
  start(recipe) {
@@ -169,711 +625,408 @@ var JustRunner = class {
169
625
  child.stdout?.on("data", push);
170
626
  child.stderr?.on("data", push);
171
627
  child.on("error", (err) => {
172
- this.pushLine(`[zdashboard] spawn error: ${err.message}
173
- `);
174
- });
175
- child.on("exit", (code) => {
176
- if (this.pending) {
177
- this.pushLine(this.pending + "\n");
178
- this.pending = "";
179
- }
180
- this.child = null;
181
- this.state = "exited";
182
- this.code = code ?? 0;
183
- this.emit({ type: "state", state: "exited", recipe: this.recipe, code: this.code });
184
- });
185
- }
186
- pushLine(line) {
187
- this.buffer.push(line);
188
- if (this.buffer.length > MAX_BUFFER) this.buffer.shift();
189
- this.emit({ type: "log", text: line });
190
- }
191
- stop() {
192
- this.killChild();
193
- }
194
- restart(recipe) {
195
- const target = recipe ?? this.recipe;
196
- if (target) this.start(target);
197
- }
198
- killChild() {
199
- const child = this.child;
200
- if (child?.pid) {
201
- try {
202
- if (process.platform === "win32") spawn("taskkill", ["/PID", String(child.pid), "/T", "/F"]);
203
- else child.kill("SIGTERM");
204
- } catch {
205
- }
206
- }
207
- this.child = null;
208
- }
209
- };
210
-
211
- // src/server/bugs.ts
212
- import fs2 from "fs";
213
- import path2 from "path";
214
-
215
- // src/server/api/fetch.ts
216
- import ky from "ky";
217
-
218
- // src/server/errors.ts
219
- var HttpError = class extends Error {
220
- constructor(status, message, body) {
221
- super(message);
222
- this.status = status;
223
- this.body = body;
224
- this.name = "HttpError";
225
- }
226
- status;
227
- body;
228
- };
229
- var NetworkError = class extends Error {
230
- constructor(message, cause) {
231
- super(message);
232
- this.cause = cause;
233
- this.name = "NetworkError";
234
- }
235
- cause;
236
- };
237
-
238
- // src/server/api/fetch.ts
239
- async function fetchJson(url, init) {
240
- try {
241
- const res = await ky(url, { ...init, timeout: 8e3, retry: 2 });
242
- return await res.json();
243
- } catch (e) {
244
- if (e instanceof HttpError) throw e;
245
- if (e instanceof Error && e.name === "TimeoutError") {
246
- throw new NetworkError(`\u8BF7\u6C42\u8D85\u65F6: ${url}`);
247
- }
248
- if (e instanceof Error && e.name === "HTTPError") {
249
- throw new HttpError(e.status ?? 500, e.message);
250
- }
251
- throw new NetworkError(`\u8BF7\u6C42\u5931\u8D25: ${url}`, e);
252
- }
253
- }
254
-
255
- // src/server/bugs.ts
256
- function loadZgoalConfig(root2) {
257
- const file = path2.join(root2, ".zgoal", "config.yaml");
258
- if (!fs2.existsSync(file)) return null;
259
- const kv = {};
260
- for (const line of fs2.readFileSync(file, "utf8").split("\n")) {
261
- const m = line.match(/^\s*([A-Za-z_]\w*)\s*:\s*(.+?)\s*$/);
262
- if (m && !m[2].startsWith("#")) kv[m[1]] = m[2].replace(/^["']|["']$/g, "");
263
- }
264
- const product = Number(kv.product);
265
- if (!kv.url || !product) return null;
266
- return {
267
- url: kv.url.replace(/\/+$/, ""),
268
- account: kv.account ?? "",
269
- password: kv.password,
270
- token: kv.token,
271
- product
272
- };
273
- }
274
- var tokenCache = null;
275
- async function getToken(cfg) {
276
- if (cfg.token) return cfg.token;
277
- const key = `${cfg.url}|${cfg.account}|${cfg.password ?? ""}`;
278
- if (tokenCache && tokenCache.key === key && Date.now() - tokenCache.at < 10 * 6e4) return tokenCache.token;
279
- const json = await fetchJson(`${cfg.url}/api.php/v1/tokens`, {
280
- method: "POST",
281
- headers: { "Content-Type": "application/json" },
282
- body: JSON.stringify({ account: cfg.account, password: cfg.password })
283
- });
284
- const token = typeof json.token === "string" ? json.token : "";
285
- if (!token) throw new Error("token \u83B7\u53D6\u5931\u8D25:\u68C0\u67E5 account / password");
286
- tokenCache = { key, token, at: Date.now() };
287
- return token;
288
- }
289
- function normBug(b, account) {
290
- const assigned = b.assignedTo;
291
- const assignedTo = typeof assigned === "string" ? assigned : assigned && typeof assigned === "object" && "realname" in assigned ? String(assigned.realname ?? "") : "";
292
- const assignedAccount = typeof assigned === "string" ? assigned : assigned && typeof assigned === "object" ? String(assigned.account ?? "") : "";
293
- const mine = !!account && (assignedAccount === account || assignedTo === account);
294
- return {
295
- id: Number(b.id),
296
- title: String(b.title ?? ""),
297
- severity: b.severity ?? 4,
298
- pri: b.pri ?? 3,
299
- status: String(b.status ?? ""),
300
- assignedTo,
301
- openedBy: typeof b.openedBy === "string" ? b.openedBy : void 0,
302
- mine
303
- };
304
- }
305
- async function fetchBugs(root2) {
306
- const cfg = loadZgoalConfig(root2);
307
- if (!cfg) return { ok: false, error: ".zgoal/config.yaml \u7F3A\u5931\u6216 url/product \u672A\u914D\u7F6E(\u7531 zgoal skill \u521B\u5EFA)" };
308
- try {
309
- const token = await getToken(cfg);
310
- const json = await fetchJson(
311
- `${cfg.url}/api.php/v1/products/${cfg.product}/bugs?page=1&limit=100`,
312
- { headers: { Token: token } }
313
- );
314
- const raw = Array.isArray(json.bugs) ? json.bugs : [];
315
- return { ok: true, url: cfg.url, total: Number(json.total ?? raw.length), bugs: raw.map((b) => normBug(b, cfg.account)) };
316
- } catch (e) {
317
- const msg = e instanceof Error ? e.message : String(e);
318
- 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` };
319
- }
320
- }
321
-
322
- // src/server/plugins.ts
323
- import fs3 from "fs";
324
- import path3 from "path";
325
- import { fileURLToPath } from "url";
326
- var __dirname = path3.dirname(fileURLToPath(import.meta.url));
327
- var builtinPlugins = /* @__PURE__ */ new Map();
328
- function registerBuiltin(plugin) {
329
- builtinPlugins.set(plugin.mode, plugin);
330
- }
331
- function allBuiltins() {
332
- return Array.from(builtinPlugins.values());
333
- }
334
-
335
- // src/server/review-store.ts
336
- import fs4 from "fs";
337
- import path4 from "path";
338
- import YAML from "yaml";
339
- var REVIEW_FILE = "review.yaml";
340
- var ReviewStore = class {
341
- root;
342
- file;
343
- onChange;
344
- constructor(root2, onChange) {
345
- this.root = root2;
346
- this.file = path4.join(root2, REVIEW_FILE);
347
- this.onChange = onChange;
348
- }
349
- exists() {
350
- return fs4.existsSync(this.file);
351
- }
352
- read() {
353
- try {
354
- const parsed = YAML.parse(fs4.readFileSync(this.file, "utf8"));
355
- if (!parsed || !Array.isArray(parsed.items)) return { status: "draft", items: [] };
356
- return parsed;
357
- } catch {
358
- return { status: "draft", items: [] };
359
- }
360
- }
361
- write(data) {
362
- fs4.writeFileSync(this.file, YAML.stringify(data), "utf8");
363
- this.onChange?.();
364
- }
365
- updateItem(id, patch) {
366
- const data = this.read();
367
- const item = data.items.find((i) => i.id === id);
368
- if (!item) throw new Error(`item ${id} not found`);
369
- if (patch.answer !== void 0) item.answer = patch.answer;
370
- if (patch.state !== void 0) item.state = patch.state;
371
- if (patch.state === "answered" && !patch.answer && !item.answer) item.answer = "";
372
- this.write(data);
373
- return data;
374
- }
375
- setStatus(status) {
376
- const data = this.read();
377
- if (status === "passed" && data.items.some((i) => i.state === "open")) {
378
- throw new Error("\u5B58\u5728\u672A\u5904\u7406\u7684\u8BC4\u5BA1\u9879(open),\u4E0D\u80FD\u901A\u8FC7");
379
- }
380
- data.status = status;
381
- this.write(data);
382
- return data;
383
- }
384
- docs() {
385
- try {
386
- return fs4.readdirSync(this.root).filter((f) => /\.(md|markdown)$/i.test(f) && fs4.statSync(path4.join(this.root, f)).isFile()).sort();
387
- } catch {
388
- return [];
389
- }
390
- }
391
- };
392
-
393
- // src/server/design-assets.ts
394
- import fs5 from "fs";
395
- import path5 from "path";
396
- var PAGE_EXTS = [".html", ".htm"];
397
- var ICON_EXTS = [".svg", ".png", ".ico", ".jpg", ".jpeg", ".gif", ".webp"];
398
- var VIDEO_EXTS = [".mp4", ".webm", ".mov", ".ogg", ".ogv"];
399
- var AUDIO_EXTS = [".mp3", ".wav", ".flac", ".aac", ".m4a"];
400
- var CODE_EXTS = [".js", ".mjs", ".ts", ".tsx", ".jsx", ".css", ".json", ".txt", ".xml", ".yml", ".yaml", ".sh", ".md"];
401
- var FONT_EXTS = [".woff", ".woff2", ".ttf", ".otf"];
402
- var TOKEN_RE = /token|theme|design|color|palette|typograph/i;
403
- function categorize(rel, ext) {
404
- if (rel.indexOf("components/") === 0) return "component";
405
- if (VIDEO_EXTS.includes(ext)) return "video";
406
- if (AUDIO_EXTS.includes(ext)) return "audio";
407
- if (ext === ".pdf") return "pdf";
408
- if (ext === ".md") return "md";
409
- if (FONT_EXTS.includes(ext)) return "font";
410
- if (ICON_EXTS.includes(ext)) return "icon";
411
- if (PAGE_EXTS.includes(ext)) return "page";
412
- if (CODE_EXTS.includes(ext)) return TOKEN_RE.test(rel) && (ext === ".css" || ext === ".json") ? "token" : "code";
413
- return "other";
414
- }
415
- function scanAssets(root2) {
416
- const out = {};
417
- const keys = ["page", "component", "icon", "token", "md", "video", "audio", "pdf", "code", "font", "other"];
418
- for (const k of keys) out[k] = [];
419
- const SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "coverage", ".next", ".cache"]);
420
- function walk(dir, rel) {
421
- let ents;
422
- try {
423
- ents = fs5.readdirSync(dir, { withFileTypes: true });
424
- } catch {
425
- return;
426
- }
427
- for (const ent of ents) {
428
- if (ent.name.startsWith(".") || SKIP.has(ent.name)) continue;
429
- const r = rel ? `${rel}/${ent.name}` : ent.name;
430
- if (ent.isDirectory()) {
431
- walk(path5.join(dir, ent.name), r);
432
- continue;
433
- }
434
- const ext = path5.extname(ent.name).toLowerCase();
435
- const t = categorize(r, ext);
436
- out[t].push({ path: r, name: ent.name, ext, type: t });
437
- }
438
- }
439
- walk(root2, "");
440
- return out;
441
- }
442
-
443
- // package.json
444
- var package_default = {
445
- name: "zdashboard",
446
- version: "1.6.2",
447
- description: "ZCode skill dashboard platform \u2014 pluggable viewers for zdesign/zview/zreview/zgoal",
448
- type: "module",
449
- bin: {
450
- zdashboard: "./dist/cli.js"
451
- },
452
- files: [
453
- "dist"
454
- ],
455
- publishConfig: {
456
- access: "public"
457
- },
458
- scripts: {
459
- dev: "vite",
460
- build: "tsup && vite build",
461
- "build:web": "vite build",
462
- "build:node": "tsup",
463
- start: "node dist/cli.js",
464
- preview: "vite preview"
465
- },
466
- dependencies: {
467
- "@radix-ui/react-scroll-area": "^1.2.0",
468
- "@radix-ui/react-separator": "^1.1.0",
469
- "@radix-ui/react-slot": "^1.1.0",
470
- "@radix-ui/react-tooltip": "^1.1.2",
471
- "@uidotdev/usehooks": "^2.4.0",
472
- "ansi-to-react": "^6.1.6",
473
- "class-variance-authority": "^0.7.0",
474
- clsx: "^2.1.1",
475
- "date-fns": "^3.6.0",
476
- filesize: "^11.0.0",
477
- "highlight.js": "^11.11.1",
478
- katex: "^0.16.11",
479
- ky: "^1.7.0",
480
- "lodash-es": "^4.17.21",
481
- "lucide-react": "^0.460.0",
482
- react: "^18.3.1",
483
- "react-dom": "^18.3.1",
484
- "react-error-boundary": "^5.0.0",
485
- "react-markdown": "^9.0.1",
486
- "rehype-autolink-headings": "^7.1.0",
487
- "rehype-highlight": "^7.0.1",
488
- "rehype-katex": "^7.0.1",
489
- "rehype-raw": "^7.0.0",
490
- "rehype-slug": "^6.0.0",
491
- "remark-frontmatter": "^5.0.0",
492
- "remark-gfm": "^4.0.0",
493
- "remark-math": "^6.0.0",
494
- sonner: "^1.5.0",
495
- "tailwind-merge": "^2.5.4",
496
- "use-debounce": "^10.0.0",
497
- yaml: "^2.9.0"
498
- },
499
- devDependencies: {
500
- "@tailwindcss/typography": "^0.5.20",
501
- "@testing-library/jest-dom": "^7.0.1",
502
- "@testing-library/react": "^16.3.2",
503
- "@types/lodash-es": "^4.17.12",
504
- "@types/node": "^22.9.0",
505
- "@types/react": "^18.3.12",
506
- "@types/react-dom": "^18.3.1",
507
- "@vitejs/plugin-react": "^4.7.0",
508
- autoprefixer: "^10.4.20",
509
- jsdom: "^30.0.1",
510
- postcss: "^8.4.49",
511
- tailwindcss: "^3.4.14",
512
- "tailwindcss-animate": "^1.0.7",
513
- tsup: "^8.3.5",
514
- typescript: "^5.6.3",
515
- vite: "^5.4.10",
516
- vitest: "^1.6.0"
517
- },
518
- pnpm: {
519
- onlyBuiltDependencies: [
520
- "esbuild"
521
- ]
628
+ this.pushLine(`[zdashboard] spawn error: ${err.message}
629
+ `);
630
+ });
631
+ child.on("exit", (code) => {
632
+ if (this.pending) {
633
+ this.pushLine(this.pending + "\n");
634
+ this.pending = "";
635
+ }
636
+ this.child = null;
637
+ this.state = "exited";
638
+ this.code = code ?? 0;
639
+ this.emit({ type: "state", state: "exited", recipe: this.recipe, code: this.code });
640
+ });
641
+ }
642
+ pushLine(line) {
643
+ this.buffer.push(line);
644
+ if (this.buffer.length > MAX_BUFFER) this.buffer.shift();
645
+ this.emit({ type: "log", text: line });
646
+ }
647
+ stop() {
648
+ this.killChild();
649
+ }
650
+ restart(recipe) {
651
+ const target = recipe ?? this.recipe;
652
+ if (target) this.start(target);
653
+ }
654
+ killChild() {
655
+ const child = this.child;
656
+ if (child?.pid) {
657
+ try {
658
+ if (process.platform === "win32") spawn("taskkill", ["/PID", String(child.pid), "/T", "/F"]);
659
+ else child.kill("SIGTERM");
660
+ } catch {
661
+ }
662
+ }
663
+ this.child = null;
522
664
  }
523
665
  };
524
666
 
525
- // src/server/index.ts
526
- var VERSION = package_default.version;
527
- var __dirname2 = path6.dirname(fileURLToPath2(import.meta.url));
528
- var STOP_TOKEN = crypto.randomBytes(12).toString("hex");
529
- var INJECT = `<script>(function(){try{var es=new EventSource('/__reload');es.addEventListener('reload',function(){location.reload();});es.onerror=function(){es.close();};}catch(e){}document.addEventListener('click',function(e){var t=e.target;if(t&&t.closest){var a=t.closest('a[target]');if(a&&a.target!=='_self'){a.target='_self';}}},true);})();</script>`;
530
- var MIME = {
531
- ".html": "text/html; charset=utf-8",
532
- ".htm": "text/html; charset=utf-8",
533
- ".css": "text/css; charset=utf-8",
534
- ".js": "application/javascript; charset=utf-8",
535
- ".mjs": "application/javascript; charset=utf-8",
536
- ".json": "application/json; charset=utf-8",
537
- ".svg": "image/svg+xml",
538
- ".png": "image/png",
539
- ".ico": "image/x-icon",
540
- ".jpg": "image/jpeg",
541
- ".jpeg": "image/jpeg",
542
- ".gif": "image/gif",
543
- ".webp": "image/webp",
544
- ".md": "text/markdown; charset=utf-8",
545
- ".txt": "text/plain; charset=utf-8",
546
- ".sql": "text/plain; charset=utf-8",
547
- ".csv": "text/plain; charset=utf-8",
548
- ".tsv": "text/plain; charset=utf-8",
549
- ".yaml": "text/yaml; charset=utf-8",
550
- ".yml": "text/yaml; charset=utf-8",
551
- ".xml": "application/xml; charset=utf-8",
552
- ".py": "text/plain; charset=utf-8",
553
- ".ts": "text/plain; charset=utf-8",
554
- ".java": "text/plain; charset=utf-8",
555
- ".go": "text/plain; charset=utf-8",
556
- ".rs": "text/plain; charset=utf-8",
557
- ".rb": "text/plain; charset=utf-8",
558
- ".php": "text/plain; charset=utf-8",
559
- ".c": "text/plain; charset=utf-8",
560
- ".cpp": "text/plain; charset=utf-8",
561
- ".h": "text/plain; charset=utf-8",
562
- ".cs": "text/plain; charset=utf-8",
563
- ".swift": "text/plain; charset=utf-8",
564
- ".kt": "text/plain; charset=utf-8",
565
- ".scala": "text/plain; charset=utf-8",
566
- ".sh": "text/plain; charset=utf-8",
567
- ".bash": "text/plain; charset=utf-8",
568
- ".zsh": "text/plain; charset=utf-8",
569
- ".fish": "text/plain; charset=utf-8",
570
- ".env": "text/plain; charset=utf-8",
571
- ".gitignore": "text/plain; charset=utf-8",
572
- ".dockerfile": "text/plain; charset=utf-8",
573
- ".woff": "font/woff",
574
- ".woff2": "font/woff2",
575
- ".ttf": "font/woff",
576
- ".map": "application/json; charset=utf-8"
577
- };
578
- function readBody(req) {
579
- return new Promise((resolve) => {
580
- let data = "";
581
- req.on("data", (c) => data += c);
582
- req.on("end", () => resolve(data));
583
- });
584
- }
585
- function createServer(opts) {
586
- const ROOT = path6.resolve(opts.root);
587
- const PORT0 = opts.port ?? 4190;
588
- const OPEN = !!opts.open;
589
- const APP_DIR = opts.dashboardDir ?? path6.resolve(__dirname2, "web");
590
- if (!fs6.existsSync(ROOT)) fs6.mkdirSync(ROOT, { recursive: true });
591
- const det2 = opts.detect;
592
- const runner = new JustRunner(ROOT);
593
- const MODE = opts.mode;
594
- registerBuiltin({
595
- mode: "bugs",
596
- label: "\u7985\u9053 Bugs",
597
- icon: "\u{1F3AF}",
598
- apiRoutes: { "/__bugs": async (_, res) => {
599
- res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
600
- fetchBugs(ROOT).then((r) => res.end(JSON.stringify(r)));
601
- } }
602
- });
603
- registerBuiltin({ mode: "view", label: "\u9879\u76EE\u6D4F\u89C8", icon: "\u{1F441}\uFE0F" });
604
- const reviewStore = new ReviewStore(ROOT);
605
- registerBuiltin({
606
- mode: "review",
607
- label: "\u6587\u6863\u8BC4\u5BA1",
608
- icon: "\u2705",
609
- apiRoutes: {
610
- "/__review": async (_, res) => {
667
+ // src/plugins/just/index.ts
668
+ var apply2 = {
669
+ inject: ["server", "dashboard"],
670
+ apply(ctx, config) {
671
+ const root = config.root;
672
+ ctx.inject(["server"], () => {
673
+ if (!ctx.server?.route) return;
674
+ const runner = new JustRunner(root);
675
+ ctx.effect(() => () => runner.stop());
676
+ ctx.server.route("/__just/recipes", async (_req, res) => {
611
677
  res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
612
- res.end(JSON.stringify(reviewStore.read()));
613
- },
614
- "/__review/item": async (req, res) => {
615
- if (req.headers["x-stop-token"] !== STOP_TOKEN) {
678
+ try {
679
+ const recipes = await runner.recipes();
680
+ res.end(JSON.stringify(recipes));
681
+ } catch {
682
+ res.end(JSON.stringify([]));
683
+ }
684
+ });
685
+ ctx.server.sse("/__just/logs", (res) => {
686
+ const unsub = runner.subscribe((ev) => {
687
+ res.write(`data: ${JSON.stringify(ev)}
688
+
689
+ `);
690
+ });
691
+ return unsub;
692
+ });
693
+ ctx.server.route("/__just/start", async (req, res) => {
694
+ if (req.headers["x-stop-token"] !== ctx.server.stopToken) {
616
695
  res.writeHead(403);
617
696
  res.end("forbidden");
618
697
  return;
619
698
  }
620
- (async () => {
621
- try {
622
- const body = JSON.parse(await readBody(req) || "{}");
623
- const data = reviewStore.updateItem(body.id, { answer: body.answer, state: body.state });
624
- res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
625
- res.end(JSON.stringify(data));
626
- } catch (e) {
627
- res.writeHead(400, { "Content-Type": "application/json; charset=utf-8" });
628
- res.end(JSON.stringify({ error: e.message }));
629
- }
630
- })();
631
- return;
632
- },
633
- "/__review/status": async (req, res) => {
634
- if (req.headers["x-stop-token"] !== STOP_TOKEN) {
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) {
635
723
  res.writeHead(403);
636
724
  res.end("forbidden");
637
725
  return;
638
726
  }
639
- (async () => {
640
- try {
641
- const body = JSON.parse(await readBody(req) || "{}");
642
- const data = reviewStore.setStatus(body.status);
643
- res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
644
- res.end(JSON.stringify(data));
645
- } catch (e) {
646
- res.writeHead(400, { "Content-Type": "application/json; charset=utf-8" });
647
- res.end(JSON.stringify({ error: e.message }));
648
- }
649
- })();
650
- return;
651
- },
652
- "/__docs": async (_, res) => {
653
- res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
654
- res.end(JSON.stringify(reviewStore.docs()));
655
- }
656
- }
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) => {
732
+ if (req.headers["x-stop-token"] !== ctx.server.stopToken) {
733
+ res.writeHead(403);
734
+ res.end("forbidden");
735
+ return;
736
+ }
737
+ const body = await readBody(req);
738
+ let recipe;
739
+ try {
740
+ recipe = JSON.parse(body || "{}").recipe;
741
+ } catch {
742
+ }
743
+ const target = recipe ?? runner.info().recipe;
744
+ if (!target) {
745
+ res.writeHead(400);
746
+ res.end('{"error":"no recipe"}');
747
+ return;
748
+ }
749
+ const recipes = await runner.recipes();
750
+ if (!recipes.some((r) => r.name === target)) {
751
+ res.writeHead(403);
752
+ res.end('{"error":"unknown recipe"}');
753
+ return;
754
+ }
755
+ runner.restart(target);
756
+ res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
757
+ res.end(JSON.stringify(runner.info()));
758
+ });
759
+ ctx.dashboard.register({ mode: "just", label: "Just Runner", icon: "\u{1F4DC}", description: "Just \u4EFB\u52A1\u65E5\u5FD7\u4E0E\u6267\u884C" });
760
+ });
761
+ }
762
+ };
763
+ async function readBody(req) {
764
+ return new Promise((resolve) => {
765
+ let data = "";
766
+ req.on("data", (c) => {
767
+ data += c;
768
+ });
769
+ req.on("end", () => resolve(data));
657
770
  });
658
- registerBuiltin({ mode: "design", label: "\u8BBE\u8BA1\u8D44\u4EA7", icon: "\u{1F3A8}" });
659
- registerBuiltin({
660
- mode: "apply",
661
- label: "\u6267\u884C\u8FDB\u5EA6",
662
- icon: "\u2699\uFE0F",
663
- apiRoutes: {
664
- "/__apply": async (_, res) => {
665
- res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
666
- res.end(JSON.stringify(scanApplyChanges(ROOT)));
667
- },
668
- "/__apply/change": async (req, res) => {
669
- (async () => {
670
- try {
671
- const url = new URL(req.url || "", "http://x");
672
- const name = url.searchParams.get("name");
673
- if (!name) {
674
- res.writeHead(400);
675
- res.end(JSON.stringify({ error: "missing name" }));
676
- return;
677
- }
678
- const data = readApplyChange(ROOT, name);
679
- res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
680
- res.end(JSON.stringify(data));
681
- } catch (e) {
682
- res.writeHead(400);
683
- res.end(JSON.stringify({ error: e.message }));
684
- }
685
- })();
686
- return;
687
- }
771
+ }
772
+
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);
688
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 })
689
845
  });
690
- const clients = /* @__PURE__ */ new Set();
691
- const broadcast = (ev, data = "") => {
692
- const payload = `event: ${ev}
693
- data: ${JSON.stringify(data == null ? "" : data)}
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
+ }
694
883
 
695
- `;
696
- for (const c of clients) c.write(payload);
697
- };
698
- function serveFile(filePath, res, injectHtml) {
699
- fs6.readFile(filePath, (err, data) => {
700
- if (err) {
701
- res.writeHead(404);
702
- return res.end("Not found");
703
- }
704
- const ext = path6.extname(filePath).toLowerCase();
705
- const ct = MIME[ext] ?? "application/octet-stream";
706
- let body = data;
707
- if (injectHtml && ext === ".html") {
708
- const s = data.toString("utf8");
709
- body = Buffer.from(s.indexOf("</body>") >= 0 ? s.replace("</body>", INJECT + "</body>") : s + INJECT);
710
- }
711
- res.writeHead(200, { "Content-Type": ct, "Cache-Control": "no-cache" });
712
- res.end(body);
884
+ // src/plugins/bugs/index.ts
885
+ var apply3 = {
886
+ inject: ["server", "dashboard"],
887
+ apply(ctx, config) {
888
+ const root = config.root;
889
+ ctx.inject(["server"], () => {
890
+ if (!ctx.server?.route) return;
891
+ ctx.dashboard.register({ mode: "bugs", label: "\u7985\u9053 Bugs", icon: "\u{1F3AF}", description: "\u53EA\u8BFB bug \u5217\u8868" });
892
+ ctx.server.route("/__bugs", async (_req, res) => {
893
+ res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
894
+ try {
895
+ const result = await fetchBugs(root);
896
+ res.end(JSON.stringify(result));
897
+ } catch (e) {
898
+ res.end(JSON.stringify({ ok: false, error: e instanceof Error ? e.message : "unknown error" }));
899
+ }
900
+ });
713
901
  });
714
902
  }
715
- function handler(req, res) {
716
- const url = req.url.split("?")[0];
717
- if (url === "/__reload") {
718
- res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" });
719
- res.write(": connected\n\n");
720
- clients.add(res);
721
- req.on("close", () => clients.delete(res));
722
- return;
723
- }
724
- if (url === "/__config") {
725
- res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
726
- return res.end(JSON.stringify({ stopToken: STOP_TOKEN, mode: MODE ?? null }));
727
- }
728
- if (url === "/__stop" && req.method === "POST") {
729
- if (req.headers["x-stop-token"] === STOP_TOKEN) {
730
- res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
731
- res.end('{"ok":true}');
732
- runner.stop();
733
- setTimeout(() => {
734
- try {
735
- server.close();
736
- } catch (e) {
737
- console.error("[zdashboard] server close failed:", e);
738
- }
739
- process.exit(0);
740
- }, 50);
741
- } else {
742
- res.writeHead(403);
743
- res.end("forbidden");
744
- }
745
- return;
903
+ };
904
+
905
+ // src/server/review-store.ts
906
+ import fs6 from "fs";
907
+ import path5 from "path";
908
+ import YAML from "yaml";
909
+ var REVIEW_FILE = "review.yaml";
910
+ var ReviewStore = class {
911
+ root;
912
+ file;
913
+ onChange;
914
+ constructor(root, onChange) {
915
+ this.root = root;
916
+ this.file = path5.join(root, REVIEW_FILE);
917
+ this.onChange = onChange;
918
+ }
919
+ exists() {
920
+ return fs6.existsSync(this.file);
921
+ }
922
+ read() {
923
+ try {
924
+ const parsed = YAML.parse(fs6.readFileSync(this.file, "utf8"));
925
+ if (!parsed || !Array.isArray(parsed.items)) return { status: "draft", items: [] };
926
+ return parsed;
927
+ } catch {
928
+ return { status: "draft", items: [] };
746
929
  }
747
- if (url === "/__files") {
748
- if (MODE === "design") {
749
- res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
750
- return res.end(JSON.stringify(scanAssets(ROOT)));
751
- }
752
- const tree = scanTree(ROOT, det2.hasOpenspec, det2.hasDocs);
753
- const payload = { tree, ...det2 };
754
- res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
755
- return res.end(JSON.stringify(payload));
930
+ }
931
+ write(data) {
932
+ fs6.writeFileSync(this.file, YAML.stringify(data), "utf8");
933
+ this.onChange?.();
934
+ }
935
+ updateItem(id, patch) {
936
+ const data = this.read();
937
+ const item = data.items.find((i) => i.id === id);
938
+ if (!item) throw new Error(`item ${id} not found`);
939
+ if (patch.answer !== void 0) item.answer = patch.answer;
940
+ if (patch.state !== void 0) item.state = patch.state;
941
+ if (patch.state === "answered" && !patch.answer && !item.answer) item.answer = "";
942
+ this.write(data);
943
+ return data;
944
+ }
945
+ setStatus(status) {
946
+ const data = this.read();
947
+ if (status === "passed" && data.items.some((i) => i.state === "open")) {
948
+ throw new Error("\u5B58\u5728\u672A\u5904\u7406\u7684\u8BC4\u5BA1\u9879(open),\u4E0D\u80FD\u901A\u8FC7");
756
949
  }
757
- if (url === "/__just/recipes") {
758
- res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
759
- runner.recipes().then((r) => res.end(JSON.stringify(r)));
760
- return;
950
+ data.status = status;
951
+ this.write(data);
952
+ return data;
953
+ }
954
+ docs() {
955
+ try {
956
+ return fs6.readdirSync(this.root).filter((f) => /\.(md|markdown)$/i.test(f) && fs6.statSync(path5.join(this.root, f)).isFile()).sort();
957
+ } catch {
958
+ return [];
761
959
  }
762
- if (url === "/__just/logs") {
763
- res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" });
764
- res.write(": connected\n\n");
765
- const unsub = runner.subscribe((ev) => res.write(`data: ${JSON.stringify(ev)}
960
+ }
961
+ };
766
962
 
767
- `));
768
- req.on("close", unsub);
769
- return;
770
- }
771
- const justAction = url.match(/^\/__just\/(start|stop|restart)$/);
772
- if (justAction && req.method === "POST") {
773
- (async () => {
774
- if (req.headers["x-stop-token"] !== STOP_TOKEN) {
963
+ // src/plugins/review/index.ts
964
+ var apply4 = {
965
+ inject: ["server", "dashboard", "reload"],
966
+ apply(ctx, config) {
967
+ const root = config.root;
968
+ ctx.inject(["server"], () => {
969
+ if (!ctx.server?.route) return;
970
+ ctx.dashboard.register({ mode: "review", label: "\u6587\u6863\u8BC4\u5BA1", icon: "\u2705", description: "review.yaml \u9010\u9879\u5BF9\u9F50" });
971
+ const reviewStore = new ReviewStore(root, () => {
972
+ ctx.reload.broadcast("files");
973
+ });
974
+ ctx.server.route("/__review", async (_req, res) => {
975
+ res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
976
+ res.end(JSON.stringify(reviewStore.read()));
977
+ });
978
+ ctx.server.route("/__review/item", async (req, res) => {
979
+ if (req.headers["x-stop-token"] !== ctx.server.stopToken) {
775
980
  res.writeHead(403);
776
981
  res.end("forbidden");
777
982
  return;
778
983
  }
779
- const body = await readBody(req);
780
- let recipe;
781
984
  try {
782
- recipe = JSON.parse(body || "{}").recipe;
985
+ const body = JSON.parse(await readBody2(req) || "{}");
986
+ const data = reviewStore.updateItem(body.id, { answer: body.answer, state: body.state });
987
+ res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
988
+ res.end(JSON.stringify(data));
783
989
  } catch (e) {
784
- console.error("[zdashboard] invalid just action body:", e);
785
- }
786
- const act = justAction[1];
787
- if (act === "start" || act === "restart") {
788
- const target = recipe ?? runner.info().recipe;
789
- if (!target) {
790
- res.writeHead(400);
791
- res.end('{"error":"no recipe"}');
792
- return;
793
- }
794
- const recipes = await runner.recipes();
795
- if (!recipes.some((r) => r.name === target)) {
796
- res.writeHead(403);
797
- res.end('{"error":"unknown recipe"}');
798
- return;
799
- }
800
- runner.start(target);
801
- } else {
802
- runner.stop();
990
+ res.writeHead(400, { "Content-Type": "application/json; charset=utf-8" });
991
+ res.end(JSON.stringify({ error: e instanceof Error ? e.message : "unknown error" }));
803
992
  }
804
- res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
805
- res.end(JSON.stringify(runner.info()));
806
- })();
807
- return;
808
- }
809
- let handled = false;
810
- for (const plugin of allBuiltins()) {
811
- if (!plugin.apiRoutes) continue;
812
- for (const [route, handler2] of Object.entries(plugin.apiRoutes)) {
813
- if (url === route) {
814
- handled = true;
815
- handler2(req, res, ROOT);
993
+ });
994
+ ctx.server.route("/__review/status", async (req, res) => {
995
+ if (req.headers["x-stop-token"] !== ctx.server.stopToken) {
996
+ res.writeHead(403);
997
+ res.end("forbidden");
816
998
  return;
817
999
  }
818
- }
819
- }
820
- if (url === "/") return serveFile(path6.join(APP_DIR, "index.html"), res, false);
821
- if (url.indexOf("/__app/") === 0) {
822
- const fp2 = path6.join(APP_DIR, url.slice(7));
823
- if (fp2 !== APP_DIR && fp2.indexOf(APP_DIR + path6.sep) !== 0) {
824
- res.writeHead(403);
825
- return res.end("Forbidden");
826
- }
827
- return serveFile(fp2, res, false);
828
- }
829
- if (url.indexOf("/assets/") === 0) {
830
- const fp2 = path6.join(APP_DIR, decodeURIComponent(url));
831
- if (fp2.indexOf(APP_DIR + path6.sep) !== 0) {
832
- res.writeHead(403);
833
- return res.end("Forbidden");
834
- }
835
- return serveFile(fp2, res, false);
836
- }
837
- const fp = path6.join(ROOT, decodeURIComponent(url));
838
- if (fp !== ROOT && fp.indexOf(ROOT + path6.sep) !== 0) {
839
- res.writeHead(403);
840
- return res.end("Forbidden");
841
- }
842
- return serveFile(fp, res, true);
843
- }
844
- let server;
845
- function start(port) {
846
- server = http.createServer(handler);
847
- server.on("error", (err) => {
848
- if (err.code === "EADDRINUSE") {
849
- console.log(`[zdashboard] port ${port} busy, trying ${port + 1}`);
850
- start(port + 1);
851
- } else throw err;
852
- });
853
- server.listen(port, () => {
854
- const u = `http://localhost:${port}`;
855
- console.log(`[zdashboard] v${VERSION} dashboard -> ${u}`);
856
- console.log(`[zdashboard] project -> ${ROOT}`);
857
- console.log(`[zdashboard] mode -> ${MODE ?? "(auto)"}`);
858
- console.log(`[zdashboard] detect -> openspec:${det2.hasOpenspec} docs:${det2.hasDocs} just:${det2.hasJust} bugs:${det2.hasBugs}`);
859
- if (OPEN) exec(process.platform === "darwin" ? `open ${u}` : `start ${u}`);
1000
+ try {
1001
+ const body = JSON.parse(await readBody2(req) || "{}");
1002
+ const data = reviewStore.setStatus(body.status);
1003
+ res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
1004
+ res.end(JSON.stringify(data));
1005
+ } catch (e) {
1006
+ res.writeHead(400, { "Content-Type": "application/json; charset=utf-8" });
1007
+ res.end(JSON.stringify({ error: e instanceof Error ? e.message : "unknown error" }));
1008
+ }
1009
+ });
1010
+ ctx.server.route("/__docs", async (_req, res) => {
1011
+ res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
1012
+ res.end(JSON.stringify(reviewStore.docs()));
1013
+ });
860
1014
  });
861
1015
  }
862
- let debounce;
863
- try {
864
- fs6.watch(ROOT, { recursive: true }, () => {
865
- clearTimeout(debounce);
866
- debounce = setTimeout(() => {
867
- broadcast("reload");
868
- broadcast("files");
869
- console.log(`[zdashboard] change -> reload + refresh tree (${clients.size} client${clients.size === 1 ? "" : "s"})`);
870
- }, 150);
1016
+ };
1017
+ async function readBody2(req) {
1018
+ return new Promise((resolve) => {
1019
+ let data = "";
1020
+ req.on("data", (c) => {
1021
+ data += c;
871
1022
  });
872
- } catch {
873
- console.log("[zdashboard] watch unavailable - static only.");
874
- }
875
- start(PORT0);
1023
+ req.on("end", () => resolve(data));
1024
+ });
876
1025
  }
1026
+
1027
+ // src/plugins/apply/scan.ts
1028
+ import fs7 from "fs";
1029
+ import path6 from "path";
877
1030
  function countTasks(md) {
878
1031
  const all = (md.match(/^\s*-\s*\[[ xX]\]\s*/gm) || []).length;
879
1032
  const done = (md.match(/^\s*-\s*\[[xX]\]\s*/gm) || []).length;
@@ -881,73 +1034,277 @@ function countTasks(md) {
881
1034
  }
882
1035
  function readText(p) {
883
1036
  try {
884
- return fs6.readFileSync(p, "utf8");
1037
+ return fs7.readFileSync(p, "utf8");
885
1038
  } catch {
886
1039
  return "";
887
1040
  }
888
1041
  }
889
- function scanApplyChanges(root2) {
890
- const changesDir = path6.join(root2, "openspec", "changes");
891
- if (!fs6.existsSync(changesDir)) return [];
1042
+ function scanApplyChanges(root) {
1043
+ const changesDir = path6.join(root, "openspec", "changes");
1044
+ if (!fs7.existsSync(changesDir)) return [];
892
1045
  const out = [];
893
- for (const ent of fs6.readdirSync(changesDir, { withFileTypes: true })) {
1046
+ for (const ent of fs7.readdirSync(changesDir, { withFileTypes: true })) {
894
1047
  if (!ent.isDirectory() || ent.name.startsWith(".") || ent.name === "archive") continue;
895
1048
  const dir = path6.join(changesDir, ent.name);
896
1049
  const tasks = readText(path6.join(dir, "tasks.md"));
897
1050
  const { total, done } = countTasks(tasks);
898
- out.push({ name: ent.name, path: `openspec/changes/${ent.name}`, total, done, hasProposal: fs6.existsSync(path6.join(dir, "proposal.md")), hasDesign: fs6.existsSync(path6.join(dir, "design.md")) });
1051
+ out.push({
1052
+ name: ent.name,
1053
+ path: `openspec/changes/${ent.name}`,
1054
+ total,
1055
+ done,
1056
+ hasProposal: fs7.existsSync(path6.join(dir, "proposal.md")),
1057
+ hasDesign: fs7.existsSync(path6.join(dir, "design.md"))
1058
+ });
899
1059
  }
900
1060
  out.sort((a, b) => a.name.localeCompare(b.name));
901
1061
  return out;
902
1062
  }
903
- function readApplyChange(root2, name) {
904
- const dir = path6.join(root2, "openspec", "changes", name);
1063
+ function readApplyChange(root, name) {
1064
+ const dir = path6.join(root, "openspec", "changes", name);
905
1065
  const proposal = readText(path6.join(dir, "proposal.md"));
906
1066
  const design = readText(path6.join(dir, "design.md"));
907
1067
  const tasks = readText(path6.join(dir, "tasks.md"));
908
1068
  const { total, done } = countTasks(tasks);
909
- return { name, path: `openspec/changes/${name}`, total, done, hasProposal: !!proposal, hasDesign: !!design, proposal, design, tasks };
1069
+ return {
1070
+ name,
1071
+ path: `openspec/changes/${name}`,
1072
+ total,
1073
+ done,
1074
+ hasProposal: !!proposal,
1075
+ hasDesign: !!design,
1076
+ proposal,
1077
+ design,
1078
+ tasks
1079
+ };
910
1080
  }
911
1081
 
912
- // src/server/detect.ts
913
- import fs7 from "fs";
914
- import path7 from "path";
915
- import { execFile as execFile2 } from "child_process";
916
- function justAvailable(cwd) {
917
- return new Promise((resolve) => {
918
- const child = execFile2("just", ["--list", "--unsorted"], { cwd, timeout: 5e3 }, (err) => {
919
- resolve(!err);
1082
+ // src/plugins/apply/index.ts
1083
+ var apply5 = {
1084
+ inject: ["server", "dashboard"],
1085
+ apply(ctx, config) {
1086
+ const root = config.root;
1087
+ ctx.inject(["server"], () => {
1088
+ if (!ctx.server?.route) return;
1089
+ ctx.dashboard.register({ mode: "apply", label: "\u6267\u884C\u8FDB\u5EA6", icon: "\u2699\uFE0F", description: "OpenSpec change \u4EFB\u52A1\u8FDB\u5EA6" });
1090
+ ctx.server.route("/__apply", async (_req, res) => {
1091
+ res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
1092
+ res.end(JSON.stringify(scanApplyChanges(root)));
1093
+ });
1094
+ ctx.server.route("/__apply/change", async (req, res) => {
1095
+ const url = new URL(req.url || "", "http://x");
1096
+ const name = url.searchParams.get("name");
1097
+ if (!name) {
1098
+ res.writeHead(400);
1099
+ res.end(JSON.stringify({ error: "missing name" }));
1100
+ return;
1101
+ }
1102
+ if (name.includes("..") || name.includes("/") || name.includes("\\")) {
1103
+ res.writeHead(400);
1104
+ res.end(JSON.stringify({ error: "invalid name" }));
1105
+ return;
1106
+ }
1107
+ try {
1108
+ const data = readApplyChange(root, name);
1109
+ res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
1110
+ res.end(JSON.stringify(data));
1111
+ } catch (e) {
1112
+ res.writeHead(400);
1113
+ res.end(JSON.stringify({ error: e instanceof Error ? e.message : "unknown error" }));
1114
+ }
1115
+ });
920
1116
  });
921
- if (child.killed) resolve(false);
922
- });
1117
+ }
1118
+ };
1119
+
1120
+ // src/server/design-assets.ts
1121
+ import fs8 from "fs";
1122
+ import path7 from "path";
1123
+ var PAGE_EXTS = [".html", ".htm"];
1124
+ var ICON_EXTS = [".svg", ".png", ".ico", ".jpg", ".jpeg", ".gif", ".webp"];
1125
+ var VIDEO_EXTS = [".mp4", ".webm", ".mov", ".ogg", ".ogv"];
1126
+ var AUDIO_EXTS = [".mp3", ".wav", ".flac", ".aac", ".m4a"];
1127
+ var CODE_EXTS = [".js", ".mjs", ".ts", ".tsx", ".jsx", ".css", ".json", ".txt", ".xml", ".yml", ".yaml", ".sh", ".md"];
1128
+ var FONT_EXTS = [".woff", ".woff2", ".ttf", ".otf"];
1129
+ var TOKEN_RE = /token|theme|design|color|palette|typograph/i;
1130
+ function categorize(rel, ext) {
1131
+ if (rel.indexOf("components/") === 0) return "component";
1132
+ if (VIDEO_EXTS.includes(ext)) return "video";
1133
+ if (AUDIO_EXTS.includes(ext)) return "audio";
1134
+ if (ext === ".pdf") return "pdf";
1135
+ if (ext === ".md") return "md";
1136
+ if (FONT_EXTS.includes(ext)) return "font";
1137
+ if (ICON_EXTS.includes(ext)) return "icon";
1138
+ if (PAGE_EXTS.includes(ext)) return "page";
1139
+ if (CODE_EXTS.includes(ext)) return TOKEN_RE.test(rel) && (ext === ".css" || ext === ".json") ? "token" : "code";
1140
+ return "other";
923
1141
  }
924
- async function detect(root2) {
925
- const hasOpenspec = fs7.existsSync(path7.join(root2, "openspec"));
926
- const hasDocs = fs7.existsSync(path7.join(root2, "docs"));
927
- const hasJust = await justAvailable(root2);
928
- const hasBugs = fs7.existsSync(path7.join(root2, ".zgoal", "config.yaml"));
929
- return { hasOpenspec, hasDocs, hasJust, hasBugs };
1142
+ function scanAssets(root) {
1143
+ const out = {};
1144
+ const keys = ["page", "component", "icon", "token", "md", "video", "audio", "pdf", "code", "font", "other"];
1145
+ for (const k of keys) out[k] = [];
1146
+ const SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "coverage", ".next", ".cache"]);
1147
+ function walk(dir, rel) {
1148
+ let ents;
1149
+ try {
1150
+ ents = fs8.readdirSync(dir, { withFileTypes: true });
1151
+ } catch {
1152
+ return;
1153
+ }
1154
+ for (const ent of ents) {
1155
+ if (ent.name.startsWith(".") || SKIP.has(ent.name)) continue;
1156
+ const r = rel ? `${rel}/${ent.name}` : ent.name;
1157
+ if (ent.isDirectory()) {
1158
+ walk(path7.join(dir, ent.name), r);
1159
+ continue;
1160
+ }
1161
+ const ext = path7.extname(ent.name).toLowerCase();
1162
+ const t = categorize(r, ext);
1163
+ out[t].push({ path: r, name: ent.name, ext, type: t });
1164
+ }
1165
+ }
1166
+ walk(root, "");
1167
+ return out;
930
1168
  }
931
1169
 
1170
+ // src/plugins/design/index.ts
1171
+ var apply6 = {
1172
+ inject: ["server", "dashboard"],
1173
+ apply(ctx, config) {
1174
+ const root = config.root;
1175
+ ctx.inject(["server"], () => {
1176
+ if (!ctx.server?.route) return;
1177
+ ctx.dashboard.register({ mode: "design", label: "\u8BBE\u8BA1\u8D44\u4EA7", icon: "\u{1F3A8}", description: "\u9875\u9762/\u7EC4\u4EF6/\u56FE\u6807/Token \u5206\u7C7B\u9884\u89C8" });
1178
+ ctx.server.route("/__design/assets", async (_req, res) => {
1179
+ res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
1180
+ res.end(JSON.stringify(scanAssets(root)));
1181
+ });
1182
+ });
1183
+ }
1184
+ };
1185
+
1186
+ // src/plugins/view/index.ts
1187
+ var apply7 = {
1188
+ inject: ["dashboard"],
1189
+ apply(ctx) {
1190
+ ctx.dashboard.register({ mode: "view", label: "\u9879\u76EE\u6D4F\u89C8", icon: "\u{1F441}\uFE0F", description: "openspec / docs / \u6587\u6863\u9884\u89C8" });
1191
+ }
1192
+ };
1193
+
932
1194
  // src/cli.ts
933
- function parseArgs(a) {
934
- const o = {};
935
- for (let i = 0; i < a.length; i++) {
936
- if (a[i].indexOf("--") === 0) {
937
- const n = a[i + 1];
938
- o[a[i].slice(2)] = n && n.indexOf("--") !== 0 ? a[++i] : true;
1195
+ var __dirname2 = path8.dirname(fileURLToPath2(import.meta.url));
1196
+ function parseArgs() {
1197
+ const args = process.argv.slice(2);
1198
+ const opts = {};
1199
+ for (let i = 0; i < args.length; i++) {
1200
+ const a = args[i];
1201
+ if (a.startsWith("--")) {
1202
+ const key = a.slice(2);
1203
+ const next = args[i + 1];
1204
+ if (next && !next.startsWith("--")) {
1205
+ opts[key] = next;
1206
+ i++;
1207
+ } else {
1208
+ opts[key] = true;
1209
+ }
1210
+ }
1211
+ }
1212
+ return {
1213
+ dir: typeof opts.dir === "string" ? opts.dir : process.cwd(),
1214
+ port: typeof opts.port === "string" ? Number(opts.port) : 4190,
1215
+ open: !!opts.open,
1216
+ page: typeof opts.page === "string" ? opts.page : null,
1217
+ plugins: typeof opts.plugins === "string" ? opts.plugins : null
1218
+ };
1219
+ }
1220
+ async function pathExists(p) {
1221
+ try {
1222
+ await access(p);
1223
+ return true;
1224
+ } catch {
1225
+ return false;
1226
+ }
1227
+ }
1228
+ async function loadExternal(ctx, dir, root) {
1229
+ if (!dir) return;
1230
+ let tsxLoaded = false;
1231
+ try {
1232
+ const { register } = await import("tsx/esm/api");
1233
+ register();
1234
+ tsxLoaded = true;
1235
+ } catch {
1236
+ }
1237
+ try {
1238
+ const entries = await pathExists(dir) ? await readdir(dir, { withFileTypes: true }) : [];
1239
+ for (const ent of entries) {
1240
+ if (!ent.isDirectory()) continue;
1241
+ const candidates = tsxLoaded ? ["index.ts", "index.js", "index.mjs"] : ["index.js", "index.mjs"];
1242
+ for (const name of candidates) {
1243
+ const p = path8.join(dir, ent.name, name);
1244
+ if (await pathExists(p)) {
1245
+ try {
1246
+ const mod = await import(p);
1247
+ const plugin = mod.default ?? mod;
1248
+ if (plugin?.apply) {
1249
+ try {
1250
+ await ctx.plugin(plugin, { root });
1251
+ } catch (e) {
1252
+ console.error(`[zdashboard] failed to apply plugin ${ent.name}:`, e);
1253
+ break;
1254
+ }
1255
+ const m = ctx.dashboard.get(ent.name);
1256
+ if (m && m.mode === ent.name) {
1257
+ const patch = { external: true };
1258
+ const webDir = path8.join(dir, ent.name, "web");
1259
+ if (await pathExists(path8.join(webDir, "index.html"))) {
1260
+ ctx.server.static(`${PLUGIN_STATIC_PREFIX}${ent.name}/`, webDir);
1261
+ if (!m.viewerUrl) patch.viewerUrl = `${PLUGIN_STATIC_PREFIX}${ent.name}/`;
1262
+ }
1263
+ ctx.dashboard.register({ ...m, ...patch });
1264
+ }
1265
+ }
1266
+ } catch (e) {
1267
+ console.error(`[zdashboard] failed to load plugin ${ent.name}:`, e);
1268
+ }
1269
+ break;
1270
+ }
1271
+ }
1272
+ }
1273
+ } catch {
1274
+ }
1275
+ }
1276
+ async function main() {
1277
+ const args = parseArgs();
1278
+ const root = path8.resolve(args.dir);
1279
+ const appDir = path8.resolve(__dirname2, "web");
1280
+ const det = await detect(root);
1281
+ const ctx = new Context();
1282
+ ctx.plugin(ServerService, { root, appDir, port: args.port, open: args.open, detect: det, page: args.page });
1283
+ ctx.plugin(ReloadService, { root });
1284
+ ctx.plugin(apply, { root });
1285
+ ctx.plugin(DashboardService);
1286
+ const plugins = [
1287
+ { name: "just", apply: apply2 },
1288
+ { name: "bugs", apply: apply3 },
1289
+ { name: "review", apply: apply4 },
1290
+ { name: "apply", apply: apply5 },
1291
+ { name: "design", apply: apply6 },
1292
+ { name: "view", apply: apply7 }
1293
+ ];
1294
+ for (const p of plugins) {
1295
+ try {
1296
+ ctx.plugin(p.apply, { root });
1297
+ } catch (e) {
1298
+ console.error(`[zdashboard] plugin ${p.name} failed:`, e);
939
1299
  }
940
1300
  }
941
- return o;
1301
+ if (args.plugins) {
1302
+ await loadExternal(ctx, path8.resolve(args.plugins), root);
1303
+ }
1304
+ return ctx;
942
1305
  }
943
- var args = parseArgs(process.argv.slice(2));
944
- var root = args.dir ?? ".";
945
- var det = await detect(root);
946
- createServer({
947
- root,
948
- port: args.port ? parseInt(args.port, 10) : void 0,
949
- open: !!args.open,
950
- detect: det,
951
- mode: args.mode
1306
+ await main().catch((e) => {
1307
+ console.error(e);
1308
+ process.exit(1);
952
1309
  });
953
1310
  //# sourceMappingURL=cli.js.map