zdashboard 2.6.2 → 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 (30) hide show
  1. package/dist/cli.js +728 -284
  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-Bc03q6Gz.js → FileIcon-tDB2mYGC.js} +1 -1
  9. package/dist/web/assets/{FilterPills-CSZQenbh.js → FilterPills-CvO0pz85.js} +1 -1
  10. package/dist/web/assets/{ProgressBar-ycyJBwG-.js → ProgressBar-DIoIeYBA.js} +1 -1
  11. package/dist/web/assets/{Workspace-jCfxaTOM.js → Workspace-B2nKQnQf.js} +1 -1
  12. package/dist/web/assets/{badge-DUO7nDks.js → badge-CmIMTBDj.js} +1 -1
  13. package/dist/web/assets/index-BHL7sr_b.css +1 -0
  14. package/dist/web/assets/{index-BrvOKJRF.js → index-CtlFZEcM.js} +1 -1
  15. package/dist/web/assets/{index-Co_rMXPa.js → index-yx7txO50.js} +1565 -1565
  16. package/dist/web/assets/web-BBv8hkwo.js +1 -0
  17. package/dist/web/assets/{web-Bt0s1qFE.js → web-BX2cajm6.js} +1 -1
  18. package/dist/web/assets/web-BpBEbcYZ.js +1 -0
  19. package/dist/web/assets/web-CGbbhItO.js +2 -0
  20. package/dist/web/assets/web-CcDed_yx.js +1 -0
  21. package/dist/web/assets/{web-CCoznDNk.js → web-DsIjKVnn.js} +1 -1
  22. package/dist/web/assets/web-Eo4S0U_k.js +2 -0
  23. package/dist/web/favicon.svg +13 -5
  24. package/dist/web/index.html +2 -2
  25. package/package.json +1 -1
  26. package/dist/web/assets/index-4aiRmNbP.css +0 -1
  27. package/dist/web/assets/web-BKh2HY35.js +0 -2
  28. package/dist/web/assets/web-BrsVNrGk.js +0 -1
  29. package/dist/web/assets/web-DxK2pSVl.js +0 -1
  30. package/dist/web/assets/web-ShIDOvFD.js +0 -1
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.2",
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
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,13 +1342,13 @@ 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";
1345
+ import fs7 from "fs";
1346
+ import path6 from "path";
1347
+ import YAML from "yaml";
1250
1348
  var REVIEW_DIR = ".zreview";
