afterbefore 0.2.14 → 0.2.16
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 +41 -30
- package/dist/chunk-CC6QEACR.js +496 -0
- package/dist/chunk-CC6QEACR.js.map +1 -0
- package/dist/overlay/index.js +770 -576
- package/dist/overlay/index.js.map +1 -1
- package/dist/server/middleware.js +17 -1
- package/dist/server/middleware.js.map +1 -1
- package/dist/server/route.d.ts +6 -2
- package/dist/server/route.js +29 -4
- package/dist/server/route.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-N33DB2F6.js +0 -271
- package/dist/chunk-N33DB2F6.js.map +0 -1
package/README.md
CHANGED
|
@@ -3,9 +3,7 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/afterbefore)
|
|
4
4
|
[](https://www.npmjs.com/package/afterbefore)
|
|
5
5
|
|
|
6
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
81
|
+
## Where screenshots go
|
|
73
82
|
|
|
74
|
-
Screenshots
|
|
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
|
-
|
|
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
|
-
##
|
|
91
|
+
## After capture
|
|
91
92
|
|
|
92
|
-
|
|
93
|
+
Once a screenshot is captured, click the icon to access:
|
|
93
94
|
|
|
94
|
-
- **Open Folder** — opens
|
|
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
|
|
97
|
-
- **Reset** — start
|
|
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)
|
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
// src/server/config.ts
|
|
2
|
+
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { homedir } from "os";
|
|
5
|
+
import { NextResponse } from "next/server";
|
|
6
|
+
import { execFile } from "child_process";
|
|
7
|
+
import { promisify } from "util";
|
|
8
|
+
var execFileAsync = promisify(execFile);
|
|
9
|
+
var CONFIG_DIR = join(process.cwd(), ".afterbefore");
|
|
10
|
+
var CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
11
|
+
var DEFAULT_SAVE_DIR = join(homedir(), "Desktop");
|
|
12
|
+
async function readConfig() {
|
|
13
|
+
try {
|
|
14
|
+
const raw = await readFile(CONFIG_PATH, "utf-8");
|
|
15
|
+
const parsed = JSON.parse(raw);
|
|
16
|
+
return { saveDir: parsed.saveDir || DEFAULT_SAVE_DIR };
|
|
17
|
+
} catch {
|
|
18
|
+
return { saveDir: DEFAULT_SAVE_DIR };
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
async function writeConfig(config) {
|
|
22
|
+
await mkdir(CONFIG_DIR, { recursive: true });
|
|
23
|
+
await writeFile(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n");
|
|
24
|
+
}
|
|
25
|
+
async function getSaveDir() {
|
|
26
|
+
const config = await readConfig();
|
|
27
|
+
return config.saveDir;
|
|
28
|
+
}
|
|
29
|
+
async function handleGetConfig() {
|
|
30
|
+
const config = await readConfig();
|
|
31
|
+
return NextResponse.json(config);
|
|
32
|
+
}
|
|
33
|
+
async function handleSetConfig(req) {
|
|
34
|
+
let body;
|
|
35
|
+
try {
|
|
36
|
+
body = await req.json();
|
|
37
|
+
} catch {
|
|
38
|
+
return NextResponse.json({ error: "Invalid JSON" }, { status: 400 });
|
|
39
|
+
}
|
|
40
|
+
if (typeof body.saveDir !== "string" || body.saveDir.trim() === "") {
|
|
41
|
+
return NextResponse.json({ error: "Invalid saveDir" }, { status: 400 });
|
|
42
|
+
}
|
|
43
|
+
const config = { saveDir: body.saveDir.trim() };
|
|
44
|
+
await writeConfig(config);
|
|
45
|
+
return NextResponse.json(config);
|
|
46
|
+
}
|
|
47
|
+
async function handlePickFolder() {
|
|
48
|
+
if (process.platform !== "darwin") {
|
|
49
|
+
return NextResponse.json(
|
|
50
|
+
{ error: "Folder picker is only supported on macOS" },
|
|
51
|
+
{ status: 501 }
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
const { stdout } = await execFileAsync("osascript", [
|
|
56
|
+
"-e",
|
|
57
|
+
'POSIX path of (choose folder with prompt "Select screenshot save location")'
|
|
58
|
+
]);
|
|
59
|
+
const folder = stdout.trim().replace(/\/$/, "");
|
|
60
|
+
if (!folder) {
|
|
61
|
+
return NextResponse.json({ cancelled: true });
|
|
62
|
+
}
|
|
63
|
+
return NextResponse.json({ folder });
|
|
64
|
+
} catch {
|
|
65
|
+
return NextResponse.json({ cancelled: true });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// src/server/save.ts
|
|
70
|
+
import { NextResponse as NextResponse2 } from "next/server";
|
|
71
|
+
import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
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";
|
|
77
|
+
import { execFile as execFile2 } from "child_process";
|
|
78
|
+
import { promisify as promisify2 } from "util";
|
|
79
|
+
var execFileAsync2 = promisify2(execFile2);
|
|
80
|
+
async function getBranch() {
|
|
81
|
+
const { stdout } = await execFileAsync2("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
82
|
+
cwd: process.cwd()
|
|
83
|
+
});
|
|
84
|
+
return stdout.trim();
|
|
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
|
+
}
|
|
105
|
+
var VALID_MODES = ["fullpage", "viewport", "component"];
|
|
106
|
+
var DATA_URL_PREFIX = "data:image/png;base64,";
|
|
107
|
+
async function handleSave(req) {
|
|
108
|
+
let body;
|
|
109
|
+
try {
|
|
110
|
+
body = await req.json();
|
|
111
|
+
} catch {
|
|
112
|
+
return NextResponse2.json(
|
|
113
|
+
{ error: "Invalid JSON body" },
|
|
114
|
+
{ status: 400 }
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
const { mode, image } = body;
|
|
118
|
+
if (!VALID_MODES.includes(mode)) {
|
|
119
|
+
return NextResponse2.json(
|
|
120
|
+
{ error: `Invalid mode: must be "fullpage", "viewport", or "component"` },
|
|
121
|
+
{ status: 400 }
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
if (typeof image !== "string" || !image.startsWith(DATA_URL_PREFIX)) {
|
|
125
|
+
return NextResponse2.json(
|
|
126
|
+
{ error: "Invalid image: must be a data:image/png;base64 data URL" },
|
|
127
|
+
{ status: 400 }
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
const base64 = image.slice(DATA_URL_PREFIX.length);
|
|
131
|
+
const buffer = Buffer.from(base64, "base64");
|
|
132
|
+
const [branch, repoName, saveDir] = await Promise.all([
|
|
133
|
+
getBranch(),
|
|
134
|
+
getRepoName(),
|
|
135
|
+
getSaveDir()
|
|
136
|
+
]);
|
|
137
|
+
const filename = `${branch}.png`;
|
|
138
|
+
const filepath = join2(saveDir, filename);
|
|
139
|
+
try {
|
|
140
|
+
await writeFile2(filepath, buffer);
|
|
141
|
+
} catch (err) {
|
|
142
|
+
return NextResponse2.json(
|
|
143
|
+
{ error: "Failed to write screenshot", detail: String(err) },
|
|
144
|
+
{ status: 500 }
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
await saveGlobalCopy(buffer, repoName, branch);
|
|
149
|
+
} catch {
|
|
150
|
+
}
|
|
151
|
+
return NextResponse2.json({ success: true, path: filepath });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// src/server/push.ts
|
|
155
|
+
import { NextResponse as NextResponse3 } from "next/server";
|
|
156
|
+
import { execFile as execFile3 } from "child_process";
|
|
157
|
+
import { access, mkdir as mkdir3, copyFile } from "fs/promises";
|
|
158
|
+
import { join as join3 } from "path";
|
|
159
|
+
import { promisify as promisify3 } from "util";
|
|
160
|
+
var execFileAsync3 = promisify3(execFile3);
|
|
161
|
+
async function run(cmd, args) {
|
|
162
|
+
return execFileAsync3(cmd, args, { cwd: process.cwd() });
|
|
163
|
+
}
|
|
164
|
+
async function ghAvailable() {
|
|
165
|
+
try {
|
|
166
|
+
await run("gh", ["--version"]);
|
|
167
|
+
return true;
|
|
168
|
+
} catch {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
async function getPrInfo() {
|
|
173
|
+
try {
|
|
174
|
+
const { stdout } = await run("gh", [
|
|
175
|
+
"pr",
|
|
176
|
+
"view",
|
|
177
|
+
"--json",
|
|
178
|
+
"number,url,headRepository,headRefName"
|
|
179
|
+
]);
|
|
180
|
+
return JSON.parse(stdout);
|
|
181
|
+
} catch {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
async function fileExists(path) {
|
|
186
|
+
try {
|
|
187
|
+
await access(path);
|
|
188
|
+
return true;
|
|
189
|
+
} catch {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
async function handlePush(req) {
|
|
194
|
+
if (!await ghAvailable()) {
|
|
195
|
+
return NextResponse3.json(
|
|
196
|
+
{ success: false, error: "GitHub CLI (gh) is not installed or not in PATH" },
|
|
197
|
+
{ status: 500 }
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
const pr = await getPrInfo();
|
|
201
|
+
if (!pr) {
|
|
202
|
+
return NextResponse3.json(
|
|
203
|
+
{ success: false, error: "No PR found for current branch" },
|
|
204
|
+
{ status: 404 }
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
const saveDir = await getSaveDir();
|
|
208
|
+
const branch = pr.headRefName;
|
|
209
|
+
const screenshotPath = join3(saveDir, `${branch}.png`);
|
|
210
|
+
if (!await fileExists(screenshotPath)) {
|
|
211
|
+
return NextResponse3.json(
|
|
212
|
+
{ success: false, error: `Missing screenshot: ${branch}.png` },
|
|
213
|
+
{ status: 400 }
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
const repoDir = join3(process.cwd(), ".afterbefore");
|
|
217
|
+
const repoFile = join3(repoDir, `${branch}.png`);
|
|
218
|
+
try {
|
|
219
|
+
await mkdir3(repoDir, { recursive: true });
|
|
220
|
+
await copyFile(screenshotPath, repoFile);
|
|
221
|
+
} catch (err) {
|
|
222
|
+
return NextResponse3.json(
|
|
223
|
+
{ success: false, error: "Failed to copy screenshot into repo", detail: String(err) },
|
|
224
|
+
{ status: 500 }
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
try {
|
|
228
|
+
await run("git", ["add", repoFile]);
|
|
229
|
+
await run("git", [
|
|
230
|
+
"commit",
|
|
231
|
+
"-m",
|
|
232
|
+
"chore: add screenshot"
|
|
233
|
+
]);
|
|
234
|
+
} catch (err) {
|
|
235
|
+
const msg = String(err);
|
|
236
|
+
if (!msg.includes("nothing to commit")) {
|
|
237
|
+
return NextResponse3.json(
|
|
238
|
+
{ success: false, error: "Failed to commit screenshot", detail: msg },
|
|
239
|
+
{ status: 500 }
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
try {
|
|
244
|
+
await run("git", ["push"]);
|
|
245
|
+
} catch (err) {
|
|
246
|
+
return NextResponse3.json(
|
|
247
|
+
{ success: false, error: "Failed to push to remote", detail: String(err) },
|
|
248
|
+
{ status: 500 }
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
const owner = pr.headRepository.owner.login;
|
|
252
|
+
const repo = pr.headRepository.name;
|
|
253
|
+
const rawBase = `https://raw.githubusercontent.com/${owner}/${repo}/${branch}`;
|
|
254
|
+
const imageUrl = `${rawBase}/.afterbefore/${branch}.png`;
|
|
255
|
+
const ts = Date.now();
|
|
256
|
+
const commentBody = `## Screenshot
|
|
257
|
+
|
|
258
|
+
`;
|
|
259
|
+
let commentUrl;
|
|
260
|
+
try {
|
|
261
|
+
const { stdout } = await run("gh", [
|
|
262
|
+
"pr",
|
|
263
|
+
"comment",
|
|
264
|
+
String(pr.number),
|
|
265
|
+
"--body",
|
|
266
|
+
commentBody
|
|
267
|
+
]);
|
|
268
|
+
commentUrl = stdout.trim() || void 0;
|
|
269
|
+
} catch (err) {
|
|
270
|
+
return NextResponse3.json(
|
|
271
|
+
{ success: false, error: "Failed to post PR comment", detail: String(err) },
|
|
272
|
+
{ status: 500 }
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
return NextResponse3.json({
|
|
276
|
+
success: true,
|
|
277
|
+
pr: pr.number,
|
|
278
|
+
prUrl: pr.url,
|
|
279
|
+
commentUrl
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// src/server/open.ts
|
|
284
|
+
import { NextResponse as NextResponse4 } from "next/server";
|
|
285
|
+
import { execFile as execFile4 } from "child_process";
|
|
286
|
+
import { mkdir as mkdir4 } from "fs/promises";
|
|
287
|
+
import { join as join4 } from "path";
|
|
288
|
+
import { homedir as homedir3 } from "os";
|
|
289
|
+
function isSafeName(name) {
|
|
290
|
+
return /^[a-zA-Z0-9._\-]+$/.test(name);
|
|
291
|
+
}
|
|
292
|
+
function isSafeBranchName(name) {
|
|
293
|
+
return /^[a-zA-Z0-9._\-/]+$/.test(name) && !name.includes("..") && !name.startsWith("/") && !name.endsWith("/");
|
|
294
|
+
}
|
|
295
|
+
async function handleOpen(req) {
|
|
296
|
+
let body = null;
|
|
297
|
+
try {
|
|
298
|
+
body = await req.json();
|
|
299
|
+
} catch {
|
|
300
|
+
body = null;
|
|
301
|
+
}
|
|
302
|
+
let dir;
|
|
303
|
+
if (body?.repo || body?.branch) {
|
|
304
|
+
if (typeof body.repo !== "string" || typeof body.branch !== "string" || !isSafeName(body.repo) || !isSafeBranchName(body.branch)) {
|
|
305
|
+
return NextResponse4.json({ error: "Invalid parameter" }, { status: 400 });
|
|
306
|
+
}
|
|
307
|
+
dir = join4(homedir3(), ".afterbefore", body.repo, body.branch);
|
|
308
|
+
await mkdir4(dir, { recursive: true });
|
|
309
|
+
} else {
|
|
310
|
+
dir = await getSaveDir();
|
|
311
|
+
}
|
|
312
|
+
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer" : "xdg-open";
|
|
313
|
+
execFile4(cmd, [dir]);
|
|
314
|
+
return NextResponse4.json({ success: true });
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// src/server/history.ts
|
|
318
|
+
import { NextResponse as NextResponse5 } from "next/server";
|
|
319
|
+
import { access as access2, readdir, readFile as readFile2, rename, unlink } from "fs/promises";
|
|
320
|
+
import { join as join5, resolve } from "path";
|
|
321
|
+
import { homedir as homedir4 } from "os";
|
|
322
|
+
var ARCHIVE_DIR = join5(homedir4(), ".afterbefore");
|
|
323
|
+
var MAX_SCREENSHOTS = 50;
|
|
324
|
+
function isSafeName2(name) {
|
|
325
|
+
return /^[a-zA-Z0-9._\-]+$/.test(name);
|
|
326
|
+
}
|
|
327
|
+
function isSafeBranchName2(name) {
|
|
328
|
+
return /^[a-zA-Z0-9._\-/]+$/.test(name) && !name.includes("..") && !name.startsWith("/") && !name.endsWith("/");
|
|
329
|
+
}
|
|
330
|
+
async function listDirs(dir) {
|
|
331
|
+
try {
|
|
332
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
333
|
+
return entries.filter((e) => e.isDirectory()).map((e) => e.name).sort();
|
|
334
|
+
} catch {
|
|
335
|
+
return [];
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
async function listBranches(repoDir) {
|
|
339
|
+
const branches = [];
|
|
340
|
+
async function walk(dir, prefix) {
|
|
341
|
+
try {
|
|
342
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
343
|
+
const hasPngs = entries.some((e) => e.isFile() && e.name.endsWith(".png"));
|
|
344
|
+
if (hasPngs && prefix) {
|
|
345
|
+
branches.push(prefix);
|
|
346
|
+
}
|
|
347
|
+
for (const entry of entries) {
|
|
348
|
+
if (entry.isDirectory()) {
|
|
349
|
+
await walk(join5(dir, entry.name), prefix ? `${prefix}/${entry.name}` : entry.name);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
} catch {
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
await walk(repoDir, "");
|
|
356
|
+
return branches.sort();
|
|
357
|
+
}
|
|
358
|
+
async function listPngs(dir) {
|
|
359
|
+
try {
|
|
360
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
361
|
+
return entries.filter((e) => e.isFile() && e.name.endsWith(".png")).map((e) => ({
|
|
362
|
+
filename: e.name,
|
|
363
|
+
timestamp: e.name.replace(/\.png$/, "")
|
|
364
|
+
})).sort((a, b) => b.filename.localeCompare(a.filename)).slice(0, MAX_SCREENSHOTS);
|
|
365
|
+
} catch {
|
|
366
|
+
return [];
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
async function handleHistoryList(req) {
|
|
370
|
+
const url = req.nextUrl;
|
|
371
|
+
const repoParam = url.searchParams.get("repo");
|
|
372
|
+
const branchParam = url.searchParams.get("branch");
|
|
373
|
+
const [currentRepo, currentBranch] = await Promise.all([
|
|
374
|
+
getRepoName(),
|
|
375
|
+
getBranch()
|
|
376
|
+
]);
|
|
377
|
+
const repos = await listDirs(ARCHIVE_DIR);
|
|
378
|
+
let branches = [];
|
|
379
|
+
let screenshots = [];
|
|
380
|
+
const selectedRepo = repoParam || currentRepo;
|
|
381
|
+
if (selectedRepo && isSafeName2(selectedRepo)) {
|
|
382
|
+
branches = await listBranches(join5(ARCHIVE_DIR, selectedRepo));
|
|
383
|
+
const selectedBranch = branchParam || currentBranch;
|
|
384
|
+
if (selectedBranch && isSafeBranchName2(selectedBranch)) {
|
|
385
|
+
screenshots = await listPngs(join5(ARCHIVE_DIR, selectedRepo, selectedBranch));
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return NextResponse5.json({
|
|
389
|
+
repos,
|
|
390
|
+
currentRepo,
|
|
391
|
+
branches,
|
|
392
|
+
currentBranch,
|
|
393
|
+
screenshots
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
async function handleHistoryRename(req) {
|
|
397
|
+
let body;
|
|
398
|
+
try {
|
|
399
|
+
body = await req.json();
|
|
400
|
+
} catch {
|
|
401
|
+
return NextResponse5.json({ error: "Invalid JSON" }, { status: 400 });
|
|
402
|
+
}
|
|
403
|
+
const { repo, branch, oldName, newName } = body;
|
|
404
|
+
if (!repo || !branch || !oldName || !newName) {
|
|
405
|
+
return NextResponse5.json({ error: "Missing fields" }, { status: 400 });
|
|
406
|
+
}
|
|
407
|
+
const safeName = newName.endsWith(".png") ? newName : `${newName}.png`;
|
|
408
|
+
if (!isSafeName2(repo) || !isSafeBranchName2(branch) || !isSafeName2(oldName) || !isSafeName2(safeName)) {
|
|
409
|
+
return NextResponse5.json({ error: "Invalid parameter" }, { status: 400 });
|
|
410
|
+
}
|
|
411
|
+
const dir = resolve(ARCHIVE_DIR, repo, branch);
|
|
412
|
+
const oldPath = join5(dir, oldName);
|
|
413
|
+
const newPath = join5(dir, safeName);
|
|
414
|
+
if (!oldPath.startsWith(ARCHIVE_DIR) || !newPath.startsWith(ARCHIVE_DIR)) {
|
|
415
|
+
return NextResponse5.json({ error: "Invalid parameter" }, { status: 400 });
|
|
416
|
+
}
|
|
417
|
+
if (safeName !== oldName) {
|
|
418
|
+
try {
|
|
419
|
+
await access2(newPath);
|
|
420
|
+
return NextResponse5.json({ error: "File already exists" }, { status: 409 });
|
|
421
|
+
} catch {
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
try {
|
|
425
|
+
await rename(oldPath, newPath);
|
|
426
|
+
return NextResponse5.json({ success: true, filename: safeName });
|
|
427
|
+
} catch {
|
|
428
|
+
return NextResponse5.json({ error: "Rename failed" }, { status: 500 });
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
async function handleHistoryDelete(req) {
|
|
432
|
+
let body;
|
|
433
|
+
try {
|
|
434
|
+
body = await req.json();
|
|
435
|
+
} catch {
|
|
436
|
+
return NextResponse5.json({ error: "Invalid JSON" }, { status: 400 });
|
|
437
|
+
}
|
|
438
|
+
const { repo, branch, file } = body;
|
|
439
|
+
if (!repo || !branch || !file) {
|
|
440
|
+
return NextResponse5.json({ error: "Missing fields" }, { status: 400 });
|
|
441
|
+
}
|
|
442
|
+
if (!isSafeName2(repo) || !isSafeBranchName2(branch) || !isSafeName2(file)) {
|
|
443
|
+
return NextResponse5.json({ error: "Invalid parameter" }, { status: 400 });
|
|
444
|
+
}
|
|
445
|
+
const filepath = resolve(ARCHIVE_DIR, repo, branch, file);
|
|
446
|
+
if (!filepath.startsWith(ARCHIVE_DIR)) {
|
|
447
|
+
return NextResponse5.json({ error: "Invalid parameter" }, { status: 400 });
|
|
448
|
+
}
|
|
449
|
+
try {
|
|
450
|
+
await unlink(filepath);
|
|
451
|
+
return NextResponse5.json({ success: true });
|
|
452
|
+
} catch {
|
|
453
|
+
return NextResponse5.json({ error: "Delete failed" }, { status: 500 });
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
async function handleHistoryImage(req) {
|
|
457
|
+
const url = req.nextUrl;
|
|
458
|
+
const repo = url.searchParams.get("repo");
|
|
459
|
+
const branch = url.searchParams.get("branch");
|
|
460
|
+
const file = url.searchParams.get("file");
|
|
461
|
+
if (!repo || !branch || !file) {
|
|
462
|
+
return NextResponse5.json({ error: "Missing repo, branch, or file param" }, { status: 400 });
|
|
463
|
+
}
|
|
464
|
+
if (!isSafeName2(repo) || !isSafeBranchName2(branch) || !isSafeName2(file)) {
|
|
465
|
+
return NextResponse5.json({ error: "Invalid parameter" }, { status: 400 });
|
|
466
|
+
}
|
|
467
|
+
const filepath = resolve(ARCHIVE_DIR, repo, branch, file);
|
|
468
|
+
if (!filepath.startsWith(ARCHIVE_DIR)) {
|
|
469
|
+
return NextResponse5.json({ error: "Invalid parameter" }, { status: 400 });
|
|
470
|
+
}
|
|
471
|
+
try {
|
|
472
|
+
const buffer = await readFile2(filepath);
|
|
473
|
+
return new NextResponse5(buffer, {
|
|
474
|
+
headers: {
|
|
475
|
+
"Content-Type": "image/png",
|
|
476
|
+
"Cache-Control": "private, max-age=3600"
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
} catch {
|
|
480
|
+
return NextResponse5.json({ error: "Not found" }, { status: 404 });
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
export {
|
|
485
|
+
handleGetConfig,
|
|
486
|
+
handleSetConfig,
|
|
487
|
+
handlePickFolder,
|
|
488
|
+
handleSave,
|
|
489
|
+
handlePush,
|
|
490
|
+
handleOpen,
|
|
491
|
+
handleHistoryList,
|
|
492
|
+
handleHistoryRename,
|
|
493
|
+
handleHistoryDelete,
|
|
494
|
+
handleHistoryImage
|
|
495
|
+
};
|
|
496
|
+
//# sourceMappingURL=chunk-CC6QEACR.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`;\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 { mkdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { homedir } from \"node:os\";\nimport { getSaveDir } from \"./config\";\n\ninterface OpenRequestBody {\n repo?: string;\n branch?: string;\n}\n\nfunction isSafeName(name: string): boolean {\n return /^[a-zA-Z0-9._\\-]+$/.test(name);\n}\n\nfunction isSafeBranchName(name: string): boolean {\n return /^[a-zA-Z0-9._\\-/]+$/.test(name) && !name.includes(\"..\") && !name.startsWith(\"/\") && !name.endsWith(\"/\");\n}\n\nexport async function handleOpen(req: NextRequest): Promise<NextResponse> {\n let body: OpenRequestBody | null = null;\n try {\n body = (await req.json()) as OpenRequestBody;\n } catch {\n body = null;\n }\n\n let dir: string;\n if (body?.repo || body?.branch) {\n if (\n typeof body.repo !== \"string\" ||\n typeof body.branch !== \"string\" ||\n !isSafeName(body.repo) ||\n !isSafeBranchName(body.branch)\n ) {\n return NextResponse.json({ error: \"Invalid parameter\" }, { status: 400 });\n }\n\n dir = join(homedir(), \".afterbefore\", body.repo, body.branch);\n await mkdir(dir, { recursive: true });\n } else {\n dir = await getSaveDir();\n }\n\n const cmd =\n process.platform === \"darwin\"\n ? \"open\"\n : process.platform === \"win32\"\n ? \"explorer\"\n : \"xdg-open\";\n\n execFile(cmd, [dir]);\n\n return NextResponse.json({ success: true });\n}\n","import { NextRequest, NextResponse } from \"next/server\";\nimport { access, readdir, readFile, rename, unlink } 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 handleHistoryRename(req: NextRequest): Promise<NextResponse> {\n let body: { repo: string; branch: string; oldName: string; newName: string };\n try {\n body = await req.json();\n } catch {\n return NextResponse.json({ error: \"Invalid JSON\" }, { status: 400 });\n }\n\n const { repo, branch, oldName, newName } = body;\n if (!repo || !branch || !oldName || !newName) {\n return NextResponse.json({ error: \"Missing fields\" }, { status: 400 });\n }\n\n // Ensure newName ends with .png\n const safeName = newName.endsWith(\".png\") ? newName : `${newName}.png`;\n\n if (!isSafeName(repo) || !isSafeBranchName(branch) || !isSafeName(oldName) || !isSafeName(safeName)) {\n return NextResponse.json({ error: \"Invalid parameter\" }, { status: 400 });\n }\n\n const dir = resolve(ARCHIVE_DIR, repo, branch);\n const oldPath = join(dir, oldName);\n const newPath = join(dir, safeName);\n\n if (!oldPath.startsWith(ARCHIVE_DIR) || !newPath.startsWith(ARCHIVE_DIR)) {\n return NextResponse.json({ error: \"Invalid parameter\" }, { status: 400 });\n }\n\n if (safeName !== oldName) {\n try {\n await access(newPath);\n return NextResponse.json({ error: \"File already exists\" }, { status: 409 });\n } catch {\n // Target does not exist, continue.\n }\n }\n\n try {\n await rename(oldPath, newPath);\n return NextResponse.json({ success: true, filename: safeName });\n } catch {\n return NextResponse.json({ error: \"Rename failed\" }, { status: 500 });\n }\n}\n\nexport async function handleHistoryDelete(req: NextRequest): Promise<NextResponse> {\n let body: { repo: string; branch: string; file: string };\n try {\n body = await req.json();\n } catch {\n return NextResponse.json({ error: \"Invalid JSON\" }, { status: 400 });\n }\n\n const { repo, branch, file } = body;\n if (!repo || !branch || !file) {\n return NextResponse.json({ error: \"Missing fields\" }, { status: 400 });\n }\n\n if (!isSafeName(repo) || !isSafeBranchName(branch) || !isSafeName(file)) {\n return NextResponse.json({ error: \"Invalid parameter\" }, { status: 400 });\n }\n\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 await unlink(filepath);\n return NextResponse.json({ success: true });\n } catch {\n return NextResponse.json({ error: \"Delete failed\" }, { status: 500 });\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","mkdir","join","homedir","NextResponse","join","homedir","mkdir","execFile","NextResponse","access","readFile","join","homedir","join","homedir","isSafeName","isSafeBranchName","NextResponse","access","readFile"]}
|