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.
- package/dist/cli.js +728 -284
- 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-Bc03q6Gz.js → FileIcon-tDB2mYGC.js} +1 -1
- package/dist/web/assets/{FilterPills-CSZQenbh.js → FilterPills-CvO0pz85.js} +1 -1
- package/dist/web/assets/{ProgressBar-ycyJBwG-.js → ProgressBar-DIoIeYBA.js} +1 -1
- package/dist/web/assets/{Workspace-jCfxaTOM.js → Workspace-B2nKQnQf.js} +1 -1
- package/dist/web/assets/{badge-DUO7nDks.js → badge-CmIMTBDj.js} +1 -1
- package/dist/web/assets/index-BHL7sr_b.css +1 -0
- package/dist/web/assets/{index-BrvOKJRF.js → index-CtlFZEcM.js} +1 -1
- package/dist/web/assets/{index-Co_rMXPa.js → index-yx7txO50.js} +1565 -1565
- package/dist/web/assets/web-BBv8hkwo.js +1 -0
- package/dist/web/assets/{web-Bt0s1qFE.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-CCoznDNk.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/index-4aiRmNbP.css +0 -1
- package/dist/web/assets/web-BKh2HY35.js +0 -2
- package/dist/web/assets/web-BrsVNrGk.js +0 -1
- package/dist/web/assets/web-DxK2pSVl.js +0 -1
- 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
|
|
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
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,13 +1342,13 @@ var apply4 = {
|
|
|
1244
1342
|
};
|
|
1245
1343
|
|
|
1246
1344
|
// src/server/review-store.ts
|
|
1247
|
-
import
|
|
1248
|
-
import
|
|
1249
|
-
import
|
|
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 =
|
|
1252
|
-
var DOCS_DIR =
|
|
1253
|
-
var DIAGRAMS_DIR =
|
|
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
|
|
1360
|
+
return fs7.existsSync(path6.join(this.root, REVIEW_FILE));
|
|
1263
1361
|
}
|
|
1264
1362
|
read() {
|
|
1265
|
-
const fullPath =
|
|
1266
|
-
if (!
|
|
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 =
|
|
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
|
-
|
|
1288
|
-
|
|
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 =
|
|
1337
|
-
if (!
|
|
1434
|
+
const docsDir = path6.join(this.root, DOCS_DIR);
|
|
1435
|
+
if (!fs7.existsSync(docsDir)) return [];
|
|
1338
1436
|
try {
|
|
1339
|
-
return
|
|
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 =
|
|
1346
|
-
if (!
|
|
1347
|
-
return
|
|
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 =
|
|
1351
|
-
if (!
|
|
1448
|
+
const dir = path6.join(this.root, DIAGRAMS_DIR);
|
|
1449
|
+
if (!fs7.existsSync(dir)) return [];
|
|
1352
1450
|
try {
|
|
1353
|
-
return
|
|
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 =
|
|
1360
|
-
if (!
|
|
1361
|
-
return
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1500
|
-
import
|
|
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
|
|
1616
|
+
return fs8.readFileSync(p, "utf8");
|
|
1519
1617
|
} catch {
|
|
1520
1618
|
return "";
|
|
1521
1619
|
}
|
|
1522
1620
|
}
|
|
1523
1621
|
function worktreeDir(root, name) {
|
|
1524
|
-
return
|
|
1622
|
+
return path7.join(root, ".zworktree", name);
|
|
1525
1623
|
}
|
|
1526
1624
|
function changeDir(root, name) {
|
|
1527
|
-
return
|
|
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 =
|
|
1542
|
-
if (!
|
|
1639
|
+
const changesDir = path7.join(root, "openspec", "changes");
|
|
1640
|
+
if (!fs8.existsSync(changesDir)) return [];
|
|
1543
1641
|
const out = [];
|
|
1544
|
-
for (const ent of
|
|
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(
|
|
1645
|
+
const tasks = readText(path7.join(wt, "openspec", "changes", ent.name, "tasks.md"));
|
|
1548
1646
|
if (!tasks) {
|
|
1549
|
-
const fallback = readText(
|
|
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 =
|
|
1553
|
-
const mainChange =
|
|
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:
|
|
1560
|
-
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")),
|
|
1561
1659
|
// worktree 目录存在即算执行中(tasks.md 可能尚未落盘)
|
|
1562
|
-
inWorktree:
|
|
1660
|
+
inWorktree: fs8.existsSync(wtChange)
|
|
1563
1661
|
});
|
|
1564
1662
|
}
|
|
1565
1663
|
} else {
|
|
1566
1664
|
const { total, done } = countTasks(tasks);
|
|
1567
|
-
const wtChange =
|
|
1568
|
-
const mainChange =
|
|
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:
|
|
1576
|
-
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")),
|
|
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 =
|
|
1588
|
-
const designFrom =
|
|
1589
|
-
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");
|
|
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 =
|
|
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/
|
|
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
|
-
|
|
1681
|
-
|
|
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:
|
|
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({
|
|
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
|
-
|
|
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({
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
1846
|
-
if (await pathExists(
|
|
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 =
|
|
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 =
|
|
2321
|
+
const appDir = path12.resolve(__dirname2, "web");
|
|
1880
2322
|
const startPort = oldRecord && !args.portExplicit ? oldRecord.port : args.port;
|
|
1881
|
-
const zdevDir =
|
|
1882
|
-
const dataDir =
|
|
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,
|
|
2360
|
+
await loadExternal(ctx, path12.resolve(args.plugins), root);
|
|
1917
2361
|
}
|
|
1918
2362
|
const shutdown = () => {
|
|
1919
2363
|
try {
|