laxy-verify 1.1.0 → 1.1.2
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 +212 -0
- package/dist/auth.js +44 -33
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,9 +5,221 @@ Frontend quality gate for AI-generated code. Build + Lighthouse verification wit
|
|
|
5
5
|
```bash
|
|
6
6
|
npx laxy-verify --init # Auto-detect framework, generate config
|
|
7
7
|
npx laxy-verify . # Run verification
|
|
8
|
+
npx laxy-verify login # Log in for Pro/Pro+ features
|
|
8
9
|
npx laxy-verify --badge # Show shields.io badge
|
|
9
10
|
```
|
|
10
11
|
|
|
12
|
+
## Quick Start
|
|
13
|
+
|
|
14
|
+
### 1. Initialize
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
cd your-project
|
|
18
|
+
npx laxy-verify --init
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
This generates `.laxy.yml` and `.github/workflows/laxy-verify.yml` automatically by detecting your framework and package manager.
|
|
22
|
+
|
|
23
|
+
### 2. Run Locally
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npx laxy-verify .
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### 3. Add to CI
|
|
30
|
+
|
|
31
|
+
Push the generated workflow file. Every PR gets a quality gate with grade comment and status check.
|
|
32
|
+
|
|
33
|
+
## Grades
|
|
34
|
+
|
|
35
|
+
| Grade | Requirement |
|
|
36
|
+
|-------|------------|
|
|
37
|
+
| **Gold** | Build passes + all Lighthouse thresholds pass + all viewports pass (Pro+ only) |
|
|
38
|
+
| **Silver** | Build passes + Lighthouse exceeds all thresholds |
|
|
39
|
+
| **Bronze** | Build passes (Lighthouse not run or below threshold) |
|
|
40
|
+
| **Unverified** | Build failed |
|
|
41
|
+
|
|
42
|
+
`fail_on` controls the minimum acceptable grade. Default: `bronze`.
|
|
43
|
+
|
|
44
|
+
> **Gold grade** is only achievable with a Pro+ account. It requires multi-viewport Lighthouse (desktop/tablet/mobile) to all pass in addition to the standard thresholds.
|
|
45
|
+
|
|
46
|
+
## Pro / Pro+ Features
|
|
47
|
+
|
|
48
|
+
Log in to unlock paid plan features:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npx laxy-verify login # Log in with your laxy-blue.vercel.app account
|
|
52
|
+
npx laxy-verify whoami # Check current login status
|
|
53
|
+
npx laxy-verify logout # Remove saved credentials
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
| Feature | Free | Pro | Pro+ |
|
|
57
|
+
|---------|------|-----|------|
|
|
58
|
+
| Build verification | ✅ | ✅ | ✅ |
|
|
59
|
+
| Lighthouse (1 run) | ✅ | ✅ | ✅ |
|
|
60
|
+
| Silver grade | ✅ | ✅ | ✅ |
|
|
61
|
+
| Bronze grade | ✅ | ✅ | ✅ |
|
|
62
|
+
| Lighthouse (3 runs, averaged) | ❌ | ✅ | ✅ |
|
|
63
|
+
| Gold grade eligibility | ❌ | ✅ | ✅ |
|
|
64
|
+
| Verbose failure output | ❌ | ✅ | ✅ |
|
|
65
|
+
| Multi-viewport Lighthouse (desktop/tablet/mobile) | ❌ | ❌ | ✅ |
|
|
66
|
+
| Failure analysis report | ❌ | ❌ | ✅ |
|
|
67
|
+
| Fast Lane (priority queue) | ❌ | ❌ | ✅ |
|
|
68
|
+
|
|
69
|
+
### Login
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
npx laxy-verify login
|
|
73
|
+
# Enter your email and password for laxy-blue.vercel.app
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Credentials are stored in `~/.laxy/credentials.json` (chmod 600). Token expires after 30 days.
|
|
77
|
+
|
|
78
|
+
For CI environments, set the `LAXY_TOKEN` environment variable instead of logging in interactively:
|
|
79
|
+
|
|
80
|
+
```yaml
|
|
81
|
+
env:
|
|
82
|
+
LAXY_TOKEN: ${{ secrets.LAXY_TOKEN }}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Multi-viewport (Pro+)
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
npx laxy-verify --multi-viewport .
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Runs Lighthouse on three viewports: desktop, tablet (1024×768), and mobile. Gold grade requires all three to pass the configured thresholds.
|
|
92
|
+
|
|
93
|
+
## Configuration
|
|
94
|
+
|
|
95
|
+
All fields optional in `.laxy.yml`:
|
|
96
|
+
|
|
97
|
+
```yaml
|
|
98
|
+
framework: "auto" # auto | nextjs | vite | cra | sveltekit
|
|
99
|
+
build_command: "" # default: auto-detected from package.json
|
|
100
|
+
dev_command: "" # default: auto-detected
|
|
101
|
+
package_manager: "auto" # auto | npm | pnpm | yarn | bun
|
|
102
|
+
port: 3000 # dev server port
|
|
103
|
+
build_timeout: 300 # seconds (default 5m)
|
|
104
|
+
dev_timeout: 60 # seconds for dev server start (90 in CI mode)
|
|
105
|
+
lighthouse_runs: 1 # @lhci/cli runs (CI mode auto-sets to 3)
|
|
106
|
+
|
|
107
|
+
thresholds:
|
|
108
|
+
performance: 70 # CI mode applies -10 offset (effective: 60)
|
|
109
|
+
accessibility: 85
|
|
110
|
+
seo: 80
|
|
111
|
+
best_practices: 80
|
|
112
|
+
|
|
113
|
+
fail_on: "bronze" # unverified | bronze | silver | gold
|
|
114
|
+
# unverified = never fail (informational only)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**fail_on vs build failure:** Build failure always produces grade `Unverified` and exit code 1, regardless of `fail_on`. `fail_on: unverified` means informational only (always exit 0).
|
|
118
|
+
|
|
119
|
+
**--ci flag:** Lowers Performance threshold by 10, sets `lighthouse_runs=3` (when not explicitly set), and increases `dev_timeout` to 90s. Auto-set when `CI=true` env var exists.
|
|
120
|
+
|
|
121
|
+
## CLI Options
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
npx laxy-verify [project-dir] Default: current directory
|
|
125
|
+
|
|
126
|
+
Options:
|
|
127
|
+
--format console | json Output format (default: console)
|
|
128
|
+
--ci CI mode: -10 Performance, runs=3
|
|
129
|
+
--config <path> Path to .laxy.yml
|
|
130
|
+
--fail-on unverified|bronze|silver|gold Override fail_on
|
|
131
|
+
--skip-lighthouse Build-only verification (max Bronze)
|
|
132
|
+
--badge Show shields.io badge (reads .laxy-result.json)
|
|
133
|
+
--init Generate .laxy.yml + GitHub workflow
|
|
134
|
+
--multi-viewport Pro+: run Lighthouse on desktop/tablet/mobile
|
|
135
|
+
|
|
136
|
+
Subcommands:
|
|
137
|
+
login [email] Log in to unlock Pro/Pro+ features
|
|
138
|
+
logout Remove saved credentials
|
|
139
|
+
whoami Show current login status
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## exit codes
|
|
143
|
+
|
|
144
|
+
| Code | Meaning |
|
|
145
|
+
|------|--------|
|
|
146
|
+
| 0 | Grade meets or exceeds `fail_on` threshold |
|
|
147
|
+
| 1 | Grade worse than `fail_on`, or build failed |
|
|
148
|
+
| 2 | Configuration error (no package.json, invalid YAML, etc.) |
|
|
149
|
+
|
|
150
|
+
## GitHub Action
|
|
151
|
+
|
|
152
|
+
```yaml
|
|
153
|
+
- uses: psungmin24/laxy-verify@v1
|
|
154
|
+
with:
|
|
155
|
+
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
156
|
+
fail-on: silver # optional, default: bronze
|
|
157
|
+
config: .laxy.yml # optional
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Posts a PR comment with grade and Lighthouse scores, and sets a commit status check.
|
|
161
|
+
|
|
162
|
+
For Pro/Pro+ features in CI, add `LAXY_TOKEN` to your repository secrets:
|
|
163
|
+
|
|
164
|
+
```yaml
|
|
165
|
+
- uses: psungmin24/laxy-verify@v1
|
|
166
|
+
with:
|
|
167
|
+
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
168
|
+
fail-on: gold # Gold requires Pro+ account
|
|
169
|
+
env:
|
|
170
|
+
LAXY_TOKEN: ${{ secrets.LAXY_TOKEN }}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Limitations (v1)
|
|
174
|
+
|
|
175
|
+
- **Monorepos:** Not supported. Run `npx laxy-verify apps/web` for the app subdirectory.
|
|
176
|
+
- **Lighthouse accuracy:** Scores are measured in dev mode (`npm run dev`). Production scores are typically higher.
|
|
177
|
+
- **Fork PRs:** Comments and status checks are not posted on PRs from forks (GitHub security restriction).
|
|
178
|
+
- **Yarn Berry:** May require manual `package_manager` configuration if using Corepack.
|
|
179
|
+
- **Gold grade:** Requires Pro+ plan and `--multi-viewport` flag. All three viewports must pass Lighthouse thresholds.
|
|
180
|
+
|
|
181
|
+
## Expected CI Timing
|
|
182
|
+
|
|
183
|
+
- Build: 15-30s
|
|
184
|
+
- Dev server start: 5-20s
|
|
185
|
+
- Lighthouse (1 run): ~15s
|
|
186
|
+
- Lighthouse (3 runs CI): ~45s
|
|
187
|
+
- Multi-viewport (Pro+, 3 viewports): ~60s
|
|
188
|
+
- **Total: 35-95s per PR**
|
|
189
|
+
|
|
190
|
+
## .laxy-result.json
|
|
191
|
+
|
|
192
|
+
Written after every run:
|
|
193
|
+
|
|
194
|
+
```json
|
|
195
|
+
{
|
|
196
|
+
"grade": "Silver",
|
|
197
|
+
"timestamp": "2026-04-07T10:30:00Z",
|
|
198
|
+
"build": { "success": true, "durationMs": 12300, "errors": [] },
|
|
199
|
+
"lighthouse": { "performance": 87, "accessibility": 92, "seo": 88, "bestPractices": 90, "runs": 1 },
|
|
200
|
+
"thresholds": { "performance": 70, "accessibility": 85, "seo": 80, "bestPractices": 80 },
|
|
201
|
+
"ciMode": false,
|
|
202
|
+
"framework": "nextjs",
|
|
203
|
+
"exitCode": 0,
|
|
204
|
+
"config_fail_on": "bronze"
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Use `npx laxy-verify --badge` to output a shields.io badge markdown from this file.
|
|
209
|
+
|
|
210
|
+
## Badge
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
npx laxy-verify --badge
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Output: ``
|
|
217
|
+
|
|
218
|
+
## License
|
|
219
|
+
|
|
220
|
+
MIT
|
|
221
|
+
|
|
222
|
+
|
|
11
223
|
## Quick Start
|
|
12
224
|
|
|
13
225
|
### 1. Initialize
|
package/dist/auth.js
CHANGED
|
@@ -52,7 +52,7 @@ const os = __importStar(require("node:os"));
|
|
|
52
52
|
const readline = __importStar(require("node:readline"));
|
|
53
53
|
const CREDENTIALS_DIR = path.join(os.homedir(), ".laxy");
|
|
54
54
|
const CREDENTIALS_PATH = path.join(CREDENTIALS_DIR, "credentials.json");
|
|
55
|
-
exports.LAXY_API_URL = process.env.LAXY_API_URL ?? "https://laxy.
|
|
55
|
+
exports.LAXY_API_URL = process.env.LAXY_API_URL ?? "https://laxy-blue.vercel.app";
|
|
56
56
|
function loadToken() {
|
|
57
57
|
// 환경변수 우선
|
|
58
58
|
const envToken = process.env.LAXY_TOKEN;
|
|
@@ -121,48 +121,48 @@ function whoami() {
|
|
|
121
121
|
console.log(" 인증 정보를 읽을 수 없습니다.");
|
|
122
122
|
}
|
|
123
123
|
}
|
|
124
|
-
/** readline으로
|
|
125
|
-
function
|
|
124
|
+
/** readline으로 한 줄 입력 */
|
|
125
|
+
function askQuestion(prompt, mute = false) {
|
|
126
126
|
return new Promise((resolve) => {
|
|
127
127
|
const rl = readline.createInterface({
|
|
128
128
|
input: process.stdin,
|
|
129
|
-
output: process.stdout,
|
|
130
|
-
terminal:
|
|
129
|
+
output: mute ? undefined : process.stdout,
|
|
130
|
+
terminal: false,
|
|
131
131
|
});
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if (process.stdin.isTTY) {
|
|
135
|
-
// @ts-ignore — private API
|
|
136
|
-
process.stdin._handle?.setRawMode?.(true);
|
|
132
|
+
if (mute) {
|
|
133
|
+
process.stdout.write(prompt);
|
|
137
134
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
if (
|
|
135
|
+
// readline.question 대신 line 이벤트 사용 (Windows 호환성)
|
|
136
|
+
let answered = false;
|
|
137
|
+
rl.question(mute ? "" : prompt, (ans) => {
|
|
138
|
+
if (answered)
|
|
139
|
+
return;
|
|
140
|
+
answered = true;
|
|
141
|
+
if (mute)
|
|
142
142
|
process.stdout.write("\n");
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
else if (char === "\u0008" || char === "\u007f") {
|
|
147
|
-
password = password.slice(0, -1);
|
|
148
|
-
}
|
|
149
|
-
else {
|
|
150
|
-
password += char;
|
|
151
|
-
}
|
|
143
|
+
rl.close();
|
|
144
|
+
rl.removeAllListeners();
|
|
145
|
+
resolve(ans.trim());
|
|
152
146
|
});
|
|
147
|
+
// 비밀번호 모드에서 라인 이벤트 수동 처리 (일부 Windows 터미널 호환)
|
|
148
|
+
if (mute) {
|
|
149
|
+
rl.on("line", (line) => {
|
|
150
|
+
if (answered)
|
|
151
|
+
return;
|
|
152
|
+
answered = true;
|
|
153
|
+
process.stdout.write("\n");
|
|
154
|
+
rl.close();
|
|
155
|
+
rl.removeAllListeners();
|
|
156
|
+
resolve(line.trim());
|
|
157
|
+
});
|
|
158
|
+
}
|
|
153
159
|
});
|
|
154
160
|
}
|
|
155
161
|
async function login(emailArg) {
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
return Promise.resolve(emailArg);
|
|
161
|
-
}
|
|
162
|
-
return new Promise((res) => rl.question(" 이메일: ", (ans) => { rl.close(); res(ans.trim()); }));
|
|
163
|
-
};
|
|
164
|
-
const email = await askEmail();
|
|
165
|
-
const password = await promptPassword(" 비밀번호: ");
|
|
162
|
+
const email = emailArg
|
|
163
|
+
? emailArg.trim()
|
|
164
|
+
: await askQuestion(" 이메일: ");
|
|
165
|
+
const password = await askQuestion(" 비밀번호: ", true);
|
|
166
166
|
console.log("\n 로그인 중...");
|
|
167
167
|
let res;
|
|
168
168
|
try {
|
|
@@ -176,6 +176,17 @@ async function login(emailArg) {
|
|
|
176
176
|
console.error(` ❌ 서버에 연결할 수 없습니다. (${exports.LAXY_API_URL})`);
|
|
177
177
|
process.exit(1);
|
|
178
178
|
}
|
|
179
|
+
// HTML(404/500 에러 페이지) 응답 감지
|
|
180
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
181
|
+
if (!contentType.includes("application/json")) {
|
|
182
|
+
const body = await res.text();
|
|
183
|
+
const preview = body.slice(0, 200).replace(/\n/g, " ");
|
|
184
|
+
console.error(` ❌ 서버가 JSON이 아닌 응답을 반환했습니다. (HTTP ${res.status})`);
|
|
185
|
+
console.error(` URL: ${exports.LAXY_API_URL}/api/cli-auth`);
|
|
186
|
+
console.error(` 응답 미리보기: ${preview}`);
|
|
187
|
+
console.error(` 해결 방법: Vercel 환경변수(CLI_JWT_SECRET, SUPABASE_SERVICE_ROLE_KEY)가 설정됐는지 확인하세요.`);
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
179
190
|
const data = (await res.json());
|
|
180
191
|
if (!res.ok || !data.token) {
|
|
181
192
|
console.error(` ❌ ${data.error ?? "로그인에 실패했습니다."}`);
|