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 +322 -304
- package/dist/auth.d.ts +5 -0
- package/dist/auth.js +34 -0
- package/dist/cli.js +127 -21
- package/dist/comment.js +15 -12
- package/dist/crawler.d.ts +1 -0
- package/dist/crawler.js +1 -0
- package/dist/entitlement.js +4 -2
- package/dist/serve.js +56 -53
- package/dist/trend.d.ts +1 -0
- package/dist/trend.js +3 -2
- package/package.json +67 -67
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
|
-
##
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
-
|
|
89
|
-
-
|
|
90
|
-
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
```bash
|
|
124
|
-
|
|
125
|
-
npx laxy-verify
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
-
|
|
183
|
-
-
|
|
184
|
-
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
-
|
|
273
|
-
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
-
|
|
282
|
-
-
|
|
283
|
-
-
|
|
284
|
-
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
-
|
|
291
|
-
-
|
|
292
|
-
-
|
|
293
|
-
-
|
|
294
|
-
|
|
295
|
-
##
|
|
296
|
-
|
|
297
|
-
-
|
|
298
|
-
|
|
299
|
-
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
-
|
|
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
|
-
|
|
380
|
-
|
|
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(``);
|
|
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
|
-
|
|
414
|
-
|
|
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
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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
|
-
|
|
423
|
-
|
|
471
|
+
if (useExistingServer) {
|
|
472
|
+
// Skip build when using an external server
|
|
473
|
+
buildResult = { success: true, durationMs: 0, errors: [] };
|
|
424
474
|
}
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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
|
-
//
|
|
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
|
|
45
|
-
|
|
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
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
|
package/dist/entitlement.js
CHANGED
|
@@ -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
|
-
//
|
|
73
|
-
github_actions:
|
|
73
|
+
// github_actions — Pro 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
|
|
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
|
+
}
|