zdashboard 1.6.2 → 2.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 +206 -41
- package/dist/cli.js +1189 -832
- package/dist/cli.js.map +1 -1
- package/dist/web/assets/MdViewer-DHLa9u9L.js +1 -0
- package/dist/web/assets/file-text-D4672ckY.js +6 -0
- package/dist/web/assets/index-BRLQFQ6k.js +29 -0
- package/dist/web/assets/index-Deg587va.js +87 -0
- package/dist/web/assets/index-Dqj3ddhK.css +1 -0
- package/dist/web/assets/katex-BTcE7LSd.css +10 -0
- package/dist/web/assets/katex.min-D64GJ8fh.js +270 -0
- package/dist/web/assets/web-2je-DAWY.js +30 -0
- package/dist/web/assets/{Viewer-DgUxcwns.js → web-4tpIem4N.js} +3 -3
- package/dist/web/assets/web-Bgs6ipDo.js +31 -0
- package/dist/web/assets/{Viewer-CvXxR4rr.js → web-C0d3eOAl.js} +7 -2
- package/dist/web/assets/{DesignViewer-DIFMIK04.js → web-DIAM7Pif.js} +6 -6
- package/dist/web/assets/web-DqBNmBY8.js +25 -0
- package/dist/web/index.html +2 -2
- package/package.json +4 -1
- 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,138 +1,594 @@
|
|
|
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 path8 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
|
-
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;
|
|
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
|
+
});
|
|
34
19
|
}
|
|
35
|
-
function
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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: "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 });
|
|
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 (e) {
|
|
75
|
-
console.error("[zdashboard] scan root etc failed:", e);
|
|
76
|
-
}
|
|
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;
|
|
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 };
|
|
80
26
|
}
|
|
81
27
|
|
|
82
|
-
// src/
|
|
83
|
-
import {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
28
|
+
// src/cli.ts
|
|
29
|
+
import { Context } from "cordis";
|
|
30
|
+
|
|
31
|
+
// src/core/server.ts
|
|
32
|
+
import http from "http";
|
|
33
|
+
import fs2 from "fs";
|
|
34
|
+
import path2 from "path";
|
|
35
|
+
import crypto from "crypto";
|
|
36
|
+
import { exec } from "child_process";
|
|
37
|
+
import { fileURLToPath } from "url";
|
|
38
|
+
|
|
39
|
+
// package.json
|
|
40
|
+
var package_default = {
|
|
41
|
+
name: "zdashboard",
|
|
42
|
+
version: "2.0.0",
|
|
43
|
+
description: "ZCode skill dashboard platform \u2014 pluggable viewers for zdesign/zview/zreview/zgoal",
|
|
44
|
+
type: "module",
|
|
45
|
+
bin: {
|
|
46
|
+
zdashboard: "./dist/cli.js"
|
|
47
|
+
},
|
|
48
|
+
files: [
|
|
49
|
+
"dist"
|
|
50
|
+
],
|
|
51
|
+
publishConfig: {
|
|
52
|
+
access: "public"
|
|
53
|
+
},
|
|
54
|
+
scripts: {
|
|
55
|
+
dev: "vite",
|
|
56
|
+
build: "tsup && vite build",
|
|
57
|
+
"build:web": "vite build",
|
|
58
|
+
"build:node": "tsup",
|
|
59
|
+
start: "node dist/cli.js",
|
|
60
|
+
preview: "vite preview"
|
|
61
|
+
},
|
|
62
|
+
dependencies: {
|
|
63
|
+
"@radix-ui/react-scroll-area": "^1.2.0",
|
|
64
|
+
"@radix-ui/react-separator": "^1.1.0",
|
|
65
|
+
"@radix-ui/react-slot": "^1.1.0",
|
|
66
|
+
"@radix-ui/react-tooltip": "^1.1.2",
|
|
67
|
+
"@uidotdev/usehooks": "^2.4.0",
|
|
68
|
+
"ansi-to-react": "^6.1.6",
|
|
69
|
+
"class-variance-authority": "^0.7.0",
|
|
70
|
+
clsx: "^2.1.1",
|
|
71
|
+
cordis: "4.0.0-rc.8",
|
|
72
|
+
cosmokit: "^1.8.1",
|
|
73
|
+
"date-fns": "^3.6.0",
|
|
74
|
+
filesize: "^11.0.0",
|
|
75
|
+
"highlight.js": "^11.11.1",
|
|
76
|
+
katex: "^0.16.11",
|
|
77
|
+
ky: "^1.7.0",
|
|
78
|
+
"lodash-es": "^4.17.21",
|
|
79
|
+
"lucide-react": "^0.460.0",
|
|
80
|
+
react: "^18.3.1",
|
|
81
|
+
"react-dom": "^18.3.1",
|
|
82
|
+
"react-error-boundary": "^5.0.0",
|
|
83
|
+
"react-markdown": "^9.0.1",
|
|
84
|
+
"rehype-autolink-headings": "^7.1.0",
|
|
85
|
+
"rehype-highlight": "^7.0.1",
|
|
86
|
+
"rehype-katex": "^7.0.1",
|
|
87
|
+
"rehype-raw": "^7.0.0",
|
|
88
|
+
"rehype-slug": "^6.0.0",
|
|
89
|
+
"remark-frontmatter": "^5.0.0",
|
|
90
|
+
"remark-gfm": "^4.0.0",
|
|
91
|
+
"remark-math": "^6.0.0",
|
|
92
|
+
sonner: "^1.5.0",
|
|
93
|
+
"tailwind-merge": "^2.5.4",
|
|
94
|
+
tsx: "^4.7.0",
|
|
95
|
+
"use-debounce": "^10.0.0",
|
|
96
|
+
yaml: "^2.9.0"
|
|
97
|
+
},
|
|
98
|
+
devDependencies: {
|
|
99
|
+
"@tailwindcss/typography": "^0.5.20",
|
|
100
|
+
"@testing-library/jest-dom": "^7.0.1",
|
|
101
|
+
"@testing-library/react": "^16.3.2",
|
|
102
|
+
"@types/lodash-es": "^4.17.12",
|
|
103
|
+
"@types/node": "^22.9.0",
|
|
104
|
+
"@types/react": "^18.3.12",
|
|
105
|
+
"@types/react-dom": "^18.3.1",
|
|
106
|
+
"@vitejs/plugin-react": "^4.7.0",
|
|
107
|
+
autoprefixer: "^10.4.20",
|
|
108
|
+
jsdom: "^30.0.1",
|
|
109
|
+
postcss: "^8.4.49",
|
|
110
|
+
tailwindcss: "^3.4.14",
|
|
111
|
+
"tailwindcss-animate": "^1.0.7",
|
|
112
|
+
tsup: "^8.3.5",
|
|
113
|
+
typescript: "^5.6.3",
|
|
114
|
+
vite: "^5.4.10",
|
|
115
|
+
vitest: "^1.6.0"
|
|
116
|
+
},
|
|
117
|
+
pnpm: {
|
|
118
|
+
onlyBuiltDependencies: [
|
|
119
|
+
"esbuild"
|
|
120
|
+
]
|
|
98
121
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// src/core/server.ts
|
|
125
|
+
import { Service } from "cordis";
|
|
126
|
+
var VERSION = package_default.version;
|
|
127
|
+
var __dirname = path2.dirname(fileURLToPath(import.meta.url));
|
|
128
|
+
var MIME = {
|
|
129
|
+
".html": "text/html; charset=utf-8",
|
|
130
|
+
".htm": "text/html; charset=utf-8",
|
|
131
|
+
".css": "text/css; charset=utf-8",
|
|
132
|
+
".js": "application/javascript; charset=utf-8",
|
|
133
|
+
".mjs": "application/javascript; charset=utf-8",
|
|
134
|
+
".json": "application/json; charset=utf-8",
|
|
135
|
+
".svg": "image/svg+xml",
|
|
136
|
+
".png": "image/png",
|
|
137
|
+
".ico": "image/x-icon",
|
|
138
|
+
".jpg": "image/jpeg",
|
|
139
|
+
".jpeg": "image/jpeg",
|
|
140
|
+
".gif": "image/gif",
|
|
141
|
+
".webp": "image/webp",
|
|
142
|
+
".md": "text/markdown; charset=utf-8",
|
|
143
|
+
".txt": "text/plain; charset=utf-8",
|
|
144
|
+
".sql": "text/plain; charset=utf-8",
|
|
145
|
+
".csv": "text/plain; charset=utf-8",
|
|
146
|
+
".tsv": "text/plain; charset=utf-8",
|
|
147
|
+
".yaml": "text/yaml; charset=utf-8",
|
|
148
|
+
".yml": "text/yaml; charset=utf-8",
|
|
149
|
+
".xml": "application/xml; charset=utf-8",
|
|
150
|
+
".py": "text/plain; charset=utf-8",
|
|
151
|
+
".ts": "text/plain; charset=utf-8",
|
|
152
|
+
".java": "text/plain; charset=utf-8",
|
|
153
|
+
".go": "text/plain; charset=utf-8",
|
|
154
|
+
".rs": "text/plain; charset=utf-8",
|
|
155
|
+
".rb": "text/plain; charset=utf-8",
|
|
156
|
+
".php": "text/plain; charset=utf-8",
|
|
157
|
+
".c": "text/plain; charset=utf-8",
|
|
158
|
+
".cpp": "text/plain; charset=utf-8",
|
|
159
|
+
".h": "text/plain; charset=utf-8",
|
|
160
|
+
".cs": "text/plain; charset=utf-8",
|
|
161
|
+
".swift": "text/plain; charset=utf-8",
|
|
162
|
+
".kt": "text/plain; charset=utf-8",
|
|
163
|
+
".scala": "text/plain; charset=utf-8",
|
|
164
|
+
".sh": "text/plain; charset=utf-8",
|
|
165
|
+
".bash": "text/plain; charset=utf-8",
|
|
166
|
+
".zsh": "text/plain; charset=utf-8",
|
|
167
|
+
".fish": "text/plain; charset=utf-8",
|
|
168
|
+
".env": "text/plain; charset=utf-8",
|
|
169
|
+
".gitignore": "text/plain; charset=utf-8",
|
|
170
|
+
".dockerfile": "text/plain; charset=utf-8",
|
|
171
|
+
".woff": "font/woff",
|
|
172
|
+
".woff2": "font/woff2",
|
|
173
|
+
".ttf": "font/woff",
|
|
174
|
+
".map": "application/json; charset=utf-8"
|
|
175
|
+
};
|
|
176
|
+
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>`;
|
|
177
|
+
var PLUGIN_STATIC_PREFIX = "/__plugin/";
|
|
178
|
+
var ServerService = class extends Service {
|
|
179
|
+
stopToken;
|
|
180
|
+
root;
|
|
181
|
+
appDir;
|
|
182
|
+
open;
|
|
183
|
+
page;
|
|
184
|
+
det;
|
|
185
|
+
routes = /* @__PURE__ */ new Map();
|
|
186
|
+
sses = /* @__PURE__ */ new Map();
|
|
187
|
+
prefixStatic = /* @__PURE__ */ new Map();
|
|
188
|
+
server;
|
|
189
|
+
constructor(ctx, config) {
|
|
190
|
+
super(ctx, "server");
|
|
191
|
+
this.stopToken = crypto.randomBytes(12).toString("hex");
|
|
192
|
+
this.root = config.root;
|
|
193
|
+
this.appDir = config.appDir;
|
|
194
|
+
this.open = config.open;
|
|
195
|
+
this.page = config.page;
|
|
196
|
+
this.det = config.detect;
|
|
197
|
+
ctx.effect(() => () => this.dispose());
|
|
198
|
+
this.route("/__config", (_req, res) => {
|
|
199
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
|
|
200
|
+
res.end(JSON.stringify({ stopToken: this.stopToken, version: VERSION, root: this.root }));
|
|
201
|
+
});
|
|
202
|
+
this.route("/__stop", async (req, res) => {
|
|
203
|
+
if (req.headers["x-stop-token"] !== this.stopToken) {
|
|
204
|
+
res.writeHead(403);
|
|
205
|
+
res.end("forbidden");
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
|
|
209
|
+
res.end('{"ok":true}');
|
|
210
|
+
this.stop();
|
|
123
211
|
});
|
|
212
|
+
this.start(config.port);
|
|
124
213
|
}
|
|
125
|
-
|
|
126
|
-
this.
|
|
127
|
-
|
|
128
|
-
fn({ type: "state", state: this.state, recipe: this.recipe, code: this.code });
|
|
129
|
-
return () => this.clients.delete(fn);
|
|
214
|
+
route(path9, handler) {
|
|
215
|
+
this.routes.set(path9, handler);
|
|
216
|
+
this.ctx.effect(() => () => this.routes.delete(path9));
|
|
130
217
|
}
|
|
131
|
-
|
|
132
|
-
|
|
218
|
+
sse(path9, onConnect) {
|
|
219
|
+
this.sses.set(path9, onConnect);
|
|
220
|
+
this.ctx.effect(() => () => this.sses.delete(path9));
|
|
133
221
|
}
|
|
134
|
-
|
|
135
|
-
|
|
222
|
+
static(prefix, dir) {
|
|
223
|
+
this.prefixStatic.set(prefix, dir);
|
|
224
|
+
this.ctx.effect(() => () => this.prefixStatic.delete(prefix));
|
|
225
|
+
}
|
|
226
|
+
serveFile(filePath, res, injectHtml) {
|
|
227
|
+
fs2.readFile(filePath, (err, data) => {
|
|
228
|
+
if (err) {
|
|
229
|
+
res.writeHead(404);
|
|
230
|
+
return res.end("Not found");
|
|
231
|
+
}
|
|
232
|
+
const ext = path2.extname(filePath).toLowerCase();
|
|
233
|
+
const ct = MIME[ext] ?? "application/octet-stream";
|
|
234
|
+
let body = data;
|
|
235
|
+
if (injectHtml && ext === ".html") {
|
|
236
|
+
const s = data.toString("utf8");
|
|
237
|
+
body = Buffer.from(s.indexOf("</body>") >= 0 ? s.replace("</body>", INJECT + "</body>") : s + INJECT);
|
|
238
|
+
}
|
|
239
|
+
res.writeHead(200, { "Content-Type": ct, "Cache-Control": "no-cache" });
|
|
240
|
+
res.end(body);
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
servePrefix(url, res) {
|
|
244
|
+
for (const [prefix, dir] of this.prefixStatic) {
|
|
245
|
+
if (url.indexOf(prefix) === 0) {
|
|
246
|
+
let rel = this.safeDecode(url.slice(prefix.length));
|
|
247
|
+
if (!rel || rel === "/" || !path2.extname(rel)) rel = path2.join(rel || "", "index.html");
|
|
248
|
+
const fp = path2.join(dir, rel);
|
|
249
|
+
if (fp.indexOf(dir + path2.sep) !== 0) {
|
|
250
|
+
res.writeHead(403);
|
|
251
|
+
return res.end("Forbidden");
|
|
252
|
+
}
|
|
253
|
+
this.serveFile(fp, res, true);
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
safeDecode(raw) {
|
|
260
|
+
try {
|
|
261
|
+
return decodeURIComponent(raw);
|
|
262
|
+
} catch {
|
|
263
|
+
return raw;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
handler = (req, res) => {
|
|
267
|
+
try {
|
|
268
|
+
const rawUrl = req.url || "/";
|
|
269
|
+
const url = rawUrl.split("?")[0];
|
|
270
|
+
const rh = this.routes.get(url);
|
|
271
|
+
if (rh) {
|
|
272
|
+
rh(req, res);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
const sh = this.sses.get(url);
|
|
276
|
+
if (sh) {
|
|
277
|
+
res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" });
|
|
278
|
+
res.write(": connected\n\n");
|
|
279
|
+
const cleanup = sh(res);
|
|
280
|
+
req.on("close", () => {
|
|
281
|
+
cleanup?.();
|
|
282
|
+
});
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
if (this.servePrefix(url, res)) return;
|
|
286
|
+
if (url === "/" || url.indexOf("/__app/") === 0 || url.indexOf("/assets/") === 0) {
|
|
287
|
+
let fp2 = this.appDir;
|
|
288
|
+
if (url !== "/") fp2 = path2.join(this.appDir, this.safeDecode(url));
|
|
289
|
+
if (url.indexOf("/__app/") === 0) fp2 = path2.join(this.appDir, url.slice(7));
|
|
290
|
+
if (fp2 !== this.appDir && fp2.indexOf(this.appDir + path2.sep) !== 0) {
|
|
291
|
+
res.writeHead(403);
|
|
292
|
+
return res.end("Forbidden");
|
|
293
|
+
}
|
|
294
|
+
if (url === "/") fp2 = path2.join(this.appDir, "index.html");
|
|
295
|
+
return this.serveFile(fp2, res, false);
|
|
296
|
+
}
|
|
297
|
+
const fp = path2.join(this.root, this.safeDecode(url));
|
|
298
|
+
if (fp !== this.root && fp.indexOf(this.root + path2.sep) !== 0) {
|
|
299
|
+
res.writeHead(403);
|
|
300
|
+
return res.end("Forbidden");
|
|
301
|
+
}
|
|
302
|
+
return this.serveFile(fp, res, true);
|
|
303
|
+
} catch (e) {
|
|
304
|
+
try {
|
|
305
|
+
res.writeHead(400);
|
|
306
|
+
res.end("bad request");
|
|
307
|
+
} catch {
|
|
308
|
+
}
|
|
309
|
+
console.error("[zdashboard] handler error", e);
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
start(port) {
|
|
313
|
+
this.server = http.createServer(this.handler);
|
|
314
|
+
this.server.on("error", (err) => {
|
|
315
|
+
if (err.code === "EADDRINUSE") {
|
|
316
|
+
console.log(`[zdashboard] port ${port} busy, trying ${port + 1}`);
|
|
317
|
+
this.start(port + 1);
|
|
318
|
+
} else throw err;
|
|
319
|
+
});
|
|
320
|
+
this.server.listen(port, "127.0.0.1", () => {
|
|
321
|
+
const u = `http://localhost:${port}`;
|
|
322
|
+
const target = this.page ? `${u}#${this.page}` : u;
|
|
323
|
+
console.log(`[zdashboard] v${VERSION} dashboard -> ${u}`);
|
|
324
|
+
console.log(`[zdashboard] project -> ${this.root}`);
|
|
325
|
+
console.log(`[zdashboard] detect -> openspec:${this.det.hasOpenspec} docs:${this.det.hasDocs} just:${this.det.hasJust} bugs:${this.det.hasBugs}`);
|
|
326
|
+
if (this.open) exec(process.platform === "darwin" ? `open ${target}` : `start ${target}`);
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
stop() {
|
|
330
|
+
if (this.server) {
|
|
331
|
+
try {
|
|
332
|
+
this.server.close();
|
|
333
|
+
} catch {
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
setTimeout(() => {
|
|
337
|
+
try {
|
|
338
|
+
this.ctx.root.fiber.dispose();
|
|
339
|
+
} catch {
|
|
340
|
+
}
|
|
341
|
+
process.exit(0);
|
|
342
|
+
}, 50);
|
|
343
|
+
}
|
|
344
|
+
dispose() {
|
|
345
|
+
if (this.server) {
|
|
346
|
+
try {
|
|
347
|
+
this.server.close();
|
|
348
|
+
} catch {
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
// src/core/reload.ts
|
|
355
|
+
import fs3 from "fs";
|
|
356
|
+
import { Service as Service2 } from "cordis";
|
|
357
|
+
var WATCH_DEBOUNCE_MS = 150;
|
|
358
|
+
var ReloadService = class extends Service2 {
|
|
359
|
+
static inject = ["server"];
|
|
360
|
+
clients = /* @__PURE__ */ new Set();
|
|
361
|
+
watcher;
|
|
362
|
+
timer;
|
|
363
|
+
constructor(ctx, config) {
|
|
364
|
+
super(ctx, "reload");
|
|
365
|
+
this.ctx.effect(() => () => this.dispose());
|
|
366
|
+
this.ctx.server.sse("/__reload", (res) => {
|
|
367
|
+
this.clients.add(res);
|
|
368
|
+
return () => this.clients.delete(res);
|
|
369
|
+
});
|
|
370
|
+
try {
|
|
371
|
+
this.watcher = fs3.watch(config.root, { recursive: true }, () => {
|
|
372
|
+
if (this.timer) clearTimeout(this.timer);
|
|
373
|
+
this.timer = setTimeout(() => {
|
|
374
|
+
this.broadcast("reload");
|
|
375
|
+
this.broadcast("files");
|
|
376
|
+
}, WATCH_DEBOUNCE_MS);
|
|
377
|
+
});
|
|
378
|
+
} catch {
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
broadcast(ev, data = "") {
|
|
382
|
+
const payload = `event: ${ev}
|
|
383
|
+
data: ${JSON.stringify(data == null ? "" : data)}
|
|
384
|
+
|
|
385
|
+
`;
|
|
386
|
+
for (const c of this.clients) {
|
|
387
|
+
try {
|
|
388
|
+
c.write(payload);
|
|
389
|
+
} catch {
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
dispose() {
|
|
394
|
+
if (this.watcher) {
|
|
395
|
+
try {
|
|
396
|
+
this.watcher.close();
|
|
397
|
+
} catch {
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
if (this.timer) {
|
|
401
|
+
clearTimeout(this.timer);
|
|
402
|
+
}
|
|
403
|
+
for (const c of this.clients) {
|
|
404
|
+
try {
|
|
405
|
+
c.end();
|
|
406
|
+
} catch {
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
this.clients.clear();
|
|
410
|
+
}
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
// src/server/spec-scan.ts
|
|
414
|
+
import fs4 from "fs";
|
|
415
|
+
import path3 from "path";
|
|
416
|
+
function walkFiles(absDir, relDir, depth = 0) {
|
|
417
|
+
if (depth > 4) return [];
|
|
418
|
+
let ents;
|
|
419
|
+
try {
|
|
420
|
+
ents = fs4.readdirSync(absDir, { withFileTypes: true });
|
|
421
|
+
} catch {
|
|
422
|
+
return [];
|
|
423
|
+
}
|
|
424
|
+
const nodes = [];
|
|
425
|
+
for (const ent of ents) {
|
|
426
|
+
if (ent.name.startsWith(".") || ent.name === "node_modules") continue;
|
|
427
|
+
const rel = relDir ? `${relDir}/${ent.name}` : ent.name;
|
|
428
|
+
if (ent.isDirectory()) {
|
|
429
|
+
nodes.push({ name: ent.name, kind: "dir", children: walkFiles(path3.join(absDir, ent.name), rel, depth + 1) });
|
|
430
|
+
} else {
|
|
431
|
+
nodes.push({ name: ent.name, kind: "file", path: rel });
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
nodes.sort((a, b) => a.kind === b.kind ? a.name.localeCompare(b.name) : a.kind === "dir" ? -1 : 1);
|
|
435
|
+
return nodes;
|
|
436
|
+
}
|
|
437
|
+
function scanTree(root, hasOpenspec, hasDocs) {
|
|
438
|
+
const tree = [];
|
|
439
|
+
if (hasOpenspec && fs4.existsSync(path3.join(root, "openspec", "changes"))) {
|
|
440
|
+
const changesDir = path3.join(root, "openspec", "changes");
|
|
441
|
+
const active = [];
|
|
442
|
+
const archived = [];
|
|
443
|
+
for (const ent of fs4.readdirSync(changesDir, { withFileTypes: true })) {
|
|
444
|
+
if (!ent.isDirectory() || ent.name.startsWith(".") || ent.name === "archive") continue;
|
|
445
|
+
active.push({ name: ent.name, kind: "dir", children: walkFiles(path3.join(changesDir, ent.name), `openspec/changes/${ent.name}`) });
|
|
446
|
+
}
|
|
447
|
+
active.sort((a, b) => a.name.localeCompare(b.name));
|
|
448
|
+
const archiveDir = path3.join(changesDir, "archive");
|
|
449
|
+
if (fs4.existsSync(archiveDir)) {
|
|
450
|
+
for (const ent of fs4.readdirSync(archiveDir, { withFileTypes: true })) {
|
|
451
|
+
if (!ent.isDirectory() || ent.name.startsWith(".")) continue;
|
|
452
|
+
archived.push({ name: ent.name, kind: "dir", children: walkFiles(path3.join(archiveDir, ent.name), `openspec/changes/archive/${ent.name}`) });
|
|
453
|
+
}
|
|
454
|
+
archived.sort((a, b) => b.name.localeCompare(a.name));
|
|
455
|
+
}
|
|
456
|
+
if (active.length) tree.push({ name: `\u8FDB\u884C\u4E2D (${active.length})`, kind: "dir", children: active });
|
|
457
|
+
if (archived.length) tree.push({ name: "archive", kind: "dir", defaultCollapsed: true, children: archived });
|
|
458
|
+
const specsDir = path3.join(root, "openspec", "specs");
|
|
459
|
+
if (fs4.existsSync(specsDir)) {
|
|
460
|
+
const specs = walkFiles(specsDir, "openspec/specs");
|
|
461
|
+
if (specs.length) tree.push({ name: "specs", kind: "dir", children: specs });
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
if (hasDocs && fs4.existsSync(path3.join(root, "docs"))) {
|
|
465
|
+
const docs = walkFiles(path3.join(root, "docs"), "docs");
|
|
466
|
+
if (docs.length) tree.push({ name: "docs", kind: "dir", children: docs });
|
|
467
|
+
}
|
|
468
|
+
const skip = /* @__PURE__ */ new Set(["openspec", "docs", "node_modules", ".git", "dist", "test-server"]);
|
|
469
|
+
const etc = [];
|
|
470
|
+
try {
|
|
471
|
+
for (const ent of fs4.readdirSync(root, { withFileTypes: true })) {
|
|
472
|
+
if (ent.name.startsWith(".") || skip.has(ent.name)) continue;
|
|
473
|
+
const ext = path3.extname(ent.name).toLowerCase();
|
|
474
|
+
if (ent.isFile() && (ext === ".md" || ext === ".markdown")) etc.push({ name: ent.name, kind: "file", path: ent.name });
|
|
475
|
+
}
|
|
476
|
+
} catch (e) {
|
|
477
|
+
console.error("[zdashboard] scan root etc failed:", e);
|
|
478
|
+
}
|
|
479
|
+
etc.sort((a, b) => a.name.localeCompare(b.name));
|
|
480
|
+
if (etc.length) tree.push({ name: `\u5176\u4ED6 (${etc.length})`, kind: "dir", children: etc });
|
|
481
|
+
return tree;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// src/core/tree.ts
|
|
485
|
+
var apply = {
|
|
486
|
+
inject: ["server"],
|
|
487
|
+
apply(ctx, config) {
|
|
488
|
+
let cached = null;
|
|
489
|
+
const getDet = async () => {
|
|
490
|
+
if (cached?.v) return cached.v;
|
|
491
|
+
if (!cached) cached = { p: detect(config.root).catch(() => ({ hasOpenspec: false, hasDocs: false, hasJust: false, hasBugs: false })) };
|
|
492
|
+
const d = await cached.p;
|
|
493
|
+
cached.v = d;
|
|
494
|
+
return d;
|
|
495
|
+
};
|
|
496
|
+
const server = ctx.server;
|
|
497
|
+
if (server?.route) {
|
|
498
|
+
server.route("/__files", async (_req, res) => {
|
|
499
|
+
try {
|
|
500
|
+
const d = await getDet();
|
|
501
|
+
const tree = scanTree(config.root, d.hasOpenspec, d.hasDocs);
|
|
502
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
|
|
503
|
+
res.end(JSON.stringify({ tree, ...d }));
|
|
504
|
+
} catch {
|
|
505
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
|
|
506
|
+
res.end(JSON.stringify({ tree: [], hasOpenspec: false, hasDocs: false, hasJust: false, hasBugs: false }));
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
// src/core/manifest.ts
|
|
514
|
+
import { Service as Service3 } from "cordis";
|
|
515
|
+
var DashboardService = class extends Service3 {
|
|
516
|
+
static inject = ["server"];
|
|
517
|
+
plugins = /* @__PURE__ */ new Map();
|
|
518
|
+
constructor(ctx) {
|
|
519
|
+
super(ctx, "dashboard");
|
|
520
|
+
this.ctx.effect(() => () => this.plugins.clear());
|
|
521
|
+
this.ctx.server.route("/__plugins", (_req, res) => {
|
|
522
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
|
|
523
|
+
res.end(JSON.stringify({ plugins: Array.from(this.plugins.values()) }));
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
register(manifest) {
|
|
527
|
+
this.plugins.set(manifest.mode, manifest);
|
|
528
|
+
this.ctx.effect(() => () => this.plugins.delete(manifest.mode));
|
|
529
|
+
}
|
|
530
|
+
list() {
|
|
531
|
+
return Array.from(this.plugins.values());
|
|
532
|
+
}
|
|
533
|
+
get(mode) {
|
|
534
|
+
return this.plugins.get(mode);
|
|
535
|
+
}
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
// src/server/just-runner.ts
|
|
539
|
+
import { spawn, execFile as execFile2 } from "child_process";
|
|
540
|
+
var MAX_BUFFER = 1e3;
|
|
541
|
+
var JustRunner = class {
|
|
542
|
+
cwd;
|
|
543
|
+
child = null;
|
|
544
|
+
recipe = null;
|
|
545
|
+
state = "idle";
|
|
546
|
+
code = null;
|
|
547
|
+
buffer = [];
|
|
548
|
+
pending = "";
|
|
549
|
+
// 行缓冲:块缓冲输出(如 maven)的 chunk 会在行中间断开,攒到 \n 才切行
|
|
550
|
+
clients = /* @__PURE__ */ new Set();
|
|
551
|
+
recipesCache = null;
|
|
552
|
+
constructor(cwd) {
|
|
553
|
+
this.cwd = cwd;
|
|
554
|
+
}
|
|
555
|
+
recipes() {
|
|
556
|
+
if (this.recipesCache) return Promise.resolve(this.recipesCache);
|
|
557
|
+
return new Promise((resolve) => {
|
|
558
|
+
execFile2("just", ["--list", "--unsorted"], { cwd: this.cwd, maxBuffer: 1 << 20, timeout: 8e3 }, (err, stdout) => {
|
|
559
|
+
if (err) {
|
|
560
|
+
resolve([]);
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
const out = [];
|
|
564
|
+
const seen = /* @__PURE__ */ new Set();
|
|
565
|
+
for (const line of stdout.split(/\r?\n/).slice(1)) {
|
|
566
|
+
const trimmed = line.trim();
|
|
567
|
+
if (!trimmed) continue;
|
|
568
|
+
const hashIdx = trimmed.indexOf("#");
|
|
569
|
+
const sig = (hashIdx >= 0 ? trimmed.slice(0, hashIdx) : trimmed).trim();
|
|
570
|
+
if (!sig) continue;
|
|
571
|
+
const name = sig.split(/\s+/)[0];
|
|
572
|
+
if (seen.has(name)) continue;
|
|
573
|
+
seen.add(name);
|
|
574
|
+
out.push({ name, description: hashIdx >= 0 ? trimmed.slice(hashIdx + 1).trim() : "" });
|
|
575
|
+
}
|
|
576
|
+
this.recipesCache = out;
|
|
577
|
+
resolve(out);
|
|
578
|
+
});
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
subscribe(fn) {
|
|
582
|
+
this.clients.add(fn);
|
|
583
|
+
for (const text of this.buffer) fn({ type: "log", text });
|
|
584
|
+
fn({ type: "state", state: this.state, recipe: this.recipe, code: this.code });
|
|
585
|
+
return () => this.clients.delete(fn);
|
|
586
|
+
}
|
|
587
|
+
emit(ev) {
|
|
588
|
+
for (const fn of this.clients) fn(ev);
|
|
589
|
+
}
|
|
590
|
+
info() {
|
|
591
|
+
return { state: this.state, recipe: this.recipe, code: this.code };
|
|
136
592
|
}
|
|
137
593
|
/** 启动 recipe(调用方须先用 recipes() 校验名字);自动停旧进程 */
|
|
138
594
|
start(recipe) {
|
|
@@ -169,711 +625,408 @@ var JustRunner = class {
|
|
|
169
625
|
child.stdout?.on("data", push);
|
|
170
626
|
child.stderr?.on("data", push);
|
|
171
627
|
child.on("error", (err) => {
|
|
172
|
-
this.pushLine(`[zdashboard] spawn error: ${err.message}
|
|
173
|
-
`);
|
|
174
|
-
});
|
|
175
|
-
child.on("exit", (code) => {
|
|
176
|
-
if (this.pending) {
|
|
177
|
-
this.pushLine(this.pending + "\n");
|
|
178
|
-
this.pending = "";
|
|
179
|
-
}
|
|
180
|
-
this.child = null;
|
|
181
|
-
this.state = "exited";
|
|
182
|
-
this.code = code ?? 0;
|
|
183
|
-
this.emit({ type: "state", state: "exited", recipe: this.recipe, code: this.code });
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
pushLine(line) {
|
|
187
|
-
this.buffer.push(line);
|
|
188
|
-
if (this.buffer.length > MAX_BUFFER) this.buffer.shift();
|
|
189
|
-
this.emit({ type: "log", text: line });
|
|
190
|
-
}
|
|
191
|
-
stop() {
|
|
192
|
-
this.killChild();
|
|
193
|
-
}
|
|
194
|
-
restart(recipe) {
|
|
195
|
-
const target = recipe ?? this.recipe;
|
|
196
|
-
if (target) this.start(target);
|
|
197
|
-
}
|
|
198
|
-
killChild() {
|
|
199
|
-
const child = this.child;
|
|
200
|
-
if (child?.pid) {
|
|
201
|
-
try {
|
|
202
|
-
if (process.platform === "win32") spawn("taskkill", ["/PID", String(child.pid), "/T", "/F"]);
|
|
203
|
-
else child.kill("SIGTERM");
|
|
204
|
-
} catch {
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
this.child = null;
|
|
208
|
-
}
|
|
209
|
-
};
|
|
210
|
-
|
|
211
|
-
// src/server/bugs.ts
|
|
212
|
-
import fs2 from "fs";
|
|
213
|
-
import path2 from "path";
|
|
214
|
-
|
|
215
|
-
// src/server/api/fetch.ts
|
|
216
|
-
import ky from "ky";
|
|
217
|
-
|
|
218
|
-
// src/server/errors.ts
|
|
219
|
-
var HttpError = class extends Error {
|
|
220
|
-
constructor(status, message, body) {
|
|
221
|
-
super(message);
|
|
222
|
-
this.status = status;
|
|
223
|
-
this.body = body;
|
|
224
|
-
this.name = "HttpError";
|
|
225
|
-
}
|
|
226
|
-
status;
|
|
227
|
-
body;
|
|
228
|
-
};
|
|
229
|
-
var NetworkError = class extends Error {
|
|
230
|
-
constructor(message, cause) {
|
|
231
|
-
super(message);
|
|
232
|
-
this.cause = cause;
|
|
233
|
-
this.name = "NetworkError";
|
|
234
|
-
}
|
|
235
|
-
cause;
|
|
236
|
-
};
|
|
237
|
-
|
|
238
|
-
// src/server/api/fetch.ts
|
|
239
|
-
async function fetchJson(url, init) {
|
|
240
|
-
try {
|
|
241
|
-
const res = await ky(url, { ...init, timeout: 8e3, retry: 2 });
|
|
242
|
-
return await res.json();
|
|
243
|
-
} catch (e) {
|
|
244
|
-
if (e instanceof HttpError) throw e;
|
|
245
|
-
if (e instanceof Error && e.name === "TimeoutError") {
|
|
246
|
-
throw new NetworkError(`\u8BF7\u6C42\u8D85\u65F6: ${url}`);
|
|
247
|
-
}
|
|
248
|
-
if (e instanceof Error && e.name === "HTTPError") {
|
|
249
|
-
throw new HttpError(e.status ?? 500, e.message);
|
|
250
|
-
}
|
|
251
|
-
throw new NetworkError(`\u8BF7\u6C42\u5931\u8D25: ${url}`, e);
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// src/server/bugs.ts
|
|
256
|
-
function loadZgoalConfig(root2) {
|
|
257
|
-
const file = path2.join(root2, ".zgoal", "config.yaml");
|
|
258
|
-
if (!fs2.existsSync(file)) return null;
|
|
259
|
-
const kv = {};
|
|
260
|
-
for (const line of fs2.readFileSync(file, "utf8").split("\n")) {
|
|
261
|
-
const m = line.match(/^\s*([A-Za-z_]\w*)\s*:\s*(.+?)\s*$/);
|
|
262
|
-
if (m && !m[2].startsWith("#")) kv[m[1]] = m[2].replace(/^["']|["']$/g, "");
|
|
263
|
-
}
|
|
264
|
-
const product = Number(kv.product);
|
|
265
|
-
if (!kv.url || !product) return null;
|
|
266
|
-
return {
|
|
267
|
-
url: kv.url.replace(/\/+$/, ""),
|
|
268
|
-
account: kv.account ?? "",
|
|
269
|
-
password: kv.password,
|
|
270
|
-
token: kv.token,
|
|
271
|
-
product
|
|
272
|
-
};
|
|
273
|
-
}
|
|
274
|
-
var tokenCache = null;
|
|
275
|
-
async function getToken(cfg) {
|
|
276
|
-
if (cfg.token) return cfg.token;
|
|
277
|
-
const key = `${cfg.url}|${cfg.account}|${cfg.password ?? ""}`;
|
|
278
|
-
if (tokenCache && tokenCache.key === key && Date.now() - tokenCache.at < 10 * 6e4) return tokenCache.token;
|
|
279
|
-
const json = await fetchJson(`${cfg.url}/api.php/v1/tokens`, {
|
|
280
|
-
method: "POST",
|
|
281
|
-
headers: { "Content-Type": "application/json" },
|
|
282
|
-
body: JSON.stringify({ account: cfg.account, password: cfg.password })
|
|
283
|
-
});
|
|
284
|
-
const token = typeof json.token === "string" ? json.token : "";
|
|
285
|
-
if (!token) throw new Error("token \u83B7\u53D6\u5931\u8D25:\u68C0\u67E5 account / password");
|
|
286
|
-
tokenCache = { key, token, at: Date.now() };
|
|
287
|
-
return token;
|
|
288
|
-
}
|
|
289
|
-
function normBug(b, account) {
|
|
290
|
-
const assigned = b.assignedTo;
|
|
291
|
-
const assignedTo = typeof assigned === "string" ? assigned : assigned && typeof assigned === "object" && "realname" in assigned ? String(assigned.realname ?? "") : "";
|
|
292
|
-
const assignedAccount = typeof assigned === "string" ? assigned : assigned && typeof assigned === "object" ? String(assigned.account ?? "") : "";
|
|
293
|
-
const mine = !!account && (assignedAccount === account || assignedTo === account);
|
|
294
|
-
return {
|
|
295
|
-
id: Number(b.id),
|
|
296
|
-
title: String(b.title ?? ""),
|
|
297
|
-
severity: b.severity ?? 4,
|
|
298
|
-
pri: b.pri ?? 3,
|
|
299
|
-
status: String(b.status ?? ""),
|
|
300
|
-
assignedTo,
|
|
301
|
-
openedBy: typeof b.openedBy === "string" ? b.openedBy : void 0,
|
|
302
|
-
mine
|
|
303
|
-
};
|
|
304
|
-
}
|
|
305
|
-
async function fetchBugs(root2) {
|
|
306
|
-
const cfg = loadZgoalConfig(root2);
|
|
307
|
-
if (!cfg) return { ok: false, error: ".zgoal/config.yaml \u7F3A\u5931\u6216 url/product \u672A\u914D\u7F6E(\u7531 zgoal skill \u521B\u5EFA)" };
|
|
308
|
-
try {
|
|
309
|
-
const token = await getToken(cfg);
|
|
310
|
-
const json = await fetchJson(
|
|
311
|
-
`${cfg.url}/api.php/v1/products/${cfg.product}/bugs?page=1&limit=100`,
|
|
312
|
-
{ headers: { Token: token } }
|
|
313
|
-
);
|
|
314
|
-
const raw = Array.isArray(json.bugs) ? json.bugs : [];
|
|
315
|
-
return { ok: true, url: cfg.url, total: Number(json.total ?? raw.length), bugs: raw.map((b) => normBug(b, cfg.account)) };
|
|
316
|
-
} catch (e) {
|
|
317
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
318
|
-
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` };
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
// src/server/plugins.ts
|
|
323
|
-
import fs3 from "fs";
|
|
324
|
-
import path3 from "path";
|
|
325
|
-
import { fileURLToPath } from "url";
|
|
326
|
-
var __dirname = path3.dirname(fileURLToPath(import.meta.url));
|
|
327
|
-
var builtinPlugins = /* @__PURE__ */ new Map();
|
|
328
|
-
function registerBuiltin(plugin) {
|
|
329
|
-
builtinPlugins.set(plugin.mode, plugin);
|
|
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
|
-
]
|
|
628
|
+
this.pushLine(`[zdashboard] spawn error: ${err.message}
|
|
629
|
+
`);
|
|
630
|
+
});
|
|
631
|
+
child.on("exit", (code) => {
|
|
632
|
+
if (this.pending) {
|
|
633
|
+
this.pushLine(this.pending + "\n");
|
|
634
|
+
this.pending = "";
|
|
635
|
+
}
|
|
636
|
+
this.child = null;
|
|
637
|
+
this.state = "exited";
|
|
638
|
+
this.code = code ?? 0;
|
|
639
|
+
this.emit({ type: "state", state: "exited", recipe: this.recipe, code: this.code });
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
pushLine(line) {
|
|
643
|
+
this.buffer.push(line);
|
|
644
|
+
if (this.buffer.length > MAX_BUFFER) this.buffer.shift();
|
|
645
|
+
this.emit({ type: "log", text: line });
|
|
646
|
+
}
|
|
647
|
+
stop() {
|
|
648
|
+
this.killChild();
|
|
649
|
+
}
|
|
650
|
+
restart(recipe) {
|
|
651
|
+
const target = recipe ?? this.recipe;
|
|
652
|
+
if (target) this.start(target);
|
|
653
|
+
}
|
|
654
|
+
killChild() {
|
|
655
|
+
const child = this.child;
|
|
656
|
+
if (child?.pid) {
|
|
657
|
+
try {
|
|
658
|
+
if (process.platform === "win32") spawn("taskkill", ["/PID", String(child.pid), "/T", "/F"]);
|
|
659
|
+
else child.kill("SIGTERM");
|
|
660
|
+
} catch {
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
this.child = null;
|
|
522
664
|
}
|
|
523
665
|
};
|
|
524
666
|
|
|
525
|
-
// src/
|
|
526
|
-
var
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
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) => {
|
|
667
|
+
// src/plugins/just/index.ts
|
|
668
|
+
var apply2 = {
|
|
669
|
+
inject: ["server", "dashboard"],
|
|
670
|
+
apply(ctx, config) {
|
|
671
|
+
const root = config.root;
|
|
672
|
+
ctx.inject(["server"], () => {
|
|
673
|
+
if (!ctx.server?.route) return;
|
|
674
|
+
const runner = new JustRunner(root);
|
|
675
|
+
ctx.effect(() => () => runner.stop());
|
|
676
|
+
ctx.server.route("/__just/recipes", async (_req, res) => {
|
|
611
677
|
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
678
|
+
try {
|
|
679
|
+
const recipes = await runner.recipes();
|
|
680
|
+
res.end(JSON.stringify(recipes));
|
|
681
|
+
} catch {
|
|
682
|
+
res.end(JSON.stringify([]));
|
|
683
|
+
}
|
|
684
|
+
});
|
|
685
|
+
ctx.server.sse("/__just/logs", (res) => {
|
|
686
|
+
const unsub = runner.subscribe((ev) => {
|
|
687
|
+
res.write(`data: ${JSON.stringify(ev)}
|
|
688
|
+
|
|
689
|
+
`);
|
|
690
|
+
});
|
|
691
|
+
return unsub;
|
|
692
|
+
});
|
|
693
|
+
ctx.server.route("/__just/start", async (req, res) => {
|
|
694
|
+
if (req.headers["x-stop-token"] !== ctx.server.stopToken) {
|
|
616
695
|
res.writeHead(403);
|
|
617
696
|
res.end("forbidden");
|
|
618
697
|
return;
|
|
619
698
|
}
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
699
|
+
const body = await readBody(req);
|
|
700
|
+
let recipe;
|
|
701
|
+
try {
|
|
702
|
+
recipe = JSON.parse(body || "{}").recipe;
|
|
703
|
+
} catch {
|
|
704
|
+
}
|
|
705
|
+
const target = recipe ?? runner.info().recipe;
|
|
706
|
+
if (!target) {
|
|
707
|
+
res.writeHead(400);
|
|
708
|
+
res.end('{"error":"no recipe"}');
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
const recipes = await runner.recipes();
|
|
712
|
+
if (!recipes.some((r) => r.name === target)) {
|
|
713
|
+
res.writeHead(403);
|
|
714
|
+
res.end('{"error":"unknown recipe"}');
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
runner.start(target);
|
|
718
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
|
|
719
|
+
res.end(JSON.stringify(runner.info()));
|
|
720
|
+
});
|
|
721
|
+
ctx.server.route("/__just/stop", async (req, res) => {
|
|
722
|
+
if (req.headers["x-stop-token"] !== ctx.server.stopToken) {
|
|
635
723
|
res.writeHead(403);
|
|
636
724
|
res.end("forbidden");
|
|
637
725
|
return;
|
|
638
726
|
}
|
|
639
|
-
(
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
727
|
+
runner.stop();
|
|
728
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
|
|
729
|
+
res.end(JSON.stringify(runner.info()));
|
|
730
|
+
});
|
|
731
|
+
ctx.server.route("/__just/restart", async (req, res) => {
|
|
732
|
+
if (req.headers["x-stop-token"] !== ctx.server.stopToken) {
|
|
733
|
+
res.writeHead(403);
|
|
734
|
+
res.end("forbidden");
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
const body = await readBody(req);
|
|
738
|
+
let recipe;
|
|
739
|
+
try {
|
|
740
|
+
recipe = JSON.parse(body || "{}").recipe;
|
|
741
|
+
} catch {
|
|
742
|
+
}
|
|
743
|
+
const target = recipe ?? runner.info().recipe;
|
|
744
|
+
if (!target) {
|
|
745
|
+
res.writeHead(400);
|
|
746
|
+
res.end('{"error":"no recipe"}');
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
const recipes = await runner.recipes();
|
|
750
|
+
if (!recipes.some((r) => r.name === target)) {
|
|
751
|
+
res.writeHead(403);
|
|
752
|
+
res.end('{"error":"unknown recipe"}');
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
runner.restart(target);
|
|
756
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
|
|
757
|
+
res.end(JSON.stringify(runner.info()));
|
|
758
|
+
});
|
|
759
|
+
ctx.dashboard.register({ mode: "just", label: "Just Runner", icon: "\u{1F4DC}", description: "Just \u4EFB\u52A1\u65E5\u5FD7\u4E0E\u6267\u884C" });
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
};
|
|
763
|
+
async function readBody(req) {
|
|
764
|
+
return new Promise((resolve) => {
|
|
765
|
+
let data = "";
|
|
766
|
+
req.on("data", (c) => {
|
|
767
|
+
data += c;
|
|
768
|
+
});
|
|
769
|
+
req.on("end", () => resolve(data));
|
|
657
770
|
});
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// src/server/bugs.ts
|
|
774
|
+
import fs5 from "fs";
|
|
775
|
+
import path4 from "path";
|
|
776
|
+
|
|
777
|
+
// src/server/api/fetch.ts
|
|
778
|
+
import ky from "ky";
|
|
779
|
+
|
|
780
|
+
// src/server/errors.ts
|
|
781
|
+
var HttpError = class extends Error {
|
|
782
|
+
constructor(status, message, body) {
|
|
783
|
+
super(message);
|
|
784
|
+
this.status = status;
|
|
785
|
+
this.body = body;
|
|
786
|
+
this.name = "HttpError";
|
|
787
|
+
}
|
|
788
|
+
status;
|
|
789
|
+
body;
|
|
790
|
+
};
|
|
791
|
+
var NetworkError = class extends Error {
|
|
792
|
+
constructor(message, cause) {
|
|
793
|
+
super(message);
|
|
794
|
+
this.cause = cause;
|
|
795
|
+
this.name = "NetworkError";
|
|
796
|
+
}
|
|
797
|
+
cause;
|
|
798
|
+
};
|
|
799
|
+
|
|
800
|
+
// src/server/api/fetch.ts
|
|
801
|
+
async function fetchJson(url, init) {
|
|
802
|
+
try {
|
|
803
|
+
const res = await ky(url, { ...init, timeout: 8e3, retry: 2 });
|
|
804
|
+
return await res.json();
|
|
805
|
+
} catch (e) {
|
|
806
|
+
if (e instanceof HttpError) throw e;
|
|
807
|
+
if (e instanceof Error && e.name === "TimeoutError") {
|
|
808
|
+
throw new NetworkError(`\u8BF7\u6C42\u8D85\u65F6: ${url}`);
|
|
809
|
+
}
|
|
810
|
+
if (e instanceof Error && e.name === "HTTPError") {
|
|
811
|
+
throw new HttpError(e.status ?? 500, e.message);
|
|
688
812
|
}
|
|
813
|
+
throw new NetworkError(`\u8BF7\u6C42\u5931\u8D25: ${url}`, e);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// src/server/bugs.ts
|
|
818
|
+
function loadZgoalConfig(root) {
|
|
819
|
+
const file = path4.join(root, ".zgoal", "config.yaml");
|
|
820
|
+
if (!fs5.existsSync(file)) return null;
|
|
821
|
+
const kv = {};
|
|
822
|
+
for (const line of fs5.readFileSync(file, "utf8").split("\n")) {
|
|
823
|
+
const m = line.match(/^\s*([A-Za-z_]\w*)\s*:\s*(.+?)\s*$/);
|
|
824
|
+
if (m && !m[2].startsWith("#")) kv[m[1]] = m[2].replace(/^["']|["']$/g, "");
|
|
825
|
+
}
|
|
826
|
+
const product = Number(kv.product);
|
|
827
|
+
if (!kv.url || !product) return null;
|
|
828
|
+
return {
|
|
829
|
+
url: kv.url.replace(/\/+$/, ""),
|
|
830
|
+
account: kv.account ?? "",
|
|
831
|
+
password: kv.password,
|
|
832
|
+
token: kv.token,
|
|
833
|
+
product
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
var tokenCache = null;
|
|
837
|
+
async function getToken(cfg) {
|
|
838
|
+
if (cfg.token) return cfg.token;
|
|
839
|
+
const key = `${cfg.url}|${cfg.account}|${cfg.password ?? ""}`;
|
|
840
|
+
if (tokenCache && tokenCache.key === key && Date.now() - tokenCache.at < 10 * 6e4) return tokenCache.token;
|
|
841
|
+
const json = await fetchJson(`${cfg.url}/api.php/v1/tokens`, {
|
|
842
|
+
method: "POST",
|
|
843
|
+
headers: { "Content-Type": "application/json" },
|
|
844
|
+
body: JSON.stringify({ account: cfg.account, password: cfg.password })
|
|
689
845
|
});
|
|
690
|
-
const
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
846
|
+
const token = typeof json.token === "string" ? json.token : "";
|
|
847
|
+
if (!token) throw new Error("token \u83B7\u53D6\u5931\u8D25:\u68C0\u67E5 account / password");
|
|
848
|
+
tokenCache = { key, token, at: Date.now() };
|
|
849
|
+
return token;
|
|
850
|
+
}
|
|
851
|
+
function normBug(b, account) {
|
|
852
|
+
const assigned = b.assignedTo;
|
|
853
|
+
const assignedTo = typeof assigned === "string" ? assigned : assigned && typeof assigned === "object" && "realname" in assigned ? String(assigned.realname ?? "") : "";
|
|
854
|
+
const assignedAccount = typeof assigned === "string" ? assigned : assigned && typeof assigned === "object" ? String(assigned.account ?? "") : "";
|
|
855
|
+
const mine = !!account && (assignedAccount === account || assignedTo === account);
|
|
856
|
+
return {
|
|
857
|
+
id: Number(b.id),
|
|
858
|
+
title: String(b.title ?? ""),
|
|
859
|
+
severity: b.severity ?? 4,
|
|
860
|
+
pri: b.pri ?? 3,
|
|
861
|
+
status: String(b.status ?? ""),
|
|
862
|
+
assignedTo,
|
|
863
|
+
openedBy: typeof b.openedBy === "string" ? b.openedBy : void 0,
|
|
864
|
+
mine
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
async function fetchBugs(root) {
|
|
868
|
+
const cfg = loadZgoalConfig(root);
|
|
869
|
+
if (!cfg) return { ok: false, error: ".zgoal/config.yaml \u7F3A\u5931\u6216 url/product \u672A\u914D\u7F6E(\u7531 zgoal skill \u521B\u5EFA)" };
|
|
870
|
+
try {
|
|
871
|
+
const token = await getToken(cfg);
|
|
872
|
+
const json = await fetchJson(
|
|
873
|
+
`${cfg.url}/api.php/v1/products/${cfg.product}/bugs?page=1&limit=100`,
|
|
874
|
+
{ headers: { Token: token } }
|
|
875
|
+
);
|
|
876
|
+
const raw = Array.isArray(json.bugs) ? json.bugs : [];
|
|
877
|
+
return { ok: true, url: cfg.url, total: Number(json.total ?? raw.length), bugs: raw.map((b) => normBug(b, cfg.account)) };
|
|
878
|
+
} catch (e) {
|
|
879
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
880
|
+
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` };
|
|
881
|
+
}
|
|
882
|
+
}
|
|
694
883
|
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
res.end(body);
|
|
884
|
+
// src/plugins/bugs/index.ts
|
|
885
|
+
var apply3 = {
|
|
886
|
+
inject: ["server", "dashboard"],
|
|
887
|
+
apply(ctx, config) {
|
|
888
|
+
const root = config.root;
|
|
889
|
+
ctx.inject(["server"], () => {
|
|
890
|
+
if (!ctx.server?.route) return;
|
|
891
|
+
ctx.dashboard.register({ mode: "bugs", label: "\u7985\u9053 Bugs", icon: "\u{1F3AF}", description: "\u53EA\u8BFB bug \u5217\u8868" });
|
|
892
|
+
ctx.server.route("/__bugs", async (_req, res) => {
|
|
893
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
|
|
894
|
+
try {
|
|
895
|
+
const result = await fetchBugs(root);
|
|
896
|
+
res.end(JSON.stringify(result));
|
|
897
|
+
} catch (e) {
|
|
898
|
+
res.end(JSON.stringify({ ok: false, error: e instanceof Error ? e.message : "unknown error" }));
|
|
899
|
+
}
|
|
900
|
+
});
|
|
713
901
|
});
|
|
714
902
|
}
|
|
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;
|
|
903
|
+
};
|
|
904
|
+
|
|
905
|
+
// src/server/review-store.ts
|
|
906
|
+
import fs6 from "fs";
|
|
907
|
+
import path5 from "path";
|
|
908
|
+
import YAML from "yaml";
|
|
909
|
+
var REVIEW_FILE = "review.yaml";
|
|
910
|
+
var ReviewStore = class {
|
|
911
|
+
root;
|
|
912
|
+
file;
|
|
913
|
+
onChange;
|
|
914
|
+
constructor(root, onChange) {
|
|
915
|
+
this.root = root;
|
|
916
|
+
this.file = path5.join(root, REVIEW_FILE);
|
|
917
|
+
this.onChange = onChange;
|
|
918
|
+
}
|
|
919
|
+
exists() {
|
|
920
|
+
return fs6.existsSync(this.file);
|
|
921
|
+
}
|
|
922
|
+
read() {
|
|
923
|
+
try {
|
|
924
|
+
const parsed = YAML.parse(fs6.readFileSync(this.file, "utf8"));
|
|
925
|
+
if (!parsed || !Array.isArray(parsed.items)) return { status: "draft", items: [] };
|
|
926
|
+
return parsed;
|
|
927
|
+
} catch {
|
|
928
|
+
return { status: "draft", items: [] };
|
|
746
929
|
}
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
930
|
+
}
|
|
931
|
+
write(data) {
|
|
932
|
+
fs6.writeFileSync(this.file, YAML.stringify(data), "utf8");
|
|
933
|
+
this.onChange?.();
|
|
934
|
+
}
|
|
935
|
+
updateItem(id, patch) {
|
|
936
|
+
const data = this.read();
|
|
937
|
+
const item = data.items.find((i) => i.id === id);
|
|
938
|
+
if (!item) throw new Error(`item ${id} not found`);
|
|
939
|
+
if (patch.answer !== void 0) item.answer = patch.answer;
|
|
940
|
+
if (patch.state !== void 0) item.state = patch.state;
|
|
941
|
+
if (patch.state === "answered" && !patch.answer && !item.answer) item.answer = "";
|
|
942
|
+
this.write(data);
|
|
943
|
+
return data;
|
|
944
|
+
}
|
|
945
|
+
setStatus(status) {
|
|
946
|
+
const data = this.read();
|
|
947
|
+
if (status === "passed" && data.items.some((i) => i.state === "open")) {
|
|
948
|
+
throw new Error("\u5B58\u5728\u672A\u5904\u7406\u7684\u8BC4\u5BA1\u9879(open),\u4E0D\u80FD\u901A\u8FC7");
|
|
756
949
|
}
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
950
|
+
data.status = status;
|
|
951
|
+
this.write(data);
|
|
952
|
+
return data;
|
|
953
|
+
}
|
|
954
|
+
docs() {
|
|
955
|
+
try {
|
|
956
|
+
return fs6.readdirSync(this.root).filter((f) => /\.(md|markdown)$/i.test(f) && fs6.statSync(path5.join(this.root, f)).isFile()).sort();
|
|
957
|
+
} catch {
|
|
958
|
+
return [];
|
|
761
959
|
}
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
res.write(": connected\n\n");
|
|
765
|
-
const unsub = runner.subscribe((ev) => res.write(`data: ${JSON.stringify(ev)}
|
|
960
|
+
}
|
|
961
|
+
};
|
|
766
962
|
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
const
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
963
|
+
// src/plugins/review/index.ts
|
|
964
|
+
var apply4 = {
|
|
965
|
+
inject: ["server", "dashboard", "reload"],
|
|
966
|
+
apply(ctx, config) {
|
|
967
|
+
const root = config.root;
|
|
968
|
+
ctx.inject(["server"], () => {
|
|
969
|
+
if (!ctx.server?.route) return;
|
|
970
|
+
ctx.dashboard.register({ mode: "review", label: "\u6587\u6863\u8BC4\u5BA1", icon: "\u2705", description: "review.yaml \u9010\u9879\u5BF9\u9F50" });
|
|
971
|
+
const reviewStore = new ReviewStore(root, () => {
|
|
972
|
+
ctx.reload.broadcast("files");
|
|
973
|
+
});
|
|
974
|
+
ctx.server.route("/__review", async (_req, res) => {
|
|
975
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
|
|
976
|
+
res.end(JSON.stringify(reviewStore.read()));
|
|
977
|
+
});
|
|
978
|
+
ctx.server.route("/__review/item", async (req, res) => {
|
|
979
|
+
if (req.headers["x-stop-token"] !== ctx.server.stopToken) {
|
|
775
980
|
res.writeHead(403);
|
|
776
981
|
res.end("forbidden");
|
|
777
982
|
return;
|
|
778
983
|
}
|
|
779
|
-
const body = await readBody(req);
|
|
780
|
-
let recipe;
|
|
781
984
|
try {
|
|
782
|
-
|
|
985
|
+
const body = JSON.parse(await readBody2(req) || "{}");
|
|
986
|
+
const data = reviewStore.updateItem(body.id, { answer: body.answer, state: body.state });
|
|
987
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
|
|
988
|
+
res.end(JSON.stringify(data));
|
|
783
989
|
} 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();
|
|
990
|
+
res.writeHead(400, { "Content-Type": "application/json; charset=utf-8" });
|
|
991
|
+
res.end(JSON.stringify({ error: e instanceof Error ? e.message : "unknown error" }));
|
|
803
992
|
}
|
|
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);
|
|
993
|
+
});
|
|
994
|
+
ctx.server.route("/__review/status", async (req, res) => {
|
|
995
|
+
if (req.headers["x-stop-token"] !== ctx.server.stopToken) {
|
|
996
|
+
res.writeHead(403);
|
|
997
|
+
res.end("forbidden");
|
|
816
998
|
return;
|
|
817
999
|
}
|
|
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}`);
|
|
1000
|
+
try {
|
|
1001
|
+
const body = JSON.parse(await readBody2(req) || "{}");
|
|
1002
|
+
const data = reviewStore.setStatus(body.status);
|
|
1003
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
|
|
1004
|
+
res.end(JSON.stringify(data));
|
|
1005
|
+
} catch (e) {
|
|
1006
|
+
res.writeHead(400, { "Content-Type": "application/json; charset=utf-8" });
|
|
1007
|
+
res.end(JSON.stringify({ error: e instanceof Error ? e.message : "unknown error" }));
|
|
1008
|
+
}
|
|
1009
|
+
});
|
|
1010
|
+
ctx.server.route("/__docs", async (_req, res) => {
|
|
1011
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
|
|
1012
|
+
res.end(JSON.stringify(reviewStore.docs()));
|
|
1013
|
+
});
|
|
860
1014
|
});
|
|
861
1015
|
}
|
|
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);
|
|
1016
|
+
};
|
|
1017
|
+
async function readBody2(req) {
|
|
1018
|
+
return new Promise((resolve) => {
|
|
1019
|
+
let data = "";
|
|
1020
|
+
req.on("data", (c) => {
|
|
1021
|
+
data += c;
|
|
871
1022
|
});
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
}
|
|
875
|
-
start(PORT0);
|
|
1023
|
+
req.on("end", () => resolve(data));
|
|
1024
|
+
});
|
|
876
1025
|
}
|
|
1026
|
+
|
|
1027
|
+
// src/plugins/apply/scan.ts
|
|
1028
|
+
import fs7 from "fs";
|
|
1029
|
+
import path6 from "path";
|
|
877
1030
|
function countTasks(md) {
|
|
878
1031
|
const all = (md.match(/^\s*-\s*\[[ xX]\]\s*/gm) || []).length;
|
|
879
1032
|
const done = (md.match(/^\s*-\s*\[[xX]\]\s*/gm) || []).length;
|
|
@@ -881,73 +1034,277 @@ function countTasks(md) {
|
|
|
881
1034
|
}
|
|
882
1035
|
function readText(p) {
|
|
883
1036
|
try {
|
|
884
|
-
return
|
|
1037
|
+
return fs7.readFileSync(p, "utf8");
|
|
885
1038
|
} catch {
|
|
886
1039
|
return "";
|
|
887
1040
|
}
|
|
888
1041
|
}
|
|
889
|
-
function scanApplyChanges(
|
|
890
|
-
const changesDir = path6.join(
|
|
891
|
-
if (!
|
|
1042
|
+
function scanApplyChanges(root) {
|
|
1043
|
+
const changesDir = path6.join(root, "openspec", "changes");
|
|
1044
|
+
if (!fs7.existsSync(changesDir)) return [];
|
|
892
1045
|
const out = [];
|
|
893
|
-
for (const ent of
|
|
1046
|
+
for (const ent of fs7.readdirSync(changesDir, { withFileTypes: true })) {
|
|
894
1047
|
if (!ent.isDirectory() || ent.name.startsWith(".") || ent.name === "archive") continue;
|
|
895
1048
|
const dir = path6.join(changesDir, ent.name);
|
|
896
1049
|
const tasks = readText(path6.join(dir, "tasks.md"));
|
|
897
1050
|
const { total, done } = countTasks(tasks);
|
|
898
|
-
out.push({
|
|
1051
|
+
out.push({
|
|
1052
|
+
name: ent.name,
|
|
1053
|
+
path: `openspec/changes/${ent.name}`,
|
|
1054
|
+
total,
|
|
1055
|
+
done,
|
|
1056
|
+
hasProposal: fs7.existsSync(path6.join(dir, "proposal.md")),
|
|
1057
|
+
hasDesign: fs7.existsSync(path6.join(dir, "design.md"))
|
|
1058
|
+
});
|
|
899
1059
|
}
|
|
900
1060
|
out.sort((a, b) => a.name.localeCompare(b.name));
|
|
901
1061
|
return out;
|
|
902
1062
|
}
|
|
903
|
-
function readApplyChange(
|
|
904
|
-
const dir = path6.join(
|
|
1063
|
+
function readApplyChange(root, name) {
|
|
1064
|
+
const dir = path6.join(root, "openspec", "changes", name);
|
|
905
1065
|
const proposal = readText(path6.join(dir, "proposal.md"));
|
|
906
1066
|
const design = readText(path6.join(dir, "design.md"));
|
|
907
1067
|
const tasks = readText(path6.join(dir, "tasks.md"));
|
|
908
1068
|
const { total, done } = countTasks(tasks);
|
|
909
|
-
return {
|
|
1069
|
+
return {
|
|
1070
|
+
name,
|
|
1071
|
+
path: `openspec/changes/${name}`,
|
|
1072
|
+
total,
|
|
1073
|
+
done,
|
|
1074
|
+
hasProposal: !!proposal,
|
|
1075
|
+
hasDesign: !!design,
|
|
1076
|
+
proposal,
|
|
1077
|
+
design,
|
|
1078
|
+
tasks
|
|
1079
|
+
};
|
|
910
1080
|
}
|
|
911
1081
|
|
|
912
|
-
// src/
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
1082
|
+
// src/plugins/apply/index.ts
|
|
1083
|
+
var apply5 = {
|
|
1084
|
+
inject: ["server", "dashboard"],
|
|
1085
|
+
apply(ctx, config) {
|
|
1086
|
+
const root = config.root;
|
|
1087
|
+
ctx.inject(["server"], () => {
|
|
1088
|
+
if (!ctx.server?.route) return;
|
|
1089
|
+
ctx.dashboard.register({ mode: "apply", label: "\u6267\u884C\u8FDB\u5EA6", icon: "\u2699\uFE0F", description: "OpenSpec change \u4EFB\u52A1\u8FDB\u5EA6" });
|
|
1090
|
+
ctx.server.route("/__apply", async (_req, res) => {
|
|
1091
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
|
|
1092
|
+
res.end(JSON.stringify(scanApplyChanges(root)));
|
|
1093
|
+
});
|
|
1094
|
+
ctx.server.route("/__apply/change", async (req, res) => {
|
|
1095
|
+
const url = new URL(req.url || "", "http://x");
|
|
1096
|
+
const name = url.searchParams.get("name");
|
|
1097
|
+
if (!name) {
|
|
1098
|
+
res.writeHead(400);
|
|
1099
|
+
res.end(JSON.stringify({ error: "missing name" }));
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
if (name.includes("..") || name.includes("/") || name.includes("\\")) {
|
|
1103
|
+
res.writeHead(400);
|
|
1104
|
+
res.end(JSON.stringify({ error: "invalid name" }));
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
try {
|
|
1108
|
+
const data = readApplyChange(root, name);
|
|
1109
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
|
|
1110
|
+
res.end(JSON.stringify(data));
|
|
1111
|
+
} catch (e) {
|
|
1112
|
+
res.writeHead(400);
|
|
1113
|
+
res.end(JSON.stringify({ error: e instanceof Error ? e.message : "unknown error" }));
|
|
1114
|
+
}
|
|
1115
|
+
});
|
|
920
1116
|
});
|
|
921
|
-
|
|
922
|
-
|
|
1117
|
+
}
|
|
1118
|
+
};
|
|
1119
|
+
|
|
1120
|
+
// src/server/design-assets.ts
|
|
1121
|
+
import fs8 from "fs";
|
|
1122
|
+
import path7 from "path";
|
|
1123
|
+
var PAGE_EXTS = [".html", ".htm"];
|
|
1124
|
+
var ICON_EXTS = [".svg", ".png", ".ico", ".jpg", ".jpeg", ".gif", ".webp"];
|
|
1125
|
+
var VIDEO_EXTS = [".mp4", ".webm", ".mov", ".ogg", ".ogv"];
|
|
1126
|
+
var AUDIO_EXTS = [".mp3", ".wav", ".flac", ".aac", ".m4a"];
|
|
1127
|
+
var CODE_EXTS = [".js", ".mjs", ".ts", ".tsx", ".jsx", ".css", ".json", ".txt", ".xml", ".yml", ".yaml", ".sh", ".md"];
|
|
1128
|
+
var FONT_EXTS = [".woff", ".woff2", ".ttf", ".otf"];
|
|
1129
|
+
var TOKEN_RE = /token|theme|design|color|palette|typograph/i;
|
|
1130
|
+
function categorize(rel, ext) {
|
|
1131
|
+
if (rel.indexOf("components/") === 0) return "component";
|
|
1132
|
+
if (VIDEO_EXTS.includes(ext)) return "video";
|
|
1133
|
+
if (AUDIO_EXTS.includes(ext)) return "audio";
|
|
1134
|
+
if (ext === ".pdf") return "pdf";
|
|
1135
|
+
if (ext === ".md") return "md";
|
|
1136
|
+
if (FONT_EXTS.includes(ext)) return "font";
|
|
1137
|
+
if (ICON_EXTS.includes(ext)) return "icon";
|
|
1138
|
+
if (PAGE_EXTS.includes(ext)) return "page";
|
|
1139
|
+
if (CODE_EXTS.includes(ext)) return TOKEN_RE.test(rel) && (ext === ".css" || ext === ".json") ? "token" : "code";
|
|
1140
|
+
return "other";
|
|
923
1141
|
}
|
|
924
|
-
|
|
925
|
-
const
|
|
926
|
-
const
|
|
927
|
-
const
|
|
928
|
-
const
|
|
929
|
-
|
|
1142
|
+
function scanAssets(root) {
|
|
1143
|
+
const out = {};
|
|
1144
|
+
const keys = ["page", "component", "icon", "token", "md", "video", "audio", "pdf", "code", "font", "other"];
|
|
1145
|
+
for (const k of keys) out[k] = [];
|
|
1146
|
+
const SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", "coverage", ".next", ".cache"]);
|
|
1147
|
+
function walk(dir, rel) {
|
|
1148
|
+
let ents;
|
|
1149
|
+
try {
|
|
1150
|
+
ents = fs8.readdirSync(dir, { withFileTypes: true });
|
|
1151
|
+
} catch {
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1154
|
+
for (const ent of ents) {
|
|
1155
|
+
if (ent.name.startsWith(".") || SKIP.has(ent.name)) continue;
|
|
1156
|
+
const r = rel ? `${rel}/${ent.name}` : ent.name;
|
|
1157
|
+
if (ent.isDirectory()) {
|
|
1158
|
+
walk(path7.join(dir, ent.name), r);
|
|
1159
|
+
continue;
|
|
1160
|
+
}
|
|
1161
|
+
const ext = path7.extname(ent.name).toLowerCase();
|
|
1162
|
+
const t = categorize(r, ext);
|
|
1163
|
+
out[t].push({ path: r, name: ent.name, ext, type: t });
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
walk(root, "");
|
|
1167
|
+
return out;
|
|
930
1168
|
}
|
|
931
1169
|
|
|
1170
|
+
// src/plugins/design/index.ts
|
|
1171
|
+
var apply6 = {
|
|
1172
|
+
inject: ["server", "dashboard"],
|
|
1173
|
+
apply(ctx, config) {
|
|
1174
|
+
const root = config.root;
|
|
1175
|
+
ctx.inject(["server"], () => {
|
|
1176
|
+
if (!ctx.server?.route) return;
|
|
1177
|
+
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" });
|
|
1178
|
+
ctx.server.route("/__design/assets", async (_req, res) => {
|
|
1179
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-cache" });
|
|
1180
|
+
res.end(JSON.stringify(scanAssets(root)));
|
|
1181
|
+
});
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1184
|
+
};
|
|
1185
|
+
|
|
1186
|
+
// src/plugins/view/index.ts
|
|
1187
|
+
var apply7 = {
|
|
1188
|
+
inject: ["dashboard"],
|
|
1189
|
+
apply(ctx) {
|
|
1190
|
+
ctx.dashboard.register({ mode: "view", label: "\u9879\u76EE\u6D4F\u89C8", icon: "\u{1F441}\uFE0F", description: "openspec / docs / \u6587\u6863\u9884\u89C8" });
|
|
1191
|
+
}
|
|
1192
|
+
};
|
|
1193
|
+
|
|
932
1194
|
// src/cli.ts
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
1195
|
+
var __dirname2 = path8.dirname(fileURLToPath2(import.meta.url));
|
|
1196
|
+
function parseArgs() {
|
|
1197
|
+
const args = process.argv.slice(2);
|
|
1198
|
+
const opts = {};
|
|
1199
|
+
for (let i = 0; i < args.length; i++) {
|
|
1200
|
+
const a = args[i];
|
|
1201
|
+
if (a.startsWith("--")) {
|
|
1202
|
+
const key = a.slice(2);
|
|
1203
|
+
const next = args[i + 1];
|
|
1204
|
+
if (next && !next.startsWith("--")) {
|
|
1205
|
+
opts[key] = next;
|
|
1206
|
+
i++;
|
|
1207
|
+
} else {
|
|
1208
|
+
opts[key] = true;
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
return {
|
|
1213
|
+
dir: typeof opts.dir === "string" ? opts.dir : process.cwd(),
|
|
1214
|
+
port: typeof opts.port === "string" ? Number(opts.port) : 4190,
|
|
1215
|
+
open: !!opts.open,
|
|
1216
|
+
page: typeof opts.page === "string" ? opts.page : null,
|
|
1217
|
+
plugins: typeof opts.plugins === "string" ? opts.plugins : null
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
async function pathExists(p) {
|
|
1221
|
+
try {
|
|
1222
|
+
await access(p);
|
|
1223
|
+
return true;
|
|
1224
|
+
} catch {
|
|
1225
|
+
return false;
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
async function loadExternal(ctx, dir, root) {
|
|
1229
|
+
if (!dir) return;
|
|
1230
|
+
let tsxLoaded = false;
|
|
1231
|
+
try {
|
|
1232
|
+
const { register } = await import("tsx/esm/api");
|
|
1233
|
+
register();
|
|
1234
|
+
tsxLoaded = true;
|
|
1235
|
+
} catch {
|
|
1236
|
+
}
|
|
1237
|
+
try {
|
|
1238
|
+
const entries = await pathExists(dir) ? await readdir(dir, { withFileTypes: true }) : [];
|
|
1239
|
+
for (const ent of entries) {
|
|
1240
|
+
if (!ent.isDirectory()) continue;
|
|
1241
|
+
const candidates = tsxLoaded ? ["index.ts", "index.js", "index.mjs"] : ["index.js", "index.mjs"];
|
|
1242
|
+
for (const name of candidates) {
|
|
1243
|
+
const p = path8.join(dir, ent.name, name);
|
|
1244
|
+
if (await pathExists(p)) {
|
|
1245
|
+
try {
|
|
1246
|
+
const mod = await import(p);
|
|
1247
|
+
const plugin = mod.default ?? mod;
|
|
1248
|
+
if (plugin?.apply) {
|
|
1249
|
+
try {
|
|
1250
|
+
await ctx.plugin(plugin, { root });
|
|
1251
|
+
} catch (e) {
|
|
1252
|
+
console.error(`[zdashboard] failed to apply plugin ${ent.name}:`, e);
|
|
1253
|
+
break;
|
|
1254
|
+
}
|
|
1255
|
+
const m = ctx.dashboard.get(ent.name);
|
|
1256
|
+
if (m && m.mode === ent.name) {
|
|
1257
|
+
const patch = { external: true };
|
|
1258
|
+
const webDir = path8.join(dir, ent.name, "web");
|
|
1259
|
+
if (await pathExists(path8.join(webDir, "index.html"))) {
|
|
1260
|
+
ctx.server.static(`${PLUGIN_STATIC_PREFIX}${ent.name}/`, webDir);
|
|
1261
|
+
if (!m.viewerUrl) patch.viewerUrl = `${PLUGIN_STATIC_PREFIX}${ent.name}/`;
|
|
1262
|
+
}
|
|
1263
|
+
ctx.dashboard.register({ ...m, ...patch });
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
} catch (e) {
|
|
1267
|
+
console.error(`[zdashboard] failed to load plugin ${ent.name}:`, e);
|
|
1268
|
+
}
|
|
1269
|
+
break;
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
} catch {
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
async function main() {
|
|
1277
|
+
const args = parseArgs();
|
|
1278
|
+
const root = path8.resolve(args.dir);
|
|
1279
|
+
const appDir = path8.resolve(__dirname2, "web");
|
|
1280
|
+
const det = await detect(root);
|
|
1281
|
+
const ctx = new Context();
|
|
1282
|
+
ctx.plugin(ServerService, { root, appDir, port: args.port, open: args.open, detect: det, page: args.page });
|
|
1283
|
+
ctx.plugin(ReloadService, { root });
|
|
1284
|
+
ctx.plugin(apply, { root });
|
|
1285
|
+
ctx.plugin(DashboardService);
|
|
1286
|
+
const plugins = [
|
|
1287
|
+
{ name: "just", apply: apply2 },
|
|
1288
|
+
{ name: "bugs", apply: apply3 },
|
|
1289
|
+
{ name: "review", apply: apply4 },
|
|
1290
|
+
{ name: "apply", apply: apply5 },
|
|
1291
|
+
{ name: "design", apply: apply6 },
|
|
1292
|
+
{ name: "view", apply: apply7 }
|
|
1293
|
+
];
|
|
1294
|
+
for (const p of plugins) {
|
|
1295
|
+
try {
|
|
1296
|
+
ctx.plugin(p.apply, { root });
|
|
1297
|
+
} catch (e) {
|
|
1298
|
+
console.error(`[zdashboard] plugin ${p.name} failed:`, e);
|
|
939
1299
|
}
|
|
940
1300
|
}
|
|
941
|
-
|
|
1301
|
+
if (args.plugins) {
|
|
1302
|
+
await loadExternal(ctx, path8.resolve(args.plugins), root);
|
|
1303
|
+
}
|
|
1304
|
+
return ctx;
|
|
942
1305
|
}
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
createServer({
|
|
947
|
-
root,
|
|
948
|
-
port: args.port ? parseInt(args.port, 10) : void 0,
|
|
949
|
-
open: !!args.open,
|
|
950
|
-
detect: det,
|
|
951
|
-
mode: args.mode
|
|
1306
|
+
await main().catch((e) => {
|
|
1307
|
+
console.error(e);
|
|
1308
|
+
process.exit(1);
|
|
952
1309
|
});
|
|
953
1310
|
//# sourceMappingURL=cli.js.map
|