@demon-utils/playwright 0.1.6 → 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 +187 -523
- package/dist/bin/demon-demo-review.js.map +7 -7
- 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 +1320 -867
- package/dist/index.js.map +16 -8
- 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 -97
- package/src/bin/demoon.ts +140 -0
- package/src/feedback-server.ts +138 -0
- package/src/git-context.test.ts +68 -2
- package/src/git-context.ts +48 -9
- package/src/github-issue.test.ts +188 -0
- package/src/github-issue.ts +139 -0
- package/src/html-generator.e2e.test.ts +361 -80
- package/src/index.ts +9 -3
- package/src/orchestrator.test.ts +183 -0
- package/src/orchestrator.ts +341 -0
- package/src/review-generator.ts +221 -0
- package/src/review-types.ts +3 -0
- package/src/review.ts +13 -7
- package/src/html-generator.test.ts +0 -561
- package/src/html-generator.ts +0 -461
|
@@ -2,10 +2,17 @@
|
|
|
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
|
|
14
|
+
import { spawn } from "node:child_process";
|
|
15
|
+
import { Readable } from "node:stream";
|
|
9
16
|
var GIT_DIFF_MAX_CHARS = 50000;
|
|
10
17
|
function buildReviewPrompt(options) {
|
|
11
18
|
const { filenames, stepsMap, gitDiff, guidelines } = options;
|
|
@@ -191,14 +198,14 @@ function parseLlmResponse(raw) {
|
|
|
191
198
|
}
|
|
192
199
|
function defaultSpawn(cmd) {
|
|
193
200
|
const [command, ...args] = cmd;
|
|
194
|
-
const proc =
|
|
195
|
-
|
|
196
|
-
stderr: "pipe"
|
|
201
|
+
const proc = spawn(command, args, {
|
|
202
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
197
203
|
});
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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 };
|
|
202
209
|
}
|
|
203
210
|
function concatUint8Arrays(arrays) {
|
|
204
211
|
const totalLength = arrays.reduce((sum, a) => sum + a.length, 0);
|
|
@@ -211,449 +218,35 @@ function concatUint8Arrays(arrays) {
|
|
|
211
218
|
return result;
|
|
212
219
|
}
|
|
213
220
|
|
|
214
|
-
// src/html-generator.ts
|
|
215
|
-
function escapeHtml(s) {
|
|
216
|
-
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
217
|
-
}
|
|
218
|
-
function escapeAttr(s) {
|
|
219
|
-
return escapeHtml(s);
|
|
220
|
-
}
|
|
221
|
-
function renderReviewSection(review) {
|
|
222
|
-
const bannerClass = review.verdict === "approve" ? "approve" : "request-changes";
|
|
223
|
-
const verdictLabel = review.verdict === "approve" ? "Approved" : "Changes Requested";
|
|
224
|
-
const highlightsHtml = review.highlights.map((h) => `<li>${escapeHtml(h)}</li>`).join(`
|
|
225
|
-
`);
|
|
226
|
-
const issuesHtml = review.issues.length > 0 ? review.issues.map((issue) => {
|
|
227
|
-
const badgeLabel = issue.severity.toUpperCase();
|
|
228
|
-
return `<div class="issue ${issue.severity}"><span class="severity-badge">${badgeLabel}</span> <span class="issue-text">${escapeHtml(issue.description)}</span><button class="feedback-add-issue" data-issue="${escapeAttr(issue.description)}">+</button></div>`;
|
|
229
|
-
}).join(`
|
|
230
|
-
`) : '<p class="no-issues">No issues found.</p>';
|
|
231
|
-
return `<section class="review-section">
|
|
232
|
-
<div class="verdict-banner ${bannerClass}">
|
|
233
|
-
<strong>${verdictLabel}</strong>: ${escapeHtml(review.verdictReason)}
|
|
234
|
-
</div>
|
|
235
|
-
<div class="review-body">
|
|
236
|
-
<h2>Summary</h2>
|
|
237
|
-
<p>${escapeHtml(review.summary)}</p>
|
|
238
|
-
<h2>Highlights</h2>
|
|
239
|
-
<ul class="highlights-list">
|
|
240
|
-
${highlightsHtml}
|
|
241
|
-
</ul>
|
|
242
|
-
<h2>Issues</h2>
|
|
243
|
-
${issuesHtml}
|
|
244
|
-
</div>
|
|
245
|
-
</section>`;
|
|
246
|
-
}
|
|
247
|
-
function generateReviewHtml(options) {
|
|
248
|
-
const { metadata, title = "Demo Review" } = options;
|
|
249
|
-
if (metadata.demos.length === 0) {
|
|
250
|
-
throw new Error("metadata.demos must not be empty");
|
|
251
|
-
}
|
|
252
|
-
const firstDemo = metadata.demos[0];
|
|
253
|
-
const demoButtons = metadata.demos.map((demo, i) => {
|
|
254
|
-
const activeClass = i === 0 ? ' class="active"' : "";
|
|
255
|
-
return `<li><button data-index="${i}"${activeClass}>${escapeHtml(demo.file)}</button></li>`;
|
|
256
|
-
}).join(`
|
|
257
|
-
`);
|
|
258
|
-
const metadataJson = JSON.stringify(metadata).replace(/<\//g, "<\\/");
|
|
259
|
-
const reviewHtml = metadata.review ? renderReviewSection(metadata.review) : "";
|
|
260
|
-
const hasReview = !!metadata.review;
|
|
261
|
-
const defaultTab = hasReview ? "summary" : "demos";
|
|
262
|
-
return `<!DOCTYPE html>
|
|
263
|
-
<html lang="en">
|
|
264
|
-
<head>
|
|
265
|
-
<meta charset="UTF-8">
|
|
266
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
267
|
-
<title>${escapeHtml(title)}</title>
|
|
268
|
-
<style>
|
|
269
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
270
|
-
body { font-family: system-ui, -apple-system, sans-serif; background: #1a1a2e; color: #e0e0e0; min-height: 100vh; }
|
|
271
|
-
header { padding: 1rem 2rem; background: #16213e; border-bottom: 1px solid #0f3460; }
|
|
272
|
-
header h1 { font-size: 1.4rem; color: #e94560; }
|
|
273
|
-
.tab-bar { display: flex; gap: 0; background: #16213e; border-bottom: 2px solid #0f3460; padding: 0 2rem; }
|
|
274
|
-
.tab-btn { padding: 0.7rem 1.5rem; background: none; border: none; border-bottom: 3px solid transparent; color: #999; font-size: 0.95rem; cursor: pointer; font-family: inherit; transition: all 0.15s; margin-bottom: -2px; }
|
|
275
|
-
.tab-btn:hover { color: #e0e0e0; }
|
|
276
|
-
.tab-btn.active { color: #e94560; border-bottom-color: #e94560; }
|
|
277
|
-
.tab-panel { display: none; }
|
|
278
|
-
.tab-panel.active { display: block; }
|
|
279
|
-
.review-section { padding: 1.5rem 2rem; }
|
|
280
|
-
.verdict-banner { padding: 1rem 1.5rem; border-radius: 6px; font-size: 1rem; margin-bottom: 1.5rem; }
|
|
281
|
-
.verdict-banner.approve { background: #1b4332; border: 1px solid #2d6a4f; color: #95d5b2; }
|
|
282
|
-
.verdict-banner.request-changes { background: #4a1520; border: 1px solid #842029; color: #f5c6cb; }
|
|
283
|
-
.review-body { max-width: 900px; }
|
|
284
|
-
.review-body h2 { font-size: 1.1rem; color: #e94560; margin: 1.2rem 0 0.5rem; }
|
|
285
|
-
.review-body p { font-size: 0.95rem; line-height: 1.6; color: #ccc; }
|
|
286
|
-
.highlights-list { list-style: disc; padding-left: 1.5rem; margin-bottom: 0.5rem; }
|
|
287
|
-
.highlights-list li { font-size: 0.95rem; line-height: 1.5; color: #95d5b2; margin-bottom: 0.3rem; }
|
|
288
|
-
.issue { padding: 0.6rem 0.8rem; margin-bottom: 0.5rem; border-radius: 4px; font-size: 0.9rem; line-height: 1.4; }
|
|
289
|
-
.issue.major { background: rgba(132, 32, 41, 0.3); border-left: 4px solid #dc3545; }
|
|
290
|
-
.issue.minor { background: rgba(255, 193, 7, 0.1); border-left: 4px solid #ffc107; }
|
|
291
|
-
.issue.nit { background: rgba(108, 117, 125, 0.2); border-left: 4px solid #6c757d; }
|
|
292
|
-
.severity-badge { display: inline-block; font-size: 0.7rem; font-weight: bold; padding: 0.15rem 0.4rem; border-radius: 3px; margin-right: 0.5rem; vertical-align: middle; }
|
|
293
|
-
.issue.major .severity-badge { background: #dc3545; color: #fff; }
|
|
294
|
-
.issue.minor .severity-badge { background: #ffc107; color: #000; }
|
|
295
|
-
.issue.nit .severity-badge { background: #6c757d; color: #fff; }
|
|
296
|
-
.no-issues { color: #95d5b2; font-style: italic; }
|
|
297
|
-
.demos-section { padding: 1rem 0; }
|
|
298
|
-
.review-layout { display: flex; height: 600px; }
|
|
299
|
-
.video-panel { flex: 4; padding: 1rem; display: flex; align-items: center; justify-content: center; background: #0f0f23; }
|
|
300
|
-
.video-wrapper { position: relative; width: 100%; max-height: 100%; display: flex; flex-direction: column; }
|
|
301
|
-
.video-wrapper video { width: 100%; max-height: calc(100% - 36px); border-radius: 4px 4px 0 0; display: block; cursor: pointer; }
|
|
302
|
-
.video-controls { display: flex; align-items: center; gap: 8px; padding: 6px 10px; background: #16213e; border-radius: 0 0 4px 4px; }
|
|
303
|
-
.video-controls button { background: none; border: none; color: #e0e0e0; cursor: pointer; font-size: 1rem; padding: 0; width: 20px; display: flex; align-items: center; justify-content: center; }
|
|
304
|
-
.video-controls button:hover { color: #e94560; }
|
|
305
|
-
.video-controls input[type="range"] { flex: 1; height: 4px; accent-color: #e94560; cursor: pointer; }
|
|
306
|
-
.video-controls .vc-time { font-size: 0.75rem; color: #999; white-space: nowrap; font-variant-numeric: tabular-nums; }
|
|
307
|
-
.side-panel { flex: 1; min-width: 260px; max-width: 360px; padding: 1rem; overflow-y: auto; background: #16213e; border-left: 1px solid #0f3460; }
|
|
308
|
-
.side-panel h2 { font-size: 1rem; margin-bottom: 0.5rem; color: #e94560; }
|
|
309
|
-
.side-panel section { margin-bottom: 1.5rem; }
|
|
310
|
-
#demo-list { list-style: none; }
|
|
311
|
-
#demo-list li { margin-bottom: 0.25rem; }
|
|
312
|
-
#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; }
|
|
313
|
-
#demo-list button:hover { background: #0f3460; }
|
|
314
|
-
#demo-list button.active { background: #e94560; color: #fff; border-color: #e94560; }
|
|
315
|
-
#summary-text { font-size: 0.9rem; line-height: 1.5; color: #ccc; }
|
|
316
|
-
#steps-list { list-style: none; }
|
|
317
|
-
#steps-list li { margin-bottom: 0.3rem; }
|
|
318
|
-
#steps-list button { width: 100%; text-align: left; padding: 0.4rem 0.6rem; background: transparent; color: #53a8b6; border: none; border-left: 3px solid transparent; cursor: pointer; font-size: 0.85rem; transition: all 0.2s; }
|
|
319
|
-
#steps-list button:hover { color: #e94560; }
|
|
320
|
-
#steps-list button.step-active { background: rgba(233, 69, 96, 0.15); color: #e94560; border-left-color: #e94560; }
|
|
321
|
-
.timestamp { font-weight: bold; margin-right: 0.4rem; color: #e94560; }
|
|
322
|
-
.issue { position: relative; }
|
|
323
|
-
.feedback-add-issue { position: absolute; right: 0.5rem; top: 50%; transform: translateY(-50%); background: none; border: 1px solid #53a8b6; color: #53a8b6; border-radius: 4px; cursor: pointer; font-size: 0.85rem; padding: 0.1rem 0.45rem; line-height: 1; }
|
|
324
|
-
.feedback-add-issue:hover { background: #53a8b6; color: #1a1a2e; }
|
|
325
|
-
#feedback-selection-btn { display: none; position: absolute; z-index: 1000; padding: 0.35rem 0.7rem; background: #e94560; color: #fff; border: none; border-radius: 4px; cursor: pointer; font-size: 0.8rem; white-space: nowrap; }
|
|
326
|
-
.feedback-layout { display: flex; gap: 1.5rem; padding: 1.5rem 2rem; }
|
|
327
|
-
.feedback-left { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1rem; }
|
|
328
|
-
.feedback-right { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 0.5rem; }
|
|
329
|
-
#feedback-list { list-style: none; padding: 0; }
|
|
330
|
-
#feedback-list li { display: flex; align-items: center; gap: 0.5rem; padding: 0.4rem 0.6rem; background: #16213e; border: 1px solid #0f3460; border-radius: 4px; margin-bottom: 0.4rem; font-size: 0.9rem; }
|
|
331
|
-
#feedback-list li span { flex: 1; }
|
|
332
|
-
.feedback-remove { background: none; border: none; color: #dc3545; cursor: pointer; font-size: 0.9rem; padding: 0 0.3rem; }
|
|
333
|
-
.feedback-remove:hover { color: #ff6b7a; }
|
|
334
|
-
#feedback-general { width: 100%; min-height: 100px; background: #16213e; color: #e0e0e0; border: 1px solid #0f3460; border-radius: 4px; padding: 0.6rem; font-family: inherit; font-size: 0.9rem; resize: vertical; }
|
|
335
|
-
#feedback-preview { background: #0f0f23; color: #ccc; border: 1px solid #0f3460; border-radius: 4px; padding: 1rem; white-space: pre-wrap; font-size: 0.85rem; line-height: 1.5; flex: 1; min-height: 200px; overflow-y: auto; }
|
|
336
|
-
#feedback-copy { align-self: flex-end; padding: 0.5rem 1rem; background: none; border: 1px solid #53a8b6; color: #53a8b6; border-radius: 4px; cursor: pointer; font-size: 0.85rem; }
|
|
337
|
-
#feedback-copy:hover { background: #53a8b6; color: #1a1a2e; }
|
|
338
|
-
</style>
|
|
339
|
-
</head>
|
|
340
|
-
<body>
|
|
341
|
-
<header>
|
|
342
|
-
<h1>${escapeHtml(title)}</h1>
|
|
343
|
-
</header>
|
|
344
|
-
<nav class="tab-bar">
|
|
345
|
-
${hasReview ? `<button class="tab-btn${defaultTab === "summary" ? " active" : ""}" data-tab="summary">Summary</button>` : ""}
|
|
346
|
-
<button class="tab-btn${defaultTab === "demos" ? " active" : ""}" data-tab="demos">Demos</button>
|
|
347
|
-
${hasReview ? `<button class="tab-btn" data-tab="feedback">Feedback</button>` : ""}
|
|
348
|
-
</nav>
|
|
349
|
-
<main>
|
|
350
|
-
${hasReview ? `<div id="tab-summary" class="tab-panel${defaultTab === "summary" ? " active" : ""}">
|
|
351
|
-
${reviewHtml}
|
|
352
|
-
</div>` : ""}
|
|
353
|
-
<div id="tab-demos" class="tab-panel${defaultTab === "demos" ? " active" : ""}">
|
|
354
|
-
<section class="demos-section">
|
|
355
|
-
<div class="review-layout">
|
|
356
|
-
<div class="video-panel">
|
|
357
|
-
<div class="video-wrapper">
|
|
358
|
-
<video id="review-video" src="${escapeAttr(firstDemo.file)}"></video>
|
|
359
|
-
<div class="video-controls">
|
|
360
|
-
<button id="vc-play" aria-label="Play">▶</button>
|
|
361
|
-
<input id="vc-seek" type="range" min="0" max="100" value="0" step="0.1">
|
|
362
|
-
<span class="vc-time" id="vc-time">0:00 / 0:00</span>
|
|
363
|
-
</div>
|
|
364
|
-
</div>
|
|
365
|
-
</div>
|
|
366
|
-
<div class="side-panel">
|
|
367
|
-
<section>
|
|
368
|
-
<h2>Demos</h2>
|
|
369
|
-
<ul id="demo-list">
|
|
370
|
-
${demoButtons}
|
|
371
|
-
</ul>
|
|
372
|
-
</section>
|
|
373
|
-
<section>
|
|
374
|
-
<h2>Summary</h2>
|
|
375
|
-
<p id="summary-text"></p>
|
|
376
|
-
</section>
|
|
377
|
-
<section id="steps-section">
|
|
378
|
-
<h2>Steps</h2>
|
|
379
|
-
<ul id="steps-list"></ul>
|
|
380
|
-
</section>
|
|
381
|
-
</div>
|
|
382
|
-
</div>
|
|
383
|
-
</section>
|
|
384
|
-
</div>
|
|
385
|
-
${hasReview ? `<div id="tab-feedback" class="tab-panel">
|
|
386
|
-
<div class="feedback-layout">
|
|
387
|
-
<div class="feedback-left">
|
|
388
|
-
<h2>Feedback Items</h2>
|
|
389
|
-
<ul id="feedback-list"></ul>
|
|
390
|
-
<h2>General Feedback</h2>
|
|
391
|
-
<textarea id="feedback-general" placeholder="Add general feedback here..."></textarea>
|
|
392
|
-
</div>
|
|
393
|
-
<div class="feedback-right">
|
|
394
|
-
<h2>Preview</h2>
|
|
395
|
-
<pre id="feedback-preview"></pre>
|
|
396
|
-
<button id="feedback-copy">Copy to clipboard</button>
|
|
397
|
-
</div>
|
|
398
|
-
</div>
|
|
399
|
-
</div>` : ""}
|
|
400
|
-
</main>
|
|
401
|
-
${hasReview ? `<button id="feedback-selection-btn">Add to feedback</button>` : ""}
|
|
402
|
-
<script>
|
|
403
|
-
(function() {
|
|
404
|
-
// Tab switching
|
|
405
|
-
var tabBtns = document.querySelectorAll(".tab-btn");
|
|
406
|
-
var tabPanels = document.querySelectorAll(".tab-panel");
|
|
407
|
-
tabBtns.forEach(function(btn) {
|
|
408
|
-
btn.addEventListener("click", function() {
|
|
409
|
-
var target = btn.getAttribute("data-tab");
|
|
410
|
-
tabBtns.forEach(function(b) { b.classList.toggle("active", b === btn); });
|
|
411
|
-
tabPanels.forEach(function(p) { p.classList.toggle("active", p.id === "tab-" + target); });
|
|
412
|
-
});
|
|
413
|
-
});
|
|
414
|
-
|
|
415
|
-
var metadata = ${metadataJson};
|
|
416
|
-
var video = document.getElementById("review-video");
|
|
417
|
-
var summaryText = document.getElementById("summary-text");
|
|
418
|
-
var stepsList = document.getElementById("steps-list");
|
|
419
|
-
var demoButtons = document.querySelectorAll("#demo-list button");
|
|
420
|
-
|
|
421
|
-
function esc(s) {
|
|
422
|
-
var d = document.createElement("div");
|
|
423
|
-
d.appendChild(document.createTextNode(s));
|
|
424
|
-
return d.innerHTML;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
function formatTime(seconds) {
|
|
428
|
-
var m = Math.floor(seconds / 60);
|
|
429
|
-
var s = Math.floor(seconds % 60);
|
|
430
|
-
return m + ":" + (s < 10 ? "0" : "") + s;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
function selectDemo(index) {
|
|
434
|
-
var demo = metadata.demos[index];
|
|
435
|
-
video.src = demo.file;
|
|
436
|
-
video.load();
|
|
437
|
-
summaryText.textContent = demo.summary;
|
|
438
|
-
|
|
439
|
-
demoButtons.forEach(function(btn, i) {
|
|
440
|
-
btn.classList.toggle("active", i === index);
|
|
441
|
-
});
|
|
442
|
-
|
|
443
|
-
var stepsHtml = "";
|
|
444
|
-
demo.steps.forEach(function(step) {
|
|
445
|
-
stepsHtml += '<li><button data-time="' + step.timestampSeconds + '">' +
|
|
446
|
-
'<span class="timestamp">' + esc(formatTime(step.timestampSeconds)) + '</span>' +
|
|
447
|
-
esc(step.text) + '</button></li>';
|
|
448
|
-
});
|
|
449
|
-
stepsList.innerHTML = stepsHtml;
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
demoButtons.forEach(function(btn) {
|
|
453
|
-
btn.addEventListener("click", function() {
|
|
454
|
-
selectDemo(parseInt(btn.getAttribute("data-index"), 10));
|
|
455
|
-
});
|
|
456
|
-
});
|
|
457
|
-
|
|
458
|
-
stepsList.addEventListener("click", function(e) {
|
|
459
|
-
var btn = e.target.closest("button[data-time]");
|
|
460
|
-
if (btn) {
|
|
461
|
-
video.currentTime = parseFloat(btn.getAttribute("data-time"));
|
|
462
|
-
video.play();
|
|
463
|
-
}
|
|
464
|
-
});
|
|
465
|
-
|
|
466
|
-
video.addEventListener("timeupdate", function() {
|
|
467
|
-
var buttons = document.querySelectorAll("#steps-list button[data-time]");
|
|
468
|
-
var ct = video.currentTime;
|
|
469
|
-
var activeIdx = -1;
|
|
470
|
-
buttons.forEach(function(btn, i) {
|
|
471
|
-
if (parseFloat(btn.getAttribute("data-time")) <= ct) activeIdx = i;
|
|
472
|
-
btn.classList.remove("step-active");
|
|
473
|
-
});
|
|
474
|
-
if (activeIdx >= 0) buttons[activeIdx].classList.add("step-active");
|
|
475
|
-
});
|
|
476
|
-
|
|
477
|
-
selectDemo(0);
|
|
478
|
-
|
|
479
|
-
// Custom video controls
|
|
480
|
-
var playBtn = document.getElementById("vc-play");
|
|
481
|
-
var seekBar = document.getElementById("vc-seek");
|
|
482
|
-
var timeDisplay = document.getElementById("vc-time");
|
|
483
|
-
var seeking = false;
|
|
484
|
-
|
|
485
|
-
function fmtTime(sec) {
|
|
486
|
-
var m = Math.floor(sec / 60);
|
|
487
|
-
var s = Math.floor(sec % 60);
|
|
488
|
-
return m + ":" + (s < 10 ? "0" : "") + s;
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
function updateTime() {
|
|
492
|
-
var cur = video.currentTime || 0;
|
|
493
|
-
var dur = video.duration || 0;
|
|
494
|
-
timeDisplay.textContent = fmtTime(cur) + " / " + fmtTime(dur);
|
|
495
|
-
if (!seeking && dur) seekBar.value = (cur / dur) * 100;
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
function updatePlayBtn() {
|
|
499
|
-
playBtn.innerHTML = video.paused ? "▶" : "▮▮";
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
playBtn.addEventListener("click", function() {
|
|
503
|
-
video.paused ? video.play() : video.pause();
|
|
504
|
-
});
|
|
505
|
-
video.addEventListener("click", function() {
|
|
506
|
-
video.paused ? video.play() : video.pause();
|
|
507
|
-
});
|
|
508
|
-
video.addEventListener("play", updatePlayBtn);
|
|
509
|
-
video.addEventListener("pause", updatePlayBtn);
|
|
510
|
-
video.addEventListener("ended", updatePlayBtn);
|
|
511
|
-
video.addEventListener("timeupdate", updateTime);
|
|
512
|
-
video.addEventListener("loadedmetadata", updateTime);
|
|
513
|
-
|
|
514
|
-
seekBar.addEventListener("input", function() {
|
|
515
|
-
seeking = true;
|
|
516
|
-
if (video.duration) {
|
|
517
|
-
video.currentTime = (seekBar.value / 100) * video.duration;
|
|
518
|
-
}
|
|
519
|
-
});
|
|
520
|
-
seekBar.addEventListener("change", function() { seeking = false; });
|
|
521
|
-
|
|
522
|
-
// Feedback tab logic
|
|
523
|
-
if (document.getElementById("tab-feedback")) {
|
|
524
|
-
var feedbackItems = [];
|
|
525
|
-
var feedbackList = document.getElementById("feedback-list");
|
|
526
|
-
var feedbackGeneral = document.getElementById("feedback-general");
|
|
527
|
-
var feedbackPreview = document.getElementById("feedback-preview");
|
|
528
|
-
var feedbackCopy = document.getElementById("feedback-copy");
|
|
529
|
-
var selectionBtn = document.getElementById("feedback-selection-btn");
|
|
530
|
-
|
|
531
|
-
function addFeedbackItem(text) {
|
|
532
|
-
var trimmed = text.trim();
|
|
533
|
-
if (!trimmed) return;
|
|
534
|
-
for (var i = 0; i < feedbackItems.length; i++) {
|
|
535
|
-
if (feedbackItems[i] === trimmed) return;
|
|
536
|
-
}
|
|
537
|
-
feedbackItems.push(trimmed);
|
|
538
|
-
renderFeedback();
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
function removeFeedbackItem(index) {
|
|
542
|
-
feedbackItems.splice(index, 1);
|
|
543
|
-
renderFeedback();
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
function renderFeedback() {
|
|
547
|
-
var html = "";
|
|
548
|
-
feedbackItems.forEach(function(item, i) {
|
|
549
|
-
html += '<li><span>' + esc(item) + '</span><button class="feedback-remove" data-index="' + i + '">X</button></li>';
|
|
550
|
-
});
|
|
551
|
-
feedbackList.innerHTML = html;
|
|
552
|
-
updatePreview();
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
function updatePreview() {
|
|
556
|
-
var lines = "";
|
|
557
|
-
feedbackItems.forEach(function(item, i) {
|
|
558
|
-
lines += (i + 1) + ". Address: " + item + "\\n";
|
|
559
|
-
});
|
|
560
|
-
var general = feedbackGeneral.value.trim();
|
|
561
|
-
if (general) {
|
|
562
|
-
lines += "\\nGeneral feedback:\\n" + general;
|
|
563
|
-
}
|
|
564
|
-
feedbackPreview.textContent = lines;
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
// Issue "+" buttons
|
|
568
|
-
var summaryTab = document.getElementById("tab-summary");
|
|
569
|
-
if (summaryTab) {
|
|
570
|
-
summaryTab.addEventListener("click", function(e) {
|
|
571
|
-
var btn = e.target.closest(".feedback-add-issue");
|
|
572
|
-
if (btn) {
|
|
573
|
-
addFeedbackItem(btn.getAttribute("data-issue"));
|
|
574
|
-
}
|
|
575
|
-
});
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
// Text selection floating button
|
|
579
|
-
var selectionTimeout;
|
|
580
|
-
document.addEventListener("mouseup", function(e) {
|
|
581
|
-
clearTimeout(selectionTimeout);
|
|
582
|
-
selectionTimeout = setTimeout(function() {
|
|
583
|
-
var sel = window.getSelection();
|
|
584
|
-
var text = sel ? sel.toString().trim() : "";
|
|
585
|
-
if (!text) return;
|
|
586
|
-
var anchor = sel.anchorNode;
|
|
587
|
-
var inSummary = false;
|
|
588
|
-
var node = anchor;
|
|
589
|
-
while (node) {
|
|
590
|
-
if (node.id === "tab-summary") { inSummary = true; break; }
|
|
591
|
-
node = node.parentNode;
|
|
592
|
-
}
|
|
593
|
-
if (!inSummary) return;
|
|
594
|
-
selectionBtn.style.display = "block";
|
|
595
|
-
selectionBtn.style.left = e.pageX + "px";
|
|
596
|
-
selectionBtn.style.top = (e.pageY - 35) + "px";
|
|
597
|
-
selectionBtn._selectedText = text;
|
|
598
|
-
}, 100);
|
|
599
|
-
});
|
|
600
|
-
|
|
601
|
-
selectionBtn.addEventListener("click", function() {
|
|
602
|
-
if (selectionBtn._selectedText) {
|
|
603
|
-
addFeedbackItem(selectionBtn._selectedText);
|
|
604
|
-
}
|
|
605
|
-
selectionBtn.style.display = "none";
|
|
606
|
-
window.getSelection().removeAllRanges();
|
|
607
|
-
});
|
|
608
|
-
|
|
609
|
-
document.addEventListener("mousedown", function(e) {
|
|
610
|
-
if (e.target !== selectionBtn) {
|
|
611
|
-
selectionBtn.style.display = "none";
|
|
612
|
-
}
|
|
613
|
-
});
|
|
614
|
-
|
|
615
|
-
// Remove buttons
|
|
616
|
-
feedbackList.addEventListener("click", function(e) {
|
|
617
|
-
var btn = e.target.closest(".feedback-remove");
|
|
618
|
-
if (btn) {
|
|
619
|
-
removeFeedbackItem(parseInt(btn.getAttribute("data-index"), 10));
|
|
620
|
-
}
|
|
621
|
-
});
|
|
622
|
-
|
|
623
|
-
// Textarea input
|
|
624
|
-
feedbackGeneral.addEventListener("input", updatePreview);
|
|
625
|
-
|
|
626
|
-
// Copy button
|
|
627
|
-
feedbackCopy.addEventListener("click", function() {
|
|
628
|
-
var text = feedbackPreview.textContent;
|
|
629
|
-
function onCopied() {
|
|
630
|
-
feedbackCopy.textContent = "Copied!";
|
|
631
|
-
setTimeout(function() { feedbackCopy.textContent = "Copy to clipboard"; }, 1500);
|
|
632
|
-
}
|
|
633
|
-
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
634
|
-
navigator.clipboard.writeText(text).then(onCopied, onCopied);
|
|
635
|
-
} else {
|
|
636
|
-
onCopied();
|
|
637
|
-
}
|
|
638
|
-
});
|
|
639
|
-
|
|
640
|
-
renderFeedback();
|
|
641
|
-
}
|
|
642
|
-
})();
|
|
643
|
-
</script>
|
|
644
|
-
</body>
|
|
645
|
-
</html>`;
|
|
646
|
-
}
|
|
647
|
-
|
|
648
221
|
// src/git-context.ts
|
|
649
|
-
|
|
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
|
+
}
|
|
650
242
|
var defaultExec = async (cmd, cwd) => {
|
|
651
|
-
const
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
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}` : ""}`);
|
|
655
248
|
}
|
|
656
|
-
return proc.stdout
|
|
249
|
+
return proc.stdout ?? "";
|
|
657
250
|
};
|
|
658
251
|
var defaultReadFile = (path) => {
|
|
659
252
|
return readFileSync(path, "utf-8");
|
|
@@ -662,12 +255,17 @@ async function getRepoContext(demosDir, options) {
|
|
|
662
255
|
const exec = options?.exec ?? defaultExec;
|
|
663
256
|
const readFile = options?.readFile ?? defaultReadFile;
|
|
664
257
|
const gitRoot = (await exec(["git", "rev-parse", "--show-toplevel"], demosDir)).trim();
|
|
258
|
+
const diffBase = options?.diffBase ?? await detectDefaultBase(exec, gitRoot);
|
|
665
259
|
let gitDiff;
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
gitDiff = workingDiff;
|
|
260
|
+
if (diffBase) {
|
|
261
|
+
gitDiff = (await exec(["git", "diff", `${diffBase}...HEAD`], gitRoot)).trim();
|
|
669
262
|
} else {
|
|
670
|
-
|
|
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
|
+
}
|
|
671
269
|
}
|
|
672
270
|
const lsOutput = (await exec(["git", "ls-files"], gitRoot)).trim();
|
|
673
271
|
const files = lsOutput.split(`
|
|
@@ -686,100 +284,166 @@ ${content}`);
|
|
|
686
284
|
return { gitDiff, guidelines };
|
|
687
285
|
}
|
|
688
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
|
+
}
|
|
297
|
+
function escapeHtml(s) {
|
|
298
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
299
|
+
}
|
|
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);
|
|
304
|
+
}
|
|
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);
|
|
325
|
+
}
|
|
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;
|
|
351
|
+
}
|
|
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 };
|
|
404
|
+
}
|
|
405
|
+
|
|
689
406
|
// src/bin/demon-demo-review.ts
|
|
690
407
|
var dir;
|
|
691
408
|
var agent;
|
|
409
|
+
var diffBase;
|
|
692
410
|
var args = process.argv.slice(2);
|
|
693
411
|
for (let i = 0;i < args.length; i++) {
|
|
694
412
|
if (args[i] === "--agent") {
|
|
695
413
|
agent = args[++i];
|
|
414
|
+
} else if (args[i] === "--base") {
|
|
415
|
+
diffBase = args[++i];
|
|
696
416
|
} else if (!dir) {
|
|
697
417
|
dir = args[i];
|
|
698
418
|
}
|
|
699
419
|
}
|
|
700
420
|
if (!dir) {
|
|
701
|
-
console.error("Usage: demon-demo-review [--agent <path>] <directory>");
|
|
702
|
-
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)");
|
|
703
424
|
process.exit(1);
|
|
704
425
|
}
|
|
705
426
|
var resolved = resolve(dir);
|
|
706
|
-
if (!
|
|
427
|
+
if (!existsSync2(resolved) || !statSync(resolved).isDirectory()) {
|
|
707
428
|
console.error(`Error: "${resolved}" is not a valid directory.`);
|
|
708
429
|
process.exit(1);
|
|
709
430
|
}
|
|
710
|
-
var
|
|
711
|
-
if (
|
|
712
|
-
|
|
713
|
-
if (!entry.isDirectory())
|
|
714
|
-
continue;
|
|
715
|
-
const subdir = join(resolved, entry.name);
|
|
716
|
-
for (const f of readdirSync(subdir)) {
|
|
717
|
-
if (f.endsWith(".webm")) {
|
|
718
|
-
webmFiles.push(join(subdir, f));
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
webmFiles.sort();
|
|
724
|
-
if (webmFiles.length === 0) {
|
|
725
|
-
console.error(`Error: No .webm files found in "${resolved}" or its subdirectories.`);
|
|
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.`);
|
|
726
434
|
process.exit(1);
|
|
727
435
|
}
|
|
728
|
-
for (const file of
|
|
729
|
-
console.log(file);
|
|
436
|
+
for (const file of demoFiles) {
|
|
437
|
+
console.log(file.path);
|
|
730
438
|
}
|
|
731
|
-
var stepsMap = {};
|
|
732
|
-
for (const webmFile of webmFiles) {
|
|
733
|
-
const stepsPath = join(dirname(webmFile), "demo-steps.json");
|
|
734
|
-
if (!existsSync(stepsPath))
|
|
735
|
-
continue;
|
|
736
|
-
try {
|
|
737
|
-
const raw = readFileSync2(stepsPath, "utf-8");
|
|
738
|
-
const parsed = JSON.parse(raw);
|
|
739
|
-
if (Array.isArray(parsed)) {
|
|
740
|
-
stepsMap[basename(webmFile)] = parsed;
|
|
741
|
-
}
|
|
742
|
-
} catch {}
|
|
743
|
-
}
|
|
744
|
-
if (Object.keys(stepsMap).length === 0) {
|
|
745
|
-
console.error("Error: No demo-steps.json found alongside any .webm files.");
|
|
746
|
-
console.error("Use DemoRecorder in your demo tests to generate step data.");
|
|
747
|
-
process.exit(1);
|
|
748
|
-
}
|
|
749
|
-
var gitDiff;
|
|
750
|
-
var guidelines;
|
|
751
439
|
try {
|
|
752
|
-
const repoContext = await getRepoContext(resolved);
|
|
753
|
-
gitDiff = repoContext.gitDiff;
|
|
754
|
-
guidelines = repoContext.guidelines;
|
|
755
|
-
} catch (err) {
|
|
756
|
-
console.warn("Warning: Could not gather repo context:", err instanceof Error ? err.message : err);
|
|
757
|
-
}
|
|
758
|
-
try {
|
|
759
|
-
const basenames = webmFiles.map((f) => basename(f));
|
|
760
|
-
const prompt = buildReviewPrompt({ filenames: basenames, stepsMap, gitDiff, guidelines });
|
|
761
440
|
console.log("Invoking claude to generate review metadata...");
|
|
762
|
-
const
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
demos: llmResponse.demos.map((demo) => ({
|
|
766
|
-
file: demo.file,
|
|
767
|
-
summary: demo.summary,
|
|
768
|
-
steps: stepsMap[demo.file] ?? []
|
|
769
|
-
})),
|
|
770
|
-
review: llmResponse.review
|
|
771
|
-
};
|
|
772
|
-
const outputPath = join(resolved, "review-metadata.json");
|
|
773
|
-
writeFileSync(outputPath, JSON.stringify(metadata, null, 2) + `
|
|
774
|
-
`);
|
|
775
|
-
console.log(`Review metadata written to ${outputPath}`);
|
|
776
|
-
const html = generateReviewHtml({ metadata });
|
|
777
|
-
const htmlPath = join(resolved, "review.html");
|
|
778
|
-
writeFileSync(htmlPath, html);
|
|
779
|
-
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));
|
|
780
444
|
} catch (err) {
|
|
781
|
-
console.error("Error generating review
|
|
445
|
+
console.error("Error generating review:", err instanceof Error ? err.message : err);
|
|
782
446
|
process.exit(1);
|
|
783
447
|
}
|
|
784
448
|
|
|
785
|
-
//# debugId=
|
|
449
|
+
//# debugId=C76A8A3848513E7E64756E2164756E21
|