zdashboard 2.6.1 → 2.7.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.
Files changed (36) hide show
  1. package/dist/cli.js +861 -282
  2. package/dist/cli.js.map +1 -1
  3. package/dist/web/assets/ApprovalPanel-CXQRioSw.js +1 -0
  4. package/dist/web/assets/BatchDashboard-BigPUG8g.js +2 -0
  5. package/dist/web/assets/CheckpointViewer-DBmTkC_E.js +1 -0
  6. package/dist/web/assets/ConfigField-BqjVlteZ.js +1 -0
  7. package/dist/web/assets/DependencyGraph-DPVneSQh.js +1 -0
  8. package/dist/web/assets/FileIcon-tDB2mYGC.js +279 -0
  9. package/dist/web/assets/{FilterPills-B6u1IBeC.js → FilterPills-CvO0pz85.js} +1 -1
  10. package/dist/web/assets/{ProgressBar-CbyvyLzw.js → ProgressBar-DIoIeYBA.js} +1 -1
  11. package/dist/web/assets/{Workspace-DQmfVJh6.js → Workspace-B2nKQnQf.js} +1 -1
  12. package/dist/web/assets/badge-CmIMTBDj.js +1 -0
  13. package/dist/web/assets/index-BHL7sr_b.css +1 -0
  14. package/dist/web/assets/{index-BvuOeKbq.js → index-CtlFZEcM.js} +1 -1
  15. package/dist/web/assets/{index-BfA6l4to.js → index-yx7txO50.js} +1565 -1565
  16. package/dist/web/assets/web-BBv8hkwo.js +1 -0
  17. package/dist/web/assets/web-BNjs9lCy.js +139 -0
  18. package/dist/web/assets/{web-BXCBKn6i.js → web-BX2cajm6.js} +1 -1
  19. package/dist/web/assets/web-BpBEbcYZ.js +1 -0
  20. package/dist/web/assets/web-CGbbhItO.js +2 -0
  21. package/dist/web/assets/web-CcDed_yx.js +1 -0
  22. package/dist/web/assets/{web-C8I10oqh.js → web-DsIjKVnn.js} +1 -1
  23. package/dist/web/assets/web-Eo4S0U_k.js +2 -0
  24. package/dist/web/favicon.svg +13 -5
  25. package/dist/web/index.html +2 -2
  26. package/package.json +1 -1
  27. package/dist/web/assets/FileIcon-B0-2gV6a.js +0 -10
  28. package/dist/web/assets/MdViewer-Cvh3TNQI.js +0 -270
  29. package/dist/web/assets/badge-DTb6qo3S.js +0 -1
  30. package/dist/web/assets/index-DQkehbup.css +0 -1
  31. package/dist/web/assets/web-5newSm97.js +0 -1
  32. package/dist/web/assets/web-BOg9UnpC.js +0 -1
  33. package/dist/web/assets/web-BcTJQUKr.js +0 -1
  34. package/dist/web/assets/web-CLCAgjmg.js +0 -1
  35. package/dist/web/assets/web-CXE89yN1.js +0 -2
  36. /package/dist/web/assets/{MdViewer-BTcE7LSd.css → FileIcon-BTcE7LSd.css} +0 -0
package/dist/cli.js CHANGED
@@ -1,156 +1,41 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import path11 from "path";
5
- import fs11 from "fs";
4
+ import path12 from "path";
5
+ import fs12 from "fs";
6
6
  import { fileURLToPath as fileURLToPath2 } from "url";
7
7
  import { access, readdir } from "fs/promises";
8
8
  import { parseArgs as utilParseArgs } from "util";
9
9
 
10
10
  // src/server/detect.ts
11
- import fs2 from "fs";
12
- import path2 from "path";
13
- import { execFile } from "child_process";
14
-
15
- // src/server/bugs.ts
16
11
  import fs from "fs";
17
12
  import path from "path";
