laxy-verify 1.1.32 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +322 -322
  2. package/dist/audit/broken-links.d.ts +21 -21
  3. package/dist/audit/broken-links.js +86 -86
  4. package/dist/auth.d.ts +11 -6
  5. package/dist/auth.js +222 -188
  6. package/dist/cli.js +806 -724
  7. package/dist/comment.d.ts +21 -21
  8. package/dist/comment.js +134 -131
  9. package/dist/crawler.d.ts +36 -35
  10. package/dist/crawler.js +357 -356
  11. package/dist/e2e.d.ts +49 -49
  12. package/dist/e2e.js +565 -539
  13. package/dist/entitlement.d.ts +11 -11
  14. package/dist/entitlement.js +90 -88
  15. package/dist/init.js +87 -87
  16. package/dist/multi-viewport.d.ts +31 -31
  17. package/dist/multi-viewport.js +298 -298
  18. package/dist/playwright-runner.d.ts +16 -16
  19. package/dist/playwright-runner.js +208 -208
  20. package/dist/report-markdown.d.ts +39 -39
  21. package/dist/report-markdown.js +386 -386
  22. package/dist/security-audit.d.ts +9 -9
  23. package/dist/security-audit.js +64 -64
  24. package/dist/serve.d.ts +13 -13
  25. package/dist/serve.js +249 -246
  26. package/dist/trend.d.ts +50 -49
  27. package/dist/trend.js +148 -147
  28. package/dist/verification-core/index.d.ts +3 -3
  29. package/dist/verification-core/index.js +19 -19
  30. package/dist/verification-core/report.d.ts +14 -14
  31. package/dist/verification-core/report.js +409 -404
  32. package/dist/verification-core/tier-policy.d.ts +13 -13
  33. package/dist/verification-core/tier-policy.js +60 -60
  34. package/dist/verification-core/types.d.ts +108 -108
  35. package/dist/verification-core/types.js +2 -2
  36. package/dist/visual-diff.d.ts +26 -26
  37. package/dist/visual-diff.js +178 -178
  38. package/package.json +67 -67
