@demon-utils/playwright 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/demon-demo-init.js +56 -0
- package/dist/bin/demon-demo-init.js.map +10 -0
- package/dist/bin/demon-demo-review.js +331 -185
- package/dist/bin/demon-demo-review.js.map +7 -6
- package/dist/bin/demoon.js +1445 -0
- package/dist/bin/demoon.js.map +22 -0
- package/dist/bin/review-template.html +62 -0
- package/dist/github-issue.js +749 -0
- package/dist/github-issue.js.map +16 -0
- package/dist/index.js +1537 -236
- package/dist/index.js.map +16 -5
- package/dist/orchestrator.js +1421 -0
- package/dist/orchestrator.js.map +20 -0
- package/dist/review-generator.js +424 -0
- package/dist/review-generator.js.map +12 -0
- package/dist/review-template.html +62 -0
- package/package.json +11 -2
- package/src/bin/demon-demo-init.ts +59 -0
- package/src/bin/demon-demo-review.ts +19 -32
- package/src/bin/demoon.ts +140 -0
- package/src/feedback-server.ts +138 -0
- package/src/git-context.test.ts +156 -0
- package/src/git-context.ts +101 -0
- package/src/github-issue.test.ts +188 -0
- package/src/github-issue.ts +139 -0
- package/src/html-generator.e2e.test.ts +630 -0
- package/src/index.ts +17 -5
- package/src/orchestrator.test.ts +183 -0
- package/src/orchestrator.ts +341 -0
- package/src/recorder.test.ts +161 -0
- package/src/recorder.ts +74 -0
- package/src/review-generator.ts +221 -0
- package/src/review-types.ts +22 -1
- package/src/review.test.ts +257 -59
- package/src/review.ts +160 -38
- package/src/html-generator.test.ts +0 -195
- package/src/html-generator.ts +0 -152
|
@@ -2,37 +2,98 @@
|
|
|
2
2
|
// @bun
|
|
3
3
|
|
|
4
4
|
// src/bin/demon-demo-review.ts
|
|
5
|
-
import { existsSync
|
|
6
|
-
import { resolve
|
|
5
|
+
import { existsSync as existsSync2, statSync } from "fs";
|
|
6
|
+
import { resolve } from "path";
|
|
7
|
+
|
|
8
|
+
// src/review-generator.ts
|
|
9
|
+
import { existsSync, readdirSync, readFileSync as readFileSync2, writeFileSync } from "node:fs";
|
|
10
|
+
import { join, dirname, relative } from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
7
12
|
|
|
8
13
|
// src/review.ts
|
|
9
|
-
|
|
10
|
-
|
|
14
|
+
import { spawn } from "node:child_process";
|
|
15
|
+
import { Readable } from "node:stream";
|
|
16
|
+
var GIT_DIFF_MAX_CHARS = 50000;
|
|
17
|
+
function buildReviewPrompt(options) {
|
|
18
|
+
const { filenames, stepsMap, gitDiff, guidelines } = options;
|
|
19
|
+
const demoEntries = filenames.map((f) => {
|
|
20
|
+
const steps = stepsMap[f] ?? [];
|
|
21
|
+
const stepLines = steps.map((s) => `- [${s.timestampSeconds}s] ${s.text}`).join(`
|
|
11
22
|
`);
|
|
12
|
-
|
|
23
|
+
return `Video: ${f}
|
|
24
|
+
Recorded steps:
|
|
25
|
+
${stepLines || "(no steps recorded)"}`;
|
|
26
|
+
});
|
|
27
|
+
const sections = [];
|
|
28
|
+
if (guidelines && guidelines.length > 0) {
|
|
29
|
+
sections.push(`## Coding Guidelines
|
|
30
|
+
|
|
31
|
+
${guidelines.join(`
|
|
32
|
+
|
|
33
|
+
`)}`);
|
|
34
|
+
}
|
|
35
|
+
if (gitDiff) {
|
|
36
|
+
let diff = gitDiff;
|
|
37
|
+
if (diff.length > GIT_DIFF_MAX_CHARS) {
|
|
38
|
+
diff = diff.slice(0, GIT_DIFF_MAX_CHARS) + `
|
|
39
|
+
|
|
40
|
+
... (diff truncated at 50k characters)`;
|
|
41
|
+
}
|
|
42
|
+
sections.push(`## Git Diff
|
|
43
|
+
|
|
44
|
+
\`\`\`diff
|
|
45
|
+
${diff}
|
|
46
|
+
\`\`\``);
|
|
47
|
+
}
|
|
48
|
+
sections.push(`## Demo Recordings
|
|
49
|
+
|
|
50
|
+
${demoEntries.join(`
|
|
13
51
|
|
|
14
|
-
|
|
52
|
+
`)}`);
|
|
53
|
+
return `You are a code reviewer. You are given a git diff, coding guidelines, and demo recordings that show the feature in action.
|
|
15
54
|
|
|
16
|
-
|
|
55
|
+
${sections.join(`
|
|
56
|
+
|
|
57
|
+
`)}
|
|
58
|
+
|
|
59
|
+
## Task
|
|
60
|
+
|
|
61
|
+
Review the code changes and demo recordings. Generate a JSON object matching this exact schema:
|
|
17
62
|
|
|
18
63
|
{
|
|
19
64
|
"demos": [
|
|
20
65
|
{
|
|
21
66
|
"file": "<filename>",
|
|
22
|
-
"summary": "<a
|
|
23
|
-
"annotations": [
|
|
24
|
-
{ "timestampSeconds": <number>, "text": "<annotation text>" }
|
|
25
|
-
]
|
|
67
|
+
"summary": "<a meaningful sentence describing what this demo showcases based on the steps>"
|
|
26
68
|
}
|
|
27
|
-
]
|
|
69
|
+
],
|
|
70
|
+
"review": {
|
|
71
|
+
"summary": "<2-3 sentence overview of the changes>",
|
|
72
|
+
"highlights": ["<positive aspect 1>", "<positive aspect 2>"],
|
|
73
|
+
"verdict": "approve" | "request_changes",
|
|
74
|
+
"verdictReason": "<one sentence justifying the verdict>",
|
|
75
|
+
"issues": [
|
|
76
|
+
{
|
|
77
|
+
"severity": "major" | "minor" | "nit",
|
|
78
|
+
"description": "<what the issue is and how to fix it>"
|
|
79
|
+
}
|
|
80
|
+
]
|
|
81
|
+
}
|
|
28
82
|
}
|
|
29
83
|
|
|
30
84
|
Rules:
|
|
31
85
|
- Return ONLY the JSON object, no markdown fences or extra text.
|
|
32
86
|
- Include one entry in "demos" for each filename, in the same order.
|
|
33
|
-
-
|
|
34
|
-
-
|
|
35
|
-
- "
|
|
87
|
+
- "file" must exactly match the provided filename.
|
|
88
|
+
- "verdict" must be exactly "approve" or "request_changes".
|
|
89
|
+
- Use "request_changes" if there are any "major" issues.
|
|
90
|
+
- "severity" must be exactly "major", "minor", or "nit".
|
|
91
|
+
- "major": bugs, security issues, broken functionality, guideline violations.
|
|
92
|
+
- "minor": code quality, readability, missing edge cases.
|
|
93
|
+
- "nit": style, naming, trivial improvements.
|
|
94
|
+
- "highlights" must have at least one entry.
|
|
95
|
+
- "issues" can be an empty array if there are no issues.
|
|
96
|
+
- Verify that demo steps demonstrate the acceptance criteria being met.`;
|
|
36
97
|
}
|
|
37
98
|
async function invokeClaude(prompt, options) {
|
|
38
99
|
const spawnFn = options?.spawn ?? defaultSpawn;
|
|
@@ -53,10 +114,25 @@ async function invokeClaude(prompt, options) {
|
|
|
53
114
|
}
|
|
54
115
|
return output.trim();
|
|
55
116
|
}
|
|
56
|
-
|
|
117
|
+
var VALID_VERDICTS = new Set(["approve", "request_changes"]);
|
|
118
|
+
var VALID_SEVERITIES = new Set(["major", "minor", "nit"]);
|
|
119
|
+
function extractJson(raw) {
|
|
120
|
+
try {
|
|
121
|
+
JSON.parse(raw);
|
|
122
|
+
return raw;
|
|
123
|
+
} catch {}
|
|
124
|
+
const start = raw.indexOf("{");
|
|
125
|
+
const end = raw.lastIndexOf("}");
|
|
126
|
+
if (start === -1 || end === -1 || end <= start) {
|
|
127
|
+
throw new Error(`No JSON object found in LLM response: ${raw.slice(0, 200)}`);
|
|
128
|
+
}
|
|
129
|
+
return raw.slice(start, end + 1);
|
|
130
|
+
}
|
|
131
|
+
function parseLlmResponse(raw) {
|
|
132
|
+
const jsonStr = extractJson(raw);
|
|
57
133
|
let parsed;
|
|
58
134
|
try {
|
|
59
|
-
parsed = JSON.parse(
|
|
135
|
+
parsed = JSON.parse(jsonStr);
|
|
60
136
|
} catch {
|
|
61
137
|
throw new Error(`Invalid JSON from LLM: ${raw.slice(0, 200)}`);
|
|
62
138
|
}
|
|
@@ -78,34 +154,58 @@ function parseReviewMetadata(raw) {
|
|
|
78
154
|
if (typeof d["summary"] !== "string") {
|
|
79
155
|
throw new Error("Each demo must have a 'summary' string");
|
|
80
156
|
}
|
|
81
|
-
|
|
82
|
-
|
|
157
|
+
}
|
|
158
|
+
if (typeof obj["review"] !== "object" || obj["review"] === null) {
|
|
159
|
+
throw new Error("Missing 'review' object in response");
|
|
160
|
+
}
|
|
161
|
+
const review = obj["review"];
|
|
162
|
+
if (typeof review["summary"] !== "string") {
|
|
163
|
+
throw new Error("review.summary must be a string");
|
|
164
|
+
}
|
|
165
|
+
if (!Array.isArray(review["highlights"])) {
|
|
166
|
+
throw new Error("review.highlights must be an array");
|
|
167
|
+
}
|
|
168
|
+
if (review["highlights"].length === 0) {
|
|
169
|
+
throw new Error("review.highlights must not be empty");
|
|
170
|
+
}
|
|
171
|
+
for (const h of review["highlights"]) {
|
|
172
|
+
if (typeof h !== "string") {
|
|
173
|
+
throw new Error("Each highlight must be a string");
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (typeof review["verdict"] !== "string" || !VALID_VERDICTS.has(review["verdict"])) {
|
|
177
|
+
throw new Error("review.verdict must be 'approve' or 'request_changes'");
|
|
178
|
+
}
|
|
179
|
+
if (typeof review["verdictReason"] !== "string") {
|
|
180
|
+
throw new Error("review.verdictReason must be a string");
|
|
181
|
+
}
|
|
182
|
+
if (!Array.isArray(review["issues"])) {
|
|
183
|
+
throw new Error("review.issues must be an array");
|
|
184
|
+
}
|
|
185
|
+
for (const issue of review["issues"]) {
|
|
186
|
+
if (typeof issue !== "object" || issue === null) {
|
|
187
|
+
throw new Error("Each issue must be an object");
|
|
83
188
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
throw new Error("Each annotation must have a 'timestampSeconds' number");
|
|
91
|
-
}
|
|
92
|
-
if (typeof a["text"] !== "string") {
|
|
93
|
-
throw new Error("Each annotation must have a 'text' string");
|
|
94
|
-
}
|
|
189
|
+
const i = issue;
|
|
190
|
+
if (typeof i["severity"] !== "string" || !VALID_SEVERITIES.has(i["severity"])) {
|
|
191
|
+
throw new Error("Each issue severity must be 'major', 'minor', or 'nit'");
|
|
192
|
+
}
|
|
193
|
+
if (typeof i["description"] !== "string") {
|
|
194
|
+
throw new Error("Each issue must have a 'description' string");
|
|
95
195
|
}
|
|
96
196
|
}
|
|
97
197
|
return parsed;
|
|
98
198
|
}
|
|
99
199
|
function defaultSpawn(cmd) {
|
|
100
200
|
const [command, ...args] = cmd;
|
|
101
|
-
const proc =
|
|
102
|
-
|
|
103
|
-
stderr: "pipe"
|
|
201
|
+
const proc = spawn(command, args, {
|
|
202
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
104
203
|
});
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
204
|
+
const exitCode = new Promise((resolve) => {
|
|
205
|
+
proc.on("close", (code) => resolve(code ?? 1));
|
|
206
|
+
});
|
|
207
|
+
const stdout = Readable.toWeb(proc.stdout);
|
|
208
|
+
return { exitCode, stdout };
|
|
109
209
|
}
|
|
110
210
|
function concatUint8Arrays(arrays) {
|
|
111
211
|
const totalLength = arrays.reduce((sum, a) => sum + a.length, 0);
|
|
@@ -118,186 +218,232 @@ function concatUint8Arrays(arrays) {
|
|
|
118
218
|
return result;
|
|
119
219
|
}
|
|
120
220
|
|
|
121
|
-
// src/
|
|
221
|
+
// src/git-context.ts
|
|
222
|
+
import { readFileSync } from "node:fs";
|
|
223
|
+
import { spawnSync } from "node:child_process";
|
|
224
|
+
async function detectDefaultBase(exec, gitRoot) {
|
|
225
|
+
let currentBranch;
|
|
226
|
+
try {
|
|
227
|
+
currentBranch = (await exec(["git", "rev-parse", "--abbrev-ref", "HEAD"], gitRoot)).trim();
|
|
228
|
+
} catch {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
if (currentBranch === "main" || currentBranch === "master") {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
for (const candidate of ["main", "master"]) {
|
|
235
|
+
try {
|
|
236
|
+
await exec(["git", "rev-parse", "--verify", candidate], gitRoot);
|
|
237
|
+
return candidate;
|
|
238
|
+
} catch {}
|
|
239
|
+
}
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
var defaultExec = async (cmd, cwd) => {
|
|
243
|
+
const [command, ...args] = cmd;
|
|
244
|
+
const proc = spawnSync(command, args, { cwd, encoding: "utf-8" });
|
|
245
|
+
if (proc.status !== 0) {
|
|
246
|
+
const stderr = (proc.stderr ?? "").trim();
|
|
247
|
+
throw new Error(`Command failed (exit ${proc.status}): ${cmd.join(" ")}${stderr ? `: ${stderr}` : ""}`);
|
|
248
|
+
}
|
|
249
|
+
return proc.stdout ?? "";
|
|
250
|
+
};
|
|
251
|
+
var defaultReadFile = (path) => {
|
|
252
|
+
return readFileSync(path, "utf-8");
|
|
253
|
+
};
|
|
254
|
+
async function getRepoContext(demosDir, options) {
|
|
255
|
+
const exec = options?.exec ?? defaultExec;
|
|
256
|
+
const readFile = options?.readFile ?? defaultReadFile;
|
|
257
|
+
const gitRoot = (await exec(["git", "rev-parse", "--show-toplevel"], demosDir)).trim();
|
|
258
|
+
const diffBase = options?.diffBase ?? await detectDefaultBase(exec, gitRoot);
|
|
259
|
+
let gitDiff;
|
|
260
|
+
if (diffBase) {
|
|
261
|
+
gitDiff = (await exec(["git", "diff", `${diffBase}...HEAD`], gitRoot)).trim();
|
|
262
|
+
} else {
|
|
263
|
+
const workingDiff = (await exec(["git", "diff", "HEAD"], gitRoot)).trim();
|
|
264
|
+
if (workingDiff.length > 0) {
|
|
265
|
+
gitDiff = workingDiff;
|
|
266
|
+
} else {
|
|
267
|
+
gitDiff = (await exec(["git", "diff", "HEAD~1..HEAD"], gitRoot)).trim();
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
const lsOutput = (await exec(["git", "ls-files"], gitRoot)).trim();
|
|
271
|
+
const files = lsOutput.split(`
|
|
272
|
+
`).filter((f) => f.length > 0);
|
|
273
|
+
const guidelinePatterns = ["CLAUDE.md", "SKILL.md"];
|
|
274
|
+
const guidelines = [];
|
|
275
|
+
for (const file of files) {
|
|
276
|
+
const basename = file.split("/").pop() ?? "";
|
|
277
|
+
if (guidelinePatterns.includes(basename)) {
|
|
278
|
+
const fullPath = `${gitRoot}/${file}`;
|
|
279
|
+
const content = readFile(fullPath);
|
|
280
|
+
guidelines.push(`# ${file}
|
|
281
|
+
${content}`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return { gitDiff, guidelines };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// src/review-generator.ts
|
|
288
|
+
function getReviewTemplate() {
|
|
289
|
+
const currentFile = fileURLToPath(import.meta.url);
|
|
290
|
+
const distDir = dirname(currentFile);
|
|
291
|
+
const templatePath = join(distDir, "review-template.html");
|
|
292
|
+
if (!existsSync(templatePath)) {
|
|
293
|
+
throw new Error(`Review template not found at ${templatePath}. ` + `Make sure to build the review-app package first.`);
|
|
294
|
+
}
|
|
295
|
+
return readFileSync2(templatePath, "utf-8");
|
|
296
|
+
}
|
|
122
297
|
function escapeHtml(s) {
|
|
123
298
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
124
299
|
}
|
|
125
|
-
function
|
|
126
|
-
|
|
300
|
+
function generateReviewHtml(appData) {
|
|
301
|
+
const template = getReviewTemplate();
|
|
302
|
+
const jsonData = JSON.stringify(appData);
|
|
303
|
+
return template.replace("<title>Demo Review</title>", `<title>${escapeHtml(appData.title)}</title>`).replace('"{{__INJECT_REVIEW_DATA__}}"', jsonData);
|
|
127
304
|
}
|
|
128
|
-
function
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
body { font-family: system-ui, -apple-system, sans-serif; background: #1a1a2e; color: #e0e0e0; min-height: 100vh; }
|
|
149
|
-
header { padding: 1rem 2rem; background: #16213e; border-bottom: 1px solid #0f3460; }
|
|
150
|
-
header h1 { font-size: 1.4rem; color: #e94560; }
|
|
151
|
-
.review-layout { display: flex; height: calc(100vh - 60px); }
|
|
152
|
-
.video-panel { flex: 4; padding: 1rem; display: flex; align-items: center; justify-content: center; background: #0f0f23; }
|
|
153
|
-
.video-panel video { width: 100%; max-height: 100%; border-radius: 4px; }
|
|
154
|
-
.side-panel { flex: 1; min-width: 260px; max-width: 360px; padding: 1rem; overflow-y: auto; background: #16213e; border-left: 1px solid #0f3460; }
|
|
155
|
-
.side-panel h2 { font-size: 1rem; margin-bottom: 0.5rem; color: #e94560; }
|
|
156
|
-
.side-panel section { margin-bottom: 1.5rem; }
|
|
157
|
-
#demo-list { list-style: none; }
|
|
158
|
-
#demo-list li { margin-bottom: 0.25rem; }
|
|
159
|
-
#demo-list button { width: 100%; text-align: left; padding: 0.4rem 0.6rem; background: #1a1a2e; color: #e0e0e0; border: 1px solid #0f3460; border-radius: 4px; cursor: pointer; font-size: 0.85rem; }
|
|
160
|
-
#demo-list button:hover { background: #0f3460; }
|
|
161
|
-
#demo-list button.active { background: #e94560; color: #fff; border-color: #e94560; }
|
|
162
|
-
#summary-text { font-size: 0.9rem; line-height: 1.5; color: #ccc; }
|
|
163
|
-
#annotations-list { list-style: none; }
|
|
164
|
-
#annotations-list li { margin-bottom: 0.3rem; }
|
|
165
|
-
#annotations-list button { width: 100%; text-align: left; padding: 0.3rem 0.5rem; background: transparent; color: #53a8b6; border: none; cursor: pointer; font-size: 0.85rem; }
|
|
166
|
-
#annotations-list button:hover { color: #e94560; text-decoration: underline; }
|
|
167
|
-
.timestamp { font-weight: bold; margin-right: 0.4rem; color: #e94560; }
|
|
168
|
-
</style>
|
|
169
|
-
</head>
|
|
170
|
-
<body>
|
|
171
|
-
<header>
|
|
172
|
-
<h1>${escapeHtml(title)}</h1>
|
|
173
|
-
</header>
|
|
174
|
-
<main class="review-layout">
|
|
175
|
-
<div class="video-panel">
|
|
176
|
-
<video id="review-video" controls src="${escapeAttr(firstDemo.file)}"></video>
|
|
177
|
-
</div>
|
|
178
|
-
<div class="side-panel">
|
|
179
|
-
<section>
|
|
180
|
-
<h2>Demos</h2>
|
|
181
|
-
<ul id="demo-list">
|
|
182
|
-
${demoButtons}
|
|
183
|
-
</ul>
|
|
184
|
-
</section>
|
|
185
|
-
<section>
|
|
186
|
-
<h2>Summary</h2>
|
|
187
|
-
<p id="summary-text"></p>
|
|
188
|
-
</section>
|
|
189
|
-
<section>
|
|
190
|
-
<h2>Annotations</h2>
|
|
191
|
-
<ul id="annotations-list"></ul>
|
|
192
|
-
</section>
|
|
193
|
-
</div>
|
|
194
|
-
</main>
|
|
195
|
-
<script>
|
|
196
|
-
(function() {
|
|
197
|
-
var metadata = ${metadataJson};
|
|
198
|
-
var video = document.getElementById("review-video");
|
|
199
|
-
var summaryText = document.getElementById("summary-text");
|
|
200
|
-
var annotationsList = document.getElementById("annotations-list");
|
|
201
|
-
var demoButtons = document.querySelectorAll("#demo-list button");
|
|
202
|
-
|
|
203
|
-
function esc(s) {
|
|
204
|
-
var d = document.createElement("div");
|
|
205
|
-
d.appendChild(document.createTextNode(s));
|
|
206
|
-
return d.innerHTML;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
function formatTime(seconds) {
|
|
210
|
-
var m = Math.floor(seconds / 60);
|
|
211
|
-
var s = Math.floor(seconds % 60);
|
|
212
|
-
return m + ":" + (s < 10 ? "0" : "") + s;
|
|
305
|
+
function discoverDemoFiles(directory) {
|
|
306
|
+
const files = [];
|
|
307
|
+
const processFile = (filePath, filename) => {
|
|
308
|
+
const relativePath = relative(directory, filePath);
|
|
309
|
+
if (filename.endsWith(".webm")) {
|
|
310
|
+
files.push({ path: filePath, filename, relativePath, type: "web-ux" });
|
|
311
|
+
} else if (filename.endsWith(".jsonl")) {
|
|
312
|
+
files.push({ path: filePath, filename, relativePath, type: "log-based" });
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
for (const f of readdirSync(directory)) {
|
|
316
|
+
processFile(join(directory, f), f);
|
|
317
|
+
}
|
|
318
|
+
if (files.length === 0) {
|
|
319
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
320
|
+
if (!entry.isDirectory())
|
|
321
|
+
continue;
|
|
322
|
+
const subdir = join(directory, entry.name);
|
|
323
|
+
for (const f of readdirSync(subdir)) {
|
|
324
|
+
processFile(join(subdir, f), f);
|
|
213
325
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return files.sort((a, b) => a.filename.localeCompare(b.filename));
|
|
329
|
+
}
|
|
330
|
+
async function generateReview(options) {
|
|
331
|
+
const { directory, agent, feedbackEndpoint, title = "Demo Review", diffBase } = options;
|
|
332
|
+
const demoFiles = discoverDemoFiles(directory);
|
|
333
|
+
const webUxDemos = demoFiles.filter((d) => d.type === "web-ux");
|
|
334
|
+
const logBasedDemos = demoFiles.filter((d) => d.type === "log-based");
|
|
335
|
+
if (demoFiles.length === 0) {
|
|
336
|
+
throw new Error(`No .webm or .jsonl files found in "${directory}" or its subdirectories.`);
|
|
337
|
+
}
|
|
338
|
+
const filenameToRelativePath = new Map(demoFiles.map((d) => [d.filename, d.relativePath]));
|
|
339
|
+
const stepsMapByFilename = {};
|
|
340
|
+
const stepsMapByRelativePath = {};
|
|
341
|
+
for (const demo of webUxDemos) {
|
|
342
|
+
const stepsPath = join(dirname(demo.path), "demo-steps.json");
|
|
343
|
+
if (!existsSync(stepsPath))
|
|
344
|
+
continue;
|
|
345
|
+
try {
|
|
346
|
+
const raw = readFileSync2(stepsPath, "utf-8");
|
|
347
|
+
const parsed = JSON.parse(raw);
|
|
348
|
+
if (Array.isArray(parsed)) {
|
|
349
|
+
stepsMapByFilename[demo.filename] = parsed;
|
|
350
|
+
stepsMapByRelativePath[demo.relativePath] = parsed;
|
|
232
351
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
352
|
+
} catch {}
|
|
353
|
+
}
|
|
354
|
+
const logsMap = {};
|
|
355
|
+
for (const demo of logBasedDemos) {
|
|
356
|
+
logsMap[demo.relativePath] = readFileSync2(demo.path, "utf-8");
|
|
357
|
+
}
|
|
358
|
+
const hasWebUxDemos = webUxDemos.length > 0;
|
|
359
|
+
const hasLogDemos = logBasedDemos.length > 0;
|
|
360
|
+
if (hasWebUxDemos && Object.keys(stepsMapByFilename).length === 0) {
|
|
361
|
+
throw new Error("No demo-steps.json found alongside any .webm files. " + "Use DemoRecorder in your demo tests to generate step data.");
|
|
362
|
+
}
|
|
363
|
+
if (!hasWebUxDemos && !hasLogDemos) {
|
|
364
|
+
throw new Error("No demo files found.");
|
|
365
|
+
}
|
|
366
|
+
let gitDiff;
|
|
367
|
+
let guidelines;
|
|
368
|
+
try {
|
|
369
|
+
const repoContext = await getRepoContext(directory, { diffBase });
|
|
370
|
+
gitDiff = repoContext.gitDiff;
|
|
371
|
+
guidelines = repoContext.guidelines;
|
|
372
|
+
} catch {}
|
|
373
|
+
const allFilenames = demoFiles.map((d) => d.filename);
|
|
374
|
+
const prompt = buildReviewPrompt({ filenames: allFilenames, stepsMap: stepsMapByFilename, gitDiff, guidelines });
|
|
375
|
+
const rawOutput = await invokeClaude(prompt, { agent });
|
|
376
|
+
const llmResponse = parseLlmResponse(rawOutput);
|
|
377
|
+
const typeMap = new Map(demoFiles.map((d) => [d.filename, d.type]));
|
|
378
|
+
const metadata = {
|
|
379
|
+
demos: llmResponse.demos.map((demo) => {
|
|
380
|
+
const relativePath = filenameToRelativePath.get(demo.file) ?? demo.file;
|
|
381
|
+
return {
|
|
382
|
+
file: relativePath,
|
|
383
|
+
type: typeMap.get(demo.file) ?? "web-ux",
|
|
384
|
+
summary: demo.summary,
|
|
385
|
+
steps: stepsMapByRelativePath[relativePath] ?? []
|
|
386
|
+
};
|
|
387
|
+
}),
|
|
388
|
+
review: llmResponse.review
|
|
389
|
+
};
|
|
390
|
+
const metadataPath = join(directory, "review-metadata.json");
|
|
391
|
+
writeFileSync(metadataPath, JSON.stringify(metadata, null, 2) + `
|
|
392
|
+
`);
|
|
393
|
+
const appData = {
|
|
394
|
+
metadata,
|
|
395
|
+
title,
|
|
396
|
+
videos: {},
|
|
397
|
+
logs: Object.keys(logsMap).length > 0 ? logsMap : undefined,
|
|
398
|
+
feedbackEndpoint
|
|
399
|
+
};
|
|
400
|
+
const html = generateReviewHtml(appData);
|
|
401
|
+
const htmlPath = join(directory, "review.html");
|
|
402
|
+
writeFileSync(htmlPath, html);
|
|
403
|
+
return { htmlPath, metadataPath, metadata };
|
|
253
404
|
}
|
|
254
405
|
|
|
255
406
|
// src/bin/demon-demo-review.ts
|
|
256
407
|
var dir;
|
|
257
408
|
var agent;
|
|
409
|
+
var diffBase;
|
|
258
410
|
var args = process.argv.slice(2);
|
|
259
411
|
for (let i = 0;i < args.length; i++) {
|
|
260
412
|
if (args[i] === "--agent") {
|
|
261
413
|
agent = args[++i];
|
|
414
|
+
} else if (args[i] === "--base") {
|
|
415
|
+
diffBase = args[++i];
|
|
262
416
|
} else if (!dir) {
|
|
263
417
|
dir = args[i];
|
|
264
418
|
}
|
|
265
419
|
}
|
|
266
420
|
if (!dir) {
|
|
267
|
-
console.error("Usage: demon-demo-review [--agent <path>] <directory>");
|
|
268
|
-
console.error(" Discovers .webm
|
|
421
|
+
console.error("Usage: demon-demo-review [--agent <path>] [--base <ref>] <directory>");
|
|
422
|
+
console.error(" Discovers .webm and .jsonl demo files in the given directory.");
|
|
423
|
+
console.error(" --base <ref> Base commit/branch for diff (auto-detects main/master if on feature branch)");
|
|
269
424
|
process.exit(1);
|
|
270
425
|
}
|
|
271
426
|
var resolved = resolve(dir);
|
|
272
|
-
if (!
|
|
427
|
+
if (!existsSync2(resolved) || !statSync(resolved).isDirectory()) {
|
|
273
428
|
console.error(`Error: "${resolved}" is not a valid directory.`);
|
|
274
429
|
process.exit(1);
|
|
275
430
|
}
|
|
276
|
-
var
|
|
277
|
-
if (
|
|
278
|
-
console.error(`Error: No .webm files found in "${resolved}".`);
|
|
431
|
+
var demoFiles = discoverDemoFiles(resolved);
|
|
432
|
+
if (demoFiles.length === 0) {
|
|
433
|
+
console.error(`Error: No .webm or .jsonl files found in "${resolved}" or its subdirectories.`);
|
|
279
434
|
process.exit(1);
|
|
280
435
|
}
|
|
281
|
-
for (const file of
|
|
282
|
-
console.log(file);
|
|
436
|
+
for (const file of demoFiles) {
|
|
437
|
+
console.log(file.path);
|
|
283
438
|
}
|
|
284
439
|
try {
|
|
285
|
-
const basenames = webmFiles.map((f) => basename(f));
|
|
286
|
-
const prompt = buildReviewPrompt(basenames);
|
|
287
440
|
console.log("Invoking claude to generate review metadata...");
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
writeFileSync(outputPath, JSON.stringify(metadata, null, 2) + `
|
|
292
|
-
`);
|
|
293
|
-
console.log(`Review metadata written to ${outputPath}`);
|
|
294
|
-
const html = generateReviewHtml({ metadata });
|
|
295
|
-
const htmlPath = join(resolved, "review.html");
|
|
296
|
-
writeFileSync(htmlPath, html);
|
|
297
|
-
console.log(resolve(htmlPath));
|
|
441
|
+
const result = await generateReview({ directory: resolved, agent, diffBase });
|
|
442
|
+
console.log(`Review metadata written to ${result.metadataPath}`);
|
|
443
|
+
console.log(resolve(result.htmlPath));
|
|
298
444
|
} catch (err) {
|
|
299
|
-
console.error("Error generating review
|
|
445
|
+
console.error("Error generating review:", err instanceof Error ? err.message : err);
|
|
300
446
|
process.exit(1);
|
|
301
447
|
}
|
|
302
448
|
|
|
303
|
-
//# debugId=
|
|
449
|
+
//# debugId=C76A8A3848513E7E64756E2164756E21
|