1251
- var REVIEW_FILE = path7.join(REVIEW_DIR, "review.yaml");
1252
- var DOCS_DIR = path7.join(REVIEW_DIR, "docs");
1253
- var DIAGRAMS_DIR = path7.join(REVIEW_DIR, "diagrams");
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");
1254
1352
  var ReviewStore = class {
1255
1353
  root;
1256
1354
  onChange;
@@ -1259,15 +1357,15 @@ var ReviewStore = class {
1259
1357
  this.onChange = onChange;
1260
1358
  }
1261
1359
  exists() {
1262
- return fs8.existsSync(path7.join(this.root, REVIEW_FILE));
1360
+ return fs7.existsSync(path6.join(this.root, REVIEW_FILE));
1263
1361
  }
1264
1362
  read() {
1265
- const fullPath = path7.join(this.root, REVIEW_FILE);
1266
- if (!fs8.existsSync(fullPath)) {
1363
+ const fullPath = path6.join(this.root, REVIEW_FILE);
1364
+ if (!fs7.existsSync(fullPath)) {
1267
1365
  return { status: "draft", summary: "", documents: [], codebases: [], diagrams: [], items: [] };
1268
1366
  }
1269
1367
  try {
1270
- const parsed = YAML2.parse(fs8.readFileSync(fullPath, "utf8"));
1368
+ const parsed = YAML.parse(fs7.readFileSync(fullPath, "utf8"));
1271
1369
  if (!parsed || !Array.isArray(parsed.items)) {
1272
1370
  return { status: "draft", summary: parsed?.summary ?? "", documents: parsed?.documents ?? [], codebases: parsed?.codebases ?? [], diagrams: parsed?.diagrams ?? [], items: [] };
1273
1371
  }
@@ -1284,8 +1382,8 @@ var ReviewStore = class {
1284
1382
  }
1285
1383
  }
1286
1384
  write(data) {
1287
- fs8.mkdirSync(path7.join(this.root, REVIEW_DIR), { recursive: true });
1288
- fs8.writeFileSync(path7.join(this.root, REVIEW_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");
1289
1387
  this.onChange?.();
1290
1388
  }
1291
1389
  updateItem(id, patch) {
@@ -1333,32 +1431,32 @@ var ReviewStore = class {
1333
1431
  return data;
1334
1432
  }
1335
1433
  listDocs() {
1336
- const docsDir = path7.join(this.root, DOCS_DIR);
1337
- if (!fs8.existsSync(docsDir)) return [];
1434
+ const docsDir = path6.join(this.root, DOCS_DIR);
1435
+ if (!fs7.existsSync(docsDir)) return [];
1338
1436
  try {
1339
- return fs8.readdirSync(docsDir).filter((f) => /\.(md|markdown|txt)$/i.test(f)).sort();
1437
+ return fs7.readdirSync(docsDir).filter((f) => /\.(md|markdown|txt)$/i.test(f)).sort();
1340
1438
  } catch {
1341
1439
  return [];
1342
1440
  }
1343
1441
  }
1344
1442
  readDoc(name) {
1345
- const fullPath = path7.join(this.root, DOCS_DIR, name);
1346
- if (!fs8.existsSync(fullPath)) throw new Error(`doc ${name} not found`);
1347
- return fs8.readFileSync(fullPath, "utf8");
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");
1348
1446
  }
1349
1447
  listDiagrams() {
1350
- const dir = path7.join(this.root, DIAGRAMS_DIR);
1351
- if (!fs8.existsSync(dir)) return [];
1448
+ const dir = path6.join(this.root, DIAGRAMS_DIR);
1449
+ if (!fs7.existsSync(dir)) return [];
1352
1450
  try {
1353
- return fs8.readdirSync(dir).filter((f) => /\.html$/i.test(f)).sort();
1451
+ return fs7.readdirSync(dir).filter((f) => /\.html$/i.test(f)).sort();
1354
1452
  } catch {
1355
1453
  return [];
1356
1454
  }
1357
1455
  }
1358
1456
  readDiagram(name) {
1359
- const fullPath = path7.join(this.root, DIAGRAMS_DIR, name);
1360
- if (!fs8.existsSync(fullPath)) throw new Error(`diagram ${name} not found`);
1361
- return fs8.readFileSync(fullPath, "utf8");
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");
1362
1460
  }
1363
1461
  findItem(items, id) {
1364
1462
  for (const item of items) {
@@ -1413,7 +1511,7 @@ var apply5 = {
1413
1511
  return;
1414
1512
  }
1415
1513
  try {
1416
- const body = JSON.parse(await readBody(req) || "{}");
1514
+ const body = JSON.parse(await readBody2(req) || "{}");
1417
1515
  const data = reviewStore.updateItem(body.id, {
1418
1516
  answer: body.answer,
1419
1517
  state: body.state,
@@ -1434,7 +1532,7 @@ var apply5 = {
1434
1532
  return;
1435
1533
  }
1436
1534
  try {
1437
- const body = JSON.parse(await readBody(req) || "{}");
1535
+ const body = JSON.parse(await readBody2(req) || "{}");
1438
1536
  const data = reviewStore.addChild(body.parentId, body.title, body.priority);
1439
1537
  res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
1440
1538
  res.end(JSON.stringify(data));
@@ -1450,7 +1548,7 @@ var apply5 = {
1450
1548
  return;
1451
1549
  }
1452
1550
  try {
1453
- const body = JSON.parse(await readBody(req) || "{}");
1551
+ const body = JSON.parse(await readBody2(req) || "{}");
1454
1552
  const data = reviewStore.removeItem(body.id);
1455
1553
  res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
1456
1554
  res.end(JSON.stringify(data));
@@ -1466,7 +1564,7 @@ var apply5 = {
1466
1564
  return;
1467
1565
  }
1468
1566
  try {
1469
- const body = JSON.parse(await readBody(req) || "{}");
1567
+ const body = JSON.parse(await readBody2(req) || "{}");
1470
1568
  const data = reviewStore.setStatus(body.status);
1471
1569
  res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
1472
1570
  res.end(JSON.stringify(data));
@@ -1496,8 +1594,8 @@ var apply5 = {
1496
1594
  };
1497
1595
 
1498
1596
  // src/plugins/apply/scan.ts
1499
- import fs9 from "fs";
1500
- import path8 from "path";
1597
+ import fs8 from "fs";
1598
+ import path7 from "path";
1501
1599
 
1502
1600
  // src/plugins/apply/parse-tasks.ts
1503
1601
  var TASK_RE = /^\s*-\s*\[([ xX])\]\s*(.*)$/;
@@ -1515,16 +1613,16 @@ function countTasks(md) {
1515
1613
  // src/plugins/apply/scan.ts
1516
1614
  function readText(p) {
1517
1615
  try {
1518
- return fs9.readFileSync(p, "utf8");
1616
+ return fs8.readFileSync(p, "utf8");
1519
1617
  } catch {
1520
1618
  return "";
1521
1619
  }
1522
1620
  }
1523
1621
  function worktreeDir(root, name) {
1524
- return path8.join(root, ".zworktree", name);
1622
+ return path7.join(root, ".zworktree", name);
1525
1623
  }
1526
1624
  function changeDir(root, name) {
1527
- return path8.join(root, "openspec", "changes", name);
1625
+ return path7.join(root, "openspec", "changes", name);
1528
1626
  }
1529
1627
  function parseDependsOn(proposal) {
1530
1628
  const deps = [];
@@ -1538,42 +1636,42 @@ function parseDependsOn(proposal) {
1538
1636
  return deps;
1539
1637
  }
1540
1638
  function scanApplyChanges(root) {
1541
- const changesDir = path8.join(root, "openspec", "changes");
1542
- if (!fs9.existsSync(changesDir)) return [];
1639
+ const changesDir = path7.join(root, "openspec", "changes");
1640
+ if (!fs8.existsSync(changesDir)) return [];
1543
1641
  const out = [];
1544
- for (const ent of fs9.readdirSync(changesDir, { withFileTypes: true })) {
1642
+ for (const ent of fs8.readdirSync(changesDir, { withFileTypes: true })) {
1545
1643
  if (!ent.isDirectory() || ent.name.startsWith(".") || ent.name === "archive") continue;
1546
1644
  const wt = worktreeDir(root, ent.name);
1547
- 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"));
1548
1646
  if (!tasks) {
1549
- const fallback = readText(path8.join(changesDir, ent.name, "tasks.md"));
1647
+ const fallback = readText(path7.join(changesDir, ent.name, "tasks.md"));
1550
1648
  if (fallback) {
1551
1649
  const { total, done } = countTasks(fallback);
1552
- const wtChange = path8.join(wt, "openspec", "changes", ent.name);
1553
- 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);
1554
1652
  out.push({
1555
1653
  name: ent.name,
1556
1654
  path: `openspec/changes/${ent.name}`,
1557
1655
  total,
1558
1656
  done,
1559
- hasProposal: fs9.existsSync(path8.join(wtChange, "proposal.md")) || fs9.existsSync(path8.join(mainChange, "proposal.md")),
1560
- 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")),
1561
1659
  // worktree 目录存在即算执行中(tasks.md 可能尚未落盘)
1562
- inWorktree: fs9.existsSync(wtChange)
1660
+ inWorktree: fs8.existsSync(wtChange)
1563
1661
  });
1564
1662
  }
1565
1663
  } else {
1566
1664
  const { total, done } = countTasks(tasks);
1567
- const wtChange = path8.join(wt, "openspec", "changes", ent.name);
1568
- 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);
1569
1667
  out.push({
1570
1668
  name: ent.name,
1571
1669
  path: `openspec/changes/${ent.name}`,
1572
1670
  total,
1573
1671
  done,
1574
1672
  // 任一侧存在即算(详情读取是 worktree 优先、主目录兜底,列表标记保持一致)
1575
- hasProposal: fs9.existsSync(path8.join(wtChange, "proposal.md")) || fs9.existsSync(path8.join(mainChange, "proposal.md")),
1576
- 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")),
1577
1675
  inWorktree: true
1578
1676
  });
1579
1677
  }
@@ -1584,14 +1682,14 @@ function scanApplyChanges(root) {
1584
1682
  function readApplyChange(root, name) {
1585
1683
  const wt = worktreeDir(root, name);
1586
1684
  const mainChangeDir = changeDir(root, name);
1587
- 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");
1588
- 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");
1589
- 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");
1590
1688
  const proposal = readText(proposalFrom);
1591
1689
  const design = readText(designFrom);
1592
1690
  const tasks = readText(tasksFrom);
1593
1691
  const { total, done } = countTasks(tasks);
1594
- const inWorktree = fs9.existsSync(path8.join(wt, "openspec", "changes", name));
1692
+ const inWorktree = fs8.existsSync(path7.join(wt, "openspec", "changes", name));
1595
1693
  const dependsOn = parseDependsOn(proposal);
1596
1694
  const hasTestStrategy = /^##\s+测试策略\s*$/m.test(design);
1597
1695
  return {
@@ -1648,8 +1746,12 @@ var apply6 = {
1648
1746
  }
1649
1747
  };
1650
1748
 
1651
- // src/server/design-assets.ts
1749
+ // src/plugins/design/index.ts
1652
1750
  import path9 from "path";
1751
+ import fs9 from "fs";
1752
+
1753
+ // src/server/design-assets.ts
1754
+ import path8 from "path";
1653
1755
  var PAGE_EXTS = [".html", ".htm"];
1654
1756
  var ICON_EXTS = [".svg", ".png", ".ico", ".jpg", ".jpeg", ".gif", ".webp"];
1655
1757
  var VIDEO_EXTS = [".mp4", ".webm", ".mov", ".ogg", ".ogv"];
@@ -1672,30 +1774,62 @@ function categorize(rel, ext) {
1672
1774
  }
1673
1775
  return null;
1674
1776
  }
1675
- function scanAssets(root) {
1777
+ function scanAssets(root, subDir = "") {
1676
1778
  const out = {};
1677
1779
  const keys = ["page", "component", "icon", "token", "md", "video", "audio", "pdf", "font"];
1678
1780
  for (const k of keys) out[k] = [];
1679
1781
  const SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "coverage", ".next", ".cache"]);
1680
- walkDir(root, { skip: SKIP, onFile: (abs, rel) => {
1681
- 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();
1682
1785
  const t = categorize(rel, ext);
1683
- 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 });
1684
1787
  } });
1685
1788
  return out;
1686
1789
  }
1687
1790
 
1688
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
+ }
1689
1800
  var apply7 = {
1690
1801
  inject: ["server", "dashboard"],
1691
1802
  apply(ctx, config) {
1692
1803
  const root = config.root;
1693
1804
  ctx.inject(["server"], () => {
1694
1805
  if (!ctx.server?.route) return;
1695
- 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
+ });
1696
1815
  ctx.server.route("/__design/assets", async (_req, res) => {
1697
1816
  res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
1698
- 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));
1699
1833
  });
1700
1834
  });
1701
1835
  }
@@ -1705,7 +1839,17 @@ var apply7 = {
1705
1839
  var apply8 = {
1706
1840
  inject: ["dashboard"],
1707
1841
  apply(ctx) {
1708
- 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
+ });
1709
1853
  }
1710
1854
  };
1711
1855
 
@@ -1776,8 +1920,250 @@ var apply9 = {
1776
1920
  }
1777
1921
  };
1778
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
+
1779
2164
  // src/cli.ts
1780
- var __dirname2 = path11.dirname(fileURLToPath2(import.meta.url));
2165
+ import YAML2 from "yaml";
2166
+ var __dirname2 = path12.dirname(fileURLToPath2(import.meta.url));
1781
2167
  var DEFAULT_PORT = 4190;
1782
2168
  function parseArgs() {
1783
2169
  const args = process.argv.slice(2);
@@ -1812,6 +2198,62 @@ async function pathExists(p) {
1812
2198
  return false;
1813
2199
  }
1814
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
+ }
1815
2257
  async function loadExternal(ctx, dir, root) {
1816
2258
  if (!dir) return;
1817
2259
  let tsxLoaded = false;
@@ -1827,7 +2269,7 @@ async function loadExternal(ctx, dir, root) {
1827
2269
  if (!ent.isDirectory()) continue;
1828
2270
  const candidates = tsxLoaded ? ["index.ts", "index.js", "index.mjs"] : ["index.js", "index.mjs"];
1829
2271
  for (const name of candidates) {
1830
- const p = path11.join(dir, ent.name, name);
2272
+ const p = path12.join(dir, ent.name, name);
1831
2273
  if (await pathExists(p)) {
1832
2274
  try {
1833
2275
  const mod = await import(p);
@@ -1842,8 +2284,8 @@ async function loadExternal(ctx, dir, root) {
1842
2284
  const m = ctx.dashboard.get(ent.name);
1843
2285
  if (m && m.mode === ent.name) {
1844
2286
  const patch = { external: true };
1845
- const webDir = path11.join(dir, ent.name, "web");
1846
- 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"))) {
1847
2289
  ctx.server.static(`${PLUGIN_STATIC_PREFIX}${ent.name}/`, webDir);
1848
2290
  if (!m.viewerUrl) patch.viewerUrl = `${PLUGIN_STATIC_PREFIX}${ent.name}/`;
1849
2291
  }
@@ -1862,7 +2304,7 @@ async function loadExternal(ctx, dir, root) {
1862
2304
  }
1863
2305
  async function main() {
1864
2306
  const args = parseArgs();
1865
- const root = path11.resolve(args.dir);
2307
+ const root = path12.resolve(args.dir);
1866
2308
  const existing = await findReusable(root);
1867
2309
  if (existing && !args.restart) {
1868
2310
  const u = `http://localhost:${existing.port}` + (args.page ? `#${args.page}` : "");
@@ -1876,11 +2318,12 @@ async function main() {
1876
2318
  console.log(`[zdashboard] --restart\uFF1A\u505C\u6B62\u65E7\u5B9E\u4F8B pid=${oldRecord.pid}`);
1877
2319
  await stopInstance(oldRecord);
1878
2320
  }
1879
- const appDir = path11.resolve(__dirname2, "web");
2321
+ const appDir = path12.resolve(__dirname2, "web");
1880
2322
  const startPort = oldRecord && !args.portExplicit ? oldRecord.port : args.port;
1881
- const zdevDir = path11.join(root, ".zdev");
1882
- const dataDir = fs11.existsSync(zdevDir) ? ".zdev/" : "";
2323
+ const zdevDir = path12.join(root, ".zdev");
2324
+ const dataDir = fs12.existsSync(zdevDir) ? ".zdev/" : "";
1883
2325
  const det = await detect(root);
2326
+ migrateLegacyBugsConfig(root);
1884
2327
  const ctx = new Context();
1885
2328
  ctx.plugin(ServerService, {
1886
2329
  root,
@@ -1902,6 +2345,7 @@ async function main() {
1902
2345
  { name: "bugs", apply: apply4 },
1903
2346
  { name: "review", apply: apply5 },
1904
2347
  { name: "apply", apply: apply6 },
2348
+ { name: "apply-batch", apply: apply10 },
1905
2349
  { name: "design", apply: apply7 },
1906
2350
  { name: "view", apply: apply8 }
1907
2351
  ];
@@ -1913,7 +2357,7 @@ async function main() {
1913
2357
  }
1914
2358
  }
1915
2359
  if (args.plugins) {
1916
- await loadExternal(ctx, path11.resolve(args.plugins), root);
2360
+ await loadExternal(ctx, path12.resolve(args.plugins), root);
1917
2361
  }
1918
2362
  const shutdown = () => {
1919
2363
  try {