laxy-verify 1.1.33 → 1.2.1

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 CHANGED
@@ -1,304 +1,322 @@
1
- # laxy-verify
2
-
3
- `laxy-verify` is a deployment blocker gate for frontend apps.
4
-
5
- Every verification run includes the same build, Lighthouse, E2E, multi-viewport, security, visual diff, and stability coverage regardless of plan.
6
-
7
- ## Quick start
8
-
9
- Run it on a frontend app:
10
-
11
- ```bash
12
- cd your-project
13
- npx laxy-verify .
14
- ```
15
-
16
- Generate config plus GitHub Actions workflow:
17
-
18
- ```bash
19
- npx laxy-verify --init
20
- ```
21
-
22
- That creates:
23
-
24
- - `.laxy.yml`
25
- - `.github/workflows/laxy-verify.yml`
26
-
27
- Optional: log in to connect the CLI to your Laxy account:
28
-
29
- ```bash
30
- npx laxy-verify login
31
- npx laxy-verify whoami
32
- ```
33
-
34
- For CI, set `LAXY_TOKEN` instead of interactive login:
35
-
36
- ```yaml
37
- env:
38
- LAXY_TOKEN: ${{ secrets.LAXY_TOKEN }}
39
- ```
40
-
41
- ## Why laxy-verify?
42
-
43
- Most teams already have some mix of:
44
-
45
- - `npm run build`
46
- - Lighthouse
47
- - Playwright or smoke checks
48
- - CI status rules
49
-
50
- The gap is not "can these tools exist together?" The gap is "who turns that pile of output into a safe merge or release decision?"
51
-
52
- `laxy-verify` gives you:
53
-
54
- - one command instead of a custom build plus audit plus smoke-check script stack
55
- - one result file instead of scattered logs
56
- - one blocker-first decision instead of "build passed, but do we actually trust this release?"
57
- - optional account-linked automation on top of the same verification run
58
-
59
- This is most useful if you ship frontend apps and want a practical gate before:
60
-
61
- - merge
62
- - client review
63
- - QA handoff
64
- - production release
65
-
66
- ## The failures it is meant to catch
67
-
68
- Use `laxy-verify` when you want to catch things like:
69
-
70
- - the production build passes locally but fails in CI
71
- - the app opens, but a key button or form flow is broken
72
- - desktop looks fine, but a mobile CTA is pushed out of view
73
- - Lighthouse looks acceptable, but the user-visible path is still not safe to ship
74
- - a PR needs a clear hold reason instead of a vague "something failed"
75
-
76
- ## What it actually checks
77
-
78
- A standard run includes:
79
-
80
- - production build success
81
- - Lighthouse thresholds (3 runs for stable evidence)
82
- - E2E scenarios for user-visible flows
83
- - multi-viewport checks (desktop, tablet, mobile)
84
- - blocker-aware reporting and release decisions
85
-
86
- Every run includes:
87
-
88
- - security audit
89
- - visual diff evidence
90
- - stability pass (second E2E run)
91
-
92
- ## What you get from one run
93
-
94
- - a release decision such as `quick-pass`, `client-ready`, `release-ready`, `hold`, or `investigate`
95
- - a verification grade: `Gold`, `Silver`, `Bronze`, or `Unverified`
96
- - `.laxy-result.json` for CI and automation
97
- - `laxy-verify-report.md` for human review and AI handoff
98
-
99
- Grades still exist, but they are not the main point. The main point is whether the run found blockers you should stop on.
100
-
101
- ## Quick start
102
-
103
- Run it on a frontend app:
104
-
105
- ```bash
106
- cd your-project
107
- npx laxy-verify .
108
- ```
109
-
110
- Generate config plus CI workflow:
111
-
112
- ```bash
113
- npx laxy-verify --init
114
- ```
115
-
116
- That creates:
117
-
118
- - `.laxy.yml`
119
- - `.github/workflows/laxy-verify.yml`
120
-
121
- Optional: log in to connect the CLI to your Laxy account:
122
-
123
- ```bash
124
- npx laxy-verify login
125
- npx laxy-verify whoami
126
- ```
127
-
128
- For CI, set `LAXY_TOKEN` instead of interactive login:
129
-
130
- ```yaml
131
- env:
132
- LAXY_TOKEN: ${{ secrets.LAXY_TOKEN }}
133
- ```
134
-
135
- ## Example workflow
136
-
137
- 1. Run `npx laxy-verify .` locally before opening or merging a PR.
138
- 2. Fix broken builds, broken flows, and visible regressions.
139
- 3. Commit `.laxy.yml`.
140
- 4. Run `npx laxy-verify --init`.
141
- 5. Let the GitHub Action apply the same gate on every PR.
142
-
143
- ## Example output
144
-
145
- ```text
146
- Decision: client-ready
147
- Grade: Gold
148
-
149
- Passed:
150
- - production build
151
- - Lighthouse thresholds
152
- - core user flows
153
- - desktop, tablet, and mobile viewport checks
154
-
155
- Artifacts:
156
- - .laxy-result.json
157
- - laxy-verify-report.md
158
- ```
159
-
160
- ## The decision it helps you make
161
-
162
- `laxy-verify` answers a delivery decision, not just a score:
163
-
164
- - Would this break for real users right now?
165
- - What would block a client demo or QA handoff?
166
- - Is there enough evidence to merge or release with confidence?
167
-
168
- All verification checks already run without a paid plan gate.
169
-
170
- ## Grades
171
-
172
- | Grade | Meaning |
173
- |---|---|
174
- | Gold | Build passed, E2E passed, Lighthouse passed, and release-level evidence passed |
175
- | Silver | Build passed and E2E passed |
176
- | Bronze | Build passed |
177
- | Unverified | Build failed |
178
-
179
- Default Lighthouse thresholds:
180
-
181
- - Performance `>= 70`
182
- - Accessibility `>= 85`
183
- - SEO `>= 80`
184
- - Best Practices `>= 80`
185
-
186
- ## Config
187
-
188
- All fields in `.laxy.yml` are optional.
189
-
190
- ```yaml
191
- framework: auto
192
- build_command: ""
193
- dev_command: ""
194
- package_manager: auto
195
- port: 3000
196
- build_timeout: 300
197
- dev_timeout: 60
198
- lighthouse_runs: 1
199
- fail_on: bronze
200
-
201
- thresholds:
202
- performance: 70
203
- accessibility: 85
204
- seo: 80
205
- best_practices: 80
206
-
207
- crawl: false
208
- max_crawl_depth: 3
209
- max_crawl_pages: 10
210
- browsers:
211
- - chromium
212
- ```
213
-
214
- Useful adjustments:
215
-
216
- - raise `fail_on` in CI when you want stricter gates
217
- - set `build_command` or `dev_command` if auto-detection is not enough
218
- - increase `lighthouse_runs` for more stable performance evidence
219
- - point the CLI at the actual app directory in a monorepo
220
-
221
- ## CLI
222
-
223
- ```text
224
- npx laxy-verify [project-dir]
225
-
226
- Options:
227
- --format console|json
228
- --ci
229
- --config <path>
230
- --fail-on unverified|bronze|silver|gold
231
- --skip-lighthouse
232
- --plan-override free|pro|team
233
- --badge
234
- --init
235
- --multi-viewport
236
- --help
237
-
238
- Subcommands:
239
- login [email]
240
- logout
241
- whoami
242
- ```
243
-
244
- `--plan-override` is only for plan-label and automation testing. Verification coverage stays the same on every plan.
245
-
246
- ## Result files
247
-
248
- Every run writes `.laxy-result.json`.
249
-
250
- When the run finds something worth reviewing, it also writes `laxy-verify-report.md`.
251
-
252
- Typical use:
253
-
254
- - `.laxy-result.json` for CI parsing and machine decisions
255
- - `laxy-verify-report.md` for human review, PR discussion, or AI-assisted fixes
256
-
257
- The markdown report is designed to be readable and easy to paste into coding tools.
258
-
259
- It includes:
260
-
261
- - the main stop-or-ship decision in plain English
262
- - what passed
263
- - blockers and warnings
264
- - exact verification evidence
265
- - failed scenarios
266
- - a `Copy For AI` section
267
-
268
- ## What this is good at
269
-
270
- Use `laxy-verify` when you want:
271
-
272
- - a merge or release gate for frontend apps
273
- - one repeatable command for build plus audit plus visible-flow verification
274
- - a decision that non-authors can understand
275
- - JSON output for automation without building your own wrapper
276
-
277
- ## What this is not
278
-
279
- `laxy-verify` is not trying to replace:
280
-
281
- - your full Playwright suite
282
- - deep visual QA by designers
283
- - production observability
284
- - manual exploratory testing
285
-
286
- It is a pre-merge and pre-release verification layer, not your entire quality system.
287
-
288
- ## Limitations
289
-
290
- - monorepos should target the real app subdirectory
291
- - dev-server-based Lighthouse is not identical to production hosting
292
- - visual diff and viewport checks increase runtime
293
- - best stability is on current LTS Node releases
294
-
295
- ## Requirements
296
-
297
- - Node `>=20.18.0 <25`
298
- - a frontend app with a runnable build flow
299
- - optional: `playwright` if your project already uses it
300
-
301
- ## Links
302
-
303
- - GitHub: https://github.com/SUNgm24/Laxy/tree/main/laxy-verify
304
- - Issues: https://github.com/SUNgm24/Laxy/issues
1
+ # laxy-verify
2
+
3
+ `laxy-verify` is a deployment blocker gate for frontend apps.
4
+
5
+ Every verification run includes the same build, Lighthouse, E2E, multi-viewport, security, visual diff, and stability coverage regardless of plan.
6
+
7
+ ## Quick start
8
+
9
+ Run it on a frontend app:
10
+
11
+ ```bash
12
+ cd your-project
13
+ npx laxy-verify .
14
+ ```
15
+
16
+ Generate config plus GitHub Actions workflow:
17
+
18
+ ```bash
19
+ npx laxy-verify --init
20
+ ```
21
+
22
+ That creates:
23
+
24
+ - `.laxy.yml`
25
+ - `.github/workflows/laxy-verify.yml`
26
+
27
+ Optional: log in to connect the CLI to your Laxy account:
28
+
29
+ ```bash
30
+ npx laxy-verify login
31
+ npx laxy-verify whoami
32
+ ```
33
+
34
+ For CI, set `LAXY_TOKEN` instead of interactive login:
35
+
36
+ ```yaml
37
+ env:
38
+ LAXY_TOKEN: ${{ secrets.LAXY_TOKEN }}
39
+ ```
40
+
41
+ ## Why laxy-verify?
42
+
43
+ Most teams already have some mix of:
44
+
45
+ - `npm run build`
46
+ - Lighthouse
47
+ - Playwright or smoke checks
48
+ - CI status rules
49
+
50
+ The gap is not "can these tools exist together?" The gap is "who turns that pile of output into a safe merge or release decision?"
51
+
52
+ `laxy-verify` gives you:
53
+
54
+ - one command instead of a custom build plus audit plus smoke-check script stack
55
+ - one result file instead of scattered logs
56
+ - one blocker-first decision instead of "build passed, but do we actually trust this release?"
57
+ - optional account-linked automation on top of the same verification run
58
+
59
+ This is most useful if you ship frontend apps and want a practical gate before:
60
+
61
+ - merge
62
+ - client review
63
+ - QA handoff
64
+ - production release
65
+
66
+ ## vs other tools
67
+
68
+ No single competitor covers this combination. Here is where each tool stops:
69
+
70
+ | | laxy-verify | LHCI | Checkly | Percy / Argos | Unlighthouse | QA Wolf |
71
+ |--|--|--|--|--|--|--|
72
+ | Build failure detection | yes | no | no | no | no | no |
73
+ | Auto-generated E2E | yes | no | write your own | no | no | yes |
74
+ | Security audit | yes | no | no | no | no | no |
75
+ | Lighthouse (multi-run avg) | yes | yes | no | no | yes | no |
76
+ | Visual diff | yes | no | no | yes | no | no |
77
+ | Multi-viewport checks | yes | no | separate config | no | yes | no |
78
+ | Ship/hold release decision | yes | score only | no | no | no | no |
79
+ | Zero config to start | yes | no | no | no | partial | no |
80
+ | Free full coverage | yes | yes | limited | limited | yes | enterprise |
81
+
82
+ LHCI gives you Lighthouse. Percy gives you visual diffs. Checkly watches your production uptime. None of them produce a merge or release decision from one command.
83
+
84
+ ## The failures it is meant to catch
85
+
86
+ Use `laxy-verify` when you want to catch things like:
87
+
88
+ - the production build passes locally but fails in CI
89
+ - the app opens, but a key button or form flow is broken
90
+ - desktop looks fine, but a mobile CTA is pushed out of view
91
+ - Lighthouse looks acceptable, but the user-visible path is still not safe to ship
92
+ - a PR needs a clear hold reason instead of a vague "something failed"
93
+
94
+ ## What it actually checks
95
+
96
+ A standard run includes:
97
+
98
+ - production build success
99
+ - Lighthouse thresholds (3 runs for stable evidence)
100
+ - E2E scenarios for user-visible flows
101
+ - multi-viewport checks (desktop, tablet, mobile)
102
+ - blocker-aware reporting and release decisions
103
+
104
+ Every run includes:
105
+
106
+ - security audit
107
+ - visual diff evidence
108
+ - stability pass (second E2E run)
109
+
110
+ ## What you get from one run
111
+
112
+ - a release decision such as `quick-pass`, `client-ready`, `release-ready`, `hold`, or `investigate`
113
+ - a verification grade: `Gold`, `Silver`, `Bronze`, or `Unverified`
114
+ - `.laxy-result.json` for CI and automation
115
+ - `laxy-verify-report.md` for human review and AI handoff
116
+
117
+ Grades still exist, but they are not the main point. The main point is whether the run found blockers you should stop on.
118
+
119
+ ## Quick start
120
+
121
+ Run it on a frontend app:
122
+
123
+ ```bash
124
+ cd your-project
125
+ npx laxy-verify .
126
+ ```
127
+
128
+ Generate config plus CI workflow:
129
+
130
+ ```bash
131
+ npx laxy-verify --init
132
+ ```
133
+
134
+ That creates:
135
+
136
+ - `.laxy.yml`
137
+ - `.github/workflows/laxy-verify.yml`
138
+
139
+ Optional: log in to connect the CLI to your Laxy account:
140
+
141
+ ```bash
142
+ npx laxy-verify login
143
+ npx laxy-verify whoami
144
+ ```
145
+
146
+ For CI, set `LAXY_TOKEN` instead of interactive login:
147
+
148
+ ```yaml
149
+ env:
150
+ LAXY_TOKEN: ${{ secrets.LAXY_TOKEN }}
151
+ ```
152
+
153
+ ## Example workflow
154
+
155
+ 1. Run `npx laxy-verify .` locally before opening or merging a PR.
156
+ 2. Fix broken builds, broken flows, and visible regressions.
157
+ 3. Commit `.laxy.yml`.
158
+ 4. Run `npx laxy-verify --init`.
159
+ 5. Let the GitHub Action apply the same gate on every PR.
160
+
161
+ ## Example output
162
+
163
+ ```text
164
+ Decision: client-ready
165
+ Grade: Gold
166
+
167
+ Passed:
168
+ - production build
169
+ - Lighthouse thresholds
170
+ - core user flows
171
+ - desktop, tablet, and mobile viewport checks
172
+
173
+ Artifacts:
174
+ - .laxy-result.json
175
+ - laxy-verify-report.md
176
+ ```
177
+
178
+ ## The decision it helps you make
179
+
180
+ `laxy-verify` answers a delivery decision, not just a score:
181
+
182
+ - Would this break for real users right now?
183
+ - What would block a client demo or QA handoff?
184
+ - Is there enough evidence to merge or release with confidence?
185
+
186
+ All verification checks already run without a paid plan gate.
187
+
188
+ ## Grades
189
+
190
+ | Grade | Meaning |
191
+ |---|---|
192
+ | Gold | Build passed, E2E passed, Lighthouse passed, and release-level evidence passed |
193
+ | Silver | Build passed and E2E passed |
194
+ | Bronze | Build passed |
195
+ | Unverified | Build failed |
196
+
197
+ Default Lighthouse thresholds:
198
+
199
+ - Performance `>= 70`
200
+ - Accessibility `>= 85`
201
+ - SEO `>= 80`
202
+ - Best Practices `>= 80`
203
+
204
+ ## Config
205
+
206
+ All fields in `.laxy.yml` are optional.
207
+
208
+ ```yaml
209
+ framework: auto
210
+ build_command: ""
211
+ dev_command: ""
212
+ package_manager: auto
213
+ port: 3000
214
+ build_timeout: 300
215
+ dev_timeout: 60
216
+ lighthouse_runs: 1
217
+ fail_on: bronze
218
+
219
+ thresholds:
220
+ performance: 70
221
+ accessibility: 85
222
+ seo: 80
223
+ best_practices: 80
224
+
225
+ crawl: false
226
+ max_crawl_depth: 3
227
+ max_crawl_pages: 10
228
+ browsers:
229
+ - chromium
230
+ ```
231
+
232
+ Useful adjustments:
233
+
234
+ - raise `fail_on` in CI when you want stricter gates
235
+ - set `build_command` or `dev_command` if auto-detection is not enough
236
+ - increase `lighthouse_runs` for more stable performance evidence
237
+ - point the CLI at the actual app directory in a monorepo
238
+
239
+ ## CLI
240
+
241
+ ```text
242
+ npx laxy-verify [project-dir]
243
+
244
+ Options:
245
+ --format console|json
246
+ --ci
247
+ --config <path>
248
+ --fail-on unverified|bronze|silver|gold
249
+ --skip-lighthouse
250
+ --plan-override free|pro|team
251
+ --badge
252
+ --init
253
+ --multi-viewport
254
+ --help
255
+
256
+ Subcommands:
257
+ login [email]
258
+ logout
259
+ whoami
260
+ ```
261
+
262
+ `--plan-override` is only for plan-label and automation testing. Verification coverage stays the same on every plan.
263
+
264
+ ## Result files
265
+
266
+ Every run writes `.laxy-result.json`.
267
+
268
+ When the run finds something worth reviewing, it also writes `laxy-verify-report.md`.
269
+
270
+ Typical use:
271
+
272
+ - `.laxy-result.json` for CI parsing and machine decisions
273
+ - `laxy-verify-report.md` for human review, PR discussion, or AI-assisted fixes
274
+
275
+ The markdown report is designed to be readable and easy to paste into coding tools.
276
+
277
+ It includes:
278
+
279
+ - the main stop-or-ship decision in plain English
280
+ - what passed
281
+ - blockers and warnings
282
+ - exact verification evidence
283
+ - failed scenarios
284
+ - a `Copy For AI` section
285
+
286
+ ## What this is good at
287
+
288
+ Use `laxy-verify` when you want:
289
+
290
+ - a merge or release gate for frontend apps
291
+ - one repeatable command for build plus audit plus visible-flow verification
292
+ - a decision that non-authors can understand
293
+ - JSON output for automation without building your own wrapper
294
+
295
+ ## What this is not
296
+
297
+ `laxy-verify` is not trying to replace:
298
+
299
+ - your full Playwright suite
300
+ - deep visual QA by designers
301
+ - production observability
302
+ - manual exploratory testing
303
+
304
+ It is a pre-merge and pre-release verification layer, not your entire quality system.
305
+
306
+ ## Limitations
307
+
308
+ - monorepos should target the real app subdirectory
309
+ - dev-server-based Lighthouse is not identical to production hosting
310
+ - visual diff and viewport checks increase runtime
311
+ - best stability is on current LTS Node releases
312
+
313
+ ## Requirements
314
+
315
+ - Node `>=20.18.0 <25`
316
+ - a frontend app with a runnable build flow
317
+ - optional: `playwright` if your project already uses it
318
+
319
+ ## Links
320
+
321
+ - GitHub: https://github.com/SUNgm24/Laxy/tree/main/laxy-verify
322
+ - Issues: https://github.com/SUNgm24/Laxy/issues
package/dist/auth.d.ts CHANGED
@@ -3,4 +3,9 @@ export declare function loadToken(): string | null;
3
3
  export declare function saveToken(token: string, email: string, expiresInSec: number): void;
