zdashboard 1.0.1 → 1.2.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
@@ -2,8 +2,8 @@
2
2
 
3
3
  // src/server/index.ts
4
4
  import http from "http";
5
- import fs4 from "fs";
6
- import path4 from "path";
5
+ import fs6 from "fs";
6
+ import path6 from "path";
7
7
  import crypto from "crypto";
8
8
  import { exec } from "child_process";
9
9
  import { fileURLToPath as fileURLToPath2 } from "url";
@@ -210,6 +210,48 @@ var JustRunner = class {
210
210
  // src/server/bugs.ts
211
211
  import fs2 from "fs";
212
212
  import path2 from "path";
213
+
214
+ // src/server/api/fetch.ts
215
+ import ky from "ky";
216
+
217
+ // src/server/errors.ts
218
+ var HttpError = class extends Error {
219
+ constructor(status, message, body) {
220
+ super(message);
221
+ this.status = status;
222
+ this.body = body;
223
+ this.name = "HttpError";
224
+ }
225
+ status;
226
+ body;
227
+ };
228
+ var NetworkError = class extends Error {
229
+ constructor(message, cause) {
230
+ super(message);
231
+ this.cause = cause;
232
+ this.name = "NetworkError";
233
+ }
234
+ cause;
235
+ };
236
+
237
+ // src/server/api/fetch.ts
238
+ async function fetchJson(url, init) {
239
+ try {
240
+ const res = await ky(url, { ...init, timeout: 8e3, retry: 2 });
241
+ return await res.json();
242
+ } catch (e) {
243
+ if (e instanceof HttpError) throw e;
244
+ if (e instanceof Error && e.name === "TimeoutError") {
245
+ throw new NetworkError(`\u8BF7\u6C42\u8D85\u65F6: ${url}`);
246
+ }
247
+ if (e instanceof Error && e.name === "HTTPError") {
248
+ throw new HttpError(e.status ?? 500, e.message);
249
+ }
250
+ throw new NetworkError(`\u8BF7\u6C42\u5931\u8D25: ${url}`, e);
251
+ }
252
+ }
253
+
254
+ // src/server/bugs.ts
213
255
  function loadZgoalConfig(root2) {
214
256
  const file = path2.join(root2, ".zgoal", "config.yaml");
215
257
  if (!fs2.existsSync(file)) return null;
@@ -228,26 +270,6 @@ function loadZgoalConfig(root2) {
228
270
  product
229
271
  };
230
272
  }
231
- async function fetchJson(url, init) {
232
- const ctrl = new AbortController();
233
- const timer = setTimeout(() => ctrl.abort(), 8e3);
234
- try {
235
- const res = await fetch(url, { ...init, signal: ctrl.signal });
236
- const text = await res.text();
237
- let json = {};
238
- try {
239
- json = JSON.parse(text);
240
- } catch {
241
- }
242
- if (!res.ok) {
243
- const err = json.error;
244
- throw new Error(`HTTP ${res.status}${typeof err === "string" ? `: ${err}` : ""}`);
245
- }
246
- return json;
247
- } finally {
248
- clearTimeout(timer);
249
- }
250
- }
251
273
  var tokenCache = null;
252
274
  async function getToken(cfg) {
253
275
  if (cfg.token) return cfg.token;
@@ -309,15 +331,128 @@ function allBuiltins() {
309
331
  return Array.from(builtinPlugins.values());
310
332
  }
311
333
 
334
+ // src/server/review-store.ts
335
+ import fs4 from "fs";
336
+ import path4 from "path";
337
+ import YAML from "yaml";
338
+ var REVIEW_FILE = "review.yaml";
339
+ var ReviewStore = class {
340
+ root;
341
+ file;
342
+ onChange;
343
+ constructor(root2, onChange) {
344
+ this.root = root2;
345
+ this.file = path4.join(root2, REVIEW_FILE);
346
+ this.onChange = onChange;
347
+ }
348
+ exists() {
349
+ return fs4.existsSync(this.file);
350
+ }
351
+ read() {
352
+ try {
353
+ const parsed = YAML.parse(fs4.readFileSync(this.file, "utf8"));
354
+ if (!parsed || !Array.isArray(parsed.items)) return { status: "draft", items: [] };
355
+ return parsed;
356
+ } catch {
357
+ return { status: "draft", items: [] };
358
+ }
359
+ }
360
+ write(data) {
361
+ fs4.writeFileSync(this.file, YAML.stringify(data), "utf8");
362
+ this.onChange?.();
363
+ }
364
+ updateItem(id, patch) {
365
+ const data = this.read();
366
+ const item = data.items.find((i) => i.id === id);
367
+ if (!item) throw new Error(`item ${id} not found`);
368
+ if (patch.answer !== void 0) item.answer = patch.answer;
369
+ if (patch.state !== void 0) item.state = patch.state;
370
+ if (patch.state === "answered" && !patch.answer && !item.answer) item.answer = "";
371
+ this.write(data);
372
+ return data;
373
+ }
374
+ setStatus(status) {
375
+ const data = this.read();
376
+ if (status === "passed" && data.items.some((i) => i.state === "open")) {
377
+ throw new Error("\u5B58\u5728\u672A\u5904\u7406\u7684\u8BC4\u5BA1\u9879(open),\u4E0D\u80FD\u901A\u8FC7");
378
+ }
379
+ data.status = status;
380
+ this.write(data);
381
+ return data;
382
+ }
383
+ docs() {
384
+ try {
385
+ return fs4.readdirSync(this.root).filter((f) => /\.(md|markdown)$/i.test(f) && fs4.statSync(path4.join(this.root, f)).isFile()).sort();
386
+ } catch {
387
+ return [];
388
+ }
389
+ }
390
+ };
391
+
392
+ // src/server/design-assets.ts
393
+ import fs5 from "fs";
394
+ import path5 from "path";
395
+ var PAGE_EXTS = [".html", ".htm"];
396
+ var ICON_EXTS = [".svg", ".png", ".ico", ".jpg", ".jpeg", ".gif", ".webp"];
397
+ var VIDEO_EXTS = [".mp4", ".webm", ".mov", ".ogg", ".ogv"];
398
+ var AUDIO_EXTS = [".mp3", ".wav", ".flac", ".aac", ".m4a"];
399
+ var CODE_EXTS = [".js", ".mjs", ".ts", ".tsx", ".jsx", ".css", ".json", ".txt", ".xml", ".yml", ".yaml", ".sh", ".md"];
400
+ var FONT_EXTS = [".woff", ".woff2", ".ttf", ".otf"];
401
+ var TOKEN_RE = /token|theme|design|color|palette|typograph/i;
402
+ function categorize(rel, ext) {
403
+ if (rel.indexOf("components/") === 0) return "component";
404
+ if (VIDEO_EXTS.includes(ext)) return "video";
405
+ if (AUDIO_EXTS.includes(ext)) return "audio";
406
+ if (ext === ".pdf") return "pdf";
407
+ if (ext === ".md") return "md";
408
+ if (FONT_EXTS.includes(ext)) return "font";
409
+ if (ICON_EXTS.includes(ext)) return "icon";
410
+ if (PAGE_EXTS.includes(ext)) return "page";
411
+ if (CODE_EXTS.includes(ext)) return TOKEN_RE.test(rel) && (ext === ".css" || ext === ".json") ? "token" : "code";
412
+ return "other";
413
+ }
414
+ function scanAssets(root2) {
415
+ const out = {};
416
+ const keys = ["page", "component", "icon", "token", "md", "video", "audio", "pdf", "code", "font", "other"];
417
+ for (const k of keys) out[k] = [];
418
+ function walk(dir, rel) {
419
+ let ents;
420
+ try {
421
+ ents = fs5.readdirSync(dir, { withFileTypes: true });
422
+ } catch {
423
+ return;
424
+ }
425
+ for (const ent of ents) {
426
+ if (ent.name.charAt(0) === ".") continue;
427
+ const r = rel ? `${rel}/${ent.name}` : ent.name;
428
+ if (ent.isDirectory()) {
429
+ walk(path5.join(dir, ent.name), r);
430
+ continue;
431
+ }
432
+ const ext = path5.extname(ent.name).toLowerCase();
433
+ const t = categorize(r, ext);
434
+ out[t].push({ path: r, name: ent.name, ext, type: t });
435
+ }
436
+ }
437
+ walk(root2, "");
438
+ return out;
439
+ }
440
+
312
441
  // package.json
313
442
  var package_default = {
314
443
  name: "zdashboard",
315
- version: "1.0.1",
444
+ version: "1.2.0",
316
445
  description: "ZCode skill dashboard platform \u2014 pluggable viewers for zdesign/zview/zreview/zgoal",
317
446
  type: "module",
318
- bin: { zdashboard: "./dist/cli.js" },
319
- files: ["dist"],
320
- publishConfig: { access: "public" },
447
+ bin: {
448
+ zdashboard: "./dist/cli.js"
449
+ },
450
+ files: [
451
+ "dist"
452
+ ],
453
+ publishConfig: {
454
+ access: "public"
455
+ },
321
456
  scripts: {
322
457
  dev: "vite",
323
458
  build: "tsup && vite build",
@@ -331,47 +466,63 @@ var package_default = {
331
466
  "@radix-ui/react-separator": "^1.1.0",
332
467
  "@radix-ui/react-slot": "^1.1.0",
333
468
  "@radix-ui/react-tooltip": "^1.1.2",
469
+ "@uidotdev/usehooks": "^2.4.0",
334
470
  "ansi-to-react": "^6.1.6",
335
471
  "class-variance-authority": "^0.7.0",
336
472
  clsx: "^2.1.1",
473
+ "date-fns": "^3.6.0",
474
+ filesize: "^11.0.0",
337
475
  "highlight.js": "^11.11.1",
338
476
  katex: "^0.16.11",
477
+ ky: "^1.7.0",
478
+ "lodash-es": "^4.17.21",
339
479
  "lucide-react": "^0.460.0",
340
480
  react: "^18.3.1",
341
481
  "react-dom": "^18.3.1",
482
+ "react-error-boundary": "^5.0.0",
342
483
  "react-markdown": "^9.0.1",
343
- "remark-frontmatter": "^5.0.0",
344
- "remark-gfm": "^4.0.0",
345
- "remark-math": "^6.0.0",
346
484
  "rehype-autolink-headings": "^7.1.0",
347
485
  "rehype-highlight": "^7.0.1",
348
486
  "rehype-katex": "^7.0.1",
349
487
  "rehype-raw": "^7.0.0",
350
488
  "rehype-slug": "^6.0.0",
351
- "tailwind-merge": "^2.5.4"
489
+ "remark-frontmatter": "^5.0.0",
490
+ "remark-gfm": "^4.0.0",
491
+ "remark-math": "^6.0.0",
492
+ sonner: "^1.5.0",
493
+ "tailwind-merge": "^2.5.4",
494
+ "use-debounce": "^10.0.0",
495
+ yaml: "^2.9.0"
352
496
  },
353
497
  devDependencies: {
354
498
  "@tailwindcss/typography": "^0.5.20",
499
+ "@testing-library/jest-dom": "^7.0.1",
500
+ "@testing-library/react": "^16.3.2",
501
+ "@types/lodash-es": "^4.17.12",
355
502
  "@types/node": "^22.9.0",
356
503
  "@types/react": "^18.3.12",
357
504
  "@types/react-dom": "^18.3.1",
358
- typescript: "^5.6.3",
359
- vite: "^5.4.10",
360
- "@vitejs/plugin-react": "^4.3.3",
505
+ "@vitejs/plugin-react": "^4.7.0",
506
+ autoprefixer: "^10.4.20",
507
+ jsdom: "^30.0.1",
508
+ postcss: "^8.4.49",
361
509
  tailwindcss: "^3.4.14",
362
510
  "tailwindcss-animate": "^1.0.7",
363
- postcss: "^8.4.49",
364
- autoprefixer: "^10.4.20",
365
- tsup: "^8.3.5"
511
+ tsup: "^8.3.5",
512
+ typescript: "^5.6.3",
513
+ vite: "^5.4.10",
514
+ vitest: "^1.6.0"
366
515
  },
367
516
  pnpm: {
368
- onlyBuiltDependencies: ["esbuild"]
517
+ onlyBuiltDependencies: [
518
+ "esbuild"
519
+ ]
369
520
  }
370
521
  };
371
522
 
372
523
  // src/server/index.ts
373
524
  var VERSION = package_default.version;
374
- var __dirname2 = path4.dirname(fileURLToPath2(import.meta.url));
525
+ var __dirname2 = path6.dirname(fileURLToPath2(import.meta.url));
375
526
  var STOP_TOKEN = crypto.randomBytes(12).toString("hex");
376
527
  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>`;
377
528
  var MIME = {
@@ -405,11 +556,11 @@ function readBody(req) {
405
556
  });
406
557
  }
407
558
  function createServer(opts) {
408
- const ROOT = path4.resolve(opts.root);
559
+ const ROOT = path6.resolve(opts.root);
409
560
  const PORT0 = opts.port ?? 4190;
410
561
  const OPEN = !!opts.open;
411
- const APP_DIR = opts.dashboardDir ?? path4.resolve(__dirname2, "web");
412
- if (!fs4.existsSync(ROOT)) fs4.mkdirSync(ROOT, { recursive: true });
562
+ const APP_DIR = opts.dashboardDir ?? path6.resolve(__dirname2, "web");
563
+ if (!fs6.existsSync(ROOT)) fs6.mkdirSync(ROOT, { recursive: true });
413
564
  const det2 = opts.detect;
414
565
  const runner = new JustRunner(ROOT);
415
566
  const MODE = opts.mode;
@@ -423,7 +574,61 @@ function createServer(opts) {
423
574
  } }
424
575
  });
425
576
  registerBuiltin({ mode: "view", label: "\u9879\u76EE\u6D4F\u89C8", icon: "\u{1F441}\uFE0F" });
426
- registerBuiltin({ mode: "review", label: "\u6587\u6863\u8BC4\u5BA1", icon: "\u2705" });
577
+ const reviewStore = new ReviewStore(ROOT);
578
+ registerBuiltin({
579
+ mode: "review",
580
+ label: "\u6587\u6863\u8BC4\u5BA1",
581
+ icon: "\u2705",
582
+ apiRoutes: {
583
+ "/__review": async (_, res) => {
584
+ res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
585
+ res.end(JSON.stringify(reviewStore.read()));
586
+ },
587
+ "/__review/item": async (req, res) => {
588
+ if (req.headers["x-stop-token"] !== STOP_TOKEN) {
589
+ res.writeHead(403);
590
+ res.end("forbidden");
591
+ return;
592
+ }
593
+ (async () => {
594
+ try {
595
+ const body = JSON.parse(await readBody(req) || "{}");
596
+ const data = reviewStore.updateItem(body.id, { answer: body.answer, state: body.state });
597
+ res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
598
+ res.end(JSON.stringify(data));
599
+ } catch (e) {
600
+ res.writeHead(400, { "Content-Type": "application/json; charset=utf-8" });
601
+ res.end(JSON.stringify({ error: e.message }));
602
+ }
603
+ })();
604
+ return;
605
+ },
606
+ "/__review/status": async (req, res) => {
607
+ if (req.headers["x-stop-token"] !== STOP_TOKEN) {
608
+ res.writeHead(403);
609
+ res.end("forbidden");
610
+ return;
611
+ }
612
+ (async () => {
613
+ try {
614
+ const body = JSON.parse(await readBody(req) || "{}");
615
+ const data = reviewStore.setStatus(body.status);
616
+ res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
617
+ res.end(JSON.stringify(data));
618
+ } catch (e) {
619
+ res.writeHead(400, { "Content-Type": "application/json; charset=utf-8" });
620
+ res.end(JSON.stringify({ error: e.message }));
621
+ }
622
+ })();
623
+ return;
624
+ },
625
+ "/__docs": async (_, res) => {
626
+ res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
627
+ res.end(JSON.stringify(reviewStore.docs()));
628
+ }
629
+ }
630
+ });
631
+ registerBuiltin({ mode: "design", label: "\u8BBE\u8BA1\u8D44\u4EA7", icon: "\u{1F3A8}" });
427
632
  const clients = /* @__PURE__ */ new Set();
428
633
  const broadcast = (ev, data = "") => {
429
634
  const payload = `event: ${ev}
@@ -433,12 +638,12 @@ data: ${JSON.stringify(data == null ? "" : data)}
433
638
  for (const c of clients) c.write(payload);
434
639
  };
435
640
  function serveFile(filePath, res, injectHtml) {
436
- fs4.readFile(filePath, (err, data) => {
641
+ fs6.readFile(filePath, (err, data) => {
437
642
  if (err) {
438
643
  res.writeHead(404);
439
644
  return res.end("Not found");
440
645
  }
441
- const ext = path4.extname(filePath).toLowerCase();
646
+ const ext = path6.extname(filePath).toLowerCase();
442
647
  const ct = MIME[ext] ?? "application/octet-stream";
443
648
  let body = data;
444
649
  if (injectHtml && ext === ".html") {
@@ -481,6 +686,10 @@ data: ${JSON.stringify(data == null ? "" : data)}
481
686
  return;
482
687
  }
483
688
  if (url === "/__files") {
689
+ if (MODE === "design") {
690
+ res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
691
+ return res.end(JSON.stringify(scanAssets(ROOT)));
692
+ }
484
693
  const tree = scanTree(ROOT, det2.hasOpenspec, det2.hasDocs);
485
694
  const payload = { tree, ...det2 };
486
695
  res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
@@ -548,25 +757,25 @@ data: ${JSON.stringify(data == null ? "" : data)}
548
757
  }
549
758
  }
550
759
  }
551
- if (url === "/") return serveFile(path4.join(APP_DIR, "index.html"), res, false);
760
+ if (url === "/") return serveFile(path6.join(APP_DIR, "index.html"), res, false);
552
761
  if (url.indexOf("/__app/") === 0) {
553
- const fp2 = path4.join(APP_DIR, url.slice(7));
554
- if (fp2 !== APP_DIR && fp2.indexOf(APP_DIR + path4.sep) !== 0) {
762
+ const fp2 = path6.join(APP_DIR, url.slice(7));
763
+ if (fp2 !== APP_DIR && fp2.indexOf(APP_DIR + path6.sep) !== 0) {
555
764
  res.writeHead(403);
556
765
  return res.end("Forbidden");
557
766
  }
558
767
  return serveFile(fp2, res, false);
559
768
  }
560
769
  if (url.indexOf("/assets/") === 0) {
561
- const fp2 = path4.join(APP_DIR, decodeURIComponent(url));
562
- if (fp2.indexOf(APP_DIR + path4.sep) !== 0) {
770
+ const fp2 = path6.join(APP_DIR, decodeURIComponent(url));
771
+ if (fp2.indexOf(APP_DIR + path6.sep) !== 0) {
563
772
  res.writeHead(403);
564
773
  return res.end("Forbidden");
565
774
  }
566
775
  return serveFile(fp2, res, false);
567
776
  }
568
- const fp = path4.join(ROOT, decodeURIComponent(url));
569
- if (fp !== ROOT && fp.indexOf(ROOT + path4.sep) !== 0) {
777
+ const fp = path6.join(ROOT, decodeURIComponent(url));
778
+ if (fp !== ROOT && fp.indexOf(ROOT + path6.sep) !== 0) {
570
779
  res.writeHead(403);
571
780
  return res.end("Forbidden");
572
781
  }
@@ -592,7 +801,7 @@ data: ${JSON.stringify(data == null ? "" : data)}
592
801
  }
593
802
  let debounce;
594
803
  try {
595
- fs4.watch(ROOT, { recursive: true }, () => {
804
+ fs6.watch(ROOT, { recursive: true }, () => {
596
805
  clearTimeout(debounce);
597
806
  debounce = setTimeout(() => {
598
807
  broadcast("reload");
@@ -607,8 +816,8 @@ data: ${JSON.stringify(data == null ? "" : data)}
607
816
  }
608
817
 
609
818
  // src/server/detect.ts
610
- import fs5 from "fs";
611
- import path5 from "path";
819
+ import fs7 from "fs";
820
+ import path7 from "path";
612
821
  import { execFile as execFile2 } from "child_process";
613
822
  function justAvailable(cwd) {
614
823
  return new Promise((resolve) => {
@@ -619,10 +828,10 @@ function justAvailable(cwd) {
619
828
  });
620
829
  }
621
830
  async function detect(root2) {
622
- const hasOpenspec = fs5.existsSync(path5.join(root2, "openspec"));
623
- const hasDocs = fs5.existsSync(path5.join(root2, "docs"));
831
+ const hasOpenspec = fs7.existsSync(path7.join(root2, "openspec"));
832
+ const hasDocs = fs7.existsSync(path7.join(root2, "docs"));
624
833
  const hasJust = await justAvailable(root2);
625
- const hasBugs = fs5.existsSync(path5.join(root2, ".zgoal", "config.yaml"));
834
+ const hasBugs = fs7.existsSync(path7.join(root2, ".zgoal", "config.yaml"));
626
835
  return { hasOpenspec, hasDocs, hasJust, hasBugs };
627
836
  }
628
837