@juangadm/pre-post 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.
- package/LICENSE +21 -0
- package/README.md +227 -0
- package/dist/bin/cli.d.ts +3 -0
- package/dist/bin/cli.d.ts.map +1 -0
- package/dist/bin/cli.js +388 -0
- package/dist/bin/cli.js.map +1 -0
- package/dist/browser.d.ts +25 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +82 -0
- package/dist/browser.js.map +1 -0
- package/dist/capture.d.ts +9 -0
- package/dist/capture.d.ts.map +1 -0
- package/dist/capture.js +52 -0
- package/dist/capture.js.map +1 -0
- package/dist/clipboard.d.ts +9 -0
- package/dist/clipboard.d.ts.map +1 -0
- package/dist/clipboard.js +26 -0
- package/dist/clipboard.js.map +1 -0
- package/dist/filename.d.ts +24 -0
- package/dist/filename.d.ts.map +1 -0
- package/dist/filename.js +76 -0
- package/dist/filename.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +69 -0
- package/dist/index.js.map +1 -0
- package/dist/routes/generic.d.ts +11 -0
- package/dist/routes/generic.d.ts.map +1 -0
- package/dist/routes/generic.js +106 -0
- package/dist/routes/generic.js.map +1 -0
- package/dist/routes/nextjs.d.ts +14 -0
- package/dist/routes/nextjs.d.ts.map +1 -0
- package/dist/routes/nextjs.js +196 -0
- package/dist/routes/nextjs.js.map +1 -0
- package/dist/routes.d.ts +21 -0
- package/dist/routes.d.ts.map +1 -0
- package/dist/routes.js +174 -0
- package/dist/routes.js.map +1 -0
- package/dist/types.d.ts +98 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +9 -0
- package/dist/types.js.map +1 -0
- package/dist/upload.d.ts +35 -0
- package/dist/upload.d.ts.map +1 -0
- package/dist/upload.js +116 -0
- package/dist/upload.js.map +1 -0
- package/dist/viewport.d.ts +3 -0
- package/dist/viewport.d.ts.map +1 -0
- package/dist/viewport.js +11 -0
- package/dist/viewport.js.map +1 -0
- package/package.json +66 -0
- package/skill/SKILL.md +246 -0
- package/skill/scripts/adapters/0x0st.sh +36 -0
- package/skill/scripts/adapters/blob.sh +55 -0
- package/skill/scripts/adapters/gist.sh +59 -0
- package/skill/scripts/adapters/git-native.sh +75 -0
- package/skill/scripts/upload-and-copy.sh +183 -0
package/dist/upload.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image upload for generating shareable URLs.
|
|
3
|
+
* Default: git-native (commits to .pre-post/ on current branch).
|
|
4
|
+
* Opt-in: 0x0.st, Vercel Blob, generic PUT via --upload-url.
|
|
5
|
+
*/
|
|
6
|
+
import { execSync } from 'child_process';
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
const DEFAULT_UPLOAD_URL = 'https://0x0.st';
|
|
10
|
+
/**
|
|
11
|
+
* Upload an image and return a public URL.
|
|
12
|
+
* Auto-detects upload method from URL pattern.
|
|
13
|
+
*/
|
|
14
|
+
export async function uploadImage(image, filename, uploadUrl = DEFAULT_UPLOAD_URL) {
|
|
15
|
+
// 0x0.st uses multipart form upload
|
|
16
|
+
if (uploadUrl.includes('0x0.st')) {
|
|
17
|
+
return upload0x0st(image, filename, uploadUrl);
|
|
18
|
+
}
|
|
19
|
+
// Vercel Blob uses PUT with specific headers
|
|
20
|
+
if (uploadUrl.includes('blob.vercel')) {
|
|
21
|
+
return uploadVercelBlob(image, filename, uploadUrl);
|
|
22
|
+
}
|
|
23
|
+
// Generic PUT upload (common for S3-compatible services)
|
|
24
|
+
return uploadGenericPut(image, filename, uploadUrl);
|
|
25
|
+
}
|
|
26
|
+
async function upload0x0st(image, filename, url) {
|
|
27
|
+
const formData = new FormData();
|
|
28
|
+
formData.append('file', new Blob([image]), filename);
|
|
29
|
+
const response = await fetch(url, {
|
|
30
|
+
method: 'POST',
|
|
31
|
+
headers: { 'User-Agent': 'before-after-cli/1.0' },
|
|
32
|
+
body: formData,
|
|
33
|
+
});
|
|
34
|
+
const result = (await response.text()).trim();
|
|
35
|
+
if (!result.startsWith('http')) {
|
|
36
|
+
throw new Error(`Upload failed: ${result}`);
|
|
37
|
+
}
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
async function uploadVercelBlob(image, filename, url) {
|
|
41
|
+
const response = await fetch(`${url}/${filename}`, {
|
|
42
|
+
method: 'PUT',
|
|
43
|
+
headers: { 'Content-Type': 'image/png' },
|
|
44
|
+
body: image,
|
|
45
|
+
});
|
|
46
|
+
if (!response.ok) {
|
|
47
|
+
throw new Error(`Upload failed: ${response.statusText}`);
|
|
48
|
+
}
|
|
49
|
+
const result = await response.json();
|
|
50
|
+
return result.url;
|
|
51
|
+
}
|
|
52
|
+
async function uploadGenericPut(image, filename, url) {
|
|
53
|
+
const response = await fetch(`${url}/${filename}`, {
|
|
54
|
+
method: 'PUT',
|
|
55
|
+
headers: { 'Content-Type': 'image/png' },
|
|
56
|
+
body: image,
|
|
57
|
+
});
|
|
58
|
+
if (!response.ok) {
|
|
59
|
+
throw new Error(`Upload failed: ${response.statusText}`);
|
|
60
|
+
}
|
|
61
|
+
// Try to parse JSON response, fall back to URL from location header or constructed URL
|
|
62
|
+
const contentType = response.headers.get('content-type') || '';
|
|
63
|
+
if (contentType.includes('application/json')) {
|
|
64
|
+
const result = await response.json();
|
|
65
|
+
if (result.url)
|
|
66
|
+
return result.url;
|
|
67
|
+
}
|
|
68
|
+
return response.headers.get('location') || `${url}/${filename}`;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Write an image to .pre-post/ in the repo root, stage it, and return
|
|
72
|
+
* the raw.githubusercontent.com URL it will resolve to after push.
|
|
73
|
+
*/
|
|
74
|
+
export function uploadGitNative(image, filename) {
|
|
75
|
+
const repoRoot = execSync('git rev-parse --show-toplevel', { encoding: 'utf-8' }).trim();
|
|
76
|
+
const branch = execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf-8' }).trim();
|
|
77
|
+
const remoteUrl = execSync('git remote get-url origin', { encoding: 'utf-8' }).trim();
|
|
78
|
+
// Parse owner/repo from HTTPS or SSH remote URL
|
|
79
|
+
const ownerRepo = remoteUrl
|
|
80
|
+
.replace(/^(https?:\/\/github\.com\/|git@github\.com:)/, '')
|
|
81
|
+
.replace(/\.git$/, '');
|
|
82
|
+
const destDir = path.join(repoRoot, '.pre-post');
|
|
83
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
84
|
+
const dest = path.join(destDir, filename);
|
|
85
|
+
fs.writeFileSync(dest, image);
|
|
86
|
+
execSync(`git add -f "${dest}"`);
|
|
87
|
+
return `https://raw.githubusercontent.com/${ownerRepo}/${branch}/.pre-post/${filename}`;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Commit and push all staged .pre-post/ screenshots in one batch.
|
|
91
|
+
*/
|
|
92
|
+
export function commitAndPushScreenshots() {
|
|
93
|
+
execSync('git commit -m "chore: add pre/post screenshots"');
|
|
94
|
+
execSync('git push origin HEAD');
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Upload before/after images and return URLs.
|
|
98
|
+
* When uploadUrl is provided, uses the HTTP-based upload path.
|
|
99
|
+
* Otherwise, uses git-native (commit to .pre-post/).
|
|
100
|
+
*/
|
|
101
|
+
export async function uploadBeforeAfter(before, after, uploadUrl) {
|
|
102
|
+
// If an explicit upload URL is provided, use HTTP upload
|
|
103
|
+
if (uploadUrl) {
|
|
104
|
+
const [beforeUrl, afterUrl] = await Promise.all([
|
|
105
|
+
uploadImage(before.image, before.filename, uploadUrl),
|
|
106
|
+
uploadImage(after.image, after.filename, uploadUrl),
|
|
107
|
+
]);
|
|
108
|
+
return { beforeUrl, afterUrl };
|
|
109
|
+
}
|
|
110
|
+
// Default: git-native — stage both, then commit+push once
|
|
111
|
+
const beforeUrl = uploadGitNative(before.image, before.filename);
|
|
112
|
+
const afterUrl = uploadGitNative(after.image, after.filename);
|
|
113
|
+
commitAndPushScreenshots();
|
|
114
|
+
return { beforeUrl, afterUrl };
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=upload.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"upload.js","sourceRoot":"","sources":["../src/upload.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"viewport.d.ts","sourceRoot":"","sources":["../src/viewport.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,YAAY,EAAoC,MAAM,YAAY,CAAC;AAE5F,wBAAgB,eAAe,CAAC,MAAM,CAAC,EAAE,cAAc,GAAG,YAAY,CAQrE"}
|
package/dist/viewport.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { VIEWPORT_PRESETS } from './types.js';
|
|
2
|
+
export function resolveViewport(config) {
|
|
3
|
+
if (!config) {
|
|
4
|
+
return VIEWPORT_PRESETS.desktop;
|
|
5
|
+
}
|
|
6
|
+
if (typeof config === 'string') {
|
|
7
|
+
return VIEWPORT_PRESETS[config];
|
|
8
|
+
}
|
|
9
|
+
return config;
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=viewport.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"viewport.js","sourceRoot":"","sources":["../src/viewport.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgD,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAE5F,MAAM,UAAU,eAAe,CAAC,MAAuB;IACrD,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO,gBAAgB,CAAC,OAAO,CAAC;IAClC,CAAC;IACD,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE,CAAC;QAC/B,OAAO,gBAAgB,CAAC,MAAwB,CAAC,CAAC;IACpD,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@juangadm/pre-post",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Visual diff tool for PRs — captures before/after screenshots of web pages",
|
|
5
|
+
"author": "Juan Gabriel Delgado (forked from James Clements)",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/juangadm/pre-post.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/juangadm/pre-post",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/juangadm/pre-post/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"screenshot",
|
|
17
|
+
"before-after",
|
|
18
|
+
"visual-diff",
|
|
19
|
+
"comparison",
|
|
20
|
+
"capture",
|
|
21
|
+
"web",
|
|
22
|
+
"testing",
|
|
23
|
+
"cli",
|
|
24
|
+
"playwright",
|
|
25
|
+
"pr-screenshots"
|
|
26
|
+
],
|
|
27
|
+
"type": "module",
|
|
28
|
+
"main": "dist/index.js",
|
|
29
|
+
"types": "dist/index.d.ts",
|
|
30
|
+
"bin": {
|
|
31
|
+
"pre-post": "dist/bin/cli.js"
|
|
32
|
+
},
|
|
33
|
+
"exports": {
|
|
34
|
+
".": {
|
|
35
|
+
"import": "./dist/index.js",
|
|
36
|
+
"types": "./dist/index.d.ts"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"dist",
|
|
41
|
+
"skill"
|
|
42
|
+
],
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsc -p tsconfig.pkg.json",
|
|
45
|
+
"prepublishOnly": "rm -rf dist && npm run build && npm run test:unit",
|
|
46
|
+
"test": "vitest run",
|
|
47
|
+
"test:watch": "vitest",
|
|
48
|
+
"test:unit": "vitest run tests/unit",
|
|
49
|
+
"test:browser": "vitest run tests/browser",
|
|
50
|
+
"test:integration": "vitest run tests/integration"
|
|
51
|
+
},
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"playwright": "^1.50.0"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@types/node": "^22",
|
|
57
|
+
"typescript": "5.7.3",
|
|
58
|
+
"vitest": "^3.2.1"
|
|
59
|
+
},
|
|
60
|
+
"pnpm": {
|
|
61
|
+
"overrides": {
|
|
62
|
+
"@types/react": "19.2.7",
|
|
63
|
+
"@types/react-dom": "19.2.3"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
package/skill/SKILL.md
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pre-post
|
|
3
|
+
description: Captures before/after screenshots of web pages for visual comparison in PRs. Use when user says "take before and after", "screenshot comparison", "visual diff", "PR screenshots", or after making visual UI changes.
|
|
4
|
+
allowed-tools:
|
|
5
|
+
- Bash(npx pre-post *)
|
|
6
|
+
- Bash(pre-post *)
|
|
7
|
+
- Bash(*/upload-and-copy.sh *)
|
|
8
|
+
- Bash(git add *)
|
|
9
|
+
- Bash(git commit -m *)
|
|
10
|
+
- Bash(git push origin *)
|
|
11
|
+
- Bash(curl -s -o /dev/null -w *)
|
|
12
|
+
- Bash(gh pr view *)
|
|
13
|
+
- Bash(gh pr edit *)
|
|
14
|
+
- Bash(lsof -i *)
|
|
15
|
+
- Bash(mkdir -p /tmp/pre-post)
|
|
16
|
+
- Bash(git diff *)
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# Pre-Post Screenshot Skill
|
|
20
|
+
|
|
21
|
+
> **Package:** `pre-post`
|
|
22
|
+
> Visual diff tool for PRs — fastest path from code change to visual documentation.
|
|
23
|
+
|
|
24
|
+
## Agent Behavior Rules
|
|
25
|
+
|
|
26
|
+
**DO NOT:**
|
|
27
|
+
- Switch git branches, stash changes, start dev servers, or assume what "before" is
|
|
28
|
+
- Use `--full` unless user explicitly asks for full page / full scroll capture
|
|
29
|
+
- Post screenshots to PR without user approval
|
|
30
|
+
|
|
31
|
+
**DO:**
|
|
32
|
+
- Use `--markdown` when user wants PR integration or markdown output
|
|
33
|
+
- Use `--responsive` to capture both desktop and mobile viewports
|
|
34
|
+
- Use `--mobile` / `--tablet` if user mentions phone, mobile, tablet, responsive
|
|
35
|
+
- Assume current state is **After** (localhost = after, production = before)
|
|
36
|
+
- Show screenshots to user before posting to PR
|
|
37
|
+
- If user provides only one URL, **ASK**: "What URL should I use for the 'before' state? (production URL, preview deployment, or another local port)"
|
|
38
|
+
|
|
39
|
+
## Execution Order
|
|
40
|
+
|
|
41
|
+
### 1. Pre-flight Checks
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# Detect running dev server
|
|
45
|
+
lsof -i :3000 2>/dev/null || lsof -i :3001 2>/dev/null || lsof -i :5173 2>/dev/null || lsof -i :8080 2>/dev/null
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
If no dev server is running, tell the user to start one.
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# Check production URL is accessible
|
|
52
|
+
curl -s -o /dev/null -w "%{http_code}" "<production-url>"
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
- **200** → proceed
|
|
56
|
+
- **401/403** → warn user: "Production URL requires authentication. Options: (1) provide a public URL, (2) skip 'before' and capture after-only, (3) provide auth cookies"
|
|
57
|
+
- **No production URL** → "after-only" mode: screenshot localhost only, label as current state
|
|
58
|
+
|
|
59
|
+
### 2. Route Detection + Refinement
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
# Detect affected routes from git diff
|
|
63
|
+
npx pre-post detect
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
This outputs JSON with detected routes, confidence levels, and source files.
|
|
67
|
+
|
|
68
|
+
**Claude's role:** Review the JSON output using conversation context:
|
|
69
|
+
- Add routes you know are affected from the work done in this session
|
|
70
|
+
- Remove false positives (e.g., API-only changes)
|
|
71
|
+
- For dynamic routes (e.g., `/blog/[slug]`), ask user for a sample value
|
|
72
|
+
- Present to user: "I'll screenshot these routes: `/dashboard`, `/settings`. Want to add or change any?"
|
|
73
|
+
|
|
74
|
+
### 3. Screenshot Capture
|
|
75
|
+
|
|
76
|
+
**Option A: CLI (preferred — deterministic)**
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
# Single route, desktop only
|
|
80
|
+
npx pre-post compare \
|
|
81
|
+
--before-base https://prod.com \
|
|
82
|
+
--after-base http://localhost:3000 \
|
|
83
|
+
--routes /dashboard \
|
|
84
|
+
--output /tmp/pre-post
|
|
85
|
+
|
|
86
|
+
# Multiple routes, responsive (desktop + mobile)
|
|
87
|
+
npx pre-post compare \
|
|
88
|
+
--before-base https://prod.com \
|
|
89
|
+
--after-base http://localhost:3000 \
|
|
90
|
+
--routes /dashboard,/settings,/ \
|
|
91
|
+
--responsive \
|
|
92
|
+
--output /tmp/pre-post
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**Option B: Playwright MCP (for more control)**
|
|
96
|
+
|
|
97
|
+
Use when you need custom waits, interactions, or complex page states:
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
browser_resize(1280, 800)
|
|
101
|
+
browser_navigate("https://prod.com/dashboard")
|
|
102
|
+
browser_wait_for(time: 3)
|
|
103
|
+
browser_take_screenshot(filename: "/tmp/pre-post/dashboard-desktop-before.png")
|
|
104
|
+
|
|
105
|
+
browser_navigate("http://localhost:3000/dashboard")
|
|
106
|
+
browser_wait_for(time: 3)
|
|
107
|
+
browser_take_screenshot(filename: "/tmp/pre-post/dashboard-desktop-after.png")
|
|
108
|
+
|
|
109
|
+
# Mobile
|
|
110
|
+
browser_resize(375, 812)
|
|
111
|
+
browser_navigate("https://prod.com/dashboard")
|
|
112
|
+
browser_wait_for(time: 3)
|
|
113
|
+
browser_take_screenshot(filename: "/tmp/pre-post/dashboard-mobile-before.png")
|
|
114
|
+
|
|
115
|
+
browser_navigate("http://localhost:3000/dashboard")
|
|
116
|
+
browser_wait_for(time: 3)
|
|
117
|
+
browser_take_screenshot(filename: "/tmp/pre-post/dashboard-mobile-after.png")
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### 4. User Approval
|
|
121
|
+
|
|
122
|
+
Show screenshots in conversation. Ask: "Here are the before/after screenshots. Should I post to PR, retake any, or add more pages?"
|
|
123
|
+
|
|
124
|
+
### 5. Upload + PR Markdown
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
# Upload and generate markdown
|
|
128
|
+
mkdir -p /tmp/pre-post
|
|
129
|
+
./scripts/upload-and-copy.sh /tmp/pre-post/before.png /tmp/pre-post/after.png --markdown
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Or use the CLI's built-in upload:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
npx pre-post <before.png> <after.png> --markdown
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
For multi-route PRs, generate this format:
|
|
139
|
+
|
|
140
|
+
```markdown
|
|
141
|
+
## Visual Changes
|
|
142
|
+
|
|
143
|
+
### `/dashboard`
|
|
144
|
+
|
|
145
|
+
<details open>
|
|
146
|
+
<summary>Desktop (1280x800)</summary>
|
|
147
|
+
|
|
148
|
+
| Pre | Post |
|
|
149
|
+
|:---:|:----:|
|
|
150
|
+
|  |  |
|
|
151
|
+
</details>
|
|
152
|
+
|
|
153
|
+
<details>
|
|
154
|
+
<summary>Mobile (375x812)</summary>
|
|
155
|
+
|
|
156
|
+
| Pre | Post |
|
|
157
|
+
|:---:|:----:|
|
|
158
|
+
|  |  |
|
|
159
|
+
</details>
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
*Captured by [pre-post](https://github.com/juangadm/pre-post)*
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### 6. PR Integration
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
# Get current PR
|
|
169
|
+
gh pr view --json number,body
|
|
170
|
+
|
|
171
|
+
# Append screenshots to PR body
|
|
172
|
+
gh pr edit <number> --body "<existing-body>
|
|
173
|
+
|
|
174
|
+
<generated-markdown>"
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
If no `gh` CLI: output markdown and tell user to paste manually.
|
|
178
|
+
|
|
179
|
+
## Quick Reference
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
# Basic usage (two URLs)
|
|
183
|
+
pre-post site.com localhost:3000
|
|
184
|
+
|
|
185
|
+
# Detect routes from git diff
|
|
186
|
+
pre-post detect
|
|
187
|
+
pre-post detect --framework nextjs-app
|
|
188
|
+
|
|
189
|
+
# Compare with auto-detected routes
|
|
190
|
+
pre-post run --before-base https://prod.com --after-base http://localhost:3000
|
|
191
|
+
|
|
192
|
+
# Compare specific routes
|
|
193
|
+
pre-post compare --before-base URL --after-base URL --routes /dashboard,/settings
|
|
194
|
+
|
|
195
|
+
# Responsive (desktop + mobile)
|
|
196
|
+
pre-post compare --before-base URL --after-base URL --responsive
|
|
197
|
+
|
|
198
|
+
# From existing images
|
|
199
|
+
pre-post before.png after.png --markdown
|
|
200
|
+
|
|
201
|
+
# Via npx
|
|
202
|
+
npx pre-post detect
|
|
203
|
+
npx pre-post compare --before-base URL --after-base URL
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
| Flag | Description |
|
|
207
|
+
|------|-------------|
|
|
208
|
+
| `-m, --mobile` | Mobile viewport (375x812) |
|
|
209
|
+
| `-t, --tablet` | Tablet viewport (768x1024) |
|
|
210
|
+
| `--size <WxH>` | Custom viewport |
|
|
211
|
+
| `-f, --full` | Full scrollable page |
|
|
212
|
+
| `-s, --selector` | CSS selector to capture |
|
|
213
|
+
| `-r, --responsive` | Desktop + mobile capture |
|
|
214
|
+
| `--routes <paths>` | Explicit route list (comma-separated) |
|
|
215
|
+
| `--max-routes <n>` | Max detected routes (default: 5) |
|
|
216
|
+
| `--framework <name>` | Force framework detection |
|
|
217
|
+
| `--before-base <url>` | Production URL |
|
|
218
|
+
| `--after-base <url>` | Localhost URL |
|
|
219
|
+
| `-o, --output` | Output directory (default: ~/Downloads) |
|
|
220
|
+
| `--markdown` | Upload images & output markdown |
|
|
221
|
+
| `--upload-url <url>` | Upload endpoint (overrides git-native default) |
|
|
222
|
+
|
|
223
|
+
## Image Upload
|
|
224
|
+
|
|
225
|
+
Screenshots are committed to `.pre-post/` on the current PR branch and served via `raw.githubusercontent.com`. This is the default — no external services needed.
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
# Default (git-native — commits to PR branch)
|
|
229
|
+
./scripts/upload-and-copy.sh before.png after.png --markdown
|
|
230
|
+
|
|
231
|
+
# Fallback: 0x0.st (no signup needed, 365-day expiry)
|
|
232
|
+
IMAGE_ADAPTER=0x0st ./scripts/upload-and-copy.sh before.png after.png --markdown
|
|
233
|
+
|
|
234
|
+
# GitHub Gist
|
|
235
|
+
IMAGE_ADAPTER=gist ./scripts/upload-and-copy.sh before.png after.png --markdown
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## Error Reference
|
|
239
|
+
|
|
240
|
+
| Error | Fix |
|
|
241
|
+
|-------|-----|
|
|
242
|
+
| `command not found` | `npm install -g pre-post` |
|
|
243
|
+
| `browserType.launch: Executable doesn't exist` | `npx playwright install chromium` |
|
|
244
|
+
| 401/403 on production URL | See pre-flight section above |
|
|
245
|
+
| Element not found | Verify selector exists on page |
|
|
246
|
+
| No changed files detected | Specify routes manually with `--routes` |
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# 0x0.st adapter - Free, no-signup file hosting
|
|
3
|
+
# https://0x0.st
|
|
4
|
+
#
|
|
5
|
+
# Usage: ./0x0st.sh <file>
|
|
6
|
+
# Output: URL to uploaded file (stdout)
|
|
7
|
+
#
|
|
8
|
+
# Notes:
|
|
9
|
+
# - Files expire after 365 days (or sooner for larger files)
|
|
10
|
+
# - Max file size: 512 MiB
|
|
11
|
+
# - No authentication required
|
|
12
|
+
|
|
13
|
+
set -e
|
|
14
|
+
|
|
15
|
+
FILE="$1"
|
|
16
|
+
|
|
17
|
+
if [[ -z "$FILE" ]]; then
|
|
18
|
+
echo "Usage: $0 <file>" >&2
|
|
19
|
+
exit 1
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
if [[ ! -f "$FILE" ]]; then
|
|
23
|
+
echo "Error: File not found: $FILE" >&2
|
|
24
|
+
exit 1
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
# Upload to 0x0.st - returns the URL directly
|
|
28
|
+
URL=$(curl -s -A "before-after-cli/1.0" -F "file=@$FILE" https://0x0.st)
|
|
29
|
+
|
|
30
|
+
# Validate we got a URL back
|
|
31
|
+
if [[ ! "$URL" =~ ^https?:// ]]; then
|
|
32
|
+
echo "Error: Upload failed. Response: $URL" >&2
|
|
33
|
+
exit 1
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
echo "$URL"
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Custom blob storage adapter - For self-hosted or custom endpoints
|
|
3
|
+
#
|
|
4
|
+
# Usage: ./blob.sh <file>
|
|
5
|
+
# Output: URL to uploaded file (stdout)
|
|
6
|
+
#
|
|
7
|
+
# Environment:
|
|
8
|
+
# BLOB_UPLOAD_URL Required. URL endpoint for uploading images
|
|
9
|
+
#
|
|
10
|
+
# Notes:
|
|
11
|
+
# - Expects endpoint to accept multipart form upload with "file" field
|
|
12
|
+
# - Expects response to be either:
|
|
13
|
+
# - Plain text URL
|
|
14
|
+
# - JSON with "url" field
|
|
15
|
+
|
|
16
|
+
set -e
|
|
17
|
+
|
|
18
|
+
FILE="$1"
|
|
19
|
+
|
|
20
|
+
if [[ -z "$FILE" ]]; then
|
|
21
|
+
echo "Usage: $0 <file>" >&2
|
|
22
|
+
exit 1
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
if [[ ! -f "$FILE" ]]; then
|
|
26
|
+
echo "Error: File not found: $FILE" >&2
|
|
27
|
+
exit 1
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
if [[ -z "$BLOB_UPLOAD_URL" ]]; then
|
|
31
|
+
echo "Error: BLOB_UPLOAD_URL environment variable not set" >&2
|
|
32
|
+
echo "" >&2
|
|
33
|
+
echo "Set it with:" >&2
|
|
34
|
+
echo " export BLOB_UPLOAD_URL='https://your-blob-service.com/upload'" >&2
|
|
35
|
+
exit 1
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
# Upload file
|
|
39
|
+
RESPONSE=$(curl -s -X POST -F "file=@$FILE" "$BLOB_UPLOAD_URL")
|
|
40
|
+
|
|
41
|
+
# Try to extract URL from JSON response
|
|
42
|
+
URL=$(echo "$RESPONSE" | grep -o '"url"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/"url"[[:space:]]*:[[:space:]]*"//' | sed 's/"$//' || true)
|
|
43
|
+
|
|
44
|
+
# Fallback: maybe the response is just the URL
|
|
45
|
+
if [[ -z "$URL" ]]; then
|
|
46
|
+
URL="$RESPONSE"
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
# Validate we got a URL back
|
|
50
|
+
if [[ ! "$URL" =~ ^https?:// ]]; then
|
|
51
|
+
echo "Error: Upload failed. Response: $RESPONSE" >&2
|
|
52
|
+
exit 1
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
echo "$URL"
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# GitHub Gist adapter - Upload images via gh CLI
|
|
3
|
+
#
|
|
4
|
+
# Usage: ./gist.sh <file>
|
|
5
|
+
# Output: Raw URL to uploaded file (stdout)
|
|
6
|
+
#
|
|
7
|
+
# Requirements:
|
|
8
|
+
# - gh CLI installed and authenticated
|
|
9
|
+
#
|
|
10
|
+
# Notes:
|
|
11
|
+
# - Creates a public gist for each upload
|
|
12
|
+
# - Files persist indefinitely
|
|
13
|
+
# - Requires GitHub authentication
|
|
14
|
+
|
|
15
|
+
set -e
|
|
16
|
+
|
|
17
|
+
FILE="$1"
|
|
18
|
+
|
|
19
|
+
if [[ -z "$FILE" ]]; then
|
|
20
|
+
echo "Usage: $0 <file>" >&2
|
|
21
|
+
exit 1
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
if [[ ! -f "$FILE" ]]; then
|
|
25
|
+
echo "Error: File not found: $FILE" >&2
|
|
26
|
+
exit 1
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
# Check gh is available
|
|
30
|
+
if ! command -v gh &> /dev/null; then
|
|
31
|
+
echo "Error: gh CLI not found. Install from https://cli.github.com" >&2
|
|
32
|
+
exit 1
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
# Create gist and capture output
|
|
36
|
+
GIST_OUTPUT=$(gh gist create "$FILE" --public 2>&1)
|
|
37
|
+
|
|
38
|
+
# Extract the gist URL from output
|
|
39
|
+
GIST_URL=$(echo "$GIST_OUTPUT" | grep -o 'https://gist.github.com/[^ ]*' | head -1)
|
|
40
|
+
|
|
41
|
+
if [[ -z "$GIST_URL" ]]; then
|
|
42
|
+
echo "Error: Failed to create gist. Output: $GIST_OUTPUT" >&2
|
|
43
|
+
exit 1
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
# Convert to raw URL
|
|
47
|
+
# Format: https://gist.github.com/user/id -> https://gist.githubusercontent.com/user/id/raw/filename
|
|
48
|
+
GIST_ID=$(echo "$GIST_URL" | sed 's|.*/||')
|
|
49
|
+
FILENAME=$(basename "$FILE")
|
|
50
|
+
|
|
51
|
+
# Get the raw URL via gh api
|
|
52
|
+
RAW_URL=$(gh api "gists/$GIST_ID" --jq ".files[\"$FILENAME\"].raw_url")
|
|
53
|
+
|
|
54
|
+
if [[ -z "$RAW_URL" ]]; then
|
|
55
|
+
echo "Error: Could not get raw URL for gist" >&2
|
|
56
|
+
exit 1
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
echo "$RAW_URL"
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Git-native adapter - Commit screenshots to .pre-post/ on the PR branch
|
|
3
|
+
#
|
|
4
|
+
# Usage: ./git-native.sh <file>
|
|
5
|
+
# Output: raw.githubusercontent.com URL (stdout)
|
|
6
|
+
#
|
|
7
|
+
# Requirements:
|
|
8
|
+
# - Inside a git repo with a remote named "origin"
|
|
9
|
+
# - Current branch pushed to origin
|
|
10
|
+
#
|
|
11
|
+
# Notes:
|
|
12
|
+
# - Stages only the specific file, never `git add .`
|
|
13
|
+
# - Does NOT commit or push — the caller (upload-and-copy.sh) batches that
|
|
14
|
+
# - Returns the raw GitHub URL the file will be available at after push
|
|
15
|
+
|
|
16
|
+
set -e
|
|
17
|
+
|
|
18
|
+
FILE="$1"
|
|
19
|
+
|
|
20
|
+
if [[ -z "$FILE" ]]; then
|
|
21
|
+
echo "Usage: $0 <file>" >&2
|
|
22
|
+
exit 1
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
if [[ ! -f "$FILE" ]]; then
|
|
26
|
+
echo "Error: File not found: $FILE" >&2
|
|
27
|
+
exit 1
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
# Must be inside a git repo
|
|
31
|
+
if ! git rev-parse --is-inside-work-tree &>/dev/null; then
|
|
32
|
+
echo "Error: Not inside a git repository" >&2
|
|
33
|
+
exit 1
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
# Parse owner/repo from origin remote
|
|
37
|
+
REMOTE_URL=$(git remote get-url origin 2>/dev/null)
|
|
38
|
+
if [[ -z "$REMOTE_URL" ]]; then
|
|
39
|
+
echo "Error: No 'origin' remote found" >&2
|
|
40
|
+
exit 1
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
# Extract owner/repo from HTTPS or SSH URL
|
|
44
|
+
# HTTPS: https://github.com/owner/repo.git
|
|
45
|
+
# SSH: git@github.com:owner/repo.git
|
|
46
|
+
OWNER_REPO=$(echo "$REMOTE_URL" | sed -E 's#^(https?://github\.com/|git@github\.com:)##; s#\.git$##')
|
|
47
|
+
|
|
48
|
+
if [[ -z "$OWNER_REPO" || ! "$OWNER_REPO" =~ / ]]; then
|
|
49
|
+
echo "Error: Could not parse owner/repo from remote: $REMOTE_URL" >&2
|
|
50
|
+
exit 1
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
# Get current branch
|
|
54
|
+
BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
|
|
55
|
+
if [[ -z "$BRANCH" || "$BRANCH" == "HEAD" ]]; then
|
|
56
|
+
echo "Error: Could not determine current branch (detached HEAD?)" >&2
|
|
57
|
+
exit 1
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
# Copy file into .pre-post/ at the repo root
|
|
61
|
+
REPO_ROOT=$(git rev-parse --show-toplevel)
|
|
62
|
+
DEST_DIR="$REPO_ROOT/.pre-post"
|
|
63
|
+
mkdir -p "$DEST_DIR"
|
|
64
|
+
|
|
65
|
+
FILENAME=$(basename "$FILE")
|
|
66
|
+
DEST="$DEST_DIR/$FILENAME"
|
|
67
|
+
cp "$FILE" "$DEST"
|
|
68
|
+
|
|
69
|
+
# Stage only this specific file (-f to override .gitignore)
|
|
70
|
+
git add -f "$DEST"
|
|
71
|
+
|
|
72
|
+
echo "Staged: .pre-post/$FILENAME" >&2
|
|
73
|
+
|
|
74
|
+
# Return the raw GitHub URL (will resolve after push)
|
|
75
|
+
echo "https://raw.githubusercontent.com/$OWNER_REPO/$BRANCH/.pre-post/$FILENAME"
|