4
4
  export declare function clearToken(): void;
5
5
  export declare function whoami(): void;
6
+ /**
7
+ * Get or create a stable repo_id for the given project directory.
8
+ * Stored in ~/.laxy/repos.json keyed by absolute project path.
9
+ */
10
+ export declare function getOrCreateRepoId(projectDir: string): string;
6
11
  export declare function login(emailArg?: string): Promise<void>;
package/dist/auth.js CHANGED
@@ -38,6 +38,7 @@ exports.loadToken = loadToken;
38
38
  exports.saveToken = saveToken;
39
39
  exports.clearToken = clearToken;
40
40
  exports.whoami = whoami;
41
+ exports.getOrCreateRepoId = getOrCreateRepoId;
41
42
  exports.login = login;
42
43
  /**
43
44
  * CLI authentication helpers for laxy-verify.
@@ -49,8 +50,10 @@ exports.login = login;
49
50
  const fs = __importStar(require("node:fs"));
50
51
  const path = __importStar(require("node:path"));
51
52
  const os = __importStar(require("node:os"));
53
+ const crypto = __importStar(require("node:crypto"));
52
54
  const CREDENTIALS_DIR = path.join(os.homedir(), ".laxy");
53
55
  const CREDENTIALS_PATH = path.join(CREDENTIALS_DIR, "credentials.json");
56
+ const REPOS_PATH = path.join(CREDENTIALS_DIR, "repos.json");
54
57
  exports.LAXY_API_URL = process.env.LAXY_API_URL ?? "https://laxy-blue.vercel.app";
55
58
  function loadToken() {
56
59
  const envToken = process.env.LAXY_TOKEN;
@@ -123,6 +126,37 @@ function whoami() {
123
126
  console.log(" Saved CLI credentials could not be read.");
124
127
  }
125
128
  }
129
+ /**
130
+ * Get or create a stable repo_id for the given project directory.
131
+ * Stored in ~/.laxy/repos.json keyed by absolute project path.
132
+ */
133
+ function getOrCreateRepoId(projectDir) {
134
+ const absDir = path.resolve(projectDir);
135
+ let repos = {};
136
+ try {
137
+ if (fs.existsSync(REPOS_PATH)) {
138
+ repos = JSON.parse(fs.readFileSync(REPOS_PATH, "utf-8"));
139
+ }
140
+ }
141
+ catch {
142
+ repos = {};
143
+ }
144
+ if (repos[absDir])
145
+ return repos[absDir];
146
+ // Generate new repo_id
147
+ const newId = crypto.randomUUID();
148
+ repos[absDir] = newId;
149
+ try {
150
+ if (!fs.existsSync(CREDENTIALS_DIR)) {
151
+ fs.mkdirSync(CREDENTIALS_DIR, { recursive: true });
152
+ }
153
+ fs.writeFileSync(REPOS_PATH, JSON.stringify(repos, null, 2), { encoding: "utf-8", mode: 0o600 });
154
+ }
155
+ catch {
156
+ // If we can't save, still return the generated id for this run
157
+ }
158
+ return newId;
159
+ }
126
160
  /**
127
161
  * Read a line from stdin without creating a readline interface.
128
162
  * This avoids the Windows UV_HANDLE_CLOSING assertion that can happen
package/dist/cli.js CHANGED
@@ -158,6 +158,7 @@ function parseArgs() {
158
158
  failureAnalysis: flags["failure-analysis"] !== undefined,
159
159
  crawl: flags.crawl !== undefined,
160
160
  planOverride: flags["plan-override"],
161
+ port: flags.port !== undefined ? Number(flags.port) : undefined,
161
162
  help: flags.help !== undefined || flags.h !== undefined,
162
163
  };
163
164
  }
@@ -322,6 +323,7 @@ async function run() {
322
323
  --config <path> Path to .laxy.yml
323
324
  --fail-on unverified | bronze | silver | gold
324
325
  --skip-lighthouse Skip Lighthouse but still run build and E2E
326
+ --port <port> Use an already-running dev server on this port (skip build & server start)
325
327
  --plan-override free | pro | team (testing metadata only)
326
328
  --multi-viewport Lighthouse on desktop/tablet/mobile
327
329
  --crawl Crawl the app to discover routes before E2E
@@ -338,6 +340,7 @@ async function run() {
338
340
  npx laxy-verify . # Run in current directory
339
341
  npx laxy-verify . --ci # CI mode
340
342
  npx laxy-verify . --fail-on silver # Block Bronze or worse
343
+ npx laxy-verify . --port 3001 # Use existing dev server on port 3001
341
344
 
342
345
  Docs: https://github.com/SUNgm24/Laxy/tree/main/laxy-verify
343
346
  `);
@@ -360,6 +363,23 @@ async function run() {
360
363
  return;
361
364
  }
362
365
  if (args.init) {
366
+ let initEntitlements = null;
367
+ try {
368
+ initEntitlements = await (0, entitlement_js_1.getEntitlements)();
369
+ }
370
+ catch {
371
+ // ignore fetch errors
372
+ }
373
+ const initPlan = initEntitlements?.plan ?? "free";
374
+ const hasProAccess = initPlan === "pro" || initPlan === "team";
375
+ if (!hasProAccess) {
376
+ console.log("\n GitHub Actions integration is a Pro feature.");
377
+ console.log(" Log in and upgrade to Pro to use --init.");
378
+ console.log(" → laxy-verify login");
379
+ console.log(" → https://laxy.dev/pricing\n");
380
+ exitGracefully(1);
381
+ return;
382
+ }
363
383
  (0, init_js_1.runInit)(args.projectDir);
364
384
  if (!args.initRun) {
365
385
  console.log("\n Next step: run npx laxy-verify . (or use --init --run to continue immediately)");
@@ -376,8 +396,24 @@ async function run() {
376
396
  return;
377
397
  }
378
398
  const content = JSON.parse(fs.readFileSync(resultPath, "utf-8"));
379
- const badge = (0, badge_js_1.generateBadge)(content.grade);
380
- console.log(badge);
399
+ // Pro: 실시간 배지 URL (verify_runs 기반)
400
+ let badgeEntitlements = null;
401
+ try {
402
+ badgeEntitlements = await (0, entitlement_js_1.getEntitlements)();
403
+ }
404
+ catch { /* ignore */ }
405
+ const badgePlan = badgeEntitlements?.plan ?? "free";
406
+ const badgeIsPro = badgePlan === "pro" || badgePlan === "team";
407
+ if (badgeIsPro) {
408
+ const { LAXY_API_URL } = await Promise.resolve().then(() => __importStar(require("./auth.js")));
409
+ const repoId = (0, auth_js_1.getOrCreateRepoId)(args.projectDir);
410
+ const dynamicUrl = `${LAXY_API_URL}/api/badge/${repoId}`;
411
+ console.log(`![Laxy](${dynamicUrl})`);
412
+ }
413
+ else {
414
+ const badge = (0, badge_js_1.generateBadge)(content.grade);
415
+ console.log(badge);
416
+ }
381
417
  exitGracefully(0);
