@juangadm/pre-post 0.3.0 → 1.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.
Files changed (153) hide show
  1. package/README.md +174 -233
  2. package/dist/baseline.d.ts +119 -0
  3. package/dist/baseline.d.ts.map +1 -0
  4. package/dist/baseline.js +316 -0
  5. package/dist/baseline.js.map +1 -0
  6. package/dist/bin/cli.js +243 -458
  7. package/dist/bin/cli.js.map +1 -1
  8. package/dist/browser.d.ts +68 -25
  9. package/dist/browser.d.ts.map +1 -1
  10. package/dist/browser.js +500 -105
  11. package/dist/browser.js.map +1 -1
  12. package/dist/capture.d.ts +1 -6
  13. package/dist/capture.d.ts.map +1 -1
  14. package/dist/capture.js +12 -36
  15. package/dist/capture.js.map +1 -1
  16. package/dist/commands/compare.d.ts +18 -0
  17. package/dist/commands/compare.d.ts.map +1 -0
  18. package/dist/commands/compare.js +82 -0
  19. package/dist/commands/compare.js.map +1 -0
  20. package/dist/commands/detect.d.ts +25 -0
  21. package/dist/commands/detect.d.ts.map +1 -0
  22. package/dist/commands/detect.js +22 -0
  23. package/dist/commands/detect.js.map +1 -0
  24. package/dist/commands/doctor.d.ts +42 -0
  25. package/dist/commands/doctor.d.ts.map +1 -0
  26. package/dist/commands/doctor.js +119 -0
  27. package/dist/commands/doctor.js.map +1 -0
  28. package/dist/commands/login.d.ts +6 -0
  29. package/dist/commands/login.d.ts.map +1 -0
  30. package/dist/commands/login.js +34 -0
  31. package/dist/commands/login.js.map +1 -0
  32. package/dist/commands/pr.d.ts +32 -0
  33. package/dist/commands/pr.d.ts.map +1 -0
  34. package/dist/commands/pr.js +259 -0
  35. package/dist/commands/pr.js.map +1 -0
  36. package/dist/commands/prune.d.ts +10 -0
  37. package/dist/commands/prune.d.ts.map +1 -0
  38. package/dist/commands/prune.js +13 -0
  39. package/dist/commands/prune.js.map +1 -0
  40. package/dist/comparison.d.ts +104 -0
  41. package/dist/comparison.d.ts.map +1 -0
  42. package/dist/comparison.js +236 -0
  43. package/dist/comparison.js.map +1 -0
  44. package/dist/config.d.ts +46 -0
  45. package/dist/config.d.ts.map +1 -0
  46. package/dist/config.js +82 -0
  47. package/dist/config.js.map +1 -0
  48. package/dist/deployments.d.ts +71 -0
  49. package/dist/deployments.d.ts.map +1 -0
  50. package/dist/deployments.js +204 -0
  51. package/dist/deployments.js.map +1 -0
  52. package/dist/diff-pool.d.ts +17 -0
  53. package/dist/diff-pool.d.ts.map +1 -0
  54. package/dist/diff-pool.js +92 -0
  55. package/dist/diff-pool.js.map +1 -0
  56. package/dist/diff-worker.d.ts +32 -0
  57. package/dist/diff-worker.d.ts.map +1 -0
  58. package/dist/diff-worker.js +39 -0
  59. package/dist/diff-worker.js.map +1 -0
  60. package/dist/diff.d.ts +47 -0
  61. package/dist/diff.d.ts.map +1 -0
  62. package/dist/diff.js +281 -0
  63. package/dist/diff.js.map +1 -0
  64. package/dist/doctor.d.ts +79 -0
  65. package/dist/doctor.d.ts.map +1 -0
  66. package/dist/doctor.js +138 -0
  67. package/dist/doctor.js.map +1 -0
  68. package/dist/errors.d.ts +38 -0
  69. package/dist/errors.d.ts.map +1 -0
  70. package/dist/errors.js +59 -0
  71. package/dist/errors.js.map +1 -0
  72. package/dist/git.d.ts +61 -0
  73. package/dist/git.d.ts.map +1 -0
  74. package/dist/git.js +182 -0
  75. package/dist/git.js.map +1 -0
  76. package/dist/github.d.ts +163 -0
  77. package/dist/github.d.ts.map +1 -0
  78. package/dist/github.js +411 -0
  79. package/dist/github.js.map +1 -0
  80. package/dist/index.d.ts +22 -23
  81. package/dist/index.d.ts.map +1 -1
  82. package/dist/index.js +17 -67
  83. package/dist/index.js.map +1 -1
  84. package/dist/landing.d.ts +48 -0
  85. package/dist/landing.d.ts.map +1 -0
  86. package/dist/landing.js +93 -0
  87. package/dist/landing.js.map +1 -0
  88. package/dist/pkg.d.ts +17 -0
  89. package/dist/pkg.d.ts.map +1 -0
  90. package/dist/pkg.js +49 -0
  91. package/dist/pkg.js.map +1 -0
  92. package/dist/report.d.ts +20 -0
  93. package/dist/report.d.ts.map +1 -0
  94. package/dist/report.js +130 -0
  95. package/dist/report.js.map +1 -0
  96. package/dist/routes/imports.d.ts +46 -0
  97. package/dist/routes/imports.d.ts.map +1 -0
  98. package/dist/routes/imports.js +208 -0
  99. package/dist/routes/imports.js.map +1 -0
  100. package/dist/routes/vite.d.ts +15 -0
  101. package/dist/routes/vite.d.ts.map +1 -0
  102. package/dist/routes/vite.js +130 -0
  103. package/dist/routes/vite.js.map +1 -0
  104. package/dist/routes.d.ts +65 -11
  105. package/dist/routes.d.ts.map +1 -1
  106. package/dist/routes.js +346 -141
  107. package/dist/routes.js.map +1 -1
  108. package/dist/run.d.ts +116 -0
  109. package/dist/run.d.ts.map +1 -0
  110. package/dist/run.js +316 -0
  111. package/dist/run.js.map +1 -0
  112. package/dist/sameness.d.ts +91 -0
  113. package/dist/sameness.d.ts.map +1 -0
  114. package/dist/sameness.js +157 -0
  115. package/dist/sameness.js.map +1 -0
  116. package/dist/sessions.d.ts +50 -0
  117. package/dist/sessions.d.ts.map +1 -0
  118. package/dist/sessions.js +80 -0
  119. package/dist/sessions.js.map +1 -0
  120. package/dist/shift.d.ts +67 -0
  121. package/dist/shift.d.ts.map +1 -0
  122. package/dist/shift.js +184 -0
  123. package/dist/shift.js.map +1 -0
  124. package/dist/types.d.ts +179 -68
  125. package/dist/types.d.ts.map +1 -1
  126. package/dist/types.js +1 -0
  127. package/dist/types.js.map +1 -1
  128. package/dist/url.d.ts +12 -0
  129. package/dist/url.d.ts.map +1 -0
  130. package/dist/url.js +30 -0
  131. package/dist/url.js.map +1 -0
  132. package/dist/viewport.d.ts +6 -1
  133. package/dist/viewport.d.ts.map +1 -1
  134. package/dist/viewport.js +13 -7
  135. package/dist/viewport.js.map +1 -1
  136. package/package.json +21 -14
  137. package/skill/SKILL.md +56 -221
  138. package/dist/clipboard.d.ts +0 -9
  139. package/dist/clipboard.d.ts.map +0 -1
  140. package/dist/clipboard.js +0 -26
  141. package/dist/clipboard.js.map +0 -1
  142. package/dist/upload.d.ts +0 -39
  143. package/dist/upload.d.ts.map +0 -1
  144. package/dist/upload.js +0 -159
  145. package/dist/upload.js.map +0 -1
  146. package/dist/video.d.ts +0 -19
  147. package/dist/video.d.ts.map +0 -1
  148. package/dist/video.js +0 -263
  149. package/dist/video.js.map +0 -1
  150. package/skill/scripts/adapters/0x0st.sh +0 -36
  151. package/skill/scripts/adapters/blob.sh +0 -55
  152. package/skill/scripts/adapters/git-native.sh +0 -51
  153. package/skill/scripts/upload-and-copy.sh +0 -230