package/dist/cli.js CHANGED
@@ -1,306 +1,306 @@
1
- #!/usr/bin/env node
2
- "use strict";
3
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
- if (k2 === undefined) k2 = k;
5
- var desc = Object.getOwnPropertyDescriptor(m, k);
6
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
- desc = { enumerable: true, get: function() { return m[k]; } };
8
- }
9
- Object.defineProperty(o, k2, desc);
10
- }) : (function(o, m, k, k2) {
11
- if (k2 === undefined) k2 = k;
12
- o[k2] = m[k];
13
- }));
14
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
- Object.defineProperty(o, "default", { enumerable: true, value: v });
16
- }) : function(o, v) {
17
- o["default"] = v;
18
- });
19
- var __importStar = (this && this.__importStar) || (function () {
20
- var ownKeys = function(o) {
21
- ownKeys = Object.getOwnPropertyNames || function (o) {
22
- var ar = [];
23
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
- return ar;
25
- };
26
- return ownKeys(o);
27
- };
28
- return function (mod) {
29
- if (mod && mod.__esModule) return mod;
30
- var result = {};
31
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
- __setModuleDefault(result, mod);
33
- return result;
34
- };
35
- })();
36
- var __importDefault = (this && this.__importDefault) || function (mod) {
37
- return (mod && mod.__esModule) ? mod : { "default": mod };
38
- };
39
- Object.defineProperty(exports, "__esModule", { value: true });
40
- const fs = __importStar(require("node:fs"));
41
- const path = __importStar(require("node:path"));
42
- const config_js_1 = require("./config.js");
43
- const detect_js_1 = require("./detect.js");
44
- const build_js_1 = require("./build.js");
45
- const serve_js_1 = require("./serve.js");
46
- const lighthouse_js_1 = require("./lighthouse.js");
47
- const grade_js_1 = require("./grade.js");
48
- const init_js_1 = require("./init.js");
49
- const badge_js_1 = require("./badge.js");
50
- const comment_js_1 = require("./comment.js");
51
- const status_js_1 = require("./status.js");
52
- const auth_js_1 = require("./auth.js");
53
- const entitlement_js_1 = require("./entitlement.js");
54
- const multi_viewport_js_1 = require("./multi-viewport.js");
55
- const e2e_js_1 = require("./e2e.js");
56
- const playwright_runner_js_1 = require("./playwright-runner.js");
57
- const report_markdown_js_1 = require("./report-markdown.js");
58
- const visual_diff_js_1 = require("./visual-diff.js");
59
- const security_audit_js_1 = require("./security-audit.js");
60
- const broken_links_js_1 = require("./audit/broken-links.js");
61
- const index_js_1 = require("./verification-core/index.js");
62
- const trend_js_1 = require("./trend.js");
63
- const package_json_1 = __importDefault(require("../package.json"));
64
- let activeDevServerPid;
65
- let activeDevServerCleanup = null;
66
- async function cleanupActiveDevServer() {
67
- if (activeDevServerPid === undefined)
68
- return;
69
- if (activeDevServerCleanup) {
70
- await activeDevServerCleanup;
71
- return;
72
- }
73
- const pid = activeDevServerPid;
74
- activeDevServerPid = undefined;
75
- activeDevServerCleanup = (0, serve_js_1.stopDevServer)(pid).finally(() => {
76
- activeDevServerCleanup = null;
77
- });
78
- await activeDevServerCleanup;
79
- }
80
- function installSignalCleanupHandlers() {
81
- const handleSignal = (signal) => {
82
- void cleanupActiveDevServer().finally(() => {
83
- const exitCode = signal === "SIGINT" ? 130 : 143;
84
- exitGracefully(exitCode);
85
- });
86
- };
87
- process.once("SIGINT", () => handleSignal("SIGINT"));
88
- process.once("SIGTERM", () => handleSignal("SIGTERM"));
89
- }
90
- function shouldFailVerificationResult(report, failOn) {
91
- if (failOn === "unverified")
92
- return false;
93
- if (report.verdict === "build-failed" || report.verdict === "hold")
94
- return true;
95
- if (report.verdict === "investigate")
96
- return true;
97
- return (0, grade_js_1.isWorseOrEqual)(report.grade, failOn);
98
- }
99
- function exitGracefully(code) {
100
- if (process.platform === "win32") {
101
- setTimeout(() => process.exit(code), 100);
102
- return;
103
- }
104
- process.exit(code);
105
- }
106
- async function ensurePortAvailableForVerification(port) {
107
- const status = await (0, serve_js_1.probeServerStatus)(port);
108
- if (status === null)
109
- return;
110
- throw new Error(`An existing local server is already responding on port ${port} (HTTP ${status}). Stop the running app before using laxy-verify, because the verification build can invalidate an active dev session.`);
111
- }
112
- function parseArgs() {
113
- const raw = process.argv.slice(2);
114
- let projectDir = ".";
115
- const flags = {};
116
- for (let i = 0; i < raw.length; i++) {
117
- const arg = raw[i];
118
- if (arg.startsWith("--")) {
119
- const eqIndex = arg.indexOf("=");
120
- if (eqIndex >= 0) {
121
- const key = arg.slice(2, eqIndex);
122
- flags[key] = arg.slice(eqIndex + 1);
123
- }
124
- else {
125
- const key = arg.slice(2);
126
- if (i + 1 < raw.length && !raw[i + 1].startsWith("-")) {
127
- flags[key] = raw[++i];
128
- }
129
- else {
130
- flags[key] = "true";
131
- }
132
- }
133
- }
134
- else if (projectDir === ".") {
135
- projectDir = arg;
136
- }
137
- }
138
- let subcommand;
139
- let subcommandArg;
140
- if (projectDir === "login" || projectDir === "logout" || projectDir === "whoami") {
141
- subcommand = projectDir;
142
- projectDir = ".";
143
- subcommandArg = flags.email;
144
- }
145
- return {
146
- projectDir: path.resolve(projectDir),
147
- subcommand,
148
- subcommandArg,
149
- format: flags.format ?? "console",
150
- ciMode: flags.ci !== undefined || process.env.CI === "true",
151
- configPath: flags.config,
152
- failOn: flags["fail-on"] ?? undefined,
153
- skipLighthouse: flags["skip-lighthouse"] !== undefined,
154
- badge: flags.badge !== undefined,
155
- init: flags.init !== undefined,
156
- initRun: flags.init !== undefined && flags.run !== undefined,
157
- multiViewport: flags["multi-viewport"] !== undefined,
158
- failureAnalysis: flags["failure-analysis"] !== undefined,
159
- crawl: flags.crawl !== undefined,
160
- planOverride: flags["plan-override"],
161
- help: flags.help !== undefined || flags.h !== undefined,
162
- };
163
- }
164
- function writeResultFile(projectDir, result) {
165
- const filePath = path.join(projectDir, ".laxy-result.json");
166
- fs.writeFileSync(filePath, JSON.stringify(result, null, 2) + "\n", "utf-8");
167
- }
168
- function summarizeViewportIssues(scores, thresholds) {
169
- if (!scores)
170
- return { count: 0 };
171
- const failed = [];
172
- for (const [label, viewportScores] of Object.entries(scores)) {
173
- if (!viewportScores) {
174
- failed.push(`${label}: missing`);
175
- continue;
176
- }
177
- const passes = viewportScores.performance >= thresholds.performance &&
178
- viewportScores.accessibility >= thresholds.accessibility &&
179
- viewportScores.seo >= thresholds.seo &&
180
- viewportScores.bestPractices >= thresholds.bestPractices;
181
- if (!passes) {
182
- failed.push(`${label}: P${viewportScores.performance} A${viewportScores.accessibility} SEO${viewportScores.seo} BP${viewportScores.bestPractices}`);
183
- }
184
- }
185
- return {
186
- count: failed.length,
187
- summary: failed.length > 0 ? failed.join(" | ") : "All checked viewports passed.",
188
- };
189
- }
190
- function consoleOutput(result) {
191
- const gradeLabel = result.grade;
192
- const checkEmoji = result.grade !== "Unverified" ? " OK" : "";
193
- console.log(`\n Laxy Verify: blocker check ${gradeLabel}${checkEmoji}`);
194
- console.log(` Build: ${result.build.success ? `OK (${result.build.durationMs}ms)` : "FAILED"}`);
195
- if (result.build.errors.length > 0) {
196
- console.log(" Errors:");
197
- const firstError = result.build.errors.find((line) => /error/i.test(line));
198
- const last5 = result.build.errors.slice(-5);
199
- const toShow = firstError && !last5.includes(firstError)
200
- ? [firstError, " ...", ...last5]
201
- : last5;
202
- for (const line of toShow)
203
- console.error(` ${line}`);
204
- }
205
- if (result.lighthouse !== null) {
206
- const lh = result.lighthouse;
207
- const t = result.thresholds;
208
- const check = (passed) => (passed ? " OK" : " FAIL");
209
- console.log(" Lighthouse:");
210
- console.log(` Performance: ${lh.performance} / ${t.performance}${check(lh.performance >= t.performance)}`);
211
- console.log(` Accessibility: ${lh.accessibility} / ${t.accessibility}${check(lh.accessibility >= t.accessibility)}`);
212
- console.log(` SEO: ${lh.seo} / ${t.seo}${check(lh.seo >= t.seo)}`);
213
- console.log(` Best Practices: ${lh.bestPractices} / ${t.bestPractices}${check(lh.bestPractices >= t.bestPractices)}`);
214
- console.log(` Runs: ${lh.runs}`);
215
- }
216
- else {
217
- console.log(" Lighthouse: skipped");
218
- }
219
- if (result.e2e) {
220
- console.log(` E2E: ${result.e2e.passed}/${result.e2e.total} passed`);
221
- }
222
- if (result.crossBrowser && result.crossBrowser.length > 0) {
223
- console.log(" Cross-browser:");
224
- for (const cbr of result.crossBrowser) {
225
- const status = cbr.failed === 0 ? "OK" : "FAIL";
226
- console.log(` ${cbr.browser}: ${cbr.passed}/${cbr.results.length} passed ${status}`);
227
- }
228
- }
229
- if (result.visualDiff) {
230
- console.log(` Visual diff: ${(0, visual_diff_js_1.formatVisualDiffSummary)(result.visualDiff)}`);
231
- }
232
- if (result.security) {
233
- console.log(` Security: ${result.security.summary}`);
234
- }
235
- if (result.mobileLighthouse) {
236
- const ml = result.mobileLighthouse;
237
- console.log(` Mobile LH: P=${ml.performance} A=${ml.accessibility} SEO=${ml.seo} BP=${ml.bestPractices}`);
238
- }
239
- if (result.verification) {
240
- const view = result.verification.view;
241
- const verboseFailure = result._verbose_failure ?? true;
242
- const failureAnalysis = result._failure_analysis ?? true;
243
- console.log(` Verification depth: ${view.tier}`);
244
- console.log(` Decision question: ${view.question}`);
245
- console.log(` Decision: ${view.verdict} (${view.confidence})`);
246
- console.log(` Why it stopped here: ${view.summary}`);
247
- // Passed/Failed checks summary
248
- if (view.passes.length > 0) {
249
- const passedChecks = view.passes.filter((p) => p.passed).map((p) => p.label);
250
- const failedChecks = view.passes.filter((p) => !p.passed).map((p) => p.label);
251
- if (passedChecks.length > 0)
252
- console.log(` Passed: ${passedChecks.join(", ")}`);
253
- if (failedChecks.length > 0)
254
- console.log(` Failed: ${failedChecks.join(", ")}`);
255
- }
256
- // Blockers: ?�목?� 모든 ?�어, Fix ?�션?� verbose_failure(Pro+) ?�상?�서 ?�시
257
- if (view.blockers.length > 0) {
258
- console.log(" Deployment blockers:");
259
- for (const blocker of view.blockers) {
260
- console.log(` - ${blocker.title}`);
261
- if (verboseFailure)
262
- console.log(` Fix: ${blocker.action}`);
263
- }
264
- }
265
- // Warnings: always show all
266
- if (view.warnings.length > 0) {
267
- console.log(" Risks to review:");
268
- for (const warning of view.warnings) {
269
- console.log(` - ${warning.title}`);
270
- if (failureAnalysis)
271
- console.log(` Review: ${warning.action}`);
272
- }
273
- }
274
- if (view.nextActions.length > 0) {
275
- console.log(" Next actions:");
276
- for (const action of view.nextActions)
277
- console.log(` - ${action}`);
278
- }
279
- // Evidence: failure_analysis(Pro+)???�체, verbose_failure(Pro)??3�? Free??2�?
280
- const evidenceLimit = failureAnalysis ? view.failureEvidence.length : verboseFailure ? 3 : 2;
281
- const evidenceToShow = view.failureEvidence.slice(0, evidenceLimit);
282
- if (evidenceToShow.length > 0) {
283
- console.log(" Evidence collected:");
284
- for (const item of evidenceToShow)
285
- console.log(` - ${item}`);
286
- }
287
- }
288
- if (result.github) {
289
- if (result.github.status === "comment_posted")
290
- console.log(" PR comment: posted");
291
- if (result.github.status === "status_set")
292
- console.log(` Status check: ${result.github.grade}`);
293
- }
294
- console.log(" Result: .laxy-result.json");
295
- if (result.markdownReportPath) {
296
- console.log(` Report: ${path.basename(result.markdownReportPath)}`);
297
- }
298
- console.log(` Exit code: ${result.exitCode}`);
299
- }
300
- async function run() {
301
- installSignalCleanupHandlers();
302
- const args = parseArgs();
303
- if (args.help) {
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ var __importDefault = (this && this.__importDefault) || function (mod) {
37
+ return (mod && mod.__esModule) ? mod : { "default": mod };
38
+ };
39
+ Object.defineProperty(exports, "__esModule", { value: true });
40
+ const fs = __importStar(require("node:fs"));
41
+ const path = __importStar(require("node:path"));
42
+ const config_js_1 = require("./config.js");
43
+ const detect_js_1 = require("./detect.js");
44
+ const build_js_1 = require("./build.js");
45
+ const serve_js_1 = require("./serve.js");
46
+ const lighthouse_js_1 = require("./lighthouse.js");
47
+ const grade_js_1 = require("./grade.js");
48
+ const init_js_1 = require("./init.js");
49
+ const badge_js_1 = require("./badge.js");
50
+ const comment_js_1 = require("./comment.js");
51
+ const status_js_1 = require("./status.js");
52
+ const auth_js_1 = require("./auth.js");
53
+ const entitlement_js_1 = require("./entitlement.js");
54
+ const multi_viewport_js_1 = require("./multi-viewport.js");
55
+ const e2e_js_1 = require("./e2e.js");
56
+ const playwright_runner_js_1 = require("./playwright-runner.js");
57
+ const report_markdown_js_1 = require("./report-markdown.js");
58
+ const visual_diff_js_1 = require("./visual-diff.js");
59
+ const security_audit_js_1 = require("./security-audit.js");
60
+ const broken_links_js_1 = require("./audit/broken-links.js");
61
+ const index_js_1 = require("./verification-core/index.js");
62
+ const trend_js_1 = require("./trend.js");
63
+ const package_json_1 = __importDefault(require("../package.json"));
64
+ let activeDevServerPid;
65
+ let activeDevServerCleanup = null;
66
+ async function cleanupActiveDevServer() {
67
+ if (activeDevServerPid === undefined)
68
+ return;
69
+ if (activeDevServerCleanup) {
70
+ await activeDevServerCleanup;
71
+ return;
72
+ }
73
+ const pid = activeDevServerPid;
74
+ activeDevServerPid = undefined;
75
+ activeDevServerCleanup = (0, serve_js_1.stopDevServer)(pid).finally(() => {
76
+ activeDevServerCleanup = null;
77
+ });
78
+ await activeDevServerCleanup;
79
+ }
80
+ function installSignalCleanupHandlers() {
81
+ const handleSignal = (signal) => {
82
+ void cleanupActiveDevServer().finally(() => {
83
+ const exitCode = signal === "SIGINT" ? 130 : 143;
84
+ exitGracefully(exitCode);
85
+ });
86
+ };
87
+ process.once("SIGINT", () => handleSignal("SIGINT"));
88
+ process.once("SIGTERM", () => handleSignal("SIGTERM"));
89
+ }
90
+ function shouldFailVerificationResult(report, failOn) {
91
+ if (failOn === "unverified")
92
+ return false;
93
+ if (report.verdict === "build-failed" || report.verdict === "hold")
94
+ return true;
95
+ if (report.verdict === "investigate")
96
+ return true;
97
+ return (0, grade_js_1.isWorseOrEqual)(report.grade, failOn);
98
+ }
99
+ function exitGracefully(code) {
100
+ if (process.platform === "win32") {
101
+ setTimeout(() => process.exit(code), 100);
102
+ return;
103
+ }
104
+ process.exit(code);
105
+ }
106
+ async function ensurePortAvailableForVerification(port) {
107
+ const status = await (0, serve_js_1.probeServerStatus)(port);
108
+ if (status === null)
109
+ return;
110
+ throw new Error(`An existing local server is already responding on port ${port} (HTTP ${status}). Stop the running app before using laxy-verify, because the verification build can invalidate an active dev session.`);
111
+ }
112
+ function parseArgs() {
113
+ const raw = process.argv.slice(2);
114
+ let projectDir = ".";
115
+ const flags = {};
116
+ for (let i = 0; i < raw.length; i++) {
117
+ const arg = raw[i];
118
+ if (arg.startsWith("--")) {
119
+ const eqIndex = arg.indexOf("=");
120
+ if (eqIndex >= 0) {
121
+ const key = arg.slice(2, eqIndex);
122
+ flags[key] = arg.slice(eqIndex + 1);
123
+ }
124
+ else {
125
+ const key = arg.slice(2);
126
+ if (i + 1 < raw.length && !raw[i + 1].startsWith("-")) {
127
+ flags[key] = raw[++i];
128
+ }
129
+ else {
130
+ flags[key] = "true";
131
+ }
132
+ }
133
+ }
134
+ else if (projectDir === ".") {
135
+ projectDir = arg;
136
+ }
137
+ }
138
+ let subcommand;
139
+ let subcommandArg;
140
+ if (projectDir === "login" || projectDir === "logout" || projectDir === "whoami") {
141
+ subcommand = projectDir;
142
+ projectDir = ".";
143
+ subcommandArg = flags.email;
144
+ }
145
+ return {
146
+ projectDir: path.resolve(projectDir),
147
+ subcommand,
148
+ subcommandArg,
149
+ format: flags.format ?? "console",
150
+ ciMode: flags.ci !== undefined || process.env.CI === "true",
151
+ configPath: flags.config,
152
+ failOn: flags["fail-on"] ?? undefined,
153
+ skipLighthouse: flags["skip-lighthouse"] !== undefined,
154
+ badge: flags.badge !== undefined,
155
+ init: flags.init !== undefined,
156
+ initRun: flags.init !== undefined && flags.run !== undefined,
157
+ multiViewport: flags["multi-viewport"] !== undefined,
158
+ failureAnalysis: flags["failure-analysis"] !== undefined,
159
+ crawl: flags.crawl !== undefined,
160
+ planOverride: flags["plan-override"],
161
+ help: flags.help !== undefined || flags.h !== undefined,
162
+ };
163
+ }
164
+ function writeResultFile(projectDir, result) {
165
+ const filePath = path.join(projectDir, ".laxy-result.json");
166
+ fs.writeFileSync(filePath, JSON.stringify(result, null, 2) + "\n", "utf-8");
167
+ }
168
+ function summarizeViewportIssues(scores, thresholds) {
169
+ if (!scores)
170
+ return { count: 0 };
171
+ const failed = [];
172
+ for (const [label, viewportScores] of Object.entries(scores)) {
173
+ if (!viewportScores) {
174
+ failed.push(`${label}: missing`);
175
+ continue;
176
+ }
177
+ const passes = viewportScores.performance >= thresholds.performance &&
178
+ viewportScores.accessibility >= thresholds.accessibility &&
179
+ viewportScores.seo >= thresholds.seo &&
180
+ viewportScores.bestPractices >= thresholds.bestPractices;
181
+ if (!passes) {
182
+ failed.push(`${label}: P${viewportScores.performance} A${viewportScores.accessibility} SEO${viewportScores.seo} BP${viewportScores.bestPractices}`);
183
+ }
184
+ }
185
+ return {
186
+ count: failed.length,
187
+ summary: failed.length > 0 ? failed.join(" | ") : "All checked viewports passed.",
188
+ };
189
+ }
190
+ function consoleOutput(result) {
191
+ const gradeLabel = result.grade;
192
+ const checkEmoji = result.grade !== "Unverified" ? " OK" : "";
193
+ console.log(`\n Laxy Verify: blocker check ${gradeLabel}${checkEmoji}`);
194
+ console.log(` Build: ${result.build.success ? `OK (${result.build.durationMs}ms)` : "FAILED"}`);
195
+ if (result.build.errors.length > 0) {
196
+ console.log(" Errors:");
197
+ const firstError = result.build.errors.find((line) => /error/i.test(line));
198
+ const last5 = result.build.errors.slice(-5);
199
+ const toShow = firstError && !last5.includes(firstError)
200
+ ? [firstError, " ...", ...last5]
201
+ : last5;
202
+ for (const line of toShow)
203
+ console.error(` ${line}`);
204
+ }
205
+ if (result.lighthouse !== null) {
206
+ const lh = result.lighthouse;
207
+ const t = result.thresholds;
208
+ const check = (passed) => (passed ? " OK" : " FAIL");
209
+ console.log(" Lighthouse:");
210
+ console.log(` Performance: ${lh.performance} / ${t.performance}${check(lh.performance >= t.performance)}`);
211
+ console.log(` Accessibility: ${lh.accessibility} / ${t.accessibility}${check(lh.accessibility >= t.accessibility)}`);
212
+ console.log(` SEO: ${lh.seo} / ${t.seo}${check(lh.seo >= t.seo)}`);
213
+ console.log(` Best Practices: ${lh.bestPractices} / ${t.bestPractices}${check(lh.bestPractices >= t.bestPractices)}`);
214
+ console.log(` Runs: ${lh.runs}`);
215
+ }
216
+ else {
217
+ console.log(" Lighthouse: skipped");
218
+ }
219
+ if (result.e2e) {
220
+ console.log(` E2E: ${result.e2e.passed}/${result.e2e.total} passed`);
221
+ }
222
+ if (result.crossBrowser && result.crossBrowser.length > 0) {
223
+ console.log(" Cross-browser:");
224
+ for (const cbr of result.crossBrowser) {
225
+ const status = cbr.failed === 0 ? "OK" : "FAIL";
226
+ console.log(` ${cbr.browser}: ${cbr.passed}/${cbr.results.length} passed ${status}`);
227
+ }
228
+ }
229
+ if (result.visualDiff) {
230
+ console.log(` Visual diff: ${(0, visual_diff_js_1.formatVisualDiffSummary)(result.visualDiff)}`);
231
+ }
232
+ if (result.security) {
233
+ console.log(` Security: ${result.security.summary}`);
234
+ }
235
+ if (result.mobileLighthouse) {
236
+ const ml = result.mobileLighthouse;
237
+ console.log(` Mobile LH: P=${ml.performance} A=${ml.accessibility} SEO=${ml.seo} BP=${ml.bestPractices}`);
238
+ }
239
+ if (result.verification) {
240
+ const view = result.verification.view;
241
+ const verboseFailure = result._verbose_failure ?? true;
242
+ const failureAnalysis = result._failure_analysis ?? true;
243
+ console.log(` Verification depth: ${view.tier}`);
244
+ console.log(` Decision question: ${view.question}`);
245
+ console.log(` Decision: ${view.verdict} (${view.confidence})`);
246
+ console.log(` Why it stopped here: ${view.summary}`);
247
+ // Passed/Failed checks summary
248
+ if (view.passes.length > 0) {
249
+ const passedChecks = view.passes.filter((p) => p.passed).map((p) => p.label);
250
+ const failedChecks = view.passes.filter((p) => !p.passed).map((p) => p.label);
251
+ if (passedChecks.length > 0)
252
+ console.log(` Passed: ${passedChecks.join(", ")}`);
253
+ if (failedChecks.length > 0)
254
+ console.log(` Failed: ${failedChecks.join(", ")}`);
255
+ }
256
+ // Blockers: ?�목?� 모든 ?�어, Fix ?�션?� verbose_failure(Pro+) ?�상?�서 ?�시
257
+ if (view.blockers.length > 0) {
258
+ console.log(" Deployment blockers:");
259
+ for (const blocker of view.blockers) {
260
+ console.log(` - ${blocker.title}`);
261
+ if (verboseFailure)
262
+ console.log(` Fix: ${blocker.action}`);
263
+ }
264
+ }
265
+ // Warnings: always show all
266
+ if (view.warnings.length > 0) {
267
+ console.log(" Risks to review:");
268
+ for (const warning of view.warnings) {
269
+ console.log(` - ${warning.title}`);
270
+ if (failureAnalysis)
271
+ console.log(` Review: ${warning.action}`);
272
+ }
273
+ }
274
+ if (view.nextActions.length > 0) {
275
+ console.log(" Next actions:");
276
+ for (const action of view.nextActions)
277
+ console.log(` - ${action}`);
278
+ }
279
+ // Evidence: failure_analysis(Pro+)???�체, verbose_failure(Pro)??3�? Free??2�?
280
+ const evidenceLimit = failureAnalysis ? view.failureEvidence.length : verboseFailure ? 3 : 2;
281
+ const evidenceToShow = view.failureEvidence.slice(0, evidenceLimit);
282
+ if (evidenceToShow.length > 0) {
283
+ console.log(" Evidence collected:");
284
+ for (const item of evidenceToShow)
285
+ console.log(` - ${item}`);
286
+ }
287
+ }
288
+ if (result.github) {
289
+ if (result.github.status === "comment_posted")
290
+ console.log(" PR comment: posted");
291
+ if (result.github.status === "status_set")
292
+ console.log(` Status check: ${result.github.grade}`);
293
+ }
294
+ console.log(" Result: .laxy-result.json");
295
+ if (result.markdownReportPath) {
296
+ console.log(` Report: ${path.basename(result.markdownReportPath)}`);
297
+ }
298
+ console.log(` Exit code: ${result.exitCode}`);
299
+ }
300
+ async function run() {
301
+ installSignalCleanupHandlers();
302
+ const args = parseArgs();
303
+ if (args.help) {
304
304
  console.log(`
305
305
  laxy-verify v${package_json_1.default.version}
306
306
  Deployment blocker gate for frontend apps
@@ -340,424 +340,506 @@ async function run() {
340
340
  npx laxy-verify . --fail-on silver # Block Bronze or worse
341
341
 
342
342
  Docs: https://github.com/SUNgm24/Laxy/tree/main/laxy-verify
343
- `);
344
- exitGracefully(0);
345
- return;
346
- }
347
- if (args.subcommand === "login") {
348
- await (0, auth_js_1.login)(args.subcommandArg);
349
- exitGracefully(0);
350
- return;
351
- }
352
- if (args.subcommand === "logout") {
353
- (0, auth_js_1.clearToken)();
354
- exitGracefully(0);
355
- return;
356
- }
357
- if (args.subcommand === "whoami") {
358
- (0, auth_js_1.whoami)();
359
- exitGracefully(0);
360
- return;
361
- }
362
- if (args.init) {
363
- (0, init_js_1.runInit)(args.projectDir);
364
- if (!args.initRun) {
365
- console.log("\n Next step: run npx laxy-verify . (or use --init --run to continue immediately)");
366
- exitGracefully(0);
367
- return;
368
- }
369
- console.log("\n Config created. Starting verification...\n");
370
- }
371
- if (args.badge) {
372
- const resultPath = path.join(args.projectDir, ".laxy-result.json");
373
- if (!fs.existsSync(resultPath)) {
374
- console.error("Error: .laxy-result.json not found. Run `npx laxy-verify .` first to generate it.");
375
- exitGracefully(2);
376
- return;
377
- }
378
- const content = JSON.parse(fs.readFileSync(resultPath, "utf-8"));
379
- const badge = (0, badge_js_1.generateBadge)(content.grade);
380
- console.log(badge);
381
- exitGracefully(0);
382
- return;
383
- }
384
- let config;
385
- try {
386
- config = (0, config_js_1.loadConfig)({
387
- dir: args.projectDir,
388
- configPath: args.configPath,
389
- ciMode: args.ciMode,
390
- cliFlags: {
391
- failOn: args.failOn,
392
- skipLighthouse: args.skipLighthouse,
393
- },
394
- });
395
- }
396
- catch (err) {
397
- console.error(`Config error: ${err instanceof Error ? err.message : String(err)}`);
398
- exitGracefully(2);
399
- return;
400
- }
401
- let detected;
402
- try {
403
- detected = (0, detect_js_1.detect)(args.projectDir);
404
- }
405
- catch (err) {
406
- console.error(`Detection error: ${err instanceof Error ? err.message : String(err)}`);
407
- exitGracefully(2);
408
- return;
409
- }
410
- const buildCmd = config.build_command || detected.buildCmd;
411
- const devCmd = config.dev_command || detected.devCmd;
412
- const port = config.port;
413
- try {
414
- await ensurePortAvailableForVerification(port);
415
- }
416
- catch (err) {
417
- console.error(`Preflight error: ${err instanceof Error ? err.message : String(err)}`);
418
- exitGracefully(2);
419
- return;
420
- }
421
- let buildResult;
422
- try {
423
- buildResult = await (0, build_js_1.runBuild)(buildCmd, config.build_timeout, args.projectDir);
424
- }
425
- catch (err) {
426
- buildResult = {
427
- success: false,
428
- durationMs: 0,
429
- errors: err instanceof Error ? [err.message] : [String(err)],
430
- };
431
- }
432
- let scores;
433
- let lighthouseResult = null;
434
- let lighthouseErrorCount = 0;
435
- const adjustedThresholds = {
436
- performance: config.ciMode ? config.thresholds.performance - 10 : config.thresholds.performance,
437
- accessibility: config.thresholds.accessibility,
438
- seo: config.thresholds.seo,
439
- bestPractices: config.thresholds.bestPractices,
440
- };
441
- let entitlements = null;
442
- try {
443
- entitlements = await (0, entitlement_js_1.getEntitlements)();
444
- (0, entitlement_js_1.printPlanBanner)(entitlements);
445
- }
446
- catch {
447
- // Ignore entitlement errors and keep the free feature set.
448
- }
449
- const features = entitlements ?? {
450
- plan: "free",
451
- // All verification features run on every plan
452
- // Automation features ??not available on Free
453
- github_actions: false,
454
- queue_priority: false,
455
- parallel_execution: false,
456
- };
457
- let effectiveFeatures = features;
458
- if (args.planOverride) {
459
- try {
460
- effectiveFeatures = (0, entitlement_js_1.applyPlanOverride)(features, args.planOverride);
461
- console.log(` Plan override: ${(0, entitlement_js_1.normalizePlan)(features.plan)} -> ${effectiveFeatures.plan} (verification behavior is unchanged)`);
462
- }
463
- catch (overrideErr) {
464
- console.error(`Plan override error: ${overrideErr instanceof Error ? overrideErr.message : String(overrideErr)}`);
465
- exitGracefully(2);
466
- return;
467
- }
468
- }
469
- // Always run 3 Lighthouse passes for median score
470
- if (config.lighthouse_runs < 3) {
471
- config = { ...config, lighthouse_runs: 3 };
472
- }
473
- let multiViewportScores = null;
474
- let allViewportsOk = false;
475
- let e2eResult;
476
- let crossBrowserResults;
477
- let e2eCoverageGaps = [];
478
- let e2eConsoleErrors = [];
479
- let e2eStabilityPassed = true;
480
- let visualDiffResult = null;
481
- let securityAuditResult = null;
482
- let mobileLighthouseScores = null;
483
- let brokenLinksResult = null;
484
- const failureEvidence = [];
485
- if (buildResult.success) {
486
- let servePid;
487
- 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;
491
- const verifyUrl = `http://127.0.0.1:${port}/`;
492
- const verificationTier = (0, index_js_1.planToVerificationTier)(effectiveFeatures.plan);
493
- if (!args.skipLighthouse) {
494
- try {
495
- const lhResult = await (0, lighthouse_js_1.runLighthouse)(port, config.lighthouse_runs);
496
- lighthouseErrorCount = lhResult.errors.length;
497
- scores = lhResult.scores ?? undefined;
498
- if (scores) {
499
- lighthouseResult = {
500
- performance: scores.performance,
501
- accessibility: scores.accessibility,
502
- seo: scores.seo,
503
- bestPractices: scores.bestPractices,
504
- runs: config.lighthouse_runs,
505
- };
506
- }
507
- }
508
- catch (lhErr) {
509
- console.error(`Lighthouse error: ${lhErr instanceof Error ? lhErr.message : String(lhErr)}`);
510
- }
511
- }
512
- if (!args.skipLighthouse) {
513
- try {
514
- multiViewportScores = await (0, multi_viewport_js_1.runMultiViewportLighthouse)(port);
515
- (0, multi_viewport_js_1.printMultiViewportResults)(multiViewportScores, adjustedThresholds);
516
- allViewportsOk = (0, multi_viewport_js_1.allViewportsPass)(multiViewportScores, adjustedThresholds);
517
- if (multiViewportScores.screenshotDiffs) {
518
- for (const diff of multiViewportScores.screenshotDiffs) {
519
- if (!diff.baselineCreated && diff.diffPercent > 10) {
520
- failureEvidence.push(`Viewport screenshot: ${diff.viewport} diff ${diff.diffPercent}% exceeds 10% threshold`);
521
- }
522
- }
523
- }
524
- }
525
- catch (mvErr) {
526
- console.error(`Multi-viewport error: ${mvErr instanceof Error ? mvErr.message : String(mvErr)}`);
527
- }
528
- }
529
- // Security audit (npm audit)
530
- try {
531
- securityAuditResult = await (0, security_audit_js_1.runSecurityAudit)(args.projectDir);
532
- if (securityAuditResult.critical > 0 || securityAuditResult.high > 0) {
533
- failureEvidence.push(`Security: ${securityAuditResult.summary}`);
534
- }
535
- }
536
- catch (secErr) {
537
- console.error(`Security audit error: ${secErr instanceof Error ? secErr.message : String(secErr)}`);
538
- }
539
- const crawlEnabled = args.crawl || config.crawl;
540
- const crawlOpts = crawlEnabled
541
- ? { enabled: true, maxDepth: config.max_crawl_depth, maxPages: config.max_crawl_pages }
542
- : undefined;
543
- let lastE2EScenarios;
544
- try {
545
- const e2eRuns = await (0, e2e_js_1.runVerifyE2E)(verifyUrl, verificationTier, config.scenarios, crawlOpts);
546
- lastE2EScenarios = e2eRuns.scenarios;
547
- e2eResult = {
548
- passed: e2eRuns.passed,
549
- failed: e2eRuns.failed,
550
- total: e2eRuns.results.length,
551
- results: e2eRuns.results,
552
- };
553
- e2eCoverageGaps = e2eRuns.coverageGaps;
554
- e2eConsoleErrors = e2eRuns.consoleErrors;
555
- // Broken links audit ??runs after E2E so we have the crawl result
556
- if (e2eRuns.crawlResult && e2eRuns.crawlResult.totalLinks > 0) {
557
- try {
558
- brokenLinksResult = await (0, broken_links_js_1.auditBrokenLinks)(e2eRuns.crawlResult, verifyUrl);
559
- if (brokenLinksResult.hasBrokenLinks) {
560
- failureEvidence.push(`Broken links: ${brokenLinksResult.summary}`);
561
- }
562
- }
563
- catch (blErr) {
564
- console.error(`Broken links audit error: ${blErr instanceof Error ? blErr.message : String(blErr)}`);
565
- }
566
- }
567
- // E2E stability: run a second time if first run passed all
568
- if (e2eRuns.passed === e2eRuns.results.length && e2eRuns.results.length > 0) {
569
- console.log(" Running stability pass (run 2/2)...");
570
- const e2eRuns2 = await (0, e2e_js_1.runVerifyE2E)(verifyUrl, verificationTier, config.scenarios, crawlOpts);
571
- if (e2eRuns2.passed < e2eRuns2.results.length) {
572
- e2eStabilityPassed = false;
573
- e2eCoverageGaps.push("Stability check failed on second run");
574
- const failedNames = e2eRuns2.results
575
- .filter((r) => !r.passed)
576
- .map((r) => r.name)
577
- .join(", ");
578
- failureEvidence.push(`E2E stability: second run failed (${failedNames})`);
579
- }
580
- else {
581
- console.log(" Stability pass: OK (2/2 runs passed)");
582
- }
583
- }
584
- if (e2eRuns.coverageGaps.length > 0) {
585
- console.error(`E2E coverage warning: ${e2eRuns.coverageGaps.join(" ")}`);
586
- }
587
- if (e2eRuns.consoleErrors.length > 0) {
588
- console.error(`E2E console errors: ${e2eRuns.consoleErrors.length} detected`);
589
- }
590
- }
591
- catch (e2eErr) {
592
- console.error(`E2E error: ${e2eErr instanceof Error ? e2eErr.message : String(e2eErr)}`);
593
- }
594
- // Cross-browser E2E via Playwright (if non-chromium browsers configured)
595
- const extraBrowsers = (config.browsers || []).filter((b) => b !== "chromium" && ["firefox", "webkit"].includes(b));
596
- if (extraBrowsers.length > 0 && lastE2EScenarios && lastE2EScenarios.length > 0) {
597
- const pwAvailable = await (0, playwright_runner_js_1.isPlaywrightAvailable)();
598
- if (pwAvailable) {
599
- try {
600
- crossBrowserResults = await (0, playwright_runner_js_1.runPlaywrightE2E)(verifyUrl, lastE2EScenarios, extraBrowsers);
601
- for (const cbr of crossBrowserResults) {
602
- console.log(` Cross-browser ${cbr.browser}: ${cbr.passed}/${cbr.results.length} passed`);
603
- if (cbr.failed > 0) {
604
- const failedNames = cbr.results.filter(r => !r.passed).map(r => r.name).join(", ");
605
- failureEvidence.push(`Cross-browser ${cbr.browser}: ${failedNames} failed`);
606
- }
607
- }
608
- }
609
- catch (cbErr) {
610
- console.error(`Cross-browser error: ${cbErr instanceof Error ? cbErr.message : String(cbErr)}`);
611
- }
612
- }
613
- else {
614
- console.log(" Note: Cross-browser testing requires playwright. Run: npm install -D playwright && npx playwright install");
615
- }
616
- }
617
- try {
618
- visualDiffResult = await (0, visual_diff_js_1.runVisualDiff)(args.projectDir, verifyUrl, "verify");
619
- }
620
- catch (visualErr) {
621
- console.error(`Visual diff error: ${visualErr instanceof Error ? visualErr.message : String(visualErr)}`);
622
- }
623
- }
624
- catch (serveErr) {
625
- console.error(`Dev server error: ${serveErr instanceof Error ? serveErr.message : String(serveErr)}`);
626
- }
627
- finally {
628
- if (servePid) {
629
- await cleanupActiveDevServer();
630
- }
631
- }
632
- }
633
- const verificationTier = (0, index_js_1.planToVerificationTier)(effectiveFeatures.plan);
634
- const viewportSummary = summarizeViewportIssues(multiViewportScores, adjustedThresholds);
635
- failureEvidence.push(...buildResult.errors.slice(0, 3).map((error) => `Build: ${error}`), ...(lighthouseErrorCount > 0 ? [`Lighthouse: ${lighthouseErrorCount} run error(s) were recorded during collection.`] : []), ...(e2eResult
636
- ? e2eResult.results
637
- .filter((scenario) => !scenario.passed)
638
- .slice(0, 2)
639
- .map((scenario) => `E2E: ${scenario.name}${scenario.error ? ` - ${scenario.error}` : ""}`)
640
- : []), ...e2eConsoleErrors.slice(0, 2).map((e) => `Console: ${e}`), ...e2eCoverageGaps.slice(0, 2).map((gap) => `E2E coverage: ${gap}`), ...(viewportSummary.count > 0 && viewportSummary.summary ? [`Viewport: ${viewportSummary.summary}`] : []), ...(visualDiffResult
641
- ? [
642
- `Visual diff: ${(0, visual_diff_js_1.formatVisualDiffSummary)(visualDiffResult)}`,
643
- ]
644
- : []));
645
- const verificationReport = (0, index_js_1.buildVerificationReport)({
646
- buildSuccess: buildResult.success,
647
- buildErrors: buildResult.errors,
648
- e2ePassed: e2eResult?.passed,
649
- e2eTotal: e2eResult?.total,
650
- e2eCoverageGaps,
651
- e2eConsoleErrorCount: e2eConsoleErrors.length,
652
- e2eStabilityPassed,
653
- lighthouseSkipped: args.skipLighthouse,
654
- lighthouseErrorCount,
655
- viewportIssues: multiViewportScores ? viewportSummary.count : undefined,
656
- multiViewportPassed: multiViewportScores ? allViewportsOk : undefined,
657
- multiViewportSummary: multiViewportScores ? viewportSummary.summary : undefined,
658
- visualDiffVerdict: visualDiffResult?.verdict,
659
- visualDiffPercentage: visualDiffResult?.diffPercentage,
660
- hasVisualBaseline: visualDiffResult?.hasBaseline,
661
- lighthouseScores: scores,
662
- mobileLighthouseScores: mobileLighthouseScores ?? undefined,
663
- securityAudit: securityAuditResult
664
- ? {
665
- totalVulnerabilities: securityAuditResult.totalVulnerabilities,
666
- critical: securityAuditResult.critical,
667
- high: securityAuditResult.high,
668
- summary: securityAuditResult.summary,
669
- }
670
- : undefined,
671
- brokenLinksAudit: brokenLinksResult
672
- ? {
673
- checkedCount: brokenLinksResult.checkedCount,
674
- brokenCount: brokenLinksResult.brokenLinks.length,
675
- summary: brokenLinksResult.summary,
676
- }
677
- : undefined,
678
- failureEvidence,
679
- }, {
680
- tier: verificationTier,
681
- thresholds: adjustedThresholds,
682
- });
683
- const verificationView = (0, index_js_1.getTierVerificationView)(verificationReport);
684
- const unifiedGrade = verificationReport.grade;
685
- const exitCode = shouldFailVerificationResult(verificationReport, config.fail_on) ? 1 : 0;
686
- const resultObj = {
687
- grade: unifiedGrade.charAt(0).toUpperCase() + unifiedGrade.slice(1),
688
- timestamp: new Date().toISOString(),
689
- build: {
690
- success: buildResult.success,
691
- durationMs: buildResult.durationMs,
692
- errors: buildResult.errors,
693
- },
694
- e2e: e2eResult,
695
- crossBrowser: crossBrowserResults,
696
- lighthouse: lighthouseResult,
697
- mobileLighthouse: mobileLighthouseScores,
698
- security: securityAuditResult,
699
- visualDiff: visualDiffResult,
700
- thresholds: adjustedThresholds,
701
- ciMode: config.ciMode,
702
- framework: detected.framework,
703
- exitCode,
704
- config_fail_on: config.fail_on,
705
- _plan: effectiveFeatures.plan,
706
- _verbose_failure: true,
707
- _failure_analysis: true,
708
- verification: {
709
- tier: verificationTier,
710
- report: verificationReport,
711
- view: verificationView,
712
- },
713
- };
714
- const markdownReportPath = (0, report_markdown_js_1.getMarkdownReportPath)(args.projectDir);
715
- if ((0, report_markdown_js_1.shouldWriteMarkdownReport)(resultObj)) {
716
- const markdownReport = (0, report_markdown_js_1.buildMarkdownReport)(args.projectDir, resultObj);
717
- fs.writeFileSync(markdownReportPath, markdownReport, "utf-8");
718
- resultObj.markdownReportPath = markdownReportPath;
719
- }
720
- else if (fs.existsSync(markdownReportPath)) {
721
- fs.rmSync(markdownReportPath, { force: true });
722
- }
723
- const inGitHubActions = !!process.env.GITHUB_ACTIONS;
724
- if (inGitHubActions) {
725
- try {
726
- if (process.env.GITHUB_EVENT_NAME === "pull_request") {
727
- let trendDelta = null;
728
- const baseSnapshot = (0, trend_js_1.loadBaseSnapshot)((0, trend_js_1.getBaseResultPath)(args.projectDir));
729
- if (baseSnapshot) {
730
- const currentSnapshot = {
731
- grade: resultObj.grade,
732
- lighthouse: resultObj.lighthouse,
733
- e2e: resultObj.e2e ? { passed: resultObj.e2e.passed, total: resultObj.e2e.total } : null,
734
- timestamp: resultObj.timestamp,
735
- };
736
- trendDelta = (0, trend_js_1.computeTrendDelta)(currentSnapshot, baseSnapshot);
737
- }
738
- await (0, comment_js_1.postPRComment)(resultObj, trendDelta);
739
- resultObj.github = { status: "comment_posted", grade: resultObj.grade };
740
- }
741
- await (0, status_js_1.createStatusCheck)({ grade: resultObj.grade, exitCode: resultObj.exitCode });
742
- resultObj.github ??= { status: "status_set", grade: resultObj.grade };
743
- }
744
- catch (ghErr) {
745
- console.error(`GitHub API warning: ${ghErr instanceof Error ? ghErr.message : String(ghErr)}`);
746
- }
747
- }
748
- writeResultFile(args.projectDir, resultObj);
749
- if (args.format === "json") {
750
- console.log(JSON.stringify(resultObj, null, 2));
751
- }
752
- else {
753
- consoleOutput(resultObj);
754
- }
755
- if (inGitHubActions && process.env.GITHUB_OUTPUT) {
756
- fs.appendFileSync(process.env.GITHUB_OUTPUT, `grade=${resultObj.grade}\n`);
757
- }
758
- exitGracefully(exitCode);
759
- }
760
- run().catch((err) => {
761
- console.error(`Fatal error: ${err instanceof Error ? err.message : String(err)}`);
762
- exitGracefully(1);
763
- });
343
+ `);
344
+ exitGracefully(0);
345
+ return;
346
+ }
347
+ if (args.subcommand === "login") {
348
+ await (0, auth_js_1.login)(args.subcommandArg);
349
+ exitGracefully(0);
350
+ return;
351
+ }
352
+ if (args.subcommand === "logout") {
353
+ (0, auth_js_1.clearToken)();
354
+ exitGracefully(0);
355
+ return;
356
+ }
357
+ if (args.subcommand === "whoami") {
358
+ (0, auth_js_1.whoami)();
359
+ exitGracefully(0);
360
+ return;
361
+ }
362
+ if (args.init) {
363
+ let initEntitlements = null;
364
+ try {
365
+ initEntitlements = await (0, entitlement_js_1.getEntitlements)();
366
+ }
367
+ catch {
368
+ // ignore fetch errors
369
+ }
370
+ const initPlan = initEntitlements?.plan ?? "free";
371
+ const hasProAccess = initPlan === "pro" || initPlan === "team";
372
+ if (!hasProAccess) {
373
+ console.log("\n GitHub Actions integration is a Pro feature.");
374
+ console.log(" Log in and upgrade to Pro to use --init.");
375
+ console.log(" → laxy-verify login");
376
+ console.log(" → https://laxy.dev/pricing\n");
377
+ exitGracefully(1);
378
+ return;
379
+ }
380
+ (0, init_js_1.runInit)(args.projectDir);
381
+ if (!args.initRun) {
382
+ console.log("\n Next step: run npx laxy-verify . (or use --init --run to continue immediately)");
383
+ exitGracefully(0);
384
+ return;
385
+ }
386
+ console.log("\n Config created. Starting verification...\n");
387
+ }
388
+ if (args.badge) {
389
+ const resultPath = path.join(args.projectDir, ".laxy-result.json");
390
+ if (!fs.existsSync(resultPath)) {
391
+ console.error("Error: .laxy-result.json not found. Run `npx laxy-verify .` first to generate it.");
392
+ exitGracefully(2);
393
+ return;
394
+ }
395
+ const content = JSON.parse(fs.readFileSync(resultPath, "utf-8"));
396
+ // Pro: 실시간 배지 URL (verify_runs 기반)
397
+ let badgeEntitlements = null;
398
+ try {
399
+ badgeEntitlements = await (0, entitlement_js_1.getEntitlements)();
400
+ }
401
+ catch { /* ignore */ }
402
+ const badgePlan = badgeEntitlements?.plan ?? "free";
403
+ const badgeIsPro = badgePlan === "pro" || badgePlan === "team";
404
+ if (badgeIsPro) {
405
+ const { LAXY_API_URL } = await Promise.resolve().then(() => __importStar(require("./auth.js")));
406
+ const repoId = (0, auth_js_1.getOrCreateRepoId)(args.projectDir);
407
+ const dynamicUrl = `${LAXY_API_URL}/api/badge/${repoId}`;
408
+ console.log(`![Laxy](${dynamicUrl})`);
409
+ }
410
+ else {
411
+ const badge = (0, badge_js_1.generateBadge)(content.grade);
412
+ console.log(badge);
413
+ }
414
+ exitGracefully(0);
415
+ return;
416
+ }
417
+ let config;
418
+ try {
419
+ config = (0, config_js_1.loadConfig)({
420
+ dir: args.projectDir,
421
+ configPath: args.configPath,
422
+ ciMode: args.ciMode,
423
+ cliFlags: {
424
+ failOn: args.failOn,
425
+ skipLighthouse: args.skipLighthouse,
426
+ },
427
+ });
428
+ }
429
+ catch (err) {
430
+ console.error(`Config error: ${err instanceof Error ? err.message : String(err)}`);
431
+ exitGracefully(2);
432
+ return;
433
+ }
434
+ let detected;
435
+ try {
436
+ detected = (0, detect_js_1.detect)(args.projectDir);
437
+ }
438
+ catch (err) {
439
+ console.error(`Detection error: ${err instanceof Error ? err.message : String(err)}`);
440
+ exitGracefully(2);
441
+ return;
442
+ }
443
+ const buildCmd = config.build_command || detected.buildCmd;
444
+ const devCmd = config.dev_command || detected.devCmd;
445
+ const port = config.port;
446
+ try {
447
+ await ensurePortAvailableForVerification(port);
448
+ }
449
+ catch (err) {
450
+ console.error(`Preflight error: ${err instanceof Error ? err.message : String(err)}`);
451
+ exitGracefully(2);
452
+ return;
453
+ }
454
+ let buildResult;
455
+ try {
456
+ buildResult = await (0, build_js_1.runBuild)(buildCmd, config.build_timeout, args.projectDir);
457
+ }
458
+ catch (err) {
459
+ buildResult = {
460
+ success: false,
461
+ durationMs: 0,
462
+ errors: err instanceof Error ? [err.message] : [String(err)],
463
+ };
464
+ }
465
+ let scores;
466
+ let lighthouseResult = null;
467
+ let lighthouseErrorCount = 0;
468
+ const adjustedThresholds = {
469
+ performance: config.ciMode ? config.thresholds.performance - 10 : config.thresholds.performance,
470
+ accessibility: config.thresholds.accessibility,
471
+ seo: config.thresholds.seo,
472
+ bestPractices: config.thresholds.bestPractices,
473
+ };
474
+ let entitlements = null;
475
+ try {
476
+ entitlements = await (0, entitlement_js_1.getEntitlements)();
477
+ (0, entitlement_js_1.printPlanBanner)(entitlements);
478
+ }
479
+ catch {
480
+ // Ignore entitlement errors and keep the free feature set.
481
+ }
482
+ const features = entitlements ?? {
483
+ plan: "free",
484
+ // All verification features run on every plan
485
+ // Automation features ??not available on Free
486
+ github_actions: false,
487
+ queue_priority: false,
488
+ parallel_execution: false,
489
+ };
490
+ let effectiveFeatures = features;
491
+ if (args.planOverride) {
492
+ try {
493
+ effectiveFeatures = (0, entitlement_js_1.applyPlanOverride)(features, args.planOverride);
494
+ console.log(` Plan override: ${(0, entitlement_js_1.normalizePlan)(features.plan)} -> ${effectiveFeatures.plan} (verification behavior is unchanged)`);
495
+ }
496
+ catch (overrideErr) {
497
+ console.error(`Plan override error: ${overrideErr instanceof Error ? overrideErr.message : String(overrideErr)}`);
498
+ exitGracefully(2);
499
+ return;
500
+ }
501
+ }
502
+ // Always run 3 Lighthouse passes for median score
503
+ if (config.lighthouse_runs < 3) {
504
+ config = { ...config, lighthouse_runs: 3 };
505
+ }
506
+ let multiViewportScores = null;
507
+ let allViewportsOk = false;
508
+ let e2eResult;
509
+ let crossBrowserResults;
510
+ let e2eCoverageGaps = [];
511
+ let e2eConsoleErrors = [];
512
+ let e2eStabilityPassed = true;
513
+ let visualDiffResult = null;
514
+ let securityAuditResult = null;
515
+ let mobileLighthouseScores = null;
516
+ let brokenLinksResult = null;
517
+ const failureEvidence = [];
518
+ if (buildResult.success) {
519
+ let servePid;
520
+ try {
521
+ const serve = await (0, serve_js_1.startDevServer)(devCmd, port, config.dev_timeout, args.projectDir);
522
+ servePid = serve.pid;
523
+ activeDevServerPid = serve.pid;
524
+ const verifyUrl = `http://127.0.0.1:${port}/`;
525
+ const verificationTier = (0, index_js_1.planToVerificationTier)(effectiveFeatures.plan);
526
+ if (!args.skipLighthouse) {
527
+ try {
528
+ const lhResult = await (0, lighthouse_js_1.runLighthouse)(port, config.lighthouse_runs);
529
+ lighthouseErrorCount = lhResult.errors.length;
530
+ scores = lhResult.scores ?? undefined;
531
+ if (scores) {
532
+ lighthouseResult = {
533
+ performance: scores.performance,
534
+ accessibility: scores.accessibility,
535
+ seo: scores.seo,
536
+ bestPractices: scores.bestPractices,
537
+ runs: config.lighthouse_runs,
538
+ };
539
+ }
540
+ }
541
+ catch (lhErr) {
542
+ console.error(`Lighthouse error: ${lhErr instanceof Error ? lhErr.message : String(lhErr)}`);
543
+ }
544
+ }
545
+ if (!args.skipLighthouse) {
546
+ try {
547
+ multiViewportScores = await (0, multi_viewport_js_1.runMultiViewportLighthouse)(port);
548
+ (0, multi_viewport_js_1.printMultiViewportResults)(multiViewportScores, adjustedThresholds);
549
+ allViewportsOk = (0, multi_viewport_js_1.allViewportsPass)(multiViewportScores, adjustedThresholds);
550
+ if (multiViewportScores.screenshotDiffs) {
551
+ for (const diff of multiViewportScores.screenshotDiffs) {
552
+ if (!diff.baselineCreated && diff.diffPercent > 10) {
553
+ failureEvidence.push(`Viewport screenshot: ${diff.viewport} diff ${diff.diffPercent}% exceeds 10% threshold`);
554
+ }
555
+ }
556
+ }
557
+ }
558
+ catch (mvErr) {
559
+ console.error(`Multi-viewport error: ${mvErr instanceof Error ? mvErr.message : String(mvErr)}`);
560
+ }
561
+ }
562
+ // Security audit (npm audit)
563
+ try {
564
+ securityAuditResult = await (0, security_audit_js_1.runSecurityAudit)(args.projectDir);
565
+ if (securityAuditResult.critical > 0 || securityAuditResult.high > 0) {
566
+ failureEvidence.push(`Security: ${securityAuditResult.summary}`);
567
+ }
568
+ }
569
+ catch (secErr) {
570
+ console.error(`Security audit error: ${secErr instanceof Error ? secErr.message : String(secErr)}`);
571
+ }
572
+ const crawlEnabled = args.crawl || config.crawl;
573
+ const crawlOpts = crawlEnabled
574
+ ? { enabled: true, maxDepth: config.max_crawl_depth, maxPages: config.max_crawl_pages }
575
+ : undefined;
576
+ let lastE2EScenarios;
577
+ try {
578
+ const e2eRuns = await (0, e2e_js_1.runVerifyE2E)(verifyUrl, verificationTier, config.scenarios, crawlOpts);
579
+ lastE2EScenarios = e2eRuns.scenarios;
580
+ e2eResult = {
581
+ passed: e2eRuns.passed,
582
+ failed: e2eRuns.failed,
583
+ total: e2eRuns.results.length,
584
+ results: e2eRuns.results,
585
+ };
586
+ e2eCoverageGaps = e2eRuns.coverageGaps;
587
+ e2eConsoleErrors = e2eRuns.consoleErrors;
588
+ // Merge console errors captured during crawl phase
589
+ if (e2eRuns.crawlResult) {
590
+ const crawlConsoleErrors = e2eRuns.crawlResult.pages.flatMap((p) => p.consoleErrors);
591
+ if (crawlConsoleErrors.length > 0) {
592
+ const unique = crawlConsoleErrors.filter((e) => !e2eConsoleErrors.includes(e));
593
+ e2eConsoleErrors = [...e2eConsoleErrors, ...unique];
594
+ }
595
+ }
596
+ // Broken links audit runs after E2E so we have the crawl result
597
+ if (e2eRuns.crawlResult && e2eRuns.crawlResult.totalLinks > 0) {
598
+ try {
599
+ brokenLinksResult = await (0, broken_links_js_1.auditBrokenLinks)(e2eRuns.crawlResult, verifyUrl);
600
+ if (brokenLinksResult.hasBrokenLinks) {
601
+ failureEvidence.push(`Broken links: ${brokenLinksResult.summary}`);
602
+ }
603
+ }
604
+ catch (blErr) {
605
+ console.error(`Broken links audit error: ${blErr instanceof Error ? blErr.message : String(blErr)}`);
606
+ }
607
+ }
608
+ // E2E stability: run a second time if first run passed all
609
+ if (e2eRuns.passed === e2eRuns.results.length && e2eRuns.results.length > 0) {
610
+ console.log(" Running stability pass (run 2/2)...");
611
+ const e2eRuns2 = await (0, e2e_js_1.runVerifyE2E)(verifyUrl, verificationTier, config.scenarios, crawlOpts);
612
+ if (e2eRuns2.passed < e2eRuns2.results.length) {
613
+ e2eStabilityPassed = false;
614
+ e2eCoverageGaps.push("Stability check failed on second run");
615
+ const failedNames = e2eRuns2.results
616
+ .filter((r) => !r.passed)
617
+ .map((r) => r.name)
618
+ .join(", ");
619
+ failureEvidence.push(`E2E stability: second run failed (${failedNames})`);
620
+ }
621
+ else {
622
+ console.log(" Stability pass: OK (2/2 runs passed)");
623
+ }
624
+ }
625
+ if (e2eRuns.coverageGaps.length > 0) {
626
+ console.error(`E2E coverage warning: ${e2eRuns.coverageGaps.join(" ")}`);
627
+ }
628
+ if (e2eRuns.consoleErrors.length > 0) {
629
+ console.error(`E2E console errors: ${e2eRuns.consoleErrors.length} detected`);
630
+ }
631
+ }
632
+ catch (e2eErr) {
633
+ console.error(`E2E error: ${e2eErr instanceof Error ? e2eErr.message : String(e2eErr)}`);
634
+ }
635
+ // Cross-browser E2E via Playwright (if non-chromium browsers configured)
636
+ const extraBrowsers = (config.browsers || []).filter((b) => b !== "chromium" && ["firefox", "webkit"].includes(b));
637
+ if (extraBrowsers.length > 0 && lastE2EScenarios && lastE2EScenarios.length > 0) {
638
+ const pwAvailable = await (0, playwright_runner_js_1.isPlaywrightAvailable)();
639
+ if (pwAvailable) {
640
+ try {
641
+ crossBrowserResults = await (0, playwright_runner_js_1.runPlaywrightE2E)(verifyUrl, lastE2EScenarios, extraBrowsers);
642
+ for (const cbr of crossBrowserResults) {
643
+ console.log(` Cross-browser ${cbr.browser}: ${cbr.passed}/${cbr.results.length} passed`);
644
+ if (cbr.failed > 0) {
645
+ const failedNames = cbr.results.filter(r => !r.passed).map(r => r.name).join(", ");
646
+ failureEvidence.push(`Cross-browser ${cbr.browser}: ${failedNames} failed`);
647
+ }
648
+ }
649
+ }
650
+ catch (cbErr) {
651
+ console.error(`Cross-browser error: ${cbErr instanceof Error ? cbErr.message : String(cbErr)}`);
652
+ }
653
+ }
654
+ else {
655
+ console.log(" Note: Cross-browser testing requires playwright. Run: npm install -D playwright && npx playwright install");
656
+ }
657
+ }
658
+ try {
659
+ visualDiffResult = await (0, visual_diff_js_1.runVisualDiff)(args.projectDir, verifyUrl, "verify");
660
+ }
661
+ catch (visualErr) {
662
+ console.error(`Visual diff error: ${visualErr instanceof Error ? visualErr.message : String(visualErr)}`);
663
+ }
664
+ }
665
+ catch (serveErr) {
666
+ console.error(`Dev server error: ${serveErr instanceof Error ? serveErr.message : String(serveErr)}`);
667
+ }
668
+ finally {
669
+ if (servePid) {
670
+ await cleanupActiveDevServer();
671
+ }
672
+ }
673
+ }
674
+ const verificationTier = (0, index_js_1.planToVerificationTier)(effectiveFeatures.plan);
675
+ const viewportSummary = summarizeViewportIssues(multiViewportScores, adjustedThresholds);
676
+ failureEvidence.push(...buildResult.errors.slice(0, 3).map((error) => `Build: ${error}`), ...(lighthouseErrorCount > 0 ? [`Lighthouse: ${lighthouseErrorCount} run error(s) were recorded during collection.`] : []), ...(e2eResult
677
+ ? e2eResult.results
678
+ .filter((scenario) => !scenario.passed)
679
+ .slice(0, 2)
680
+ .map((scenario) => `E2E: ${scenario.name}${scenario.error ? ` - ${scenario.error}` : ""}`)
681
+ : []), ...e2eConsoleErrors.slice(0, 2).map((e) => `Console: ${e}`), ...e2eCoverageGaps.slice(0, 2).map((gap) => `E2E coverage: ${gap}`), ...(viewportSummary.count > 0 && viewportSummary.summary ? [`Viewport: ${viewportSummary.summary}`] : []), ...(visualDiffResult
682
+ ? [
683
+ `Visual diff: ${(0, visual_diff_js_1.formatVisualDiffSummary)(visualDiffResult)}`,
684
+ ]
685
+ : []));
686
+ const verificationReport = (0, index_js_1.buildVerificationReport)({
687
+ buildSuccess: buildResult.success,
688
+ buildErrors: buildResult.errors,
689
+ e2ePassed: e2eResult?.passed,
690
+ e2eTotal: e2eResult?.total,
691
+ e2eCoverageGaps,
692
+ e2eConsoleErrorCount: e2eConsoleErrors.length,
693
+ e2eStabilityPassed,
694
+ lighthouseSkipped: args.skipLighthouse,
695
+ lighthouseErrorCount,
696
+ viewportIssues: multiViewportScores ? viewportSummary.count : undefined,
697
+ multiViewportPassed: multiViewportScores ? allViewportsOk : undefined,
698
+ multiViewportSummary: multiViewportScores ? viewportSummary.summary : undefined,
699
+ visualDiffVerdict: visualDiffResult?.verdict,
700
+ visualDiffPercentage: visualDiffResult?.diffPercentage,
701
+ hasVisualBaseline: visualDiffResult?.hasBaseline,
702
+ lighthouseScores: scores,
703
+ mobileLighthouseScores: mobileLighthouseScores ?? undefined,
704
+ securityAudit: securityAuditResult
705
+ ? {
706
+ totalVulnerabilities: securityAuditResult.totalVulnerabilities,
707
+ critical: securityAuditResult.critical,
708
+ high: securityAuditResult.high,
709
+ summary: securityAuditResult.summary,
710
+ }
711
+ : undefined,
712
+ brokenLinksAudit: brokenLinksResult
713
+ ? {
714
+ checkedCount: brokenLinksResult.checkedCount,
715
+ brokenCount: brokenLinksResult.brokenLinks.length,
716
+ summary: brokenLinksResult.summary,
717
+ }
718
+ : undefined,
719
+ failureEvidence,
720
+ }, {
721
+ tier: verificationTier,
722
+ thresholds: adjustedThresholds,
723
+ });
724
+ const verificationView = (0, index_js_1.getTierVerificationView)(verificationReport);
725
+ const unifiedGrade = verificationReport.grade;
726
+ const exitCode = shouldFailVerificationResult(verificationReport, config.fail_on) ? 1 : 0;
727
+ const resultObj = {
728
+ grade: unifiedGrade.charAt(0).toUpperCase() + unifiedGrade.slice(1),
729
+ timestamp: new Date().toISOString(),
730
+ build: {
731
+ success: buildResult.success,
732
+ durationMs: buildResult.durationMs,
733
+ errors: buildResult.errors,
734
+ },
735
+ e2e: e2eResult,
736
+ crossBrowser: crossBrowserResults,
737
+ lighthouse: lighthouseResult,
738
+ mobileLighthouse: mobileLighthouseScores,
739
+ security: securityAuditResult,
740
+ visualDiff: visualDiffResult,
741
+ thresholds: adjustedThresholds,
742
+ ciMode: config.ciMode,
743
+ framework: detected.framework,
744
+ exitCode,
745
+ config_fail_on: config.fail_on,
746
+ _plan: effectiveFeatures.plan,
747
+ _verbose_failure: true,
748
+ _failure_analysis: true,
749
+ verification: {
750
+ tier: verificationTier,
751
+ report: verificationReport,
752
+ view: verificationView,
753
+ },
754
+ };
755
+ const markdownReportPath = (0, report_markdown_js_1.getMarkdownReportPath)(args.projectDir);
756
+ if ((0, report_markdown_js_1.shouldWriteMarkdownReport)(resultObj)) {
757
+ const markdownReport = (0, report_markdown_js_1.buildMarkdownReport)(args.projectDir, resultObj);
758
+ fs.writeFileSync(markdownReportPath, markdownReport, "utf-8");
759
+ resultObj.markdownReportPath = markdownReportPath;
760
+ }
761
+ else if (fs.existsSync(markdownReportPath)) {
762
+ fs.rmSync(markdownReportPath, { force: true });
763
+ }
764
+ const inGitHubActions = !!process.env.GITHUB_ACTIONS;
765
+ if (inGitHubActions) {
766
+ try {
767
+ if (process.env.GITHUB_EVENT_NAME === "pull_request") {
768
+ let trendDelta = null;
769
+ const baseSnapshot = (0, trend_js_1.loadBaseSnapshot)((0, trend_js_1.getBaseResultPath)(args.projectDir));
770
+ if (baseSnapshot) {
771
+ const currentSnapshot = {
772
+ grade: resultObj.grade,
773
+ lighthouse: resultObj.lighthouse,
774
+ e2e: resultObj.e2e ? { passed: resultObj.e2e.passed, total: resultObj.e2e.total } : null,
775
+ timestamp: resultObj.timestamp,
776
+ };
777
+ trendDelta = (0, trend_js_1.computeTrendDelta)(currentSnapshot, baseSnapshot);
778
+ }
779
+ await (0, comment_js_1.postPRComment)(resultObj, trendDelta);
780
+ resultObj.github = { status: "comment_posted", grade: resultObj.grade };
781
+ }
782
+ await (0, status_js_1.createStatusCheck)({ grade: resultObj.grade, exitCode: resultObj.exitCode });
783
+ resultObj.github ??= { status: "status_set", grade: resultObj.grade };
784
+ }
785
+ catch (ghErr) {
786
+ console.error(`GitHub API warning: ${ghErr instanceof Error ? ghErr.message : String(ghErr)}`);
787
+ }
788
+ }
789
+ writeResultFile(args.projectDir, resultObj);
790
+ // Pro 이상: 결과를 서버에 저장 (배지, 공유 링크용)
791
+ const token = (0, auth_js_1.loadToken)();
792
+ const isPro = effectiveFeatures.plan === "pro" || effectiveFeatures.plan === "team";
793
+ if (token && isPro) {
794
+ try {
795
+ const repoId = (0, auth_js_1.getOrCreateRepoId)(args.projectDir);
796
+ const { LAXY_API_URL } = await Promise.resolve().then(() => __importStar(require("./auth.js")));
797
+ const saveRes = await fetch(`${LAXY_API_URL}/api/v1/cli-results`, {
798
+ method: "POST",
799
+ headers: {
800
+ "Content-Type": "application/json",
801
+ Authorization: `Bearer ${token}`,
802
+ },
803
+ body: JSON.stringify({
804
+ repo_id: repoId,
805
+ project_name: path.basename(path.resolve(args.projectDir)),
806
+ grade: unifiedGrade,
807
+ verdict: verificationReport.verdict,
808
+ scores: {
809
+ performance: scores?.performance,
810
+ accessibility: scores?.accessibility,
811
+ seo: scores?.seo,
812
+ best_practices: scores?.bestPractices,
813
+ e2e_passed: e2eResult?.passed,
814
+ e2e_total: e2eResult?.total,
815
+ security_critical: securityAuditResult?.critical,
816
+ console_error_count: e2eConsoleErrors.length,
817
+ broken_link_count: brokenLinksResult?.brokenLinks.length,
818
+ },
819
+ full_result: { framework: detected.framework },
820
+ }),
821
+ });
822
+ if (!saveRes.ok) {
823
+ const errBody = await saveRes.json().catch(() => ({}));
824
+ console.warn(` [warn] Result save failed (${saveRes.status}): ${errBody.error ?? "unknown error"}`);
825
+ }
826
+ }
827
+ catch {
828
+ // 네트워크 오류는 검증 결과에 영향 없음
829
+ }
830
+ }
831
+ if (args.format === "json") {
832
+ console.log(JSON.stringify(resultObj, null, 2));
833
+ }
834
+ else {
835
+ consoleOutput(resultObj);
836
+ }
837
+ if (inGitHubActions && process.env.GITHUB_OUTPUT) {
838
+ fs.appendFileSync(process.env.GITHUB_OUTPUT, `grade=${resultObj.grade}\n`);
839
+ }
840
+ exitGracefully(exitCode);
841
+ }
842
+ run().catch((err) => {
843
+ console.error(`Fatal error: ${err instanceof Error ? err.message : String(err)}`);
844
+ exitGracefully(1);
845
+ });