zdashboard 1.0.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 +71 -0
- package/dist/cli.js +650 -0
- package/dist/cli.js.map +1 -0
- package/dist/web/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/dist/web/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/web/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/web/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/web/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/web/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/dist/web/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/web/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/dist/web/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/web/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/web/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/web/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/dist/web/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/web/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/dist/web/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/web/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/dist/web/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/web/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/web/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/dist/web/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/web/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/web/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/web/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/web/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/dist/web/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/dist/web/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/web/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/web/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/web/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/dist/web/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/web/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/web/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/web/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/dist/web/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/web/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/dist/web/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/web/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/web/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/web/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/dist/web/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/web/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/dist/web/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/web/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/web/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/web/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/web/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/web/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/dist/web/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/web/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/dist/web/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/web/assets/index-B46znErN.js +436 -0
- package/dist/web/assets/index-B7v5E7v0.css +10 -0
- package/dist/web/favicon.svg +7 -0
- package/dist/web/index.html +14 -0
- package/package.json +58 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,650 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/server/index.ts
|
|
4
|
+
import http from "http";
|
|
5
|
+
import fs4 from "fs";
|
|
6
|
+
import path4 from "path";
|
|
7
|
+
import crypto from "crypto";
|
|
8
|
+
import { exec } from "child_process";
|
|
9
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
10
|
+
|
|
11
|
+
// src/server/spec-scan.ts
|
|
12
|
+
import fs from "fs";
|
|
13
|
+
import path from "path";
|
|
14
|
+
function walkFiles(absDir, relDir, depth = 0) {
|
|
15
|
+
if (depth > 4) return [];
|
|
16
|
+
let ents;
|
|
17
|
+
try {
|
|
18
|
+
ents = fs.readdirSync(absDir, { withFileTypes: true });
|
|
19
|
+
} catch {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
const nodes = [];
|
|
23
|
+
for (const ent of ents) {
|
|
24
|
+
if (ent.name.startsWith(".") || ent.name === "node_modules") continue;
|
|
25
|
+
const rel = relDir ? `${relDir}/${ent.name}` : ent.name;
|
|
26
|
+
if (ent.isDirectory()) {
|
|
27
|
+
nodes.push({ name: ent.name, kind: "dir", children: walkFiles(path.join(absDir, ent.name), rel, depth + 1) });
|
|
28
|
+
} else {
|
|
29
|
+
nodes.push({ name: ent.name, kind: "file", path: rel });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
nodes.sort((a, b) => a.kind === b.kind ? a.name.localeCompare(b.name) : a.kind === "dir" ? -1 : 1);
|
|
33
|
+
return nodes;
|
|
34
|
+
}
|
|
35
|
+
function scanTree(root2, hasOpenspec, hasDocs) {
|
|
36
|
+
const tree = [];
|
|
37
|
+
if (hasOpenspec && fs.existsSync(path.join(root2, "openspec", "changes"))) {
|
|
38
|
+
const changesDir = path.join(root2, "openspec", "changes");
|
|
39
|
+
const active = [];
|
|
40
|
+
const archived = [];
|
|
41
|
+
for (const ent of fs.readdirSync(changesDir, { withFileTypes: true })) {
|
|
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: `\u5F52\u6863 (${archived.length})`, 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: "\u80FD\u529B Specs", kind: "dir", children: specs });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (hasDocs && fs.existsSync(path.join(root2, "docs"))) {
|
|
63
|
+
const docs = walkFiles(path.join(root2, "docs"), "docs");
|
|
64
|
+
if (docs.length) tree.push({ name: "docs", kind: "dir", children: docs });
|
|
65
|
+
}
|
|
66
|
+
const skip = /* @__PURE__ */ new Set(["openspec", "docs", "node_modules", ".git", "dist", "test-server"]);
|
|
67
|
+
const etc = [];
|
|
68
|
+
try {
|
|
69
|
+
for (const ent of fs.readdirSync(root2, { withFileTypes: true })) {
|
|
70
|
+
if (ent.name.startsWith(".") || skip.has(ent.name)) continue;
|
|
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
|
+
}
|
|
74
|
+
} catch {
|
|
75
|
+
}
|
|
76
|
+
etc.sort((a, b) => a.name.localeCompare(b.name));
|
|
77
|
+
if (etc.length) tree.push({ name: `\u5176\u4ED6 (${etc.length})`, kind: "dir", children: etc });
|
|
78
|
+
return tree;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// src/server/just-runner.ts
|
|
82
|
+
import { spawn, execFile } from "child_process";
|
|
83
|
+
var MAX_BUFFER = 1e3;
|
|
84
|
+
var JustRunner = class {
|
|
85
|
+
cwd;
|
|
86
|
+
child = null;
|
|
87
|
+
recipe = null;
|
|
88
|
+
state = "idle";
|
|
89
|
+
code = null;
|
|
90
|
+
buffer = [];
|
|
91
|
+
pending = "";
|
|
92
|
+
// 行缓冲:块缓冲输出(如 maven)的 chunk 会在行中间断开,攒到 \n 才切行
|
|
93
|
+
clients = /* @__PURE__ */ new Set();
|
|
94
|
+
recipesCache = null;
|
|
95
|
+
constructor(cwd) {
|
|
96
|
+
this.cwd = cwd;
|
|
97
|
+
}
|
|
98
|
+
recipes() {
|
|
99
|
+
if (this.recipesCache) return Promise.resolve(this.recipesCache);
|
|
100
|
+
return new Promise((resolve) => {
|
|
101
|
+
execFile("just", ["--list", "--unsorted"], { cwd: this.cwd, maxBuffer: 1 << 20, timeout: 8e3 }, (err, stdout) => {
|
|
102
|
+
if (err) {
|
|
103
|
+
resolve([]);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const out = [];
|
|
107
|
+
const seen = /* @__PURE__ */ new Set();
|
|
108
|
+
for (const line of stdout.split(/\r?\n/).slice(1)) {
|
|
109
|
+
const trimmed = line.trim();
|
|
110
|
+
if (!trimmed) continue;
|
|
111
|
+
const hashIdx = trimmed.indexOf("#");
|
|
112
|
+
const sig = (hashIdx >= 0 ? trimmed.slice(0, hashIdx) : trimmed).trim();
|
|
113
|
+
if (!sig) continue;
|
|
114
|
+
const name = sig.split(/\s+/)[0];
|
|
115
|
+
if (seen.has(name)) continue;
|
|
116
|
+
seen.add(name);
|
|
117
|
+
out.push({ name, description: hashIdx >= 0 ? trimmed.slice(hashIdx + 1).trim() : "" });
|
|
118
|
+
}
|
|
119
|
+
this.recipesCache = out;
|
|
120
|
+
resolve(out);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
subscribe(fn) {
|
|
125
|
+
this.clients.add(fn);
|
|
126
|
+
for (const text of this.buffer) fn({ type: "log", text });
|
|
127
|
+
fn({ type: "state", state: this.state, recipe: this.recipe, code: this.code });
|
|
128
|
+
return () => this.clients.delete(fn);
|
|
129
|
+
}
|
|
130
|
+
emit(ev) {
|
|
131
|
+
for (const fn of this.clients) fn(ev);
|
|
132
|
+
}
|
|
133
|
+
info() {
|
|
134
|
+
return { state: this.state, recipe: this.recipe, code: this.code };
|
|
135
|
+
}
|
|
136
|
+
/** 启动 recipe(调用方须先用 recipes() 校验名字);自动停旧进程 */
|
|
137
|
+
start(recipe) {
|
|
138
|
+
this.killChild();
|
|
139
|
+
this.recipe = recipe;
|
|
140
|
+
this.code = null;
|
|
141
|
+
this.state = "running";
|
|
142
|
+
this.buffer = [];
|
|
143
|
+
this.pending = "";
|
|
144
|
+
this.emit({ type: "clear" });
|
|
145
|
+
this.emit({ type: "state", state: "running", recipe, code: null });
|
|
146
|
+
const child = spawn("just", [recipe], {
|
|
147
|
+
cwd: this.cwd,
|
|
148
|
+
shell: true,
|
|
149
|
+
env: {
|
|
150
|
+
...process.env,
|
|
151
|
+
FORCE_COLOR: "1",
|
|
152
|
+
// node 生态(chalk 等)
|
|
153
|
+
// maven 检测非 tty 会关颜色;经 MAVEN_OPTS 强制开(保留用户已有值)
|
|
154
|
+
MAVEN_OPTS: `${process.env.MAVEN_OPTS ?? ""} -Dstyle.color=always`.trim(),
|
|
155
|
+
CI: ""
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
this.child = child;
|
|
159
|
+
const push = (d) => {
|
|
160
|
+
this.pending += d.toString();
|
|
161
|
+
let idx;
|
|
162
|
+
while ((idx = this.pending.indexOf("\n")) >= 0) {
|
|
163
|
+
const line = this.pending.slice(0, idx + 1);
|
|
164
|
+
this.pending = this.pending.slice(idx + 1);
|
|
165
|
+
this.pushLine(line);
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
child.stdout?.on("data", push);
|
|
169
|
+
child.stderr?.on("data", push);
|
|
170
|
+
child.on("error", (err) => {
|
|
171
|
+
this.pushLine(`[zview] spawn error: ${err.message}
|
|
172
|
+
`);
|
|
173
|
+
});
|
|
174
|
+
child.on("exit", (code) => {
|
|
175
|
+
if (this.pending) {
|
|
176
|
+
this.pushLine(this.pending + "\n");
|
|
177
|
+
this.pending = "";
|
|
178
|
+
}
|
|
179
|
+
this.child = null;
|
|
180
|
+
this.state = "exited";
|
|
181
|
+
this.code = code ?? 0;
|
|
182
|
+
this.emit({ type: "state", state: "exited", recipe: this.recipe, code: this.code });
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
pushLine(line) {
|
|
186
|
+
this.buffer.push(line);
|
|
187
|
+
if (this.buffer.length > MAX_BUFFER) this.buffer.shift();
|
|
188
|
+
this.emit({ type: "log", text: line });
|
|
189
|
+
}
|
|
190
|
+
stop() {
|
|
191
|
+
this.killChild();
|
|
192
|
+
}
|
|
193
|
+
restart(recipe) {
|
|
194
|
+
const target = recipe ?? this.recipe;
|
|
195
|
+
if (target) this.start(target);
|
|
196
|
+
}
|
|
197
|
+
killChild() {
|
|
198
|
+
const child = this.child;
|
|
199
|
+
if (child?.pid) {
|
|
200
|
+
try {
|
|
201
|
+
if (process.platform === "win32") spawn("taskkill", ["/PID", String(child.pid), "/T", "/F"]);
|
|
202
|
+
else child.kill("SIGTERM");
|
|
203
|
+
} catch {
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
this.child = null;
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
// src/server/bugs.ts
|
|
211
|
+
import fs2 from "fs";
|
|
212
|
+
import path2 from "path";
|
|
213
|
+
function loadZgoalConfig(root2) {
|
|
214
|
+
const file = path2.join(root2, ".zgoal", "config.yaml");
|
|
215
|
+
if (!fs2.existsSync(file)) return null;
|
|
216
|
+
const kv = {};
|
|
217
|
+
for (const line of fs2.readFileSync(file, "utf8").split("\n")) {
|
|
218
|
+
const m = line.match(/^\s*([A-Za-z_]\w*)\s*:\s*(.+?)\s*$/);
|
|
219
|
+
if (m && !m[2].startsWith("#")) kv[m[1]] = m[2].replace(/^["']|["']$/g, "");
|
|
220
|
+
}
|
|
221
|
+
const product = Number(kv.product);
|
|
222
|
+
if (!kv.url || !product) return null;
|
|
223
|
+
return {
|
|
224
|
+
url: kv.url.replace(/\/+$/, ""),
|
|
225
|
+
account: kv.account ?? "",
|
|
226
|
+
password: kv.password,
|
|
227
|
+
token: kv.token,
|
|
228
|
+
product
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
async function fetchJson(url, init) {
|
|
232
|
+
const ctrl = new AbortController();
|
|
233
|
+
const timer = setTimeout(() => ctrl.abort(), 8e3);
|
|
234
|
+
try {
|
|
235
|
+
const res = await fetch(url, { ...init, signal: ctrl.signal });
|
|
236
|
+
const text = await res.text();
|
|
237
|
+
let json = {};
|
|
238
|
+
try {
|
|
239
|
+
json = JSON.parse(text);
|
|
240
|
+
} catch {
|
|
241
|
+
}
|
|
242
|
+
if (!res.ok) {
|
|
243
|
+
const err = json.error;
|
|
244
|
+
throw new Error(`HTTP ${res.status}${typeof err === "string" ? `: ${err}` : ""}`);
|
|
245
|
+
}
|
|
246
|
+
return json;
|
|
247
|
+
} finally {
|
|
248
|
+
clearTimeout(timer);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
var tokenCache = null;
|
|
252
|
+
async function getToken(cfg) {
|
|
253
|
+
if (cfg.token) return cfg.token;
|
|
254
|
+
const key = `${cfg.url}|${cfg.account}|${cfg.password ?? ""}`;
|
|
255
|
+
if (tokenCache && tokenCache.key === key && Date.now() - tokenCache.at < 10 * 6e4) return tokenCache.token;
|
|
256
|
+
const json = await fetchJson(`${cfg.url}/api.php/v1/tokens`, {
|
|
257
|
+
method: "POST",
|
|
258
|
+
headers: { "Content-Type": "application/json" },
|
|
259
|
+
body: JSON.stringify({ account: cfg.account, password: cfg.password })
|
|
260
|
+
});
|
|
261
|
+
const token = typeof json.token === "string" ? json.token : "";
|
|
262
|
+
if (!token) throw new Error("token \u83B7\u53D6\u5931\u8D25:\u68C0\u67E5 account / password");
|
|
263
|
+
tokenCache = { key, token, at: Date.now() };
|
|
264
|
+
return token;
|
|
265
|
+
}
|
|
266
|
+
function normBug(b, account) {
|
|
267
|
+
const assigned = b.assignedTo;
|
|
268
|
+
const assignedTo = typeof assigned === "string" ? assigned : assigned && typeof assigned === "object" && "realname" in assigned ? String(assigned.realname ?? "") : "";
|
|
269
|
+
const assignedAccount = typeof assigned === "string" ? assigned : assigned && typeof assigned === "object" ? String(assigned.account ?? "") : "";
|
|
270
|
+
const mine = !!account && (assignedAccount === account || assignedTo === account);
|
|
271
|
+
return {
|
|
272
|
+
id: Number(b.id),
|
|
273
|
+
title: String(b.title ?? ""),
|
|
274
|
+
severity: b.severity ?? 4,
|
|
275
|
+
pri: b.pri ?? 3,
|
|
276
|
+
status: String(b.status ?? ""),
|
|
277
|
+
assignedTo,
|
|
278
|
+
openedBy: typeof b.openedBy === "string" ? b.openedBy : void 0,
|
|
279
|
+
mine
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
async function fetchBugs(root2) {
|
|
283
|
+
const cfg = loadZgoalConfig(root2);
|
|
284
|
+
if (!cfg) return { ok: false, error: ".zgoal/config.yaml \u7F3A\u5931\u6216 url/product \u672A\u914D\u7F6E(\u7531 zgoal skill \u521B\u5EFA)" };
|
|
285
|
+
try {
|
|
286
|
+
const token = await getToken(cfg);
|
|
287
|
+
const json = await fetchJson(
|
|
288
|
+
`${cfg.url}/api.php/v1/products/${cfg.product}/bugs?page=1&limit=100`,
|
|
289
|
+
{ headers: { Token: token } }
|
|
290
|
+
);
|
|
291
|
+
const raw = Array.isArray(json.bugs) ? json.bugs : [];
|
|
292
|
+
return { ok: true, url: cfg.url, total: Number(json.total ?? raw.length), bugs: raw.map((b) => normBug(b, cfg.account)) };
|
|
293
|
+
} catch (e) {
|
|
294
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
295
|
+
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` };
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// src/server/plugins.ts
|
|
300
|
+
import fs3 from "fs";
|
|
301
|
+
import path3 from "path";
|
|
302
|
+
import { fileURLToPath } from "url";
|
|
303
|
+
var __dirname = path3.dirname(fileURLToPath(import.meta.url));
|
|
304
|
+
var builtinPlugins = /* @__PURE__ */ new Map();
|
|
305
|
+
function registerBuiltin(plugin) {
|
|
306
|
+
builtinPlugins.set(plugin.mode, plugin);
|
|
307
|
+
}
|
|
308
|
+
function allBuiltins() {
|
|
309
|
+
return Array.from(builtinPlugins.values());
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// package.json
|
|
313
|
+
var package_default = {
|
|
314
|
+
name: "zdashboard",
|
|
315
|
+
version: "1.0.0",
|
|
316
|
+
description: "ZCode skill dashboard platform \u2014 pluggable viewers for zdesign/zview/zreview/zgoal",
|
|
317
|
+
type: "module",
|
|
318
|
+
bin: { zdashboard: "./dist/cli.js" },
|
|
319
|
+
files: ["dist"],
|
|
320
|
+
publishConfig: { access: "public" },
|
|
321
|
+
scripts: {
|
|
322
|
+
dev: "vite",
|
|
323
|
+
build: "tsup && vite build",
|
|
324
|
+
"build:web": "vite build",
|
|
325
|
+
"build:node": "tsup",
|
|
326
|
+
start: "node dist/cli.js",
|
|
327
|
+
preview: "vite preview"
|
|
328
|
+
},
|
|
329
|
+
dependencies: {
|
|
330
|
+
"@radix-ui/react-scroll-area": "^1.2.0",
|
|
331
|
+
"@radix-ui/react-separator": "^1.1.0",
|
|
332
|
+
"@radix-ui/react-slot": "^1.1.0",
|
|
333
|
+
"@radix-ui/react-tooltip": "^1.1.2",
|
|
334
|
+
"ansi-to-react": "^6.1.6",
|
|
335
|
+
"class-variance-authority": "^0.7.0",
|
|
336
|
+
clsx: "^2.1.1",
|
|
337
|
+
"highlight.js": "^11.11.1",
|
|
338
|
+
katex: "^0.16.11",
|
|
339
|
+
"lucide-react": "^0.460.0",
|
|
340
|
+
react: "^18.3.1",
|
|
341
|
+
"react-dom": "^18.3.1",
|
|
342
|
+
"react-markdown": "^9.0.1",
|
|
343
|
+
"remark-frontmatter": "^5.0.0",
|
|
344
|
+
"remark-gfm": "^4.0.0",
|
|
345
|
+
"remark-math": "^6.0.0",
|
|
346
|
+
"rehype-autolink-headings": "^7.1.0",
|
|
347
|
+
"rehype-highlight": "^7.0.1",
|
|
348
|
+
"rehype-katex": "^7.0.1",
|
|
349
|
+
"rehype-raw": "^7.0.0",
|
|
350
|
+
"rehype-slug": "^6.0.0",
|
|
351
|
+
"tailwind-merge": "^2.5.4"
|
|
352
|
+
},
|
|
353
|
+
devDependencies: {
|
|
354
|
+
"@tailwindcss/typography": "^0.5.20",
|
|
355
|
+
"@types/node": "^22.9.0",
|
|
356
|
+
"@types/react": "^18.3.12",
|
|
357
|
+
"@types/react-dom": "^18.3.1",
|
|
358
|
+
typescript: "^5.6.3",
|
|
359
|
+
vite: "^5.4.10",
|
|
360
|
+
"@vitejs/plugin-react": "^4.3.3",
|
|
361
|
+
tailwindcss: "^3.4.14",
|
|
362
|
+
"tailwindcss-animate": "^1.0.7",
|
|
363
|
+
postcss: "^8.4.49",
|
|
364
|
+
autoprefixer: "^10.4.20",
|
|
365
|
+
tsup: "^8.3.5"
|
|
366
|
+
},
|
|
367
|
+
pnpm: {
|
|
368
|
+
onlyBuiltDependencies: ["esbuild"]
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
// src/server/index.ts
|
|
373
|
+
var VERSION = package_default.version;
|
|
374
|
+
var __dirname2 = path4.dirname(fileURLToPath2(import.meta.url));
|
|
375
|
+
var STOP_TOKEN = crypto.randomBytes(12).toString("hex");
|
|
376
|
+
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>`;
|
|
377
|
+
var MIME = {
|
|
378
|
+
".html": "text/html; charset=utf-8",
|
|
379
|
+
".htm": "text/html; charset=utf-8",
|
|
380
|
+
".css": "text/css; charset=utf-8",
|
|
381
|
+
".js": "application/javascript; charset=utf-8",
|
|
382
|
+
".mjs": "application/javascript; charset=utf-8",
|
|
383
|
+
".json": "application/json; charset=utf-8",
|
|
384
|
+
".svg": "image/svg+xml",
|
|
385
|
+
".png": "image/png",
|
|
386
|
+
".ico": "image/x-icon",
|
|
387
|
+
".jpg": "image/jpeg",
|
|
388
|
+
".jpeg": "image/jpeg",
|
|
389
|
+
".gif": "image/gif",
|
|
390
|
+
".webp": "image/webp",
|
|
391
|
+
".md": "text/markdown; charset=utf-8",
|
|
392
|
+
".txt": "text/plain; charset=utf-8",
|
|
393
|
+
".yml": "text/yaml; charset=utf-8",
|
|
394
|
+
".yaml": "text/yaml; charset=utf-8",
|
|
395
|
+
".woff": "font/woff",
|
|
396
|
+
".woff2": "font/woff2",
|
|
397
|
+
".ttf": "font/ttf",
|
|
398
|
+
".map": "application/json; charset=utf-8"
|
|
399
|
+
};
|
|
400
|
+
function readBody(req) {
|
|
401
|
+
return new Promise((resolve) => {
|
|
402
|
+
let data = "";
|
|
403
|
+
req.on("data", (c) => data += c);
|
|
404
|
+
req.on("end", () => resolve(data));
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
function createServer(opts) {
|
|
408
|
+
const ROOT = path4.resolve(opts.root);
|
|
409
|
+
const PORT0 = opts.port ?? 4190;
|
|
410
|
+
const OPEN = !!opts.open;
|
|
411
|
+
const APP_DIR = opts.dashboardDir ?? path4.resolve(__dirname2, "web");
|
|
412
|
+
if (!fs4.existsSync(ROOT)) fs4.mkdirSync(ROOT, { recursive: true });
|
|
413
|
+
const det2 = opts.detect;
|
|
414
|
+
const runner = new JustRunner(ROOT);
|
|
415
|
+
const MODE = opts.mode;
|
|
416
|
+
registerBuiltin({
|
|
417
|
+
mode: "bugs",
|
|
418
|
+
label: "\u7985\u9053 Bugs",
|
|
419
|
+
icon: "\u{1F3AF}",
|
|
420
|
+
apiRoutes: { "/__bugs": async (_, res) => {
|
|
421
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
|
|
422
|
+
fetchBugs(ROOT).then((r) => res.end(JSON.stringify(r)));
|
|
423
|
+
} }
|
|
424
|
+
});
|
|
425
|
+
registerBuiltin({ mode: "view", label: "\u9879\u76EE\u6D4F\u89C8", icon: "\u{1F441}\uFE0F" });
|
|
426
|
+
registerBuiltin({ mode: "review", label: "\u6587\u6863\u8BC4\u5BA1", icon: "\u2705" });
|
|
427
|
+
const clients = /* @__PURE__ */ new Set();
|
|
428
|
+
const broadcast = (ev, data = "") => {
|
|
429
|
+
const payload = `event: ${ev}
|
|
430
|
+
data: ${JSON.stringify(data == null ? "" : data)}
|
|
431
|
+
|
|
432
|
+
`;
|
|
433
|
+
for (const c of clients) c.write(payload);
|
|
434
|
+
};
|
|
435
|
+
function serveFile(filePath, res, injectHtml) {
|
|
436
|
+
fs4.readFile(filePath, (err, data) => {
|
|
437
|
+
if (err) {
|
|
438
|
+
res.writeHead(404);
|
|
439
|
+
return res.end("Not found");
|
|
440
|
+
}
|
|
441
|
+
const ext = path4.extname(filePath).toLowerCase();
|
|
442
|
+
const ct = MIME[ext] ?? "application/octet-stream";
|
|
443
|
+
let body = data;
|
|
444
|
+
if (injectHtml && ext === ".html") {
|
|
445
|
+
const s = data.toString("utf8");
|
|
446
|
+
body = Buffer.from(s.indexOf("</body>") >= 0 ? s.replace("</body>", INJECT + "</body>") : s + INJECT);
|
|
447
|
+
}
|
|
448
|
+
res.writeHead(200, { "Content-Type": ct, "Cache-Control": "no-cache" });
|
|
449
|
+
res.end(body);
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
function handler(req, res) {
|
|
453
|
+
const url = req.url.split("?")[0];
|
|
454
|
+
if (url === "/__reload") {
|
|
455
|
+
res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" });
|
|
456
|
+
res.write(": connected\n\n");
|
|
457
|
+
clients.add(res);
|
|
458
|
+
req.on("close", () => clients.delete(res));
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
if (url === "/__config") {
|
|
462
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
|
|
463
|
+
return res.end(JSON.stringify({ stopToken: STOP_TOKEN }));
|
|
464
|
+
}
|
|
465
|
+
if (url === "/__stop" && req.method === "POST") {
|
|
466
|
+
if (req.headers["x-stop-token"] === STOP_TOKEN) {
|
|
467
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
|
|
468
|
+
res.end('{"ok":true}');
|
|
469
|
+
runner.stop();
|
|
470
|
+
setTimeout(() => {
|
|
471
|
+
try {
|
|
472
|
+
server.close();
|
|
473
|
+
} catch {
|
|
474
|
+
}
|
|
475
|
+
process.exit(0);
|
|
476
|
+
}, 50);
|
|
477
|
+
} else {
|
|
478
|
+
res.writeHead(403);
|
|
479
|
+
res.end("forbidden");
|
|
480
|
+
}
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
if (url === "/__files") {
|
|
484
|
+
const tree = scanTree(ROOT, det2.hasOpenspec, det2.hasDocs);
|
|
485
|
+
const payload = { tree, ...det2 };
|
|
486
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
|
|
487
|
+
return res.end(JSON.stringify(payload));
|
|
488
|
+
}
|
|
489
|
+
if (url === "/__just/recipes") {
|
|
490
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
|
|
491
|
+
runner.recipes().then((r) => res.end(JSON.stringify(r)));
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
if (url === "/__just/logs") {
|
|
495
|
+
res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" });
|
|
496
|
+
res.write(": connected\n\n");
|
|
497
|
+
const unsub = runner.subscribe((ev) => res.write(`data: ${JSON.stringify(ev)}
|
|
498
|
+
|
|
499
|
+
`));
|
|
500
|
+
req.on("close", unsub);
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
const justAction = url.match(/^\/__just\/(start|stop|restart)$/);
|
|
504
|
+
if (justAction && req.method === "POST") {
|
|
505
|
+
(async () => {
|
|
506
|
+
if (req.headers["x-stop-token"] !== STOP_TOKEN) {
|
|
507
|
+
res.writeHead(403);
|
|
508
|
+
res.end("forbidden");
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
const body = await readBody(req);
|
|
512
|
+
let recipe;
|
|
513
|
+
try {
|
|
514
|
+
recipe = JSON.parse(body || "{}").recipe;
|
|
515
|
+
} catch {
|
|
516
|
+
}
|
|
517
|
+
const act = justAction[1];
|
|
518
|
+
if (act === "start" || act === "restart") {
|
|
519
|
+
const target = recipe ?? runner.info().recipe;
|
|
520
|
+
if (!target) {
|
|
521
|
+
res.writeHead(400);
|
|
522
|
+
res.end('{"error":"no recipe"}');
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
const recipes = await runner.recipes();
|
|
526
|
+
if (!recipes.some((r) => r.name === target)) {
|
|
527
|
+
res.writeHead(403);
|
|
528
|
+
res.end('{"error":"unknown recipe"}');
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
runner.start(target);
|
|
532
|
+
} else {
|
|
533
|
+
runner.stop();
|
|
534
|
+
}
|
|
535
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
|
|
536
|
+
res.end(JSON.stringify(runner.info()));
|
|
537
|
+
})();
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
let handled = false;
|
|
541
|
+
for (const plugin of allBuiltins()) {
|
|
542
|
+
if (!plugin.apiRoutes) continue;
|
|
543
|
+
for (const [route, handler2] of Object.entries(plugin.apiRoutes)) {
|
|
544
|
+
if (url === route) {
|
|
545
|
+
handled = true;
|
|
546
|
+
handler2(req, res, ROOT);
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
if (url === "/") return serveFile(path4.join(APP_DIR, "index.html"), res, false);
|
|
552
|
+
if (url.indexOf("/__app/") === 0) {
|
|
553
|
+
const fp2 = path4.join(APP_DIR, url.slice(7));
|
|
554
|
+
if (fp2 !== APP_DIR && fp2.indexOf(APP_DIR + path4.sep) !== 0) {
|
|
555
|
+
res.writeHead(403);
|
|
556
|
+
return res.end("Forbidden");
|
|
557
|
+
}
|
|
558
|
+
return serveFile(fp2, res, false);
|
|
559
|
+
}
|
|
560
|
+
if (url.indexOf("/assets/") === 0) {
|
|
561
|
+
const fp2 = path4.join(APP_DIR, decodeURIComponent(url));
|
|
562
|
+
if (fp2.indexOf(APP_DIR + path4.sep) !== 0) {
|
|
563
|
+
res.writeHead(403);
|
|
564
|
+
return res.end("Forbidden");
|
|
565
|
+
}
|
|
566
|
+
return serveFile(fp2, res, false);
|
|
567
|
+
}
|
|
568
|
+
const fp = path4.join(ROOT, decodeURIComponent(url));
|
|
569
|
+
if (fp !== ROOT && fp.indexOf(ROOT + path4.sep) !== 0) {
|
|
570
|
+
res.writeHead(403);
|
|
571
|
+
return res.end("Forbidden");
|
|
572
|
+
}
|
|
573
|
+
return serveFile(fp, res, true);
|
|
574
|
+
}
|
|
575
|
+
let server;
|
|
576
|
+
function start(port) {
|
|
577
|
+
server = http.createServer(handler);
|
|
578
|
+
server.on("error", (err) => {
|
|
579
|
+
if (err.code === "EADDRINUSE") {
|
|
580
|
+
console.log(`[zdashboard] port ${port} busy, trying ${port + 1}`);
|
|
581
|
+
start(port + 1);
|
|
582
|
+
} else throw err;
|
|
583
|
+
});
|
|
584
|
+
server.listen(port, () => {
|
|
585
|
+
const u = `http://localhost:${port}`;
|
|
586
|
+
console.log(`[zdashboard] v${VERSION} dashboard -> ${u}`);
|
|
587
|
+
console.log(`[zdashboard] project -> ${ROOT}`);
|
|
588
|
+
console.log(`[zdashboard] mode -> ${MODE ?? "(auto)"}`);
|
|
589
|
+
console.log(`[zdashboard] detect -> openspec:${det2.hasOpenspec} docs:${det2.hasDocs} just:${det2.hasJust} bugs:${det2.hasBugs}`);
|
|
590
|
+
if (OPEN) exec(process.platform === "darwin" ? `open ${u}` : `start ${u}`);
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
let debounce;
|
|
594
|
+
try {
|
|
595
|
+
fs4.watch(ROOT, { recursive: true }, () => {
|
|
596
|
+
clearTimeout(debounce);
|
|
597
|
+
debounce = setTimeout(() => {
|
|
598
|
+
broadcast("reload");
|
|
599
|
+
broadcast("files");
|
|
600
|
+
console.log(`[zdashboard] change -> reload + refresh tree (${clients.size} client${clients.size === 1 ? "" : "s"})`);
|
|
601
|
+
}, 150);
|
|
602
|
+
});
|
|
603
|
+
} catch {
|
|
604
|
+
console.log("[zdashboard] watch unavailable - static only.");
|
|
605
|
+
}
|
|
606
|
+
start(PORT0);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// src/server/detect.ts
|
|
610
|
+
import fs5 from "fs";
|
|
611
|
+
import path5 from "path";
|
|
612
|
+
import { execFile as execFile2 } from "child_process";
|
|
613
|
+
function justAvailable(cwd) {
|
|
614
|
+
return new Promise((resolve) => {
|
|
615
|
+
const child = execFile2("just", ["--list", "--unsorted"], { cwd, timeout: 5e3 }, (err) => {
|
|
616
|
+
resolve(!err);
|
|
617
|
+
});
|
|
618
|
+
if (child.killed) resolve(false);
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
async function detect(root2) {
|
|
622
|
+
const hasOpenspec = fs5.existsSync(path5.join(root2, "openspec"));
|
|
623
|
+
const hasDocs = fs5.existsSync(path5.join(root2, "docs"));
|
|
624
|
+
const hasJust = await justAvailable(root2);
|
|
625
|
+
const hasBugs = fs5.existsSync(path5.join(root2, ".zgoal", "config.yaml"));
|
|
626
|
+
return { hasOpenspec, hasDocs, hasJust, hasBugs };
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// src/cli.ts
|
|
630
|
+
function parseArgs(a) {
|
|
631
|
+
const o = {};
|
|
632
|
+
for (let i = 0; i < a.length; i++) {
|
|
633
|
+
if (a[i].indexOf("--") === 0) {
|
|
634
|
+
const n = a[i + 1];
|
|
635
|
+
o[a[i].slice(2)] = n && n.indexOf("--") !== 0 ? a[++i] : true;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
return o;
|
|
639
|
+
}
|
|
640
|
+
var args = parseArgs(process.argv.slice(2));
|
|
641
|
+
var root = args.dir ?? ".";
|
|
642
|
+
var det = await detect(root);
|
|
643
|
+
createServer({
|
|
644
|
+
root,
|
|
645
|
+
port: args.port ? parseInt(args.port, 10) : void 0,
|
|
646
|
+
open: !!args.open,
|
|
647
|
+
detect: det,
|
|
648
|
+
mode: args.mode
|
|
649
|
+
});
|
|
650
|
+
//# sourceMappingURL=cli.js.map
|