afterbefore 0.2.14 → 0.2.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,9 +3,7 @@
3
3
  [![npm version](https://img.shields.io/npm/v/afterbefore?color=blue)](https://www.npmjs.com/package/afterbefore)
4
4
  [![npm downloads](https://img.shields.io/npm/dm/afterbefore)](https://www.npmjs.com/package/afterbefore)
5
5
 
6
- Before/after screenshot capture for Next.js development. A floating icon in your dev server — click to capture before, make your changes, click to capture after.
7
-
8
- No CLI. No Playwright. No second server. The browser is the tool.
6
+ Screenshot capture for Next.js development. A floating overlay in your dev server — click to capture components, viewports, or full pages. No CLI. No Playwright. No second server.
9
7
 
10
8
  ## Setup
11
9
 
@@ -51,60 +49,73 @@ export default withAfterBefore({
51
49
 
52
50
  The config wrapper adds a dev-only rewrite from `/__afterbefore/*` to the API route. It does nothing in production.
53
51
 
54
- > **Next.js 16+ / Turbopack:** If `withAfterBefore` fails to import (`ERR_PACKAGE_PATH_NOT_EXPORTED`), you can inline the rewrite as a workaround:
52
+ > **Next.js 16+ / Turbopack:** If `withAfterBefore` fails to import (`ERR_PACKAGE_PATH_NOT_EXPORTED`), you can inline the rewrite:
55
53
  >
56
54
  > ```ts
57
- > // Workaround if withAfterBefore import fails with Turbopack:
58
55
  > rewrites: process.env.NODE_ENV === "development"
59
56
  > ? async () => [{ source: "/__afterbefore/:path*", destination: "/api/afterbefore/:path*" }]
60
57
  > : undefined,
61
58
  > ```
62
59
 
60
+ ## Capture modes
63
61
 
64
- ## How it works
62
+ | Mode | Behavior |
63
+ |------|----------|
64
+ | **Component** | Hover to inspect elements — click to capture a specific component |
65
+ | **Viewport** | Captures the visible browser area |
66
+ | **Full Page** | Captures the entire document height |
67
+
68
+ Screenshots are rendered from the DOM using [snapdom](https://github.com/nicorevin/snapdom), which captures exactly what you see — logged-in state, open modals, scroll position, form inputs — without needing a headless browser.
69
+
70
+ ## Frame settings
71
+
72
+ When using **Component** mode, you can wrap the captured element in a presentation frame:
73
+
74
+ - **Frame size** — presets (1920×1080, 1080×1080, 1200×630, 1080×1920) or custom dimensions
75
+ - **Background** — solid color (editable hex) or uploaded image (cover-fit)
76
+ - **Padding** — adjustable spacing around the component
77
+ - **Auto-scaling** — components that exceed the frame are scaled down to fit
65
78
 
66
- 1. A floating icon appears in the bottom-left corner of your dev server
67
- 2. Click it and choose a capture mode — the screenshot is saved as `before.png`
68
- 3. Make your changes
69
- 4. Click again — the screenshot is saved as `after.png`
70
- 5. Open the output folder, copy markdown for a PR comment, or push directly to your GitHub PR
79
+ Frame settings persist in `localStorage` across sessions.
71
80
 
72
- Screenshots are captured using [html-to-image](https://github.com/nicorevin/html-to-image), which renders the DOM to a canvas. This captures exactly what you see — logged-in state, open modals, scroll position, form inputs — without needing a headless browser.
81
+ ## Where screenshots go
73
82
 
74
- Screenshots land on your Desktop, named after your branch:
83
+ Screenshots are saved to your Desktop by default, named after the current git branch:
75
84
 
76
85
  ```
77
- ~/Desktop/
78
- my-feature-before.png
79
- my-feature-after.png
86
+ ~/Desktop/my-feature.png
80
87
  ```
81
88
 
82
- ## Capture modes
83
-
84
- | Mode | Behavior |
85
- |------|----------|
86
- | **Viewport** | Captures the visible area |
87
- | **Full Page** | Captures the entire document height |
88
- | **Select Area** | Crosshair cursor — drag a rectangle to capture a specific region |
89
+ You can change the save location from the toolbar settings. On macOS, a native folder picker is available. The setting is stored in `.afterbefore/config.json` in your project root.
89
90
 
90
- ## PR integration
91
+ ## After capture
91
92
 
92
- When both screenshots are captured, click the icon to:
93
+ Once a screenshot is captured, click the icon to access:
93
94
 
94
- - **Open Folder** — opens `.afterbefore/` in your file manager
95
+ - **Open Folder** — opens the save directory in your file manager
95
96
  - **Copy Markdown** — copies a before/after comparison table to your clipboard
96
- - **Push to PR** — commits the screenshots, pushes, and posts a PR comment with the images (requires [GitHub CLI](https://cli.github.com/))
97
- - **Reset** — start over
97
+ - **Push to PR** — commits the screenshot to `.afterbefore/`, pushes, and posts a PR comment with the image (requires [GitHub CLI](https://cli.github.com/))
98
+ - **Reset** — start a new capture
99
+
100
+ ## How it works
101
+
102
+ 1. A floating camera icon appears in the corner of your dev server (draggable)
103
+ 2. Click it to open the toolbar — pick a capture mode
104
+ 3. For **Component** mode, hover over elements to highlight them, then click to capture
105
+ 4. For **Viewport** or **Full Page**, the capture happens immediately
106
+ 5. The screenshot is sent to the API route, which writes it to disk
107
+ 6. The status menu appears with options to share or start over
108
+
109
+ The overlay and all dev UI elements (including Next.js indicators) are automatically excluded from captures.
98
110
 
99
111
  ## Known limitations
100
112
 
101
113
  - Web fonts may render slightly differently than in the browser
102
114
  - Complex CSS (`backdrop-filter`, `mix-blend-mode`) may not reproduce perfectly
103
115
  - iframes and tainted canvas/WebGL content are not captured
116
+ - Folder picker is macOS-only (other platforms use the default save directory)
104
117
  - Next.js only (React overlay, API routes)
105
118
 
106
- For pixel-perfect CI screenshots, see the roadmap for Phase 1 (Playwright-based CLI).
107
-
108
119
  ## Requirements
109
120
 
110
121
  - Next.js >= 14 (app router)
@@ -68,8 +68,12 @@ async function handlePickFolder() {
68
68
 
69
69
  // src/server/save.ts
70
70
  import { NextResponse as NextResponse2 } from "next/server";
71
- import { writeFile as writeFile2 } from "fs/promises";
71
+ import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
72
72
  import { join as join2 } from "path";
73
+ import { homedir as homedir2 } from "os";
74
+
75
+ // src/server/git.ts
76
+ import { basename } from "path";
73
77
  import { execFile as execFile2 } from "child_process";
74
78
  import { promisify as promisify2 } from "util";
75
79
  var execFileAsync2 = promisify2(execFile2);
@@ -79,6 +83,25 @@ async function getBranch() {
79
83
  });
80
84
  return stdout.trim();
81
85
  }
86
+ async function getRepoName() {
87
+ try {
88
+ const { stdout } = await execFileAsync2("git", ["rev-parse", "--show-toplevel"], {
89
+ cwd: process.cwd()
90
+ });
91
+ return basename(stdout.trim());
92
+ } catch {
93
+ return basename(process.cwd());
94
+ }
95
+ }
96
+
97
+ // src/server/save.ts
98
+ async function saveGlobalCopy(buffer, repoName, branch) {
99
+ const globalDir = join2(homedir2(), ".afterbefore", repoName, branch);
100
+ await mkdir2(globalDir, { recursive: true });
101
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
102
+ const filepath = join2(globalDir, `${timestamp}.png`);
103
+ await writeFile2(filepath, buffer);
104
+ }
82
105
  var VALID_MODES = ["fullpage", "viewport", "component"];
83
106
  var DATA_URL_PREFIX = "data:image/png;base64,";
84
107
  async function handleSave(req) {
@@ -106,8 +129,11 @@ async function handleSave(req) {
106
129
  }
107
130
  const base64 = image.slice(DATA_URL_PREFIX.length);
108
131
  const buffer = Buffer.from(base64, "base64");
109
- const branch = await getBranch();
110
- const saveDir = await getSaveDir();
132
+ const [branch, repoName, saveDir] = await Promise.all([
133
+ getBranch(),
134
+ getRepoName(),
135
+ getSaveDir()
136
+ ]);
111
137
  const filename = `${branch}.png`;
112
138
  const filepath = join2(saveDir, filename);
113
139
  try {
@@ -118,13 +144,17 @@ async function handleSave(req) {
118
144
  { status: 500 }
119
145
  );
120
146
  }
147
+ try {
148
+ await saveGlobalCopy(buffer, repoName, branch);
149
+ } catch {
150
+ }
121
151
  return NextResponse2.json({ success: true, path: filepath });
122
152
  }
123
153
 
124
154
  // src/server/push.ts
125
155
  import { NextResponse as NextResponse3 } from "next/server";
126
156
  import { execFile as execFile3 } from "child_process";
127
- import { access, mkdir as mkdir2, copyFile } from "fs/promises";
157
+ import { access, mkdir as mkdir3, copyFile } from "fs/promises";
128
158
  import { join as join3 } from "path";
129
159
  import { promisify as promisify3 } from "util";
130
160
  var execFileAsync3 = promisify3(execFile3);
@@ -186,7 +216,7 @@ async function handlePush(req) {
186
216
  const repoDir = join3(process.cwd(), ".afterbefore");
187
217
  const repoFile = join3(repoDir, `${branch}.png`);
188
218
  try {
189
- await mkdir2(repoDir, { recursive: true });
219
+ await mkdir3(repoDir, { recursive: true });
190
220
  await copyFile(screenshotPath, repoFile);
191
221
  } catch (err) {
192
222
  return NextResponse3.json(
@@ -260,12 +290,121 @@ async function handleOpen(_req) {
260
290
  return NextResponse4.json({ success: true });
261
291
  }
262
292
 
293
+ // src/server/history.ts
294
+ import { NextResponse as NextResponse5 } from "next/server";
295
+ import { readdir, readFile as readFile2 } from "fs/promises";
296
+ import { join as join4, resolve } from "path";
297
+ import { homedir as homedir3 } from "os";
298
+ var ARCHIVE_DIR = join4(homedir3(), ".afterbefore");
299
+ var MAX_SCREENSHOTS = 50;
300
+ function isSafeName(name) {
301
+ return /^[a-zA-Z0-9._\-]+$/.test(name);
302
+ }
303
+ function isSafeBranchName(name) {
304
+ return /^[a-zA-Z0-9._\-/]+$/.test(name) && !name.includes("..") && !name.startsWith("/") && !name.endsWith("/");
305
+ }
306
+ async function listDirs(dir) {
307
+ try {
308
+ const entries = await readdir(dir, { withFileTypes: true });
309
+ return entries.filter((e) => e.isDirectory()).map((e) => e.name).sort();
310
+ } catch {
311
+ return [];
312
+ }
313
+ }
314
+ async function listBranches(repoDir) {
315
+ const branches = [];
316
+ async function walk(dir, prefix) {
317
+ try {
318
+ const entries = await readdir(dir, { withFileTypes: true });
319
+ const hasPngs = entries.some((e) => e.isFile() && e.name.endsWith(".png"));
320
+ if (hasPngs && prefix) {
321
+ branches.push(prefix);
322
+ }
323
+ for (const entry of entries) {
324
+ if (entry.isDirectory()) {
325
+ await walk(join4(dir, entry.name), prefix ? `${prefix}/${entry.name}` : entry.name);
326
+ }
327
+ }
328
+ } catch {
329
+ }
330
+ }
331
+ await walk(repoDir, "");
332
+ return branches.sort();
333
+ }
334
+ async function listPngs(dir) {
335
+ try {
336
+ const entries = await readdir(dir, { withFileTypes: true });
337
+ return entries.filter((e) => e.isFile() && e.name.endsWith(".png")).map((e) => ({
338
+ filename: e.name,
339
+ timestamp: e.name.replace(/\.png$/, "")
340
+ })).sort((a, b) => b.filename.localeCompare(a.filename)).slice(0, MAX_SCREENSHOTS);
341
+ } catch {
342
+ return [];
343
+ }
344
+ }
345
+ async function handleHistoryList(req) {
346
+ const url = req.nextUrl;
347
+ const repoParam = url.searchParams.get("repo");
348
+ const branchParam = url.searchParams.get("branch");
349
+ const [currentRepo, currentBranch] = await Promise.all([
350
+ getRepoName(),
351
+ getBranch()
352
+ ]);
353
+ const repos = await listDirs(ARCHIVE_DIR);
354
+ let branches = [];
355
+ let screenshots = [];
356
+ const selectedRepo = repoParam || currentRepo;
357
+ if (selectedRepo && isSafeName(selectedRepo)) {
358
+ branches = await listBranches(join4(ARCHIVE_DIR, selectedRepo));
359
+ const selectedBranch = branchParam || currentBranch;
360
+ if (selectedBranch && isSafeBranchName(selectedBranch)) {
361
+ screenshots = await listPngs(join4(ARCHIVE_DIR, selectedRepo, selectedBranch));
362
+ }
363
+ }
364
+ return NextResponse5.json({
365
+ repos,
366
+ currentRepo,
367
+ branches,
368
+ currentBranch,
369
+ screenshots
370
+ });
371
+ }
372
+ async function handleHistoryImage(req) {
373
+ const url = req.nextUrl;
374
+ const repo = url.searchParams.get("repo");
375
+ const branch = url.searchParams.get("branch");
376
+ const file = url.searchParams.get("file");
377
+ if (!repo || !branch || !file) {
378
+ return NextResponse5.json({ error: "Missing repo, branch, or file param" }, { status: 400 });
379
+ }
380
+ if (!isSafeName(repo) || !isSafeBranchName(branch) || !isSafeName(file)) {
381
+ return NextResponse5.json({ error: "Invalid parameter" }, { status: 400 });
382
+ }
383
+ const filepath = resolve(ARCHIVE_DIR, repo, branch, file);
384
+ if (!filepath.startsWith(ARCHIVE_DIR)) {
385
+ return NextResponse5.json({ error: "Invalid parameter" }, { status: 400 });
386
+ }
387
+ try {
388
+ const buffer = await readFile2(filepath);
389
+ return new NextResponse5(buffer, {
390
+ headers: {
391
+ "Content-Type": "image/png",
392
+ "Cache-Control": "private, max-age=3600"
393
+ }
394
+ });
395
+ } catch {
396
+ return NextResponse5.json({ error: "Not found" }, { status: 404 });
397
+ }
398
+ }
399
+
263
400
  export {
264
401
  handleGetConfig,
265
402
  handleSetConfig,
266
403
  handlePickFolder,
267
404
  handleSave,
268
405
  handlePush,
269
- handleOpen
406
+ handleOpen,
407
+ handleHistoryList,
408
+ handleHistoryImage
270
409
  };
271
- //# sourceMappingURL=chunk-N33DB2F6.js.map
410
+ //# sourceMappingURL=chunk-XSDO6N5Q.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/server/config.ts","../src/server/save.ts","../src/server/git.ts","../src/server/push.ts","../src/server/open.ts","../src/server/history.ts"],"sourcesContent":["import { readFile, writeFile, mkdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { homedir } from \"node:os\";\nimport { NextRequest, NextResponse } from \"next/server\";\nimport { execFile } from \"node:child_process\";\nimport { promisify } from \"node:util\";\n\nconst execFileAsync = promisify(execFile);\n\ninterface Config {\n saveDir: string;\n}\n\nconst CONFIG_DIR = join(process.cwd(), \".afterbefore\");\nconst CONFIG_PATH = join(CONFIG_DIR, \"config.json\");\nconst DEFAULT_SAVE_DIR = join(homedir(), \"Desktop\");\n\nasync function readConfig(): Promise<Config> {\n try {\n const raw = await readFile(CONFIG_PATH, \"utf-8\");\n const parsed = JSON.parse(raw);\n return { saveDir: parsed.saveDir || DEFAULT_SAVE_DIR };\n } catch {\n return { saveDir: DEFAULT_SAVE_DIR };\n }\n}\n\nasync function writeConfig(config: Config): Promise<void> {\n await mkdir(CONFIG_DIR, { recursive: true });\n await writeFile(CONFIG_PATH, JSON.stringify(config, null, 2) + \"\\n\");\n}\n\nexport async function getSaveDir(): Promise<string> {\n const config = await readConfig();\n return config.saveDir;\n}\n\nexport async function handleGetConfig(): Promise<NextResponse> {\n const config = await readConfig();\n return NextResponse.json(config);\n}\n\nexport async function handleSetConfig(req: NextRequest): Promise<NextResponse> {\n let body: Partial<Config>;\n try {\n body = await req.json();\n } catch {\n return NextResponse.json({ error: \"Invalid JSON\" }, { status: 400 });\n }\n\n if (typeof body.saveDir !== \"string\" || body.saveDir.trim() === \"\") {\n return NextResponse.json({ error: \"Invalid saveDir\" }, { status: 400 });\n }\n\n const config: Config = { saveDir: body.saveDir.trim() };\n await writeConfig(config);\n return NextResponse.json(config);\n}\n\nexport async function handlePickFolder(): Promise<NextResponse> {\n if (process.platform !== \"darwin\") {\n return NextResponse.json(\n { error: \"Folder picker is only supported on macOS\" },\n { status: 501 },\n );\n }\n\n try {\n const { stdout } = await execFileAsync(\"osascript\", [\n \"-e\",\n 'POSIX path of (choose folder with prompt \"Select screenshot save location\")',\n ]);\n const folder = stdout.trim().replace(/\\/$/, \"\");\n if (!folder) {\n return NextResponse.json({ cancelled: true });\n }\n return NextResponse.json({ folder });\n } catch {\n // User cancelled the dialog\n return NextResponse.json({ cancelled: true });\n }\n}\n","import { NextRequest, NextResponse } from \"next/server\";\nimport { writeFile, mkdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { homedir } from \"node:os\";\nimport { getSaveDir } from \"./config\";\nimport { getBranch, getRepoName } from \"./git\";\n\nasync function saveGlobalCopy(buffer: Buffer, repoName: string, branch: string): Promise<void> {\n const globalDir = join(homedir(), \".afterbefore\", repoName, branch);\n await mkdir(globalDir, { recursive: true });\n const timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n const filepath = join(globalDir, `${timestamp}.png`);\n await writeFile(filepath, buffer);\n}\n\nconst VALID_MODES = [\"fullpage\", \"viewport\", \"component\"] as const;\n\nconst DATA_URL_PREFIX = \"data:image/png;base64,\";\n\ninterface SaveRequestBody {\n mode: string;\n image: string;\n}\n\nexport async function handleSave(req: NextRequest): Promise<NextResponse> {\n let body: SaveRequestBody;\n try {\n body = await req.json();\n } catch {\n return NextResponse.json(\n { error: \"Invalid JSON body\" },\n { status: 400 },\n );\n }\n\n const { mode, image } = body;\n\n if (!VALID_MODES.includes(mode as (typeof VALID_MODES)[number])) {\n return NextResponse.json(\n { error: `Invalid mode: must be \"fullpage\", \"viewport\", or \"component\"` },\n { status: 400 },\n );\n }\n\n if (typeof image !== \"string\" || !image.startsWith(DATA_URL_PREFIX)) {\n return NextResponse.json(\n { error: \"Invalid image: must be a data:image/png;base64 data URL\" },\n { status: 400 },\n );\n }\n\n const base64 = image.slice(DATA_URL_PREFIX.length);\n const buffer = Buffer.from(base64, \"base64\");\n\n const [branch, repoName, saveDir] = await Promise.all([\n getBranch(),\n getRepoName(),\n getSaveDir(),\n ]);\n const filename = `${branch}.png`;\n const filepath = join(saveDir, filename);\n\n try {\n await writeFile(filepath, buffer);\n } catch (err) {\n return NextResponse.json(\n { error: \"Failed to write screenshot\", detail: String(err) },\n { status: 500 },\n );\n }\n\n // Always keep a copy in the global ~/.afterbefore/<repo>/<branch>/ archive\n try {\n await saveGlobalCopy(buffer, repoName, branch);\n } catch {\n // Non-fatal: don't fail the save if the global copy fails\n }\n\n return NextResponse.json({ success: true, path: filepath });\n}\n","import { basename } from \"node:path\";\nimport { execFile } from \"node:child_process\";\nimport { promisify } from \"node:util\";\n\nconst execFileAsync = promisify(execFile);\n\nexport async function getBranch(): Promise<string> {\n const { stdout } = await execFileAsync(\"git\", [\"rev-parse\", \"--abbrev-ref\", \"HEAD\"], {\n cwd: process.cwd(),\n });\n return stdout.trim();\n}\n\nexport async function getRepoName(): Promise<string> {\n try {\n const { stdout } = await execFileAsync(\"git\", [\"rev-parse\", \"--show-toplevel\"], {\n cwd: process.cwd(),\n });\n return basename(stdout.trim());\n } catch {\n return basename(process.cwd());\n }\n}\n","import { NextRequest, NextResponse } from \"next/server\";\nimport { execFile } from \"node:child_process\";\nimport { access, mkdir, copyFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { promisify } from \"node:util\";\nimport { getSaveDir } from \"./config\";\n\nconst execFileAsync = promisify(execFile);\n\ninterface PrInfo {\n number: number;\n url: string;\n headRepository: { owner: { login: string }; name: string };\n headRefName: string;\n}\n\nasync function run(\n cmd: string,\n args: string[],\n): Promise<{ stdout: string; stderr: string }> {\n return execFileAsync(cmd, args, { cwd: process.cwd() });\n}\n\nasync function ghAvailable(): Promise<boolean> {\n try {\n await run(\"gh\", [\"--version\"]);\n return true;\n } catch {\n return false;\n }\n}\n\nasync function getPrInfo(): Promise<PrInfo | null> {\n try {\n const { stdout } = await run(\"gh\", [\n \"pr\",\n \"view\",\n \"--json\",\n \"number,url,headRepository,headRefName\",\n ]);\n return JSON.parse(stdout) as PrInfo;\n } catch {\n return null;\n }\n}\n\nasync function fileExists(path: string): Promise<boolean> {\n try {\n await access(path);\n return true;\n } catch {\n return false;\n }\n}\n\nexport async function handlePush(req: NextRequest): Promise<NextResponse> {\n // 1. Check gh CLI availability\n if (!(await ghAvailable())) {\n return NextResponse.json(\n { success: false, error: \"GitHub CLI (gh) is not installed or not in PATH\" },\n { status: 500 },\n );\n }\n\n // 2. Check for active PR\n const pr = await getPrInfo();\n if (!pr) {\n return NextResponse.json(\n { success: false, error: \"No PR found for current branch\" },\n { status: 404 },\n );\n }\n\n // 3. Check that screenshot file exists in save directory\n const saveDir = await getSaveDir();\n const branch = pr.headRefName;\n const screenshotPath = join(saveDir, `${branch}.png`);\n\n if (!(await fileExists(screenshotPath))) {\n return NextResponse.json(\n { success: false, error: `Missing screenshot: ${branch}.png` },\n { status: 400 },\n );\n }\n\n // 4. Copy into repo, stage, commit, and push\n const repoDir = join(process.cwd(), \".afterbefore\");\n const repoFile = join(repoDir, `${branch}.png`);\n\n try {\n await mkdir(repoDir, { recursive: true });\n await copyFile(screenshotPath, repoFile);\n } catch (err) {\n return NextResponse.json(\n { success: false, error: \"Failed to copy screenshot into repo\", detail: String(err) },\n { status: 500 },\n );\n }\n\n try {\n await run(\"git\", [\"add\", repoFile]);\n await run(\"git\", [\n \"commit\",\n \"-m\",\n \"chore: add screenshot\",\n ]);\n } catch (err) {\n const msg = String(err);\n if (!msg.includes(\"nothing to commit\")) {\n return NextResponse.json(\n { success: false, error: \"Failed to commit screenshot\", detail: msg },\n { status: 500 },\n );\n }\n }\n\n try {\n await run(\"git\", [\"push\"]);\n } catch (err) {\n return NextResponse.json(\n { success: false, error: \"Failed to push to remote\", detail: String(err) },\n { status: 500 },\n );\n }\n\n // 5. Build raw GitHub URL for the image\n const owner = pr.headRepository.owner.login;\n const repo = pr.headRepository.name;\n const rawBase = `https://raw.githubusercontent.com/${owner}/${repo}/${branch}`;\n const imageUrl = `${rawBase}/.afterbefore/${branch}.png`;\n\n // Cache-bust with timestamp so GitHub doesn't serve stale images\n const ts = Date.now();\n const commentBody = `## Screenshot\\n\\n![screenshot](${imageUrl}?t=${ts})`;\n\n // 6. Post PR comment\n let commentUrl: string | undefined;\n try {\n const { stdout } = await run(\"gh\", [\n \"pr\",\n \"comment\",\n String(pr.number),\n \"--body\",\n commentBody,\n ]);\n commentUrl = stdout.trim() || undefined;\n } catch (err) {\n return NextResponse.json(\n { success: false, error: \"Failed to post PR comment\", detail: String(err) },\n { status: 500 },\n );\n }\n\n return NextResponse.json({\n success: true,\n pr: pr.number,\n prUrl: pr.url,\n commentUrl,\n });\n}\n","import { NextRequest, NextResponse } from \"next/server\";\nimport { execFile } from \"node:child_process\";\nimport { getSaveDir } from \"./config\";\n\nexport async function handleOpen(_req: NextRequest): Promise<NextResponse> {\n const desktop = await getSaveDir();\n\n const cmd =\n process.platform === \"darwin\"\n ? \"open\"\n : process.platform === \"win32\"\n ? \"explorer\"\n : \"xdg-open\";\n\n execFile(cmd, [desktop]);\n\n return NextResponse.json({ success: true });\n}\n","import { NextRequest, NextResponse } from \"next/server\";\nimport { readdir, readFile } from \"node:fs/promises\";\nimport { join, resolve } from \"node:path\";\nimport { homedir } from \"node:os\";\nimport { getBranch, getRepoName } from \"./git\";\n\nconst ARCHIVE_DIR = join(homedir(), \".afterbefore\");\nconst MAX_SCREENSHOTS = 50;\n\nfunction isSafeName(name: string): boolean {\n return /^[a-zA-Z0-9._\\-]+$/.test(name);\n}\n\n/** Branch names can contain slashes (e.g. \"fix/something\"). Reject \"..\" and leading/trailing slashes. */\nfunction isSafeBranchName(name: string): boolean {\n return /^[a-zA-Z0-9._\\-/]+$/.test(name) && !name.includes(\"..\") && !name.startsWith(\"/\") && !name.endsWith(\"/\");\n}\n\nasync function listDirs(dir: string): Promise<string[]> {\n try {\n const entries = await readdir(dir, { withFileTypes: true });\n return entries.filter((e) => e.isDirectory()).map((e) => e.name).sort();\n } catch {\n return [];\n }\n}\n\n/**\n * Recursively find branch paths under a repo directory.\n * A branch path is any relative path that contains .png files.\n * e.g. \"main\", \"fix/capture-mode-toolbar\"\n */\nasync function listBranches(repoDir: string): Promise<string[]> {\n const branches: string[] = [];\n\n async function walk(dir: string, prefix: string) {\n try {\n const entries = await readdir(dir, { withFileTypes: true });\n const hasPngs = entries.some((e) => e.isFile() && e.name.endsWith(\".png\"));\n if (hasPngs && prefix) {\n branches.push(prefix);\n }\n for (const entry of entries) {\n if (entry.isDirectory()) {\n await walk(join(dir, entry.name), prefix ? `${prefix}/${entry.name}` : entry.name);\n }\n }\n } catch {\n // directory doesn't exist or can't be read\n }\n }\n\n await walk(repoDir, \"\");\n return branches.sort();\n}\n\nasync function listPngs(dir: string): Promise<{ filename: string; timestamp: string }[]> {\n try {\n const entries = await readdir(dir, { withFileTypes: true });\n return entries\n .filter((e) => e.isFile() && e.name.endsWith(\".png\"))\n .map((e) => ({\n filename: e.name,\n timestamp: e.name.replace(/\\.png$/, \"\"),\n }))\n .sort((a, b) => b.filename.localeCompare(a.filename))\n .slice(0, MAX_SCREENSHOTS);\n } catch {\n return [];\n }\n}\n\nexport async function handleHistoryList(req: NextRequest): Promise<NextResponse> {\n const url = req.nextUrl;\n const repoParam = url.searchParams.get(\"repo\");\n const branchParam = url.searchParams.get(\"branch\");\n\n const [currentRepo, currentBranch] = await Promise.all([\n getRepoName(),\n getBranch(),\n ]);\n\n const repos = await listDirs(ARCHIVE_DIR);\n\n let branches: string[] = [];\n let screenshots: { filename: string; timestamp: string }[] = [];\n\n const selectedRepo = repoParam || currentRepo;\n if (selectedRepo && isSafeName(selectedRepo)) {\n branches = await listBranches(join(ARCHIVE_DIR, selectedRepo));\n\n const selectedBranch = branchParam || currentBranch;\n if (selectedBranch && isSafeBranchName(selectedBranch)) {\n screenshots = await listPngs(join(ARCHIVE_DIR, selectedRepo, selectedBranch));\n }\n }\n\n return NextResponse.json({\n repos,\n currentRepo,\n branches,\n currentBranch,\n screenshots,\n });\n}\n\nexport async function handleHistoryImage(req: NextRequest): Promise<NextResponse> {\n const url = req.nextUrl;\n const repo = url.searchParams.get(\"repo\");\n const branch = url.searchParams.get(\"branch\");\n const file = url.searchParams.get(\"file\");\n\n if (!repo || !branch || !file) {\n return NextResponse.json({ error: \"Missing repo, branch, or file param\" }, { status: 400 });\n }\n\n if (!isSafeName(repo) || !isSafeBranchName(branch) || !isSafeName(file)) {\n return NextResponse.json({ error: \"Invalid parameter\" }, { status: 400 });\n }\n\n // Extra safety: ensure resolved path stays within the archive directory\n const filepath = resolve(ARCHIVE_DIR, repo, branch, file);\n if (!filepath.startsWith(ARCHIVE_DIR)) {\n return NextResponse.json({ error: \"Invalid parameter\" }, { status: 400 });\n }\n\n try {\n const buffer = await readFile(filepath);\n return new NextResponse(buffer, {\n headers: {\n \"Content-Type\": \"image/png\",\n \"Cache-Control\": \"private, max-age=3600\",\n },\n });\n } catch {\n return NextResponse.json({ error: \"Not found\" }, { status: 404 });\n }\n}\n"],"mappings":"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","names":["NextResponse","writeFile","mkdir","join","homedir","execFile","promisify","execFileAsync","join","homedir","mkdir","writeFile","NextResponse","NextResponse","execFile","mkdir","join","promisify","execFileAsync","promisify","execFile","NextResponse","join","mkdir","NextResponse","execFile","execFile","NextResponse","NextResponse","readFile","join","homedir","join","homedir","NextResponse","readFile"]}