zdashboard 2.6.2 → 2.8.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/README.md +1 -0
- package/dist/cli.js +695 -708
- package/dist/cli.js.map +1 -1
- package/dist/web/assets/BatchView-C7Y83zJ8.js +2 -0
- package/dist/web/assets/CheckpointViewer-Dnakl7qw.js +1 -0
- package/dist/web/assets/{FileIcon-Bc03q6Gz.js → CodeViewer-DKQhzhtt.js} +31 -31
- package/dist/web/assets/DependencyGraph-M-usNrkC.js +1 -0
- package/dist/web/assets/FileIcon-AydLkQKG.js +1 -0
- package/dist/web/assets/{FilterPills-CSZQenbh.js → FilterPills-X52teHq7.js} +1 -1
- package/dist/web/assets/{ProgressBar-ycyJBwG-.js → ProgressBar-C8W_FpI2.js} +1 -1
- package/dist/web/assets/Sidebar-BUEb7NX4.js +1 -0
- package/dist/web/assets/Sidebar-R9gmd6Ss.js +1 -0
- package/dist/web/assets/SingleChangeView-Cfr_Wkzz.js +1 -0
- package/dist/web/assets/Workspace-C2bvxp8p.js +1 -0
- package/dist/web/assets/Workspace-CkU9yWvw.js +5 -0
- package/dist/web/assets/Workspace-CqTemGJp.js +2 -0
- package/dist/web/assets/Workspace-Do07KofT.js +1 -0
- package/dist/web/assets/Workspace-qzGIvIU-.js +1 -0
- package/dist/web/assets/client-Cw4K1Nx3.js +1 -0
- package/dist/web/assets/{index-Co_rMXPa.js → index-B1epVp2c.js} +1522 -1522
- package/dist/web/assets/index-BJDneNeU.css +1 -0
- package/dist/web/assets/{index-BrvOKJRF.js → index-DOZp2EHd.js} +2 -2
- package/dist/web/assets/usePluginData-jGCocXxu.js +1 -0
- package/dist/web/assets/web-BROwAvbQ.js +2 -0
- package/dist/web/assets/web-C-_Hr8JN.js +2 -0
- package/dist/web/assets/web-C4mUiGNR.js +2 -0
- package/dist/web/assets/web-He9MXVk-.js +2 -0
- package/dist/web/assets/web-VATakLhV.js +2 -0
- package/dist/web/favicon.svg +13 -5
- package/dist/web/index.html +3 -3
- package/package.json +5 -1
- package/dist/web/assets/Workspace-jCfxaTOM.js +0 -1
- package/dist/web/assets/badge-DUO7nDks.js +0 -1
- package/dist/web/assets/index-4aiRmNbP.css +0 -1
- package/dist/web/assets/web-BKh2HY35.js +0 -2
- package/dist/web/assets/web-BNjs9lCy.js +0 -139
- package/dist/web/assets/web-BrsVNrGk.js +0 -1
- package/dist/web/assets/web-Bt0s1qFE.js +0 -5
- package/dist/web/assets/web-CCoznDNk.js +0 -2
- package/dist/web/assets/web-DxK2pSVl.js +0 -1
- package/dist/web/assets/web-ShIDOvFD.js +0 -1
- /package/dist/web/assets/{FileIcon-BTcE7LSd.css → CodeViewer-BTcE7LSd.css} +0 -0
package/dist/cli.js
CHANGED
|
@@ -8,150 +8,26 @@ 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
|
-
|
|
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";
|
|
14
|
+
var JUST_PROBE_TIMEOUT_MS = 5e3;
|
|
15
|
+
var JUST_FALLBACK_TIMEOUT_MS = 3e3;
|
|
141
16
|
function justAvailable(cwd) {
|
|
142
|
-
|
|
143
|
-
const child = execFile("just",
|
|
144
|
-
resolve(!err);
|
|
145
|
-
});
|
|
17
|
+
const probe = (args, timeout) => new Promise((resolve) => {
|
|
18
|
+
const child = execFile("just", args, { cwd, timeout }, (err) => resolve(!err));
|
|
146
19
|
if (child.killed) resolve(false);
|
|
147
20
|
});
|
|
21
|
+
return probe(["--list", "--unsupported"], JUST_PROBE_TIMEOUT_MS).then((ok) => ok ? true : probe(["--version"], JUST_FALLBACK_TIMEOUT_MS));
|
|
148
22
|
}
|
|
149
23
|
async function detect(root) {
|
|
150
|
-
const hasOpenspec =
|
|
151
|
-
const hasDocs =
|
|
24
|
+
const hasOpenspec = fs.existsSync(path.join(root, "openspec"));
|
|
25
|
+
const hasDocs = fs.existsSync(path.join(root, "docs"));
|
|
152
26
|
const hasJust = await justAvailable(root);
|
|
153
|
-
|
|
154
|
-
|
|
27
|
+
return { hasOpenspec, hasDocs, hasJust };
|
|
28
|
+
}
|
|
29
|
+
async function detectLiveShape(root) {
|
|
30
|
+
return detect(root);
|
|
155
31
|
}
|
|
156
32
|
|
|
157
33
|
// src/cli.ts
|
|
@@ -159,15 +35,15 @@ import { Context } from "cordis";
|
|
|
159
35
|
|
|
160
36
|
// src/core/server.ts
|
|
161
37
|
import http from "http";
|
|
162
|
-
import
|
|
163
|
-
import
|
|
38
|
+
import fs3 from "fs";
|
|
39
|
+
import path3 from "path";
|
|
164
40
|
import crypto from "crypto";
|
|
165
41
|
import { fileURLToPath } from "url";
|
|
166
42
|
|
|
167
43
|
// package.json
|
|
168
44
|
var package_default = {
|
|
169
45
|
name: "zdashboard",
|
|
170
|
-
version: "2.
|
|
46
|
+
version: "2.8.0",
|
|
171
47
|
description: "ZCode skill dashboard platform \u2014 pluggable viewers for zdesign/zview/zreview/zgoal",
|
|
172
48
|
type: "module",
|
|
173
49
|
bin: {
|
|
@@ -185,6 +61,9 @@ var package_default = {
|
|
|
185
61
|
"build:web": "vite build",
|
|
186
62
|
"build:node": "tsup",
|
|
187
63
|
start: "node dist/cli.js",
|
|
64
|
+
test: "vitest run",
|
|
65
|
+
typecheck: "tsc --noEmit",
|
|
66
|
+
"test:coverage": "vitest run --coverage",
|
|
188
67
|
preview: "vite preview"
|
|
189
68
|
},
|
|
190
69
|
dependencies: {
|
|
@@ -236,6 +115,7 @@ var package_default = {
|
|
|
236
115
|
"@types/react": "^18.3.12",
|
|
237
116
|
"@types/react-dom": "^18.3.1",
|
|
238
117
|
"@vitejs/plugin-react": "^4.7.0",
|
|
118
|
+
"@vitest/coverage-v8": "1.6.0",
|
|
239
119
|
autoprefixer: "^10.4.20",
|
|
240
120
|
jsdom: "^30.0.1",
|
|
241
121
|
postcss: "^8.4.49",
|
|
@@ -257,20 +137,20 @@ var package_default = {
|
|
|
257
137
|
import { Service } from "cordis";
|
|
258
138
|
|
|
259
139
|
// src/core/instance.ts
|
|
260
|
-
import
|
|
261
|
-
import
|
|
140
|
+
import fs2 from "fs";
|
|
141
|
+
import path2 from "path";
|
|
262
142
|
var RECORD_FILE = ".zdev/dashboard.json";
|
|
263
143
|
var VERIFY_TIMEOUT_MS = 1500;
|
|
264
144
|
var STOP_POLL_MS = 2e3;
|
|
265
145
|
var STOP_POLL_INTERVAL_MS = 100;
|
|
266
146
|
var STOP_FINAL_WAIT_MS = 100;
|
|
267
147
|
function recordPath(root) {
|
|
268
|
-
return
|
|
148
|
+
return path2.join(root, RECORD_FILE);
|
|
269
149
|
}
|
|
270
150
|
function readRecord(root) {
|
|
271
151
|
try {
|
|
272
152
|
const fp = recordPath(root);
|
|
273
|
-
const raw =
|
|
153
|
+
const raw = fs2.readFileSync(fp, "utf-8");
|
|
274
154
|
const rec = JSON.parse(raw);
|
|
275
155
|
if (typeof rec.pid === "number" && typeof rec.port === "number" && typeof rec.root === "string" && typeof rec.startedAt === "string") {
|
|
276
156
|
return rec;
|
|
@@ -281,20 +161,89 @@ function readRecord(root) {
|
|
|
281
161
|
}
|
|
282
162
|
}
|
|
283
163
|
function writeRecord(root, port) {
|
|
284
|
-
|
|
164
|
+
const fp = recordPath(root);
|
|
165
|
+
fs2.mkdirSync(path2.dirname(fp), { recursive: true });
|
|
166
|
+
const old = readRecord(root);
|
|
285
167
|
const rec = {
|
|
286
168
|
pid: process.pid,
|
|
287
169
|
port,
|
|
288
170
|
root,
|
|
289
171
|
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
290
172
|
};
|
|
291
|
-
|
|
173
|
+
if (old?.plugins && typeof old.plugins === "object") {
|
|
174
|
+
rec.plugins = old.plugins;
|
|
175
|
+
}
|
|
176
|
+
const tmp = fp + ".tmp";
|
|
177
|
+
fs2.writeFileSync(tmp, JSON.stringify(rec, null, 2) + "\n");
|
|
178
|
+
fs2.renameSync(tmp, fp);
|
|
179
|
+
}
|
|
180
|
+
function readPluginsConfig(root) {
|
|
181
|
+
try {
|
|
182
|
+
const rec = readRecord(root);
|
|
183
|
+
if (rec?.plugins && typeof rec.plugins === "object") {
|
|
184
|
+
return rec.plugins;
|
|
185
|
+
}
|
|
186
|
+
} catch {
|
|
187
|
+
}
|
|
188
|
+
return {};
|
|
189
|
+
}
|
|
190
|
+
function writePluginsConfig(root, plugins) {
|
|
191
|
+
const fp = recordPath(root);
|
|
192
|
+
let rec;
|
|
193
|
+
try {
|
|
194
|
+
const raw = fs2.readFileSync(fp, "utf-8");
|
|
195
|
+
rec = JSON.parse(raw);
|
|
196
|
+
} catch {
|
|
197
|
+
rec = { pid: process.pid, port: 0, root, startedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
198
|
+
}
|
|
199
|
+
rec.plugins = plugins;
|
|
200
|
+
const tmp = fp + ".tmp";
|
|
201
|
+
fs2.writeFileSync(tmp, JSON.stringify(rec, null, 2) + "\n");
|
|
202
|
+
fs2.renameSync(tmp, fp);
|
|
203
|
+
}
|
|
204
|
+
function stripDeadPluginConfig(root, manifests) {
|
|
205
|
+
try {
|
|
206
|
+
const rec = readRecord(root);
|
|
207
|
+
if (!rec?.plugins || typeof rec.plugins !== "object") return false;
|
|
208
|
+
const byMode = new Map(manifests.map((m) => [m.mode, m]));
|
|
209
|
+
const stored = rec.plugins;
|
|
210
|
+
const plugins = { ...stored };
|
|
211
|
+
let changed = false;
|
|
212
|
+
for (const mode of Object.keys(plugins)) {
|
|
213
|
+
const m = byMode.get(mode);
|
|
214
|
+
if (!m || m.external) continue;
|
|
215
|
+
const section = plugins[mode];
|
|
216
|
+
if (section === null || typeof section !== "object") continue;
|
|
217
|
+
const keys = Object.keys(section);
|
|
218
|
+
if (!m.config || Object.keys(m.config).length === 0) {
|
|
219
|
+
if (keys.length > 0) {
|
|
220
|
+
delete plugins[mode];
|
|
221
|
+
changed = true;
|
|
222
|
+
}
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
const kept = {};
|
|
226
|
+
for (const key of Object.keys(m.config)) {
|
|
227
|
+
if (key in section) kept[key] = section[key];
|
|
228
|
+
}
|
|
229
|
+
if (Object.keys(kept).length !== keys.length) {
|
|
230
|
+
if (Object.keys(kept).length === 0) delete plugins[mode];
|
|
231
|
+
else plugins[mode] = kept;
|
|
232
|
+
changed = true;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (!changed) return false;
|
|
236
|
+
writePluginsConfig(root, plugins);
|
|
237
|
+
return true;
|
|
238
|
+
} catch {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
292
241
|
}
|
|
293
242
|
function clearRecord(root) {
|
|
294
243
|
try {
|
|
295
244
|
const rec = readRecord(root);
|
|
296
245
|
if (rec && rec.pid !== process.pid) return;
|
|
297
|
-
|
|
246
|
+
fs2.unlinkSync(recordPath(root));
|
|
298
247
|
} catch {
|
|
299
248
|
}
|
|
300
249
|
}
|
|
@@ -366,7 +315,7 @@ function openUrl(url) {
|
|
|
366
315
|
// src/core/server.ts
|
|
367
316
|
import { execFile as execFile3 } from "child_process";
|
|
368
317
|
var VERSION = package_default.version;
|
|
369
|
-
var __dirname =
|
|
318
|
+
var __dirname = path3.dirname(fileURLToPath(import.meta.url));
|
|
370
319
|
var MIME = {
|
|
371
320
|
".html": "text/html; charset=utf-8",
|
|
372
321
|
".htm": "text/html; charset=utf-8",
|
|
@@ -421,8 +370,20 @@ var MIME = {
|
|
|
421
370
|
".ttf": "font/woff",
|
|
422
371
|
".map": "application/json; charset=utf-8"
|
|
423
372
|
};
|
|
424
|
-
var INJECT = `<script>(function(){
|
|
373
|
+
var INJECT = `<script>(function(){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
374
|
var PLUGIN_STATIC_PREFIX = "/__plugin/";
|
|
375
|
+
function json(res, data, status = 200) {
|
|
376
|
+
res.writeHead(status, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
|
|
377
|
+
res.end(JSON.stringify(data));
|
|
378
|
+
}
|
|
379
|
+
function guarded(req, res, token) {
|
|
380
|
+
if (req.headers["x-stop-token"] !== token) {
|
|
381
|
+
res.writeHead(403);
|
|
382
|
+
res.end("forbidden");
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
return true;
|
|
386
|
+
}
|
|
426
387
|
var ServerService = class extends Service {
|
|
427
388
|
stopToken;
|
|
428
389
|
root;
|
|
@@ -449,17 +410,43 @@ var ServerService = class extends Service {
|
|
|
449
410
|
ctx.effect(() => () => this.dispose());
|
|
450
411
|
this.refreshGitInfo();
|
|
451
412
|
this.route("/__config", (_req, res) => {
|
|
452
|
-
res
|
|
453
|
-
res.end(JSON.stringify({ stopToken: this.stopToken, version: VERSION, root: this.root, ...this.gitInfo }));
|
|
413
|
+
json(res, { stopToken: this.stopToken, version: VERSION, root: this.root, ...this.gitInfo });
|
|
454
414
|
});
|
|
455
|
-
this.route("/
|
|
456
|
-
|
|
415
|
+
this.route("/__detect", async (_req, res) => {
|
|
416
|
+
const shape = await detectLiveShape(this.root);
|
|
417
|
+
json(res, shape);
|
|
418
|
+
});
|
|
419
|
+
this.route("/favicon.svg", (req, res) => {
|
|
420
|
+
this.serveFile(path3.join(this.appDir, "favicon.svg"), req, res, false);
|
|
421
|
+
});
|
|
422
|
+
this.route("/__file-content", async (req, res) => {
|
|
423
|
+
const url = new URL(req.url || "/", "http://localhost");
|
|
424
|
+
const rawPath = url.pathname.replace(/^\/__file-content\/?/, "");
|
|
425
|
+
if (!rawPath || rawPath === "/") {
|
|
426
|
+
res.writeHead(400);
|
|
427
|
+
res.end("missing path");
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
const filePath = path3.join(this.root, decodeURIComponent(rawPath));
|
|
431
|
+
if (!filePath.startsWith(this.root + path3.sep) && filePath !== this.root) {
|
|
457
432
|
res.writeHead(403);
|
|
458
|
-
res.end("
|
|
433
|
+
res.end("Forbidden");
|
|
459
434
|
return;
|
|
460
435
|
}
|
|
461
|
-
|
|
462
|
-
|
|
436
|
+
fs3.stat(filePath, (err) => {
|
|
437
|
+
if (err) {
|
|
438
|
+
res.writeHead(404);
|
|
439
|
+
return res.end("Not found");
|
|
440
|
+
}
|
|
441
|
+
const ext = path3.extname(filePath).toLowerCase();
|
|
442
|
+
const ct = MIME[ext] ?? "application/octet-stream";
|
|
443
|
+
res.writeHead(200, { "Content-Type": ct, "Cache-Control": "no-cache" });
|
|
444
|
+
fs3.createReadStream(filePath).pipe(res);
|
|
445
|
+
});
|
|
446
|
+
});
|
|
447
|
+
this.route("/__stop", async (req, res) => {
|
|
448
|
+
if (!guarded(req, res, this.stopToken)) return;
|
|
449
|
+
json(res, { ok: true });
|
|
463
450
|
this.stop();
|
|
464
451
|
});
|
|
465
452
|
this.start(config.port);
|
|
@@ -476,13 +463,24 @@ var ServerService = class extends Service {
|
|
|
476
463
|
this.prefixStatic.set(prefix, dir);
|
|
477
464
|
this.ctx.effect(() => () => this.prefixStatic.delete(prefix));
|
|
478
465
|
}
|
|
466
|
+
pluginsConfig = {};
|
|
467
|
+
getPluginConfig(mode) {
|
|
468
|
+
return this.pluginsConfig[mode] ?? {};
|
|
469
|
+
}
|
|
470
|
+
refreshPluginsConfig() {
|
|
471
|
+
this.pluginsConfig = readPluginsConfig(this.root);
|
|
472
|
+
}
|
|
473
|
+
savePluginConfig(merged) {
|
|
474
|
+
this.pluginsConfig = merged;
|
|
475
|
+
writePluginsConfig(this.root, merged);
|
|
476
|
+
}
|
|
479
477
|
serveFile(filePath, req, res, injectHtml) {
|
|
480
|
-
|
|
478
|
+
fs3.stat(filePath, (err, stat) => {
|
|
481
479
|
if (err) {
|
|
482
480
|
res.writeHead(404);
|
|
483
481
|
return res.end("Not found");
|
|
484
482
|
}
|
|
485
|
-
const ext =
|
|
483
|
+
const ext = path3.extname(filePath).toLowerCase();
|
|
486
484
|
const ct = MIME[ext] ?? "application/octet-stream";
|
|
487
485
|
const rangeHeader = req.headers["range"];
|
|
488
486
|
let range = null;
|
|
@@ -506,7 +504,7 @@ var ServerService = class extends Service {
|
|
|
506
504
|
"Accept-Ranges": "bytes",
|
|
507
505
|
"Cache-Control": "no-cache"
|
|
508
506
|
});
|
|
509
|
-
const stream =
|
|
507
|
+
const stream = fs3.createReadStream(filePath, { start, end });
|
|
510
508
|
stream.on("error", () => {
|
|
511
509
|
try {
|
|
512
510
|
res.destroy();
|
|
@@ -516,7 +514,7 @@ var ServerService = class extends Service {
|
|
|
516
514
|
stream.pipe(res);
|
|
517
515
|
return;
|
|
518
516
|
}
|
|
519
|
-
|
|
517
|
+
fs3.readFile(filePath, (err2, data) => {
|
|
520
518
|
if (err2) {
|
|
521
519
|
res.writeHead(404);
|
|
522
520
|
return res.end("Not found");
|
|
@@ -535,9 +533,9 @@ var ServerService = class extends Service {
|
|
|
535
533
|
for (const [prefix, dir] of this.prefixStatic) {
|
|
536
534
|
if (url.indexOf(prefix) === 0) {
|
|
537
535
|
let rel = this.safeDecode(url.slice(prefix.length));
|
|
538
|
-
if (!rel || rel === "/" || !
|
|
539
|
-
const fp =
|
|
540
|
-
if (fp.indexOf(dir +
|
|
536
|
+
if (!rel || rel === "/" || !path3.extname(rel)) rel = path3.join(rel || "", "index.html");
|
|
537
|
+
const fp = path3.join(dir, rel);
|
|
538
|
+
if (fp.indexOf(dir + path3.sep) !== 0) {
|
|
541
539
|
res.writeHead(403);
|
|
542
540
|
res.end("Forbidden");
|
|
543
541
|
return false;
|
|
@@ -564,6 +562,12 @@ var ServerService = class extends Service {
|
|
|
564
562
|
rh(req, res);
|
|
565
563
|
return;
|
|
566
564
|
}
|
|
565
|
+
for (const [rp, rh2] of this.routes) {
|
|
566
|
+
if (url.startsWith(rp + "/")) {
|
|
567
|
+
rh2(req, res);
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
567
571
|
const sh = this.sses.get(url);
|
|
568
572
|
if (sh) {
|
|
569
573
|
res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" });
|
|
@@ -577,17 +581,17 @@ var ServerService = class extends Service {
|
|
|
577
581
|
if (this.servePrefix(url, req, res)) return;
|
|
578
582
|
if (url === "/" || url.indexOf("/__app/") === 0 || url.indexOf("/assets/") === 0) {
|
|
579
583
|
let fp2 = this.appDir;
|
|
580
|
-
if (url !== "/") fp2 =
|
|
581
|
-
if (url.indexOf("/__app/") === 0) fp2 =
|
|
582
|
-
if (fp2 !== this.appDir && fp2.indexOf(this.appDir +
|
|
584
|
+
if (url !== "/") fp2 = path3.join(this.appDir, this.safeDecode(url));
|
|
585
|
+
if (url.indexOf("/__app/") === 0) fp2 = path3.join(this.appDir, url.slice(7));
|
|
586
|
+
if (fp2 !== this.appDir && fp2.indexOf(this.appDir + path3.sep) !== 0) {
|
|
583
587
|
res.writeHead(403);
|
|
584
588
|
return res.end("Forbidden");
|
|
585
589
|
}
|
|
586
|
-
if (url === "/") fp2 =
|
|
590
|
+
if (url === "/") fp2 = path3.join(this.appDir, "index.html");
|
|
587
591
|
return this.serveFile(fp2, req, res, false);
|
|
588
592
|
}
|
|
589
|
-
const fp =
|
|
590
|
-
if (fp !== this.root && fp.indexOf(this.root +
|
|
593
|
+
const fp = path3.join(this.root, this.safeDecode(url));
|
|
594
|
+
if (fp !== this.root && fp.indexOf(this.root + path3.sep) !== 0) {
|
|
591
595
|
res.writeHead(403);
|
|
592
596
|
return res.end("Forbidden");
|
|
593
597
|
}
|
|
@@ -612,14 +616,16 @@ var ServerService = class extends Service {
|
|
|
612
616
|
} else throw err;
|
|
613
617
|
});
|
|
614
618
|
this.server.listen(port, "127.0.0.1", () => {
|
|
615
|
-
const
|
|
619
|
+
const addr = this.server?.address();
|
|
620
|
+
const bound = typeof addr === "object" && addr ? addr.port : port;
|
|
621
|
+
const u = `http://localhost:${bound}`;
|
|
616
622
|
const target = this.page ? `${u}#${this.page}` : u;
|
|
617
623
|
console.log(`[zdashboard] v${VERSION} dashboard -> ${u}`);
|
|
618
624
|
console.log(`[zdashboard] project -> ${this.root}`);
|
|
619
|
-
console.log(`[zdashboard] detect -> openspec:${this.det.hasOpenspec} docs:${this.det.hasDocs} just:${this.det.hasJust}
|
|
625
|
+
console.log(`[zdashboard] detect -> openspec:${this.det.hasOpenspec} docs:${this.det.hasDocs} just:${this.det.hasJust}`);
|
|
620
626
|
if (this.dataDir) console.log(`[zdashboard] data -> ${this.dataDir}`);
|
|
621
627
|
if (this.open) openUrl(target);
|
|
622
|
-
this.onListen?.(
|
|
628
|
+
this.onListen?.(bound);
|
|
623
629
|
});
|
|
624
630
|
}
|
|
625
631
|
/** 项目 git 信息(分支/脏文件数),启动探测一次,文件变更时刷新 */
|
|
@@ -671,9 +677,15 @@ var ServerService = class extends Service {
|
|
|
671
677
|
};
|
|
672
678
|
|
|
673
679
|
// src/core/reload.ts
|
|
674
|
-
import
|
|
680
|
+
import fs4 from "fs";
|
|
675
681
|
import { Service as Service2 } from "cordis";
|
|
676
|
-
var WATCH_DEBOUNCE_MS =
|
|
682
|
+
var WATCH_DEBOUNCE_MS = 300;
|
|
683
|
+
var IGNORED_DIR_RE = /^(?:\.git|node_modules|dist|\.pnpm)(?:[/\\]|$)/;
|
|
684
|
+
var IGNORED_FILE_RE = /\.(?:swp|tmp|log)$|~$|^\.DS_Store$|^Thumbs\.db$/i;
|
|
685
|
+
function isIgnoredPath(filename) {
|
|
686
|
+
if (!filename) return false;
|
|
687
|
+
return IGNORED_DIR_RE.test(filename) || IGNORED_FILE_RE.test(filename);
|
|
688
|
+
}
|
|
677
689
|
var ReloadService = class extends Service2 {
|
|
678
690
|
static inject = ["server"];
|
|
679
691
|
clients = /* @__PURE__ */ new Set();
|
|
@@ -687,13 +699,14 @@ var ReloadService = class extends Service2 {
|
|
|
687
699
|
return () => this.clients.delete(res);
|
|
688
700
|
});
|
|
689
701
|
try {
|
|
690
|
-
this.watcher =
|
|
691
|
-
if (
|
|
702
|
+
this.watcher = fs4.watch(config.root, { recursive: true }, (eventType, filename) => {
|
|
703
|
+
if (isIgnoredPath(filename)) return;
|
|
692
704
|
if (this.timer) clearTimeout(this.timer);
|
|
693
705
|
this.timer = setTimeout(() => {
|
|
694
706
|
this.ctx.server.refreshGitInfo();
|
|
695
707
|
this.broadcast("reload");
|
|
696
708
|
this.broadcast("files");
|
|
709
|
+
this.timer = void 0;
|
|
697
710
|
}, WATCH_DEBOUNCE_MS);
|
|
698
711
|
});
|
|
699
712
|
} catch {
|
|
@@ -711,6 +724,10 @@ data: ${JSON.stringify(data == null ? "" : data)}
|
|
|
711
724
|
}
|
|
712
725
|
}
|
|
713
726
|
}
|
|
727
|
+
/** 插件专用频道:plugin:<mode>:<event>,客户端 usePluginData subscribe 消费 */
|
|
728
|
+
broadcastPlugin(mode, event, data = "") {
|
|
729
|
+
this.broadcast(`plugin:${mode}:${event}`, data);
|
|
730
|
+
}
|
|
714
731
|
dispose() {
|
|
715
732
|
if (this.watcher) {
|
|
716
733
|
try {
|
|
@@ -732,25 +749,26 @@ data: ${JSON.stringify(data == null ? "" : data)}
|
|
|
732
749
|
};
|
|
733
750
|
|
|
734
751
|
// src/server/spec-scan.ts
|
|
735
|
-
import fs7 from "fs";
|
|
736
|
-
import path6 from "path";
|
|
737
|
-
|
|
738
|
-
// src/server/walk.ts
|
|
739
752
|
import fs6 from "fs";
|
|
740
753
|
import path5 from "path";
|
|
754
|
+
|
|
755
|
+
// src/server/walk.ts
|
|
756
|
+
import fs5 from "fs";
|
|
757
|
+
import path4 from "path";
|
|
741
758
|
function walkDir(root, opts = {}) {
|
|
742
759
|
const { skip = /* @__PURE__ */ new Set(), maxDepth, onFile, onDir } = opts;
|
|
743
760
|
function walk(dir, rel, depth) {
|
|
744
761
|
if (typeof maxDepth === "number" && depth > maxDepth) return;
|
|
745
762
|
let ents;
|
|
746
763
|
try {
|
|
747
|
-
ents =
|
|
764
|
+
ents = fs5.readdirSync(dir, { withFileTypes: true });
|
|
748
765
|
} catch {
|
|
749
766
|
return;
|
|
750
767
|
}
|
|
751
768
|
for (const ent of ents) {
|
|
752
|
-
if (ent.name.startsWith(".")
|
|
753
|
-
|
|
769
|
+
if (ent.name.startsWith(".")) continue;
|
|
770
|
+
if (skip.has(ent.name)) continue;
|
|
771
|
+
const abs = path4.join(dir, ent.name);
|
|
754
772
|
const r = rel ? `${rel}/${ent.name}` : ent.name;
|
|
755
773
|
if (ent.isDirectory()) {
|
|
756
774
|
onDir?.(abs, r);
|
|
@@ -764,7 +782,7 @@ function walkDir(root, opts = {}) {
|
|
|
764
782
|
}
|
|
765
783
|
|
|
766
784
|
// src/server/spec-scan.ts
|
|
767
|
-
function buildTree(paths) {
|
|
785
|
+
function buildTree(paths, defaultExpandDepth = 2) {
|
|
768
786
|
const root = [];
|
|
769
787
|
const map = /* @__PURE__ */ new Map();
|
|
770
788
|
for (const p of paths) {
|
|
@@ -775,10 +793,14 @@ function buildTree(paths) {
|
|
|
775
793
|
const part = parts[i];
|
|
776
794
|
current = current ? `${current}/${part}` : part;
|
|
777
795
|
const isLast = i === parts.length - 1;
|
|
796
|
+
const depth = i;
|
|
778
797
|
if (!map.has(current)) {
|
|
798
|
+
const isDir = !isLast;
|
|
799
|
+
const collapsed = isDir && typeof defaultExpandDepth === "number" && depth > defaultExpandDepth;
|
|
779
800
|
const node = {
|
|
780
801
|
name: part,
|
|
781
802
|
kind: isLast ? "file" : "dir",
|
|
803
|
+
...collapsed ? { defaultCollapsed: true } : {},
|
|
782
804
|
...isLast ? { path: p } : { children: [] }
|
|
783
805
|
};
|
|
784
806
|
map.set(current, node);
|
|
@@ -798,88 +820,53 @@ function buildTree(paths) {
|
|
|
798
820
|
sortNodes(root);
|
|
799
821
|
return root;
|
|
800
822
|
}
|
|
801
|
-
function
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
const active = [];
|
|
806
|
-
const archived = [];
|
|
807
|
-
for (const ent of fs7.readdirSync(changesDir, { withFileTypes: true })) {
|
|
808
|
-
if (!ent.isDirectory() || ent.name.startsWith(".") || ent.name === "archive") continue;
|
|
809
|
-
const relBase = `openspec/changes/${ent.name}`;
|
|
810
|
-
const paths = [];
|
|
811
|
-
walkDir(path6.join(changesDir, ent.name), { maxDepth: 4, onFile: (_, rel) => paths.push(rel ? `${relBase}/${rel}` : relBase) });
|
|
812
|
-
const children = buildTree(paths).map((n) => ({ ...n, path: n.path ? `${relBase}/${n.path}` : void 0 }));
|
|
813
|
-
active.push({ name: ent.name, kind: "dir", children });
|
|
823
|
+
function prefixPaths(nodes, prefix) {
|
|
824
|
+
return nodes.map((n) => {
|
|
825
|
+
if (n.kind === "file" && n.path) {
|
|
826
|
+
return { ...n, path: `${prefix}/${n.path}` };
|
|
814
827
|
}
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
if (fs7.existsSync(archiveDir)) {
|
|
818
|
-
for (const ent of fs7.readdirSync(archiveDir, { withFileTypes: true })) {
|
|
819
|
-
if (!ent.isDirectory() || ent.name.startsWith(".")) continue;
|
|
820
|
-
const relBase = `openspec/changes/archive/${ent.name}`;
|
|
821
|
-
const paths = [];
|
|
822
|
-
walkDir(path6.join(archiveDir, ent.name), { maxDepth: 4, onFile: (_, rel) => paths.push(rel ? `${relBase}/${rel}` : relBase) });
|
|
823
|
-
const children = buildTree(paths).map((n) => ({ ...n, path: n.path ? `${relBase}/${n.path}` : void 0 }));
|
|
824
|
-
archived.push({ name: ent.name, kind: "dir", children });
|
|
825
|
-
}
|
|
826
|
-
archived.sort((a, b) => b.name.localeCompare(a.name));
|
|
827
|
-
}
|
|
828
|
-
if (active.length) tree.push({ name: `changes (${active.length})`, kind: "dir", children: active });
|
|
829
|
-
if (archived.length) tree.push({ name: `archive (${archived.length})`, kind: "dir", defaultCollapsed: true, children: archived });
|
|
830
|
-
const specsDir = path6.join(root, "openspec", "specs");
|
|
831
|
-
if (fs7.existsSync(specsDir)) {
|
|
832
|
-
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 }));
|
|
835
|
-
if (specs.length) tree.push({ name: "specs", kind: "dir", children: specs });
|
|
828
|
+
if (n.children) {
|
|
829
|
+
return { ...n, children: prefixPaths(n.children, prefix) };
|
|
836
830
|
}
|
|
837
|
-
|
|
838
|
-
|
|
831
|
+
return n;
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
function scanTree(root, scanDirs, opts) {
|
|
835
|
+
const tree = [];
|
|
836
|
+
const defaultExpandDepth = typeof opts?.defaultExpandDepth === "number" ? opts.defaultExpandDepth : 2;
|
|
837
|
+
const dirs = Array.isArray(scanDirs) ? scanDirs.filter((d) => typeof d === "string" && d) : [];
|
|
838
|
+
for (const dir of dirs) {
|
|
839
|
+
const dirPath = path5.join(root, dir);
|
|
840
|
+
if (!fs6.existsSync(dirPath)) continue;
|
|
839
841
|
const paths = [];
|
|
840
|
-
walkDir(
|
|
841
|
-
const
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
const etc = [];
|
|
846
|
-
try {
|
|
847
|
-
for (const ent of fs7.readdirSync(root, { withFileTypes: true })) {
|
|
848
|
-
if (ent.name.startsWith(".") || skip.has(ent.name)) continue;
|
|
849
|
-
const ext = path6.extname(ent.name).toLowerCase();
|
|
850
|
-
if (ent.isFile() && (ext === ".md" || ext === ".markdown")) etc.push({ name: ent.name, kind: "file", path: ent.name });
|
|
842
|
+
walkDir(dirPath, { maxDepth: 4, onFile: (_, rel) => paths.push(rel) });
|
|
843
|
+
const prefix = dir;
|
|
844
|
+
const children = prefixPaths(buildTree(paths, defaultExpandDepth), prefix);
|
|
845
|
+
if (children.length) {
|
|
846
|
+
tree.push({ name: `${dir} (${children.length})`, kind: "dir", children });
|
|
851
847
|
}
|
|
852
|
-
} catch (e) {
|
|
853
|
-
console.error("[zdashboard] scan root etc failed:", e);
|
|
854
848
|
}
|
|
855
|
-
etc.sort((a, b) => a.name.localeCompare(b.name));
|
|
856
|
-
if (etc.length) tree.push({ name: `other (${etc.length})`, kind: "dir", children: etc });
|
|
857
849
|
return tree;
|
|
858
850
|
}
|
|
859
851
|
|
|
860
852
|
// src/core/tree.ts
|
|
853
|
+
var CONVENTION_SCAN_DIRS = ["openspec", "docs"];
|
|
861
854
|
var apply = {
|
|
862
855
|
inject: ["server"],
|
|
863
856
|
apply(ctx, config) {
|
|
864
|
-
let cached = null;
|
|
865
|
-
const getDet = async () => {
|
|
866
|
-
if (cached?.v) return cached.v;
|
|
867
|
-
if (!cached) cached = { p: detect(config.root).catch(() => ({ hasOpenspec: false, hasDocs: false, hasJust: false, hasBugs: false })) };
|
|
868
|
-
const d = await cached.p;
|
|
869
|
-
cached.v = d;
|
|
870
|
-
return d;
|
|
871
|
-
};
|
|
872
857
|
const server = ctx.server;
|
|
873
858
|
if (server?.route) {
|
|
874
|
-
server.route("/__files", async (
|
|
859
|
+
server.route("/__files", async (req, res) => {
|
|
875
860
|
try {
|
|
876
|
-
const
|
|
877
|
-
const
|
|
861
|
+
const url = new URL(req.url || "/", "http://localhost");
|
|
862
|
+
const wt = url.searchParams.get("wt");
|
|
863
|
+
const scanRoot = wt ? decodeURIComponent(wt) : config.root;
|
|
864
|
+
const tree = scanTree(scanRoot, [...CONVENTION_SCAN_DIRS]);
|
|
878
865
|
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
|
|
879
|
-
res.end(JSON.stringify({ tree
|
|
866
|
+
res.end(JSON.stringify({ tree }));
|
|
880
867
|
} catch {
|
|
881
868
|
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
|
|
882
|
-
res.end(JSON.stringify({ tree: []
|
|
869
|
+
res.end(JSON.stringify({ tree: [] }));
|
|
883
870
|
}
|
|
884
871
|
});
|
|
885
872
|
}
|
|
@@ -966,20 +953,83 @@ var apply2 = {
|
|
|
966
953
|
|
|
967
954
|
// src/core/manifest.ts
|
|
968
955
|
import { Service as Service3 } from "cordis";
|
|
956
|
+
|
|
957
|
+
// src/core/read-body.ts
|
|
958
|
+
async function readBody(req) {
|
|
959
|
+
return new Promise((resolve) => {
|
|
960
|
+
let data = "";
|
|
961
|
+
req.on("data", (chunk) => {
|
|
962
|
+
data += chunk;
|
|
963
|
+
});
|
|
964
|
+
req.on("end", () => resolve(data));
|
|
965
|
+
req.on("error", () => resolve(""));
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// src/core/manifest.ts
|
|
969
970
|
var DashboardService = class extends Service3 {
|
|
970
|
-
static inject = ["server"];
|
|
971
|
+
static inject = ["server", "reload"];
|
|
971
972
|
plugins = /* @__PURE__ */ new Map();
|
|
973
|
+
reloadRef;
|
|
972
974
|
constructor(ctx) {
|
|
973
975
|
super(ctx, "dashboard");
|
|
974
976
|
this.ctx.effect(() => () => this.plugins.clear());
|
|
977
|
+
this.reloadRef = ctx.reload;
|
|
975
978
|
this.ctx.server.route("/__plugins", (_req, res) => {
|
|
976
979
|
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
|
|
977
980
|
res.end(JSON.stringify({ plugins: Array.from(this.plugins.values()) }));
|
|
978
981
|
});
|
|
982
|
+
this.ctx.server.route("/__plugins/config", async (req, res) => {
|
|
983
|
+
const out = {};
|
|
984
|
+
if (req.method === "GET") {
|
|
985
|
+
for (const m of this.plugins.values()) {
|
|
986
|
+
const stored = this.ctx.server.getPluginConfig(m.mode);
|
|
987
|
+
const schema = m.config ?? {};
|
|
988
|
+
const merged = {};
|
|
989
|
+
for (const [key, field] of Object.entries(schema)) {
|
|
990
|
+
merged[key] = key in stored ? stored[key] : field.default ?? "";
|
|
991
|
+
}
|
|
992
|
+
if (Object.keys(merged).length) out[m.mode] = merged;
|
|
993
|
+
}
|
|
994
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
|
|
995
|
+
res.end(JSON.stringify(out));
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
if (req.method === "POST") {
|
|
999
|
+
if (req.headers["x-stop-token"] !== this.ctx.server.stopToken) {
|
|
1000
|
+
res.writeHead(403);
|
|
1001
|
+
res.end("forbidden");
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
try {
|
|
1005
|
+
const body = JSON.parse(await readBody(req));
|
|
1006
|
+
const merged = {};
|
|
1007
|
+
for (const mode of Object.keys(body)) {
|
|
1008
|
+
merged[mode] = body[mode] ?? {};
|
|
1009
|
+
}
|
|
1010
|
+
this.ctx.server.savePluginConfig(merged);
|
|
1011
|
+
for (const mode of Object.keys(body)) {
|
|
1012
|
+
try {
|
|
1013
|
+
this.reloadRef.broadcast("config", { plugin: mode });
|
|
1014
|
+
} catch {
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
|
|
1018
|
+
res.end('{"ok":true}');
|
|
1019
|
+
} catch (e) {
|
|
1020
|
+
console.error("[config] POST error", e);
|
|
1021
|
+
res.writeHead(400);
|
|
1022
|
+
res.end('{"error":"bad request"}');
|
|
1023
|
+
}
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
res.writeHead(405);
|
|
1027
|
+
res.end('{"error":"method not allowed"}');
|
|
1028
|
+
});
|
|
979
1029
|
}
|
|
980
|
-
register(
|
|
981
|
-
this.plugins.set(
|
|
982
|
-
this.ctx.effect(() => () => this.plugins.delete(
|
|
1030
|
+
register(manifest6) {
|
|
1031
|
+
this.plugins.set(manifest6.mode, manifest6);
|
|
1032
|
+
this.ctx.effect(() => () => this.plugins.delete(manifest6.mode));
|
|
983
1033
|
}
|
|
984
1034
|
list() {
|
|
985
1035
|
return Array.from(this.plugins.values());
|
|
@@ -987,6 +1037,9 @@ var DashboardService = class extends Service3 {
|
|
|
987
1037
|
get(mode) {
|
|
988
1038
|
return this.plugins.get(mode);
|
|
989
1039
|
}
|
|
1040
|
+
getConfig(mode) {
|
|
1041
|
+
return this.ctx.server.getPluginConfig(mode);
|
|
1042
|
+
}
|
|
990
1043
|
};
|
|
991
1044
|
|
|
992
1045
|
// src/server/just-runner.ts
|
|
@@ -1128,403 +1181,202 @@ var JustRunner = class {
|
|
|
1128
1181
|
}
|
|
1129
1182
|
};
|
|
1130
1183
|
|
|
1131
|
-
// src/
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
inject: ["server", "dashboard"],
|
|
1146
|
-
apply(ctx, config) {
|
|
1147
|
-
const root = config.root;
|
|
1148
|
-
ctx.inject(["server"], () => {
|
|
1149
|
-
if (!ctx.server?.route) return;
|
|
1150
|
-
const runner = new JustRunner(root);
|
|
1151
|
-
ctx.effect(() => () => runner.stop());
|
|
1152
|
-
ctx.dashboard.register({ mode: "just", label: "Just Runner", icon: "\u{1F4DC}", description: "Just \u591A\u4EFB\u52A1\u5E76\u53D1\u6267\u884C\u4E0E\u65E5\u5FD7" });
|
|
1153
|
-
ctx.server.route("/__just/recipes", async (_req, res) => {
|
|
1154
|
-
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
|
|
1155
|
-
try {
|
|
1156
|
-
const recipes = await runner.recipes();
|
|
1157
|
-
res.end(JSON.stringify(recipes));
|
|
1158
|
-
} catch {
|
|
1159
|
-
res.end(JSON.stringify([]));
|
|
1184
|
+
// src/sdk/server.ts
|
|
1185
|
+
function definePlugin(def) {
|
|
1186
|
+
const { manifest: manifest6, setup } = def;
|
|
1187
|
+
return {
|
|
1188
|
+
inject: ["server", "dashboard", "reload"],
|
|
1189
|
+
apply(ctx) {
|
|
1190
|
+
const disposers = [];
|
|
1191
|
+
ctx.effect(() => () => {
|
|
1192
|
+
for (const fn of disposers.splice(0)) {
|
|
1193
|
+
try {
|
|
1194
|
+
fn();
|
|
1195
|
+
} catch (e) {
|
|
1196
|
+
console.error(`[zdashboard] plugin ${manifest6.mode} dispose error:`, e);
|
|
1197
|
+
}
|
|
1160
1198
|
}
|
|
1161
1199
|
});
|
|
1162
|
-
ctx.
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
`);
|
|
1167
|
-
});
|
|
1168
|
-
return unsub;
|
|
1169
|
-
});
|
|
1170
|
-
const handleAction = async (req, res, act) => {
|
|
1171
|
-
if (req.headers["x-stop-token"] !== ctx.server.stopToken) {
|
|
1172
|
-
res.writeHead(403);
|
|
1173
|
-
res.end("forbidden");
|
|
1174
|
-
return;
|
|
1175
|
-
}
|
|
1176
|
-
const body = await readBody(req);
|
|
1177
|
-
let recipe;
|
|
1200
|
+
ctx.dashboard.register(manifest6);
|
|
1201
|
+
const respondWith = (handler) => async (req, res) => {
|
|
1202
|
+
if (res.headersSent) return;
|
|
1178
1203
|
try {
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
}
|
|
1182
|
-
|
|
1183
|
-
if (
|
|
1184
|
-
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
|
|
1185
|
-
res.end('{"ok":true}');
|
|
1186
|
-
return;
|
|
1187
|
-
}
|
|
1188
|
-
if (act !== "stop") {
|
|
1189
|
-
if (!recipe) {
|
|
1190
|
-
res.writeHead(400);
|
|
1191
|
-
res.end('{"error":"no recipe"}');
|
|
1192
|
-
return;
|
|
1193
|
-
}
|
|
1194
|
-
const recipes = await runner.recipes();
|
|
1195
|
-
if (!recipes.some((r) => r.name === recipe)) {
|
|
1196
|
-
res.writeHead(403);
|
|
1197
|
-
res.end('{"error":"unknown recipe"}');
|
|
1198
|
-
return;
|
|
1199
|
-
}
|
|
1200
|
-
runner.start(recipe);
|
|
1201
|
-
} else {
|
|
1202
|
-
runner.stop(recipe || void 0);
|
|
1204
|
+
const data = await handler(req, res);
|
|
1205
|
+
if (data !== void 0 && !res.writableEnded) json(res, data);
|
|
1206
|
+
} catch (e) {
|
|
1207
|
+
console.error(`[zdashboard] plugin ${manifest6.mode} route error:`, e);
|
|
1208
|
+
if (!res.headersSent) json(res, { error: "internal" }, 500);
|
|
1203
1209
|
}
|
|
1204
|
-
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
|
|
1205
|
-
res.end(JSON.stringify(runner.list()));
|
|
1206
1210
|
};
|
|
1207
|
-
const
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1211
|
+
const pctx = {
|
|
1212
|
+
mode: manifest6.mode,
|
|
1213
|
+
config() {
|
|
1214
|
+
const schema = manifest6.config ?? {};
|
|
1215
|
+
const stored = ctx.server.getPluginConfig(manifest6.mode) ?? {};
|
|
1216
|
+
const defaults = Object.fromEntries(
|
|
1217
|
+
Object.entries(schema).map(([key, field]) => [key, field.default])
|
|
1218
|
+
);
|
|
1219
|
+
return { ...defaults, ...stored };
|
|
1220
|
+
},
|
|
1221
|
+
route(path12, handler) {
|
|
1222
|
+
ctx.server.route(path12, respondWith(handler));
|
|
1223
|
+
},
|
|
1224
|
+
guardedRoute(path12, handler) {
|
|
1225
|
+
const token = () => ctx.server.stopToken;
|
|
1226
|
+
ctx.server.route(path12, (req, res) => {
|
|
1227
|
+
if (!guarded(req, res, token())) return;
|
|
1228
|
+
void respondWith(handler)(req, res);
|
|
1214
1229
|
});
|
|
1215
|
-
}
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
};
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
inject: ["server", "dashboard"],
|
|
1228
|
-
apply(ctx, config) {
|
|
1229
|
-
const root = config.root;
|
|
1230
|
-
ctx.inject(["server"], () => {
|
|
1231
|
-
if (!ctx.server?.route) return;
|
|
1232
|
-
ctx.dashboard.register({ mode: "bugs", label: "\u7985\u9053 Bugs", icon: "\u{1F3AF}", description: "\u53EA\u8BFB bug \u5217\u8868" });
|
|
1233
|
-
ctx.server.route("/__bugs", async (_req, res) => {
|
|
1234
|
-
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
|
|
1235
|
-
try {
|
|
1236
|
-
const result = await fetchBugs(root);
|
|
1237
|
-
res.end(JSON.stringify(result));
|
|
1238
|
-
} catch (e) {
|
|
1239
|
-
res.end(JSON.stringify({ ok: false, error: e instanceof Error ? e.message : "unknown error" }));
|
|
1230
|
+
},
|
|
1231
|
+
sse(path12, handler) {
|
|
1232
|
+
ctx.server.sse(path12, handler);
|
|
1233
|
+
},
|
|
1234
|
+
static(prefix, dir) {
|
|
1235
|
+
ctx.server.static(prefix, dir);
|
|
1236
|
+
},
|
|
1237
|
+
broadcast(event, data) {
|
|
1238
|
+
ctx.reload?.broadcast(`plugin:${manifest6.mode}:${event}`, data ?? "");
|
|
1239
|
+
},
|
|
1240
|
+
onDispose(fn) {
|
|
1241
|
+
disposers.push(fn);
|
|
1240
1242
|
}
|
|
1241
|
-
});
|
|
1242
|
-
});
|
|
1243
|
-
}
|
|
1244
|
-
};
|
|
1245
|
-
|
|
1246
|
-
// src/server/review-store.ts
|
|
1247
|
-
import fs8 from "fs";
|
|
1248
|
-
import path7 from "path";
|
|
1249
|
-
import YAML2 from "yaml";
|
|
1250
|
-
var REVIEW_DIR = ".zreview";
|
|
1251
|
-
var REVIEW_FILE = path7.join(REVIEW_DIR, "review.yaml");
|
|
1252
|
-
var DOCS_DIR = path7.join(REVIEW_DIR, "docs");
|
|
1253
|
-
var DIAGRAMS_DIR = path7.join(REVIEW_DIR, "diagrams");
|
|
1254
|
-
var ReviewStore = class {
|
|
1255
|
-
root;
|
|
1256
|
-
onChange;
|
|
1257
|
-
constructor(root, onChange) {
|
|
1258
|
-
this.root = root;
|
|
1259
|
-
this.onChange = onChange;
|
|
1260
|
-
}
|
|
1261
|
-
exists() {
|
|
1262
|
-
return fs8.existsSync(path7.join(this.root, REVIEW_FILE));
|
|
1263
|
-
}
|
|
1264
|
-
read() {
|
|
1265
|
-
const fullPath = path7.join(this.root, REVIEW_FILE);
|
|
1266
|
-
if (!fs8.existsSync(fullPath)) {
|
|
1267
|
-
return { status: "draft", summary: "", documents: [], codebases: [], diagrams: [], items: [] };
|
|
1268
|
-
}
|
|
1269
|
-
try {
|
|
1270
|
-
const parsed = YAML2.parse(fs8.readFileSync(fullPath, "utf8"));
|
|
1271
|
-
if (!parsed || !Array.isArray(parsed.items)) {
|
|
1272
|
-
return { status: "draft", summary: parsed?.summary ?? "", documents: parsed?.documents ?? [], codebases: parsed?.codebases ?? [], diagrams: parsed?.diagrams ?? [], items: [] };
|
|
1273
|
-
}
|
|
1274
|
-
return {
|
|
1275
|
-
status: parsed.status ?? "draft",
|
|
1276
|
-
summary: parsed.summary ?? "",
|
|
1277
|
-
documents: parsed.documents ?? [],
|
|
1278
|
-
codebases: parsed.codebases ?? [],
|
|
1279
|
-
diagrams: parsed.diagrams ?? [],
|
|
1280
|
-
items: parsed.items
|
|
1281
1243
|
};
|
|
1282
|
-
|
|
1283
|
-
return { status: "draft", summary: "", documents: [], codebases: [], diagrams: [], items: [] };
|
|
1284
|
-
}
|
|
1285
|
-
}
|
|
1286
|
-
write(data) {
|
|
1287
|
-
fs8.mkdirSync(path7.join(this.root, REVIEW_DIR), { recursive: true });
|
|
1288
|
-
fs8.writeFileSync(path7.join(this.root, REVIEW_FILE), YAML2.stringify(data), "utf8");
|
|
1289
|
-
this.onChange?.();
|
|
1290
|
-
}
|
|
1291
|
-
updateItem(id, patch) {
|
|
1292
|
-
const data = this.read();
|
|
1293
|
-
const item = this.findItem(data.items, id);
|
|
1294
|
-
if (!item) throw new Error(`item ${id} not found`);
|
|
1295
|
-
if (patch.answer !== void 0) item.answer = patch.answer;
|
|
1296
|
-
if (patch.state !== void 0) item.state = patch.state;
|
|
1297
|
-
if (patch.priority !== void 0) item.priority = patch.priority;
|
|
1298
|
-
if (patch.title !== void 0) item.title = patch.title;
|
|
1299
|
-
if (patch.state === "answered" && !patch.answer && !item.answer) item.answer = "";
|
|
1300
|
-
this.write(data);
|
|
1301
|
-
return data;
|
|
1302
|
-
}
|
|
1303
|
-
addChild(parentId, title, priority = "medium") {
|
|
1304
|
-
const data = this.read();
|
|
1305
|
-
const parent = this.findItem(data.items, parentId);
|
|
1306
|
-
if (!parent) throw new Error(`parent ${parentId} not found`);
|
|
1307
|
-
if (!parent.children) parent.children = [];
|
|
1308
|
-
const newId = `${parentId}-${parent.children.length + 1}`;
|
|
1309
|
-
parent.children.push({
|
|
1310
|
-
id: newId,
|
|
1311
|
-
type: "decomposition",
|
|
1312
|
-
state: "open",
|
|
1313
|
-
title,
|
|
1314
|
-
priority
|
|
1315
|
-
});
|
|
1316
|
-
this.write(data);
|
|
1317
|
-
return data;
|
|
1318
|
-
}
|
|
1319
|
-
removeItem(id) {
|
|
1320
|
-
const data = this.read();
|
|
1321
|
-
const removed = this.removeItemFromList(data.items, id);
|
|
1322
|
-
if (!removed) throw new Error(`item ${id} not found`);
|
|
1323
|
-
this.write(data);
|
|
1324
|
-
return data;
|
|
1325
|
-
}
|
|
1326
|
-
setStatus(status) {
|
|
1327
|
-
const data = this.read();
|
|
1328
|
-
if (status === "passed" && this.hasOpenItems(data.items)) {
|
|
1329
|
-
throw new Error("\u5B58\u5728\u672A\u5904\u7406\u7684\u8BC4\u5BA1\u9879(open),\u4E0D\u80FD\u901A\u8FC7");
|
|
1244
|
+
setup(pctx);
|
|
1330
1245
|
}
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
return [];
|
|
1342
|
-
}
|
|
1343
|
-
}
|
|
1344
|
-
readDoc(name) {
|
|
1345
|
-
const fullPath = path7.join(this.root, DOCS_DIR, name);
|
|
1346
|
-
if (!fs8.existsSync(fullPath)) throw new Error(`doc ${name} not found`);
|
|
1347
|
-
return fs8.readFileSync(fullPath, "utf8");
|
|
1348
|
-
}
|
|
1349
|
-
listDiagrams() {
|
|
1350
|
-
const dir = path7.join(this.root, DIAGRAMS_DIR);
|
|
1351
|
-
if (!fs8.existsSync(dir)) return [];
|
|
1352
|
-
try {
|
|
1353
|
-
return fs8.readdirSync(dir).filter((f) => /\.html$/i.test(f)).sort();
|
|
1354
|
-
} catch {
|
|
1355
|
-
return [];
|
|
1356
|
-
}
|
|
1357
|
-
}
|
|
1358
|
-
readDiagram(name) {
|
|
1359
|
-
const fullPath = path7.join(this.root, DIAGRAMS_DIR, name);
|
|
1360
|
-
if (!fs8.existsSync(fullPath)) throw new Error(`diagram ${name} not found`);
|
|
1361
|
-
return fs8.readFileSync(fullPath, "utf8");
|
|
1362
|
-
}
|
|
1363
|
-
findItem(items, id) {
|
|
1364
|
-
for (const item of items) {
|
|
1365
|
-
if (item.id === id) return item;
|
|
1366
|
-
if (item.children) {
|
|
1367
|
-
const found = this.findItem(item.children, id);
|
|
1368
|
-
if (found) return found;
|
|
1369
|
-
}
|
|
1370
|
-
}
|
|
1371
|
-
return null;
|
|
1372
|
-
}
|
|
1373
|
-
removeItemFromList(items, id) {
|
|
1374
|
-
for (let i = 0; i < items.length; i++) {
|
|
1375
|
-
if (items[i].id === id) {
|
|
1376
|
-
items.splice(i, 1);
|
|
1377
|
-
return true;
|
|
1378
|
-
}
|
|
1379
|
-
if (items[i].children && this.removeItemFromList(items[i].children, id)) {
|
|
1380
|
-
return true;
|
|
1381
|
-
}
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
// src/plugins/builtin.ts
|
|
1250
|
+
function defineBuiltin(def) {
|
|
1251
|
+
let currentRoot = "";
|
|
1252
|
+
const inner = definePlugin({
|
|
1253
|
+
manifest: def.manifest,
|
|
1254
|
+
setup(pctx) {
|
|
1255
|
+
def.setup(pctx, currentRoot);
|
|
1382
1256
|
}
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1257
|
+
});
|
|
1258
|
+
return {
|
|
1259
|
+
inject: inner.inject,
|
|
1260
|
+
manifest: def.manifest,
|
|
1261
|
+
// cli 启动期死键剥离按 manifest 判定(config 声明/external)
|
|
1262
|
+
apply(ctx, config) {
|
|
1263
|
+
currentRoot = config?.root ?? "";
|
|
1264
|
+
inner.apply(ctx, config);
|
|
1389
1265
|
}
|
|
1390
|
-
|
|
1391
|
-
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
// src/plugins/just/manifest.ts
|
|
1270
|
+
var manifest = {
|
|
1271
|
+
mode: "just",
|
|
1272
|
+
label: "Just Runner",
|
|
1273
|
+
icon: "\u{1F4DC}",
|
|
1274
|
+
description: "Just \u4EFB\u52A1\u65E5\u5FD7\u4E0E\u6267\u884C",
|
|
1275
|
+
order: 50
|
|
1392
1276
|
};
|
|
1393
1277
|
|
|
1394
|
-
// src/plugins/
|
|
1395
|
-
var
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
const
|
|
1399
|
-
ctx.
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
if (req.headers["x-stop-token"] !== ctx.server.stopToken) {
|
|
1411
|
-
res.writeHead(403);
|
|
1412
|
-
res.end("forbidden");
|
|
1413
|
-
return;
|
|
1414
|
-
}
|
|
1415
|
-
try {
|
|
1416
|
-
const body = JSON.parse(await readBody(req) || "{}");
|
|
1417
|
-
const data = reviewStore.updateItem(body.id, {
|
|
1418
|
-
answer: body.answer,
|
|
1419
|
-
state: body.state,
|
|
1420
|
-
priority: body.priority,
|
|
1421
|
-
title: body.title
|
|
1422
|
-
});
|
|
1423
|
-
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
|
|
1424
|
-
res.end(JSON.stringify(data));
|
|
1425
|
-
} catch (e) {
|
|
1426
|
-
res.writeHead(400, { "Content-Type": "application/json; charset=utf-8" });
|
|
1427
|
-
res.end(JSON.stringify({ error: e instanceof Error ? e.message : "unknown error" }));
|
|
1428
|
-
}
|
|
1429
|
-
});
|
|
1430
|
-
ctx.server.route("/__review/item/add-child", async (req, res) => {
|
|
1431
|
-
if (req.headers["x-stop-token"] !== ctx.server.stopToken) {
|
|
1432
|
-
res.writeHead(403);
|
|
1433
|
-
res.end("forbidden");
|
|
1434
|
-
return;
|
|
1435
|
-
}
|
|
1436
|
-
try {
|
|
1437
|
-
const body = JSON.parse(await readBody(req) || "{}");
|
|
1438
|
-
const data = reviewStore.addChild(body.parentId, body.title, body.priority);
|
|
1439
|
-
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
|
|
1440
|
-
res.end(JSON.stringify(data));
|
|
1441
|
-
} catch (e) {
|
|
1442
|
-
res.writeHead(400, { "Content-Type": "application/json; charset=utf-8" });
|
|
1443
|
-
res.end(JSON.stringify({ error: e instanceof Error ? e.message : "unknown error" }));
|
|
1444
|
-
}
|
|
1445
|
-
});
|
|
1446
|
-
ctx.server.route("/__review/item/remove", async (req, res) => {
|
|
1447
|
-
if (req.headers["x-stop-token"] !== ctx.server.stopToken) {
|
|
1448
|
-
res.writeHead(403);
|
|
1449
|
-
res.end("forbidden");
|
|
1450
|
-
return;
|
|
1451
|
-
}
|
|
1278
|
+
// src/plugins/just/index.ts
|
|
1279
|
+
var apply3 = defineBuiltin({
|
|
1280
|
+
manifest,
|
|
1281
|
+
setup(ctx, root) {
|
|
1282
|
+
const runner = new JustRunner(root);
|
|
1283
|
+
ctx.onDispose(() => runner.stop());
|
|
1284
|
+
ctx.route("/__just/recipes", async () => {
|
|
1285
|
+
try {
|
|
1286
|
+
return await runner.recipes();
|
|
1287
|
+
} catch {
|
|
1288
|
+
return [];
|
|
1289
|
+
}
|
|
1290
|
+
});
|
|
1291
|
+
ctx.route("/__just/tasks", async () => runner.list());
|
|
1292
|
+
ctx.sse("/__just/logs", (res) => {
|
|
1293
|
+
const unsub = runner.subscribe((ev) => {
|
|
1452
1294
|
try {
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
|
|
1456
|
-
res.end(JSON.stringify(data));
|
|
1457
|
-
} catch (e) {
|
|
1458
|
-
res.writeHead(400, { "Content-Type": "application/json; charset=utf-8" });
|
|
1459
|
-
res.end(JSON.stringify({ error: e instanceof Error ? e.message : "unknown error" }));
|
|
1295
|
+
ctx.broadcast(ev.type, ev);
|
|
1296
|
+
} catch {
|
|
1460
1297
|
}
|
|
1298
|
+
res.write(`data: ${JSON.stringify(ev)}
|
|
1299
|
+
|
|
1300
|
+
`);
|
|
1461
1301
|
});
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1302
|
+
return unsub;
|
|
1303
|
+
});
|
|
1304
|
+
const handleAction = async (req, res, act) => {
|
|
1305
|
+
const body = await readBody(req);
|
|
1306
|
+
let recipe;
|
|
1307
|
+
try {
|
|
1308
|
+
recipe = JSON.parse(body || "{}").recipe;
|
|
1309
|
+
} catch {
|
|
1310
|
+
}
|
|
1311
|
+
if (act === "clear") {
|
|
1312
|
+
if (recipe) runner.clear(recipe);
|
|
1313
|
+
return { ok: true };
|
|
1314
|
+
}
|
|
1315
|
+
if (act !== "stop") {
|
|
1316
|
+
if (!recipe) {
|
|
1474
1317
|
res.writeHead(400, { "Content-Type": "application/json; charset=utf-8" });
|
|
1475
|
-
res.end(
|
|
1318
|
+
res.end('{"error":"no recipe"}');
|
|
1319
|
+
return void 0;
|
|
1476
1320
|
}
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
ctx.server.route("/__docs/:name", async (req, res) => {
|
|
1483
|
-
try {
|
|
1484
|
-
const name = req.params?.name;
|
|
1485
|
-
if (!name) throw new Error("missing doc name");
|
|
1486
|
-
const content = reviewStore.readDoc(name);
|
|
1487
|
-
res.writeHead(200, { "Content-Type": "text/markdown; charset=utf-8", "Cache-Control": "no-cache" });
|
|
1488
|
-
res.end(content);
|
|
1489
|
-
} catch (e) {
|
|
1490
|
-
res.writeHead(404, { "Content-Type": "application/json; charset=utf-8" });
|
|
1491
|
-
res.end(JSON.stringify({ error: e instanceof Error ? e.message : "not found" }));
|
|
1321
|
+
const recipes = await runner.recipes();
|
|
1322
|
+
if (!recipes.some((r) => r.name === recipe)) {
|
|
1323
|
+
res.writeHead(403, { "Content-Type": "application/json; charset=utf-8" });
|
|
1324
|
+
res.end('{"error":"unknown recipe"}');
|
|
1325
|
+
return void 0;
|
|
1492
1326
|
}
|
|
1493
|
-
|
|
1494
|
-
|
|
1327
|
+
runner.start(recipe);
|
|
1328
|
+
} else {
|
|
1329
|
+
runner.stop(recipe || void 0);
|
|
1330
|
+
}
|
|
1331
|
+
return runner.list();
|
|
1332
|
+
};
|
|
1333
|
+
const routeAction = (name) => {
|
|
1334
|
+
ctx.guardedRoute(`/__just/${name}`, ((req, res) => handleAction(req, res, name)));
|
|
1335
|
+
};
|
|
1336
|
+
routeAction("start");
|
|
1337
|
+
routeAction("stop");
|
|
1338
|
+
routeAction("restart");
|
|
1339
|
+
routeAction("clear");
|
|
1495
1340
|
}
|
|
1496
|
-
};
|
|
1341
|
+
});
|
|
1497
1342
|
|
|
1498
1343
|
// src/plugins/apply/scan.ts
|
|
1499
|
-
import
|
|
1500
|
-
import
|
|
1344
|
+
import fs7 from "fs";
|
|
1345
|
+
import path6 from "path";
|
|
1501
1346
|
|
|
1502
1347
|
// src/plugins/apply/parse-tasks.ts
|
|
1503
1348
|
var TASK_RE = /^\s*-\s*\[([ xX])\]\s*(.*)$/;
|
|
1349
|
+
var MANUAL_PREFIX = "\u{1F527}[\u4EBA\u5DE5]";
|
|
1504
1350
|
function parseTasks(md) {
|
|
1505
|
-
return md.split(
|
|
1351
|
+
return md.split(/\r?\n/).filter((l) => TASK_RE.test(l)).map((l) => {
|
|
1506
1352
|
const m = l.match(TASK_RE);
|
|
1507
|
-
|
|
1353
|
+
const text = (m?.[2] ?? l).trim();
|
|
1354
|
+
return { text, checked: m?.[1] !== " ", manual: text.startsWith(MANUAL_PREFIX) };
|
|
1508
1355
|
});
|
|
1509
1356
|
}
|
|
1510
1357
|
function countTasks(md) {
|
|
1511
1358
|
const tasks = parseTasks(md);
|
|
1512
|
-
|
|
1359
|
+
const normal = tasks.filter((t) => !t.manual);
|
|
1360
|
+
return {
|
|
1361
|
+
total: normal.length,
|
|
1362
|
+
done: normal.filter((t) => t.checked).length,
|
|
1363
|
+
manual: tasks.length - normal.length
|
|
1364
|
+
};
|
|
1513
1365
|
}
|
|
1514
1366
|
|
|
1515
1367
|
// src/plugins/apply/scan.ts
|
|
1516
1368
|
function readText(p) {
|
|
1517
1369
|
try {
|
|
1518
|
-
return
|
|
1370
|
+
return fs7.readFileSync(p, "utf8");
|
|
1519
1371
|
} catch {
|
|
1520
1372
|
return "";
|
|
1521
1373
|
}
|
|
1522
1374
|
}
|
|
1523
1375
|
function worktreeDir(root, name) {
|
|
1524
|
-
return
|
|
1376
|
+
return path6.join(root, ".zworktree", name);
|
|
1525
1377
|
}
|
|
1526
1378
|
function changeDir(root, name) {
|
|
1527
|
-
return
|
|
1379
|
+
return path6.join(root, "openspec", "changes", name);
|
|
1528
1380
|
}
|
|
1529
1381
|
function parseDependsOn(proposal) {
|
|
1530
1382
|
const deps = [];
|
|
@@ -1538,42 +1390,42 @@ function parseDependsOn(proposal) {
|
|
|
1538
1390
|
return deps;
|
|
1539
1391
|
}
|
|
1540
1392
|
function scanApplyChanges(root) {
|
|
1541
|
-
const changesDir =
|
|
1542
|
-
if (!
|
|
1393
|
+
const changesDir = path6.join(root, "openspec", "changes");
|
|
1394
|
+
if (!fs7.existsSync(changesDir)) return [];
|
|
1543
1395
|
const out = [];
|
|
1544
|
-
for (const ent of
|
|
1396
|
+
for (const ent of fs7.readdirSync(changesDir, { withFileTypes: true })) {
|
|
1545
1397
|
if (!ent.isDirectory() || ent.name.startsWith(".") || ent.name === "archive") continue;
|
|
1546
1398
|
const wt = worktreeDir(root, ent.name);
|
|
1547
|
-
const tasks = readText(
|
|
1399
|
+
const tasks = readText(path6.join(wt, "openspec", "changes", ent.name, "tasks.md"));
|
|
1548
1400
|
if (!tasks) {
|
|
1549
|
-
const fallback = readText(
|
|
1401
|
+
const fallback = readText(path6.join(changesDir, ent.name, "tasks.md"));
|
|
1550
1402
|
if (fallback) {
|
|
1551
1403
|
const { total, done } = countTasks(fallback);
|
|
1552
|
-
const wtChange =
|
|
1553
|
-
const mainChange =
|
|
1404
|
+
const wtChange = path6.join(wt, "openspec", "changes", ent.name);
|
|
1405
|
+
const mainChange = path6.join(changesDir, ent.name);
|
|
1554
1406
|
out.push({
|
|
1555
1407
|
name: ent.name,
|
|
1556
1408
|
path: `openspec/changes/${ent.name}`,
|
|
1557
1409
|
total,
|
|
1558
1410
|
done,
|
|
1559
|
-
hasProposal:
|
|
1560
|
-
hasDesign:
|
|
1411
|
+
hasProposal: fs7.existsSync(path6.join(wtChange, "proposal.md")) || fs7.existsSync(path6.join(mainChange, "proposal.md")),
|
|
1412
|
+
hasDesign: fs7.existsSync(path6.join(wtChange, "design.md")) || fs7.existsSync(path6.join(mainChange, "design.md")),
|
|
1561
1413
|
// worktree 目录存在即算执行中(tasks.md 可能尚未落盘)
|
|
1562
|
-
inWorktree:
|
|
1414
|
+
inWorktree: fs7.existsSync(wtChange)
|
|
1563
1415
|
});
|
|
1564
1416
|
}
|
|
1565
1417
|
} else {
|
|
1566
1418
|
const { total, done } = countTasks(tasks);
|
|
1567
|
-
const wtChange =
|
|
1568
|
-
const mainChange =
|
|
1419
|
+
const wtChange = path6.join(wt, "openspec", "changes", ent.name);
|
|
1420
|
+
const mainChange = path6.join(changesDir, ent.name);
|
|
1569
1421
|
out.push({
|
|
1570
1422
|
name: ent.name,
|
|
1571
1423
|
path: `openspec/changes/${ent.name}`,
|
|
1572
1424
|
total,
|
|
1573
1425
|
done,
|
|
1574
1426
|
// 任一侧存在即算(详情读取是 worktree 优先、主目录兜底,列表标记保持一致)
|
|
1575
|
-
hasProposal:
|
|
1576
|
-
hasDesign:
|
|
1427
|
+
hasProposal: fs7.existsSync(path6.join(wtChange, "proposal.md")) || fs7.existsSync(path6.join(mainChange, "proposal.md")),
|
|
1428
|
+
hasDesign: fs7.existsSync(path6.join(wtChange, "design.md")) || fs7.existsSync(path6.join(mainChange, "design.md")),
|
|
1577
1429
|
inWorktree: true
|
|
1578
1430
|
});
|
|
1579
1431
|
}
|
|
@@ -1584,14 +1436,14 @@ function scanApplyChanges(root) {
|
|
|
1584
1436
|
function readApplyChange(root, name) {
|
|
1585
1437
|
const wt = worktreeDir(root, name);
|
|
1586
1438
|
const mainChangeDir = changeDir(root, name);
|
|
1587
|
-
const proposalFrom =
|
|
1588
|
-
const designFrom =
|
|
1589
|
-
const tasksFrom =
|
|
1439
|
+
const proposalFrom = fs7.existsSync(path6.join(wt, "openspec", "changes", name, "proposal.md")) ? path6.join(wt, "openspec", "changes", name, "proposal.md") : path6.join(mainChangeDir, "proposal.md");
|
|
1440
|
+
const designFrom = fs7.existsSync(path6.join(wt, "openspec", "changes", name, "design.md")) ? path6.join(wt, "openspec", "changes", name, "design.md") : path6.join(mainChangeDir, "design.md");
|
|
1441
|
+
const tasksFrom = fs7.existsSync(path6.join(wt, "openspec", "changes", name, "tasks.md")) ? path6.join(wt, "openspec", "changes", name, "tasks.md") : path6.join(mainChangeDir, "tasks.md");
|
|
1590
1442
|
const proposal = readText(proposalFrom);
|
|
1591
1443
|
const design = readText(designFrom);
|
|
1592
1444
|
const tasks = readText(tasksFrom);
|
|
1593
1445
|
const { total, done } = countTasks(tasks);
|
|
1594
|
-
const inWorktree =
|
|
1446
|
+
const inWorktree = fs7.existsSync(path6.join(wt, "openspec", "changes", name));
|
|
1595
1447
|
const dependsOn = parseDependsOn(proposal);
|
|
1596
1448
|
const hasTestStrategy = /^##\s+测试策略\s*$/m.test(design);
|
|
1597
1449
|
return {
|
|
@@ -1610,46 +1462,113 @@ function readApplyChange(root, name) {
|
|
|
1610
1462
|
};
|
|
1611
1463
|
}
|
|
1612
1464
|
|
|
1465
|
+
// src/plugins/apply/batch.ts
|
|
1466
|
+
import fs8 from "fs";
|
|
1467
|
+
import path7 from "path";
|
|
1468
|
+
var RUN_ID_PATTERN = /^[A-Za-z0-9-]+$/;
|
|
1469
|
+
function isPlainObject(v) {
|
|
1470
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1471
|
+
}
|
|
1472
|
+
function readText2(p) {
|
|
1473
|
+
try {
|
|
1474
|
+
return fs8.readFileSync(p, "utf8");
|
|
1475
|
+
} catch {
|
|
1476
|
+
return null;
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
function readBatchState(root) {
|
|
1480
|
+
const applyDir = path7.join(root, ".zdev", "apply");
|
|
1481
|
+
const raw = readText2(path7.join(applyDir, "CURRENT"));
|
|
1482
|
+
const runId = raw?.trim() ?? "";
|
|
1483
|
+
if (!runId || !RUN_ID_PATTERN.test(runId)) {
|
|
1484
|
+
return { run: null, state: null };
|
|
1485
|
+
}
|
|
1486
|
+
const parsed = (() => {
|
|
1487
|
+
const text = readText2(path7.join(applyDir, "runs", runId, "state.json"));
|
|
1488
|
+
if (text == null) return null;
|
|
1489
|
+
try {
|
|
1490
|
+
return JSON.parse(text);
|
|
1491
|
+
} catch {
|
|
1492
|
+
return null;
|
|
1493
|
+
}
|
|
1494
|
+
})();
|
|
1495
|
+
if (!isPlainObject(parsed)) {
|
|
1496
|
+
return { run: { id: runId }, state: null };
|
|
1497
|
+
}
|
|
1498
|
+
return { run: { id: runId }, state: parsed };
|
|
1499
|
+
}
|
|
1500
|
+
function projectGraph(state) {
|
|
1501
|
+
if (!state) {
|
|
1502
|
+
return { changes: [], batches: [], conflicts: [] };
|
|
1503
|
+
}
|
|
1504
|
+
return {
|
|
1505
|
+
changes: state.changes.map((c) => ({
|
|
1506
|
+
name: c.name,
|
|
1507
|
+
status: c.status,
|
|
1508
|
+
dependencies: c.dependencies,
|
|
1509
|
+
batchIndex: c.batchIndex
|
|
1510
|
+
})),
|
|
1511
|
+
batches: state.batches,
|
|
1512
|
+
conflicts: state.conflicts
|
|
1513
|
+
};
|
|
1514
|
+
}
|
|
1515
|
+
var BATCH_LOGS_TAIL = 100;
|
|
1516
|
+
function tailLogs(state) {
|
|
1517
|
+
return (state?.logs ?? []).slice(-BATCH_LOGS_TAIL);
|
|
1518
|
+
}
|
|
1519
|
+
function readBatchPlan(root, runId) {
|
|
1520
|
+
return readText2(path7.join(root, ".zdev", "apply", "runs", runId, "plan.md"));
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
// src/plugins/apply/manifest.ts
|
|
1524
|
+
var manifest2 = {
|
|
1525
|
+
mode: "apply",
|
|
1526
|
+
label: "\u6267\u884C\u8FDB\u5EA6",
|
|
1527
|
+
icon: "\u2699\uFE0F",
|
|
1528
|
+
description: "OpenSpec \u6267\u884C\u8FDB\u5EA6 \xB7 zapply \u5355 change \u4E0E\u6279\u91CF",
|
|
1529
|
+
order: 40
|
|
1530
|
+
};
|
|
1531
|
+
|
|
1613
1532
|
// src/plugins/apply/index.ts
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
res.writeHead(400);
|
|
1644
|
-
res.end(JSON.stringify({ error: e instanceof Error ? e.message : "unknown error" }));
|
|
1645
|
-
}
|
|
1646
|
-
});
|
|
1533
|
+
function errorResponse(res, status, msg) {
|
|
1534
|
+
res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
|
|
1535
|
+
res.end(JSON.stringify({ error: msg }));
|
|
1536
|
+
return void 0;
|
|
1537
|
+
}
|
|
1538
|
+
var apply4 = defineBuiltin({
|
|
1539
|
+
manifest: manifest2,
|
|
1540
|
+
setup(ctx, root) {
|
|
1541
|
+
ctx.route("/__apply", async () => scanApplyChanges(root));
|
|
1542
|
+
ctx.route("/__apply/change", async (req, res) => {
|
|
1543
|
+
const url = new URL(req.url || "", "http://x");
|
|
1544
|
+
const name = url.searchParams.get("name");
|
|
1545
|
+
const bad = (msg) => errorResponse(res, 400, msg);
|
|
1546
|
+
if (!name) return bad("missing name");
|
|
1547
|
+
if (name.includes("..") || name.includes("/") || name.includes("\\")) return bad("invalid name");
|
|
1548
|
+
try {
|
|
1549
|
+
return readApplyChange(root, name);
|
|
1550
|
+
} catch (e) {
|
|
1551
|
+
return bad(e instanceof Error ? e.message : "unknown error");
|
|
1552
|
+
}
|
|
1553
|
+
});
|
|
1554
|
+
ctx.route("/__apply/batch", async () => readBatchState(root));
|
|
1555
|
+
ctx.route("/__apply/batch/graph", async () => projectGraph(readBatchState(root).state));
|
|
1556
|
+
ctx.route("/__apply/batch/logs", async () => tailLogs(readBatchState(root).state));
|
|
1557
|
+
ctx.route("/__apply/batch/plan", async (_req, res) => {
|
|
1558
|
+
const snap = readBatchState(root);
|
|
1559
|
+
const plan = snap.run ? readBatchPlan(root, snap.run.id) : null;
|
|
1560
|
+
if (plan == null) return errorResponse(res, 404, "plan not found");
|
|
1561
|
+
return { plan };
|
|
1647
1562
|
});
|
|
1648
1563
|
}
|
|
1649
|
-
};
|
|
1564
|
+
});
|
|
1650
1565
|
|
|
1651
|
-
// src/
|
|
1566
|
+
// src/plugins/design/index.ts
|
|
1652
1567
|
import path9 from "path";
|
|
1568
|
+
import fs9 from "fs";
|
|
1569
|
+
|
|
1570
|
+
// src/server/design-assets.ts
|
|
1571
|
+
import path8 from "path";
|
|
1653
1572
|
var PAGE_EXTS = [".html", ".htm"];
|
|
1654
1573
|
var ICON_EXTS = [".svg", ".png", ".ico", ".jpg", ".jpeg", ".gif", ".webp"];
|
|
1655
1574
|
var VIDEO_EXTS = [".mp4", ".webm", ".mov", ".ogg", ".ogv"];
|
|
@@ -1672,47 +1591,114 @@ function categorize(rel, ext) {
|
|
|
1672
1591
|
}
|
|
1673
1592
|
return null;
|
|
1674
1593
|
}
|
|
1675
|
-
function scanAssets(root) {
|
|
1594
|
+
function scanAssets(root, subDir = "") {
|
|
1676
1595
|
const out = {};
|
|
1677
1596
|
const keys = ["page", "component", "icon", "token", "md", "video", "audio", "pdf", "font"];
|
|
1678
1597
|
for (const k of keys) out[k] = [];
|
|
1679
1598
|
const SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "coverage", ".next", ".cache"]);
|
|
1680
|
-
|
|
1681
|
-
|
|
1599
|
+
const scanRoot = subDir ? path8.join(root, subDir) : root;
|
|
1600
|
+
walkDir(scanRoot, { skip: SKIP, onFile: (abs, rel) => {
|
|
1601
|
+
const ext = path8.extname(abs).toLowerCase();
|
|
1682
1602
|
const t = categorize(rel, ext);
|
|
1683
|
-
if (t) out[t].push({ path: rel, name:
|
|
1603
|
+
if (t) out[t].push({ path: rel, name: path8.basename(abs), ext, type: t });
|
|
1684
1604
|
} });
|
|
1685
1605
|
return out;
|
|
1686
1606
|
}
|
|
1687
1607
|
|
|
1608
|
+
// src/plugins/design/manifest.ts
|
|
1609
|
+
var manifest3 = {
|
|
1610
|
+
mode: "design",
|
|
1611
|
+
label: "\u8BBE\u8BA1\u8D44\u4EA7",
|
|
1612
|
+
icon: "\u{1F3A8}",
|
|
1613
|
+
description: "\u8BBE\u8BA1\u8D44\u4EA7\u5206\u7C7B\u6D4F\u89C8 \xB7 zdesign",
|
|
1614
|
+
order: 30
|
|
1615
|
+
};
|
|
1616
|
+
|
|
1688
1617
|
// src/plugins/design/index.ts
|
|
1689
|
-
var
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1618
|
+
var ASSET_KEYS = ["page", "component", "icon", "token", "md", "video", "audio", "pdf", "font"];
|
|
1619
|
+
function emptyScan() {
|
|
1620
|
+
const out = {};
|
|
1621
|
+
for (const k of ASSET_KEYS) out[k] = [];
|
|
1622
|
+
return out;
|
|
1623
|
+
}
|
|
1624
|
+
var apply5 = defineBuiltin({
|
|
1625
|
+
manifest: manifest3,
|
|
1626
|
+
setup(ctx, root) {
|
|
1627
|
+
ctx.route("/__design/assets", async () => {
|
|
1628
|
+
const designRoot = path9.join(root, ".zdev", "design");
|
|
1629
|
+
return fs9.existsSync(designRoot) ? scanAssets(designRoot) : emptyScan();
|
|
1630
|
+
});
|
|
1631
|
+
ctx.route("/__design/asset", async (req, res) => {
|
|
1632
|
+
const rel = new URL(req.url || "/", "http://localhost").searchParams.get("path") ?? "";
|
|
1633
|
+
if (!rel || rel.includes("..") || rel.includes("\\") || path9.isAbsolute(rel)) {
|
|
1634
|
+
res.writeHead(400);
|
|
1635
|
+
res.end("bad path");
|
|
1636
|
+
return;
|
|
1637
|
+
}
|
|
1638
|
+
const designRoot = path9.join(root, ".zdev", "design");
|
|
1639
|
+
const filePath = path9.join(designRoot, rel);
|
|
1640
|
+
if (!filePath.startsWith(designRoot + path9.sep)) {
|
|
1641
|
+
res.writeHead(400);
|
|
1642
|
+
res.end("bad path");
|
|
1643
|
+
return;
|
|
1644
|
+
}
|
|
1645
|
+
fs9.stat(filePath, (err) => {
|
|
1646
|
+
if (err) {
|
|
1647
|
+
res.writeHead(404);
|
|
1648
|
+
return res.end("Not found");
|
|
1649
|
+
}
|
|
1650
|
+
const ct = MIME[path9.extname(filePath).toLowerCase()] ?? "application/octet-stream";
|
|
1651
|
+
res.writeHead(200, { "Content-Type": ct, "Cache-Control": "no-cache" });
|
|
1652
|
+
const stream = fs9.createReadStream(filePath);
|
|
1653
|
+
stream.on("error", () => {
|
|
1654
|
+
try {
|
|
1655
|
+
res.destroy();
|
|
1656
|
+
} catch {
|
|
1657
|
+
}
|
|
1658
|
+
});
|
|
1659
|
+
stream.pipe(res);
|
|
1699
1660
|
});
|
|
1700
1661
|
});
|
|
1701
1662
|
}
|
|
1663
|
+
});
|
|
1664
|
+
|
|
1665
|
+
// src/plugins/view/manifest.ts
|
|
1666
|
+
var manifest4 = {
|
|
1667
|
+
mode: "view",
|
|
1668
|
+
label: "\u9879\u76EE\u6D4F\u89C8",
|
|
1669
|
+
icon: "\u{1F441}\uFE0F",
|
|
1670
|
+
description: "openspec / docs / \u6587\u6863\u9884\u89C8",
|
|
1671
|
+
order: 20
|
|
1702
1672
|
};
|
|
1703
1673
|
|
|
1704
1674
|
// src/plugins/view/index.ts
|
|
1705
|
-
var
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
ctx.dashboard.register({ mode: "view", label: "\u9879\u76EE\u6D4F\u89C8", icon: "\u{1F441}\uFE0F", description: "openspec / docs / \u6587\u6863\u9884\u89C8" });
|
|
1675
|
+
var apply6 = defineBuiltin({
|
|
1676
|
+
manifest: manifest4,
|
|
1677
|
+
setup() {
|
|
1709
1678
|
}
|
|
1710
|
-
};
|
|
1679
|
+
});
|
|
1711
1680
|
|
|
1712
1681
|
// src/plugins/stats/index.ts
|
|
1713
1682
|
import fs10 from "fs";
|
|
1714
1683
|
import path10 from "path";
|
|
1684
|
+
import { execFile as execFile6 } from "child_process";
|
|
1685
|
+
|
|
1686
|
+
// src/plugins/stats/manifest.ts
|
|
1687
|
+
var manifest5 = {
|
|
1688
|
+
mode: "stats",
|
|
1689
|
+
label: "\u9879\u76EE\u7EDF\u8BA1",
|
|
1690
|
+
icon: "\u{1F4CA}",
|
|
1691
|
+
description: "\u9879\u76EE\u6587\u4EF6\u7EDF\u8BA1 \xB7 \u626B\u63CF\u751F\u6210",
|
|
1692
|
+
order: 10
|
|
1693
|
+
};
|
|
1694
|
+
|
|
1695
|
+
// src/plugins/stats/index.ts
|
|
1715
1696
|
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", ".cache"]);
|
|
1697
|
+
function execFilePromise(file, args, opts) {
|
|
1698
|
+
return new Promise((resolve) => {
|
|
1699
|
+
execFile6(file, args, opts, (err, stdout) => resolve(err ? "" : stdout));
|
|
1700
|
+
});
|
|
1701
|
+
}
|
|
1716
1702
|
function scan(root) {
|
|
1717
1703
|
const stats = {
|
|
1718
1704
|
root: path10.basename(root),
|
|
@@ -1722,7 +1708,8 @@ function scan(root) {
|
|
|
1722
1708
|
byExt: [],
|
|
1723
1709
|
markdown: 0,
|
|
1724
1710
|
openspec: { active: 0, archived: 0 },
|
|
1725
|
-
hasJust: fs10.existsSync(path10.join(root, "justfile"))
|
|
1711
|
+
hasJust: fs10.existsSync(path10.join(root, "justfile")),
|
|
1712
|
+
worktrees: 0
|
|
1726
1713
|
};
|
|
1727
1714
|
const extMap = /* @__PURE__ */ new Map();
|
|
1728
1715
|
const walk = (dir) => {
|
|
@@ -1755,26 +1742,31 @@ function scan(root) {
|
|
|
1755
1742
|
stats.byExt = Array.from(extMap.entries()).map(([ext, count]) => ({ ext, count })).sort((a, b) => b.count - a.count).slice(0, 10);
|
|
1756
1743
|
return stats;
|
|
1757
1744
|
}
|
|
1758
|
-
var
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
ctx.
|
|
1762
|
-
const
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1745
|
+
var apply7 = defineBuiltin({
|
|
1746
|
+
manifest: manifest5,
|
|
1747
|
+
setup(ctx, root) {
|
|
1748
|
+
ctx.route("/__stats/data", async () => {
|
|
1749
|
+
const stats = scan(root);
|
|
1750
|
+
try {
|
|
1751
|
+
const wts = await listWorktrees(root);
|
|
1752
|
+
stats.worktrees = wts.length;
|
|
1753
|
+
} catch {
|
|
1754
|
+
stats.worktrees = 0;
|
|
1755
|
+
}
|
|
1756
|
+
try {
|
|
1757
|
+
const branchOut = await execFilePromise("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd: root });
|
|
1758
|
+
stats.branch = branchOut.trim() || void 0;
|
|
1759
|
+
} catch {
|
|
1760
|
+
}
|
|
1761
|
+
try {
|
|
1762
|
+
const statusOut = await execFilePromise("git", ["status", "--porcelain"], { cwd: root });
|
|
1763
|
+
stats.dirty = statusOut.trim() ? statusOut.trim().split("\n").length : 0;
|
|
1764
|
+
} catch {
|
|
1765
|
+
}
|
|
1766
|
+
return stats;
|
|
1775
1767
|
});
|
|
1776
1768
|
}
|
|
1777
|
-
};
|
|
1769
|
+
});
|
|
1778
1770
|
|
|
1779
1771
|
// src/cli.ts
|
|
1780
1772
|
var __dirname2 = path11.dirname(fileURLToPath2(import.meta.url));
|
|
@@ -1877,6 +1869,10 @@ async function main() {
|
|
|
1877
1869
|
await stopInstance(oldRecord);
|
|
1878
1870
|
}
|
|
1879
1871
|
const appDir = path11.resolve(__dirname2, "web");
|
|
1872
|
+
const BUILTIN_PLUGINS = [apply7, apply3, apply4, apply5, apply6];
|
|
1873
|
+
if (stripDeadPluginConfig(root, BUILTIN_PLUGINS.map((p) => p.manifest))) {
|
|
1874
|
+
console.log("[zdashboard] \u5DF2\u6E05\u7406 dashboard.json \u4E2D\u5185\u7F6E\u63D2\u4EF6\u7684\u8FC7\u65F6\u914D\u7F6E\u952E");
|
|
1875
|
+
}
|
|
1880
1876
|
const startPort = oldRecord && !args.portExplicit ? oldRecord.port : args.port;
|
|
1881
1877
|
const zdevDir = path11.join(root, ".zdev");
|
|
1882
1878
|
const dataDir = fs11.existsSync(zdevDir) ? ".zdev/" : "";
|
|
@@ -1896,20 +1892,11 @@ async function main() {
|
|
|
1896
1892
|
ctx.plugin(apply, { root });
|
|
1897
1893
|
ctx.plugin(apply2, { root });
|
|
1898
1894
|
ctx.plugin(DashboardService);
|
|
1899
|
-
const
|
|
1900
|
-
{ name: "stats", apply: apply9 },
|
|
1901
|
-
{ name: "just", apply: apply3 },
|
|
1902
|
-
{ name: "bugs", apply: apply4 },
|
|
1903
|
-
{ name: "review", apply: apply5 },
|
|
1904
|
-
{ name: "apply", apply: apply6 },
|
|
1905
|
-
{ name: "design", apply: apply7 },
|
|
1906
|
-
{ name: "view", apply: apply8 }
|
|
1907
|
-
];
|
|
1908
|
-
for (const p of plugins) {
|
|
1895
|
+
for (const apply8 of BUILTIN_PLUGINS) {
|
|
1909
1896
|
try {
|
|
1910
|
-
ctx.plugin(
|
|
1897
|
+
ctx.plugin(apply8, { root });
|
|
1911
1898
|
} catch (e) {
|
|
1912
|
-
console.error(
|
|
1899
|
+
console.error("[zdashboard] builtin plugin failed:", e);
|
|
1913
1900
|
}
|
|
1914
1901
|
}
|
|
1915
1902
|
if (args.plugins) {
|