demo-this-pr 0.1.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.
@@ -0,0 +1,2 @@
1
+ import type { PrKitOptions, PrKitResult } from "./types.js";
2
+ export declare function createPrKit(options: PrKitOptions): Promise<PrKitResult>;
package/dist/prKit.js ADDED
@@ -0,0 +1,99 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import { join, resolve } from "node:path";
3
+ import { buildDemoPlan } from "./demoPlan.js";
4
+ import { readGitContext } from "./git.js";
5
+ import { writePlaywrightArtifacts, runPlaywrightDemo } from "./playwright.js";
6
+ import { renderHtmlReport, renderMarkdownReport } from "./report.js";
7
+ import { runShellCommand } from "./shell.js";
8
+ export async function createPrKit(options) {
9
+ const cwd = resolve(options.cwd);
10
+ const outputDir = resolve(cwd, options.outputDir);
11
+ await mkdir(outputDir, { recursive: true });
12
+ const git = await readGitContext(cwd, options.base);
13
+ const feature = options.feature?.trim() || titleFromBranch(git.branch);
14
+ const why = options.why?.trim() || "Explain why this PR exists before requesting review.";
15
+ const flow = options.flow.length > 0 ? options.flow : defaultFlow(git.changedFiles.map((file) => file.path));
16
+ const changes = options.changes.length > 0 ? options.changes : defaultChanges(git.changedFiles.map((file) => file.path));
17
+ const technologies = options.technologies;
18
+ const demoPlan = await buildDemoPlan({
19
+ feature,
20
+ why,
21
+ demoUrl: options.demoUrl,
22
+ demoSteps: options.demoSteps,
23
+ demoPlanPath: options.demoPlanPath
24
+ });
25
+ const testResults = [];
26
+ for (const command of options.testCommands) {
27
+ testResults.push(await runShellCommand(command, cwd));
28
+ }
29
+ demoPlan.context = {
30
+ changes,
31
+ technologies,
32
+ tests: testResults.map((testResult) => ({
33
+ command: testResult.command,
34
+ status: testResult.exitCode === 0 ? "passed" : "failed",
35
+ durationMs: testResult.durationMs
36
+ })),
37
+ branch: git.branch,
38
+ base: git.base,
39
+ changedFiles: git.changedFiles.map((file) => `${file.status} ${file.path}`)
40
+ };
41
+ const { specPath, configPath, planPath, mcpGuidePath, mcpScriptPath } = await writePlaywrightArtifacts(outputDir, demoPlan);
42
+ const demoRun = options.runDemo
43
+ ? await runPlaywrightDemo(outputDir, demoPlan, { headed: options.headed })
44
+ : { attempted: false, passed: false };
45
+ const result = {
46
+ outputDir,
47
+ markdownPath: join(outputDir, "PR_REVIEW.md"),
48
+ htmlPath: join(outputDir, "PR_REVIEW.html"),
49
+ demoSpecPath: specPath,
50
+ demoPlanPath: planPath,
51
+ mcpGuidePath,
52
+ mcpScriptPath,
53
+ git,
54
+ feature,
55
+ why,
56
+ flow,
57
+ changes,
58
+ technologies,
59
+ testResults,
60
+ demoRun
61
+ };
62
+ await writeFile(result.markdownPath, renderMarkdownReport(result), "utf8");
63
+ await writeFile(result.htmlPath, renderHtmlReport(result), "utf8");
64
+ return result;
65
+ }
66
+ function defaultChanges(changedFiles) {
67
+ if (changedFiles.length === 0) {
68
+ return ["Current branch changes are shown through the configured demo flow."];
69
+ }
70
+ const files = changedFiles.slice(0, 5).join(", ");
71
+ return [`Current branch changes touch: ${files}${changedFiles.length > 5 ? ", ..." : ""}.`];
72
+ }
73
+ function titleFromBranch(branch) {
74
+ if (!branch || branch === "unknown" || branch === "HEAD") {
75
+ return "PR review kit";
76
+ }
77
+ return branch
78
+ .replace(/^[a-z]+\/+/iu, "")
79
+ .split(/[-_/\s]+/u)
80
+ .filter(Boolean)
81
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
82
+ .join(" ");
83
+ }
84
+ function defaultFlow(changedFiles) {
85
+ const files = changedFiles.slice(0, 4);
86
+ if (files.length === 0) {
87
+ return [
88
+ "Read the PR summary and confirm the intended behavior.",
89
+ "Run the generated Playwright demo or edit the demo plan.",
90
+ "Run the listed test commands before requesting review."
91
+ ];
92
+ }
93
+ return [
94
+ "Start with the PR summary and the reason for the change.",
95
+ `Review the primary touched files: ${files.join(", ")}${changedFiles.length > files.length ? ", ..." : ""}.`,
96
+ "Watch or run the Playwright demo to verify the user-visible flow.",
97
+ "Check the captured test commands and any failing output before approval."
98
+ ];
99
+ }
@@ -0,0 +1,3 @@
1
+ import type { PrKitResult } from "./types.js";
2
+ export declare function renderMarkdownReport(result: PrKitResult): string;
3
+ export declare function renderHtmlReport(result: PrKitResult): string;
package/dist/report.js ADDED
@@ -0,0 +1,239 @@
1
+ import { basename } from "node:path";
2
+ export function renderMarkdownReport(result) {
3
+ const lines = [];
4
+ lines.push(`# ${result.feature}`);
5
+ lines.push("");
6
+ lines.push(`## Why`);
7
+ lines.push("");
8
+ lines.push(result.why);
9
+ lines.push("");
10
+ lines.push("## Review flow");
11
+ lines.push("");
12
+ result.flow.forEach((step, index) => {
13
+ lines.push(`${index + 1}. ${step}`);
14
+ });
15
+ lines.push("");
16
+ lines.push("## What changed");
17
+ lines.push("");
18
+ result.changes.forEach((change) => {
19
+ lines.push(`- ${change}`);
20
+ });
21
+ lines.push("");
22
+ if (result.technologies.length > 0) {
23
+ lines.push("## Technology used");
24
+ lines.push("");
25
+ result.technologies.forEach((technology) => {
26
+ lines.push(`- ${technology}`);
27
+ });
28
+ lines.push("");
29
+ }
30
+ lines.push("## Demo");
31
+ lines.push("");
32
+ lines.push("Primary browser flow: Playwright MCP or Playwright Test with isolated Chromium.");
33
+ lines.push("");
34
+ lines.push("Video policy: generated locally for review evidence; ignored by git and not uploaded automatically.");
35
+ lines.push("");
36
+ lines.push(`MCP guide: \`${basename(result.mcpGuidePath)}\``);
37
+ lines.push(`MCP script: \`${basename(result.mcpScriptPath)}\``);
38
+ lines.push("");
39
+ if (result.demoRun.mp4Path) {
40
+ lines.push(`<video src="${result.demoRun.mp4Path}" controls width="960"></video>`);
41
+ lines.push("");
42
+ lines.push(`[Open local MP4 demo](${result.demoRun.mp4Path})`);
43
+ }
44
+ else if (result.demoRun.videoPath) {
45
+ lines.push(`<video src="${result.demoRun.videoPath}" controls width="960"></video>`);
46
+ lines.push("");
47
+ lines.push(`[Open local demo video](${result.demoRun.videoPath})`);
48
+ if (result.demoRun.conversionError) {
49
+ lines.push("");
50
+ lines.push(`MP4 conversion failed: ${result.demoRun.conversionError}`);
51
+ }
52
+ }
53
+ else if (result.demoRun.attempted) {
54
+ lines.push("Demo attempted, but no video artifact was produced.");
55
+ }
56
+ else {
57
+ lines.push("Demo spec generated but not run yet.");
58
+ }
59
+ lines.push("");
60
+ lines.push(`Optional video spec: \`${basename(result.demoSpecPath)}\``);
61
+ lines.push("");
62
+ lines.push("## Testing");
63
+ lines.push("");
64
+ if (result.testResults.length === 0) {
65
+ lines.push("No test command was run. Add `--test \"<command>\"` to capture test evidence.");
66
+ }
67
+ else {
68
+ for (const testResult of result.testResults) {
69
+ lines.push(formatTestResult(testResult));
70
+ }
71
+ }
72
+ lines.push("");
73
+ lines.push("## Regression guardrails");
74
+ lines.push("");
75
+ lines.push(regressionSummary(result));
76
+ lines.push("");
77
+ lines.push("## Changed files");
78
+ lines.push("");
79
+ lines.push(renderChangedFiles(result.git));
80
+ lines.push("");
81
+ lines.push("## Git context");
82
+ lines.push("");
83
+ lines.push(`- Branch: \`${displayGitValue(result.git.branch)}\``);
84
+ lines.push(`- Base: \`${displayGitValue(result.git.base)}\``);
85
+ lines.push(`- Head: \`${displayGitValue(result.git.headSha)}\``);
86
+ if (result.git.commits.length > 0) {
87
+ lines.push(`- Commits:`);
88
+ for (const commit of result.git.commits.slice(0, 8)) {
89
+ lines.push(` - ${commit}`);
90
+ }
91
+ }
92
+ lines.push("");
93
+ return `${lines.join("\n")}\n`;
94
+ }
95
+ export function renderHtmlReport(result) {
96
+ const video = result.demoRun.videoPath
97
+ ? `<video src="${escapeHtml(result.demoRun.videoPath)}" controls></video>`
98
+ : `<p class="muted">${result.demoRun.attempted ? "Demo attempted, but no video was produced." : "Demo spec generated but not run yet."}</p>`;
99
+ return `<!doctype html>
100
+ <html lang="en">
101
+ <head>
102
+ <meta charset="utf-8">
103
+ <meta name="viewport" content="width=device-width, initial-scale=1">
104
+ <title>${escapeHtml(result.feature)} · PR review kit</title>
105
+ <style>
106
+ :root { color-scheme: light dark; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
107
+ body { margin: 0; background: #f7f8fb; color: #18202f; }
108
+ main { max-width: 1040px; margin: 0 auto; padding: 40px 24px 64px; }
109
+ h1 { margin: 0 0 10px; font-size: 32px; letter-spacing: 0; }
110
+ h2 { margin-top: 32px; font-size: 18px; }
111
+ p, li { line-height: 1.55; }
112
+ .panel { background: white; border: 1px solid #d9deea; border-radius: 8px; padding: 22px; margin-top: 18px; }
113
+ .meta { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
114
+ .pill { border: 1px solid #cad1df; border-radius: 999px; padding: 6px 10px; font-size: 13px; background: #fff; }
115
+ video { width: 100%; border-radius: 8px; border: 1px solid #ccd3e0; background: #111827; }
116
+ code, pre { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
117
+ pre { white-space: pre-wrap; overflow-wrap: anywhere; background: #101827; color: #edf2ff; padding: 14px; border-radius: 8px; }
118
+ .muted { color: #647085; }
119
+ .pass { color: #0f7a43; font-weight: 700; }
120
+ .fail { color: #b42318; font-weight: 700; }
121
+ @media (prefers-color-scheme: dark) {
122
+ body { background: #10131a; color: #edf2ff; }
123
+ .panel, .pill { background: #161b25; border-color: #2b3446; }
124
+ .muted { color: #aab4c8; }
125
+ }
126
+ </style>
127
+ </head>
128
+ <body>
129
+ <main>
130
+ <h1>${escapeHtml(result.feature)}</h1>
131
+ <p>${escapeHtml(result.why)}</p>
132
+ <div class="meta">
133
+ <span class="pill">Branch: ${escapeHtml(displayGitValue(result.git.branch))}</span>
134
+ <span class="pill">Base: ${escapeHtml(displayGitValue(result.git.base))}</span>
135
+ <span class="pill">Head: ${escapeHtml(displayGitValue(result.git.headSha))}</span>
136
+ </div>
137
+
138
+ <section class="panel">
139
+ <h2>Demo</h2>
140
+ <p>Primary browser flow: Playwright MCP or Playwright Test with isolated Chromium.</p>
141
+ <p class="muted">Local-only review evidence. The video is ignored by git and is not uploaded automatically.</p>
142
+ <p><code>${escapeHtml(basename(result.mcpGuidePath))}</code> · <code>${escapeHtml(basename(result.mcpScriptPath))}</code></p>
143
+ ${video}
144
+ </section>
145
+
146
+ <section class="panel">
147
+ <h2>Review flow</h2>
148
+ <ol>${result.flow.map((step) => `<li>${escapeHtml(step)}</li>`).join("")}</ol>
149
+ </section>
150
+
151
+ <section class="panel">
152
+ <h2>What changed</h2>
153
+ <ul>${result.changes.map((change) => `<li>${escapeHtml(change)}</li>`).join("")}</ul>
154
+ </section>
155
+
156
+ ${result.technologies.length > 0 ? `<section class="panel">
157
+ <h2>Technology used</h2>
158
+ <ul>${result.technologies.map((technology) => `<li>${escapeHtml(technology)}</li>`).join("")}</ul>
159
+ </section>` : ""}
160
+
161
+ <section class="panel">
162
+ <h2>Testing</h2>
163
+ ${renderHtmlTests(result.testResults)}
164
+ </section>
165
+
166
+ <section class="panel">
167
+ <h2>Regression guardrails</h2>
168
+ <p>${escapeHtml(regressionSummary(result))}</p>
169
+ </section>
170
+
171
+ <section class="panel">
172
+ <h2>Changed files</h2>
173
+ ${renderHtmlChangedFiles(result.git)}
174
+ </section>
175
+ </main>
176
+ </body>
177
+ </html>
178
+ `;
179
+ }
180
+ function formatTestResult(result) {
181
+ const status = result.exitCode === 0 ? "PASS" : "FAIL";
182
+ const body = trimTestOutput(`${result.stdout}\n${result.stderr}`);
183
+ const lines = [`- ${status} \`${result.command}\` (${formatMs(result.durationMs)})`];
184
+ if (body) {
185
+ lines.push("");
186
+ lines.push("```text");
187
+ lines.push(body);
188
+ lines.push("```");
189
+ }
190
+ return lines.join("\n");
191
+ }
192
+ function displayGitValue(value) {
193
+ return value && value !== "unknown" && value !== "HEAD" ? value : "unavailable";
194
+ }
195
+ function regressionSummary(result) {
196
+ const passingTests = result.testResults.filter((testResult) => testResult.exitCode === 0).length;
197
+ const failingTests = result.testResults.filter((testResult) => testResult.exitCode !== 0).length;
198
+ const demoStatus = result.demoRun.passed ? "demo passed" : result.demoRun.attempted ? "demo attempted with issues" : "demo not run";
199
+ return `This PR kit does not claim the change is risk-free. It shows ${passingTests} passing test command(s), ${failingTests} failing test command(s), ${demoStatus}, and ${result.git.changedFiles.length} changed file(s) for review.`;
200
+ }
201
+ function renderChangedFiles(git) {
202
+ if (git.changedFiles.length === 0) {
203
+ return git.available ? "No changed files detected against the selected base." : "Git metadata unavailable.";
204
+ }
205
+ return git.changedFiles.map((file) => `- ${file.status} \`${file.path}\``).join("\n");
206
+ }
207
+ function renderHtmlChangedFiles(git) {
208
+ if (git.changedFiles.length === 0) {
209
+ return `<p class="muted">${git.available ? "No changed files detected against the selected base." : "Git metadata unavailable."}</p>`;
210
+ }
211
+ return `<ul>${git.changedFiles.map((file) => `<li><code>${escapeHtml(file.status)} ${escapeHtml(file.path)}</code></li>`).join("")}</ul>`;
212
+ }
213
+ function renderHtmlTests(results) {
214
+ if (results.length === 0) {
215
+ return `<p class="muted">No test command was run. Add <code>--test "&lt;command&gt;"</code> to capture test evidence.</p>`;
216
+ }
217
+ return results
218
+ .map((result) => {
219
+ const passed = result.exitCode === 0;
220
+ const output = trimTestOutput(`${result.stdout}\n${result.stderr}`);
221
+ return `<div>
222
+ <p><span class="${passed ? "pass" : "fail"}">${passed ? "PASS" : "FAIL"}</span> <code>${escapeHtml(result.command)}</code> (${formatMs(result.durationMs)})</p>
223
+ ${output ? `<pre>${escapeHtml(output)}</pre>` : ""}
224
+ </div>`;
225
+ })
226
+ .join("");
227
+ }
228
+ function trimTestOutput(output) {
229
+ return output.trim().split("\n").slice(-30).join("\n");
230
+ }
231
+ function formatMs(ms) {
232
+ if (ms < 1000) {
233
+ return `${ms}ms`;
234
+ }
235
+ return `${(ms / 1000).toFixed(1)}s`;
236
+ }
237
+ function escapeHtml(value) {
238
+ return value.replace(/&/gu, "&amp;").replace(/</gu, "&lt;").replace(/>/gu, "&gt;").replace(/"/gu, "&quot;");
239
+ }
package/dist/runs.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function defaultRunOutputDir(label: string | undefined): string;
package/dist/runs.js ADDED
@@ -0,0 +1,30 @@
1
+ export function defaultRunOutputDir(label) {
2
+ return `.demo-this-pr/runs/${timestamp()}-${slug(normalizeLabel(label))}`;
3
+ }
4
+ function timestamp() {
5
+ const now = new Date();
6
+ const pad = (value, size = 2) => String(value).padStart(size, "0");
7
+ return [
8
+ now.getFullYear(),
9
+ pad(now.getMonth() + 1),
10
+ pad(now.getDate())
11
+ ].join("")
12
+ + "-"
13
+ + [pad(now.getHours()), pad(now.getMinutes()), pad(now.getSeconds())].join("")
14
+ + `-${pad(now.getMilliseconds(), 3)}`;
15
+ }
16
+ function slug(value) {
17
+ return value
18
+ .toLowerCase()
19
+ .replace(/^[a-z]+\/+/u, "")
20
+ .replace(/[^a-z0-9]+/gu, "-")
21
+ .replace(/^-|-$/gu, "")
22
+ .slice(0, 56) || "current-changes";
23
+ }
24
+ function normalizeLabel(label) {
25
+ const trimmed = label?.trim();
26
+ if (!trimmed || trimmed === "unknown" || trimmed === "HEAD") {
27
+ return "current-changes";
28
+ }
29
+ return trimmed;
30
+ }
@@ -0,0 +1,3 @@
1
+ import type { CommandResult } from "./types.js";
2
+ export declare function runShellCommand(command: string, cwd: string): Promise<CommandResult>;
3
+ export declare function runProcess(command: string, args: string[], cwd: string): Promise<CommandResult>;
package/dist/shell.js ADDED
@@ -0,0 +1,61 @@
1
+ import { spawn } from "node:child_process";
2
+ export async function runShellCommand(command, cwd) {
3
+ const startedAt = Date.now();
4
+ return new Promise((resolve) => {
5
+ const child = spawn(command, {
6
+ cwd,
7
+ shell: true,
8
+ stdio: ["ignore", "pipe", "pipe"]
9
+ });
10
+ let stdout = "";
11
+ let stderr = "";
12
+ child.stdout.on("data", (chunk) => {
13
+ stdout += chunk.toString();
14
+ });
15
+ child.stderr.on("data", (chunk) => {
16
+ stderr += chunk.toString();
17
+ });
18
+ child.on("error", (error) => {
19
+ stderr += `\n${error.message}`;
20
+ });
21
+ child.on("close", (exitCode) => {
22
+ resolve({
23
+ command,
24
+ exitCode,
25
+ durationMs: Date.now() - startedAt,
26
+ stdout,
27
+ stderr
28
+ });
29
+ });
30
+ });
31
+ }
32
+ export async function runProcess(command, args, cwd) {
33
+ const startedAt = Date.now();
34
+ return new Promise((resolve) => {
35
+ const child = spawn(command, args, {
36
+ cwd,
37
+ shell: false,
38
+ stdio: ["ignore", "pipe", "pipe"]
39
+ });
40
+ let stdout = "";
41
+ let stderr = "";
42
+ child.stdout.on("data", (chunk) => {
43
+ stdout += chunk.toString();
44
+ });
45
+ child.stderr.on("data", (chunk) => {
46
+ stderr += chunk.toString();
47
+ });
48
+ child.on("error", (error) => {
49
+ stderr += `\n${error.message}`;
50
+ });
51
+ child.on("close", (exitCode) => {
52
+ resolve({
53
+ command: [command, ...args].join(" "),
54
+ exitCode,
55
+ durationMs: Date.now() - startedAt,
56
+ stdout,
57
+ stderr
58
+ });
59
+ });
60
+ });
61
+ }
@@ -0,0 +1,101 @@
1
+ export interface PrKitOptions {
2
+ cwd: string;
3
+ outputDir: string;
4
+ feature?: string;
5
+ why?: string;
6
+ base?: string;
7
+ flow: string[];
8
+ changes: string[];
9
+ technologies: string[];
10
+ testCommands: string[];
11
+ runDemo: boolean;
12
+ headed: boolean;
13
+ showReport: boolean;
14
+ demoUrl?: string;
15
+ demoSteps: string[];
16
+ demoPlanPath?: string;
17
+ }
18
+ export interface GitFileChange {
19
+ status: string;
20
+ path: string;
21
+ }
22
+ export interface GitContext {
23
+ available: boolean;
24
+ root: string;
25
+ branch: string;
26
+ base: string;
27
+ headSha: string;
28
+ commits: string[];
29
+ changedFiles: GitFileChange[];
30
+ }
31
+ export interface DemoPlan {
32
+ title: string;
33
+ baseUrl?: string;
34
+ why?: string;
35
+ context?: DemoContext;
36
+ steps: DemoStep[];
37
+ }
38
+ export interface DemoContext {
39
+ changes: string[];
40
+ technologies: string[];
41
+ tests: TestSummary[];
42
+ branch: string;
43
+ base: string;
44
+ changedFiles: string[];
45
+ }
46
+ export interface TestSummary {
47
+ command: string;
48
+ status: "passed" | "failed";
49
+ durationMs: number;
50
+ }
51
+ export interface DemoStep {
52
+ caption?: string;
53
+ goto?: string;
54
+ click?: string;
55
+ fill?: {
56
+ selector: string;
57
+ value: string;
58
+ };
59
+ press?: {
60
+ selector: string;
61
+ key: string;
62
+ };
63
+ expectText?: string;
64
+ screenshot?: string;
65
+ waitMs?: number;
66
+ }
67
+ export interface CommandResult {
68
+ command: string;
69
+ exitCode: number | null;
70
+ durationMs: number;
71
+ stdout: string;
72
+ stderr: string;
73
+ }
74
+ export interface DemoRunResult {
75
+ attempted: boolean;
76
+ passed: boolean;
77
+ command?: string;
78
+ output?: string;
79
+ videoPath?: string;
80
+ webmPath?: string;
81
+ mp4Path?: string;
82
+ conversionError?: string;
83
+ error?: string;
84
+ }
85
+ export interface PrKitResult {
86
+ outputDir: string;
87
+ markdownPath: string;
88
+ htmlPath: string;
89
+ demoSpecPath: string;
90
+ demoPlanPath: string;
91
+ mcpGuidePath: string;
92
+ mcpScriptPath: string;
93
+ git: GitContext;
94
+ feature: string;
95
+ why: string;
96
+ flow: string[];
97
+ changes: string[];
98
+ technologies: string[];
99
+ testResults: CommandResult[];
100
+ demoRun: DemoRunResult;
101
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export declare function openReportViewer(htmlPath: string): Promise<void>;
package/dist/viewer.js ADDED
@@ -0,0 +1,32 @@
1
+ import { pathToFileURL } from "node:url";
2
+ import { chromium } from "@playwright/test";
3
+ export async function openReportViewer(htmlPath) {
4
+ const browser = await chromium.launch({ headless: false });
5
+ const page = await browser.newPage({ viewport: { width: 1280, height: 900 } });
6
+ await page.goto(pathToFileURL(htmlPath).toString());
7
+ await page.bringToFront();
8
+ await new Promise((resolve) => {
9
+ let finished = false;
10
+ const finish = async () => {
11
+ if (finished)
12
+ return;
13
+ finished = true;
14
+ try {
15
+ if (browser.isConnected()) {
16
+ await browser.close();
17
+ }
18
+ }
19
+ catch {
20
+ // Browser may already be closed by the user.
21
+ }
22
+ resolve();
23
+ };
24
+ browser.on("disconnected", () => {
25
+ finished = true;
26
+ resolve();
27
+ });
28
+ page.on("close", () => {
29
+ void finish();
30
+ });
31
+ });
32
+ }
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "demo-this-pr",
3
+ "version": "0.1.0",
4
+ "description": "Record local PR demos with isolated Playwright Chromium, MP4 evidence, test output, and review-ready reports.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ },
13
+ "./package.json": "./package.json"
14
+ },
15
+ "bin": {
16
+ "demo-this-pr": "dist/cli.js",
17
+ "demo-pr": "dist/cli.js"
18
+ },
19
+ "files": [
20
+ "assets",
21
+ "dist",
22
+ "LICENSE",
23
+ "README.md"
24
+ ],
25
+ "keywords": [
26
+ "pull-request",
27
+ "playwright",
28
+ "demo",
29
+ "review",
30
+ "mp4",
31
+ "developer-tools"
32
+ ],
33
+ "scripts": {
34
+ "build": "tsc -p tsconfig.json",
35
+ "check": "npm run build && npm test",
36
+ "dev": "tsx src/cli.ts",
37
+ "pack:dry-run": "npm pack --dry-run",
38
+ "prepack": "npm run build",
39
+ "publish:check": "npm run check && npm run pack:dry-run",
40
+ "test": "vitest run"
41
+ },
42
+ "publishConfig": {
43
+ "access": "public"
44
+ },
45
+ "engines": {
46
+ "node": ">=20"
47
+ },
48
+ "dependencies": {
49
+ "@playwright/test": "^1.57.0",
50
+ "commander": "^14.0.2",
51
+ "ffmpeg-static": "^5.3.0",
52
+ "picocolors": "^1.1.1"
53
+ },
54
+ "devDependencies": {
55
+ "@types/node": "^24.10.1",
56
+ "tsx": "^4.20.6",
57
+ "typescript": "^5.9.3",
58
+ "vitest": "^4.0.13"
59
+ },
60
+ "license": "MIT"
61
+ }