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.
- package/dist/cli.js +861 -282
- package/dist/cli.js.map +1 -1
- package/dist/web/assets/ApprovalPanel-CXQRioSw.js +1 -0
- package/dist/web/assets/BatchDashboard-BigPUG8g.js +2 -0
- package/dist/web/assets/CheckpointViewer-DBmTkC_E.js +1 -0
- package/dist/web/assets/ConfigField-BqjVlteZ.js +1 -0
- package/dist/web/assets/DependencyGraph-DPVneSQh.js +1 -0
- package/dist/web/assets/FileIcon-tDB2mYGC.js +279 -0
- package/dist/web/assets/{FilterPills-B6u1IBeC.js → FilterPills-CvO0pz85.js} +1 -1
- package/dist/web/assets/{ProgressBar-CbyvyLzw.js → ProgressBar-DIoIeYBA.js} +1 -1
- package/dist/web/assets/{Workspace-DQmfVJh6.js → Workspace-B2nKQnQf.js} +1 -1
- package/dist/web/assets/badge-CmIMTBDj.js +1 -0
- package/dist/web/assets/index-BHL7sr_b.css +1 -0
- package/dist/web/assets/{index-BvuOeKbq.js → index-CtlFZEcM.js} +1 -1
- package/dist/web/assets/{index-BfA6l4to.js → index-yx7txO50.js} +1565 -1565
- package/dist/web/assets/web-BBv8hkwo.js +1 -0
- package/dist/web/assets/web-BNjs9lCy.js +139 -0
- package/dist/web/assets/{web-BXCBKn6i.js → web-BX2cajm6.js} +1 -1
- package/dist/web/assets/web-BpBEbcYZ.js +1 -0
- package/dist/web/assets/web-CGbbhItO.js +2 -0
- package/dist/web/assets/web-CcDed_yx.js +1 -0
- package/dist/web/assets/{web-C8I10oqh.js → web-DsIjKVnn.js} +1 -1
- package/dist/web/assets/web-Eo4S0U_k.js +2 -0
- package/dist/web/favicon.svg +13 -5
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/FileIcon-B0-2gV6a.js +0 -10
- package/dist/web/assets/MdViewer-Cvh3TNQI.js +0 -270
- package/dist/web/assets/badge-DTb6qo3S.js +0 -1
- package/dist/web/assets/index-DQkehbup.css +0 -1
- package/dist/web/assets/web-5newSm97.js +0 -1
- package/dist/web/assets/web-BOg9UnpC.js +0 -1
- package/dist/web/assets/web-BcTJQUKr.js +0 -1
- package/dist/web/assets/web-CLCAgjmg.js +0 -1
- package/dist/web/assets/web-CXE89yN1.js +0 -2
- /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
|
|
5
|
-
import
|
|
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
|
|
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", "--
|
|
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 =
|
|
151
|
-
const hasDocs =
|
|
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 =
|
|
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
|
|
163
|
-
import
|
|
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.
|
|
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
|
|
261
|
-
import
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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('
|
|
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(
|
|
468
|
-
this.routes.set(
|
|
469
|
-
this.ctx.effect(() => () => this.routes.delete(
|
|
376
|
+
route(path13, handler) {
|
|
377
|
+
this.routes.set(path13, handler);
|
|
378
|
+
this.ctx.effect(() => () => this.routes.delete(path13));
|
|
470
379
|
}
|
|
471
|
-
sse(
|
|
472
|
-
this.sses.set(
|
|
473
|
-
this.ctx.effect(() => () => this.sses.delete(
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 === "/" || !
|
|
539
|
-
const fp =
|
|
540
|
-
if (fp.indexOf(dir +
|
|
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 =
|
|
581
|
-
if (url.indexOf("/__app/") === 0) fp2 =
|
|
582
|
-
if (fp2 !== this.appDir && fp2.indexOf(this.appDir +
|
|
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 =
|
|
506
|
+
if (url === "/") fp2 = path3.join(this.appDir, "index.html");
|
|
587
507
|
return this.serveFile(fp2, req, res, false);
|
|
588
508
|
}
|
|
589
|
-
const fp =
|
|
590
|
-
if (fp !== this.root && fp.indexOf(this.root +
|
|
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
|
|
594
|
+
import fs4 from "fs";
|
|
675
595
|
import { Service as Service2 } from "cordis";
|
|
676
|
-
var WATCH_DEBOUNCE_MS =
|
|
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 =
|
|
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 =
|
|
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(".")
|
|
753
|
-
|
|
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
|
-
|
|
804
|
-
|
|
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
|
|
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(
|
|
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 =
|
|
817
|
-
if (
|
|
818
|
-
for (const ent of
|
|
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(
|
|
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 =
|
|
831
|
-
if (
|
|
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 &&
|
|
766
|
+
if (hasDocs && fs6.existsSync(path5.join(root, "docs"))) {
|
|
839
767
|
const paths = [];
|
|
840
|
-
walkDir(
|
|
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
|
|
848
|
-
if (ent.name.startsWith(".")
|
|
849
|
-
|
|
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
|
|
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
|
|
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({
|
|
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
|
|
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
|
|
1248
|
-
import
|
|
1249
|
-
import
|
|
1250
|
-
var
|
|
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
|
|
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 =
|
|
1267
|
-
if (!parsed || !Array.isArray(parsed.items))
|
|
1268
|
-
|
|
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
|
-
|
|
1275
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
1433
|
+
listDocs() {
|
|
1434
|
+
const docsDir = path6.join(this.root, DOCS_DIR);
|
|
1435
|
+
if (!fs7.existsSync(docsDir)) return [];
|
|
1298
1436
|
try {
|
|
1299
|
-
|
|
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: "
|
|
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
|
|
1331
|
-
const data = reviewStore.updateItem(body.id, {
|
|
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
|
|
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.
|
|
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
|
|
1365
|
-
import
|
|
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
|
|
1616
|
+
return fs8.readFileSync(p, "utf8");
|
|
1384
1617
|
} catch {
|
|
1385
1618
|
return "";
|
|
1386
1619
|
}
|
|
1387
1620
|
}
|
|
1388
1621
|
function worktreeDir(root, name) {
|
|
1389
|
-
return
|
|
1622
|
+
return path7.join(root, ".zworktree", name);
|
|
1390
1623
|
}
|
|
1391
1624
|
function changeDir(root, name) {
|
|
1392
|
-
return
|
|
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 =
|
|
1407
|
-
if (!
|
|
1639
|
+
const changesDir = path7.join(root, "openspec", "changes");
|
|
1640
|
+
if (!fs8.existsSync(changesDir)) return [];
|
|
1408
1641
|
const out = [];
|
|
1409
|
-
for (const ent of
|
|
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(
|
|
1645
|
+
const tasks = readText(path7.join(wt, "openspec", "changes", ent.name, "tasks.md"));
|
|
1413
1646
|
if (!tasks) {
|
|
1414
|
-
const fallback = readText(
|
|
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 =
|
|
1418
|
-
const mainChange =
|
|
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:
|
|
1425
|
-
hasDesign:
|
|
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:
|
|
1660
|
+
inWorktree: fs8.existsSync(wtChange)
|
|
1428
1661
|
});
|
|
1429
1662
|
}
|
|
1430
1663
|
} else {
|
|
1431
1664
|
const { total, done } = countTasks(tasks);
|
|
1432
|
-
const wtChange =
|
|
1433
|
-
const mainChange =
|
|
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:
|
|
1441
|
-
hasDesign:
|
|
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 =
|
|
1453
|
-
const designFrom =
|
|
1454
|
-
const tasksFrom =
|
|
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 =
|
|
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/
|
|
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
|
-
|
|
1546
|
-
|
|
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:
|
|
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({
|
|
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
|
-
|
|
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({
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
1711
|
-
if (await pathExists(
|
|
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 =
|
|
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 =
|
|
2321
|
+
const appDir = path12.resolve(__dirname2, "web");
|
|
1745
2322
|
const startPort = oldRecord && !args.portExplicit ? oldRecord.port : args.port;
|
|
1746
|
-
const zdevDir =
|
|
1747
|
-
const dataDir =
|
|
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,
|
|
2360
|
+
await loadExternal(ctx, path12.resolve(args.plugins), root);
|
|
1782
2361
|
}
|
|
1783
2362
|
const shutdown = () => {
|
|
1784
2363
|
try {
|