18
- import YAML from "yaml";
19
-
20
- // src/server/api/fetch.ts
21
- import ky from "ky";
22
-
23
- // src/server/errors.ts
24
- var HttpError = class extends Error {
25
- constructor(status, message, body) {
26
- super(message);
27
- this.status = status;
28
- this.body = body;
29
- this.name = "HttpError";
30
- }
31
- status;
32
- body;
33
- };
34
- var NetworkError = class extends Error {
35
- constructor(message, cause) {
36
- super(message);
37
- this.cause = cause;
38
- this.name = "NetworkError";
39
- }
40
- cause;
41
- };
42
-
43
- // src/server/api/fetch.ts
44
- async function fetchJson(url, init) {
45
- try {
46
- const res = await ky(url, { ...init, timeout: 8e3, retry: 2 });
47
- return await res.json();
48
- } catch (e) {
49
- if (e instanceof HttpError) throw e;
50
- if (e instanceof Error && e.name === "TimeoutError") {
51
- throw new NetworkError(`\u8BF7\u6C42\u8D85\u65F6: ${url}`);
52
- }
53
- if (e instanceof Error && e.name === "HTTPError") {
54
- throw new HttpError(e.status ?? 500, e.message);
55
- }
56
- throw new NetworkError(`\u8BF7\u6C42\u5931\u8D25: ${url}`, e);
57
- }
58
- }
59
-
60
- // src/server/bugs.ts
61
- var BUGS_CONFIG_CANDIDATES = [".zdev/config.yaml", ".zgoal/config.yaml"];
62
- function loadConfig(root) {
63
- for (const rel of BUGS_CONFIG_CANDIDATES) {
64
- const file = path.join(root, rel);
65
- if (!fs.existsSync(file)) continue;
66
- try {
67
- const kv = YAML.parse(fs.readFileSync(file, "utf8"));
68
- if (!kv || typeof kv !== "object") return null;
69
- const record = kv;
70
- const product = Number(record.product);
71
- if (!record.url || !product) return null;
72
- const str = (v) => typeof v === "string" ? v : v == null ? void 0 : String(v);
73
- return {
74
- url: String(record.url).replace(/\/+$/, ""),
75
- account: str(record.account) ?? "",
76
- password: str(record.password),
77
- token: str(record.token),
78
- product
79
- };
80
- } catch {
81
- return null;
82
- }
83
- }
84
- return null;
85
- }
86
- var tokenCache = null;
87
- async function getToken(cfg) {
88
- if (cfg.token) return cfg.token;
89
- const key = `${cfg.url}|${cfg.account}|${cfg.password ?? ""}`;
90
- if (tokenCache && tokenCache.key === key && Date.now() - tokenCache.at < 10 * 6e4) return tokenCache.token;
91
- const json = await fetchJson(`${cfg.url}/api.php/v1/tokens`, {
92
- method: "POST",
93
- headers: { "Content-Type": "application/json" },
94
- body: JSON.stringify({ account: cfg.account, password: cfg.password })
95
- });
96
- const token = typeof json.token === "string" ? json.token : "";
97
- if (!token) throw new Error("token \u83B7\u53D6\u5931\u8D25:\u68C0\u67E5 account / password");
98
- tokenCache = { key, token, at: Date.now() };
99
- return token;
100
- }
101
- function normBug(b, account) {
102
- const assigned = b.assignedTo;
103
- const assignedTo = typeof assigned === "string" ? assigned : assigned && typeof assigned === "object" && "realname" in assigned ? String(assigned.realname ?? "") : "";
104
- const assignedAccount = typeof assigned === "string" ? assigned : assigned && typeof assigned === "object" ? String(assigned.account ?? "") : "";
105
- const mine = !!account && (assignedAccount === account || assignedTo === account);
106
- return {
107
- id: Number(b.id),
108
- title: String(b.title ?? ""),
109
- severity: b.severity ?? 4,
110
- pri: b.pri ?? 3,
111
- status: String(b.status ?? ""),
112
- assignedTo,
113
- openedBy: typeof b.openedBy === "string" ? b.openedBy : void 0,
114
- mine
115
- };
116
- }
117
- async function fetchBugs(root) {
118
- const cfg = loadConfig(root);
119
- if (!cfg) {
120
- const anyExists = BUGS_CONFIG_CANDIDATES.some((rel) => fs.existsSync(path.join(root, rel)));
121
- return {
122
- ok: false,
123
- error: anyExists ? "\u914D\u7F6E\u5B58\u5728\u4F46\u65E0\u6548(\u7F3A url \u6216 product \u5B57\u6BB5)\u2014\u2014\u68C0\u67E5 .zdev/config.yaml" : ".zdev/config.yaml \u7F3A\u5931(\u7531 zgoal skill \u521B\u5EFA;\u5B58\u91CF .zgoal/config.yaml \u4EA6\u53EF)"
124
- };
125
- }
126
- try {
127
- const token = await getToken(cfg);
128
- const json = await fetchJson(
129
- `${cfg.url}/api.php/v1/products/${cfg.product}/bugs?page=1&limit=100`,
130
- { headers: { Token: token } }
131
- );
132
- const raw = Array.isArray(json.bugs) ? json.bugs : [];
133
- return { ok: true, url: cfg.url, total: Number(json.total ?? raw.length), bugs: raw.map((b) => normBug(b, cfg.account)) };
134
- } catch (e) {
135
- const msg = e instanceof Error ? e.message : String(e);
136
- 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` };
137
- }
138
- }
139
-
140
- // src/server/detect.ts
13
+ import { execFile } from "child_process";
141
14
  function justAvailable(cwd) {
142
15
  return new Promise((resolve) => {
143
- const child = execFile("just", ["--list", "--unsorted"], { cwd, timeout: 5e3 }, (err) => {
16
+ const child = execFile("just", ["--list", "--unsupported"], { cwd, timeout: 5e3 }, (err) => {
144
17
  resolve(!err);
145
18
  });
146
19
  if (child.killed) resolve(false);
147
20
  });
148
21
  }
22
+ function hasBugsConfig(root) {
23
+ const dashboardPath = path.join(root, ".zdev", "dashboard.json");
24
+ if (!fs.existsSync(dashboardPath)) return false;
25
+ try {
26
+ const raw = fs.readFileSync(dashboardPath, "utf-8");
27
+ const rec = JSON.parse(raw);
28
+ const bugs = rec.plugins?.bugs;
29
+ if (bugs && typeof bugs === "object" && bugs.url && bugs.product) return true;
30
+ } catch {
31
+ }
32
+ return false;
33
+ }
149
34
  async function detect(root) {
150
- const hasOpenspec = fs2.existsSync(path2.join(root, "openspec"));
151
- const hasDocs = fs2.existsSync(path2.join(root, "docs"));
35
+ const hasOpenspec = fs.existsSync(path.join(root, "openspec"));
36
+ const hasDocs = fs.existsSync(path.join(root, "docs"));
152
37
  const hasJust = await justAvailable(root);
153
- const hasBugs = BUGS_CONFIG_CANDIDATES.some((rel) => fs2.existsSync(path2.join(root, rel)));
38
+ const hasBugs = hasBugsConfig(root);
154
39
  return { hasOpenspec, hasDocs, hasJust, hasBugs };
155
40
  }
156
41
 
@@ -159,15 +44,15 @@ import { Context } from "cordis";
159
44
 
160
45
  // src/core/server.ts
161
46
  import http from "http";
162
- import fs4 from "fs";
163
- import path4 from "path";
47
+ import fs3 from "fs";
48
+ import path3 from "path";
164
49
  import crypto from "crypto";
165
50
  import { fileURLToPath } from "url";
166
51
 
167
52
  // package.json
168
53
  var package_default = {
169
54
  name: "zdashboard",
170
- version: "2.6.1",
55
+ version: "2.7.0",
171
56
  description: "ZCode skill dashboard platform \u2014 pluggable viewers for zdesign/zview/zreview/zgoal",
172
57
  type: "module",
173
58
  bin: {
@@ -257,20 +142,20 @@ var package_default = {
257
142
  import { Service } from "cordis";
258
143
 
259
144
  // src/core/instance.ts
260
- import fs3 from "fs";
261
- import path3 from "path";
145
+ import fs2 from "fs";
146
+ import path2 from "path";
262
147
  var RECORD_FILE = ".zdev/dashboard.json";
263
148
  var VERIFY_TIMEOUT_MS = 1500;
264
149
  var STOP_POLL_MS = 2e3;
265
150
  var STOP_POLL_INTERVAL_MS = 100;
266
151
  var STOP_FINAL_WAIT_MS = 100;
267
152
  function recordPath(root) {
268
- return path3.join(root, RECORD_FILE);
153
+ return path2.join(root, RECORD_FILE);
269
154
  }
270
155
  function readRecord(root) {
271
156
  try {
272
157
  const fp = recordPath(root);
273
- const raw = fs3.readFileSync(fp, "utf-8");
158
+ const raw = fs2.readFileSync(fp, "utf-8");
274
159
  const rec = JSON.parse(raw);
275
160
  if (typeof rec.pid === "number" && typeof rec.port === "number" && typeof rec.root === "string" && typeof rec.startedAt === "string") {
276
161
  return rec;
@@ -281,20 +166,44 @@ function readRecord(root) {
281
166
  }
282
167
  }
283
168
  function writeRecord(root, port) {
284
- fs3.mkdirSync(path3.dirname(recordPath(root)), { recursive: true });
169
+ fs2.mkdirSync(path2.dirname(recordPath(root)), { recursive: true });
285
170
  const rec = {
286
171
  pid: process.pid,
287
172
  port,
288
173
  root,
289
174
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
290
175
  };
291
- fs3.writeFileSync(recordPath(root), JSON.stringify(rec, null, 2) + "\n");
176
+ fs2.writeFileSync(recordPath(root), JSON.stringify(rec, null, 2) + "\n");
177
+ }
178
+ function readPluginsConfig(root) {
179
+ try {
180
+ const rec = readRecord(root);
181
+ if (rec?.plugins && typeof rec.plugins === "object") {
182
+ return rec.plugins;
183
+ }
184
+ } catch {
185
+ }
186
+ return {};
187
+ }
188
+ function writePluginsConfig(root, plugins) {
189
+ const fp = recordPath(root);
190
+ let rec;
191
+ try {
192
+ const raw = fs2.readFileSync(fp, "utf-8");
193
+ rec = JSON.parse(raw);
194
+ } catch {
195
+ rec = { pid: process.pid, port: 0, root, startedAt: (/* @__PURE__ */ new Date()).toISOString() };
196
+ }
197
+ rec.plugins = plugins;
198
+ const tmp = fp + ".tmp";
199
+ fs2.writeFileSync(tmp, JSON.stringify(rec, null, 2) + "\n");
200
+ fs2.renameSync(tmp, fp);
292
201
  }
293
202
  function clearRecord(root) {
294
203
  try {
295
204
  const rec = readRecord(root);
296
205
  if (rec && rec.pid !== process.pid) return;
297
- fs3.unlinkSync(recordPath(root));
206
+ fs2.unlinkSync(recordPath(root));
298
207
  } catch {
299
208
  }
300
209
  }
@@ -366,7 +275,7 @@ function openUrl(url) {
366
275
  // src/core/server.ts
367
276
  import { execFile as execFile3 } from "child_process";
368
277
  var VERSION = package_default.version;
369
- var __dirname = path4.dirname(fileURLToPath(import.meta.url));
278
+ var __dirname = path3.dirname(fileURLToPath(import.meta.url));
370
279
  var MIME = {
371
280
  ".html": "text/html; charset=utf-8",
372
281
  ".htm": "text/html; charset=utf-8",
@@ -421,7 +330,7 @@ var MIME = {
421
330
  ".ttf": "font/woff",
422
331
  ".map": "application/json; charset=utf-8"
423
332
  };
424
- 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>`;
333
+ var INJECT = `<script>(function(){try{var es=new EventSource('/__reload');es.addEventListener('files',function(){/* plugin \u5C40\u90E8\u5237\u65B0\u7531 useSSE \u6D88\u8D39,\u6B64\u5904\u4E0D\u518D\u6574\u9875 reload */});es.onerror=function(){/* EventSource \u539F\u751F\u91CD\u8FDE,\u9759\u9ED8\u5904\u7406 */};}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>`;
425
334
  var PLUGIN_STATIC_PREFIX = "/__plugin/";
426
335
  var ServerService = class extends Service {
427
336
  stopToken;
@@ -464,25 +373,36 @@ var ServerService = class extends Service {
464
373
  });
465
374
  this.start(config.port);
466
375
  }
467
- route(path12, handler) {
468
- this.routes.set(path12, handler);
469
- this.ctx.effect(() => () => this.routes.delete(path12));
376
+ route(path13, handler) {
377
+ this.routes.set(path13, handler);
378
+ this.ctx.effect(() => () => this.routes.delete(path13));
470
379
  }
471
- sse(path12, onConnect) {
472
- this.sses.set(path12, onConnect);
473
- this.ctx.effect(() => () => this.sses.delete(path12));
380
+ sse(path13, onConnect) {
381
+ this.sses.set(path13, onConnect);
382
+ this.ctx.effect(() => () => this.sses.delete(path13));
474
383
  }
475
384
  static(prefix, dir) {
476
385
  this.prefixStatic.set(prefix, dir);
477
386
  this.ctx.effect(() => () => this.prefixStatic.delete(prefix));
478
387
  }
388
+ pluginsConfig = {};
389
+ getPluginConfig(mode) {
390
+ return this.pluginsConfig[mode] ?? {};
391
+ }
392
+ refreshPluginsConfig() {
393
+ this.pluginsConfig = readPluginsConfig(this.root);
394
+ }
395
+ savePluginConfig(merged) {
396
+ this.pluginsConfig = merged;
397
+ writePluginsConfig(this.root, merged);
398
+ }
479
399
  serveFile(filePath, req, res, injectHtml) {
480
- fs4.stat(filePath, (err, stat) => {
400
+ fs3.stat(filePath, (err, stat) => {
481
401
  if (err) {
482
402
  res.writeHead(404);
483
403
  return res.end("Not found");
484
404
  }
485
- const ext = path4.extname(filePath).toLowerCase();
405
+ const ext = path3.extname(filePath).toLowerCase();
486
406
  const ct = MIME[ext] ?? "application/octet-stream";
487
407
  const rangeHeader = req.headers["range"];
488
408
  let range = null;
@@ -506,7 +426,7 @@ var ServerService = class extends Service {
506
426
  "Accept-Ranges": "bytes",
507
427
  "Cache-Control": "no-cache"
508
428
  });
509
- const stream = fs4.createReadStream(filePath, { start, end });
429
+ const stream = fs3.createReadStream(filePath, { start, end });
510
430
  stream.on("error", () => {
511
431
  try {
512
432
  res.destroy();
@@ -516,7 +436,7 @@ var ServerService = class extends Service {
516
436
  stream.pipe(res);
517
437
  return;
518
438
  }
519
- fs4.readFile(filePath, (err2, data) => {
439
+ fs3.readFile(filePath, (err2, data) => {
520
440
  if (err2) {
521
441
  res.writeHead(404);
522
442
  return res.end("Not found");
@@ -535,9 +455,9 @@ var ServerService = class extends Service {
535
455
  for (const [prefix, dir] of this.prefixStatic) {
536
456
  if (url.indexOf(prefix) === 0) {
537
457
  let rel = this.safeDecode(url.slice(prefix.length));
538
- if (!rel || rel === "/" || !path4.extname(rel)) rel = path4.join(rel || "", "index.html");
539
- const fp = path4.join(dir, rel);
540
- if (fp.indexOf(dir + path4.sep) !== 0) {
458
+ if (!rel || rel === "/" || !path3.extname(rel)) rel = path3.join(rel || "", "index.html");
459
+ const fp = path3.join(dir, rel);
460
+ if (fp.indexOf(dir + path3.sep) !== 0) {
541
461
  res.writeHead(403);
542
462
  res.end("Forbidden");
543
463
  return false;
@@ -577,17 +497,17 @@ var ServerService = class extends Service {
577
497
  if (this.servePrefix(url, req, res)) return;
578
498
  if (url === "/" || url.indexOf("/__app/") === 0 || url.indexOf("/assets/") === 0) {
579
499
  let fp2 = this.appDir;
580
- if (url !== "/") fp2 = path4.join(this.appDir, this.safeDecode(url));
581
- if (url.indexOf("/__app/") === 0) fp2 = path4.join(this.appDir, url.slice(7));
582
- if (fp2 !== this.appDir && fp2.indexOf(this.appDir + path4.sep) !== 0) {
500
+ if (url !== "/") fp2 = path3.join(this.appDir, this.safeDecode(url));
501
+ if (url.indexOf("/__app/") === 0) fp2 = path3.join(this.appDir, url.slice(7));
502
+ if (fp2 !== this.appDir && fp2.indexOf(this.appDir + path3.sep) !== 0) {
583
503
  res.writeHead(403);
584
504
  return res.end("Forbidden");
585
505
  }
586
- if (url === "/") fp2 = path4.join(this.appDir, "index.html");
506
+ if (url === "/") fp2 = path3.join(this.appDir, "index.html");
587
507
  return this.serveFile(fp2, req, res, false);
588
508
  }
589
- const fp = path4.join(this.root, this.safeDecode(url));
590
- if (fp !== this.root && fp.indexOf(this.root + path4.sep) !== 0) {
509
+ const fp = path3.join(this.root, this.safeDecode(url));
510
+ if (fp !== this.root && fp.indexOf(this.root + path3.sep) !== 0) {
591
511
  res.writeHead(403);
592
512
  return res.end("Forbidden");
593
513
  }
@@ -671,9 +591,9 @@ var ServerService = class extends Service {
671
591
  };
672
592
 
673
593
  // src/core/reload.ts
674
- import fs5 from "fs";
594
+ import fs4 from "fs";
675
595
  import { Service as Service2 } from "cordis";
676
- var WATCH_DEBOUNCE_MS = 150;
596
+ var WATCH_DEBOUNCE_MS = 300;
677
597
  var ReloadService = class extends Service2 {
678
598
  static inject = ["server"];
679
599
  clients = /* @__PURE__ */ new Set();
@@ -687,13 +607,15 @@ var ReloadService = class extends Service2 {
687
607
  return () => this.clients.delete(res);
688
608
  });
689
609
  try {
690
- this.watcher = fs5.watch(config.root, { recursive: true }, (eventType, filename) => {
610
+ this.watcher = fs4.watch(config.root, { recursive: true }, (eventType, filename) => {
691
611
  if (filename && /^(?:\.git|node_modules|dist|\.pnpm)(?:[/\\]|$)/.test(filename)) return;
612
+ if (filename && /\.(?:swp|tmp)$|~$|^\.DS_Store$|^Thumbs\.db$/i.test(filename)) return;
692
613
  if (this.timer) clearTimeout(this.timer);
693
614
  this.timer = setTimeout(() => {
694
615
  this.ctx.server.refreshGitInfo();
695
616
  this.broadcast("reload");
696
617
  this.broadcast("files");
618
+ this.timer = void 0;
697
619
  }, WATCH_DEBOUNCE_MS);
698
620
  });
699
621
  } catch {
@@ -732,25 +654,26 @@ data: ${JSON.stringify(data == null ? "" : data)}
732
654
  };
733
655
 
734
656
  // src/server/spec-scan.ts
735
- import fs7 from "fs";
736
- import path6 from "path";
737
-
738
- // src/server/walk.ts
739
657
  import fs6 from "fs";
740
658
  import path5 from "path";
659
+
660
+ // src/server/walk.ts
661
+ import fs5 from "fs";
662
+ import path4 from "path";
741
663
  function walkDir(root, opts = {}) {
742
- const { skip = /* @__PURE__ */ new Set(), maxDepth, onFile, onDir } = opts;
664
+ const { skip = /* @__PURE__ */ new Set(), maxDepth, showHidden, onFile, onDir } = opts;
743
665
  function walk(dir, rel, depth) {
744
666
  if (typeof maxDepth === "number" && depth > maxDepth) return;
745
667
  let ents;
746
668
  try {
747
- ents = fs6.readdirSync(dir, { withFileTypes: true });
669
+ ents = fs5.readdirSync(dir, { withFileTypes: true });
748
670
  } catch {
749
671
  return;
750
672
  }
751
673
  for (const ent of ents) {
752
- if (ent.name.startsWith(".") || skip.has(ent.name)) continue;
753
- const abs = path5.join(dir, ent.name);
674
+ if (!showHidden && ent.name.startsWith(".")) continue;
675
+ if (skip.has(ent.name)) continue;
676
+ const abs = path4.join(dir, ent.name);
754
677
  const r = rel ? `${rel}/${ent.name}` : ent.name;
755
678
  if (ent.isDirectory()) {
756
679
  onDir?.(abs, r);
@@ -764,7 +687,7 @@ function walkDir(root, opts = {}) {
764
687
  }
765
688
 
766
689
  // src/server/spec-scan.ts
767
- function buildTree(paths) {
690
+ function buildTree(paths, defaultExpandDepth = 2) {
768
691
  const root = [];
769
692
  const map = /* @__PURE__ */ new Map();
770
693
  for (const p of paths) {
@@ -775,10 +698,14 @@ function buildTree(paths) {
775
698
  const part = parts[i];
776
699
  current = current ? `${current}/${part}` : part;
777
700
  const isLast = i === parts.length - 1;
701
+ const depth = i;
778
702
  if (!map.has(current)) {
703
+ const isDir = !isLast;
704
+ const collapsed = isDir && typeof defaultExpandDepth === "number" && depth > defaultExpandDepth;
779
705
  const node = {
780
706
  name: part,
781
707
  kind: isLast ? "file" : "dir",
708
+ ...collapsed ? { defaultCollapsed: true } : {},
782
709
  ...isLast ? { path: p } : { children: [] }
783
710
  };
784
711
  map.set(current, node);
@@ -798,55 +725,60 @@ function buildTree(paths) {
798
725
  sortNodes(root);
799
726
  return root;
800
727
  }
801
- function scanTree(root, hasOpenspec, hasDocs) {
728
+ function scanTree(root, hasOpenspec, hasDocs, opts) {
802
729
  const tree = [];
803
- if (hasOpenspec && fs7.existsSync(path6.join(root, "openspec", "changes"))) {
804
- const changesDir = path6.join(root, "openspec", "changes");
730
+ const defaultExpandDepth = typeof opts?.defaultExpandDepth === "number" ? opts.defaultExpandDepth : 2;
731
+ if (hasOpenspec && fs6.existsSync(path5.join(root, "openspec", "changes"))) {
732
+ const changesDir = path5.join(root, "openspec", "changes");
805
733
  const active = [];
806
734
  const archived = [];
807
- for (const ent of fs7.readdirSync(changesDir, { withFileTypes: true })) {
735
+ for (const ent of fs6.readdirSync(changesDir, { withFileTypes: true })) {
808
736
  if (!ent.isDirectory() || ent.name.startsWith(".") || ent.name === "archive") continue;
809
737
  const relBase = `openspec/changes/${ent.name}`;
810
738
  const paths = [];
811
- walkDir(path6.join(changesDir, ent.name), { maxDepth: 4, onFile: (_, rel) => paths.push(rel ? `${relBase}/${rel}` : relBase) });
812
- const children = buildTree(paths).map((n) => ({ ...n, path: n.path ? `${relBase}/${n.path}` : void 0 }));
739
+ walkDir(path5.join(changesDir, ent.name), { maxDepth: 4, showHidden: opts?.showHidden, onFile: (_, rel) => paths.push(rel ? `${relBase}/${rel}` : relBase) });
740
+ const children = buildTree(paths, defaultExpandDepth).map((n) => ({ ...n, path: n.path ? `${relBase}/${n.path}` : void 0 }));
813
741
  active.push({ name: ent.name, kind: "dir", children });
814
742
  }
815
743
  active.sort((a, b) => a.name.localeCompare(b.name));
816
- const archiveDir = path6.join(changesDir, "archive");
817
- if (fs7.existsSync(archiveDir)) {
818
- for (const ent of fs7.readdirSync(archiveDir, { withFileTypes: true })) {
744
+ const archiveDir = path5.join(changesDir, "archive");
745
+ if (fs6.existsSync(archiveDir)) {
746
+ for (const ent of fs6.readdirSync(archiveDir, { withFileTypes: true })) {
819
747
  if (!ent.isDirectory() || ent.name.startsWith(".")) continue;
820
748
  const relBase = `openspec/changes/archive/${ent.name}`;
821
749
  const paths = [];
822
- walkDir(path6.join(archiveDir, ent.name), { maxDepth: 4, onFile: (_, rel) => paths.push(rel ? `${relBase}/${rel}` : relBase) });
823
- const children = buildTree(paths).map((n) => ({ ...n, path: n.path ? `${relBase}/${n.path}` : void 0 }));
750
+ walkDir(path5.join(archiveDir, ent.name), { maxDepth: 4, showHidden: opts?.showHidden, onFile: (_, rel) => paths.push(rel ? `${relBase}/${rel}` : relBase) });
751
+ const children = buildTree(paths, defaultExpandDepth).map((n) => ({ ...n, path: n.path ? `${relBase}/${n.path}` : void 0 }));
824
752
  archived.push({ name: ent.name, kind: "dir", children });
825
753
  }
826
754
  archived.sort((a, b) => b.name.localeCompare(a.name));
827
755
  }
828
756
  if (active.length) tree.push({ name: `changes (${active.length})`, kind: "dir", children: active });
829
757
  if (archived.length) tree.push({ name: `archive (${archived.length})`, kind: "dir", defaultCollapsed: true, children: archived });
830
- const specsDir = path6.join(root, "openspec", "specs");
831
- if (fs7.existsSync(specsDir)) {
758
+ const specsDir = path5.join(root, "openspec", "specs");
759
+ if (fs6.existsSync(specsDir)) {
832
760
  const paths = [];
833
- walkDir(specsDir, { maxDepth: 4, onFile: (_, rel) => paths.push(`openspec/specs/${rel}`) });
834
- const specs = buildTree(paths).map((n) => ({ ...n, path: n.path ? `openspec/specs/${n.path}` : void 0 }));
761
+ walkDir(specsDir, { maxDepth: 4, showHidden: opts?.showHidden, onFile: (_, rel) => paths.push(`openspec/specs/${rel}`) });
762
+ const specs = buildTree(paths, defaultExpandDepth).map((n) => ({ ...n, path: n.path ? `openspec/specs/${n.path}` : void 0 }));
835
763
  if (specs.length) tree.push({ name: "specs", kind: "dir", children: specs });
836
764
  }
837
765
  }
838
- if (hasDocs && fs7.existsSync(path6.join(root, "docs"))) {
766
+ if (hasDocs && fs6.existsSync(path5.join(root, "docs"))) {
839
767
  const paths = [];
840
- walkDir(path6.join(root, "docs"), { maxDepth: 4, onFile: (_, rel) => paths.push(`docs/${rel}`) });
841
- const docs = buildTree(paths).map((n) => ({ ...n, path: n.path ? `docs/${n.path}` : void 0 }));
768
+ walkDir(path5.join(root, "docs"), { maxDepth: 4, showHidden: opts?.showHidden, onFile: (_, rel) => paths.push(`docs/${rel}`) });
769
+ const docs = buildTree(paths, defaultExpandDepth).map((n) => ({ ...n, path: n.path ? `docs/${n.path}` : void 0 }));
842
770
  if (docs.length) tree.push({ name: "docs", kind: "dir", children: docs });
843
771
  }
844
- const skip = /* @__PURE__ */ new Set(["openspec", "docs", "node_modules", ".git", "dist", "playground"]);
772
+ const skip = /* @__PURE__ */ new Set(["openspec", "docs", "node_modules", ".git", "dist", "playground", ".zreview"]);
773
+ if (Array.isArray(opts?.hiddenDirs)) {
774
+ for (const d of opts.hiddenDirs) skip.add(d);
775
+ }
845
776
  const etc = [];
846
777
  try {
847
- for (const ent of fs7.readdirSync(root, { withFileTypes: true })) {
848
- if (ent.name.startsWith(".") || skip.has(ent.name)) continue;
849
- const ext = path6.extname(ent.name).toLowerCase();
778
+ for (const ent of fs6.readdirSync(root, { withFileTypes: true })) {
779
+ if (!opts?.showHidden && ent.name.startsWith(".")) continue;
780
+ if (skip.has(ent.name)) continue;
781
+ const ext = path5.extname(ent.name).toLowerCase();
850
782
  if (ent.isFile() && (ext === ".md" || ext === ".markdown")) etc.push({ name: ent.name, kind: "file", path: ent.name });
851
783
  }
852
784
  } catch (e) {
@@ -859,7 +791,7 @@ function scanTree(root, hasOpenspec, hasDocs) {
859
791
 
860
792
  // src/core/tree.ts
861
793
  var apply = {
862
- inject: ["server"],
794
+ inject: ["server", "dashboard"],
863
795
  apply(ctx, config) {
864
796
  let cached = null;
865
797
  const getDet = async () => {
@@ -874,7 +806,12 @@ var apply = {
874
806
  server.route("/__files", async (_req, res) => {
875
807
  try {
876
808
  const d = await getDet();
877
- const tree = scanTree(config.root, d.hasOpenspec, d.hasDocs);
809
+ const viewCfg = ctx.dashboard.getConfig("view");
810
+ const tree = scanTree(config.root, d.hasOpenspec, d.hasDocs, {
811
+ hiddenDirs: Array.isArray(viewCfg?.hiddenDirs) ? viewCfg.hiddenDirs : void 0,
812
+ defaultExpandDepth: typeof viewCfg?.defaultExpandDepth === "number" ? viewCfg.defaultExpandDepth : void 0,
813
+ showHidden: typeof viewCfg?.showHidden === "boolean" ? viewCfg.showHidden : void 0
814
+ });
878
815
  res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
879
816
  res.end(JSON.stringify({ tree, ...d }));
880
817
  } catch {
@@ -966,16 +903,79 @@ var apply2 = {
966
903
 
967
904
  // src/core/manifest.ts
968
905
  import { Service as Service3 } from "cordis";
906
+
907
+ // src/core/read-body.ts
908
+ async function readBody2(req) {
909
+ return new Promise((resolve) => {
910
+ let data = "";
911
+ req.on("data", (chunk) => {
912
+ data += chunk;
913
+ });
914
+ req.on("end", () => resolve(data));
915
+ req.on("error", () => resolve(""));
916
+ });
917
+ }
918
+
919
+ // src/core/manifest.ts
969
920
  var DashboardService = class extends Service3 {
970
- static inject = ["server"];
921
+ static inject = ["server", "reload"];
971
922
  plugins = /* @__PURE__ */ new Map();
923
+ reloadRef;
972
924
  constructor(ctx) {
973
925
  super(ctx, "dashboard");
974
926
  this.ctx.effect(() => () => this.plugins.clear());
927
+ this.reloadRef = ctx.reload;
975
928
  this.ctx.server.route("/__plugins", (_req, res) => {
976
929
  res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
977
930
  res.end(JSON.stringify({ plugins: Array.from(this.plugins.values()) }));
978
931
  });
932
+ this.ctx.server.route("/__plugins/config", async (req, res) => {
933
+ const out = {};
934
+ if (req.method === "GET") {
935
+ for (const m of this.plugins.values()) {
936
+ const stored = this.ctx.server.getPluginConfig(m.mode);
937
+ const schema = m.config ?? {};
938
+ const merged = {};
939
+ for (const [key, field] of Object.entries(schema)) {
940
+ merged[key] = key in stored ? stored[key] : field.default ?? "";
941
+ }
942
+ if (Object.keys(merged).length) out[m.mode] = merged;
943
+ }
944
+ res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
945
+ res.end(JSON.stringify(out));
946
+ return;
947
+ }
948
+ if (req.method === "POST") {
949
+ if (req.headers["x-stop-token"] !== this.ctx.server.stopToken) {
950
+ res.writeHead(403);
951
+ res.end("forbidden");
952
+ return;
953
+ }
954
+ try {
955
+ const body = JSON.parse(await readBody2(req));
956
+ const merged = {};
957
+ for (const mode of Object.keys(body)) {
958
+ merged[mode] = body[mode] ?? {};
959
+ }
960
+ this.ctx.server.savePluginConfig(merged);
961
+ for (const mode of Object.keys(body)) {
962
+ try {
963
+ this.reloadRef.broadcast("config", { plugin: mode });
964
+ } catch {
965
+ }
966
+ }
967
+ res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
968
+ res.end('{"ok":true}');
969
+ } catch (e) {
970
+ console.error("[config] POST error", e);
971
+ res.writeHead(400);
972
+ res.end('{"error":"bad request"}');
973
+ }
974
+ return;
975
+ }
976
+ res.writeHead(405);
977
+ res.end('{"error":"method not allowed"}');
978
+ });
979
979
  }
980
980
  register(manifest) {
981
981
  this.plugins.set(manifest.mode, manifest);
@@ -987,6 +987,9 @@ var DashboardService = class extends Service3 {
987
987
  get(mode) {
988
988
  return this.plugins.get(mode);
989
989
  }
990
+ getConfig(mode) {
991
+ return this.ctx.server.getPluginConfig(mode);
992
+ }
990
993
  };
991
994
 
992
995
  // src/server/just-runner.ts
@@ -1128,18 +1131,6 @@ var JustRunner = class {
1128
1131
  }
1129
1132
  };
1130
1133
 
1131
- // src/core/read-body.ts
1132
- async function readBody(req) {
1133
- return new Promise((resolve) => {
1134
- let data = "";
1135
- req.on("data", (chunk) => {
1136
- data += chunk;
1137
- });
1138
- req.on("end", () => resolve(data));
1139
- req.on("error", () => resolve(""));
1140
- });
1141
- }
1142
-
1143
1134
  // src/plugins/just/index.ts
1144
1135
  var apply3 = {
1145
1136
  inject: ["server", "dashboard"],
@@ -1173,7 +1164,7 @@ var apply3 = {
1173
1164
  res.end("forbidden");
1174
1165
  return;
1175
1166
  }
1176
- const body = await readBody(req);
1167
+ const body = await readBody2(req);
1177
1168
  let recipe;
1178
1169
  try {
1179
1170
  recipe = JSON.parse(body || "{}").recipe;
@@ -1222,6 +1213,96 @@ var apply3 = {
1222
1213
  }
1223
1214
  };
1224
1215
 
1216
+ // src/server/api/fetch.ts
1217
+ import ky from "ky";
1218
+
1219
+ // src/server/errors.ts
1220
+ var HttpError = class extends Error {
1221
+ constructor(status, message, body) {
1222
+ super(message);
1223
+ this.status = status;
1224
+ this.body = body;
1225
+ this.name = "HttpError";
1226
+ }
1227
+ status;
1228
+ body;
1229
+ };
1230
+ var NetworkError = class extends Error {
1231
+ constructor(message, cause) {
1232
+ super(message);
1233
+ this.cause = cause;
1234
+ this.name = "NetworkError";
1235
+ }
1236
+ cause;
1237
+ };
1238
+
1239
+ // src/server/api/fetch.ts
1240
+ async function fetchJson(url, init) {
1241
+ try {
1242
+ const res = await ky(url, { ...init, timeout: 8e3, retry: 2 });
1243
+ return await res.json();
1244
+ } catch (e) {
1245
+ if (e instanceof HttpError) throw e;
1246
+ if (e instanceof Error && e.name === "TimeoutError") {
1247
+ throw new NetworkError(`\u8BF7\u6C42\u8D85\u65F6: ${url}`);
1248
+ }
1249
+ if (e instanceof Error && e.name === "HTTPError") {
1250
+ throw new HttpError(e.status ?? 500, e.message);
1251
+ }
1252
+ throw new NetworkError(`\u8BF7\u6C42\u5931\u8D25: ${url}`, e);
1253
+ }
1254
+ }
1255
+
1256
+ // src/server/bugs.ts
1257
+ var tokenCache = null;
1258
+ async function getToken(cfg) {
1259
+ if (cfg.token) return cfg.token;
1260
+ const key = `${cfg.url}|${cfg.account}`;
1261
+ if (tokenCache && tokenCache.key === key && Date.now() - tokenCache.at < 10 * 6e4) return tokenCache.token;
1262
+ const json = await fetchJson(`${cfg.url}/api.php/v1/tokens`, {
1263
+ method: "POST",
1264
+ headers: { "Content-Type": "application/json" },
1265
+ body: JSON.stringify({ account: cfg.account, password: "" })
1266
+ });
1267
+ const token = typeof json.token === "string" ? json.token : "";
1268
+ if (!token) throw new Error("token \u83B7\u53D6\u5931\u8D25:\u68C0\u67E5 account / password");
1269
+ tokenCache = { key, token, at: Date.now() };
1270
+ return token;
1271
+ }
1272
+ function normBug(b, account) {
1273
+ const assigned = b.assignedTo;
1274
+ const assignedTo = typeof assigned === "string" ? assigned : assigned && typeof assigned === "object" && "realname" in assigned ? String(assigned.realname ?? "") : "";
1275
+ const assignedAccount = typeof assigned === "string" ? assigned : assigned && typeof assigned === "object" ? String(assigned.account ?? "") : "";
1276
+ const mine = !!account && (assignedAccount === account || assignedTo === account);
1277
+ return {
1278
+ id: Number(b.id),
1279
+ title: String(b.title ?? ""),
1280
+ severity: b.severity ?? 4,
1281
+ pri: b.pri ?? 3,
1282
+ status: String(b.status ?? ""),
1283
+ assignedTo,
1284
+ openedBy: typeof b.openedBy === "string" ? b.openedBy : void 0,
1285
+ mine
1286
+ };
1287
+ }
1288
+ async function fetchBugs(config) {
1289
+ if (!config.url || !config.product) {
1290
+ return { ok: false, error: "\u914D\u7F6E\u65E0\u6548\uFF1A\u8BF7\u8BBE\u7F6E\u670D\u52A1\u5668 URL \u548C\u4EA7\u54C1 ID" };
1291
+ }
1292
+ try {
1293
+ const token = await getToken(config);
1294
+ const json = await fetchJson(
1295
+ `${config.url}/api.php/v1/products/${config.product}/bugs?page=1&limit=100`,
1296
+ { headers: { Token: token } }
1297
+ );
1298
+ const raw = Array.isArray(json.bugs) ? json.bugs : [];
1299
+ return { ok: true, url: config.url, total: Number(json.total ?? raw.length), bugs: raw.map((b) => normBug(b, config.account)) };
1300
+ } catch (e) {
1301
+ const msg = e instanceof Error ? e.message : String(e);
1302
+ 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` };
1303
+ }
1304
+ }
1305
+
1225
1306
  // src/plugins/bugs/index.ts
1226
1307
  var apply4 = {
1227
1308
  inject: ["server", "dashboard"],
@@ -1229,11 +1310,28 @@ var apply4 = {
1229
1310
  const root = config.root;
1230
1311
  ctx.inject(["server"], () => {
1231
1312
  if (!ctx.server?.route) return;
1232
- ctx.dashboard.register({ mode: "bugs", label: "\u7985\u9053 Bugs", icon: "\u{1F3AF}", description: "\u53EA\u8BFB bug \u5217\u8868" });
1313
+ ctx.dashboard.register({
1314
+ mode: "bugs",
1315
+ label: "\u7985\u9053 Bugs",
1316
+ icon: "\u{1F3AF}",
1317
+ description: "\u53EA\u8BFB bug \u5217\u8868",
1318
+ config: {
1319
+ url: { type: "string", label: "\u670D\u52A1\u5668 URL", default: "" },
1320
+ account: { type: "string", label: "\u8D26\u53F7", default: "" },
1321
+ token: { type: "string", label: "Token", default: "" },
1322
+ product: { type: "number", label: "\u4EA7\u54C1 ID", default: 0 }
1323
+ }
1324
+ });
1233
1325
  ctx.server.route("/__bugs", async (_req, res) => {
1234
1326
  res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
1235
1327
  try {
1236
- const result = await fetchBugs(root);
1328
+ const bugConfig = ctx.dashboard.getConfig("bugs");
1329
+ const result = await fetchBugs({
1330
+ url: String(bugConfig?.url ?? ""),
1331
+ account: String(bugConfig?.account ?? ""),
1332
+ token: String(bugConfig?.token ?? ""),
1333
+ product: Number(bugConfig?.product ?? 0)
1334
+ });
1237
1335
  res.end(JSON.stringify(result));
1238
1336
  } catch (e) {
1239
1337
  res.end(JSON.stringify({ ok: false, error: e instanceof Error ? e.message : "unknown error" }));
@@ -1244,65 +1342,151 @@ var apply4 = {
1244
1342
  };
1245
1343
 
1246
1344
  // src/server/review-store.ts
1247
- import fs8 from "fs";
1248
- import path7 from "path";
1249
- import YAML2 from "yaml";
1250
- var REVIEW_CANDIDATES = [".zdev/review.yaml", "review.yaml"];
1345
+ import fs7 from "fs";
1346
+ import path6 from "path";
1347
+ import YAML from "yaml";
1348
+ var REVIEW_DIR = ".zreview";
1349
+ var REVIEW_FILE = path6.join(REVIEW_DIR, "review.yaml");
1350
+ var DOCS_DIR = path6.join(REVIEW_DIR, "docs");
1351
+ var DIAGRAMS_DIR = path6.join(REVIEW_DIR, "diagrams");
1251
1352
  var ReviewStore = class {
1252
1353
  root;
1253
- file;
1254
1354
  onChange;
1255
1355
  constructor(root, onChange) {
1256
1356
  this.root = root;
1257
1357
  this.onChange = onChange;
1258
- const existing = REVIEW_CANDIDATES.find((rel) => fs8.existsSync(path7.join(root, rel)));
1259
- this.file = path7.join(root, existing ?? REVIEW_CANDIDATES[0]);
1260
1358
  }
1261
1359
  exists() {
1262
- return fs8.existsSync(this.file);
1360
+ return fs7.existsSync(path6.join(this.root, REVIEW_FILE));
1263
1361
  }
1264
1362
  read() {
1363
+ const fullPath = path6.join(this.root, REVIEW_FILE);
1364
+ if (!fs7.existsSync(fullPath)) {
1365
+ return { status: "draft", summary: "", documents: [], codebases: [], diagrams: [], items: [] };
1366
+ }
1265
1367
  try {
1266
- const parsed = YAML2.parse(fs8.readFileSync(this.file, "utf8"));
1267
- if (!parsed || !Array.isArray(parsed.items)) return { status: "draft", items: [] };
1268
- return parsed;
1368
+ const parsed = YAML.parse(fs7.readFileSync(fullPath, "utf8"));
1369
+ if (!parsed || !Array.isArray(parsed.items)) {
1370
+ return { status: "draft", summary: parsed?.summary ?? "", documents: parsed?.documents ?? [], codebases: parsed?.codebases ?? [], diagrams: parsed?.diagrams ?? [], items: [] };
1371
+ }
1372
+ return {
1373
+ status: parsed.status ?? "draft",
1374
+ summary: parsed.summary ?? "",
1375
+ documents: parsed.documents ?? [],
1376
+ codebases: parsed.codebases ?? [],
1377
+ diagrams: parsed.diagrams ?? [],
1378
+ items: parsed.items
1379
+ };
1269
1380
  } catch {
1270
- return { status: "draft", items: [] };
1381
+ return { status: "draft", summary: "", documents: [], codebases: [], diagrams: [], items: [] };
1271
1382
  }
1272
1383
  }
1273
1384
  write(data) {
1274
- fs8.mkdirSync(path7.dirname(this.file), { recursive: true });
1275
- fs8.writeFileSync(this.file, YAML2.stringify(data), "utf8");
1385
+ fs7.mkdirSync(path6.join(this.root, REVIEW_DIR), { recursive: true });
1386
+ fs7.writeFileSync(path6.join(this.root, REVIEW_FILE), YAML.stringify(data), "utf8");
1276
1387
  this.onChange?.();
1277
1388
  }
1278
1389
  updateItem(id, patch) {
1279
1390
  const data = this.read();
1280
- const item = data.items.find((i) => i.id === id);
1391
+ const item = this.findItem(data.items, id);
1281
1392
  if (!item) throw new Error(`item ${id} not found`);
1282
1393
  if (patch.answer !== void 0) item.answer = patch.answer;
1283
1394
  if (patch.state !== void 0) item.state = patch.state;
1395
+ if (patch.priority !== void 0) item.priority = patch.priority;
1396
+ if (patch.title !== void 0) item.title = patch.title;
1284
1397
  if (patch.state === "answered" && !patch.answer && !item.answer) item.answer = "";
1285
1398
  this.write(data);
1286
1399
  return data;
1287
1400
  }
1401
+ addChild(parentId, title, priority = "medium") {
1402
+ const data = this.read();
1403
+ const parent = this.findItem(data.items, parentId);
1404
+ if (!parent) throw new Error(`parent ${parentId} not found`);
1405
+ if (!parent.children) parent.children = [];
1406
+ const newId = `${parentId}-${parent.children.length + 1}`;
1407
+ parent.children.push({
1408
+ id: newId,
1409
+ type: "decomposition",
1410
+ state: "open",
1411
+ title,
1412
+ priority
1413
+ });
1414
+ this.write(data);
1415
+ return data;
1416
+ }
1417
+ removeItem(id) {
1418
+ const data = this.read();
1419
+ const removed = this.removeItemFromList(data.items, id);
1420
+ if (!removed) throw new Error(`item ${id} not found`);
1421
+ this.write(data);
1422
+ return data;
1423
+ }
1288
1424
  setStatus(status) {
1289
1425
  const data = this.read();
1290
- if (status === "passed" && data.items.some((i) => i.state === "open")) {
1426
+ if (status === "passed" && this.hasOpenItems(data.items)) {
1291
1427
  throw new Error("\u5B58\u5728\u672A\u5904\u7406\u7684\u8BC4\u5BA1\u9879(open),\u4E0D\u80FD\u901A\u8FC7");
1292
1428
  }
1293
1429
  data.status = status;
1294
1430
  this.write(data);
1295
1431
  return data;
1296
1432
  }
1297
- docs() {
1433
+ listDocs() {
1434
+ const docsDir = path6.join(this.root, DOCS_DIR);
1435
+ if (!fs7.existsSync(docsDir)) return [];
1298
1436
  try {
1299
- const zdevDir = path7.join(this.root, ".zdev");
1300
- if (!fs8.existsSync(zdevDir) || !fs8.statSync(zdevDir).isDirectory()) return [];
1301
- return fs8.readdirSync(zdevDir).filter((f) => /\.(md|markdown)$/i.test(f) && fs8.statSync(path7.join(zdevDir, f)).isFile()).sort();
1437
+ return fs7.readdirSync(docsDir).filter((f) => /\.(md|markdown|txt)$/i.test(f)).sort();
1302
1438
  } catch {
1303
1439
  return [];
1304
1440
  }
1305
1441
  }
1442
+ readDoc(name) {
1443
+ const fullPath = path6.join(this.root, DOCS_DIR, name);
1444
+ if (!fs7.existsSync(fullPath)) throw new Error(`doc ${name} not found`);
1445
+ return fs7.readFileSync(fullPath, "utf8");
1446
+ }
1447
+ listDiagrams() {
1448
+ const dir = path6.join(this.root, DIAGRAMS_DIR);
1449
+ if (!fs7.existsSync(dir)) return [];
1450
+ try {
1451
+ return fs7.readdirSync(dir).filter((f) => /\.html$/i.test(f)).sort();
1452
+ } catch {
1453
+ return [];
1454
+ }
1455
+ }
1456
+ readDiagram(name) {
1457
+ const fullPath = path6.join(this.root, DIAGRAMS_DIR, name);
1458
+ if (!fs7.existsSync(fullPath)) throw new Error(`diagram ${name} not found`);
1459
+ return fs7.readFileSync(fullPath, "utf8");
1460
+ }
1461
+ findItem(items, id) {
1462
+ for (const item of items) {
1463
+ if (item.id === id) return item;
1464
+ if (item.children) {
1465
+ const found = this.findItem(item.children, id);
1466
+ if (found) return found;
1467
+ }
1468
+ }
1469
+ return null;
1470
+ }
1471
+ removeItemFromList(items, id) {
1472
+ for (let i = 0; i < items.length; i++) {
1473
+ if (items[i].id === id) {
1474
+ items.splice(i, 1);
1475
+ return true;
1476
+ }
1477
+ if (items[i].children && this.removeItemFromList(items[i].children, id)) {
1478
+ return true;
1479
+ }
1480
+ }
1481
+ return false;
1482
+ }
1483
+ hasOpenItems(items) {
1484
+ for (const item of items) {
1485
+ if (item.state === "open") return true;
1486
+ if (item.children && this.hasOpenItems(item.children)) return true;
1487
+ }
1488
+ return false;
1489
+ }
1306
1490
  };
1307
1491
 
1308
1492
  // src/plugins/review/index.ts
@@ -1312,7 +1496,7 @@ var apply5 = {
1312
1496
  const root = config.root;
1313
1497
  ctx.inject(["server"], () => {
1314
1498
  if (!ctx.server?.route) return;
1315
- ctx.dashboard.register({ mode: "review", label: "\u6587\u6863\u8BC4\u5BA1", icon: "\u2705", description: "review.yaml \u9010\u9879\u5BF9\u9F50" });
1499
+ ctx.dashboard.register({ mode: "review", label: "\u6587\u6863\u8BC4\u5BA1", icon: "\u2705", description: "\u591A\u6587\u6863\u9700\u6C42\u8BC4\u5BA1\u4E0E\u62C6\u89E3 \xB7 zreview" });
1316
1500
  const reviewStore = new ReviewStore(root, () => {
1317
1501
  ctx.reload.broadcast("files");
1318
1502
  });
@@ -1327,8 +1511,45 @@ var apply5 = {
1327
1511
  return;
1328
1512
  }
1329
1513
  try {
1330
- const body = JSON.parse(await readBody(req) || "{}");
1331
- const data = reviewStore.updateItem(body.id, { answer: body.answer, state: body.state });
1514
+ const body = JSON.parse(await readBody2(req) || "{}");
1515
+ const data = reviewStore.updateItem(body.id, {
1516
+ answer: body.answer,
1517
+ state: body.state,
1518
+ priority: body.priority,
1519
+ title: body.title
1520
+ });
1521
+ res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
1522
+ res.end(JSON.stringify(data));
1523
+ } catch (e) {
1524
+ res.writeHead(400, { "Content-Type": "application/json; charset=utf-8" });
1525
+ res.end(JSON.stringify({ error: e instanceof Error ? e.message : "unknown error" }));
1526
+ }
1527
+ });
1528
+ ctx.server.route("/__review/item/add-child", async (req, res) => {
1529
+ if (req.headers["x-stop-token"] !== ctx.server.stopToken) {
1530
+ res.writeHead(403);
1531
+ res.end("forbidden");
1532
+ return;
1533
+ }
1534
+ try {
1535
+ const body = JSON.parse(await readBody2(req) || "{}");
1536
+ const data = reviewStore.addChild(body.parentId, body.title, body.priority);
1537
+ res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
1538
+ res.end(JSON.stringify(data));
1539
+ } catch (e) {
1540
+ res.writeHead(400, { "Content-Type": "application/json; charset=utf-8" });
1541
+ res.end(JSON.stringify({ error: e instanceof Error ? e.message : "unknown error" }));
1542
+ }
1543
+ });
1544
+ ctx.server.route("/__review/item/remove", async (req, res) => {
1545
+ if (req.headers["x-stop-token"] !== ctx.server.stopToken) {
1546
+ res.writeHead(403);
1547
+ res.end("forbidden");
1548
+ return;
1549
+ }
1550
+ try {
1551
+ const body = JSON.parse(await readBody2(req) || "{}");
1552
+ const data = reviewStore.removeItem(body.id);
1332
1553
  res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
1333
1554
  res.end(JSON.stringify(data));
1334
1555
  } catch (e) {
@@ -1343,7 +1564,7 @@ var apply5 = {
1343
1564
  return;
1344
1565
  }
1345
1566
  try {
1346
- const body = JSON.parse(await readBody(req) || "{}");
1567
+ const body = JSON.parse(await readBody2(req) || "{}");
1347
1568
  const data = reviewStore.setStatus(body.status);
1348
1569
  res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
1349
1570
  res.end(JSON.stringify(data));
@@ -1354,15 +1575,27 @@ var apply5 = {
1354
1575
  });
1355
1576
  ctx.server.route("/__docs", async (_req, res) => {
1356
1577
  res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
1357
- res.end(JSON.stringify(reviewStore.docs()));
1578
+ res.end(JSON.stringify(reviewStore.listDocs()));
1579
+ });
1580
+ ctx.server.route("/__docs/:name", async (req, res) => {
1581
+ try {
1582
+ const name = req.params?.name;
1583
+ if (!name) throw new Error("missing doc name");
1584
+ const content = reviewStore.readDoc(name);
1585
+ res.writeHead(200, { "Content-Type": "text/markdown; charset=utf-8", "Cache-Control": "no-cache" });
1586
+ res.end(content);
1587
+ } catch (e) {
1588
+ res.writeHead(404, { "Content-Type": "application/json; charset=utf-8" });
1589
+ res.end(JSON.stringify({ error: e instanceof Error ? e.message : "not found" }));
1590
+ }
1358
1591
  });
1359
1592
  });
1360
1593
  }
1361
1594
  };
1362
1595
 
1363
1596
  // src/plugins/apply/scan.ts
1364
- import fs9 from "fs";
1365
- import path8 from "path";
1597
+ import fs8 from "fs";
1598
+ import path7 from "path";
1366
1599
 
1367
1600
  // src/plugins/apply/parse-tasks.ts
1368
1601
  var TASK_RE = /^\s*-\s*\[([ xX])\]\s*(.*)$/;
@@ -1380,16 +1613,16 @@ function countTasks(md) {
1380
1613
  // src/plugins/apply/scan.ts
1381
1614
  function readText(p) {
1382
1615
  try {
1383
- return fs9.readFileSync(p, "utf8");
1616
+ return fs8.readFileSync(p, "utf8");
1384
1617
  } catch {
1385
1618
  return "";
1386
1619
  }
1387
1620
  }
1388
1621
  function worktreeDir(root, name) {
1389
- return path8.join(root, ".zworktree", name);
1622
+ return path7.join(root, ".zworktree", name);
1390
1623
  }
1391
1624
  function changeDir(root, name) {
1392
- return path8.join(root, "openspec", "changes", name);
1625
+ return path7.join(root, "openspec", "changes", name);
1393
1626
  }
1394
1627
  function parseDependsOn(proposal) {
1395
1628
  const deps = [];
@@ -1403,42 +1636,42 @@ function parseDependsOn(proposal) {
1403
1636
  return deps;
1404
1637
  }
1405
1638
  function scanApplyChanges(root) {
1406
- const changesDir = path8.join(root, "openspec", "changes");
1407
- if (!fs9.existsSync(changesDir)) return [];
1639
+ const changesDir = path7.join(root, "openspec", "changes");
1640
+ if (!fs8.existsSync(changesDir)) return [];
1408
1641
  const out = [];
1409
- for (const ent of fs9.readdirSync(changesDir, { withFileTypes: true })) {
1642
+ for (const ent of fs8.readdirSync(changesDir, { withFileTypes: true })) {
1410
1643
  if (!ent.isDirectory() || ent.name.startsWith(".") || ent.name === "archive") continue;
1411
1644
  const wt = worktreeDir(root, ent.name);
1412
- const tasks = readText(path8.join(wt, "openspec", "changes", ent.name, "tasks.md"));
1645
+ const tasks = readText(path7.join(wt, "openspec", "changes", ent.name, "tasks.md"));
1413
1646
  if (!tasks) {
1414
- const fallback = readText(path8.join(changesDir, ent.name, "tasks.md"));
1647
+ const fallback = readText(path7.join(changesDir, ent.name, "tasks.md"));
1415
1648
  if (fallback) {
1416
1649
  const { total, done } = countTasks(fallback);
1417
- const wtChange = path8.join(wt, "openspec", "changes", ent.name);
1418
- const mainChange = path8.join(changesDir, ent.name);
1650
+ const wtChange = path7.join(wt, "openspec", "changes", ent.name);
1651
+ const mainChange = path7.join(changesDir, ent.name);
1419
1652
  out.push({
1420
1653
  name: ent.name,
1421
1654
  path: `openspec/changes/${ent.name}`,
1422
1655
  total,
1423
1656
  done,
1424
- hasProposal: fs9.existsSync(path8.join(wtChange, "proposal.md")) || fs9.existsSync(path8.join(mainChange, "proposal.md")),
1425
- hasDesign: fs9.existsSync(path8.join(wtChange, "design.md")) || fs9.existsSync(path8.join(mainChange, "design.md")),
1657
+ hasProposal: fs8.existsSync(path7.join(wtChange, "proposal.md")) || fs8.existsSync(path7.join(mainChange, "proposal.md")),
1658
+ hasDesign: fs8.existsSync(path7.join(wtChange, "design.md")) || fs8.existsSync(path7.join(mainChange, "design.md")),
1426
1659
  // worktree 目录存在即算执行中(tasks.md 可能尚未落盘)
1427
- inWorktree: fs9.existsSync(wtChange)
1660
+ inWorktree: fs8.existsSync(wtChange)
1428
1661
  });
1429
1662
  }
1430
1663
  } else {
1431
1664
  const { total, done } = countTasks(tasks);
1432
- const wtChange = path8.join(wt, "openspec", "changes", ent.name);
1433
- const mainChange = path8.join(changesDir, ent.name);
1665
+ const wtChange = path7.join(wt, "openspec", "changes", ent.name);
1666
+ const mainChange = path7.join(changesDir, ent.name);
1434
1667
  out.push({
1435
1668
  name: ent.name,
1436
1669
  path: `openspec/changes/${ent.name}`,
1437
1670
  total,
1438
1671
  done,
1439
1672
  // 任一侧存在即算(详情读取是 worktree 优先、主目录兜底,列表标记保持一致)
1440
- hasProposal: fs9.existsSync(path8.join(wtChange, "proposal.md")) || fs9.existsSync(path8.join(mainChange, "proposal.md")),
1441
- hasDesign: fs9.existsSync(path8.join(wtChange, "design.md")) || fs9.existsSync(path8.join(mainChange, "design.md")),
1673
+ hasProposal: fs8.existsSync(path7.join(wtChange, "proposal.md")) || fs8.existsSync(path7.join(mainChange, "proposal.md")),
1674
+ hasDesign: fs8.existsSync(path7.join(wtChange, "design.md")) || fs8.existsSync(path7.join(mainChange, "design.md")),
1442
1675
  inWorktree: true
1443
1676
  });
1444
1677
  }
@@ -1449,14 +1682,14 @@ function scanApplyChanges(root) {
1449
1682
  function readApplyChange(root, name) {
1450
1683
  const wt = worktreeDir(root, name);
1451
1684
  const mainChangeDir = changeDir(root, name);
1452
- const proposalFrom = fs9.existsSync(path8.join(wt, "openspec", "changes", name, "proposal.md")) ? path8.join(wt, "openspec", "changes", name, "proposal.md") : path8.join(mainChangeDir, "proposal.md");
1453
- const designFrom = fs9.existsSync(path8.join(wt, "openspec", "changes", name, "design.md")) ? path8.join(wt, "openspec", "changes", name, "design.md") : path8.join(mainChangeDir, "design.md");
1454
- const tasksFrom = fs9.existsSync(path8.join(wt, "openspec", "changes", name, "tasks.md")) ? path8.join(wt, "openspec", "changes", name, "tasks.md") : path8.join(mainChangeDir, "tasks.md");
1685
+ const proposalFrom = fs8.existsSync(path7.join(wt, "openspec", "changes", name, "proposal.md")) ? path7.join(wt, "openspec", "changes", name, "proposal.md") : path7.join(mainChangeDir, "proposal.md");
1686
+ const designFrom = fs8.existsSync(path7.join(wt, "openspec", "changes", name, "design.md")) ? path7.join(wt, "openspec", "changes", name, "design.md") : path7.join(mainChangeDir, "design.md");
1687
+ const tasksFrom = fs8.existsSync(path7.join(wt, "openspec", "changes", name, "tasks.md")) ? path7.join(wt, "openspec", "changes", name, "tasks.md") : path7.join(mainChangeDir, "tasks.md");
1455
1688
  const proposal = readText(proposalFrom);
1456
1689
  const design = readText(designFrom);
1457
1690
  const tasks = readText(tasksFrom);
1458
1691
  const { total, done } = countTasks(tasks);
1459
- const inWorktree = fs9.existsSync(path8.join(wt, "openspec", "changes", name));
1692
+ const inWorktree = fs8.existsSync(path7.join(wt, "openspec", "changes", name));
1460
1693
  const dependsOn = parseDependsOn(proposal);
1461
1694
  const hasTestStrategy = /^##\s+测试策略\s*$/m.test(design);
1462
1695
  return {
@@ -1513,8 +1746,12 @@ var apply6 = {
1513
1746
  }
1514
1747
  };
1515
1748
 
1516
- // src/server/design-assets.ts
1749
+ // src/plugins/design/index.ts
1517
1750
  import path9 from "path";
1751
+ import fs9 from "fs";
1752
+
1753
+ // src/server/design-assets.ts
1754
+ import path8 from "path";
1518
1755
  var PAGE_EXTS = [".html", ".htm"];
1519
1756
  var ICON_EXTS = [".svg", ".png", ".ico", ".jpg", ".jpeg", ".gif", ".webp"];
1520
1757
  var VIDEO_EXTS = [".mp4", ".webm", ".mov", ".ogg", ".ogv"];
@@ -1537,30 +1774,62 @@ function categorize(rel, ext) {
1537
1774
  }
1538
1775
  return null;
1539
1776
  }
1540
- function scanAssets(root) {
1777
+ function scanAssets(root, subDir = "") {
1541
1778
  const out = {};
1542
1779
  const keys = ["page", "component", "icon", "token", "md", "video", "audio", "pdf", "font"];
1543
1780
  for (const k of keys) out[k] = [];
1544
1781
  const SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "coverage", ".next", ".cache"]);
1545
- walkDir(root, { skip: SKIP, onFile: (abs, rel) => {
1546
- const ext = path9.extname(abs).toLowerCase();
1782
+ const scanRoot = subDir ? path8.join(root, subDir) : root;
1783
+ walkDir(scanRoot, { skip: SKIP, onFile: (abs, rel) => {
1784
+ const ext = path8.extname(abs).toLowerCase();
1547
1785
  const t = categorize(rel, ext);
1548
- if (t) out[t].push({ path: rel, name: path9.basename(abs), ext, type: t });
1786
+ if (t) out[t].push({ path: rel, name: path8.basename(abs), ext, type: t });
1549
1787
  } });
1550
1788
  return out;
1551
1789
  }
1552
1790
 
1553
1791
  // src/plugins/design/index.ts
1792
+ var ASSET_KEYS = ["page", "component", "icon", "token", "md", "video", "audio", "pdf", "font"];
1793
+ function mergeScanResults(a, b) {
1794
+ const out = {};
1795
+ for (const k of ASSET_KEYS) {
1796
+ out[k] = [...a[k], ...b[k]];
1797
+ }
1798
+ return out;
1799
+ }
1554
1800
  var apply7 = {
1555
1801
  inject: ["server", "dashboard"],
1556
1802
  apply(ctx, config) {
1557
1803
  const root = config.root;
1558
1804
  ctx.inject(["server"], () => {
1559
1805
  if (!ctx.server?.route) return;
1560
- 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" });
1806
+ ctx.dashboard.register({
1807
+ mode: "design",
1808
+ label: "\u8BBE\u8BA1\u8D44\u4EA7",
1809
+ icon: "\u{1F3A8}",
1810
+ description: "\u9875\u9762/\u7EC4\u4EF6/\u56FE\u6807/Token \u5206\u7C7B\u9884\u89C8",
1811
+ config: {
1812
+ folders: { type: "string[]", label: "\u626B\u63CF\u6587\u4EF6\u5939", default: [] }
1813
+ }
1814
+ });
1561
1815
  ctx.server.route("/__design/assets", async (_req, res) => {
1562
1816
  res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
1563
- res.end(JSON.stringify(scanAssets(root)));
1817
+ const designConfig = ctx.dashboard.getConfig("design");
1818
+ const folders = Array.isArray(designConfig?.folders) ? designConfig.folders : [];
1819
+ if (folders.length === 0) {
1820
+ res.end(JSON.stringify(scanAssets(root)));
1821
+ return;
1822
+ }
1823
+ let merged = {};
1824
+ for (const k of ASSET_KEYS) merged[k] = [];
1825
+ for (const folder of folders) {
1826
+ const folderPath = path9.resolve(root, folder);
1827
+ if (fs9.existsSync(folderPath) && fs9.statSync(folderPath).isDirectory()) {
1828
+ const result = scanAssets(folderPath);
1829
+ merged = mergeScanResults(merged, result);
1830
+ }
1831
+ }
1832
+ res.end(JSON.stringify(merged));
1564
1833
  });
1565
1834
  });
1566
1835
  }
@@ -1570,7 +1839,17 @@ var apply7 = {
1570
1839
  var apply8 = {
1571
1840
  inject: ["dashboard"],
1572
1841
  apply(ctx) {
1573
- ctx.dashboard.register({ mode: "view", label: "\u9879\u76EE\u6D4F\u89C8", icon: "\u{1F441}\uFE0F", description: "openspec / docs / \u6587\u6863\u9884\u89C8" });
1842
+ ctx.dashboard.register({
1843
+ mode: "view",
1844
+ label: "\u9879\u76EE\u6D4F\u89C8",
1845
+ icon: "\u{1F441}\uFE0F",
1846
+ description: "openspec / docs / \u6587\u6863\u9884\u89C8",
1847
+ config: {
1848
+ hiddenDirs: { type: "string[]", label: "\u9690\u85CF\u76EE\u5F55", default: [".git", "node_modules", "dist", "build"] },
1849
+ defaultExpandDepth: { type: "number", label: "\u9ED8\u8BA4\u5C55\u5F00\u6DF1\u5EA6", default: 2 },
1850
+ showHidden: { type: "boolean", label: "\u663E\u793A\u9690\u85CF\u6587\u4EF6", default: false }
1851
+ }
1852
+ });
1574
1853
  }
1575
1854
  };
1576
1855
 
@@ -1641,8 +1920,250 @@ var apply9 = {
1641
1920
  }
1642
1921
  };
1643
1922
 
1923
+ // src/server/apply-batch-store.ts
1924
+ import fs11 from "fs";
1925
+ import path11 from "path";
1926
+ var DEFAULT_STATE = {
1927
+ version: "1",
1928
+ status: "idle",
1929
+ changes: [],
1930
+ batches: [],
1931
+ currentBatchIndex: -1,
1932
+ parallelism: 2,
1933
+ logs: [],
1934
+ conflicts: []
1935
+ };
1936
+ var ApplyBatchStore = class {
1937
+ root;
1938
+ statePath;
1939
+ state;
1940
+ listeners = /* @__PURE__ */ new Set();
1941
+ constructor(root, onBroadcast) {
1942
+ this.root = root;
1943
+ this.statePath = path11.join(root, ".zapply", "batch-state.json");
1944
+ this.state = this.load();
1945
+ }
1946
+ load() {
1947
+ try {
1948
+ if (fs11.existsSync(this.statePath)) {
1949
+ return JSON.parse(fs11.readFileSync(this.statePath, "utf8"));
1950
+ }
1951
+ } catch {
1952
+ }
1953
+ return { ...DEFAULT_STATE, createdAt: (/* @__PURE__ */ new Date()).toISOString(), updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
1954
+ }
1955
+ save() {
1956
+ this.state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1957
+ fs11.mkdirSync(path11.dirname(this.statePath), { recursive: true });
1958
+ fs11.writeFileSync(this.statePath, JSON.stringify(this.state, null, 2));
1959
+ this.listeners.forEach((fn) => fn());
1960
+ }
1961
+ subscribe(fn) {
1962
+ this.listeners.add(fn);
1963
+ return () => this.listeners.delete(fn);
1964
+ }
1965
+ read() {
1966
+ return this.state;
1967
+ }
1968
+ updateStatus(status) {
1969
+ this.state.status = status;
1970
+ this.save();
1971
+ }
1972
+ updateChanges(changes) {
1973
+ this.state.changes = changes;
1974
+ this.save();
1975
+ }
1976
+ updateBatches(batches) {
1977
+ this.state.batches = batches;
1978
+ this.save();
1979
+ }
1980
+ updateParallelism(parallelism) {
1981
+ this.state.parallelism = Math.max(1, Math.min(8, parallelism));
1982
+ this.save();
1983
+ }
1984
+ addLog(log) {
1985
+ this.state.logs.push(log);
1986
+ if (this.state.logs.length > 1e3) {
1987
+ this.state.logs = this.state.logs.slice(-1e3);
1988
+ }
1989
+ this.save();
1990
+ }
1991
+ updateConflicts(conflicts) {
1992
+ this.state.conflicts = conflicts;
1993
+ this.save();
1994
+ }
1995
+ updateChange(name, patch) {
1996
+ const idx = this.state.changes.findIndex((c) => c.name === name);
1997
+ if (idx !== -1) {
1998
+ this.state.changes[idx] = { ...this.state.changes[idx], ...patch };
1999
+ this.save();
2000
+ }
2001
+ }
2002
+ updateCheckpoint(name, checkpoint) {
2003
+ const idx = this.state.changes.findIndex((c) => c.name === name);
2004
+ if (idx !== -1) {
2005
+ this.state.changes[idx].checkpoint = checkpoint;
2006
+ this.save();
2007
+ }
2008
+ }
2009
+ reset() {
2010
+ this.state = { ...DEFAULT_STATE, createdAt: (/* @__PURE__ */ new Date()).toISOString(), updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
2011
+ this.save();
2012
+ }
2013
+ };
2014
+
2015
+ // src/plugins/apply-batch/index.ts
2016
+ var apply10 = {
2017
+ inject: ["server", "dashboard", "reload"],
2018
+ apply(ctx, config) {
2019
+ const root = config.root;
2020
+ ctx.inject(["server"], () => {
2021
+ if (!ctx.server?.route) return;
2022
+ ctx.dashboard.register({
2023
+ mode: "apply-batch",
2024
+ label: "\u6279\u91CF\u6267\u884C",
2025
+ icon: "\u26A1",
2026
+ description: "zapply \u6279\u91CF\u5E76\u884C\u6267\u884C\u9A7E\u9A76\u8231"
2027
+ });
2028
+ const store = new ApplyBatchStore(root, () => {
2029
+ try {
2030
+ ctx.reload.broadcast("files");
2031
+ } catch {
2032
+ }
2033
+ });
2034
+ ctx.server.route("/__apply-batch", async (_req, res) => {
2035
+ res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
2036
+ res.end(JSON.stringify(store.read()));
2037
+ });
2038
+ ctx.server.route("/__apply-batch/status", async (req, res) => {
2039
+ try {
2040
+ const body = JSON.parse(await readBody(req) || "{}");
2041
+ store.updateStatus(body.status);
2042
+ res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
2043
+ res.end(JSON.stringify(store.read()));
2044
+ } catch (e) {
2045
+ res.writeHead(400, { "Content-Type": "application/json; charset=utf-8" });
2046
+ res.end(JSON.stringify({ error: e instanceof Error ? e.message : "unknown error" }));
2047
+ }
2048
+ });
2049
+ ctx.server.route("/__apply-batch/changes", async (_req, res) => {
2050
+ res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
2051
+ res.end(JSON.stringify(store.read().changes));
2052
+ });
2053
+ ctx.server.route("/__apply-batch/graph", async (_req, res) => {
2054
+ const state = store.read();
2055
+ const graph = {
2056
+ changes: state.changes.map((c) => ({
2057
+ name: c.name,
2058
+ status: c.status,
2059
+ dependencies: c.dependencies,
2060
+ batchIndex: c.batchIndex
2061
+ })),
2062
+ batches: state.batches,
2063
+ conflicts: state.conflicts
2064
+ };
2065
+ res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
2066
+ res.end(JSON.stringify(graph));
2067
+ });
2068
+ ctx.server.route("/__apply-batch/logs", async (_req, res) => {
2069
+ res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
2070
+ res.end(JSON.stringify(store.read().logs.slice(-100)));
2071
+ });
2072
+ ctx.server.route("/__apply-batch/approve", async (req, res) => {
2073
+ try {
2074
+ const body = JSON.parse(await readBody(req) || "{}");
2075
+ store.updateStatus("running");
2076
+ store.updateParallelism(body.parallelism ?? 2);
2077
+ if (body.skipChanges?.length) {
2078
+ for (const name of body.skipChanges) {
2079
+ store.updateChange(name, { status: "skipped" });
2080
+ }
2081
+ }
2082
+ store.addLog({
2083
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2084
+ level: "info",
2085
+ message: `\u7528\u6237\u786E\u8BA4\u6267\u884C\u8BA1\u5212\uFF0C\u5E76\u884C\u5EA6=${body.parallelism ?? 2}`
2086
+ });
2087
+ res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
2088
+ res.end(JSON.stringify(store.read()));
2089
+ } catch (e) {
2090
+ res.writeHead(400, { "Content-Type": "application/json; charset=utf-8" });
2091
+ res.end(JSON.stringify({ error: e instanceof Error ? e.message : "unknown error" }));
2092
+ }
2093
+ });
2094
+ ctx.server.route("/__apply-batch/adjust", async (req, res) => {
2095
+ try {
2096
+ const body = JSON.parse(await readBody(req) || "{}");
2097
+ if (body.parallelism) store.updateParallelism(body.parallelism);
2098
+ if (body.skipChanges?.length) {
2099
+ for (const name of body.skipChanges) {
2100
+ store.updateChange(name, { status: "skipped" });
2101
+ }
2102
+ }
2103
+ res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
2104
+ res.end(JSON.stringify(store.read()));
2105
+ } catch (e) {
2106
+ res.writeHead(400, { "Content-Type": "application/json; charset=utf-8" });
2107
+ res.end(JSON.stringify({ error: e instanceof Error ? e.message : "unknown error" }));
2108
+ }
2109
+ });
2110
+ ctx.server.route("/__apply-batch/retry", async (req, res) => {
2111
+ try {
2112
+ const body = JSON.parse(await readBody(req) || "{}");
2113
+ const name = body.name;
2114
+ if (!name) throw new Error("missing name");
2115
+ const change = store.read().changes.find((c) => c.name === name);
2116
+ if (!change) throw new Error("change not found");
2117
+ store.updateChange(name, {
2118
+ status: "pending",
2119
+ error: void 0,
2120
+ retryCount: (change.retryCount ?? 0) + 1
2121
+ });
2122
+ store.addLog({
2123
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2124
+ level: "info",
2125
+ message: `\u91CD\u8BD5 change: ${name}`,
2126
+ changeName: name
2127
+ });
2128
+ res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
2129
+ res.end(JSON.stringify(store.read()));
2130
+ } catch (e) {
2131
+ res.writeHead(400, { "Content-Type": "application/json; charset=utf-8" });
2132
+ res.end(JSON.stringify({ error: e instanceof Error ? e.message : "unknown error" }));
2133
+ }
2134
+ });
2135
+ ctx.server.route("/__apply-batch/pause", async (req, res) => {
2136
+ store.updateStatus("paused");
2137
+ store.addLog({
2138
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2139
+ level: "warn",
2140
+ message: "\u7528\u6237\u6682\u505C\u6279\u91CF\u6267\u884C"
2141
+ });
2142
+ res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
2143
+ res.end(JSON.stringify(store.read()));
2144
+ });
2145
+ ctx.server.route("/__apply-batch/resume", async (req, res) => {
2146
+ store.updateStatus("running");
2147
+ store.addLog({
2148
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2149
+ level: "info",
2150
+ message: "\u7528\u6237\u6062\u590D\u6279\u91CF\u6267\u884C"
2151
+ });
2152
+ res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
2153
+ res.end(JSON.stringify(store.read()));
2154
+ });
2155
+ ctx.server.route("/__apply-batch/reset", async (req, res) => {
2156
+ store.reset();
2157
+ res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
2158
+ res.end(JSON.stringify(store.read()));
2159
+ });
2160
+ });
2161
+ }
2162
+ };
2163
+
1644
2164
  // src/cli.ts
1645
- var __dirname2 = path11.dirname(fileURLToPath2(import.meta.url));
2165
+ import YAML2 from "yaml";
2166
+ var __dirname2 = path12.dirname(fileURLToPath2(import.meta.url));
1646
2167
  var DEFAULT_PORT = 4190;
1647
2168
  function parseArgs() {
1648
2169
  const args = process.argv.slice(2);
@@ -1677,6 +2198,62 @@ async function pathExists(p) {
1677
2198
  return false;
1678
2199
  }
1679
2200
  }
2201
+ function migrateLegacyBugsConfig(root) {
2202
+ const zgoalConfigYaml = path12.join(root, ".zgoal", "config.yaml");
2203
+ const zdevConfigYaml = path12.join(root, ".zdev", "config.yaml");
2204
+ if (fs12.existsSync(zgoalConfigYaml)) {
2205
+ try {
2206
+ fs12.unlinkSync(zgoalConfigYaml);
2207
+ } catch {
2208
+ }
2209
+ }
2210
+ if (!fs12.existsSync(zdevConfigYaml)) return;
2211
+ const pluginsConfig = readPluginsConfig(root);
2212
+ if ("bugs" in pluginsConfig) {
2213
+ try {
2214
+ fs12.unlinkSync(zdevConfigYaml);
2215
+ } catch {
2216
+ }
2217
+ return;
2218
+ }
2219
+ let yamlContent;
2220
+ try {
2221
+ yamlContent = fs12.readFileSync(zdevConfigYaml, "utf8");
2222
+ } catch {
2223
+ return;
2224
+ }
2225
+ let parsed;
2226
+ try {
2227
+ parsed = YAML2.parse(yamlContent);
2228
+ } catch {
2229
+ console.warn("[zdashboard] \u65E0\u6548\u7684 .zdev/config.yaml\uFF0C\u5DF2\u5FFD\u7565\u5E76\u5220\u9664");
2230
+ try {
2231
+ fs12.unlinkSync(zdevConfigYaml);
2232
+ } catch {
2233
+ }
2234
+ return;
2235
+ }
2236
+ if (!parsed || typeof parsed !== "object" || !parsed.url || !parsed.product) {
2237
+ console.warn("[zdashboard] .zdev/config.yaml \u7F3A\u5C11\u5FC5\u8981\u5B57\u6BB5\uFF0C\u5DF2\u5FFD\u7565\u5E76\u5220\u9664");
2238
+ try {
2239
+ fs12.unlinkSync(zdevConfigYaml);
2240
+ } catch {
2241
+ }
2242
+ return;
2243
+ }
2244
+ const bugsConfig = {
2245
+ url: String(parsed.url).replace(/\/+$/, ""),
2246
+ account: typeof parsed.account === "string" ? parsed.account : "",
2247
+ token: typeof parsed.token === "string" ? parsed.token : typeof parsed.password === "string" ? parsed.password : "",
2248
+ product: Number(parsed.product)
2249
+ };
2250
+ pluginsConfig.bugs = bugsConfig;
2251
+ writePluginsConfig(root, pluginsConfig);
2252
+ try {
2253
+ fs12.unlinkSync(zdevConfigYaml);
2254
+ } catch {
2255
+ }
2256
+ }
1680
2257
  async function loadExternal(ctx, dir, root) {
1681
2258
  if (!dir) return;
1682
2259
  let tsxLoaded = false;
@@ -1692,7 +2269,7 @@ async function loadExternal(ctx, dir, root) {
1692
2269
  if (!ent.isDirectory()) continue;
1693
2270
  const candidates = tsxLoaded ? ["index.ts", "index.js", "index.mjs"] : ["index.js", "index.mjs"];
1694
2271
  for (const name of candidates) {
1695
- const p = path11.join(dir, ent.name, name);
2272
+ const p = path12.join(dir, ent.name, name);
1696
2273
  if (await pathExists(p)) {
1697
2274
  try {
1698
2275
  const mod = await import(p);
@@ -1707,8 +2284,8 @@ async function loadExternal(ctx, dir, root) {
1707
2284
  const m = ctx.dashboard.get(ent.name);
1708
2285
  if (m && m.mode === ent.name) {
1709
2286
  const patch = { external: true };
1710
- const webDir = path11.join(dir, ent.name, "web");
1711
- if (await pathExists(path11.join(webDir, "index.html"))) {
2287
+ const webDir = path12.join(dir, ent.name, "web");
2288
+ if (await pathExists(path12.join(webDir, "index.html"))) {
1712
2289
  ctx.server.static(`${PLUGIN_STATIC_PREFIX}${ent.name}/`, webDir);
1713
2290
  if (!m.viewerUrl) patch.viewerUrl = `${PLUGIN_STATIC_PREFIX}${ent.name}/`;
1714
2291
  }
@@ -1727,7 +2304,7 @@ async function loadExternal(ctx, dir, root) {
1727
2304
  }
1728
2305
  async function main() {
1729
2306
  const args = parseArgs();
1730
- const root = path11.resolve(args.dir);
2307
+ const root = path12.resolve(args.dir);
1731
2308
  const existing = await findReusable(root);
1732
2309
  if (existing && !args.restart) {
1733
2310
  const u = `http://localhost:${existing.port}` + (args.page ? `#${args.page}` : "");
@@ -1741,11 +2318,12 @@ async function main() {
1741
2318
  console.log(`[zdashboard] --restart\uFF1A\u505C\u6B62\u65E7\u5B9E\u4F8B pid=${oldRecord.pid}`);
1742
2319
  await stopInstance(oldRecord);
1743
2320
  }
1744
- const appDir = path11.resolve(__dirname2, "web");
2321
+ const appDir = path12.resolve(__dirname2, "web");
1745
2322
  const startPort = oldRecord && !args.portExplicit ? oldRecord.port : args.port;
1746
- const zdevDir = path11.join(root, ".zdev");
1747
- const dataDir = fs11.existsSync(zdevDir) ? ".zdev/" : "";
2323
+ const zdevDir = path12.join(root, ".zdev");
2324
+ const dataDir = fs12.existsSync(zdevDir) ? ".zdev/" : "";
1748
2325
  const det = await detect(root);
2326
+ migrateLegacyBugsConfig(root);
1749
2327
  const ctx = new Context();
1750
2328
  ctx.plugin(ServerService, {
1751
2329
  root,
@@ -1767,6 +2345,7 @@ async function main() {
1767
2345
  { name: "bugs", apply: apply4 },
1768
2346
  { name: "review", apply: apply5 },
1769
2347
  { name: "apply", apply: apply6 },
2348
+ { name: "apply-batch", apply: apply10 },
1770
2349
  { name: "design", apply: apply7 },
1771
2350
  { name: "view", apply: apply8 }
1772
2351
  ];
@@ -1778,7 +2357,7 @@ async function main() {
1778
2357
  }
1779
2358
  }
1780
2359
  if (args.plugins) {
1781
- await loadExternal(ctx, path11.resolve(args.plugins), root);
2360
+ await loadExternal(ctx, path12.resolve(args.plugins), root);
1782
2361
  }
1783
2362
  const shutdown = () => {
1784
2363
  try {