@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
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join, dirname, relative } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
buildReviewPrompt,
|
|
7
|
+
invokeClaude,
|
|
8
|
+
parseLlmResponse,
|
|
9
|
+
} from "./review.ts";
|
|
10
|
+
import { getRepoContext } from "./git-context.ts";
|
|
11
|
+
import type { ReviewMetadata, DemoType } from "./review-types.ts";
|
|
12
|
+
|
|
13
|
+
export interface ReviewAppData {
|
|
14
|
+
metadata: ReviewMetadata;
|
|
15
|
+
title: string;
|
|
16
|
+
videos: Record<string, string>;
|
|
17
|
+
logs?: Record<string, string>;
|
|
18
|
+
feedbackEndpoint?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface DemoFile {
|
|
22
|
+
path: string;
|
|
23
|
+
filename: string;
|
|
24
|
+
relativePath: string;
|
|
25
|
+
type: DemoType;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface GenerateReviewOptions {
|
|
29
|
+
directory: string;
|
|
30
|
+
agent?: string;
|
|
31
|
+
feedbackEndpoint?: string;
|
|
32
|
+
title?: string;
|
|
33
|
+
diffBase?: string; // Base commit/branch for diff (auto-detected if not provided)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface GenerateReviewResult {
|
|
37
|
+
htmlPath: string;
|
|
38
|
+
metadataPath: string;
|
|
39
|
+
metadata: ReviewMetadata;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function getReviewTemplate(): string {
|
|
43
|
+
const currentFile = fileURLToPath(import.meta.url);
|
|
44
|
+
const distDir = dirname(currentFile);
|
|
45
|
+
const templatePath = join(distDir, "review-template.html");
|
|
46
|
+
|
|
47
|
+
if (!existsSync(templatePath)) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`Review template not found at ${templatePath}. ` +
|
|
50
|
+
`Make sure to build the review-app package first.`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return readFileSync(templatePath, "utf-8");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function escapeHtml(s: string): string {
|
|
58
|
+
return s
|
|
59
|
+
.replace(/&/g, "&")
|
|
60
|
+
.replace(/</g, "<")
|
|
61
|
+
.replace(/>/g, ">")
|
|
62
|
+
.replace(/"/g, """)
|
|
63
|
+
.replace(/'/g, "'");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function generateReviewHtml(appData: ReviewAppData): string {
|
|
67
|
+
const template = getReviewTemplate();
|
|
68
|
+
const jsonData = JSON.stringify(appData);
|
|
69
|
+
|
|
70
|
+
return template
|
|
71
|
+
.replace("<title>Demo Review</title>", `<title>${escapeHtml(appData.title)}</title>`)
|
|
72
|
+
.replace('"{{__INJECT_REVIEW_DATA__}}"', jsonData);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Discover demo files (.webm and .jsonl) in the given directory.
|
|
77
|
+
* Searches top-level first, then one level deep (Playwright creates per-test subdirectories).
|
|
78
|
+
*/
|
|
79
|
+
export function discoverDemoFiles(directory: string): DemoFile[] {
|
|
80
|
+
const files: DemoFile[] = [];
|
|
81
|
+
|
|
82
|
+
const processFile = (filePath: string, filename: string) => {
|
|
83
|
+
const relativePath = relative(directory, filePath);
|
|
84
|
+
if (filename.endsWith(".webm")) {
|
|
85
|
+
files.push({ path: filePath, filename, relativePath, type: "web-ux" });
|
|
86
|
+
} else if (filename.endsWith(".jsonl")) {
|
|
87
|
+
files.push({ path: filePath, filename, relativePath, type: "log-based" });
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// Search top-level
|
|
92
|
+
for (const f of readdirSync(directory)) {
|
|
93
|
+
processFile(join(directory, f), f);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// If no files found at top level, search one level deep
|
|
97
|
+
if (files.length === 0) {
|
|
98
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
99
|
+
if (!entry.isDirectory()) continue;
|
|
100
|
+
const subdir = join(directory, entry.name);
|
|
101
|
+
for (const f of readdirSync(subdir)) {
|
|
102
|
+
processFile(join(subdir, f), f);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return files.sort((a, b) => a.filename.localeCompare(b.filename));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Generate a review HTML page from demo files in the given directory.
|
|
112
|
+
* This is the main entry point for programmatic use.
|
|
113
|
+
*/
|
|
114
|
+
export async function generateReview(options: GenerateReviewOptions): Promise<GenerateReviewResult> {
|
|
115
|
+
const { directory, agent, feedbackEndpoint, title = "Demo Review", diffBase } = options;
|
|
116
|
+
|
|
117
|
+
const demoFiles = discoverDemoFiles(directory);
|
|
118
|
+
const webUxDemos = demoFiles.filter((d) => d.type === "web-ux");
|
|
119
|
+
const logBasedDemos = demoFiles.filter((d) => d.type === "log-based");
|
|
120
|
+
|
|
121
|
+
if (demoFiles.length === 0) {
|
|
122
|
+
throw new Error(`No .webm or .jsonl files found in "${directory}" or its subdirectories.`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Build maps from filename to relativePath for lookup
|
|
126
|
+
const filenameToRelativePath = new Map(demoFiles.map((d) => [d.filename, d.relativePath]));
|
|
127
|
+
|
|
128
|
+
// Collect demo-steps.json from the directory of each .webm file
|
|
129
|
+
// Key by filename for prompt builder, and by relativePath for metadata
|
|
130
|
+
const stepsMapByFilename: Record<string, Array<{ text: string; timestampSeconds: number }>> = {};
|
|
131
|
+
const stepsMapByRelativePath: Record<string, Array<{ text: string; timestampSeconds: number }>> = {};
|
|
132
|
+
for (const demo of webUxDemos) {
|
|
133
|
+
const stepsPath = join(dirname(demo.path), "demo-steps.json");
|
|
134
|
+
if (!existsSync(stepsPath)) continue;
|
|
135
|
+
try {
|
|
136
|
+
const raw = readFileSync(stepsPath, "utf-8");
|
|
137
|
+
const parsed = JSON.parse(raw);
|
|
138
|
+
if (Array.isArray(parsed)) {
|
|
139
|
+
stepsMapByFilename[demo.filename] = parsed;
|
|
140
|
+
stepsMapByRelativePath[demo.relativePath] = parsed;
|
|
141
|
+
}
|
|
142
|
+
} catch {
|
|
143
|
+
// skip malformed steps files
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Collect JSONL content for log-based demos, keyed by relativePath
|
|
148
|
+
const logsMap: Record<string, string> = {};
|
|
149
|
+
for (const demo of logBasedDemos) {
|
|
150
|
+
logsMap[demo.relativePath] = readFileSync(demo.path, "utf-8");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// For web-ux demos, require steps
|
|
154
|
+
const hasWebUxDemos = webUxDemos.length > 0;
|
|
155
|
+
const hasLogDemos = logBasedDemos.length > 0;
|
|
156
|
+
|
|
157
|
+
if (hasWebUxDemos && Object.keys(stepsMapByFilename).length === 0) {
|
|
158
|
+
throw new Error(
|
|
159
|
+
"No demo-steps.json found alongside any .webm files. " +
|
|
160
|
+
"Use DemoRecorder in your demo tests to generate step data."
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (!hasWebUxDemos && !hasLogDemos) {
|
|
165
|
+
throw new Error("No demo files found.");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Gather repo context (git diff + guidelines)
|
|
169
|
+
let gitDiff: string | undefined;
|
|
170
|
+
let guidelines: string[] | undefined;
|
|
171
|
+
try {
|
|
172
|
+
const repoContext = await getRepoContext(directory, { diffBase });
|
|
173
|
+
gitDiff = repoContext.gitDiff;
|
|
174
|
+
guidelines = repoContext.guidelines;
|
|
175
|
+
} catch {
|
|
176
|
+
// Silently continue without repo context
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const allFilenames = demoFiles.map((d) => d.filename);
|
|
180
|
+
const prompt = buildReviewPrompt({ filenames: allFilenames, stepsMap: stepsMapByFilename, gitDiff, guidelines });
|
|
181
|
+
|
|
182
|
+
const rawOutput = await invokeClaude(prompt, { agent });
|
|
183
|
+
const llmResponse = parseLlmResponse(rawOutput);
|
|
184
|
+
|
|
185
|
+
// Build a map of filename to type for easy lookup
|
|
186
|
+
const typeMap = new Map(demoFiles.map((d) => [d.filename, d.type]));
|
|
187
|
+
|
|
188
|
+
// Construct final metadata by merging LLM summaries with steps and type
|
|
189
|
+
// Convert filenames from LLM response to relative paths for proper video loading
|
|
190
|
+
const metadata: ReviewMetadata = {
|
|
191
|
+
demos: llmResponse.demos.map((demo) => {
|
|
192
|
+
const relativePath = filenameToRelativePath.get(demo.file) ?? demo.file;
|
|
193
|
+
return {
|
|
194
|
+
file: relativePath,
|
|
195
|
+
type: typeMap.get(demo.file) ?? "web-ux",
|
|
196
|
+
summary: demo.summary,
|
|
197
|
+
steps: stepsMapByRelativePath[relativePath] ?? [],
|
|
198
|
+
};
|
|
199
|
+
}),
|
|
200
|
+
review: llmResponse.review,
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const metadataPath = join(directory, "review-metadata.json");
|
|
204
|
+
writeFileSync(metadataPath, JSON.stringify(metadata, null, 2) + "\n");
|
|
205
|
+
|
|
206
|
+
// Build app data and generate HTML
|
|
207
|
+
// Videos are referenced by relative path in demo.file, no base64 encoding needed
|
|
208
|
+
const appData: ReviewAppData = {
|
|
209
|
+
metadata,
|
|
210
|
+
title,
|
|
211
|
+
videos: {},
|
|
212
|
+
logs: Object.keys(logsMap).length > 0 ? logsMap : undefined,
|
|
213
|
+
feedbackEndpoint,
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const html = generateReviewHtml(appData);
|
|
217
|
+
const htmlPath = join(directory, "review.html");
|
|
218
|
+
writeFileSync(htmlPath, html);
|
|
219
|
+
|
|
220
|
+
return { htmlPath, metadataPath, metadata };
|
|
221
|
+
}
|
package/src/review-types.ts
CHANGED
package/src/review.ts
CHANGED
|
@@ -234,18 +234,24 @@ export function parseLlmResponse(raw: string): LlmReviewResponse {
|
|
|
234
234
|
return parsed as LlmReviewResponse;
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
+
import { spawn } from "node:child_process";
|
|
238
|
+
import { Readable } from "node:stream";
|
|
239
|
+
|
|
237
240
|
function defaultSpawn(
|
|
238
241
|
cmd: string[],
|
|
239
242
|
): { exitCode: Promise<number>; stdout: ReadableStream<Uint8Array> } {
|
|
240
243
|
const [command, ...args] = cmd;
|
|
241
|
-
const proc =
|
|
242
|
-
|
|
243
|
-
stderr: "pipe",
|
|
244
|
+
const proc = spawn(command!, args, {
|
|
245
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
244
246
|
});
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
};
|
|
247
|
+
|
|
248
|
+
const exitCode = new Promise<number>((resolve) => {
|
|
249
|
+
proc.on("close", (code) => resolve(code ?? 1));
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
const stdout = Readable.toWeb(proc.stdout!) as unknown as ReadableStream<Uint8Array>;
|
|
253
|
+
|
|
254
|
+
return { exitCode, stdout };
|
|
249
255
|
}
|
|
250
256
|
|
|
251
257
|
function concatUint8Arrays(arrays: Uint8Array[]): Uint8Array {
|