382
418
  return;
383
419
  }
@@ -409,25 +445,44 @@ async function run() {
409
445
  }
410
446
  const buildCmd = config.build_command || detected.buildCmd;
411
447
  const devCmd = config.dev_command || detected.devCmd;
412
- const port = config.port;
413
- try {
414
- await ensurePortAvailableForVerification(port);
448
+ const port = args.port ?? config.port;
449
+ const useExistingServer = args.port !== undefined;
450
+ if (!useExistingServer) {
451
+ try {
452
+ await ensurePortAvailableForVerification(port);
453
+ }
454
+ catch (err) {
455
+ console.error(`Preflight error: ${err instanceof Error ? err.message : String(err)}`);
456
+ exitGracefully(2);
457
+ return;
458
+ }
415
459
  }
416
- catch (err) {
417
- console.error(`Preflight error: ${err instanceof Error ? err.message : String(err)}`);
418
- exitGracefully(2);
419
- return;
460
+ else {
461
+ // Verify the existing server is actually reachable
462
+ const status = await (0, serve_js_1.probeServerStatus)(port);
463
+ if (status === null) {
464
+ console.error(`Preflight error: No server responding on port ${port}. Make sure the dev server is running before using --port.`);
465
+ exitGracefully(2);
466
+ return;
467
+ }
468
+ console.log(`Using existing dev server on port ${port} (HTTP ${status})`);
420
469
  }
421
470
  let buildResult;
422
- try {
423
- buildResult = await (0, build_js_1.runBuild)(buildCmd, config.build_timeout, args.projectDir);
471
+ if (useExistingServer) {
472
+ // Skip build when using an external server
473
+ buildResult = { success: true, durationMs: 0, errors: [] };
424
474
  }
425
- catch (err) {
426
- buildResult = {
427
- success: false,
428
- durationMs: 0,
429
- errors: err instanceof Error ? [err.message] : [String(err)],
430
- };
475
+ else {
476
+ try {
477
+ buildResult = await (0, build_js_1.runBuild)(buildCmd, config.build_timeout, args.projectDir);
478
+ }
479
+ catch (err) {
480
+ buildResult = {
481
+ success: false,
482
+ durationMs: 0,
483
+ errors: err instanceof Error ? [err.message] : [String(err)],
484
+ };
485
+ }
431
486
  }
432
487
  let scores;
433
488
  let lighthouseResult = null;
@@ -485,9 +540,11 @@ async function run() {
485
540
  if (buildResult.success) {
486
541
  let servePid;
487
542
  try {
488
- const serve = await (0, serve_js_1.startDevServer)(devCmd, port, config.dev_timeout, args.projectDir);
489
- servePid = serve.pid;
490
- activeDevServerPid = serve.pid;
543
+ if (!useExistingServer) {
544
+ const serve = await (0, serve_js_1.startDevServer)(devCmd, port, config.dev_timeout, args.projectDir);
545
+ servePid = serve.pid;
546
+ activeDevServerPid = serve.pid;
547
+ }
491
548
  const verifyUrl = `http://127.0.0.1:${port}/`;
492
549
  const verificationTier = (0, index_js_1.planToVerificationTier)(effectiveFeatures.plan);
493
550
  if (!args.skipLighthouse) {
@@ -552,7 +609,15 @@ async function run() {
552
609
  };
553
610
  e2eCoverageGaps = e2eRuns.coverageGaps;
554
611
  e2eConsoleErrors = e2eRuns.consoleErrors;
555
- // Broken links audit ??runs after E2E so we have the crawl result
612
+ // Merge console errors captured during crawl phase
613
+ if (e2eRuns.crawlResult) {
614
+ const crawlConsoleErrors = e2eRuns.crawlResult.pages.flatMap((p) => p.consoleErrors);
615
+ if (crawlConsoleErrors.length > 0) {
616
+ const unique = crawlConsoleErrors.filter((e) => !e2eConsoleErrors.includes(e));
617
+ e2eConsoleErrors = [...e2eConsoleErrors, ...unique];
618
+ }
619
+ }
620
+ // Broken links audit — runs after E2E so we have the crawl result
556
621
  if (e2eRuns.crawlResult && e2eRuns.crawlResult.totalLinks > 0) {
557
622
  try {
558
623
  brokenLinksResult = await (0, broken_links_js_1.auditBrokenLinks)(e2eRuns.crawlResult, verifyUrl);
@@ -746,6 +811,47 @@ async function run() {
746
811
  }
747
812
  }
748
813
  writeResultFile(args.projectDir, resultObj);
814
+ // Pro 이상: 결과를 서버에 저장 (배지, 공유 링크용)
815
+ const token = (0, auth_js_1.loadToken)();
816
+ const isPro = effectiveFeatures.plan === "pro" || effectiveFeatures.plan === "team";
817
+ if (token && isPro) {
818
+ try {
819
+ const repoId = (0, auth_js_1.getOrCreateRepoId)(args.projectDir);
820
+ const { LAXY_API_URL } = await Promise.resolve().then(() => __importStar(require("./auth.js")));
821
+ const saveRes = await fetch(`${LAXY_API_URL}/api/v1/cli-results`, {
822
+ method: "POST",
823
+ headers: {
824
+ "Content-Type": "application/json",
825
+ Authorization: `Bearer ${token}`,
826
+ },
827
+ body: JSON.stringify({
828
+ repo_id: repoId,
829
+ project_name: path.basename(path.resolve(args.projectDir)),
830
+ grade: unifiedGrade,
831
+ verdict: verificationReport.verdict,
832
+ scores: {
833
+ performance: scores?.performance,
834
+ accessibility: scores?.accessibility,
835
+ seo: scores?.seo,
836
+ best_practices: scores?.bestPractices,
837
+ e2e_passed: e2eResult?.passed,
838
+ e2e_total: e2eResult?.total,
839
+ security_critical: securityAuditResult?.critical,
840
+ console_error_count: e2eConsoleErrors.length,
841
+ broken_link_count: brokenLinksResult?.brokenLinks.length,
842
+ },
843
+ full_result: { framework: detected.framework },
844
+ }),
845
+ });
846
+ if (!saveRes.ok) {
847
+ const errBody = await saveRes.json().catch(() => ({}));
848
+ console.warn(` [warn] Result save failed (${saveRes.status}): ${errBody.error ?? "unknown error"}`);
849
+ }
850
+ }
851
+ catch {
852
+ // 네트워크 오류는 검증 결과에 영향 없음
853
+ }
854
+ }
749
855
  if (args.format === "json") {
750
856
  console.log(JSON.stringify(resultObj, null, 2));
751
857
  }
package/dist/comment.js CHANGED
@@ -36,14 +36,17 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.postPRComment = postPRComment;
37
37
  const fs = __importStar(require("node:fs"));
38
38
  const github_js_1 = require("./github.js");
39
+ const trend_js_1 = require("./trend.js");
39
40
  function renderTrendTable(delta) {
40
41
  const lines = [
41
42
  "| Check | This PR | vs Base |",
42
43
  "|---|---|---|",
43
44
  ];
44
- const gradeTrendStr = delta.grade.current !== delta.grade.base
45
- ? ` (was ${delta.grade.base})`
46
- : "";
45
+ const gradeUp = (0, trend_js_1.gradeIndex)(delta.grade.current) > (0, trend_js_1.gradeIndex)(delta.grade.base);
46
+ const gradeDown = (0, trend_js_1.gradeIndex)(delta.grade.current) < (0, trend_js_1.gradeIndex)(delta.grade.base);
47
+ const gradeTrendStr = gradeUp ? ` ↑ (was ${delta.grade.base})` :
48
+ gradeDown ? ` ↓ (was ${delta.grade.base})` :
49
+ "";
47
50
  lines.push(`| Grade | **${delta.grade.current}**${gradeTrendStr} | ${delta.grade.base} |`);
48
51
  function fmtDelta(d) {
49
52
  if (d === null)
@@ -98,15 +101,15 @@ async function postPRComment(result, trend) {
98
101
  : grade === "Unverified"
99
102
  ? "The production build failed or verification could not complete, so this PR should be held."
100
103
  : `This PR did not clear the current verification gate. Grade summary: **${grade}**.`;
101
- const body = `## ${statusLabel} Laxy Verify: ${headline}
102
-
103
- ${summary}
104
-
105
- ${trendTable}${lhTable}**Fail-on threshold**: ${result.config_fail_on ?? "bronze"}
106
-
107
- Grade remains available for automation, but this check should be read first as a merge and release blocker gate.
108
-
109
- ---
104
+ const body = `## ${statusLabel} Laxy Verify: ${headline}
105
+
106
+ ${summary}
107
+
108
+ ${trendTable}${lhTable}**Fail-on threshold**: ${result.config_fail_on ?? "bronze"}
109
+
110
+ Grade remains available for automation, but this check should be read first as a merge and release blocker gate.
111
+
112
+ ---
110
113
  [Open laxy-verify docs](https://github.com/SUNgm24/Laxy/tree/main/laxy-verify)`;
111
114
  const [owner, repo] = ctx.repository.split("/");
112
115
  const url = `https://api.github.com/repos/${owner}/${repo}/issues/${prNumber}/comments`;
package/dist/crawler.d.ts CHANGED
@@ -8,6 +8,7 @@ export interface CrawlPage {
8
8
  buttons: string[];
9
9
  internalLinks: string[];
10
10
  hasConsoleErrors: boolean;
11
+ consoleErrors: string[];
11
12
  }
12
13
  export interface CrawlForm {
13
14
  selector: string;
package/dist/crawler.js CHANGED
@@ -189,6 +189,7 @@ async function crawlApp(baseUrl, options) {
189
189
  buttons: pageInfo.buttons.slice(0, 10),
190
190
  internalLinks: Array.from(new Set(internalLinks)),
191
191
  hasConsoleErrors: consoleErrors.length > 0,
192
+ consoleErrors,
192
193
  };
193
194
  pages.push(crawlPage);
194
195
  // Queue discovered internal links
@@ -65,12 +65,14 @@ function applyPlanOverride(features, overridePlan) {
65
65
  if (!overridePlan)
66
66
  return features;
67
67
  const isTeam = overridePlan === "team";
68
+ const isPro = overridePlan === "pro" || overridePlan === "team";
68
69
  return {
69
70
  ...features,
70
71
  plan: overridePlan,
71
72
  // All verification features run on every plan
72
- // Automation features — Team only
73
- github_actions: isTeam,
73
+ // github_actionsPro and Team
74
+ github_actions: isPro,
75
+ // queue_priority, parallel_execution — Team only
74
76
  queue_priority: isTeam,
75
77
  parallel_execution: isTeam,
76
78
  };
package/dist/serve.js CHANGED
@@ -87,59 +87,59 @@ function writeWindowsDevWrapper(command, port, cwd) {
87
87
  const wrapperDir = path.join(os.tmpdir(), "laxy-verify");
88
88
  fs.mkdirSync(wrapperDir, { recursive: true });
89
89
  const wrapperPath = path.join(wrapperDir, `dev-wrapper-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.cjs`);
90
- const source = `"use strict";
91
- const { spawn } = require("node:child_process");
92
-
93
- const parentPid = Number(process.env.LAXY_VERIFY_PARENT_PID || "0");
94
- const command = ${JSON.stringify(command)};
95
- const childCwd = ${JSON.stringify(cwd ?? null)};
96
- const port = ${JSON.stringify(String(port))};
97
-
98
- function killChildTree(pid) {
99
- try {
100
- const killer = spawn(process.env.ComSpec || "cmd.exe", ["/d", "/c", "taskkill /T /F /PID " + pid], {
101
- stdio: "ignore",
102
- });
103
- killer.on("exit", () => process.exit(1));
104
- } catch {
105
- process.exit(1);
106
- }
107
- }
108
-
109
- const child = spawn(process.env.ComSpec || "cmd.exe", ["/d", "/c", command], {
110
- stdio: ["ignore", "pipe", "pipe"],
111
- cwd: childCwd || undefined,
112
- env: { ...process.env, PORT: port },
113
- });
114
-
115
- child.stdout?.on("data", (chunk) => process.stdout.write(chunk));
116
- child.stderr?.on("data", (chunk) => process.stderr.write(chunk));
117
-
118
- child.on("error", (err) => {
119
- process.stderr.write((err && err.message ? err.message : String(err)) + "\\n");
120
- process.exit(1);
121
- });
122
-
123
- child.on("exit", (code) => {
124
- clearInterval(watchdog);
125
- process.exit(code ?? 1);
126
- });
127
-
128
- const watchdog = setInterval(() => {
129
- if (!parentPid) return;
130
- try {
131
- process.kill(parentPid, 0);
132
- } catch {
133
- clearInterval(watchdog);
134
- if (child.pid) {
135
- killChildTree(child.pid);
136
- return;
137
- }
138
- process.exit(1);
139
- }
140
- }, 1000);
141
-
142
- watchdog.unref?.();
90
+ const source = `"use strict";
91
+ const { spawn } = require("node:child_process");
92
+
93
+ const parentPid = Number(process.env.LAXY_VERIFY_PARENT_PID || "0");
94
+ const command = ${JSON.stringify(command)};
95
+ const childCwd = ${JSON.stringify(cwd ?? null)};
96
+ const port = ${JSON.stringify(String(port))};
97
+
98
+ function killChildTree(pid) {
99
+ try {
100
+ const killer = spawn(process.env.ComSpec || "cmd.exe", ["/d", "/c", "taskkill /T /F /PID " + pid], {
101
+ stdio: "ignore",
102
+ });
103
+ killer.on("exit", () => process.exit(1));
104
+ } catch {
105
+ process.exit(1);
106
+ }
107
+ }
108
+
109
+ const child = spawn(process.env.ComSpec || "cmd.exe", ["/d", "/c", command], {
110
+ stdio: ["ignore", "pipe", "pipe"],
111
+ cwd: childCwd || undefined,
112
+ env: { ...process.env, PORT: port },
113
+ });
114
+
115
+ child.stdout?.on("data", (chunk) => process.stdout.write(chunk));
116
+ child.stderr?.on("data", (chunk) => process.stderr.write(chunk));
117
+
118
+ child.on("error", (err) => {
119
+ process.stderr.write((err && err.message ? err.message : String(err)) + "\\n");
120
+ process.exit(1);
121
+ });
122
+
123
+ child.on("exit", (code) => {
124
+ clearInterval(watchdog);
125
+ process.exit(code ?? 1);
126
+ });
127
+
128
+ const watchdog = setInterval(() => {
129
+ if (!parentPid) return;
130
+ try {
131
+ process.kill(parentPid, 0);
132
+ } catch {
133
+ clearInterval(watchdog);
134
+ if (child.pid) {
135
+ killChildTree(child.pid);
136
+ return;
137
+ }
138
+ process.exit(1);
139
+ }
140
+ }, 1000);
141
+
142
+ watchdog.unref?.();
143
143
  `;
144
144
  fs.writeFileSync(wrapperPath, source, "utf-8");
145
145
  return wrapperPath;
@@ -219,6 +219,9 @@ async function startDevServer(command, port, timeoutSec, cwd) {
219
219
  if (proc.pid) {
220
220
  await killProcessTree(proc.pid);
221
221
  }
222
+ if (wrapperPath) {
223
+ fs.rmSync(wrapperPath, { force: true });
224
+ }
222
225
  reject(new DevServerTimeoutError(port, timeoutSec));
223
226
  return;
224
227
  }
package/dist/trend.d.ts CHANGED
@@ -43,6 +43,7 @@ export interface TrendDelta {
43
43
  base: string | null;
44
44
  };
45
45
  }
46
+ export declare function gradeIndex(grade: string): number;
46
47
  export declare function loadBaseSnapshot(baseResultPath: string): TrendSnapshot | null;
47
48
  export declare function computeTrendDelta(current: TrendSnapshot, base: TrendSnapshot): TrendDelta;
48
49
  export declare function renderTrendSection(delta: TrendDelta): string;
package/dist/trend.js CHANGED
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.gradeIndex = gradeIndex;
36
37
  exports.loadBaseSnapshot = loadBaseSnapshot;
37
38
  exports.computeTrendDelta = computeTrendDelta;
38
39
  exports.renderTrendSection = renderTrendSection;
@@ -57,9 +58,9 @@ function gradeTrend(current, base) {
57
58
  const ci = gradeIndex(current);
58
59
  const bi = gradeIndex(base);
59
60
  if (ci > bi)
60
- return ` (was ${base})`;
61
+ return ` (was ${base})`;
61
62
  if (ci < bi)
62
- return ` (was ${base})`;
63
+ return ` (was ${base})`;
63
64
  return "";
64
65
  }
65
66
  function loadBaseSnapshot(baseResultPath) {
package/package.json CHANGED
@@ -1,67 +1,67 @@
1
- {
2
- "name": "laxy-verify",
3
- "version": "1.1.33",
4
- "description": "Frontend verification CLI for build checks, Lighthouse, E2E, and release readiness",
5
- "license": "MIT",
6
- "type": "commonjs",
7
- "homepage": "https://github.com/SUNgm24/Laxy/tree/main/laxy-verify#readme",
8
- "repository": {
9
- "type": "git",
10
- "url": "git+https://github.com/SUNgm24/Laxy.git",
11
- "directory": "laxy-verify"
12
- },
13
- "bugs": {
14
- "url": "https://github.com/SUNgm24/Laxy/issues"
15
- },
16
- "keywords": [
17
- "frontend",
18
- "verification",
19
- "quality-gate",
20
- "release-readiness",
21
- "lighthouse",
22
- "e2e",
23
- "qa",
24
- "cli",
25
- "nextjs",
26
- "vite"
27
- ],
28
- "engines": {
29
- "node": ">=20.18.0"
30
- },
31
- "bin": {
32
- "laxy-verify": "dist/cli.js"
33
- },
34
- "files": [
35
- "dist/"
36
- ],
37
- "scripts": {
38
- "build": "tsc",
39
- "start": "node dist/cli.js",
40
- "test": "vitest run",
41
- "test:coverage": "vitest run --coverage"
42
- },
43
- "dependencies": {
44
- "@lhci/cli": "^0.14.0",
45
- "chrome-launcher": "^0.13.4",
46
- "js-yaml": "^4.1.0",
47
- "lighthouse": "^12.1.0",
48
- "pixelmatch": "^7.1.0",
49
- "pngjs": "^7.0.0",
50
- "puppeteer": "^24.40.0",
51
- "tree-kill": "^1.2.2"
52
- },
53
- "devDependencies": {
54
- "@types/js-yaml": "^4.0.9",
55
- "@types/node": "^20.0.0",
56
- "typescript": "^5.4.0",
57
- "vitest": "^2.0.0"
58
- },
59
- "peerDependencies": {
60
- "playwright": "^1.40.0"
61
- },
62
- "peerDependenciesMeta": {
63
- "playwright": {
64
- "optional": true
65
- }
66
- }
67
- }
1
+ {
2
+ "name": "laxy-verify",
3
+ "version": "1.2.1",
4
+ "description": "Frontend verification CLI for build checks, Lighthouse, E2E, and release readiness",
5
+ "license": "MIT",
6
+ "type": "commonjs",
7
+ "homepage": "https://github.com/SUNgm24/Laxy/tree/main/laxy-verify#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/SUNgm24/Laxy.git",
11
+ "directory": "laxy-verify"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/SUNgm24/Laxy/issues"
15
+ },
16
+ "keywords": [
17
+ "frontend",
18
+ "verification",
19
+ "quality-gate",
20
+ "release-readiness",
21
+ "lighthouse",
22
+ "e2e",
23
+ "qa",
24
+ "cli",
25
+ "nextjs",
26
+ "vite"
27
+ ],
28
+ "engines": {
29
+ "node": ">=20.18.0"
30
+ },
31
+ "bin": {
32
+ "laxy-verify": "dist/cli.js"
33
+ },
34
+ "files": [
35
+ "dist/"
36
+ ],
37
+ "scripts": {
38
+ "build": "tsc",
39
+ "start": "node dist/cli.js",
40
+ "test": "vitest run",
41
+ "test:coverage": "vitest run --coverage"
42
+ },
43
+ "dependencies": {
44
+ "@lhci/cli": "^0.14.0",
45
+ "chrome-launcher": "^0.13.4",
46
+ "js-yaml": "^4.1.0",
47
+ "lighthouse": "^12.1.0",
48
+ "pixelmatch": "^7.1.0",
49
+ "pngjs": "^7.0.0",
50
+ "puppeteer": "^24.40.0",
51
+ "tree-kill": "^1.2.2"
52
+ },
53
+ "devDependencies": {
54
+ "@types/js-yaml": "^4.0.9",
55
+ "@types/node": "^20.0.0",
56
+ "typescript": "^5.4.0",
57
+ "vitest": "^2.0.0"
58
+ },
59
+ "peerDependencies": {
60
+ "playwright": "^1.40.0"
61
+ },
62
+ "peerDependenciesMeta": {
63
+ "playwright": {
64
+ "optional": true
65
+ }
66
+ }
67
+ }