zdashboard 1.6.2 → 2.1.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 +225 -41
- package/dist/cli.js +1388 -804
- package/dist/cli.js.map +1 -1
- package/dist/web/assets/MdViewer-C8ZG_xn1.js +1 -0
- package/dist/web/assets/Workspace-Cz1A_IXM.js +1 -0
- package/dist/web/assets/file-text-DqCOBCjz.js +6 -0
- package/dist/web/assets/index-B9eiA0v2.js +29 -0
- package/dist/web/assets/index-D58_bWw5.css +1 -0
- package/dist/web/assets/index-DkOVCCZ3.js +90 -0
- package/dist/web/assets/katex-BTcE7LSd.css +10 -0
- package/dist/web/assets/katex.min-DAmb8ZJI.js +270 -0
- package/dist/web/assets/web-8ZBVzAEO.js +26 -0
- package/dist/web/assets/web-CEyEoz0i.js +30 -0
- package/dist/web/assets/web-CXHaDUKR.js +25 -0
- package/dist/web/assets/{Viewer-CvXxR4rr.js → web-CrX-i-rZ.js} +7 -2
- package/dist/web/assets/web-D_b3sVrp.js +31 -0
- package/dist/web/assets/web-M4KeFgZw.js +2 -0
- package/dist/web/assets/{Viewer-DgUxcwns.js → web-Y4h9oNHC.js} +3 -3
- package/dist/web/index.html +2 -2
- package/package.json +4 -1
- package/dist/web/assets/DesignViewer-DIFMIK04.js +0 -26
- package/dist/web/assets/ReviewViewer-CzphymDs.js +0 -31
- package/dist/web/assets/ViewViewer-2gv2jIkV.js +0 -11
- package/dist/web/assets/folder-open-YuFnfZTO.js +0 -6
- package/dist/web/assets/index-BJ8mveF-.js +0 -446
- package/dist/web/assets/index-BwdPhy39.css +0 -10
- package/dist/web/assets/index-CgsmLt7m.js +0 -2
- package/dist/web/assets/index-D5XUdKxi.js +0 -2
- package/dist/web/assets/index-LAcTTLE0.js +0 -2
- package/dist/web/assets/index-Nmrw-5B2.js +0 -2
- package/dist/web/assets/index-f9TDtJW5.js +0 -2
package/dist/cli.js
CHANGED
|
@@ -1,266 +1,943 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
// src/
|
|
4
|
-
import
|
|
5
|
-
import fs6 from "fs";
|
|
6
|
-
import path6 from "path";
|
|
7
|
-
import crypto from "crypto";
|
|
8
|
-
import { exec } from "child_process";
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import path10 from "path";
|
|
9
5
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
6
|
+
import { access, readdir } from "fs/promises";
|
|
10
7
|
|
|
11
|
-
// src/server/
|
|
8
|
+
// src/server/detect.ts
|
|
12
9
|
import fs from "fs";
|
|
13
10
|
import path from "path";
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
11
|
+
import { execFile } from "child_process";
|
|
12
|
+
function justAvailable(cwd) {
|
|
13
|
+
return new Promise((resolve) => {
|
|
14
|
+
const child = execFile("just", ["--list", "--unsorted"], { cwd, timeout: 5e3 }, (err) => {
|
|
15
|
+
resolve(!err);
|
|
16
|
+
});
|
|
17
|
+
if (child.killed) resolve(false);
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
async function detect(root) {
|
|
21
|
+
const hasOpenspec = fs.existsSync(path.join(root, "openspec"));
|
|
22
|
+
const hasDocs = fs.existsSync(path.join(root, "docs"));
|
|
23
|
+
const hasJust = await justAvailable(root);
|
|
24
|
+
const hasBugs = fs.existsSync(path.join(root, ".zgoal", "config.yaml"));
|
|
25
|
+
return { hasOpenspec, hasDocs, hasJust, hasBugs };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// src/cli.ts
|
|
29
|
+
import { Context } from "cordis";
|
|
30
|
+
|
|
31
|
+
// src/core/server.ts
|
|
32
|
+
import http from "http";
|
|
33
|
+
import fs3 from "fs";
|
|
34
|
+
import path3 from "path";
|
|
35
|
+
import crypto from "crypto";
|
|
36
|
+
import { fileURLToPath } from "url";
|
|
37
|
+
|
|
38
|
+
// package.json
|
|
39
|
+
var package_default = {
|
|
40
|
+
name: "zdashboard",
|
|
41
|
+
version: "2.1.0",
|
|
42
|
+
description: "ZCode skill dashboard platform \u2014 pluggable viewers for zdesign/zview/zreview/zgoal",
|
|
43
|
+
type: "module",
|
|
44
|
+
bin: {
|
|
45
|
+
zdashboard: "./dist/cli.js"
|
|
46
|
+
},
|
|
47
|
+
files: [
|
|
48
|
+
"dist"
|
|
49
|
+
],
|
|
50
|
+
publishConfig: {
|
|
51
|
+
access: "public"
|
|
52
|
+
},
|
|
53
|
+
scripts: {
|
|
54
|
+
dev: "vite",
|
|
55
|
+
build: "tsup && vite build",
|
|
56
|
+
"build:web": "vite build",
|
|
57
|
+
"build:node": "tsup",
|
|
58
|
+
start: "node dist/cli.js",
|
|
59
|
+
preview: "vite preview"
|
|
60
|
+
},
|
|
61
|
+
dependencies: {
|
|
62
|
+
"@radix-ui/react-scroll-area": "^1.2.0",
|
|
63
|
+
"@radix-ui/react-separator": "^1.1.0",
|
|
64
|
+
"@radix-ui/react-slot": "^1.1.0",
|
|
65
|
+
"@radix-ui/react-tooltip": "^1.1.2",
|
|
66
|
+
"@uidotdev/usehooks": "^2.4.0",
|
|
67
|
+
"ansi-to-react": "^6.1.6",
|
|
68
|
+
"class-variance-authority": "^0.7.0",
|
|
69
|
+
clsx: "^2.1.1",
|
|
70
|
+
cordis: "4.0.0-rc.8",
|
|
71
|
+
cosmokit: "^1.8.1",
|
|
72
|
+
"date-fns": "^3.6.0",
|
|
73
|
+
filesize: "^11.0.0",
|
|
74
|
+
"highlight.js": "^11.11.1",
|
|
75
|
+
katex: "^0.16.11",
|
|
76
|
+
ky: "^1.7.0",
|
|
77
|
+
"lodash-es": "^4.17.21",
|
|
78
|
+
"lucide-react": "^0.460.0",
|
|
79
|
+
react: "^18.3.1",
|
|
80
|
+
"react-dom": "^18.3.1",
|
|
81
|
+
"react-error-boundary": "^5.0.0",
|
|
82
|
+
"react-markdown": "^9.0.1",
|
|
83
|
+
"rehype-autolink-headings": "^7.1.0",
|
|
84
|
+
"rehype-highlight": "^7.0.1",
|
|
85
|
+
"rehype-katex": "^7.0.1",
|
|
86
|
+
"rehype-raw": "^7.0.0",
|
|
87
|
+
"rehype-slug": "^6.0.0",
|
|
88
|
+
"remark-frontmatter": "^5.0.0",
|
|
89
|
+
"remark-gfm": "^4.0.0",
|
|
90
|
+
"remark-math": "^6.0.0",
|
|
91
|
+
sonner: "^1.5.0",
|
|
92
|
+
"tailwind-merge": "^2.5.4",
|
|
93
|
+
tsx: "^4.7.0",
|
|
94
|
+
"use-debounce": "^10.0.0",
|
|
95
|
+
yaml: "^2.9.0"
|
|
96
|
+
},
|
|
97
|
+
devDependencies: {
|
|
98
|
+
"@tailwindcss/typography": "^0.5.20",
|
|
99
|
+
"@testing-library/jest-dom": "^7.0.1",
|
|
100
|
+
"@testing-library/react": "^16.3.2",
|
|
101
|
+
"@types/lodash-es": "^4.17.12",
|
|
102
|
+
"@types/node": "^22.9.0",
|
|
103
|
+
"@types/react": "^18.3.12",
|
|
104
|
+
"@types/react-dom": "^18.3.1",
|
|
105
|
+
"@vitejs/plugin-react": "^4.7.0",
|
|
106
|
+
autoprefixer: "^10.4.20",
|
|
107
|
+
jsdom: "^30.0.1",
|
|
108
|
+
postcss: "^8.4.49",
|
|
109
|
+
tailwindcss: "^3.4.14",
|
|
110
|
+
"tailwindcss-animate": "^1.0.7",
|
|
111
|
+
tsup: "^8.3.5",
|
|
112
|
+
typescript: "^5.6.3",
|
|
113
|
+
vite: "^5.4.10",
|
|
114
|
+
vitest: "^1.6.0"
|
|
115
|
+
},
|
|
116
|
+
pnpm: {
|
|
117
|
+
onlyBuiltDependencies: [
|
|
118
|
+
"esbuild"
|
|
119
|
+
]
|
|
31
120
|
}
|
|
32
|
-
|
|
33
|
-
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// src/core/server.ts
|
|
124
|
+
import { Service } from "cordis";
|
|
125
|
+
|
|
126
|
+
// src/core/instance.ts
|
|
127
|
+
import fs2 from "fs";
|
|
128
|
+
import path2 from "path";
|
|
129
|
+
var RECORD_FILE = ".zdev/dashboard.json";
|
|
130
|
+
var VERIFY_TIMEOUT_MS = 1500;
|
|
131
|
+
var STOP_POLL_MS = 2e3;
|
|
132
|
+
var STOP_POLL_INTERVAL_MS = 100;
|
|
133
|
+
var STOP_FINAL_WAIT_MS = 100;
|
|
134
|
+
function recordPath(root) {
|
|
135
|
+
return path2.join(root, RECORD_FILE);
|
|
34
136
|
}
|
|
35
|
-
function
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
if (!ent.isDirectory() || ent.name.startsWith(".") || ent.name === "archive") continue;
|
|
43
|
-
active.push({ name: ent.name, kind: "dir", children: walkFiles(path.join(changesDir, ent.name), `openspec/changes/${ent.name}`) });
|
|
44
|
-
}
|
|
45
|
-
active.sort((a, b) => a.name.localeCompare(b.name));
|
|
46
|
-
const archiveDir = path.join(changesDir, "archive");
|
|
47
|
-
if (fs.existsSync(archiveDir)) {
|
|
48
|
-
for (const ent of fs.readdirSync(archiveDir, { withFileTypes: true })) {
|
|
49
|
-
if (!ent.isDirectory() || ent.name.startsWith(".")) continue;
|
|
50
|
-
archived.push({ name: ent.name, kind: "dir", children: walkFiles(path.join(archiveDir, ent.name), `openspec/changes/archive/${ent.name}`) });
|
|
51
|
-
}
|
|
52
|
-
archived.sort((a, b) => b.name.localeCompare(a.name));
|
|
53
|
-
}
|
|
54
|
-
if (active.length) tree.push({ name: `\u8FDB\u884C\u4E2D (${active.length})`, kind: "dir", children: active });
|
|
55
|
-
if (archived.length) tree.push({ name: "archive", kind: "dir", defaultCollapsed: true, children: archived });
|
|
56
|
-
const specsDir = path.join(root2, "openspec", "specs");
|
|
57
|
-
if (fs.existsSync(specsDir)) {
|
|
58
|
-
const specs = walkFiles(specsDir, "openspec/specs");
|
|
59
|
-
if (specs.length) tree.push({ name: "specs", kind: "dir", children: specs });
|
|
137
|
+
function readRecord(root) {
|
|
138
|
+
try {
|
|
139
|
+
const fp = recordPath(root);
|
|
140
|
+
const raw = fs2.readFileSync(fp, "utf-8");
|
|
141
|
+
const rec = JSON.parse(raw);
|
|
142
|
+
if (typeof rec.pid === "number" && typeof rec.port === "number" && typeof rec.root === "string" && typeof rec.startedAt === "string") {
|
|
143
|
+
return rec;
|
|
60
144
|
}
|
|
145
|
+
return null;
|
|
146
|
+
} catch {
|
|
147
|
+
return null;
|
|
61
148
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
149
|
+
}
|
|
150
|
+
function writeRecord(root, port) {
|
|
151
|
+
fs2.mkdirSync(path2.dirname(recordPath(root)), { recursive: true });
|
|
152
|
+
const rec = {
|
|
153
|
+
pid: process.pid,
|
|
154
|
+
port,
|
|
155
|
+
root,
|
|
156
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
157
|
+
};
|
|
158
|
+
fs2.writeFileSync(recordPath(root), JSON.stringify(rec, null, 2) + "\n");
|
|
159
|
+
}
|
|
160
|
+
function clearRecord(root) {
|
|
161
|
+
try {
|
|
162
|
+
const rec = readRecord(root);
|
|
163
|
+
if (rec && rec.pid !== process.pid) return;
|
|
164
|
+
fs2.unlinkSync(recordPath(root));
|
|
165
|
+
} catch {
|
|
65
166
|
}
|
|
66
|
-
|
|
67
|
-
|
|
167
|
+
}
|
|
168
|
+
function isAlive(pid) {
|
|
68
169
|
try {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const ext = path.extname(ent.name).toLowerCase();
|
|
72
|
-
if (ent.isFile() && (ext === ".md" || ext === ".markdown")) etc.push({ name: ent.name, kind: "file", path: ent.name });
|
|
73
|
-
}
|
|
170
|
+
process.kill(pid, 0);
|
|
171
|
+
return true;
|
|
74
172
|
} catch (e) {
|
|
75
|
-
|
|
173
|
+
if (e.code === "ESRCH") return false;
|
|
174
|
+
return true;
|
|
76
175
|
}
|
|
77
|
-
etc.sort((a, b) => a.name.localeCompare(b.name));
|
|
78
|
-
if (etc.length) tree.push({ name: `\u5176\u4ED6 (${etc.length})`, kind: "dir", children: etc });
|
|
79
|
-
return tree;
|
|
80
176
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
recipe = null;
|
|
89
|
-
state = "idle";
|
|
90
|
-
code = null;
|
|
91
|
-
buffer = [];
|
|
92
|
-
pending = "";
|
|
93
|
-
// 行缓冲:块缓冲输出(如 maven)的 chunk 会在行中间断开,攒到 \n 才切行
|
|
94
|
-
clients = /* @__PURE__ */ new Set();
|
|
95
|
-
recipesCache = null;
|
|
96
|
-
constructor(cwd) {
|
|
97
|
-
this.cwd = cwd;
|
|
177
|
+
async function fetchWithTimeout(url) {
|
|
178
|
+
const controller = new AbortController();
|
|
179
|
+
const timer = setTimeout(() => controller.abort(), VERIFY_TIMEOUT_MS);
|
|
180
|
+
try {
|
|
181
|
+
return await fetch(url, { signal: controller.signal });
|
|
182
|
+
} finally {
|
|
183
|
+
clearTimeout(timer);
|
|
98
184
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
for (const line of stdout.split(/\r?\n/).slice(1)) {
|
|
110
|
-
const trimmed = line.trim();
|
|
111
|
-
if (!trimmed) continue;
|
|
112
|
-
const hashIdx = trimmed.indexOf("#");
|
|
113
|
-
const sig = (hashIdx >= 0 ? trimmed.slice(0, hashIdx) : trimmed).trim();
|
|
114
|
-
if (!sig) continue;
|
|
115
|
-
const name = sig.split(/\s+/)[0];
|
|
116
|
-
if (seen.has(name)) continue;
|
|
117
|
-
seen.add(name);
|
|
118
|
-
out.push({ name, description: hashIdx >= 0 ? trimmed.slice(hashIdx + 1).trim() : "" });
|
|
119
|
-
}
|
|
120
|
-
this.recipesCache = out;
|
|
121
|
-
resolve(out);
|
|
122
|
-
});
|
|
123
|
-
});
|
|
185
|
+
}
|
|
186
|
+
async function verifyRoot(port, root) {
|
|
187
|
+
try {
|
|
188
|
+
const res = await fetchWithTimeout(`http://127.0.0.1:${port}/__config`);
|
|
189
|
+
if (res.status !== 200) return false;
|
|
190
|
+
const body = await res.json();
|
|
191
|
+
if (typeof body.root !== "string") return false;
|
|
192
|
+
return body.root === root;
|
|
193
|
+
} catch {
|
|
194
|
+
return false;
|
|
124
195
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
196
|
+
}
|
|
197
|
+
async function findReusable(root) {
|
|
198
|
+
const rec = readRecord(root);
|
|
199
|
+
if (!rec) return null;
|
|
200
|
+
if (!isAlive(rec.pid)) return null;
|
|
201
|
+
if (!await verifyRoot(rec.port, root)) return null;
|
|
202
|
+
return rec;
|
|
203
|
+
}
|
|
204
|
+
async function stopInstance(record) {
|
|
205
|
+
try {
|
|
206
|
+
process.kill(record.pid, "SIGTERM");
|
|
207
|
+
} catch {
|
|
208
|
+
return;
|
|
130
209
|
}
|
|
131
|
-
|
|
132
|
-
|
|
210
|
+
const deadline = Date.now() + STOP_POLL_MS;
|
|
211
|
+
while (Date.now() < deadline) {
|
|
212
|
+
if (!isAlive(record.pid)) return;
|
|
213
|
+
await new Promise((r) => setTimeout(r, STOP_POLL_INTERVAL_MS));
|
|
133
214
|
}
|
|
134
|
-
|
|
135
|
-
|
|
215
|
+
try {
|
|
216
|
+
process.kill(record.pid, "SIGKILL");
|
|
217
|
+
} catch {
|
|
136
218
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
219
|
+
await new Promise((r) => setTimeout(r, STOP_FINAL_WAIT_MS));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// src/core/open-url.ts
|
|
223
|
+
import { exec } from "child_process";
|
|
224
|
+
function openUrl2(url) {
|
|
225
|
+
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
226
|
+
exec(`${cmd} ${url}`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// src/core/server.ts
|
|
230
|
+
var VERSION = package_default.version;
|
|
231
|
+
var __dirname = path3.dirname(fileURLToPath(import.meta.url));
|
|
232
|
+
var MIME = {
|
|
233
|
+
".html": "text/html; charset=utf-8",
|
|
234
|
+
".htm": "text/html; charset=utf-8",
|
|
235
|
+
".css": "text/css; charset=utf-8",
|
|
236
|
+
".js": "application/javascript; charset=utf-8",
|
|
237
|
+
".mjs": "application/javascript; charset=utf-8",
|
|
238
|
+
".json": "application/json; charset=utf-8",
|
|
239
|
+
".svg": "image/svg+xml",
|
|
240
|
+
".png": "image/png",
|
|
241
|
+
".ico": "image/x-icon",
|
|
242
|
+
".jpg": "image/jpeg",
|
|
243
|
+
".jpeg": "image/jpeg",
|
|
244
|
+
".gif": "image/gif",
|
|
245
|
+
".webp": "image/webp",
|
|
246
|
+
".md": "text/markdown; charset=utf-8",
|
|
247
|
+
".txt": "text/plain; charset=utf-8",
|
|
248
|
+
".sql": "text/plain; charset=utf-8",
|
|
249
|
+
".csv": "text/plain; charset=utf-8",
|
|
250
|
+
".tsv": "text/plain; charset=utf-8",
|
|
251
|
+
".yaml": "text/yaml; charset=utf-8",
|
|
252
|
+
".yml": "text/yaml; charset=utf-8",
|
|
253
|
+
".xml": "application/xml; charset=utf-8",
|
|
254
|
+
".py": "text/plain; charset=utf-8",
|
|
255
|
+
".ts": "text/plain; charset=utf-8",
|
|
256
|
+
".java": "text/plain; charset=utf-8",
|
|
257
|
+
".go": "text/plain; charset=utf-8",
|
|
258
|
+
".rs": "text/plain; charset=utf-8",
|
|
259
|
+
".rb": "text/plain; charset=utf-8",
|
|
260
|
+
".php": "text/plain; charset=utf-8",
|
|
261
|
+
".c": "text/plain; charset=utf-8",
|
|
262
|
+
".cpp": "text/plain; charset=utf-8",
|
|
263
|
+
".h": "text/plain; charset=utf-8",
|
|
264
|
+
".cs": "text/plain; charset=utf-8",
|
|
265
|
+
".swift": "text/plain; charset=utf-8",
|
|
266
|
+
".kt": "text/plain; charset=utf-8",
|
|
267
|
+
".scala": "text/plain; charset=utf-8",
|
|
268
|
+
".sh": "text/plain; charset=utf-8",
|
|
269
|
+
".bash": "text/plain; charset=utf-8",
|
|
270
|
+
".zsh": "text/plain; charset=utf-8",
|
|
271
|
+
".fish": "text/plain; charset=utf-8",
|
|
272
|
+
".env": "text/plain; charset=utf-8",
|
|
273
|
+
".gitignore": "text/plain; charset=utf-8",
|
|
274
|
+
".dockerfile": "text/plain; charset=utf-8",
|
|
275
|
+
".woff": "font/woff",
|
|
276
|
+
".woff2": "font/woff2",
|
|
277
|
+
".ttf": "font/woff",
|
|
278
|
+
".map": "application/json; charset=utf-8"
|
|
279
|
+
};
|
|
280
|
+
var INJECT = `<script>(function(){try{var es=new EventSource('/__reload');es.addEventListener('reload',function(){location.reload();});es.onerror=function(){es.close();};}catch(e){}document.addEventListener('click',function(e){var t=e.target;if(t&&t.closest){var a=t.closest('a[target]');if(a&&a.target!=='_self'){a.target='_self';}}},true);})();</script>`;
|
|
281
|
+
var PLUGIN_STATIC_PREFIX = "/__plugin/";
|
|
282
|
+
var ServerService = class extends Service {
|
|
283
|
+
stopToken;
|
|
284
|
+
root;
|
|
285
|
+
appDir;
|
|
286
|
+
open;
|
|
287
|
+
page;
|
|
288
|
+
det;
|
|
289
|
+
routes = /* @__PURE__ */ new Map();
|
|
290
|
+
sses = /* @__PURE__ */ new Map();
|
|
291
|
+
prefixStatic = /* @__PURE__ */ new Map();
|
|
292
|
+
server;
|
|
293
|
+
onListen;
|
|
294
|
+
constructor(ctx, config) {
|
|
295
|
+
super(ctx, "server");
|
|
296
|
+
this.stopToken = crypto.randomBytes(12).toString("hex");
|
|
297
|
+
this.root = config.root;
|
|
298
|
+
this.appDir = config.appDir;
|
|
299
|
+
this.open = config.open;
|
|
300
|
+
this.page = config.page;
|
|
301
|
+
this.det = config.detect;
|
|
302
|
+
this.onListen = config.onListen;
|
|
303
|
+
ctx.effect(() => () => this.dispose());
|
|
304
|
+
this.route("/__config", (_req, res) => {
|
|
305
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
|
|
306
|
+
res.end(JSON.stringify({ stopToken: this.stopToken, version: VERSION, root: this.root }));
|
|
307
|
+
});
|
|
308
|
+
this.route("/__stop", async (req, res) => {
|
|
309
|
+
if (req.headers["x-stop-token"] !== this.stopToken) {
|
|
310
|
+
res.writeHead(403);
|
|
311
|
+
res.end("forbidden");
|
|
312
|
+
return;
|
|
179
313
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
this.
|
|
183
|
-
this.emit({ type: "state", state: "exited", recipe: this.recipe, code: this.code });
|
|
314
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
|
|
315
|
+
res.end('{"ok":true}');
|
|
316
|
+
this.stop();
|
|
184
317
|
});
|
|
318
|
+
this.start(config.port);
|
|
185
319
|
}
|
|
186
|
-
|
|
187
|
-
this.
|
|
188
|
-
|
|
189
|
-
this.emit({ type: "log", text: line });
|
|
320
|
+
route(path11, handler) {
|
|
321
|
+
this.routes.set(path11, handler);
|
|
322
|
+
this.ctx.effect(() => () => this.routes.delete(path11));
|
|
190
323
|
}
|
|
191
|
-
|
|
192
|
-
this.
|
|
324
|
+
sse(path11, onConnect) {
|
|
325
|
+
this.sses.set(path11, onConnect);
|
|
326
|
+
this.ctx.effect(() => () => this.sses.delete(path11));
|
|
193
327
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
328
|
+
static(prefix, dir) {
|
|
329
|
+
this.prefixStatic.set(prefix, dir);
|
|
330
|
+
this.ctx.effect(() => () => this.prefixStatic.delete(prefix));
|
|
197
331
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
332
|
+
serveFile(filePath, res, injectHtml) {
|
|
333
|
+
fs3.readFile(filePath, (err, data) => {
|
|
334
|
+
if (err) {
|
|
335
|
+
res.writeHead(404);
|
|
336
|
+
return res.end("Not found");
|
|
337
|
+
}
|
|
338
|
+
const ext = path3.extname(filePath).toLowerCase();
|
|
339
|
+
const ct = MIME[ext] ?? "application/octet-stream";
|
|
340
|
+
let body = data;
|
|
341
|
+
if (injectHtml && ext === ".html") {
|
|
342
|
+
const s = data.toString("utf8");
|
|
343
|
+
body = Buffer.from(s.indexOf("</body>") >= 0 ? s.replace("</body>", INJECT + "</body>") : s + INJECT);
|
|
344
|
+
}
|
|
345
|
+
res.writeHead(200, { "Content-Type": ct, "Cache-Control": "no-cache" });
|
|
346
|
+
res.end(body);
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
servePrefix(url, res) {
|
|
350
|
+
for (const [prefix, dir] of this.prefixStatic) {
|
|
351
|
+
if (url.indexOf(prefix) === 0) {
|
|
352
|
+
let rel = this.safeDecode(url.slice(prefix.length));
|
|
353
|
+
if (!rel || rel === "/" || !path3.extname(rel)) rel = path3.join(rel || "", "index.html");
|
|
354
|
+
const fp = path3.join(dir, rel);
|
|
355
|
+
if (fp.indexOf(dir + path3.sep) !== 0) {
|
|
356
|
+
res.writeHead(403);
|
|
357
|
+
return res.end("Forbidden");
|
|
358
|
+
}
|
|
359
|
+
this.serveFile(fp, res, true);
|
|
360
|
+
return true;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
safeDecode(raw) {
|
|
366
|
+
try {
|
|
367
|
+
return decodeURIComponent(raw);
|
|
368
|
+
} catch {
|
|
369
|
+
return raw;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
handler = (req, res) => {
|
|
373
|
+
try {
|
|
374
|
+
const rawUrl = req.url || "/";
|
|
375
|
+
const url = rawUrl.split("?")[0];
|
|
376
|
+
const rh = this.routes.get(url);
|
|
377
|
+
if (rh) {
|
|
378
|
+
rh(req, res);
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
const sh = this.sses.get(url);
|
|
382
|
+
if (sh) {
|
|
383
|
+
res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" });
|
|
384
|
+
res.write(": connected\n\n");
|
|
385
|
+
const cleanup = sh(res);
|
|
386
|
+
req.on("close", () => {
|
|
387
|
+
cleanup?.();
|
|
388
|
+
});
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
if (this.servePrefix(url, res)) return;
|
|
392
|
+
if (url === "/" || url.indexOf("/__app/") === 0 || url.indexOf("/assets/") === 0) {
|
|
393
|
+
let fp2 = this.appDir;
|
|
394
|
+
if (url !== "/") fp2 = path3.join(this.appDir, this.safeDecode(url));
|
|
395
|
+
if (url.indexOf("/__app/") === 0) fp2 = path3.join(this.appDir, url.slice(7));
|
|
396
|
+
if (fp2 !== this.appDir && fp2.indexOf(this.appDir + path3.sep) !== 0) {
|
|
397
|
+
res.writeHead(403);
|
|
398
|
+
return res.end("Forbidden");
|
|
399
|
+
}
|
|
400
|
+
if (url === "/") fp2 = path3.join(this.appDir, "index.html");
|
|
401
|
+
return this.serveFile(fp2, res, false);
|
|
402
|
+
}
|
|
403
|
+
const fp = path3.join(this.root, this.safeDecode(url));
|
|
404
|
+
if (fp !== this.root && fp.indexOf(this.root + path3.sep) !== 0) {
|
|
405
|
+
res.writeHead(403);
|
|
406
|
+
return res.end("Forbidden");
|
|
407
|
+
}
|
|
408
|
+
return this.serveFile(fp, res, true);
|
|
409
|
+
} catch (e) {
|
|
201
410
|
try {
|
|
202
|
-
|
|
203
|
-
|
|
411
|
+
res.writeHead(400);
|
|
412
|
+
res.end("bad request");
|
|
204
413
|
} catch {
|
|
205
414
|
}
|
|
415
|
+
console.error("[zdashboard] handler error", e);
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
start(port) {
|
|
419
|
+
this.server = http.createServer(this.handler);
|
|
420
|
+
this.server.on("error", (err) => {
|
|
421
|
+
if (err.code === "EADDRINUSE") {
|
|
422
|
+
console.log(`[zdashboard] port ${port} busy, trying ${port + 1}`);
|
|
423
|
+
this.start(port + 1);
|
|
424
|
+
} else throw err;
|
|
425
|
+
});
|
|
426
|
+
this.server.listen(port, "127.0.0.1", () => {
|
|
427
|
+
const u = `http://localhost:${port}`;
|
|
428
|
+
const target = this.page ? `${u}#${this.page}` : u;
|
|
429
|
+
console.log(`[zdashboard] v${VERSION} dashboard -> ${u}`);
|
|
430
|
+
console.log(`[zdashboard] project -> ${this.root}`);
|
|
431
|
+
console.log(`[zdashboard] detect -> openspec:${this.det.hasOpenspec} docs:${this.det.hasDocs} just:${this.det.hasJust} bugs:${this.det.hasBugs}`);
|
|
432
|
+
if (this.open) openUrl2(target);
|
|
433
|
+
this.onListen?.(port);
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
stop() {
|
|
437
|
+
if (this.server) {
|
|
438
|
+
try {
|
|
439
|
+
this.server.close();
|
|
440
|
+
} catch {
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
try {
|
|
444
|
+
clearRecord(this.root);
|
|
445
|
+
} catch {
|
|
446
|
+
}
|
|
447
|
+
setTimeout(() => {
|
|
448
|
+
try {
|
|
449
|
+
this.ctx.root.fiber.dispose();
|
|
450
|
+
} catch {
|
|
451
|
+
}
|
|
452
|
+
process.exit(0);
|
|
453
|
+
}, 50);
|
|
454
|
+
}
|
|
455
|
+
dispose() {
|
|
456
|
+
if (this.server) {
|
|
457
|
+
try {
|
|
458
|
+
this.server.close();
|
|
459
|
+
} catch {
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
try {
|
|
463
|
+
clearRecord(this.root);
|
|
464
|
+
} catch {
|
|
206
465
|
}
|
|
207
|
-
this.child = null;
|
|
208
466
|
}
|
|
209
467
|
};
|
|
210
468
|
|
|
211
|
-
// src/
|
|
212
|
-
import
|
|
213
|
-
import
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
469
|
+
// src/core/reload.ts
|
|
470
|
+
import fs4 from "fs";
|
|
471
|
+
import { Service as Service2 } from "cordis";
|
|
472
|
+
var WATCH_DEBOUNCE_MS = 150;
|
|
473
|
+
var ReloadService = class extends Service2 {
|
|
474
|
+
static inject = ["server"];
|
|
475
|
+
clients = /* @__PURE__ */ new Set();
|
|
476
|
+
watcher;
|
|
477
|
+
timer;
|
|
478
|
+
constructor(ctx, config) {
|
|
479
|
+
super(ctx, "reload");
|
|
480
|
+
this.ctx.effect(() => () => this.dispose());
|
|
481
|
+
this.ctx.server.sse("/__reload", (res) => {
|
|
482
|
+
this.clients.add(res);
|
|
483
|
+
return () => this.clients.delete(res);
|
|
484
|
+
});
|
|
485
|
+
try {
|
|
486
|
+
this.watcher = fs4.watch(config.root, { recursive: true }, () => {
|
|
487
|
+
if (this.timer) clearTimeout(this.timer);
|
|
488
|
+
this.timer = setTimeout(() => {
|
|
489
|
+
this.broadcast("reload");
|
|
490
|
+
this.broadcast("files");
|
|
491
|
+
}, WATCH_DEBOUNCE_MS);
|
|
492
|
+
});
|
|
493
|
+
} catch {
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
broadcast(ev, data = "") {
|
|
497
|
+
const payload = `event: ${ev}
|
|
498
|
+
data: ${JSON.stringify(data == null ? "" : data)}
|
|
217
499
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
500
|
+
`;
|
|
501
|
+
for (const c of this.clients) {
|
|
502
|
+
try {
|
|
503
|
+
c.write(payload);
|
|
504
|
+
} catch {
|
|
505
|
+
}
|
|
506
|
+
}
|
|
225
507
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
this.
|
|
508
|
+
dispose() {
|
|
509
|
+
if (this.watcher) {
|
|
510
|
+
try {
|
|
511
|
+
this.watcher.close();
|
|
512
|
+
} catch {
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
if (this.timer) {
|
|
516
|
+
clearTimeout(this.timer);
|
|
517
|
+
}
|
|
518
|
+
for (const c of this.clients) {
|
|
519
|
+
try {
|
|
520
|
+
c.end();
|
|
521
|
+
} catch {
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
this.clients.clear();
|
|
234
525
|
}
|
|
235
|
-
cause;
|
|
236
526
|
};
|
|
237
527
|
|
|
238
|
-
// src/server/
|
|
239
|
-
|
|
528
|
+
// src/server/spec-scan.ts
|
|
529
|
+
import fs5 from "fs";
|
|
530
|
+
import path4 from "path";
|
|
531
|
+
function walkFiles(absDir, relDir, depth = 0) {
|
|
532
|
+
if (depth > 4) return [];
|
|
533
|
+
let ents;
|
|
240
534
|
try {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
535
|
+
ents = fs5.readdirSync(absDir, { withFileTypes: true });
|
|
536
|
+
} catch {
|
|
537
|
+
return [];
|
|
538
|
+
}
|
|
539
|
+
const nodes = [];
|
|
540
|
+
for (const ent of ents) {
|
|
541
|
+
if (ent.name.startsWith(".") || ent.name === "node_modules") continue;
|
|
542
|
+
const rel = relDir ? `${relDir}/${ent.name}` : ent.name;
|
|
543
|
+
if (ent.isDirectory()) {
|
|
544
|
+
nodes.push({ name: ent.name, kind: "dir", children: walkFiles(path4.join(absDir, ent.name), rel, depth + 1) });
|
|
545
|
+
} else {
|
|
546
|
+
nodes.push({ name: ent.name, kind: "file", path: rel });
|
|
247
547
|
}
|
|
248
|
-
|
|
249
|
-
|
|
548
|
+
}
|
|
549
|
+
nodes.sort((a, b) => a.kind === b.kind ? a.name.localeCompare(b.name) : a.kind === "dir" ? -1 : 1);
|
|
550
|
+
return nodes;
|
|
551
|
+
}
|
|
552
|
+
function scanTree(root, hasOpenspec, hasDocs) {
|
|
553
|
+
const tree = [];
|
|
554
|
+
if (hasOpenspec && fs5.existsSync(path4.join(root, "openspec", "changes"))) {
|
|
555
|
+
const changesDir = path4.join(root, "openspec", "changes");
|
|
556
|
+
const active = [];
|
|
557
|
+
const archived = [];
|
|
558
|
+
for (const ent of fs5.readdirSync(changesDir, { withFileTypes: true })) {
|
|
559
|
+
if (!ent.isDirectory() || ent.name.startsWith(".") || ent.name === "archive") continue;
|
|
560
|
+
active.push({ name: ent.name, kind: "dir", children: walkFiles(path4.join(changesDir, ent.name), `openspec/changes/${ent.name}`) });
|
|
250
561
|
}
|
|
251
|
-
|
|
562
|
+
active.sort((a, b) => a.name.localeCompare(b.name));
|
|
563
|
+
const archiveDir = path4.join(changesDir, "archive");
|
|
564
|
+
if (fs5.existsSync(archiveDir)) {
|
|
565
|
+
for (const ent of fs5.readdirSync(archiveDir, { withFileTypes: true })) {
|
|
566
|
+
if (!ent.isDirectory() || ent.name.startsWith(".")) continue;
|
|
567
|
+
archived.push({ name: ent.name, kind: "dir", children: walkFiles(path4.join(archiveDir, ent.name), `openspec/changes/archive/${ent.name}`) });
|
|
568
|
+
}
|
|
569
|
+
archived.sort((a, b) => b.name.localeCompare(a.name));
|
|
570
|
+
}
|
|
571
|
+
if (active.length) tree.push({ name: `\u8FDB\u884C\u4E2D (${active.length})`, kind: "dir", children: active });
|
|
572
|
+
if (archived.length) tree.push({ name: "archive", kind: "dir", defaultCollapsed: true, children: archived });
|
|
573
|
+
const specsDir = path4.join(root, "openspec", "specs");
|
|
574
|
+
if (fs5.existsSync(specsDir)) {
|
|
575
|
+
const specs = walkFiles(specsDir, "openspec/specs");
|
|
576
|
+
if (specs.length) tree.push({ name: "specs", kind: "dir", children: specs });
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
if (hasDocs && fs5.existsSync(path4.join(root, "docs"))) {
|
|
580
|
+
const docs = walkFiles(path4.join(root, "docs"), "docs");
|
|
581
|
+
if (docs.length) tree.push({ name: "docs", kind: "dir", children: docs });
|
|
582
|
+
}
|
|
583
|
+
const skip = /* @__PURE__ */ new Set(["openspec", "docs", "node_modules", ".git", "dist", "test-server"]);
|
|
584
|
+
const etc = [];
|
|
585
|
+
try {
|
|
586
|
+
for (const ent of fs5.readdirSync(root, { withFileTypes: true })) {
|
|
587
|
+
if (ent.name.startsWith(".") || skip.has(ent.name)) continue;
|
|
588
|
+
const ext = path4.extname(ent.name).toLowerCase();
|
|
589
|
+
if (ent.isFile() && (ext === ".md" || ext === ".markdown")) etc.push({ name: ent.name, kind: "file", path: ent.name });
|
|
590
|
+
}
|
|
591
|
+
} catch (e) {
|
|
592
|
+
console.error("[zdashboard] scan root etc failed:", e);
|
|
252
593
|
}
|
|
594
|
+
etc.sort((a, b) => a.name.localeCompare(b.name));
|
|
595
|
+
if (etc.length) tree.push({ name: `\u5176\u4ED6 (${etc.length})`, kind: "dir", children: etc });
|
|
596
|
+
return tree;
|
|
253
597
|
}
|
|
254
598
|
|
|
255
|
-
// src/
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
599
|
+
// src/core/tree.ts
|
|
600
|
+
var apply = {
|
|
601
|
+
inject: ["server"],
|
|
602
|
+
apply(ctx, config) {
|
|
603
|
+
let cached = null;
|
|
604
|
+
const getDet = async () => {
|
|
605
|
+
if (cached?.v) return cached.v;
|
|
606
|
+
if (!cached) cached = { p: detect(config.root).catch(() => ({ hasOpenspec: false, hasDocs: false, hasJust: false, hasBugs: false })) };
|
|
607
|
+
const d = await cached.p;
|
|
608
|
+
cached.v = d;
|
|
609
|
+
return d;
|
|
610
|
+
};
|
|
611
|
+
const server = ctx.server;
|
|
612
|
+
if (server?.route) {
|
|
613
|
+
server.route("/__files", async (_req, res) => {
|
|
614
|
+
try {
|
|
615
|
+
const d = await getDet();
|
|
616
|
+
const tree = scanTree(config.root, d.hasOpenspec, d.hasDocs);
|
|
617
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
|
|
618
|
+
res.end(JSON.stringify({ tree, ...d }));
|
|
619
|
+
} catch {
|
|
620
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
|
|
621
|
+
res.end(JSON.stringify({ tree: [], hasOpenspec: false, hasDocs: false, hasJust: false, hasBugs: false }));
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
// src/core/manifest.ts
|
|
629
|
+
import { Service as Service3 } from "cordis";
|
|
630
|
+
var DashboardService = class extends Service3 {
|
|
631
|
+
static inject = ["server"];
|
|
632
|
+
plugins = /* @__PURE__ */ new Map();
|
|
633
|
+
constructor(ctx) {
|
|
634
|
+
super(ctx, "dashboard");
|
|
635
|
+
this.ctx.effect(() => () => this.plugins.clear());
|
|
636
|
+
this.ctx.server.route("/__plugins", (_req, res) => {
|
|
637
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
|
|
638
|
+
res.end(JSON.stringify({ plugins: Array.from(this.plugins.values()) }));
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
register(manifest) {
|
|
642
|
+
this.plugins.set(manifest.mode, manifest);
|
|
643
|
+
this.ctx.effect(() => () => this.plugins.delete(manifest.mode));
|
|
644
|
+
}
|
|
645
|
+
list() {
|
|
646
|
+
return Array.from(this.plugins.values());
|
|
647
|
+
}
|
|
648
|
+
get(mode) {
|
|
649
|
+
return this.plugins.get(mode);
|
|
650
|
+
}
|
|
651
|
+
};
|
|
652
|
+
|
|
653
|
+
// src/server/just-runner.ts
|
|
654
|
+
import { spawn, execFile as execFile2 } from "child_process";
|
|
655
|
+
var MAX_BUFFER = 1e3;
|
|
656
|
+
var JustRunner = class {
|
|
657
|
+
cwd;
|
|
658
|
+
child = null;
|
|
659
|
+
recipe = null;
|
|
660
|
+
state = "idle";
|
|
661
|
+
code = null;
|
|
662
|
+
buffer = [];
|
|
663
|
+
pending = "";
|
|
664
|
+
// 行缓冲:块缓冲输出(如 maven)的 chunk 会在行中间断开,攒到 \n 才切行
|
|
665
|
+
clients = /* @__PURE__ */ new Set();
|
|
666
|
+
recipesCache = null;
|
|
667
|
+
constructor(cwd) {
|
|
668
|
+
this.cwd = cwd;
|
|
669
|
+
}
|
|
670
|
+
recipes() {
|
|
671
|
+
if (this.recipesCache) return Promise.resolve(this.recipesCache);
|
|
672
|
+
return new Promise((resolve) => {
|
|
673
|
+
execFile2("just", ["--list", "--unsorted"], { cwd: this.cwd, maxBuffer: 1 << 20, timeout: 8e3 }, (err, stdout) => {
|
|
674
|
+
if (err) {
|
|
675
|
+
resolve([]);
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
const out = [];
|
|
679
|
+
const seen = /* @__PURE__ */ new Set();
|
|
680
|
+
for (const line of stdout.split(/\r?\n/).slice(1)) {
|
|
681
|
+
const trimmed = line.trim();
|
|
682
|
+
if (!trimmed) continue;
|
|
683
|
+
const hashIdx = trimmed.indexOf("#");
|
|
684
|
+
const sig = (hashIdx >= 0 ? trimmed.slice(0, hashIdx) : trimmed).trim();
|
|
685
|
+
if (!sig) continue;
|
|
686
|
+
const name = sig.split(/\s+/)[0];
|
|
687
|
+
if (seen.has(name)) continue;
|
|
688
|
+
seen.add(name);
|
|
689
|
+
out.push({ name, description: hashIdx >= 0 ? trimmed.slice(hashIdx + 1).trim() : "" });
|
|
690
|
+
}
|
|
691
|
+
this.recipesCache = out;
|
|
692
|
+
resolve(out);
|
|
693
|
+
});
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
subscribe(fn) {
|
|
697
|
+
this.clients.add(fn);
|
|
698
|
+
for (const text of this.buffer) fn({ type: "log", text });
|
|
699
|
+
fn({ type: "state", state: this.state, recipe: this.recipe, code: this.code });
|
|
700
|
+
return () => this.clients.delete(fn);
|
|
701
|
+
}
|
|
702
|
+
emit(ev) {
|
|
703
|
+
for (const fn of this.clients) fn(ev);
|
|
704
|
+
}
|
|
705
|
+
info() {
|
|
706
|
+
return { state: this.state, recipe: this.recipe, code: this.code };
|
|
707
|
+
}
|
|
708
|
+
/** 启动 recipe(调用方须先用 recipes() 校验名字);自动停旧进程 */
|
|
709
|
+
start(recipe) {
|
|
710
|
+
this.killChild();
|
|
711
|
+
this.recipe = recipe;
|
|
712
|
+
this.code = null;
|
|
713
|
+
this.state = "running";
|
|
714
|
+
this.buffer = [];
|
|
715
|
+
this.pending = "";
|
|
716
|
+
this.emit({ type: "clear" });
|
|
717
|
+
this.emit({ type: "state", state: "running", recipe, code: null });
|
|
718
|
+
const child = spawn("just", [recipe], {
|
|
719
|
+
cwd: this.cwd,
|
|
720
|
+
shell: true,
|
|
721
|
+
env: {
|
|
722
|
+
...process.env,
|
|
723
|
+
FORCE_COLOR: "1",
|
|
724
|
+
// node 生态(chalk 等)
|
|
725
|
+
// maven 检测非 tty 会关颜色;经 MAVEN_OPTS 强制开(保留用户已有值)
|
|
726
|
+
MAVEN_OPTS: `${process.env.MAVEN_OPTS ?? ""} -Dstyle.color=always`.trim(),
|
|
727
|
+
CI: ""
|
|
728
|
+
}
|
|
729
|
+
});
|
|
730
|
+
this.child = child;
|
|
731
|
+
const push = (d) => {
|
|
732
|
+
this.pending += d.toString();
|
|
733
|
+
let idx;
|
|
734
|
+
while ((idx = this.pending.indexOf("\n")) >= 0) {
|
|
735
|
+
const line = this.pending.slice(0, idx + 1);
|
|
736
|
+
this.pending = this.pending.slice(idx + 1);
|
|
737
|
+
this.pushLine(line);
|
|
738
|
+
}
|
|
739
|
+
};
|
|
740
|
+
child.stdout?.on("data", push);
|
|
741
|
+
child.stderr?.on("data", push);
|
|
742
|
+
child.on("error", (err) => {
|
|
743
|
+
this.pushLine(`[zdashboard] spawn error: ${err.message}
|
|
744
|
+
`);
|
|
745
|
+
});
|
|
746
|
+
child.on("exit", (code) => {
|
|
747
|
+
if (this.pending) {
|
|
748
|
+
this.pushLine(this.pending + "\n");
|
|
749
|
+
this.pending = "";
|
|
750
|
+
}
|
|
751
|
+
this.child = null;
|
|
752
|
+
this.state = "exited";
|
|
753
|
+
this.code = code ?? 0;
|
|
754
|
+
this.emit({ type: "state", state: "exited", recipe: this.recipe, code: this.code });
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
pushLine(line) {
|
|
758
|
+
this.buffer.push(line);
|
|
759
|
+
if (this.buffer.length > MAX_BUFFER) this.buffer.shift();
|
|
760
|
+
this.emit({ type: "log", text: line });
|
|
761
|
+
}
|
|
762
|
+
stop() {
|
|
763
|
+
this.killChild();
|
|
764
|
+
}
|
|
765
|
+
restart(recipe) {
|
|
766
|
+
const target = recipe ?? this.recipe;
|
|
767
|
+
if (target) this.start(target);
|
|
768
|
+
}
|
|
769
|
+
killChild() {
|
|
770
|
+
const child = this.child;
|
|
771
|
+
if (child?.pid) {
|
|
772
|
+
try {
|
|
773
|
+
if (process.platform === "win32") spawn("taskkill", ["/PID", String(child.pid), "/T", "/F"]);
|
|
774
|
+
else child.kill("SIGTERM");
|
|
775
|
+
} catch {
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
this.child = null;
|
|
779
|
+
}
|
|
780
|
+
};
|
|
781
|
+
|
|
782
|
+
// src/plugins/just/index.ts
|
|
783
|
+
var apply2 = {
|
|
784
|
+
inject: ["server", "dashboard"],
|
|
785
|
+
apply(ctx, config) {
|
|
786
|
+
const root = config.root;
|
|
787
|
+
ctx.inject(["server"], () => {
|
|
788
|
+
if (!ctx.server?.route) return;
|
|
789
|
+
const runner = new JustRunner(root);
|
|
790
|
+
ctx.effect(() => () => runner.stop());
|
|
791
|
+
ctx.server.route("/__just/recipes", async (_req, res) => {
|
|
792
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
|
|
793
|
+
try {
|
|
794
|
+
const recipes = await runner.recipes();
|
|
795
|
+
res.end(JSON.stringify(recipes));
|
|
796
|
+
} catch {
|
|
797
|
+
res.end(JSON.stringify([]));
|
|
798
|
+
}
|
|
799
|
+
});
|
|
800
|
+
ctx.server.sse("/__just/logs", (res) => {
|
|
801
|
+
const unsub = runner.subscribe((ev) => {
|
|
802
|
+
res.write(`data: ${JSON.stringify(ev)}
|
|
803
|
+
|
|
804
|
+
`);
|
|
805
|
+
});
|
|
806
|
+
return unsub;
|
|
807
|
+
});
|
|
808
|
+
ctx.server.route("/__just/start", async (req, res) => {
|
|
809
|
+
if (req.headers["x-stop-token"] !== ctx.server.stopToken) {
|
|
810
|
+
res.writeHead(403);
|
|
811
|
+
res.end("forbidden");
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
const body = await readBody(req);
|
|
815
|
+
let recipe;
|
|
816
|
+
try {
|
|
817
|
+
recipe = JSON.parse(body || "{}").recipe;
|
|
818
|
+
} catch {
|
|
819
|
+
}
|
|
820
|
+
const target = recipe ?? runner.info().recipe;
|
|
821
|
+
if (!target) {
|
|
822
|
+
res.writeHead(400);
|
|
823
|
+
res.end('{"error":"no recipe"}');
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
const recipes = await runner.recipes();
|
|
827
|
+
if (!recipes.some((r) => r.name === target)) {
|
|
828
|
+
res.writeHead(403);
|
|
829
|
+
res.end('{"error":"unknown recipe"}');
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
runner.start(target);
|
|
833
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
|
|
834
|
+
res.end(JSON.stringify(runner.info()));
|
|
835
|
+
});
|
|
836
|
+
ctx.server.route("/__just/stop", async (req, res) => {
|
|
837
|
+
if (req.headers["x-stop-token"] !== ctx.server.stopToken) {
|
|
838
|
+
res.writeHead(403);
|
|
839
|
+
res.end("forbidden");
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
runner.stop();
|
|
843
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
|
|
844
|
+
res.end(JSON.stringify(runner.info()));
|
|
845
|
+
});
|
|
846
|
+
ctx.server.route("/__just/restart", async (req, res) => {
|
|
847
|
+
if (req.headers["x-stop-token"] !== ctx.server.stopToken) {
|
|
848
|
+
res.writeHead(403);
|
|
849
|
+
res.end("forbidden");
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
const body = await readBody(req);
|
|
853
|
+
let recipe;
|
|
854
|
+
try {
|
|
855
|
+
recipe = JSON.parse(body || "{}").recipe;
|
|
856
|
+
} catch {
|
|
857
|
+
}
|
|
858
|
+
const target = recipe ?? runner.info().recipe;
|
|
859
|
+
if (!target) {
|
|
860
|
+
res.writeHead(400);
|
|
861
|
+
res.end('{"error":"no recipe"}');
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
const recipes = await runner.recipes();
|
|
865
|
+
if (!recipes.some((r) => r.name === target)) {
|
|
866
|
+
res.writeHead(403);
|
|
867
|
+
res.end('{"error":"unknown recipe"}');
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
runner.restart(target);
|
|
871
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
|
|
872
|
+
res.end(JSON.stringify(runner.info()));
|
|
873
|
+
});
|
|
874
|
+
ctx.dashboard.register({ mode: "just", label: "Just Runner", icon: "\u{1F4DC}", description: "Just \u4EFB\u52A1\u65E5\u5FD7\u4E0E\u6267\u884C" });
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
};
|
|
878
|
+
async function readBody(req) {
|
|
879
|
+
return new Promise((resolve) => {
|
|
880
|
+
let data = "";
|
|
881
|
+
req.on("data", (c) => {
|
|
882
|
+
data += c;
|
|
883
|
+
});
|
|
884
|
+
req.on("end", () => resolve(data));
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// src/server/bugs.ts
|
|
889
|
+
import fs6 from "fs";
|
|
890
|
+
import path5 from "path";
|
|
891
|
+
|
|
892
|
+
// src/server/api/fetch.ts
|
|
893
|
+
import ky from "ky";
|
|
894
|
+
|
|
895
|
+
// src/server/errors.ts
|
|
896
|
+
var HttpError = class extends Error {
|
|
897
|
+
constructor(status, message, body) {
|
|
898
|
+
super(message);
|
|
899
|
+
this.status = status;
|
|
900
|
+
this.body = body;
|
|
901
|
+
this.name = "HttpError";
|
|
902
|
+
}
|
|
903
|
+
status;
|
|
904
|
+
body;
|
|
905
|
+
};
|
|
906
|
+
var NetworkError = class extends Error {
|
|
907
|
+
constructor(message, cause) {
|
|
908
|
+
super(message);
|
|
909
|
+
this.cause = cause;
|
|
910
|
+
this.name = "NetworkError";
|
|
911
|
+
}
|
|
912
|
+
cause;
|
|
913
|
+
};
|
|
914
|
+
|
|
915
|
+
// src/server/api/fetch.ts
|
|
916
|
+
async function fetchJson(url, init) {
|
|
917
|
+
try {
|
|
918
|
+
const res = await ky(url, { ...init, timeout: 8e3, retry: 2 });
|
|
919
|
+
return await res.json();
|
|
920
|
+
} catch (e) {
|
|
921
|
+
if (e instanceof HttpError) throw e;
|
|
922
|
+
if (e instanceof Error && e.name === "TimeoutError") {
|
|
923
|
+
throw new NetworkError(`\u8BF7\u6C42\u8D85\u65F6: ${url}`);
|
|
924
|
+
}
|
|
925
|
+
if (e instanceof Error && e.name === "HTTPError") {
|
|
926
|
+
throw new HttpError(e.status ?? 500, e.message);
|
|
927
|
+
}
|
|
928
|
+
throw new NetworkError(`\u8BF7\u6C42\u5931\u8D25: ${url}`, e);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// src/server/bugs.ts
|
|
933
|
+
function loadZgoalConfig(root) {
|
|
934
|
+
const file = path5.join(root, ".zgoal", "config.yaml");
|
|
935
|
+
if (!fs6.existsSync(file)) return null;
|
|
936
|
+
const kv = {};
|
|
937
|
+
for (const line of fs6.readFileSync(file, "utf8").split("\n")) {
|
|
938
|
+
const m = line.match(/^\s*([A-Za-z_]\w*)\s*:\s*(.+?)\s*$/);
|
|
939
|
+
if (m && !m[2].startsWith("#")) kv[m[1]] = m[2].replace(/^["']|["']$/g, "");
|
|
940
|
+
}
|
|
264
941
|
const product = Number(kv.product);
|
|
265
942
|
if (!kv.url || !product) return null;
|
|
266
943
|
return {
|
|
@@ -302,8 +979,8 @@ function normBug(b, account) {
|
|
|
302
979
|
mine
|
|
303
980
|
};
|
|
304
981
|
}
|
|
305
|
-
async function fetchBugs(
|
|
306
|
-
const cfg = loadZgoalConfig(
|
|
982
|
+
async function fetchBugs(root) {
|
|
983
|
+
const cfg = loadZgoalConfig(root);
|
|
307
984
|
if (!cfg) return { ok: false, error: ".zgoal/config.yaml \u7F3A\u5931\u6216 url/product \u672A\u914D\u7F6E(\u7531 zgoal skill \u521B\u5EFA)" };
|
|
308
985
|
try {
|
|
309
986
|
const token = await getToken(cfg);
|
|
@@ -319,561 +996,152 @@ async function fetchBugs(root2) {
|
|
|
319
996
|
}
|
|
320
997
|
}
|
|
321
998
|
|
|
322
|
-
// src/
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
function allBuiltins() {
|
|
332
|
-
return Array.from(builtinPlugins.values());
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
// src/server/review-store.ts
|
|
336
|
-
import fs4 from "fs";
|
|
337
|
-
import path4 from "path";
|
|
338
|
-
import YAML from "yaml";
|
|
339
|
-
var REVIEW_FILE = "review.yaml";
|
|
340
|
-
var ReviewStore = class {
|
|
341
|
-
root;
|
|
342
|
-
file;
|
|
343
|
-
onChange;
|
|
344
|
-
constructor(root2, onChange) {
|
|
345
|
-
this.root = root2;
|
|
346
|
-
this.file = path4.join(root2, REVIEW_FILE);
|
|
347
|
-
this.onChange = onChange;
|
|
348
|
-
}
|
|
349
|
-
exists() {
|
|
350
|
-
return fs4.existsSync(this.file);
|
|
351
|
-
}
|
|
352
|
-
read() {
|
|
353
|
-
try {
|
|
354
|
-
const parsed = YAML.parse(fs4.readFileSync(this.file, "utf8"));
|
|
355
|
-
if (!parsed || !Array.isArray(parsed.items)) return { status: "draft", items: [] };
|
|
356
|
-
return parsed;
|
|
357
|
-
} catch {
|
|
358
|
-
return { status: "draft", items: [] };
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
write(data) {
|
|
362
|
-
fs4.writeFileSync(this.file, YAML.stringify(data), "utf8");
|
|
363
|
-
this.onChange?.();
|
|
364
|
-
}
|
|
365
|
-
updateItem(id, patch) {
|
|
366
|
-
const data = this.read();
|
|
367
|
-
const item = data.items.find((i) => i.id === id);
|
|
368
|
-
if (!item) throw new Error(`item ${id} not found`);
|
|
369
|
-
if (patch.answer !== void 0) item.answer = patch.answer;
|
|
370
|
-
if (patch.state !== void 0) item.state = patch.state;
|
|
371
|
-
if (patch.state === "answered" && !patch.answer && !item.answer) item.answer = "";
|
|
372
|
-
this.write(data);
|
|
373
|
-
return data;
|
|
374
|
-
}
|
|
375
|
-
setStatus(status) {
|
|
376
|
-
const data = this.read();
|
|
377
|
-
if (status === "passed" && data.items.some((i) => i.state === "open")) {
|
|
378
|
-
throw new Error("\u5B58\u5728\u672A\u5904\u7406\u7684\u8BC4\u5BA1\u9879(open),\u4E0D\u80FD\u901A\u8FC7");
|
|
379
|
-
}
|
|
380
|
-
data.status = status;
|
|
381
|
-
this.write(data);
|
|
382
|
-
return data;
|
|
383
|
-
}
|
|
384
|
-
docs() {
|
|
385
|
-
try {
|
|
386
|
-
return fs4.readdirSync(this.root).filter((f) => /\.(md|markdown)$/i.test(f) && fs4.statSync(path4.join(this.root, f)).isFile()).sort();
|
|
387
|
-
} catch {
|
|
388
|
-
return [];
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
};
|
|
392
|
-
|
|
393
|
-
// src/server/design-assets.ts
|
|
394
|
-
import fs5 from "fs";
|
|
395
|
-
import path5 from "path";
|
|
396
|
-
var PAGE_EXTS = [".html", ".htm"];
|
|
397
|
-
var ICON_EXTS = [".svg", ".png", ".ico", ".jpg", ".jpeg", ".gif", ".webp"];
|
|
398
|
-
var VIDEO_EXTS = [".mp4", ".webm", ".mov", ".ogg", ".ogv"];
|
|
399
|
-
var AUDIO_EXTS = [".mp3", ".wav", ".flac", ".aac", ".m4a"];
|
|
400
|
-
var CODE_EXTS = [".js", ".mjs", ".ts", ".tsx", ".jsx", ".css", ".json", ".txt", ".xml", ".yml", ".yaml", ".sh", ".md"];
|
|
401
|
-
var FONT_EXTS = [".woff", ".woff2", ".ttf", ".otf"];
|
|
402
|
-
var TOKEN_RE = /token|theme|design|color|palette|typograph/i;
|
|
403
|
-
function categorize(rel, ext) {
|
|
404
|
-
if (rel.indexOf("components/") === 0) return "component";
|
|
405
|
-
if (VIDEO_EXTS.includes(ext)) return "video";
|
|
406
|
-
if (AUDIO_EXTS.includes(ext)) return "audio";
|
|
407
|
-
if (ext === ".pdf") return "pdf";
|
|
408
|
-
if (ext === ".md") return "md";
|
|
409
|
-
if (FONT_EXTS.includes(ext)) return "font";
|
|
410
|
-
if (ICON_EXTS.includes(ext)) return "icon";
|
|
411
|
-
if (PAGE_EXTS.includes(ext)) return "page";
|
|
412
|
-
if (CODE_EXTS.includes(ext)) return TOKEN_RE.test(rel) && (ext === ".css" || ext === ".json") ? "token" : "code";
|
|
413
|
-
return "other";
|
|
414
|
-
}
|
|
415
|
-
function scanAssets(root2) {
|
|
416
|
-
const out = {};
|
|
417
|
-
const keys = ["page", "component", "icon", "token", "md", "video", "audio", "pdf", "code", "font", "other"];
|
|
418
|
-
for (const k of keys) out[k] = [];
|
|
419
|
-
const SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "coverage", ".next", ".cache"]);
|
|
420
|
-
function walk(dir, rel) {
|
|
421
|
-
let ents;
|
|
422
|
-
try {
|
|
423
|
-
ents = fs5.readdirSync(dir, { withFileTypes: true });
|
|
424
|
-
} catch {
|
|
425
|
-
return;
|
|
426
|
-
}
|
|
427
|
-
for (const ent of ents) {
|
|
428
|
-
if (ent.name.startsWith(".") || SKIP.has(ent.name)) continue;
|
|
429
|
-
const r = rel ? `${rel}/${ent.name}` : ent.name;
|
|
430
|
-
if (ent.isDirectory()) {
|
|
431
|
-
walk(path5.join(dir, ent.name), r);
|
|
432
|
-
continue;
|
|
433
|
-
}
|
|
434
|
-
const ext = path5.extname(ent.name).toLowerCase();
|
|
435
|
-
const t = categorize(r, ext);
|
|
436
|
-
out[t].push({ path: r, name: ent.name, ext, type: t });
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
walk(root2, "");
|
|
440
|
-
return out;
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
// package.json
|
|
444
|
-
var package_default = {
|
|
445
|
-
name: "zdashboard",
|
|
446
|
-
version: "1.6.2",
|
|
447
|
-
description: "ZCode skill dashboard platform \u2014 pluggable viewers for zdesign/zview/zreview/zgoal",
|
|
448
|
-
type: "module",
|
|
449
|
-
bin: {
|
|
450
|
-
zdashboard: "./dist/cli.js"
|
|
451
|
-
},
|
|
452
|
-
files: [
|
|
453
|
-
"dist"
|
|
454
|
-
],
|
|
455
|
-
publishConfig: {
|
|
456
|
-
access: "public"
|
|
457
|
-
},
|
|
458
|
-
scripts: {
|
|
459
|
-
dev: "vite",
|
|
460
|
-
build: "tsup && vite build",
|
|
461
|
-
"build:web": "vite build",
|
|
462
|
-
"build:node": "tsup",
|
|
463
|
-
start: "node dist/cli.js",
|
|
464
|
-
preview: "vite preview"
|
|
465
|
-
},
|
|
466
|
-
dependencies: {
|
|
467
|
-
"@radix-ui/react-scroll-area": "^1.2.0",
|
|
468
|
-
"@radix-ui/react-separator": "^1.1.0",
|
|
469
|
-
"@radix-ui/react-slot": "^1.1.0",
|
|
470
|
-
"@radix-ui/react-tooltip": "^1.1.2",
|
|
471
|
-
"@uidotdev/usehooks": "^2.4.0",
|
|
472
|
-
"ansi-to-react": "^6.1.6",
|
|
473
|
-
"class-variance-authority": "^0.7.0",
|
|
474
|
-
clsx: "^2.1.1",
|
|
475
|
-
"date-fns": "^3.6.0",
|
|
476
|
-
filesize: "^11.0.0",
|
|
477
|
-
"highlight.js": "^11.11.1",
|
|
478
|
-
katex: "^0.16.11",
|
|
479
|
-
ky: "^1.7.0",
|
|
480
|
-
"lodash-es": "^4.17.21",
|
|
481
|
-
"lucide-react": "^0.460.0",
|
|
482
|
-
react: "^18.3.1",
|
|
483
|
-
"react-dom": "^18.3.1",
|
|
484
|
-
"react-error-boundary": "^5.0.0",
|
|
485
|
-
"react-markdown": "^9.0.1",
|
|
486
|
-
"rehype-autolink-headings": "^7.1.0",
|
|
487
|
-
"rehype-highlight": "^7.0.1",
|
|
488
|
-
"rehype-katex": "^7.0.1",
|
|
489
|
-
"rehype-raw": "^7.0.0",
|
|
490
|
-
"rehype-slug": "^6.0.0",
|
|
491
|
-
"remark-frontmatter": "^5.0.0",
|
|
492
|
-
"remark-gfm": "^4.0.0",
|
|
493
|
-
"remark-math": "^6.0.0",
|
|
494
|
-
sonner: "^1.5.0",
|
|
495
|
-
"tailwind-merge": "^2.5.4",
|
|
496
|
-
"use-debounce": "^10.0.0",
|
|
497
|
-
yaml: "^2.9.0"
|
|
498
|
-
},
|
|
499
|
-
devDependencies: {
|
|
500
|
-
"@tailwindcss/typography": "^0.5.20",
|
|
501
|
-
"@testing-library/jest-dom": "^7.0.1",
|
|
502
|
-
"@testing-library/react": "^16.3.2",
|
|
503
|
-
"@types/lodash-es": "^4.17.12",
|
|
504
|
-
"@types/node": "^22.9.0",
|
|
505
|
-
"@types/react": "^18.3.12",
|
|
506
|
-
"@types/react-dom": "^18.3.1",
|
|
507
|
-
"@vitejs/plugin-react": "^4.7.0",
|
|
508
|
-
autoprefixer: "^10.4.20",
|
|
509
|
-
jsdom: "^30.0.1",
|
|
510
|
-
postcss: "^8.4.49",
|
|
511
|
-
tailwindcss: "^3.4.14",
|
|
512
|
-
"tailwindcss-animate": "^1.0.7",
|
|
513
|
-
tsup: "^8.3.5",
|
|
514
|
-
typescript: "^5.6.3",
|
|
515
|
-
vite: "^5.4.10",
|
|
516
|
-
vitest: "^1.6.0"
|
|
517
|
-
},
|
|
518
|
-
pnpm: {
|
|
519
|
-
onlyBuiltDependencies: [
|
|
520
|
-
"esbuild"
|
|
521
|
-
]
|
|
522
|
-
}
|
|
523
|
-
};
|
|
524
|
-
|
|
525
|
-
// src/server/index.ts
|
|
526
|
-
var VERSION = package_default.version;
|
|
527
|
-
var __dirname2 = path6.dirname(fileURLToPath2(import.meta.url));
|
|
528
|
-
var STOP_TOKEN = crypto.randomBytes(12).toString("hex");
|
|
529
|
-
var INJECT = `<script>(function(){try{var es=new EventSource('/__reload');es.addEventListener('reload',function(){location.reload();});es.onerror=function(){es.close();};}catch(e){}document.addEventListener('click',function(e){var t=e.target;if(t&&t.closest){var a=t.closest('a[target]');if(a&&a.target!=='_self'){a.target='_self';}}},true);})();</script>`;
|
|
530
|
-
var MIME = {
|
|
531
|
-
".html": "text/html; charset=utf-8",
|
|
532
|
-
".htm": "text/html; charset=utf-8",
|
|
533
|
-
".css": "text/css; charset=utf-8",
|
|
534
|
-
".js": "application/javascript; charset=utf-8",
|
|
535
|
-
".mjs": "application/javascript; charset=utf-8",
|
|
536
|
-
".json": "application/json; charset=utf-8",
|
|
537
|
-
".svg": "image/svg+xml",
|
|
538
|
-
".png": "image/png",
|
|
539
|
-
".ico": "image/x-icon",
|
|
540
|
-
".jpg": "image/jpeg",
|
|
541
|
-
".jpeg": "image/jpeg",
|
|
542
|
-
".gif": "image/gif",
|
|
543
|
-
".webp": "image/webp",
|
|
544
|
-
".md": "text/markdown; charset=utf-8",
|
|
545
|
-
".txt": "text/plain; charset=utf-8",
|
|
546
|
-
".sql": "text/plain; charset=utf-8",
|
|
547
|
-
".csv": "text/plain; charset=utf-8",
|
|
548
|
-
".tsv": "text/plain; charset=utf-8",
|
|
549
|
-
".yaml": "text/yaml; charset=utf-8",
|
|
550
|
-
".yml": "text/yaml; charset=utf-8",
|
|
551
|
-
".xml": "application/xml; charset=utf-8",
|
|
552
|
-
".py": "text/plain; charset=utf-8",
|
|
553
|
-
".ts": "text/plain; charset=utf-8",
|
|
554
|
-
".java": "text/plain; charset=utf-8",
|
|
555
|
-
".go": "text/plain; charset=utf-8",
|
|
556
|
-
".rs": "text/plain; charset=utf-8",
|
|
557
|
-
".rb": "text/plain; charset=utf-8",
|
|
558
|
-
".php": "text/plain; charset=utf-8",
|
|
559
|
-
".c": "text/plain; charset=utf-8",
|
|
560
|
-
".cpp": "text/plain; charset=utf-8",
|
|
561
|
-
".h": "text/plain; charset=utf-8",
|
|
562
|
-
".cs": "text/plain; charset=utf-8",
|
|
563
|
-
".swift": "text/plain; charset=utf-8",
|
|
564
|
-
".kt": "text/plain; charset=utf-8",
|
|
565
|
-
".scala": "text/plain; charset=utf-8",
|
|
566
|
-
".sh": "text/plain; charset=utf-8",
|
|
567
|
-
".bash": "text/plain; charset=utf-8",
|
|
568
|
-
".zsh": "text/plain; charset=utf-8",
|
|
569
|
-
".fish": "text/plain; charset=utf-8",
|
|
570
|
-
".env": "text/plain; charset=utf-8",
|
|
571
|
-
".gitignore": "text/plain; charset=utf-8",
|
|
572
|
-
".dockerfile": "text/plain; charset=utf-8",
|
|
573
|
-
".woff": "font/woff",
|
|
574
|
-
".woff2": "font/woff2",
|
|
575
|
-
".ttf": "font/woff",
|
|
576
|
-
".map": "application/json; charset=utf-8"
|
|
577
|
-
};
|
|
578
|
-
function readBody(req) {
|
|
579
|
-
return new Promise((resolve) => {
|
|
580
|
-
let data = "";
|
|
581
|
-
req.on("data", (c) => data += c);
|
|
582
|
-
req.on("end", () => resolve(data));
|
|
583
|
-
});
|
|
584
|
-
}
|
|
585
|
-
function createServer(opts) {
|
|
586
|
-
const ROOT = path6.resolve(opts.root);
|
|
587
|
-
const PORT0 = opts.port ?? 4190;
|
|
588
|
-
const OPEN = !!opts.open;
|
|
589
|
-
const APP_DIR = opts.dashboardDir ?? path6.resolve(__dirname2, "web");
|
|
590
|
-
if (!fs6.existsSync(ROOT)) fs6.mkdirSync(ROOT, { recursive: true });
|
|
591
|
-
const det2 = opts.detect;
|
|
592
|
-
const runner = new JustRunner(ROOT);
|
|
593
|
-
const MODE = opts.mode;
|
|
594
|
-
registerBuiltin({
|
|
595
|
-
mode: "bugs",
|
|
596
|
-
label: "\u7985\u9053 Bugs",
|
|
597
|
-
icon: "\u{1F3AF}",
|
|
598
|
-
apiRoutes: { "/__bugs": async (_, res) => {
|
|
599
|
-
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
|
|
600
|
-
fetchBugs(ROOT).then((r) => res.end(JSON.stringify(r)));
|
|
601
|
-
} }
|
|
602
|
-
});
|
|
603
|
-
registerBuiltin({ mode: "view", label: "\u9879\u76EE\u6D4F\u89C8", icon: "\u{1F441}\uFE0F" });
|
|
604
|
-
const reviewStore = new ReviewStore(ROOT);
|
|
605
|
-
registerBuiltin({
|
|
606
|
-
mode: "review",
|
|
607
|
-
label: "\u6587\u6863\u8BC4\u5BA1",
|
|
608
|
-
icon: "\u2705",
|
|
609
|
-
apiRoutes: {
|
|
610
|
-
"/__review": async (_, res) => {
|
|
999
|
+
// src/plugins/bugs/index.ts
|
|
1000
|
+
var apply3 = {
|
|
1001
|
+
inject: ["server", "dashboard"],
|
|
1002
|
+
apply(ctx, config) {
|
|
1003
|
+
const root = config.root;
|
|
1004
|
+
ctx.inject(["server"], () => {
|
|
1005
|
+
if (!ctx.server?.route) return;
|
|
1006
|
+
ctx.dashboard.register({ mode: "bugs", label: "\u7985\u9053 Bugs", icon: "\u{1F3AF}", description: "\u53EA\u8BFB bug \u5217\u8868" });
|
|
1007
|
+
ctx.server.route("/__bugs", async (_req, res) => {
|
|
611
1008
|
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
res.
|
|
617
|
-
res.end("forbidden");
|
|
618
|
-
return;
|
|
619
|
-
}
|
|
620
|
-
(async () => {
|
|
621
|
-
try {
|
|
622
|
-
const body = JSON.parse(await readBody(req) || "{}");
|
|
623
|
-
const data = reviewStore.updateItem(body.id, { answer: body.answer, state: body.state });
|
|
624
|
-
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
|
|
625
|
-
res.end(JSON.stringify(data));
|
|
626
|
-
} catch (e) {
|
|
627
|
-
res.writeHead(400, { "Content-Type": "application/json; charset=utf-8" });
|
|
628
|
-
res.end(JSON.stringify({ error: e.message }));
|
|
629
|
-
}
|
|
630
|
-
})();
|
|
631
|
-
return;
|
|
632
|
-
},
|
|
633
|
-
"/__review/status": async (req, res) => {
|
|
634
|
-
if (req.headers["x-stop-token"] !== STOP_TOKEN) {
|
|
635
|
-
res.writeHead(403);
|
|
636
|
-
res.end("forbidden");
|
|
637
|
-
return;
|
|
1009
|
+
try {
|
|
1010
|
+
const result = await fetchBugs(root);
|
|
1011
|
+
res.end(JSON.stringify(result));
|
|
1012
|
+
} catch (e) {
|
|
1013
|
+
res.end(JSON.stringify({ ok: false, error: e instanceof Error ? e.message : "unknown error" }));
|
|
638
1014
|
}
|
|
639
|
-
|
|
640
|
-
try {
|
|
641
|
-
const body = JSON.parse(await readBody(req) || "{}");
|
|
642
|
-
const data = reviewStore.setStatus(body.status);
|
|
643
|
-
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
|
|
644
|
-
res.end(JSON.stringify(data));
|
|
645
|
-
} catch (e) {
|
|
646
|
-
res.writeHead(400, { "Content-Type": "application/json; charset=utf-8" });
|
|
647
|
-
res.end(JSON.stringify({ error: e.message }));
|
|
648
|
-
}
|
|
649
|
-
})();
|
|
650
|
-
return;
|
|
651
|
-
},
|
|
652
|
-
"/__docs": async (_, res) => {
|
|
653
|
-
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
|
|
654
|
-
res.end(JSON.stringify(reviewStore.docs()));
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
});
|
|
658
|
-
registerBuiltin({ mode: "design", label: "\u8BBE\u8BA1\u8D44\u4EA7", icon: "\u{1F3A8}" });
|
|
659
|
-
registerBuiltin({
|
|
660
|
-
mode: "apply",
|
|
661
|
-
label: "\u6267\u884C\u8FDB\u5EA6",
|
|
662
|
-
icon: "\u2699\uFE0F",
|
|
663
|
-
apiRoutes: {
|
|
664
|
-
"/__apply": async (_, res) => {
|
|
665
|
-
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
|
|
666
|
-
res.end(JSON.stringify(scanApplyChanges(ROOT)));
|
|
667
|
-
},
|
|
668
|
-
"/__apply/change": async (req, res) => {
|
|
669
|
-
(async () => {
|
|
670
|
-
try {
|
|
671
|
-
const url = new URL(req.url || "", "http://x");
|
|
672
|
-
const name = url.searchParams.get("name");
|
|
673
|
-
if (!name) {
|
|
674
|
-
res.writeHead(400);
|
|
675
|
-
res.end(JSON.stringify({ error: "missing name" }));
|
|
676
|
-
return;
|
|
677
|
-
}
|
|
678
|
-
const data = readApplyChange(ROOT, name);
|
|
679
|
-
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
|
|
680
|
-
res.end(JSON.stringify(data));
|
|
681
|
-
} catch (e) {
|
|
682
|
-
res.writeHead(400);
|
|
683
|
-
res.end(JSON.stringify({ error: e.message }));
|
|
684
|
-
}
|
|
685
|
-
})();
|
|
686
|
-
return;
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
});
|
|
690
|
-
const clients = /* @__PURE__ */ new Set();
|
|
691
|
-
const broadcast = (ev, data = "") => {
|
|
692
|
-
const payload = `event: ${ev}
|
|
693
|
-
data: ${JSON.stringify(data == null ? "" : data)}
|
|
694
|
-
|
|
695
|
-
`;
|
|
696
|
-
for (const c of clients) c.write(payload);
|
|
697
|
-
};
|
|
698
|
-
function serveFile(filePath, res, injectHtml) {
|
|
699
|
-
fs6.readFile(filePath, (err, data) => {
|
|
700
|
-
if (err) {
|
|
701
|
-
res.writeHead(404);
|
|
702
|
-
return res.end("Not found");
|
|
703
|
-
}
|
|
704
|
-
const ext = path6.extname(filePath).toLowerCase();
|
|
705
|
-
const ct = MIME[ext] ?? "application/octet-stream";
|
|
706
|
-
let body = data;
|
|
707
|
-
if (injectHtml && ext === ".html") {
|
|
708
|
-
const s = data.toString("utf8");
|
|
709
|
-
body = Buffer.from(s.indexOf("</body>") >= 0 ? s.replace("</body>", INJECT + "</body>") : s + INJECT);
|
|
710
|
-
}
|
|
711
|
-
res.writeHead(200, { "Content-Type": ct, "Cache-Control": "no-cache" });
|
|
712
|
-
res.end(body);
|
|
1015
|
+
});
|
|
713
1016
|
});
|
|
714
1017
|
}
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
} else {
|
|
742
|
-
res.writeHead(403);
|
|
743
|
-
res.end("forbidden");
|
|
744
|
-
}
|
|
745
|
-
return;
|
|
1018
|
+
};
|
|
1019
|
+
|
|
1020
|
+
// src/server/review-store.ts
|
|
1021
|
+
import fs7 from "fs";
|
|
1022
|
+
import path6 from "path";
|
|
1023
|
+
import YAML from "yaml";
|
|
1024
|
+
var REVIEW_FILE = "review.yaml";
|
|
1025
|
+
var ReviewStore = class {
|
|
1026
|
+
root;
|
|
1027
|
+
file;
|
|
1028
|
+
onChange;
|
|
1029
|
+
constructor(root, onChange) {
|
|
1030
|
+
this.root = root;
|
|
1031
|
+
this.file = path6.join(root, REVIEW_FILE);
|
|
1032
|
+
this.onChange = onChange;
|
|
1033
|
+
}
|
|
1034
|
+
exists() {
|
|
1035
|
+
return fs7.existsSync(this.file);
|
|
1036
|
+
}
|
|
1037
|
+
read() {
|
|
1038
|
+
try {
|
|
1039
|
+
const parsed = YAML.parse(fs7.readFileSync(this.file, "utf8"));
|
|
1040
|
+
if (!parsed || !Array.isArray(parsed.items)) return { status: "draft", items: [] };
|
|
1041
|
+
return parsed;
|
|
1042
|
+
} catch {
|
|
1043
|
+
return { status: "draft", items: [] };
|
|
746
1044
|
}
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
1045
|
+
}
|
|
1046
|
+
write(data) {
|
|
1047
|
+
fs7.writeFileSync(this.file, YAML.stringify(data), "utf8");
|
|
1048
|
+
this.onChange?.();
|
|
1049
|
+
}
|
|
1050
|
+
updateItem(id, patch) {
|
|
1051
|
+
const data = this.read();
|
|
1052
|
+
const item = data.items.find((i) => i.id === id);
|
|
1053
|
+
if (!item) throw new Error(`item ${id} not found`);
|
|
1054
|
+
if (patch.answer !== void 0) item.answer = patch.answer;
|
|
1055
|
+
if (patch.state !== void 0) item.state = patch.state;
|
|
1056
|
+
if (patch.state === "answered" && !patch.answer && !item.answer) item.answer = "";
|
|
1057
|
+
this.write(data);
|
|
1058
|
+
return data;
|
|
1059
|
+
}
|
|
1060
|
+
setStatus(status) {
|
|
1061
|
+
const data = this.read();
|
|
1062
|
+
if (status === "passed" && data.items.some((i) => i.state === "open")) {
|
|
1063
|
+
throw new Error("\u5B58\u5728\u672A\u5904\u7406\u7684\u8BC4\u5BA1\u9879(open),\u4E0D\u80FD\u901A\u8FC7");
|
|
756
1064
|
}
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
1065
|
+
data.status = status;
|
|
1066
|
+
this.write(data);
|
|
1067
|
+
return data;
|
|
1068
|
+
}
|
|
1069
|
+
docs() {
|
|
1070
|
+
try {
|
|
1071
|
+
return fs7.readdirSync(this.root).filter((f) => /\.(md|markdown)$/i.test(f) && fs7.statSync(path6.join(this.root, f)).isFile()).sort();
|
|
1072
|
+
} catch {
|
|
1073
|
+
return [];
|
|
761
1074
|
}
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
res.write(": connected\n\n");
|
|
765
|
-
const unsub = runner.subscribe((ev) => res.write(`data: ${JSON.stringify(ev)}
|
|
1075
|
+
}
|
|
1076
|
+
};
|
|
766
1077
|
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
const
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
1078
|
+
// src/plugins/review/index.ts
|
|
1079
|
+
var apply4 = {
|
|
1080
|
+
inject: ["server", "dashboard", "reload"],
|
|
1081
|
+
apply(ctx, config) {
|
|
1082
|
+
const root = config.root;
|
|
1083
|
+
ctx.inject(["server"], () => {
|
|
1084
|
+
if (!ctx.server?.route) return;
|
|
1085
|
+
ctx.dashboard.register({ mode: "review", label: "\u6587\u6863\u8BC4\u5BA1", icon: "\u2705", description: "review.yaml \u9010\u9879\u5BF9\u9F50" });
|
|
1086
|
+
const reviewStore = new ReviewStore(root, () => {
|
|
1087
|
+
ctx.reload.broadcast("files");
|
|
1088
|
+
});
|
|
1089
|
+
ctx.server.route("/__review", async (_req, res) => {
|
|
1090
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
|
|
1091
|
+
res.end(JSON.stringify(reviewStore.read()));
|
|
1092
|
+
});
|
|
1093
|
+
ctx.server.route("/__review/item", async (req, res) => {
|
|
1094
|
+
if (req.headers["x-stop-token"] !== ctx.server.stopToken) {
|
|
775
1095
|
res.writeHead(403);
|
|
776
1096
|
res.end("forbidden");
|
|
777
1097
|
return;
|
|
778
1098
|
}
|
|
779
|
-
const body = await readBody(req);
|
|
780
|
-
let recipe;
|
|
781
1099
|
try {
|
|
782
|
-
|
|
1100
|
+
const body = JSON.parse(await readBody2(req) || "{}");
|
|
1101
|
+
const data = reviewStore.updateItem(body.id, { answer: body.answer, state: body.state });
|
|
1102
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
|
|
1103
|
+
res.end(JSON.stringify(data));
|
|
783
1104
|
} catch (e) {
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
const act = justAction[1];
|
|
787
|
-
if (act === "start" || act === "restart") {
|
|
788
|
-
const target = recipe ?? runner.info().recipe;
|
|
789
|
-
if (!target) {
|
|
790
|
-
res.writeHead(400);
|
|
791
|
-
res.end('{"error":"no recipe"}');
|
|
792
|
-
return;
|
|
793
|
-
}
|
|
794
|
-
const recipes = await runner.recipes();
|
|
795
|
-
if (!recipes.some((r) => r.name === target)) {
|
|
796
|
-
res.writeHead(403);
|
|
797
|
-
res.end('{"error":"unknown recipe"}');
|
|
798
|
-
return;
|
|
799
|
-
}
|
|
800
|
-
runner.start(target);
|
|
801
|
-
} else {
|
|
802
|
-
runner.stop();
|
|
1105
|
+
res.writeHead(400, { "Content-Type": "application/json; charset=utf-8" });
|
|
1106
|
+
res.end(JSON.stringify({ error: e instanceof Error ? e.message : "unknown error" }));
|
|
803
1107
|
}
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
let handled = false;
|
|
810
|
-
for (const plugin of allBuiltins()) {
|
|
811
|
-
if (!plugin.apiRoutes) continue;
|
|
812
|
-
for (const [route, handler2] of Object.entries(plugin.apiRoutes)) {
|
|
813
|
-
if (url === route) {
|
|
814
|
-
handled = true;
|
|
815
|
-
handler2(req, res, ROOT);
|
|
1108
|
+
});
|
|
1109
|
+
ctx.server.route("/__review/status", async (req, res) => {
|
|
1110
|
+
if (req.headers["x-stop-token"] !== ctx.server.stopToken) {
|
|
1111
|
+
res.writeHead(403);
|
|
1112
|
+
res.end("forbidden");
|
|
816
1113
|
return;
|
|
817
1114
|
}
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
res.writeHead(403);
|
|
833
|
-
return res.end("Forbidden");
|
|
834
|
-
}
|
|
835
|
-
return serveFile(fp2, res, false);
|
|
836
|
-
}
|
|
837
|
-
const fp = path6.join(ROOT, decodeURIComponent(url));
|
|
838
|
-
if (fp !== ROOT && fp.indexOf(ROOT + path6.sep) !== 0) {
|
|
839
|
-
res.writeHead(403);
|
|
840
|
-
return res.end("Forbidden");
|
|
841
|
-
}
|
|
842
|
-
return serveFile(fp, res, true);
|
|
843
|
-
}
|
|
844
|
-
let server;
|
|
845
|
-
function start(port) {
|
|
846
|
-
server = http.createServer(handler);
|
|
847
|
-
server.on("error", (err) => {
|
|
848
|
-
if (err.code === "EADDRINUSE") {
|
|
849
|
-
console.log(`[zdashboard] port ${port} busy, trying ${port + 1}`);
|
|
850
|
-
start(port + 1);
|
|
851
|
-
} else throw err;
|
|
852
|
-
});
|
|
853
|
-
server.listen(port, () => {
|
|
854
|
-
const u = `http://localhost:${port}`;
|
|
855
|
-
console.log(`[zdashboard] v${VERSION} dashboard -> ${u}`);
|
|
856
|
-
console.log(`[zdashboard] project -> ${ROOT}`);
|
|
857
|
-
console.log(`[zdashboard] mode -> ${MODE ?? "(auto)"}`);
|
|
858
|
-
console.log(`[zdashboard] detect -> openspec:${det2.hasOpenspec} docs:${det2.hasDocs} just:${det2.hasJust} bugs:${det2.hasBugs}`);
|
|
859
|
-
if (OPEN) exec(process.platform === "darwin" ? `open ${u}` : `start ${u}`);
|
|
1115
|
+
try {
|
|
1116
|
+
const body = JSON.parse(await readBody2(req) || "{}");
|
|
1117
|
+
const data = reviewStore.setStatus(body.status);
|
|
1118
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
|
|
1119
|
+
res.end(JSON.stringify(data));
|
|
1120
|
+
} catch (e) {
|
|
1121
|
+
res.writeHead(400, { "Content-Type": "application/json; charset=utf-8" });
|
|
1122
|
+
res.end(JSON.stringify({ error: e instanceof Error ? e.message : "unknown error" }));
|
|
1123
|
+
}
|
|
1124
|
+
});
|
|
1125
|
+
ctx.server.route("/__docs", async (_req, res) => {
|
|
1126
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
|
|
1127
|
+
res.end(JSON.stringify(reviewStore.docs()));
|
|
1128
|
+
});
|
|
860
1129
|
});
|
|
861
1130
|
}
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
broadcast("files");
|
|
869
|
-
console.log(`[zdashboard] change -> reload + refresh tree (${clients.size} client${clients.size === 1 ? "" : "s"})`);
|
|
870
|
-
}, 150);
|
|
1131
|
+
};
|
|
1132
|
+
async function readBody2(req) {
|
|
1133
|
+
return new Promise((resolve) => {
|
|
1134
|
+
let data = "";
|
|
1135
|
+
req.on("data", (c) => {
|
|
1136
|
+
data += c;
|
|
871
1137
|
});
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
}
|
|
875
|
-
start(PORT0);
|
|
1138
|
+
req.on("end", () => resolve(data));
|
|
1139
|
+
});
|
|
876
1140
|
}
|
|
1141
|
+
|
|
1142
|
+
// src/plugins/apply/scan.ts
|
|
1143
|
+
import fs8 from "fs";
|
|
1144
|
+
import path7 from "path";
|
|
877
1145
|
function countTasks(md) {
|
|
878
1146
|
const all = (md.match(/^\s*-\s*\[[ xX]\]\s*/gm) || []).length;
|
|
879
1147
|
const done = (md.match(/^\s*-\s*\[[xX]\]\s*/gm) || []).length;
|
|
@@ -881,73 +1149,389 @@ function countTasks(md) {
|
|
|
881
1149
|
}
|
|
882
1150
|
function readText(p) {
|
|
883
1151
|
try {
|
|
884
|
-
return
|
|
1152
|
+
return fs8.readFileSync(p, "utf8");
|
|
885
1153
|
} catch {
|
|
886
1154
|
return "";
|
|
887
1155
|
}
|
|
888
1156
|
}
|
|
889
|
-
function scanApplyChanges(
|
|
890
|
-
const changesDir =
|
|
891
|
-
if (!
|
|
1157
|
+
function scanApplyChanges(root) {
|
|
1158
|
+
const changesDir = path7.join(root, "openspec", "changes");
|
|
1159
|
+
if (!fs8.existsSync(changesDir)) return [];
|
|
892
1160
|
const out = [];
|
|
893
|
-
for (const ent of
|
|
1161
|
+
for (const ent of fs8.readdirSync(changesDir, { withFileTypes: true })) {
|
|
894
1162
|
if (!ent.isDirectory() || ent.name.startsWith(".") || ent.name === "archive") continue;
|
|
895
|
-
const dir =
|
|
896
|
-
const tasks = readText(
|
|
1163
|
+
const dir = path7.join(changesDir, ent.name);
|
|
1164
|
+
const tasks = readText(path7.join(dir, "tasks.md"));
|
|
897
1165
|
const { total, done } = countTasks(tasks);
|
|
898
|
-
out.push({
|
|
1166
|
+
out.push({
|
|
1167
|
+
name: ent.name,
|
|
1168
|
+
path: `openspec/changes/${ent.name}`,
|
|
1169
|
+
total,
|
|
1170
|
+
done,
|
|
1171
|
+
hasProposal: fs8.existsSync(path7.join(dir, "proposal.md")),
|
|
1172
|
+
hasDesign: fs8.existsSync(path7.join(dir, "design.md"))
|
|
1173
|
+
});
|
|
899
1174
|
}
|
|
900
1175
|
out.sort((a, b) => a.name.localeCompare(b.name));
|
|
901
1176
|
return out;
|
|
902
1177
|
}
|
|
903
|
-
function readApplyChange(
|
|
904
|
-
const dir =
|
|
905
|
-
const proposal = readText(
|
|
906
|
-
const design = readText(
|
|
907
|
-
const tasks = readText(
|
|
1178
|
+
function readApplyChange(root, name) {
|
|
1179
|
+
const dir = path7.join(root, "openspec", "changes", name);
|
|
1180
|
+
const proposal = readText(path7.join(dir, "proposal.md"));
|
|
1181
|
+
const design = readText(path7.join(dir, "design.md"));
|
|
1182
|
+
const tasks = readText(path7.join(dir, "tasks.md"));
|
|
908
1183
|
const { total, done } = countTasks(tasks);
|
|
909
|
-
return {
|
|
1184
|
+
return {
|
|
1185
|
+
name,
|
|
1186
|
+
path: `openspec/changes/${name}`,
|
|
1187
|
+
total,
|
|
1188
|
+
done,
|
|
1189
|
+
hasProposal: !!proposal,
|
|
1190
|
+
hasDesign: !!design,
|
|
1191
|
+
proposal,
|
|
1192
|
+
design,
|
|
1193
|
+
tasks
|
|
1194
|
+
};
|
|
910
1195
|
}
|
|
911
1196
|
|
|
912
|
-
// src/
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
1197
|
+
// src/plugins/apply/index.ts
|
|
1198
|
+
var apply5 = {
|
|
1199
|
+
inject: ["server", "dashboard"],
|
|
1200
|
+
apply(ctx, config) {
|
|
1201
|
+
const root = config.root;
|
|
1202
|
+
ctx.inject(["server"], () => {
|
|
1203
|
+
if (!ctx.server?.route) return;
|
|
1204
|
+
ctx.dashboard.register({ mode: "apply", label: "\u6267\u884C\u8FDB\u5EA6", icon: "\u2699\uFE0F", description: "OpenSpec change \u4EFB\u52A1\u8FDB\u5EA6" });
|
|
1205
|
+
ctx.server.route("/__apply", async (_req, res) => {
|
|
1206
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
|
|
1207
|
+
res.end(JSON.stringify(scanApplyChanges(root)));
|
|
1208
|
+
});
|
|
1209
|
+
ctx.server.route("/__apply/change", async (req, res) => {
|
|
1210
|
+
const url = new URL(req.url || "", "http://x");
|
|
1211
|
+
const name = url.searchParams.get("name");
|
|
1212
|
+
if (!name) {
|
|
1213
|
+
res.writeHead(400);
|
|
1214
|
+
res.end(JSON.stringify({ error: "missing name" }));
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
if (name.includes("..") || name.includes("/") || name.includes("\\")) {
|
|
1218
|
+
res.writeHead(400);
|
|
1219
|
+
res.end(JSON.stringify({ error: "invalid name" }));
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1222
|
+
try {
|
|
1223
|
+
const data = readApplyChange(root, name);
|
|
1224
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
|
|
1225
|
+
res.end(JSON.stringify(data));
|
|
1226
|
+
} catch (e) {
|
|
1227
|
+
res.writeHead(400);
|
|
1228
|
+
res.end(JSON.stringify({ error: e instanceof Error ? e.message : "unknown error" }));
|
|
1229
|
+
}
|
|
1230
|
+
});
|
|
920
1231
|
});
|
|
921
|
-
|
|
922
|
-
|
|
1232
|
+
}
|
|
1233
|
+
};
|
|
1234
|
+
|
|
1235
|
+
// src/server/design-assets.ts
|
|
1236
|
+
import fs9 from "fs";
|
|
1237
|
+
import path8 from "path";
|
|
1238
|
+
var PAGE_EXTS = [".html", ".htm"];
|
|
1239
|
+
var ICON_EXTS = [".svg", ".png", ".ico", ".jpg", ".jpeg", ".gif", ".webp"];
|
|
1240
|
+
var VIDEO_EXTS = [".mp4", ".webm", ".mov", ".ogg", ".ogv"];
|
|
1241
|
+
var AUDIO_EXTS = [".mp3", ".wav", ".flac", ".aac", ".m4a"];
|
|
1242
|
+
var CODE_EXTS = [".js", ".mjs", ".ts", ".tsx", ".jsx", ".css", ".json", ".txt", ".xml", ".yml", ".yaml", ".sh", ".md"];
|
|
1243
|
+
var FONT_EXTS = [".woff", ".woff2", ".ttf", ".otf"];
|
|
1244
|
+
var TOKEN_RE = /token|theme|design|color|palette|typograph/i;
|
|
1245
|
+
function categorize(rel, ext) {
|
|
1246
|
+
if (rel.indexOf("components/") === 0) return "component";
|
|
1247
|
+
if (VIDEO_EXTS.includes(ext)) return "video";
|
|
1248
|
+
if (AUDIO_EXTS.includes(ext)) return "audio";
|
|
1249
|
+
if (ext === ".pdf") return "pdf";
|
|
1250
|
+
if (ext === ".md") return "md";
|
|
1251
|
+
if (FONT_EXTS.includes(ext)) return "font";
|
|
1252
|
+
if (ICON_EXTS.includes(ext)) return "icon";
|
|
1253
|
+
if (PAGE_EXTS.includes(ext)) return "page";
|
|
1254
|
+
if (CODE_EXTS.includes(ext)) {
|
|
1255
|
+
if (TOKEN_RE.test(rel) && (ext === ".css" || ext === ".json")) return "token";
|
|
1256
|
+
return null;
|
|
1257
|
+
}
|
|
1258
|
+
return "other";
|
|
923
1259
|
}
|
|
924
|
-
|
|
925
|
-
const
|
|
926
|
-
const
|
|
927
|
-
const
|
|
928
|
-
const
|
|
929
|
-
|
|
1260
|
+
function scanAssets(root) {
|
|
1261
|
+
const out = {};
|
|
1262
|
+
const keys = ["page", "component", "icon", "token", "md", "video", "audio", "pdf", "font", "other"];
|
|
1263
|
+
for (const k of keys) out[k] = [];
|
|
1264
|
+
const SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "coverage", ".next", ".cache"]);
|
|
1265
|
+
function walk(dir, rel) {
|
|
1266
|
+
let ents;
|
|
1267
|
+
try {
|
|
1268
|
+
ents = fs9.readdirSync(dir, { withFileTypes: true });
|
|
1269
|
+
} catch {
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1272
|
+
for (const ent of ents) {
|
|
1273
|
+
if (ent.name.startsWith(".") || SKIP.has(ent.name)) continue;
|
|
1274
|
+
const r = rel ? `${rel}/${ent.name}` : ent.name;
|
|
1275
|
+
if (ent.isDirectory()) {
|
|
1276
|
+
walk(path8.join(dir, ent.name), r);
|
|
1277
|
+
continue;
|
|
1278
|
+
}
|
|
1279
|
+
const ext = path8.extname(ent.name).toLowerCase();
|
|
1280
|
+
const t = categorize(r, ext);
|
|
1281
|
+
if (t) out[t].push({ path: r, name: ent.name, ext, type: t });
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
walk(root, "");
|
|
1285
|
+
return out;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
// src/plugins/design/index.ts
|
|
1289
|
+
var apply6 = {
|
|
1290
|
+
inject: ["server", "dashboard"],
|
|
1291
|
+
apply(ctx, config) {
|
|
1292
|
+
const root = config.root;
|
|
1293
|
+
ctx.inject(["server"], () => {
|
|
1294
|
+
if (!ctx.server?.route) return;
|
|
1295
|
+
ctx.dashboard.register({ mode: "design", label: "\u8BBE\u8BA1\u8D44\u4EA7", icon: "\u{1F3A8}", description: "\u9875\u9762/\u7EC4\u4EF6/\u56FE\u6807/Token \u5206\u7C7B\u9884\u89C8" });
|
|
1296
|
+
ctx.server.route("/__design/assets", async (_req, res) => {
|
|
1297
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
|
|
1298
|
+
res.end(JSON.stringify(scanAssets(root)));
|
|
1299
|
+
});
|
|
1300
|
+
});
|
|
1301
|
+
}
|
|
1302
|
+
};
|
|
1303
|
+
|
|
1304
|
+
// src/plugins/view/index.ts
|
|
1305
|
+
var apply7 = {
|
|
1306
|
+
inject: ["dashboard"],
|
|
1307
|
+
apply(ctx) {
|
|
1308
|
+
ctx.dashboard.register({ mode: "view", label: "\u9879\u76EE\u6D4F\u89C8", icon: "\u{1F441}\uFE0F", description: "openspec / docs / \u6587\u6863\u9884\u89C8" });
|
|
1309
|
+
}
|
|
1310
|
+
};
|
|
1311
|
+
|
|
1312
|
+
// src/plugins/stats/index.ts
|
|
1313
|
+
import fs10 from "fs";
|
|
1314
|
+
import path9 from "path";
|
|
1315
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", ".cache"]);
|
|
1316
|
+
function scan(root) {
|
|
1317
|
+
const stats = {
|
|
1318
|
+
root: path9.basename(root),
|
|
1319
|
+
files: 0,
|
|
1320
|
+
dirs: 0,
|
|
1321
|
+
totalSize: 0,
|
|
1322
|
+
byExt: [],
|
|
1323
|
+
markdown: 0,
|
|
1324
|
+
openspec: { active: 0, archived: 0 },
|
|
1325
|
+
hasJust: fs10.existsSync(path9.join(root, "justfile"))
|
|
1326
|
+
};
|
|
1327
|
+
const extMap = /* @__PURE__ */ new Map();
|
|
1328
|
+
const walk = (dir) => {
|
|
1329
|
+
let ents;
|
|
1330
|
+
try {
|
|
1331
|
+
ents = fs10.readdirSync(dir, { withFileTypes: true });
|
|
1332
|
+
} catch {
|
|
1333
|
+
return;
|
|
1334
|
+
}
|
|
1335
|
+
for (const ent of ents) {
|
|
1336
|
+
if (ent.name.startsWith(".") || SKIP_DIRS.has(ent.name)) continue;
|
|
1337
|
+
const abs = path9.join(dir, ent.name);
|
|
1338
|
+
if (ent.isDirectory()) {
|
|
1339
|
+
stats.dirs++;
|
|
1340
|
+
walk(abs);
|
|
1341
|
+
continue;
|
|
1342
|
+
}
|
|
1343
|
+
stats.files++;
|
|
1344
|
+
try {
|
|
1345
|
+
stats.totalSize += fs10.statSync(abs).size;
|
|
1346
|
+
} catch {
|
|
1347
|
+
}
|
|
1348
|
+
const ext = path9.extname(ent.name).toLowerCase() || "(\u65E0\u6269\u5C55\u540D)";
|
|
1349
|
+
extMap.set(ext, (extMap.get(ext) ?? 0) + 1);
|
|
1350
|
+
if (ext === ".md" || ext === ".markdown") stats.markdown++;
|
|
1351
|
+
}
|
|
1352
|
+
};
|
|
1353
|
+
walk(root);
|
|
1354
|
+
const changesDir = path9.join(root, "openspec", "changes");
|
|
1355
|
+
if (fs10.existsSync(changesDir)) {
|
|
1356
|
+
for (const ent of fs10.readdirSync(changesDir, { withFileTypes: true })) {
|
|
1357
|
+
if (!ent.isDirectory() || ent.name.startsWith(".")) continue;
|
|
1358
|
+
if (ent.name === "archive") {
|
|
1359
|
+
try {
|
|
1360
|
+
stats.openspec.archived = fs10.readdirSync(path9.join(changesDir, "archive"), { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).length;
|
|
1361
|
+
} catch {
|
|
1362
|
+
}
|
|
1363
|
+
} else stats.openspec.active++;
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
stats.byExt = Array.from(extMap.entries()).map(([ext, count]) => ({ ext, count })).sort((a, b) => b.count - a.count).slice(0, 10);
|
|
1367
|
+
return stats;
|
|
930
1368
|
}
|
|
1369
|
+
var apply8 = {
|
|
1370
|
+
inject: ["server", "dashboard"],
|
|
1371
|
+
apply(ctx, config) {
|
|
1372
|
+
ctx.inject(["server", "dashboard"], () => {
|
|
1373
|
+
const server = ctx.server;
|
|
1374
|
+
const dashboard = ctx.dashboard;
|
|
1375
|
+
if (!server?.route || !dashboard?.register) return;
|
|
1376
|
+
dashboard.register({
|
|
1377
|
+
mode: "stats",
|
|
1378
|
+
label: "\u9879\u76EE\u7EDF\u8BA1",
|
|
1379
|
+
icon: "\u{1F4CA}",
|
|
1380
|
+
description: "\u9879\u76EE\u6587\u4EF6\u7EDF\u8BA1 \xB7 \u626B\u63CF\u751F\u6210"
|
|
1381
|
+
});
|
|
1382
|
+
server.route("/__stats/data", (_req, res) => {
|
|
1383
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
|
|
1384
|
+
res.end(JSON.stringify(scan(config.root)));
|
|
1385
|
+
});
|
|
1386
|
+
});
|
|
1387
|
+
}
|
|
1388
|
+
};
|
|
931
1389
|
|
|
932
1390
|
// src/cli.ts
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
1391
|
+
var __dirname2 = path10.dirname(fileURLToPath2(import.meta.url));
|
|
1392
|
+
function parseArgs() {
|
|
1393
|
+
const args = process.argv.slice(2);
|
|
1394
|
+
const opts = {};
|
|
1395
|
+
for (let i = 0; i < args.length; i++) {
|
|
1396
|
+
const a = args[i];
|
|
1397
|
+
if (a.startsWith("--")) {
|
|
1398
|
+
const key = a.slice(2);
|
|
1399
|
+
const next = args[i + 1];
|
|
1400
|
+
if (next && !next.startsWith("--")) {
|
|
1401
|
+
opts[key] = next;
|
|
1402
|
+
i++;
|
|
1403
|
+
} else {
|
|
1404
|
+
opts[key] = true;
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
return {
|
|
1409
|
+
dir: typeof opts.dir === "string" ? opts.dir : process.cwd(),
|
|
1410
|
+
port: typeof opts.port === "string" ? Number(opts.port) : 4190,
|
|
1411
|
+
open: !!opts.open,
|
|
1412
|
+
page: typeof opts.page === "string" ? opts.page : null,
|
|
1413
|
+
restart: !!opts.restart,
|
|
1414
|
+
plugins: typeof opts.plugins === "string" ? opts.plugins : null
|
|
1415
|
+
};
|
|
1416
|
+
}
|
|
1417
|
+
async function pathExists(p) {
|
|
1418
|
+
try {
|
|
1419
|
+
await access(p);
|
|
1420
|
+
return true;
|
|
1421
|
+
} catch {
|
|
1422
|
+
return false;
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
async function loadExternal(ctx, dir, root) {
|
|
1426
|
+
if (!dir) return;
|
|
1427
|
+
let tsxLoaded = false;
|
|
1428
|
+
try {
|
|
1429
|
+
const { register } = await import("tsx/esm/api");
|
|
1430
|
+
register();
|
|
1431
|
+
tsxLoaded = true;
|
|
1432
|
+
} catch {
|
|
1433
|
+
}
|
|
1434
|
+
try {
|
|
1435
|
+
const entries = await pathExists(dir) ? await readdir(dir, { withFileTypes: true }) : [];
|
|
1436
|
+
for (const ent of entries) {
|
|
1437
|
+
if (!ent.isDirectory()) continue;
|
|
1438
|
+
const candidates = tsxLoaded ? ["index.ts", "index.js", "index.mjs"] : ["index.js", "index.mjs"];
|
|
1439
|
+
for (const name of candidates) {
|
|
1440
|
+
const p = path10.join(dir, ent.name, name);
|
|
1441
|
+
if (await pathExists(p)) {
|
|
1442
|
+
try {
|
|
1443
|
+
const mod = await import(p);
|
|
1444
|
+
const plugin = mod.default ?? mod;
|
|
1445
|
+
if (plugin?.apply) {
|
|
1446
|
+
try {
|
|
1447
|
+
await ctx.plugin(plugin, { root });
|
|
1448
|
+
} catch (e) {
|
|
1449
|
+
console.error(`[zdashboard] failed to apply plugin ${ent.name}:`, e);
|
|
1450
|
+
break;
|
|
1451
|
+
}
|
|
1452
|
+
const m = ctx.dashboard.get(ent.name);
|
|
1453
|
+
if (m && m.mode === ent.name) {
|
|
1454
|
+
const patch = { external: true };
|
|
1455
|
+
const webDir = path10.join(dir, ent.name, "web");
|
|
1456
|
+
if (await pathExists(path10.join(webDir, "index.html"))) {
|
|
1457
|
+
ctx.server.static(`${PLUGIN_STATIC_PREFIX}${ent.name}/`, webDir);
|
|
1458
|
+
if (!m.viewerUrl) patch.viewerUrl = `${PLUGIN_STATIC_PREFIX}${ent.name}/`;
|
|
1459
|
+
}
|
|
1460
|
+
ctx.dashboard.register({ ...m, ...patch });
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
} catch (e) {
|
|
1464
|
+
console.error(`[zdashboard] failed to load plugin ${ent.name}:`, e);
|
|
1465
|
+
}
|
|
1466
|
+
break;
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
} catch {
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
async function main() {
|
|
1474
|
+
const args = parseArgs();
|
|
1475
|
+
const root = path10.resolve(args.dir);
|
|
1476
|
+
const existing = await findReusable(root);
|
|
1477
|
+
if (existing && !args.restart) {
|
|
1478
|
+
const u = `http://localhost:${existing.port}` + (args.page ? `#${args.page}` : "");
|
|
1479
|
+
if (args.open) openUrl(u);
|
|
1480
|
+
console.log(`[zdashboard] \u5DF2\u590D\u7528\u5B9E\u4F8B ${u}\uFF08--restart \u53EF\u5F3A\u5236\u91CD\u5F00\uFF09`);
|
|
1481
|
+
await new Promise((resolve) => process.stdout.write("", () => resolve()));
|
|
1482
|
+
process.exit(0);
|
|
1483
|
+
}
|
|
1484
|
+
if (existing && args.restart) {
|
|
1485
|
+
console.log(`[zdashboard] --restart\uFF1A\u505C\u6B62\u65E7\u5B9E\u4F8B pid=${existing.pid}`);
|
|
1486
|
+
await stopInstance(existing);
|
|
1487
|
+
}
|
|
1488
|
+
const appDir = path10.resolve(__dirname2, "web");
|
|
1489
|
+
const det = await detect(root);
|
|
1490
|
+
const ctx = new Context();
|
|
1491
|
+
ctx.plugin(ServerService, {
|
|
1492
|
+
root,
|
|
1493
|
+
appDir,
|
|
1494
|
+
port: args.port,
|
|
1495
|
+
open: args.open,
|
|
1496
|
+
detect: det,
|
|
1497
|
+
page: args.page,
|
|
1498
|
+
onListen: (port) => writeRecord(root, port)
|
|
1499
|
+
});
|
|
1500
|
+
ctx.plugin(ReloadService, { root });
|
|
1501
|
+
ctx.plugin(apply, { root });
|
|
1502
|
+
ctx.plugin(DashboardService);
|
|
1503
|
+
const plugins = [
|
|
1504
|
+
{ name: "stats", apply: apply8 },
|
|
1505
|
+
{ name: "just", apply: apply2 },
|
|
1506
|
+
{ name: "bugs", apply: apply3 },
|
|
1507
|
+
{ name: "review", apply: apply4 },
|
|
1508
|
+
{ name: "apply", apply: apply5 },
|
|
1509
|
+
{ name: "design", apply: apply6 },
|
|
1510
|
+
{ name: "view", apply: apply7 }
|
|
1511
|
+
];
|
|
1512
|
+
for (const p of plugins) {
|
|
1513
|
+
try {
|
|
1514
|
+
ctx.plugin(p.apply, { root });
|
|
1515
|
+
} catch (e) {
|
|
1516
|
+
console.error(`[zdashboard] plugin ${p.name} failed:`, e);
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
if (args.plugins) {
|
|
1520
|
+
await loadExternal(ctx, path10.resolve(args.plugins), root);
|
|
1521
|
+
}
|
|
1522
|
+
const shutdown = () => {
|
|
1523
|
+
try {
|
|
1524
|
+
clearRecord(root);
|
|
1525
|
+
} catch {
|
|
1526
|
+
}
|
|
1527
|
+
process.exit(0);
|
|
1528
|
+
};
|
|
1529
|
+
process.on("SIGTERM", shutdown);
|
|
1530
|
+
process.on("SIGINT", shutdown);
|
|
1531
|
+
return ctx;
|
|
1532
|
+
}
|
|
1533
|
+
await main().catch((e) => {
|
|
1534
|
+
console.error(e);
|
|
1535
|
+
process.exit(1);
|
|
952
1536
|
});
|
|
953
1537
|
//# sourceMappingURL=cli.js.map
|