@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.
- package/README.md +174 -233
- package/dist/baseline.d.ts +119 -0
- package/dist/baseline.d.ts.map +1 -0
- package/dist/baseline.js +316 -0
- package/dist/baseline.js.map +1 -0
- package/dist/bin/cli.js +243 -458
- package/dist/bin/cli.js.map +1 -1
- package/dist/browser.d.ts +68 -25
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +500 -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 +82 -0
- package/dist/commands/compare.js.map +1 -0
- package/dist/commands/detect.d.ts +25 -0
- package/dist/commands/detect.d.ts.map +1 -0
- package/dist/commands/detect.js +22 -0
- package/dist/commands/detect.js.map +1 -0
- package/dist/commands/doctor.d.ts +42 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +119 -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 +32 -0
- package/dist/commands/pr.d.ts.map +1 -0
- package/dist/commands/pr.js +259 -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/comparison.d.ts +104 -0
- package/dist/comparison.d.ts.map +1 -0
- package/dist/comparison.js +236 -0
- package/dist/comparison.js.map +1 -0
- package/dist/config.d.ts +46 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +82 -0
- package/dist/config.js.map +1 -0
- package/dist/deployments.d.ts +71 -0
- package/dist/deployments.d.ts.map +1 -0
- package/dist/deployments.js +204 -0
- package/dist/deployments.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 +92 -0
- package/dist/diff-pool.js.map +1 -0
- package/dist/diff-worker.d.ts +32 -0
- package/dist/diff-worker.d.ts.map +1 -0
- package/dist/diff-worker.js +39 -0
- package/dist/diff-worker.js.map +1 -0
- package/dist/diff.d.ts +47 -0
- package/dist/diff.d.ts.map +1 -0
- package/dist/diff.js +281 -0
- package/dist/diff.js.map +1 -0
- package/dist/doctor.d.ts +79 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +138 -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 +61 -0
- package/dist/git.d.ts.map +1 -0
- package/dist/git.js +182 -0
- package/dist/git.js.map +1 -0
- package/dist/github.d.ts +163 -0
- package/dist/github.d.ts.map +1 -0
- package/dist/github.js +411 -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/landing.d.ts +48 -0
- package/dist/landing.d.ts.map +1 -0
- package/dist/landing.js +93 -0
- package/dist/landing.js.map +1 -0
- package/dist/pkg.d.ts +17 -0
- package/dist/pkg.d.ts.map +1 -0
- package/dist/pkg.js +49 -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 +130 -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 +65 -11
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +346 -141
- package/dist/routes.js.map +1 -1
- package/dist/run.d.ts +116 -0
- package/dist/run.d.ts.map +1 -0
- package/dist/run.js +316 -0
- package/dist/run.js.map +1 -0
- package/dist/sameness.d.ts +91 -0
- package/dist/sameness.d.ts.map +1 -0
- package/dist/sameness.js +157 -0
- package/dist/sameness.js.map +1 -0
- package/dist/sessions.d.ts +50 -0
- package/dist/sessions.d.ts.map +1 -0
- package/dist/sessions.js +80 -0
- package/dist/sessions.js.map +1 -0
- package/dist/shift.d.ts +67 -0
- package/dist/shift.d.ts.map +1 -0
- package/dist/shift.js +184 -0
- package/dist/shift.js.map +1 -0
- package/dist/types.d.ts +179 -68
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -1
- package/dist/url.d.ts +12 -0
- package/dist/url.d.ts.map +1 -0
- package/dist/url.js +30 -0
- package/dist/url.js.map +1 -0
- 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 +56 -221
- 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,283 @@
|
|
|
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
|
-
|
|
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 { doctorExitCode, 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;
|
|
24
|
+
/**
|
|
25
|
+
* parseArgs throws a TypeError for an unknown flag, and an uncaught one prints
|
|
26
|
+
* a Node-internal stack trace: eight lines of `node:internal/util/parse_args`
|
|
27
|
+
* for a typo. The CLI is mostly read by agents, so the first line has to be the
|
|
28
|
+
* thing to fix, not a frame from Node's parser.
|
|
29
|
+
*
|
|
30
|
+
* Only the parse is wrapped, and the message is Node's own — this reformats a
|
|
31
|
+
* failure, it does not decide what counts as one.
|
|
32
|
+
*/
|
|
33
|
+
function parse() {
|
|
34
|
+
try {
|
|
35
|
+
return parseArgs({
|
|
36
|
+
args: argv,
|
|
37
|
+
allowPositionals: true,
|
|
38
|
+
options: OPTIONS,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
const message = err?.message?.split('\n')[0] ?? 'could not parse the arguments';
|
|
43
|
+
console.error(`pre-post: ${message}`);
|
|
44
|
+
console.error('Run: pre-post --help');
|
|
45
|
+
process.exit(2);
|
|
46
|
+
}
|
|
20
47
|
}
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
},
|
|
49
|
-
}
|
|
48
|
+
const OPTIONS = {
|
|
49
|
+
help: { type: 'boolean', short: 'h' },
|
|
50
|
+
version: { type: 'boolean', short: 'v' },
|
|
51
|
+
before: { type: 'string' },
|
|
52
|
+
after: { type: 'string' },
|
|
53
|
+
routes: { type: 'string' },
|
|
54
|
+
'max-routes': { type: 'string' },
|
|
55
|
+
framework: { type: 'string' },
|
|
56
|
+
base: { type: 'string' },
|
|
57
|
+
'no-local-baseline': { type: 'boolean' },
|
|
58
|
+
viewports: { type: 'string' },
|
|
59
|
+
mobile: { type: 'boolean', short: 'm' },
|
|
60
|
+
tablet: { type: 'boolean', short: 't' },
|
|
61
|
+
responsive: { type: 'boolean', short: 'r' },
|
|
62
|
+
size: { type: 'string' },
|
|
63
|
+
full: { type: 'boolean', short: 'f' },
|
|
64
|
+
'viewport-only': { type: 'boolean' },
|
|
65
|
+
scale: { type: 'string' },
|
|
66
|
+
threshold: { type: 'string' },
|
|
67
|
+
'max-height': { type: 'string' },
|
|
68
|
+
wait: { type: 'string' },
|
|
69
|
+
header: { type: 'string', multiple: true },
|
|
70
|
+
cookie: { type: 'string', multiple: true },
|
|
71
|
+
output: { type: 'string', short: 'o' },
|
|
72
|
+
'dry-run': { type: 'boolean' },
|
|
73
|
+
'no-comment': { type: 'boolean' },
|
|
74
|
+
pr: { type: 'string' },
|
|
75
|
+
json: { type: 'boolean' },
|
|
76
|
+
days: { type: 'string' },
|
|
77
|
+
quiet: { type: 'boolean', short: 'q' },
|
|
78
|
+
};
|
|
79
|
+
const { values, positionals } = parse();
|
|
50
80
|
function printHelp() {
|
|
51
|
-
console.log(`
|
|
52
|
-
pre-post — Visual diff tool for PRs
|
|
53
|
-
|
|
54
|
-
USAGE:
|
|
55
|
-
pre-post <before> <after> [selector] [selector2]
|
|
56
|
-
pre-post detect [options]
|
|
57
|
-
pre-post compare --before-base <url> --after-base <url> [options]
|
|
58
|
-
pre-post run --before-base <url> --after-base <url> [options]
|
|
59
|
-
|
|
60
|
-
SUBCOMMANDS:
|
|
61
|
-
detect Detect affected routes from git diff (JSON output)
|
|
62
|
-
compare Compare before/after URLs with screenshots
|
|
63
|
-
run Auto-detect routes + compare (detect + compare)
|
|
64
|
-
|
|
65
|
-
Arguments can be URLs or image files (auto-detected).
|
|
66
|
-
Selectors are optional - use one for both, or two for different selectors.
|
|
67
|
-
|
|
68
|
-
VIEWPORT OPTIONS:
|
|
69
|
-
(default) Desktop viewport (1280x800)
|
|
70
|
-
-m, --mobile Mobile viewport (375x812)
|
|
71
|
-
-t, --tablet Tablet viewport (768x1024)
|
|
72
|
-
--size <WxH> Custom viewport (e.g., 1920x1080)
|
|
73
|
-
|
|
74
|
-
CAPTURE OPTIONS:
|
|
75
|
-
-f, --full Capture full scrollable page
|
|
76
|
-
-s, --selector <css> Scroll element into view before capture
|
|
77
|
-
-r, --responsive Capture at desktop + mobile viewports
|
|
78
|
-
|
|
79
|
-
ROUTE DETECTION OPTIONS:
|
|
80
|
-
--routes <paths> Explicit route list (comma-separated)
|
|
81
|
-
--max-routes <n> Max routes to detect (default: 5)
|
|
82
|
-
--framework <name> Force framework (nextjs-app, nextjs-pages, generic)
|
|
83
|
-
|
|
84
|
-
COMPARE OPTIONS:
|
|
85
|
-
--before-base <url> Base URL for "before" state (production)
|
|
86
|
-
--after-base <url> Base URL for "after" state (localhost)
|
|
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
|
|
81
|
+
console.log(`pre-post ${VERSION} — before/after screenshots for pull requests
|
|
117
82
|
|
|
118
|
-
|
|
119
|
-
pre-post
|
|
83
|
+
USAGE
|
|
84
|
+
pre-post pr [options] Detect routes, capture, diff, publish, comment on the PR
|
|
85
|
+
pre-post <before> <after> [options] Compare two URLs (or two PNG files)
|
|
86
|
+
pre-post detect Print the routes this branch affects (JSON)
|
|
87
|
+
pre-post ... --base <ref> Compare against <ref> instead of the detected fork point
|
|
88
|
+
pre-post login <url> Sign in once; the session is reused for captures
|
|
89
|
+
pre-post prune [--days 90] Delete screenshots for PRs closed longer ago
|
|
90
|
+
pre-post doctor Check browser, GitHub auth, dev server
|
|
120
91
|
|
|
121
|
-
|
|
122
|
-
pre-post
|
|
123
|
-
|
|
92
|
+
OPTIONS
|
|
93
|
+
--before <url> Production base URL (saved to .pre-post.json)
|
|
94
|
+
--after <url> Local base URL (auto-detected)
|
|
95
|
+
--routes /a,/b Explicit routes instead of detection
|
|
96
|
+
--max-routes <n> Cap on detected routes (default 6)
|
|
97
|
+
--viewports desktop,mobile,1440x900 Viewports (default desktop)
|
|
98
|
+
-r, --responsive Desktop and mobile
|
|
99
|
+
-m, --mobile -t, --tablet --size WxH Single-viewport shorthands
|
|
100
|
+
-f, --full | --viewport-only Full page (default for pr) or first screen only
|
|
101
|
+
--scale <n> Device scale factor (default 2)
|
|
102
|
+
--threshold <0..1> Change ratio treated as "no change" (default 0.001)
|
|
103
|
+
--max-height <px> Full-page height cap in CSS px (default 2400)
|
|
104
|
+
--wait <ms> Extra wait after the page settles
|
|
105
|
+
--header k=v Extra request header (repeatable)
|
|
106
|
+
--cookie name=value Cookie for the production host (repeatable)
|
|
107
|
+
-o, --output <dir> Where to write images (default: temp dir)
|
|
108
|
+
--no-local-baseline Do not rebuild the baseline from the base commit
|
|
109
|
+
--dry-run Capture and diff only; no upload, no comment
|
|
110
|
+
--no-comment Publish images but do not touch the PR
|
|
111
|
+
--pr <number> Target a specific PR
|
|
112
|
+
--json Machine-readable output
|
|
113
|
+
-q, --quiet Only the final summary
|
|
124
114
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
115
|
+
ENVIRONMENT
|
|
116
|
+
GH_TOKEN / GITHUB_TOKEN GitHub token (default: gh auth token)
|
|
117
|
+
VERCEL_AUTOMATION_BYPASS_SECRET Bypass Vercel Deployment Protection
|
|
118
|
+
PRE_POST_CONCURRENCY Parallel pages (default 6)
|
|
119
|
+
PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH Use a specific Chromium binary
|
|
130
120
|
`);
|
|
131
121
|
}
|
|
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);
|
|
122
|
+
function num(v, flag) {
|
|
123
|
+
if (v === undefined)
|
|
124
|
+
return undefined;
|
|
125
|
+
const n = Number(v);
|
|
126
|
+
if (!Number.isFinite(n)) {
|
|
127
|
+
console.error(`Invalid value for ${flag}: ${v}`);
|
|
128
|
+
process.exit(2);
|
|
167
129
|
}
|
|
168
|
-
return
|
|
130
|
+
return n;
|
|
169
131
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
* Returns parsed options if --video is set, undefined otherwise.
|
|
173
|
-
*/
|
|
174
|
-
function validateVideoFlags() {
|
|
175
|
-
if (!values.video)
|
|
132
|
+
function kv(list) {
|
|
133
|
+
if (!list?.length)
|
|
176
134
|
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);
|
|
135
|
+
const out = {};
|
|
136
|
+
for (const item of list) {
|
|
137
|
+
const sep = item.search(/[=:]/);
|
|
138
|
+
if (sep === -1) {
|
|
139
|
+
console.error(`Expected key=value, got: ${item}`);
|
|
140
|
+
process.exit(2);
|
|
141
|
+
}
|
|
142
|
+
out[item.slice(0, sep).trim()] = item.slice(sep + 1).trim();
|
|
195
143
|
}
|
|
196
|
-
return
|
|
144
|
+
return out;
|
|
197
145
|
}
|
|
198
|
-
function
|
|
146
|
+
function viewportsFromFlags() {
|
|
147
|
+
if (values.viewports)
|
|
148
|
+
return values.viewports.split(',').map(s => s.trim()).filter(Boolean);
|
|
149
|
+
if (values.responsive)
|
|
150
|
+
return ['desktop', 'mobile'];
|
|
199
151
|
if (values.mobile)
|
|
200
|
-
return 'mobile';
|
|
152
|
+
return ['mobile'];
|
|
201
153
|
if (values.tablet)
|
|
202
|
-
return 'tablet';
|
|
203
|
-
if (values.size)
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
return { width: parseInt(match[1]), height: parseInt(match[2]) };
|
|
207
|
-
}
|
|
208
|
-
console.error(`Invalid size: ${values.size}. Use WxH format (e.g., 1920x1080).`);
|
|
209
|
-
process.exit(1);
|
|
210
|
-
}
|
|
211
|
-
return 'desktop';
|
|
154
|
+
return ['tablet'];
|
|
155
|
+
if (values.size)
|
|
156
|
+
return [values.size];
|
|
157
|
+
return undefined;
|
|
212
158
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const maxRoutes = values['max-routes'] ? parseInt(values['max-routes']) : undefined;
|
|
219
|
-
const changedFiles = getChangedFiles();
|
|
220
|
-
if (changedFiles.length === 0) {
|
|
221
|
-
console.log(JSON.stringify({ routes: [], message: 'No changed files detected' }, null, 2));
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
|
-
const detectedFramework = framework || detectFramework();
|
|
225
|
-
const routes = detectRoutes(changedFiles, { framework, maxRoutes });
|
|
226
|
-
console.log(JSON.stringify({
|
|
227
|
-
framework: detectedFramework,
|
|
228
|
-
changedFiles,
|
|
229
|
-
routes,
|
|
230
|
-
}, null, 2));
|
|
159
|
+
/** `before`/`after` come from flags for `pr` and `compare`, from positionals otherwise. */
|
|
160
|
+
function resolveTargets() {
|
|
161
|
+
if (subcommand === 'pr' || subcommand === 'compare')
|
|
162
|
+
return { before: values.before, after: values.after };
|
|
163
|
+
return { before: positionals[0], after: positionals[1] };
|
|
231
164
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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();
|
|
165
|
+
const quiet = values.quiet || values.json;
|
|
166
|
+
const log = (msg) => { if (!quiet)
|
|
167
|
+
console.error(msg); };
|
|
168
|
+
function output(result) {
|
|
169
|
+
if (values.json) {
|
|
170
|
+
console.log(JSON.stringify(result, null, 2));
|
|
171
|
+
return;
|
|
312
172
|
}
|
|
173
|
+
console.log(buildSummary(result));
|
|
174
|
+
console.log(`Files: ${result.outputDir}`);
|
|
175
|
+
if (result.markdown && !result.commentUrl)
|
|
176
|
+
console.log('\n' + result.markdown);
|
|
313
177
|
}
|
|
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 = ['/'];
|
|
178
|
+
async function main() {
|
|
179
|
+
if (values.version) {
|
|
180
|
+
console.log(VERSION);
|
|
181
|
+
return;
|
|
345
182
|
}
|
|
346
|
-
|
|
347
|
-
}
|
|
348
|
-
// ============================================================
|
|
349
|
-
// Default mode (original before-and-after behavior)
|
|
350
|
-
// ============================================================
|
|
351
|
-
async function runDefault(videoOpts) {
|
|
352
|
-
if (values.help) {
|
|
183
|
+
if (values.help || (!subcommand && positionals.length < 2)) {
|
|
353
184
|
printHelp();
|
|
185
|
+
if (!values.help && !subcommand && positionals.length > 0)
|
|
186
|
+
process.exit(2);
|
|
354
187
|
return;
|
|
355
188
|
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
console.
|
|
375
|
-
|
|
189
|
+
const common = {
|
|
190
|
+
routes: values.routes ? values.routes.split(',').map(r => r.trim()).filter(Boolean) : undefined,
|
|
191
|
+
viewports: viewportsFromFlags(),
|
|
192
|
+
fullPage: values['viewport-only'] ? false : values.full ? true : undefined,
|
|
193
|
+
scale: num(values.scale, '--scale'),
|
|
194
|
+
threshold: num(values.threshold, '--threshold'),
|
|
195
|
+
maxHeight: num(values['max-height'], '--max-height'),
|
|
196
|
+
maxRoutes: num(values['max-routes'], '--max-routes'),
|
|
197
|
+
wait: num(values.wait, '--wait'),
|
|
198
|
+
headers: kv(values.header),
|
|
199
|
+
output: values.output,
|
|
200
|
+
log,
|
|
201
|
+
};
|
|
202
|
+
const { before, after } = resolveTargets();
|
|
203
|
+
switch (subcommand) {
|
|
204
|
+
case 'pr': {
|
|
205
|
+
// before/after come from flags here; positionals would be silently dropped.
|
|
206
|
+
if (positionals.length) {
|
|
207
|
+
console.error(`pre-post pr takes no positional arguments (got: ${positionals.join(' ')}). Use --before and --after.`);
|
|
208
|
+
process.exit(2);
|
|
376
209
|
}
|
|
210
|
+
const cookies = values.cookie ? Object.entries(kv(values.cookie)).map(([name, value]) => ({ name, value })) : undefined;
|
|
211
|
+
const result = await runPr({
|
|
212
|
+
...common,
|
|
213
|
+
before,
|
|
214
|
+
after,
|
|
215
|
+
framework: values.framework,
|
|
216
|
+
base: values.base,
|
|
217
|
+
cookies,
|
|
218
|
+
dryRun: values['dry-run'],
|
|
219
|
+
localBaseline: !values['no-local-baseline'],
|
|
220
|
+
comment: !values['no-comment'],
|
|
221
|
+
pr: num(values.pr, '--pr'),
|
|
222
|
+
version: VERSION,
|
|
223
|
+
});
|
|
224
|
+
output(result);
|
|
225
|
+
if (result.outcomes.length && result.outcomes.every(o => o.status === 'error'))
|
|
226
|
+
process.exit(1);
|
|
377
227
|
return;
|
|
378
228
|
}
|
|
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];
|
|
229
|
+
case 'detect': {
|
|
230
|
+
const detection = runDetect({ maxRoutes: common.maxRoutes, framework: values.framework, base: values.base });
|
|
231
|
+
console.log(JSON.stringify(detection, null, values.json ? 2 : 0));
|
|
232
|
+
return;
|
|
391
233
|
}
|
|
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);
|
|
234
|
+
case 'login': {
|
|
235
|
+
if (!positionals[0]) {
|
|
236
|
+
console.error('Usage: pre-post login <url>');
|
|
237
|
+
process.exit(2);
|
|
419
238
|
}
|
|
239
|
+
await runLogin(positionals[0]);
|
|
240
|
+
return;
|
|
420
241
|
}
|
|
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);
|
|
242
|
+
case 'prune': {
|
|
243
|
+
const result = await runPrune({ days: num(values.days, '--days'), dryRun: values['dry-run'] });
|
|
244
|
+
console.log(values['dry-run'] ? 'Would remove:' : 'Removed:', result.removed.length ? result.removed.join(', ') : 'nothing');
|
|
245
|
+
console.log('Kept:', result.kept.length ? result.kept.join(', ') : 'nothing');
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
case 'doctor': {
|
|
249
|
+
const checks = await runDoctor();
|
|
250
|
+
for (const c of checks) {
|
|
251
|
+
// Advisory failures are worth reading and are not failures of the
|
|
252
|
+
// command, so they must not look like the ones that are.
|
|
253
|
+
const status = c.ok ? 'ok' : c.required ? 'FAIL' : 'note';
|
|
254
|
+
console.log(`${c.name.padEnd(10)} ${status.padEnd(4)} ${c.detail}`);
|
|
446
255
|
}
|
|
256
|
+
const code = doctorExitCode(checks);
|
|
257
|
+
console.log(code === 0
|
|
258
|
+
? 'ready — pre-post pr can run'
|
|
259
|
+
: `not ready — fix: ${checks.filter(c => c.required && !c.ok).map(c => c.name).join(', ')}`);
|
|
260
|
+
process.exit(code);
|
|
447
261
|
}
|
|
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
262
|
case 'compare':
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
263
|
+
default: {
|
|
264
|
+
if (!before || !after) {
|
|
265
|
+
console.error('Two URLs (or two PNG files) are required.');
|
|
266
|
+
process.exit(2);
|
|
267
|
+
}
|
|
268
|
+
output(await runCompare({ ...common, before, after }));
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
490
271
|
}
|
|
491
272
|
}
|
|
492
273
|
main()
|
|
493
274
|
.then(() => process.exit(0))
|
|
494
|
-
.catch(
|
|
495
|
-
|
|
275
|
+
.catch(err => {
|
|
276
|
+
if (err instanceof NeedsHumanError) {
|
|
277
|
+
console.error(`\n${err.message}`);
|
|
278
|
+
process.exit(3);
|
|
279
|
+
}
|
|
280
|
+
console.error(err?.stack && process.env.PRE_POST_DEBUG ? err.stack : `Error: ${err?.message || err}`);
|
|
496
281
|
process.exit(1);
|
|
497
282
|
});
|
|
498
283
|
//# sourceMappingURL=cli.js.map
|