@juangadm/pre-post 0.3.0 → 1.0.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/README.md +102 -240
- package/dist/bin/cli.js +200 -454
- package/dist/bin/cli.js.map +1 -1
- package/dist/browser.d.ts +42 -27
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +358 -105
- package/dist/browser.js.map +1 -1
- package/dist/capture.d.ts +1 -6
- package/dist/capture.d.ts.map +1 -1
- package/dist/capture.js +12 -36
- package/dist/capture.js.map +1 -1
- package/dist/commands/compare.d.ts +18 -0
- package/dist/commands/compare.d.ts.map +1 -0
- package/dist/commands/compare.js +64 -0
- package/dist/commands/compare.js.map +1 -0
- package/dist/commands/detect.d.ts +20 -0
- package/dist/commands/detect.d.ts.map +1 -0
- package/dist/commands/detect.js +19 -0
- package/dist/commands/detect.js.map +1 -0
- package/dist/commands/doctor.d.ts +10 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +38 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/login.d.ts +6 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +34 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/pr.d.ts +28 -0
- package/dist/commands/pr.d.ts.map +1 -0
- package/dist/commands/pr.js +159 -0
- package/dist/commands/pr.js.map +1 -0
- package/dist/commands/prune.d.ts +10 -0
- package/dist/commands/prune.d.ts.map +1 -0
- package/dist/commands/prune.js +13 -0
- package/dist/commands/prune.js.map +1 -0
- package/dist/config.d.ts +36 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +60 -0
- package/dist/config.js.map +1 -0
- package/dist/diff-pool.d.ts +17 -0
- package/dist/diff-pool.d.ts.map +1 -0
- package/dist/diff-pool.js +89 -0
- package/dist/diff-pool.js.map +1 -0
- package/dist/diff-worker.d.ts +29 -0
- package/dist/diff-worker.d.ts.map +1 -0
- package/dist/diff-worker.js +38 -0
- package/dist/diff-worker.js.map +1 -0
- package/dist/diff.d.ts +37 -0
- package/dist/diff.d.ts.map +1 -0
- package/dist/diff.js +171 -0
- package/dist/diff.js.map +1 -0
- package/dist/doctor.d.ts +38 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +72 -0
- package/dist/doctor.js.map +1 -0
- package/dist/errors.d.ts +38 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +59 -0
- package/dist/errors.js.map +1 -0
- package/dist/git.d.ts +23 -0
- package/dist/git.d.ts.map +1 -0
- package/dist/git.js +86 -0
- package/dist/git.js.map +1 -0
- package/dist/github.d.ts +73 -0
- package/dist/github.d.ts.map +1 -0
- package/dist/github.js +240 -0
- package/dist/github.js.map +1 -0
- package/dist/index.d.ts +22 -23
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -67
- package/dist/index.js.map +1 -1
- package/dist/pkg.d.ts +8 -0
- package/dist/pkg.d.ts.map +1 -0
- package/dist/pkg.js +35 -0
- package/dist/pkg.js.map +1 -0
- package/dist/report.d.ts +20 -0
- package/dist/report.d.ts.map +1 -0
- package/dist/report.js +107 -0
- package/dist/report.js.map +1 -0
- package/dist/routes/imports.d.ts +46 -0
- package/dist/routes/imports.d.ts.map +1 -0
- package/dist/routes/imports.js +208 -0
- package/dist/routes/imports.js.map +1 -0
- package/dist/routes/vite.d.ts +15 -0
- package/dist/routes/vite.d.ts.map +1 -0
- package/dist/routes/vite.js +130 -0
- package/dist/routes/vite.js.map +1 -0
- package/dist/routes.d.ts +41 -14
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +271 -141
- package/dist/routes.js.map +1 -1
- package/dist/run.d.ts +36 -0
- package/dist/run.d.ts.map +1 -0
- package/dist/run.js +128 -0
- package/dist/run.js.map +1 -0
- package/dist/sessions.d.ts +50 -0
- package/dist/sessions.d.ts.map +1 -0
- package/dist/sessions.js +74 -0
- package/dist/sessions.js.map +1 -0
- package/dist/types.d.ts +118 -69
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -1
- package/dist/viewport.d.ts +6 -1
- package/dist/viewport.d.ts.map +1 -1
- package/dist/viewport.js +13 -7
- package/dist/viewport.js.map +1 -1
- package/package.json +21 -14
- package/skill/SKILL.md +31 -229
- package/dist/clipboard.d.ts +0 -9
- package/dist/clipboard.d.ts.map +0 -1
- package/dist/clipboard.js +0 -26
- package/dist/clipboard.js.map +0 -1
- package/dist/upload.d.ts +0 -39
- package/dist/upload.d.ts.map +0 -1
- package/dist/upload.js +0 -159
- package/dist/upload.js.map +0 -1
- package/dist/video.d.ts +0 -19
- package/dist/video.d.ts.map +0 -1
- package/dist/video.js +0 -263
- package/dist/video.js.map +0 -1
- package/skill/scripts/adapters/0x0st.sh +0 -36
- package/skill/scripts/adapters/blob.sh +0 -55
- package/skill/scripts/adapters/git-native.sh +0 -51
- package/skill/scripts/upload-and-copy.sh +0 -230
package/dist/bin/cli.js
CHANGED
|
@@ -1,498 +1,244 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { parseArgs } from 'node:util';
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
3
|
+
import { createRequire } from 'module';
|
|
4
|
+
import { runPr } from '../commands/pr.js';
|
|
5
|
+
import { runCompare } from '../commands/compare.js';
|
|
6
|
+
import { runLogin } from '../commands/login.js';
|
|
7
|
+
import { runPrune } from '../commands/prune.js';
|
|
8
|
+
import { runDoctor } from '../commands/doctor.js';
|
|
9
|
+
import { runDetect } from '../commands/detect.js';
|
|
10
|
+
import { NeedsHumanError } from '../errors.js';
|
|
11
|
+
import { buildSummary } from '../report.js';
|
|
12
|
+
const require = createRequire(import.meta.url);
|
|
13
|
+
const VERSION = (() => {
|
|
14
|
+
try {
|
|
15
|
+
return require('../../package.json').version;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return '0.0.0';
|
|
19
|
+
}
|
|
20
|
+
})();
|
|
21
|
+
const SUBCOMMANDS = ['pr', 'detect', 'login', 'prune', 'doctor', 'compare'];
|
|
22
|
+
const argv = process.argv.slice(2);
|
|
23
|
+
const subcommand = SUBCOMMANDS.includes(argv[0]) ? argv.shift() : null;
|
|
21
24
|
const { values, positionals } = parseArgs({
|
|
25
|
+
args: argv,
|
|
22
26
|
allowPositionals: true,
|
|
23
27
|
options: {
|
|
24
28
|
help: { type: 'boolean', short: 'h' },
|
|
25
|
-
|
|
29
|
+
version: { type: 'boolean', short: 'v' },
|
|
30
|
+
before: { type: 'string' },
|
|
31
|
+
after: { type: 'string' },
|
|
32
|
+
routes: { type: 'string' },
|
|
33
|
+
'max-routes': { type: 'string' },
|
|
34
|
+
framework: { type: 'string' },
|
|
35
|
+
viewports: { type: 'string' },
|
|
26
36
|
mobile: { type: 'boolean', short: 'm' },
|
|
27
37
|
tablet: { type: 'boolean', short: 't' },
|
|
28
|
-
|
|
29
|
-
|
|
38
|
+
responsive: { type: 'boolean', short: 'r' },
|
|
39
|
+
size: { type: 'string' },
|
|
30
40
|
full: { type: 'boolean', short: 'f' },
|
|
31
|
-
|
|
32
|
-
|
|
41
|
+
'viewport-only': { type: 'boolean' },
|
|
42
|
+
scale: { type: 'string' },
|
|
43
|
+
threshold: { type: 'string' },
|
|
44
|
+
'max-height': { type: 'string' },
|
|
45
|
+
wait: { type: 'string' },
|
|
46
|
+
header: { type: 'string', multiple: true },
|
|
47
|
+
cookie: { type: 'string', multiple: true },
|
|
33
48
|
output: { type: 'string', short: 'o' },
|
|
34
|
-
|
|
35
|
-
'
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
framework: { type: 'string' },
|
|
41
|
-
'before-base': { type: 'string' },
|
|
42
|
-
'after-base': { type: 'string' },
|
|
43
|
-
// Video/GIF options
|
|
44
|
-
video: { type: 'boolean' },
|
|
45
|
-
duration: { type: 'string' },
|
|
46
|
-
fps: { type: 'string' },
|
|
47
|
-
delay: { type: 'string' },
|
|
49
|
+
'dry-run': { type: 'boolean' },
|
|
50
|
+
'no-comment': { type: 'boolean' },
|
|
51
|
+
pr: { type: 'string' },
|
|
52
|
+
json: { type: 'boolean' },
|
|
53
|
+
days: { type: 'string' },
|
|
54
|
+
quiet: { type: 'boolean', short: 'q' },
|
|
48
55
|
},
|
|
49
56
|
});
|
|
50
57
|
function printHelp() {
|
|
51
|
-
console.log(`
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
pre-post <before> <after> [
|
|
56
|
-
pre-post detect
|
|
57
|
-
pre-post
|
|
58
|
-
pre-post
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
-
|
|
76
|
-
-
|
|
77
|
-
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
VIDEO OPTIONS:
|
|
89
|
-
--video Capture animated GIF instead of static screenshot
|
|
90
|
-
--duration <seconds> Recording duration (default: 3, max: 10)
|
|
91
|
-
--fps <n> Target frames per second (default: 5, max: 10)
|
|
92
|
-
--delay <ms> Wait after page load before recording (default: 0)
|
|
93
|
-
|
|
94
|
-
OUTPUT OPTIONS:
|
|
95
|
-
-o, --output <dir> Output directory (default: ~/Downloads)
|
|
96
|
-
--markdown Upload images & output markdown table
|
|
97
|
-
--upload-url <url> Upload endpoint (overrides git-native default)
|
|
98
|
-
Auto-detects: 0x0.st, blob.vercel, generic PUT
|
|
99
|
-
|
|
100
|
-
OTHER OPTIONS:
|
|
101
|
-
-h, --help Show this help
|
|
102
|
-
|
|
103
|
-
EXAMPLES:
|
|
104
|
-
# Compare two URLs (protocol optional)
|
|
105
|
-
pre-post google.com facebook.com
|
|
106
|
-
pre-post https://old.example.com https://new.example.com
|
|
107
|
-
|
|
108
|
-
# Detect routes from git diff
|
|
109
|
-
pre-post detect
|
|
110
|
-
pre-post detect --framework nextjs-app
|
|
111
|
-
|
|
112
|
-
# Compare with auto-detected routes
|
|
113
|
-
pre-post run --before-base https://prod.com --after-base http://localhost:3000
|
|
114
|
-
|
|
115
|
-
# Compare specific routes
|
|
116
|
-
pre-post compare --before-base https://prod.com --after-base http://localhost:3000 --routes /dashboard,/settings
|
|
117
|
-
|
|
118
|
-
# Responsive capture (desktop + mobile)
|
|
119
|
-
pre-post compare --before-base url1 --after-base url2 --responsive
|
|
120
|
-
|
|
121
|
-
# Capture animated GIFs instead of static screenshots
|
|
122
|
-
pre-post google.com facebook.com --video
|
|
123
|
-
pre-post google.com facebook.com --video --duration 5 --fps 8
|
|
124
|
-
|
|
125
|
-
# GIF capture with compare mode
|
|
126
|
-
pre-post compare --before-base url1 --after-base url2 --video --delay 500
|
|
127
|
-
|
|
128
|
-
# Use existing images (auto-detected)
|
|
129
|
-
pre-post before.png after.png --markdown
|
|
58
|
+
console.log(`pre-post ${VERSION} — before/after screenshots for pull requests
|
|
59
|
+
|
|
60
|
+
USAGE
|
|
61
|
+
pre-post pr [options] Detect routes, capture, diff, publish, comment on the PR
|
|
62
|
+
pre-post <before> <after> [options] Compare two URLs (or two PNG files)
|
|
63
|
+
pre-post detect Print the routes this branch affects (JSON)
|
|
64
|
+
pre-post login <url> Sign in once; the session is reused for captures
|
|
65
|
+
pre-post prune [--days 90] Delete screenshots for PRs closed longer ago
|
|
66
|
+
pre-post doctor Check browser, GitHub auth, dev server
|
|
67
|
+
|
|
68
|
+
OPTIONS
|
|
69
|
+
--before <url> Production base URL (saved to .pre-post.json)
|
|
70
|
+
--after <url> Local base URL (auto-detected)
|
|
71
|
+
--routes /a,/b Explicit routes instead of detection
|
|
72
|
+
--max-routes <n> Cap on detected routes (default 6)
|
|
73
|
+
--viewports desktop,mobile,1440x900 Viewports (default desktop,mobile)
|
|
74
|
+
-m, --mobile -t, --tablet --size WxH Single-viewport shorthands
|
|
75
|
+
-f, --full | --viewport-only Full page (default for pr) or first screen only
|
|
76
|
+
--scale <n> Device scale factor (default 2)
|
|
77
|
+
--threshold <0..1> Change ratio treated as "no change" (default 0.001)
|
|
78
|
+
--max-height <px> Full-page height cap in CSS px (default 2400)
|
|
79
|
+
--wait <ms> Extra wait after the page settles
|
|
80
|
+
--header k=v Extra request header (repeatable)
|
|
81
|
+
--cookie name=value Cookie for the production host (repeatable)
|
|
82
|
+
-o, --output <dir> Where to write images (default: temp dir)
|
|
83
|
+
--dry-run Capture and diff only; no upload, no comment
|
|
84
|
+
--no-comment Publish images but do not touch the PR
|
|
85
|
+
--pr <number> Target a specific PR
|
|
86
|
+
--json Machine-readable output
|
|
87
|
+
-q, --quiet Only the final summary
|
|
88
|
+
|
|
89
|
+
ENVIRONMENT
|
|
90
|
+
GH_TOKEN / GITHUB_TOKEN GitHub token (default: gh auth token)
|
|
91
|
+
VERCEL_AUTOMATION_BYPASS_SECRET Bypass Vercel Deployment Protection
|
|
92
|
+
PRE_POST_CONCURRENCY Parallel pages (default 6)
|
|
93
|
+
PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH Use a specific Chromium binary
|
|
130
94
|
`);
|
|
131
95
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
return url;
|
|
140
|
-
}
|
|
141
|
-
if (/^(localhost|127\.0\.0\.1)(:|\/|$)/i.test(url)) {
|
|
142
|
-
return `http://${url}`;
|
|
143
|
-
}
|
|
144
|
-
return `https://${url}`;
|
|
145
|
-
}
|
|
146
|
-
function parseStrictNumber(value, flag) {
|
|
147
|
-
if (!/^-?\d+(?:\.\d+)?$/.test(value.trim())) {
|
|
148
|
-
console.error(`Invalid value for ${flag}: ${value}. Expected a number.`);
|
|
149
|
-
process.exit(1);
|
|
150
|
-
}
|
|
151
|
-
const parsed = Number(value);
|
|
152
|
-
if (!Number.isFinite(parsed)) {
|
|
153
|
-
console.error(`Invalid value for ${flag}: ${value}. Expected a finite number.`);
|
|
154
|
-
process.exit(1);
|
|
155
|
-
}
|
|
156
|
-
return parsed;
|
|
157
|
-
}
|
|
158
|
-
function parseStrictInteger(value, flag) {
|
|
159
|
-
if (!/^-?\d+$/.test(value.trim())) {
|
|
160
|
-
console.error(`Invalid value for ${flag}: ${value}. Expected an integer.`);
|
|
161
|
-
process.exit(1);
|
|
162
|
-
}
|
|
163
|
-
const parsed = Number(value);
|
|
164
|
-
if (!Number.isSafeInteger(parsed)) {
|
|
165
|
-
console.error(`Invalid value for ${flag}: ${value}. Expected a safe integer.`);
|
|
166
|
-
process.exit(1);
|
|
96
|
+
function num(v, flag) {
|
|
97
|
+
if (v === undefined)
|
|
98
|
+
return undefined;
|
|
99
|
+
const n = Number(v);
|
|
100
|
+
if (!Number.isFinite(n)) {
|
|
101
|
+
console.error(`Invalid value for ${flag}: ${v}`);
|
|
102
|
+
process.exit(2);
|
|
167
103
|
}
|
|
168
|
-
return
|
|
104
|
+
return n;
|
|
169
105
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
* Returns parsed options if --video is set, undefined otherwise.
|
|
173
|
-
*/
|
|
174
|
-
function validateVideoFlags() {
|
|
175
|
-
if (!values.video)
|
|
106
|
+
function kv(list) {
|
|
107
|
+
if (!list?.length)
|
|
176
108
|
return undefined;
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
}
|
|
186
|
-
const fps = values.fps ? parseStrictInteger(values.fps, '--fps') : undefined;
|
|
187
|
-
if (fps !== undefined && (fps < 1 || fps > 10)) {
|
|
188
|
-
console.error('FPS must be between 1 and 10');
|
|
189
|
-
process.exit(1);
|
|
190
|
-
}
|
|
191
|
-
const delay = values.delay ? parseStrictInteger(values.delay, '--delay') : undefined;
|
|
192
|
-
if (delay !== undefined && delay < 0) {
|
|
193
|
-
console.error('Delay must be a non-negative integer in milliseconds');
|
|
194
|
-
process.exit(1);
|
|
109
|
+
const out = {};
|
|
110
|
+
for (const item of list) {
|
|
111
|
+
const sep = item.search(/[=:]/);
|
|
112
|
+
if (sep === -1) {
|
|
113
|
+
console.error(`Expected key=value, got: ${item}`);
|
|
114
|
+
process.exit(2);
|
|
115
|
+
}
|
|
116
|
+
out[item.slice(0, sep).trim()] = item.slice(sep + 1).trim();
|
|
195
117
|
}
|
|
196
|
-
return
|
|
118
|
+
return out;
|
|
197
119
|
}
|
|
198
|
-
function
|
|
120
|
+
function viewportsFromFlags() {
|
|
121
|
+
if (values.viewports)
|
|
122
|
+
return values.viewports.split(',').map(s => s.trim()).filter(Boolean);
|
|
123
|
+
if (values.responsive)
|
|
124
|
+
return ['desktop', 'mobile'];
|
|
199
125
|
if (values.mobile)
|
|
200
|
-
return 'mobile';
|
|
126
|
+
return ['mobile'];
|
|
201
127
|
if (values.tablet)
|
|
202
|
-
return 'tablet';
|
|
203
|
-
if (values.size)
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
return
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
const changedFiles = getChangedFiles();
|
|
220
|
-
if (changedFiles.length === 0) {
|
|
221
|
-
console.log(JSON.stringify({ routes: [], message: 'No changed files detected' }, null, 2));
|
|
128
|
+
return ['tablet'];
|
|
129
|
+
if (values.size)
|
|
130
|
+
return [values.size];
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
/** `before`/`after` come from flags for `pr` and `compare`, from positionals otherwise. */
|
|
134
|
+
function resolveTargets() {
|
|
135
|
+
if (subcommand === 'pr' || subcommand === 'compare')
|
|
136
|
+
return { before: values.before, after: values.after };
|
|
137
|
+
return { before: positionals[0], after: positionals[1] };
|
|
138
|
+
}
|
|
139
|
+
const quiet = values.quiet || values.json;
|
|
140
|
+
const log = (msg) => { if (!quiet)
|
|
141
|
+
console.error(msg); };
|
|
142
|
+
function output(result) {
|
|
143
|
+
if (values.json) {
|
|
144
|
+
console.log(JSON.stringify(result, null, 2));
|
|
222
145
|
return;
|
|
223
146
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
changedFiles,
|
|
229
|
-
routes,
|
|
230
|
-
}, null, 2));
|
|
231
|
-
}
|
|
232
|
-
// ============================================================
|
|
233
|
-
// Subcommand: compare
|
|
234
|
-
// ============================================================
|
|
235
|
-
async function runCompare(routeOverrides, videoOpts) {
|
|
236
|
-
const beforeBase = values['before-base'];
|
|
237
|
-
const afterBase = values['after-base'];
|
|
238
|
-
if (!beforeBase || !afterBase) {
|
|
239
|
-
console.error('Both --before-base and --after-base are required for compare.');
|
|
240
|
-
process.exit(1);
|
|
241
|
-
}
|
|
242
|
-
const routeList = routeOverrides
|
|
243
|
-
?? (values.routes ? values.routes.split(',').map(r => r.trim()) : ['/']);
|
|
244
|
-
const responsive = values.responsive ?? false;
|
|
245
|
-
const isVideo = values.video ?? false;
|
|
246
|
-
const ext = isVideo ? 'gif' : 'png';
|
|
247
|
-
const captureType = isVideo ? 'GIF' : 'screenshot';
|
|
248
|
-
const outputDir = values.output || path.join(process.env.HOME || '~', 'Downloads');
|
|
249
|
-
fs.mkdirSync(outputDir, { recursive: true });
|
|
250
|
-
const timestamp = new Date();
|
|
251
|
-
const viewports = responsive
|
|
252
|
-
? ['desktop', 'mobile'].map(p => ({ label: p, size: VIEWPORT_PRESETS[p] }))
|
|
253
|
-
: [{ size: resolveViewport(resolveViewportFlag()) }];
|
|
254
|
-
const tasks = [];
|
|
255
|
-
for (const route of routeList) {
|
|
256
|
-
const beforeUrl = normalizeUrl(beforeBase.replace(/\/$/, '') + route);
|
|
257
|
-
const afterUrl = normalizeUrl(afterBase.replace(/\/$/, '') + route);
|
|
258
|
-
const routeSlug = route === '/' ? 'home' : route.replace(/^\//, '').replace(/\//g, '-');
|
|
259
|
-
for (const { label: presetLabel, size: vp } of viewports) {
|
|
260
|
-
const filePrefix = presetLabel ? `${routeSlug}-${presetLabel}` : routeSlug;
|
|
261
|
-
tasks.push({
|
|
262
|
-
route,
|
|
263
|
-
presetLabel,
|
|
264
|
-
vp,
|
|
265
|
-
beforeUrl,
|
|
266
|
-
afterUrl,
|
|
267
|
-
beforeFilename: `${filePrefix}-before-${formatTimestamp(timestamp)}.${ext}`,
|
|
268
|
-
afterFilename: `${filePrefix}-after-${formatTimestamp(timestamp)}.${ext}`,
|
|
269
|
-
});
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
const totalCaptures = tasks.length * 2;
|
|
273
|
-
console.log(`Capturing ${totalCaptures} ${captureType}s across ${routeList.length} route(s)...`);
|
|
274
|
-
try {
|
|
275
|
-
// Execute all tasks in parallel (pool limits concurrency)
|
|
276
|
-
const results = await Promise.allSettled(tasks.map(async (task) => {
|
|
277
|
-
if (isVideo) {
|
|
278
|
-
const opts = { viewport: task.vp, ...videoOpts, selector: values.selector };
|
|
279
|
-
const [beforeResult, afterResult] = await Promise.all([
|
|
280
|
-
captureVideo(task.beforeUrl, opts),
|
|
281
|
-
captureVideo(task.afterUrl, opts),
|
|
282
|
-
]);
|
|
283
|
-
fs.writeFileSync(path.join(outputDir, task.beforeFilename), beforeResult.gif);
|
|
284
|
-
fs.writeFileSync(path.join(outputDir, task.afterFilename), afterResult.gif);
|
|
285
|
-
console.log(` Done: ${task.route}${task.presetLabel ? ` @ ${task.presetLabel}` : ''} (${beforeResult.frameCount}f + ${afterResult.frameCount}f)`);
|
|
286
|
-
}
|
|
287
|
-
else {
|
|
288
|
-
const [beforeResult, afterResult] = await Promise.all([
|
|
289
|
-
captureScreenshot({
|
|
290
|
-
url: task.beforeUrl, viewport: task.vp, fullPage: values.full, selector: values.selector,
|
|
291
|
-
}),
|
|
292
|
-
captureScreenshot({
|
|
293
|
-
url: task.afterUrl, viewport: task.vp, fullPage: values.full, selector: values.selector,
|
|
294
|
-
}),
|
|
295
|
-
]);
|
|
296
|
-
fs.writeFileSync(path.join(outputDir, task.beforeFilename), beforeResult.image);
|
|
297
|
-
fs.writeFileSync(path.join(outputDir, task.afterFilename), afterResult.image);
|
|
298
|
-
console.log(` Done: ${task.route}${task.presetLabel ? ` @ ${task.presetLabel}` : ''}`);
|
|
299
|
-
}
|
|
300
|
-
}));
|
|
301
|
-
const failures = results.filter((r) => r.status === 'rejected');
|
|
302
|
-
if (failures.length > 0) {
|
|
303
|
-
for (const f of failures)
|
|
304
|
-
console.error(` Failed: ${f.reason}`);
|
|
305
|
-
if (failures.length === results.length)
|
|
306
|
-
throw new Error('All captures failed');
|
|
307
|
-
}
|
|
308
|
-
console.log(`\nAll ${captureType}s saved to: ${outputDir}`);
|
|
309
|
-
}
|
|
310
|
-
finally {
|
|
311
|
-
await closeBrowser();
|
|
312
|
-
}
|
|
147
|
+
console.log(buildSummary(result));
|
|
148
|
+
console.log(`Files: ${result.outputDir}`);
|
|
149
|
+
if (result.markdown && !result.commentUrl)
|
|
150
|
+
console.log('\n' + result.markdown);
|
|
313
151
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
const beforeBase = values['before-base'];
|
|
319
|
-
const afterBase = values['after-base'];
|
|
320
|
-
if (!beforeBase || !afterBase) {
|
|
321
|
-
console.error('Both --before-base and --after-base are required for run.');
|
|
322
|
-
process.exit(1);
|
|
323
|
-
}
|
|
324
|
-
const framework = values.framework;
|
|
325
|
-
const maxRoutes = values['max-routes'] ? parseInt(values['max-routes']) : undefined;
|
|
326
|
-
const changedFiles = getChangedFiles();
|
|
327
|
-
let routeList;
|
|
328
|
-
if (values.routes) {
|
|
329
|
-
routeList = values.routes.split(',').map(r => r.trim());
|
|
330
|
-
}
|
|
331
|
-
else if (changedFiles.length > 0) {
|
|
332
|
-
const routes = detectRoutes(changedFiles, { framework, maxRoutes });
|
|
333
|
-
if (routes.length === 0) {
|
|
334
|
-
console.log('No visual routes detected. Defaulting to /');
|
|
335
|
-
routeList = ['/'];
|
|
336
|
-
}
|
|
337
|
-
else {
|
|
338
|
-
routeList = routes.map(r => r.path);
|
|
339
|
-
console.log(`Detected routes: ${routeList.join(', ')}`);
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
else {
|
|
343
|
-
console.log('No changed files detected. Defaulting to /');
|
|
344
|
-
routeList = ['/'];
|
|
152
|
+
async function main() {
|
|
153
|
+
if (values.version) {
|
|
154
|
+
console.log(VERSION);
|
|
155
|
+
return;
|
|
345
156
|
}
|
|
346
|
-
|
|
347
|
-
}
|
|
348
|
-
// ============================================================
|
|
349
|
-
// Default mode (original before-and-after behavior)
|
|
350
|
-
// ============================================================
|
|
351
|
-
async function runDefault(videoOpts) {
|
|
352
|
-
if (values.help) {
|
|
157
|
+
if (values.help || (!subcommand && positionals.length < 2)) {
|
|
353
158
|
printHelp();
|
|
159
|
+
if (!values.help && !subcommand && positionals.length > 0)
|
|
160
|
+
process.exit(2);
|
|
354
161
|
return;
|
|
355
162
|
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
163
|
+
const common = {
|
|
164
|
+
routes: values.routes ? values.routes.split(',').map(r => r.trim()).filter(Boolean) : undefined,
|
|
165
|
+
viewports: viewportsFromFlags(),
|
|
166
|
+
fullPage: values['viewport-only'] ? false : values.full ? true : undefined,
|
|
167
|
+
scale: num(values.scale, '--scale'),
|
|
168
|
+
threshold: num(values.threshold, '--threshold'),
|
|
169
|
+
maxHeight: num(values['max-height'], '--max-height'),
|
|
170
|
+
maxRoutes: num(values['max-routes'], '--max-routes'),
|
|
171
|
+
wait: num(values.wait, '--wait'),
|
|
172
|
+
headers: kv(values.header),
|
|
173
|
+
output: values.output,
|
|
174
|
+
log,
|
|
175
|
+
};
|
|
176
|
+
const { before, after } = resolveTargets();
|
|
177
|
+
switch (subcommand) {
|
|
178
|
+
case 'pr': {
|
|
179
|
+
const cookies = values.cookie ? Object.entries(kv(values.cookie)).map(([name, value]) => ({ name, value })) : undefined;
|
|
180
|
+
const result = await runPr({
|
|
181
|
+
...common,
|
|
182
|
+
before,
|
|
183
|
+
after,
|
|
184
|
+
framework: values.framework,
|
|
185
|
+
cookies,
|
|
186
|
+
dryRun: values['dry-run'],
|
|
187
|
+
comment: !values['no-comment'],
|
|
188
|
+
pr: num(values.pr, '--pr'),
|
|
189
|
+
version: VERSION,
|
|
369
190
|
});
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
else {
|
|
374
|
-
console.log(`Before: ${first}`);
|
|
375
|
-
console.log(`After: ${second}`);
|
|
376
|
-
}
|
|
191
|
+
output(result);
|
|
192
|
+
if (result.outcomes.length && result.outcomes.every(o => o.status === 'error'))
|
|
193
|
+
process.exit(1);
|
|
377
194
|
return;
|
|
378
195
|
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
let beforeSelector = values.selector;
|
|
384
|
-
let afterSelector = values.selector;
|
|
385
|
-
if (rest.length >= 1) {
|
|
386
|
-
beforeSelector = rest[0];
|
|
387
|
-
afterSelector = rest[0];
|
|
388
|
-
}
|
|
389
|
-
if (rest.length >= 2) {
|
|
390
|
-
afterSelector = rest[1];
|
|
196
|
+
case 'detect': {
|
|
197
|
+
const detection = runDetect({ maxRoutes: common.maxRoutes, framework: values.framework });
|
|
198
|
+
console.log(JSON.stringify(detection, null, values.json ? 2 : 0));
|
|
199
|
+
return;
|
|
391
200
|
}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
console.log(`Capturing ${captureType} before: ${beforeUrl}${beforeSelector ? ` (${beforeSelector})` : ''}`);
|
|
397
|
-
console.log(`Capturing ${captureType} after: ${afterUrl}${afterSelector ? ` (${afterSelector})` : ''}`);
|
|
398
|
-
if (isVideo) {
|
|
399
|
-
// Video/GIF mode
|
|
400
|
-
const resolvedViewport = resolveViewport(viewport);
|
|
401
|
-
const opts = {
|
|
402
|
-
viewport: resolvedViewport,
|
|
403
|
-
...videoOpts,
|
|
404
|
-
selector: beforeSelector,
|
|
405
|
-
};
|
|
406
|
-
const beforeResult = await captureVideo(beforeUrl, opts);
|
|
407
|
-
const afterResult = await captureVideo(afterUrl, { ...opts, selector: afterSelector });
|
|
408
|
-
const timestamp = new Date();
|
|
409
|
-
const beforeFilename = generateFilename({ url: beforeUrl, suffix: 'before', timestamp, format: 'gif' });
|
|
410
|
-
const afterFilename = generateFilename({ url: afterUrl, suffix: 'after', timestamp, format: 'gif' });
|
|
411
|
-
const beforePath = path.join(outputDir, beforeFilename);
|
|
412
|
-
const afterPath = path.join(outputDir, afterFilename);
|
|
413
|
-
fs.writeFileSync(beforePath, beforeResult.gif);
|
|
414
|
-
fs.writeFileSync(afterPath, afterResult.gif);
|
|
415
|
-
console.log(`\nSaved: ${beforePath} (${beforeResult.frameCount} frames)`);
|
|
416
|
-
console.log(`Saved: ${afterPath} (${afterResult.frameCount} frames)`);
|
|
417
|
-
if (values.markdown) {
|
|
418
|
-
await uploadAndOutputMarkdown(beforeResult.gif, afterResult.gif, beforeFilename, afterFilename);
|
|
201
|
+
case 'login': {
|
|
202
|
+
if (!positionals[0]) {
|
|
203
|
+
console.error('Usage: pre-post login <url>');
|
|
204
|
+
process.exit(2);
|
|
419
205
|
}
|
|
206
|
+
await runLogin(positionals[0]);
|
|
207
|
+
return;
|
|
420
208
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
});
|
|
435
|
-
const timestamp = new Date();
|
|
436
|
-
const beforeFilename = generateFilename({ url: beforeUrl, suffix: 'before', timestamp });
|
|
437
|
-
const afterFilename = generateFilename({ url: afterUrl, suffix: 'after', timestamp });
|
|
438
|
-
const beforePath = path.join(outputDir, beforeFilename);
|
|
439
|
-
const afterPath = path.join(outputDir, afterFilename);
|
|
440
|
-
fs.writeFileSync(beforePath, result.before.image);
|
|
441
|
-
fs.writeFileSync(afterPath, result.after.image);
|
|
442
|
-
console.log(`\nSaved: ${beforePath}`);
|
|
443
|
-
console.log(`Saved: ${afterPath}`);
|
|
444
|
-
if (values.markdown) {
|
|
445
|
-
await uploadAndOutputMarkdown(result.before.image, result.after.image, beforeFilename, afterFilename);
|
|
446
|
-
}
|
|
209
|
+
case 'prune': {
|
|
210
|
+
const result = await runPrune({ days: num(values.days, '--days'), dryRun: values['dry-run'] });
|
|
211
|
+
console.log(values['dry-run'] ? 'Would remove:' : 'Removed:', result.removed.length ? result.removed.join(', ') : 'nothing');
|
|
212
|
+
console.log('Kept:', result.kept.length ? result.kept.join(', ') : 'nothing');
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
case 'doctor': {
|
|
216
|
+
const checks = await runDoctor();
|
|
217
|
+
for (const c of checks)
|
|
218
|
+
console.log(`${c.name.padEnd(10)} ${c.ok ? 'ok' : 'FAIL'} ${c.detail}`);
|
|
219
|
+
if (checks.some(c => !c.ok && c.name === 'browser'))
|
|
220
|
+
process.exit(1);
|
|
221
|
+
return;
|
|
447
222
|
}
|
|
448
|
-
}
|
|
449
|
-
finally {
|
|
450
|
-
await closeBrowser();
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
function formatTimestamp(date) {
|
|
454
|
-
return date.toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
455
|
-
}
|
|
456
|
-
function buildMarkdownTable(beforeUrl, afterUrl) {
|
|
457
|
-
const header = '| Pre | Post |';
|
|
458
|
-
const divider = '|:---:|:----:|';
|
|
459
|
-
const row = `|  |  |`;
|
|
460
|
-
return `${header}\n${divider}\n${row}`;
|
|
461
|
-
}
|
|
462
|
-
async function uploadAndOutputMarkdown(beforeImage, afterImage, beforeFilename, afterFilename) {
|
|
463
|
-
const uploadUrl = values['upload-url'] || process.env.UPLOAD_URL;
|
|
464
|
-
console.log(`Uploading images${uploadUrl ? ` to ${uploadUrl}` : ''}...`);
|
|
465
|
-
const { beforeUrl, afterUrl } = await uploadBeforeAfter({ image: beforeImage, filename: beforeFilename }, { image: afterImage, filename: afterFilename }, uploadUrl);
|
|
466
|
-
const markdown = buildMarkdownTable(beforeUrl, afterUrl);
|
|
467
|
-
console.log(`\n${markdown}`);
|
|
468
|
-
if (copyToClipboard(markdown)) {
|
|
469
|
-
console.log('\nMarkdown copied to clipboard');
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
// ============================================================
|
|
473
|
-
// Main dispatch
|
|
474
|
-
// ============================================================
|
|
475
|
-
async function main() {
|
|
476
|
-
if (values.help && !isSubcommand) {
|
|
477
|
-
printHelp();
|
|
478
|
-
return;
|
|
479
|
-
}
|
|
480
|
-
const videoOpts = validateVideoFlags();
|
|
481
|
-
switch (subcommand) {
|
|
482
|
-
case 'detect':
|
|
483
|
-
return runDetect();
|
|
484
223
|
case 'compare':
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
224
|
+
default: {
|
|
225
|
+
if (!before || !after) {
|
|
226
|
+
console.error('Two URLs (or two PNG files) are required.');
|
|
227
|
+
process.exit(2);
|
|
228
|
+
}
|
|
229
|
+
output(await runCompare({ ...common, before, after }));
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
490
232
|
}
|
|
491
233
|
}
|
|
492
234
|
main()
|
|
493
235
|
.then(() => process.exit(0))
|
|
494
|
-
.catch(
|
|
495
|
-
|
|
236
|
+
.catch(err => {
|
|
237
|
+
if (err instanceof NeedsHumanError) {
|
|
238
|
+
console.error(`\n${err.message}`);
|
|
239
|
+
process.exit(3);
|
|
240
|
+
}
|
|
241
|
+
console.error(err?.stack && process.env.PRE_POST_DEBUG ? err.stack : `Error: ${err?.message || err}`);
|
|
496
242
|
process.exit(1);
|
|
497
243
|
});
|
|
498
244
|
//# sourceMappingURL=cli.js.map
|