package/README.md CHANGED
@@ -1,272 +1,213 @@
1
1
  # pre-post
2
2
 
3
- Visual diff tool that captures pre/post screenshots for PRs. Use it as a Claude Code skill for automatic visual documentation, or run it directly from the CLI.
4
-
5
- > Originally forked from [before-and-after](https://github.com/vercel-labs/before-and-after) by [James Clements](https://github.com/jamesclement) / Vercel Labs.
6
-
7
- ## What Changed from the Original
8
-
9
- | Area | before-and-after | pre-post |
10
- |------|-----------------|----------|
11
- | Browser engine | `agent-browser` (Vercel-proprietary) | **Playwright** (direct dependency) |
12
- | Screenshot quality | 1x | **2x retina** (`deviceScaleFactor: 2`) |
13
- | Route detection | Manual | **Automatic** from `git diff` (Next.js App/Pages Router, generic fallback) |
14
- | Responsive capture | Single viewport | **Desktop + mobile** per route (`--responsive`) |
15
- | CLI subcommands | URL pairs only | `detect`, `compare`, `run` subcommands |
16
- | Skill orchestration | Basic capture | Full workflow: route detection, Claude refinement, user approval, PR posting |
17
- | Font/animation handling | None | Waits for `document.fonts.ready`, disables CSS animations |
18
-
19
- ## How It Works
20
-
21
- ```mermaid
22
- flowchart TD
23
- A[pre-post] --> B{Mode?}
24
-
25
- B -->|Manual| C["pre-post url1 url2"]
26
- B -->|Subcommand| D{"detect | compare | run"}
27
- B -->|Claude Code Skill| E["/pre-post"]
28
-
29
- D -->|detect| F["Git: diff, staged, unstaged, untracked"]
30
- D -->|compare| J
31
- D -->|run| F
32
-
33
- F --> G{Detect framework}
34
- G -->|Next.js App Router| H[Map files → routes]
35
- G -->|Next.js Pages Router| H
36
- G -->|Generic| H
37
- H --> I{Routes found?}
38
- I -->|Yes| J[Route list with confidence]
39
- I -->|No| J2["Default to /"]
40
- J2 --> J
41
-
42
- E --> E1[Pre-flight checks]
43
- E1 --> F
44
- E1 --> K[Claude refines routes]
45
- K --> K1[User approves routes]
46
- K1 --> J
47
-
48
- C --> L[Playwright]
49
- J --> L
50
-
51
- L --> M{Launch Chromium}
52
- M -->|"PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH"| M1[Custom path — fail hard if invalid]
53
- M -->|Auto-detect| M2[System Chrome → Bundled → Cached]
54
- M1 --> P[Navigate + wait for network idle]
55
- M2 --> P
56
-
57
- P --> Q[Disable CSS animations]
58
- Q --> R[Wait for document.fonts.ready]
59
- R --> S[Take 2x retina screenshot]
60
-
61
- S --> T[Save to ~/Downloads]
62
- T --> T1{--markdown?}
63
- T1 -->|No| U[Done]
64
- T1 -->|Yes| V{Upload method}
65
- V -->|Default| W["git-native: commit to .pre-post/"]
66
- V -->|--upload-url| Y["HTTP: 0x0.st / Vercel Blob / PUT"]
67
- W --> X[Build blob+SHA URL]
68
- Y --> X
69
- X --> Z[Markdown table]
70
-
71
- Z --> Z1{Skill mode?}
72
- Z1 -->|Yes| Z2[User approves screenshots]
73
- Z2 --> Z3["gh pr edit — append to PR body"]
74
- Z1 -->|No| Z4[Copy to clipboard]
75
-
76
- style A fill:#4f46e5,color:#fff
77
- style L fill:#2563eb,color:#fff
78
- style S fill:#059669,color:#fff
79
- ```
80
-
81
- ## Prerequisites
82
-
83
- - **Node.js** 18+
84
- - **Chromium** — install once via Playwright:
85
-
86
- ```bash
87
- npx playwright install chromium
88
- ```
89
-
90
- ## CI / Sandboxed Environments
91
-
92
- pre-post needs a Chromium binary. In restricted environments where `npx playwright install chromium` fails (CDN blocked):
93
-
94
- ```bash
95
- # Point to a pre-installed Chrome/Chromium binary
96
- export PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/path/to/chrome
97
-
98
- # This is treated as an explicit override — pre-post will fail with a
99
- # clear error if the path is wrong, rather than silently falling back.
100
- ```
3
+ Before/after screenshots for pull requests. One command detects the routes your branch
4
+ changed, captures each one before and after, uploads the images to GitHub, and puts them at
5
+ the top of the PR description where a reviewer can judge them at a glance.
101
6
 
102
- When no custom path is set, pre-post auto-detects in order: system Chrome, bundled Playwright Chromium, then any builds in `~/.cache/ms-playwright/`.
7
+ It works out both sides itself, cheapest option first:
103
8
 
104
- If no browser is available at all, use image-only mode:
105
-
106
- ```bash
107
- pre-post before.png after.png --markdown
108
- ```
109
-
110
- **Proxy URLs**: If your git remote uses a proxy (e.g., `http://proxy@host/git/owner/repo`), set the repo explicitly:
111
-
112
- ```bash
113
- export GH_REPO=owner/repo
114
- ```
115
-
116
- **Google Fonts**: Container egress proxies typically block `fonts.googleapis.com`.
117
- Screenshots will render with system font fallbacks. This is cosmetic only
118
- `document.fonts.ready` still resolves and layout is preserved.
9
+ | | Pre (the baseline) | Post (this branch) |
10
+ |---|---|---|
11
+ | 1 | `--before` | `--after` |
12
+ | 2 | `before` in `.pre-post.json` | the preview deployment for this commit |
13
+ | 3 | the production deployment for the base commit | a local dev server |
14
+ | 4 | whatever is on production now | |
15
+ | 5 | the base commit, served locally | |
16
+
17
+ Rows 2 to 4 need no dev environment at all, which is the point: a preview deployment and a
18
+ production URL are enough for anyone who can open the PR. Deployments come from the GitHub
19
+ Deployments API, so Vercel, Cloudflare Pages, Netlify and Render all work with no extra
20
+ token and nothing to configure; a host that records only a commit status is read from the
21
+ deployment bot's own PR comment instead. Row 4 covers repositories that do not deploy every
22
+ push to their default branch, so nothing is deployed at the fork point it prints which
23
+ commit Pre actually came from rather than implying the base.
24
+
25
+ pre-post never guesses a baseline. If no deployment can be found it says so and tells you
26
+ the one flag that fixes it, because a baseline that is quietly the wrong site reads as 100%
27
+ changed on every route and looks exactly like a real result.
28
+
29
+ The last baseline needs no network at all: it checks the base commit into a throwaway
30
+ worktree and boots its dev script. That keeps pre-post working inside a sandboxed agent
31
+ container, a CI job, or behind an egress allowlist — and it compares against exactly what
32
+ the branch forked from, rendered in the same browser as the Post side.
33
+
34
+ > Originally forked from [before-and-after](https://github.com/vercel-labs/before-and-after) by James Clements / Vercel Labs.
35
+
36
+ ```
37
+ $ npx -y @juangadm/pre-post@latest pr
38
+
39
+ Routes (nextjs-app, 41ms): /, /pricing
40
+ / medium src/app/page.tsx imports src/components/ui/button.tsx (2 hops)
41
+ /pricing medium src/app/pricing/page.tsx imports src/components/ui/button.tsx (2 hops)
42
+ Capturing 8 screenshots (2 route(s) × 2 viewport(s)) ...
43
+ changed /pricing @ mobile (1.42%, 1811ms)
44
+ changed /pricing @ desktop (0.64%, 2036ms)
45
+ same / @ mobile (0.00%, 1990ms)
46
+ same / @ desktop (0.00%, 2211ms)
47
+ Publishing 10 image(s) to acme/web@pre-post-assets ...
48
+ Updated PR description: https://github.com/acme/web/pull/42
49
+ pre-post · PR #42 · 2 route(s) · 2 viewport(s) · 6.8s
50
+ / desktop no change
51
+ / mobile no change
52
+ /pricing desktop 0.64% changed
53
+ /pricing mobile 1.4% changed
54
+ Comment: https://github.com/acme/web/pull/42
55
+ ```
56
+
57
+ ## How it works
58
+
59
+ 1. **Routes.** Diffs the branch against the merge base with `main` — fetching that branch
60
+ first when the checkout does not have it, which is the normal shape in CI and in the
61
+ web/sandbox editors. When no shared history can be established it stops with one
62
+ sentence rather than reporting an empty diff, and `--base <ref>` names the commit
63
+ directly. Then it follows the import
64
+ graph: a change to `components/ui/button.tsx` marks every page that imports it. Next.js
65
+ App Router and Pages Router, Vite apps (React Router, file-based `src/pages`), and a
66
+ generic fallback. Monorepos are handled by picking the app that owns the changed files.
67
+ 2. **Capture.** Playwright + Chromium headless shell. The page's own clock is held
68
+ still while it loads and then run forward by a fixed budget, so a page that animates
69
+ on a timer is photographed at the same frame on both sides however fast each host
70
+ answered. Reduced motion, animations finished, caret hidden, fonts and images settled,
71
+ layout stable, lazy content primed. 2x device scale, full page (capped at 2400 CSS
72
+ px), desktop (add mobile with `-r`).
73
+ All routes and viewports run concurrently.
74
+ 3. **Diff.** Pure-JS pixel comparison in worker threads. Reports the percentage changed,
75
+ the bounding box, and a tight crop of the changed region. A route counts as changed when
76
+ the painted difference covers at least `minChangedArea` CSS pixels² (default 100, roughly
77
+ a third of a 16px icon) or at least `threshold` of the canvas — the first rule is what
78
+ decides on a page, the second on a small image.
79
+ 4. **Layout shift.** A padding change near the top of a page moves everything below it, and
80
+ pixel diffing counts every moved pixel as changed: a change a designer would call
81
+ "slightly roomier" reads as most of the page repainted, and the crop is suppressed just
82
+ when it would be most useful. So the two sides are checked for a single vertical offset
83
+ first — how far the content moved, and from which row. When one is found, Pre is re-spaced
84
+ into Post's layout and the two are compared there: the crop comes from that pair, and the
85
+ comment says `Content shifted down 48px` instead of quoting a percentage. Rows Post gained
86
+ are left as background rather than skipped, so content inserted above the shift — the
87
+ banner that caused it — still reads as new rather than as nothing. The offset is only
88
+ accepted when it accounts for most of the difference across the rows both sides share.
89
+ The raw numbers are left alone: a move is a visual change, and reporting less of one would
90
+ hide it. Reflow, where content moves both across and down, has no single offset to find,
91
+ so it is reported the way it always was.
92
+ 5. **Publish.** Images go to a `pre-post-assets` branch in the same repository via the GitHub
93
+ API, as one commit per run. Nothing is committed to the PR branch, no CI is triggered,
94
+ and the blob URLs render on private repos — a screenshot is visible to exactly whoever
95
+ can see the repository. `pre-post prune` removes images for PRs closed more than 90 days
96
+ ago, but note that it commits a deletion rather than rewriting history: the older commits
97
+ still hold the blobs, so a link handed out earlier keeps working. Treat anything captured
98
+ as permanent, and think twice before pointing pre-post at a preview holding real data.
99
+ 6. **Describe.** The images go in a delimited block at the top of the PR description,
100
+ replaced in place on every run and leaving your own text untouched. Changed routes show
101
+ a Pre/Post crop with the full page collapsed underneath; unchanged routes fold into a
102
+ single line. If the PR cannot be edited — a fork, a read-only token — it falls back to
103
+ one sticky comment.
119
104
 
120
105
  ## Install
121
106
 
122
- ```bash
123
- npm install -D @juangadm/pre-post
124
- ```
125
-
126
- ## Basic Use
127
-
128
- ### As a Claude Code Skill (recommended)
129
-
130
- After making visual UI changes, say `/pre-post` or "take pre and post screenshots". Claude will:
131
-
132
- 1. Detect affected routes from your git diff
133
- 2. Propose routes for your approval
134
- 3. Capture desktop + mobile screenshots (production vs localhost)
135
- 4. Show you the screenshots for approval
136
- 5. Upload and append markdown to your PR
107
+ Nothing to install. `npx -y @juangadm/pre-post@latest pr` downloads the CLI and, on first
108
+ use, the Chromium headless shell (~80 MB). You need Node 20+ and a GitHub token: either
109
+ `gh auth login` or `GH_TOKEN`.
137
110
 
138
- ### From the CLI
139
-
140
- Capture any two URLs:
141
-
142
- ```bash
143
- pre-post site.com localhost:3000
144
- ```
145
-
146
- Use existing images:
111
+ As a Claude Code skill:
147
112
 
148
113
  ```bash
149
- pre-post before.png after.png
114
+ npx skills add juangadm/pre-post -y
150
115
  ```
151
116
 
152
- ## CLI Subcommands
153
-
154
- ### `detect` -- Route Detection
117
+ Then say `/pre-post` after making UI changes.
155
118
 
156
- Detect affected routes from git changes:
119
+ ## Usage
157
120
 
158
121
  ```bash
159
- pre-post detect # Auto-detect framework
160
- pre-post detect --framework nextjs-app # Force framework
161
- ```
162
-
163
- Outputs JSON with route paths, confidence levels, and source files.
122
+ pre-post pr # everything, on the current branch's PR
123
+ pre-post pr --before https://acme.com # pin the baseline (saved to .pre-post.json)
124
+ pre-post pr --no-local-baseline # never build the base commit locally
125
+ pre-post pr --routes /pricing,/docs # explicit routes
126
+ pre-post pr --viewports desktop,1440x900 # custom viewports
127
+ pre-post pr --dry-run # capture + diff locally, post nothing
128
+ pre-post pr --json # machine-readable output
164
129
 
165
- ### `compare` -- URL Comparison
166
-
167
- Compare pre/post states across routes:
168
-
169
- ```bash
170
- pre-post compare --before-base https://prod.com --after-base http://localhost:3000
171
- pre-post compare --before-base URL --after-base URL --routes /dashboard,/settings
172
- pre-post compare --before-base URL --after-base URL --responsive # Desktop + mobile
130
+ pre-post https://acme.com http://localhost:3000 --routes /pricing # ad-hoc comparison
131
+ pre-post before.png after.png # diff two images
132
+ pre-post detect # which routes does this branch touch?
133
+ pre-post login https://staging.acme.com # sign in once; the session is reused
134
+ pre-post prune --days 90 # clean up the assets branch
135
+ pre-post doctor # browser, token, dev server, config
173
136
  ```
174
137
 
175
- ### `run` -- Full Auto
138
+ When something needs a human, the CLI exits with code 3 and one sentence saying what to do
139
+ (log in, start the dev server, pass `--before`). Re-running picks up where it left off.
176
140
 
177
- Combines `detect` + `compare`:
141
+ Exit codes, so a script or an agent can branch on them:
178
142
 
179
- ```bash
180
- pre-post run --before-base https://prod.com --after-base http://localhost:3000
181
- ```
143
+ | code | meaning |
144
+ |---|---|
145
+ | 0 | done; for `doctor`, `pre-post pr` can run |
146
+ | 1 | the run failed — every capture errored, or an unexpected error; for `doctor`, a required check failed |
147
+ | 2 | the arguments could not be parsed |
148
+ | 3 | something needs a human; the message says what |
182
149
 
183
- ## Options
184
-
185
- Capture a specific element using a CSS selector:
186
-
187
- ```bash
188
- pre-post url1 url2 ".hero"
189
- ```
190
-
191
- Use different selectors for pre and post:
192
-
193
- ```bash
194
- pre-post url1 url2 ".old" ".new"
195
- ```
196
-
197
- Capture at mobile (375x812), tablet (768x1024), or custom viewport:
198
-
199
- ```bash
200
- pre-post url1 url2 --mobile
201
- pre-post url1 url2 --size 1920x1080
202
- ```
150
+ `doctor` marks a check **FAIL** only when `pre-post pr` has no way to proceed without it —
151
+ the browser, a GitHub token, and being inside a git repository. Everything else prints as
152
+ **note**: no dev server running, or no `--before` saved, narrows *which* strategy a run
153
+ picks rather than stopping it, so those never change the exit code.
203
154
 
204
- Capture the entire scrollable page:
155
+ Results go into a delimited block at the top of the PR description, which re-runs replace in
156
+ place, leaving your own text untouched. If the PR cannot be edited — a fork, a read-only
157
+ token — it falls back to a single sticky comment.
205
158
 
206
- ```bash
207
- pre-post url1 url2 --full
208
- ```
159
+ ## Configuration
209
160
 
210
- Output a markdown table for PR descriptions:
161
+ Optional `.pre-post.json` in the repo root:
211
162
 
212
- ```bash
213
- pre-post url1 url2 --markdown
163
+ ```json
164
+ {
165
+ "before": "https://acme.com",
166
+ "routes": ["/"],
167
+ "samples": { "/blog/[slug]": "/blog/hello-world" },
168
+ "viewports": ["desktop"],
169
+ "fullPage": true,
170
+ "maxHeight": 2400,
171
+ "scale": 2,
172
+ "threshold": 0.001,
173
+ "minChangedArea": 100,
174
+ "maxRoutes": 6,
175
+ "ignore": ["apps/docs"],
176
+ "headers": {},
177
+ "assetsBranch": "pre-post-assets"
178
+ }
214
179
  ```
215
180
 
216
- Save to a custom location:
181
+ Environment:
217
182
 
218
- ```bash
219
- pre-post url1 url2 --output ./screenshots
220
- ```
183
+ | Variable | Purpose |
184
+ |---|---|
185
+ | `GH_TOKEN` / `GITHUB_TOKEN` | GitHub token (default: `gh auth token`) |
186
+ | `VERCEL_AUTOMATION_BYPASS_SECRET` | Bypass Vercel Deployment Protection on preview and production URLs |
187
+ | `PRE_POST_CONCURRENCY` | Parallel pages (default 6) |
188
+ | `PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH` | Use a specific Chromium binary |
189
+ | `GH_REPO` | `owner/repo` when the remote URL cannot be parsed |
221
190
 
222
- Upload to a custom image storage service:
191
+ ## Library
223
192
 
224
- ```bash
225
- pre-post url1 url2 --markdown --upload-url https://my-s3-bucket.amazonaws.com
193
+ ```ts
194
+ import { runPr, captureScreenshot, diffImages, detectRoutesForRepo } from '@juangadm/pre-post';
226
195
  ```
227
196
 
228
- By default, `--markdown` commits screenshots to the PR branch (under `.pre-post/`) and serves them via GitHub blob URLs pinned to the commit SHA. This works for both public and private repos. Use `--upload-url` to point at your own storage instead. It auto-detects the protocol for 0x0.st, Vercel Blob, and any generic PUT endpoint (like S3). Screenshots auto-append to the PR body, newest on top.
229
-
230
- ## Route Detection
231
-
232
- Pre-post automatically maps `git diff --name-only` to affected UI routes:
233
-
234
- | Changed File | Detected Route | Confidence |
235
- |---|---|---|
236
- | `app/page.tsx` | `/` | high |
237
- | `app/dashboard/page.tsx` | `/dashboard` | high |
238
- | `app/(marketing)/about/page.tsx` | `/about` (strips route groups) | high |
239
- | `app/blog/[slug]/page.tsx` | `/blog/[slug]` | high |
240
- | `app/dashboard/layout.tsx` | `/dashboard` | medium |
241
- | `app/dashboard/components/Chart.tsx` | `/dashboard` | medium |
242
- | `globals.css`, `tailwind.config.ts` | `/` | low |
243
- | `app/api/*`, `middleware.ts` | Skipped (no visual) | -- |
244
-
245
- Supports:
246
- - **Next.js App Router** (route groups, dynamic segments, parallel routes, catch-all)
247
- - **Next.js Pages Router** (`pages/`, `_app.tsx`, `_document.tsx`)
248
- - **Generic fallback** (defaults to `/`)
249
-
250
- ## Add Skill
251
-
252
- Install as an agent skill using the [`skills` CLI](https://github.com/vercel-labs/skills):
197
+ ## Development
253
198
 
254
199
  ```bash
255
- npx skills add juangadm/pre-post # interactive — pick your agent(s)
256
- npx skills add juangadm/pre-post -y # auto-install to all universal agents
257
- npx skills add juangadm/pre-post -y -g # install globally (all projects)
200
+ pnpm install
201
+ pnpm build
202
+ pnpm test:unit
203
+ TEST_BROWSER=true pnpm test # needs a Chromium; npx playwright-core install chromium-headless-shell
258
204
  ```
259
205
 
260
- This copies `skill/SKILL.md` into the appropriate directory for your agent (e.g., `.claude/skills/` for Claude Code, `.agents/skills/` for Amp/Codex/Gemini CLI, etc.).
261
-
262
- The skill uses `gh` to detect the associated PR and Playwright for screenshots.
263
-
264
- > **Note:** The npm package (`@juangadm/pre-post`) is published on [npmjs.com](https://www.npmjs.com/package/@juangadm/pre-post), not GitHub Packages so the "Packages" section on the GitHub repo page will show none.
265
-
266
- ## Credits
267
-
268
- - Original [before-and-after](https://github.com/vercel-labs/before-and-after) by [James Clements](https://github.com/jamesclement) at [Vercel Labs](https://github.com/vercel-labs)
269
- - Browser automation powered by [Playwright](https://playwright.dev/)
206
+ pnpm and npm cannot share a `node_modules`. pnpm builds a symlinked tree that npm
207
+ cannot read, so `npm install` over it fails with `Cannot read properties of null
208
+ (reading 'edgesOut')`, which names none of that. Run `rm -rf node_modules
209
+ site/node_modules` before switching either way. npm also installs the CLI only —
210
+ `site/` is a pnpm workspace member, so use pnpm to work on the site.
270
211
 
271
212
  ## License
272
213
 
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Serve the PR's base commit locally, as the "Pre" side.
3
+ *
4
+ * This is the baseline that always works: it needs no deployment, no
5
+ * production URL and no network, so it is what keeps pre-post usable inside a
6
+ * sandbox — a cloud agent container, a CI job, or any box behind an egress
7
+ * allowlist. It is also the most honest comparison available, because both
8
+ * sides then render in the same environment with the same fonts and the same
9
+ * browser, leaving the diff as the only variable.
10
+ *
11
+ * The cost is time: a checkout, an install and a dev-server boot. So callers
12
+ * reach for it after a reachable deployment, not before one.
13
+ */
14
+ export interface LocalBaseline {
15
+ url: string;
16
+ /** Stop the dev server and delete the worktree. Safe to call twice. */
17
+ stop: () => Promise<void>;
18
+ }
19
+ interface PackageManager {
20
+ bin: string;
21
+ install: string[];
22
+ /** Build `run <script>` argv, appending extra args the way this manager wants. */
23
+ run: (script: string, args: string[]) => string[];
24
+ }
25
+ /**
26
+ * Which package manager this tree *declares*, from its `packageManager` field
27
+ * or its lockfile. Defaults to npm.
28
+ *
29
+ * The field wins over the lockfile because it is the deliberate statement: a
30
+ * repository mid-migration can carry two lockfiles, but it names one manager.
31
+ */
32
+ export declare function detectPackageManager(dir: string, repoRoot?: string): PackageManager;
33
+ /**
34
+ * Is `bin` runnable here?
35
+ *
36
+ * A PATH scan rather than a `--version` subprocess: this answers before every
37
+ * local baseline, and spawning a process to learn that a process cannot be
38
+ * spawned is the wrong shape.
39
+ */
40
+ export declare function onPath(bin: string, env?: NodeJS.ProcessEnv): boolean;
41
+ export interface ManagerChoice {
42
+ /** The manager to run, or null when neither it nor npm is installed here. */
43
+ pm: PackageManager | null;
44
+ /** What the repository declares. Differs from `pm` when we fell back. */
45
+ declared: PackageManager;
46
+ }
47
+ /**
48
+ * The manager to actually run: what the repository declares, if it is installed.
49
+ *
50
+ * Declaring pnpm does not put pnpm on PATH. On a real machine it was not there,
51
+ * and the local baseline — the fallback that exists for when nothing is
52
+ * deployed — died on `pnpm install` with no hint that the missing piece was
53
+ * pnpm itself. The same shell could still build the CLI, so the tool ran while
54
+ * its last resort could not.
55
+ *
56
+ * npm ships with Node, so it is the fallback with the best odds of being
57
+ * present. It will not honour a pnpm or yarn lockfile, so the baseline it
58
+ * installs can drift from the locked versions — worth saying out loud, and
59
+ * still far better than no baseline at all.
60
+ */
61
+ export declare function resolvePackageManager(dir: string, repoRoot?: string, has?: (bin: string) => boolean): ManagerChoice;
62
+ /**
63
+ * The directory to actually serve.
64
+ *
65
+ * The app root is detected from the files a branch changed, which answers
66
+ * "which routes moved" — not "which package can be served". A PR touching only
67
+ * the CLI resolves to the repo root, which has no dev script even though the
68
+ * site next door does. So prefer the detected directory and otherwise take the
69
+ * nearest package that can start a server.
70
+ */
71
+ export declare function servableDir(treeRoot: string, appPrefix?: string): {
72
+ dir: string;
73
+ script: string;
74
+ } | null;
75
+ /**
76
+ * Copy the working checkout's local env files into a throwaway worktree.
77
+ *
78
+ * .env files are gitignored, so a fresh worktree of the base commit has none.
79
+ * An app that needs one to boot then either never starts or serves an error
80
+ * page — and an error page diffed against the real branch reports a wall of
81
+ * changes the PR never made, which is worse than reporting no baseline at all.
82
+ *
83
+ * Only ever writes inside `to`, and returns names, never values: these files
84
+ * are exactly the ones that hold secrets.
85
+ */
86
+ export declare function copyEnvFiles(from: string, to: string, appPrefix?: string): string[];
87
+ /** An OS-assigned free port. */
88
+ export declare function freePort(): Promise<number>;
89
+ export interface BaselineOptions {
90
+ repoRoot: string;
91
+ /** Commit to serve. Omit to serve the working tree as it stands. */
92
+ sha?: string;
93
+ /** Directory holding the app's package.json, relative to the repo root. */
94
+ appPrefix?: string;
95
+ /** Budget for install + boot. */
96
+ timeoutMs?: number;
97
+ /** Injectable for tests; defaults to a real PATH scan. */
98
+ pathHas?: (bin: string) => boolean;
99
+ log?: (msg: string) => void;
100
+ }
101
+ /**
102
+ * Check out `sha` into a throwaway worktree, install, and start its dev server.
103
+ *
104
+ * Returns null when this repo cannot be served this way (no dev script, no
105
+ * install, server never came up). Callers treat that as "try the next option",
106
+ * so every failure path cleans up after itself and stays quiet.
107
+ */
108
+ export declare function serveBaseCommit(opts: BaselineOptions): Promise<LocalBaseline | null>;
109
+ /**
110
+ * Boot this repository's dev server without one already running.
111
+ *
112
+ * The working tree is what the author is looking at, uncommitted edits and all,
113
+ * so it is the honest "Post" when nothing is deployed. Serving it needs no
114
+ * worktree and usually no install, which is why it is cheaper than the base
115
+ * side it gets paired with.
116
+ */
117
+ export declare function serveWorkingTree(opts: Omit<BaselineOptions, 'sha'>): Promise<LocalBaseline | null>;
118
+ export {};
119
+ //# sourceMappingURL=baseline.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"baseline.d.ts","sourceRoot":"","sources":["../src/baseline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAUH,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,uEAAuE;IACvE,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3B;AAGD,UAAU,cAAc;IACtB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,kFAAkF;IAClF,GAAG,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,MAAM,EAAE,CAAC;CACnD;AAgBD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,cAAc,CAUnF;AAED;;;;;;GAMG;AACH,wBAAgB,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,OAAO,CAUjF;AAED,MAAM,WAAW,aAAa;IAC5B,6EAA6E;IAC7E,EAAE,EAAE,cAAc,GAAG,IAAI,CAAC;IAC1B,yEAAyE;IACzE,QAAQ,EAAE,cAAc,CAAC;CAC1B;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,MAAM,EACX,QAAQ,CAAC,EAAE,MAAM,EACjB,GAAG,GAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAgB,GACrC,aAAa,CAIf;AAED;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAUxG;AAKD;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAkBnF;AAED,gCAAgC;AAChC,wBAAgB,QAAQ,IAAI,OAAO,CAAC,MAAM,CAAC,CAS1C;AAgBD,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,oEAAoE;IACpE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,2EAA2E;IAC3E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iCAAiC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,0DAA0D;IAC1D,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IACnC,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7B;AAED;;;;;;GAMG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAE1F;AAED;;;;;;;GAOG;AACH,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,IAAI,CAAC,eAAe,EAAE,KAAK,CAAC,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAExG"}