laxy-verify 1.2.0 → 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/dist/cli.js CHANGED
@@ -1,306 +1,307 @@
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
+ port: flags.port !== undefined ? Number(flags.port) : undefined,
162
+ help: flags.help !== undefined || flags.h !== undefined,
163
+ };
164
+ }
165
+ function writeResultFile(projectDir, result) {
166
+ const filePath = path.join(projectDir, ".laxy-result.json");
167
+ fs.writeFileSync(filePath, JSON.stringify(result, null, 2) + "\n", "utf-8");
168
+ }
169
+ function summarizeViewportIssues(scores, thresholds) {
170
+ if (!scores)
171
+ return { count: 0 };
172
+ const failed = [];
173
+ for (const [label, viewportScores] of Object.entries(scores)) {
174
+ if (!viewportScores) {
175
+ failed.push(`${label}: missing`);
176
+ continue;
177
+ }
178
+ const passes = viewportScores.performance >= thresholds.performance &&
179
+ viewportScores.accessibility >= thresholds.accessibility &&
180
+ viewportScores.seo >= thresholds.seo &&
181
+ viewportScores.bestPractices >= thresholds.bestPractices;
182
+ if (!passes) {
183
+ failed.push(`${label}: P${viewportScores.performance} A${viewportScores.accessibility} SEO${viewportScores.seo} BP${viewportScores.bestPractices}`);
184
+ }
185
+ }
186
+ return {
187
+ count: failed.length,
188
+ summary: failed.length > 0 ? failed.join(" | ") : "All checked viewports passed.",
189
+ };
190
+ }
191
+ function consoleOutput(result) {
192
+ const gradeLabel = result.grade;
193
+ const checkEmoji = result.grade !== "Unverified" ? " OK" : "";
194
+ console.log(`\n Laxy Verify: blocker check ${gradeLabel}${checkEmoji}`);
195
+ console.log(` Build: ${result.build.success ? `OK (${result.build.durationMs}ms)` : "FAILED"}`);
196
+ if (result.build.errors.length > 0) {
197
+ console.log(" Errors:");
198
+ const firstError = result.build.errors.find((line) => /error/i.test(line));
199
+ const last5 = result.build.errors.slice(-5);
200
+ const toShow = firstError && !last5.includes(firstError)
201
+ ? [firstError, " ...", ...last5]
202
+ : last5;
203
+ for (const line of toShow)
204
+ console.error(` ${line}`);
205
+ }
206
+ if (result.lighthouse !== null) {
207
+ const lh = result.lighthouse;
208
+ const t = result.thresholds;
209
+ const check = (passed) => (passed ? " OK" : " FAIL");
210
+ console.log(" Lighthouse:");
211
+ console.log(` Performance: ${lh.performance} / ${t.performance}${check(lh.performance >= t.performance)}`);
212
+ console.log(` Accessibility: ${lh.accessibility} / ${t.accessibility}${check(lh.accessibility >= t.accessibility)}`);
213
+ console.log(` SEO: ${lh.seo} / ${t.seo}${check(lh.seo >= t.seo)}`);
214
+ console.log(` Best Practices: ${lh.bestPractices} / ${t.bestPractices}${check(lh.bestPractices >= t.bestPractices)}`);
215
+ console.log(` Runs: ${lh.runs}`);
216
+ }
217
+ else {
218
+ console.log(" Lighthouse: skipped");
219
+ }
220
+ if (result.e2e) {
221
+ console.log(` E2E: ${result.e2e.passed}/${result.e2e.total} passed`);
222
+ }
223
+ if (result.crossBrowser && result.crossBrowser.length > 0) {
224
+ console.log(" Cross-browser:");
225
+ for (const cbr of result.crossBrowser) {
226
+ const status = cbr.failed === 0 ? "OK" : "FAIL";
227
+ console.log(` ${cbr.browser}: ${cbr.passed}/${cbr.results.length} passed ${status}`);
228
+ }
229
+ }
230
+ if (result.visualDiff) {
231
+ console.log(` Visual diff: ${(0, visual_diff_js_1.formatVisualDiffSummary)(result.visualDiff)}`);
232
+ }
233
+ if (result.security) {
234
+ console.log(` Security: ${result.security.summary}`);
235
+ }
236
+ if (result.mobileLighthouse) {
237
+ const ml = result.mobileLighthouse;
238
+ console.log(` Mobile LH: P=${ml.performance} A=${ml.accessibility} SEO=${ml.seo} BP=${ml.bestPractices}`);
239
+ }
240
+ if (result.verification) {
241
+ const view = result.verification.view;
242
+ const verboseFailure = result._verbose_failure ?? true;
243
+ const failureAnalysis = result._failure_analysis ?? true;
244
+ console.log(` Verification depth: ${view.tier}`);
245
+ console.log(` Decision question: ${view.question}`);
246
+ console.log(` Decision: ${view.verdict} (${view.confidence})`);
247
+ console.log(` Why it stopped here: ${view.summary}`);
248
+ // Passed/Failed checks summary
249
+ if (view.passes.length > 0) {
250
+ const passedChecks = view.passes.filter((p) => p.passed).map((p) => p.label);
251
+ const failedChecks = view.passes.filter((p) => !p.passed).map((p) => p.label);
252
+ if (passedChecks.length > 0)
253
+ console.log(` Passed: ${passedChecks.join(", ")}`);
254
+ if (failedChecks.length > 0)
255
+ console.log(` Failed: ${failedChecks.join(", ")}`);
256
+ }
257
+ // Blockers: ?�목?� 모든 ?�어, Fix ?�션?� verbose_failure(Pro+) ?�상?�서 ?�시
258
+ if (view.blockers.length > 0) {
259
+ console.log(" Deployment blockers:");
260
+ for (const blocker of view.blockers) {
261
+ console.log(` - ${blocker.title}`);
262
+ if (verboseFailure)
263
+ console.log(` Fix: ${blocker.action}`);
264
+ }
265
+ }
266
+ // Warnings: always show all
267
+ if (view.warnings.length > 0) {
268
+ console.log(" Risks to review:");
269
+ for (const warning of view.warnings) {
270
+ console.log(` - ${warning.title}`);
271
+ if (failureAnalysis)
272
+ console.log(` Review: ${warning.action}`);
273
+ }
274
+ }
275
+ if (view.nextActions.length > 0) {
276
+ console.log(" Next actions:");
277
+ for (const action of view.nextActions)
278
+ console.log(` - ${action}`);
279
+ }
280
+ // Evidence: failure_analysis(Pro+)???�체, verbose_failure(Pro)??3�? Free??2�?
281
+ const evidenceLimit = failureAnalysis ? view.failureEvidence.length : verboseFailure ? 3 : 2;
282
+ const evidenceToShow = view.failureEvidence.slice(0, evidenceLimit);
283
+ if (evidenceToShow.length > 0) {
284
+ console.log(" Evidence collected:");
285
+ for (const item of evidenceToShow)
286
+ console.log(` - ${item}`);
287
+ }
288
+ }
289
+ if (result.github) {
290
+ if (result.github.status === "comment_posted")
291
+ console.log(" PR comment: posted");
292
+ if (result.github.status === "status_set")
293
+ console.log(` Status check: ${result.github.grade}`);
294
+ }
295
+ console.log(" Result: .laxy-result.json");
296
+ if (result.markdownReportPath) {
297
+ console.log(` Report: ${path.basename(result.markdownReportPath)}`);
298
+ }
299
+ console.log(` Exit code: ${result.exitCode}`);
300
+ }
301
+ async function run() {
302
+ installSignalCleanupHandlers();
303
+ const args = parseArgs();
304
+ if (args.help) {
304
305
  console.log(`
305
306
  laxy-verify v${package_json_1.default.version}
306
307
  Deployment blocker gate for frontend apps
@@ -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,508 +340,530 @@ 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
- `);
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
- });
346
+ `);
347
+ exitGracefully(0);
348
+ return;
349
+ }
350
+ if (args.subcommand === "login") {
351
+ await (0, auth_js_1.login)(args.subcommandArg);
352
+ exitGracefully(0);
353
+ return;
354
+ }
355
+ if (args.subcommand === "logout") {
356
+ (0, auth_js_1.clearToken)();
357
+ exitGracefully(0);
358
+ return;
359
+ }
360
+ if (args.subcommand === "whoami") {
361
+ (0, auth_js_1.whoami)();
362
+ exitGracefully(0);
363
+ return;
364
+ }
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
+ }
383
+ (0, init_js_1.runInit)(args.projectDir);
384
+ if (!args.initRun) {
385
+ console.log("\n Next step: run npx laxy-verify . (or use --init --run to continue immediately)");
386
+ exitGracefully(0);
387
+ return;
388
+ }
389
+ console.log("\n Config created. Starting verification...\n");
390
+ }
391
+ if (args.badge) {
392
+ const resultPath = path.join(args.projectDir, ".laxy-result.json");
393
+ if (!fs.existsSync(resultPath)) {
394
+ console.error("Error: .laxy-result.json not found. Run `npx laxy-verify .` first to generate it.");
395
+ exitGracefully(2);
396
+ return;
397
+ }
398
+ const content = JSON.parse(fs.readFileSync(resultPath, "utf-8"));
399
+ // Pro: 실시간 배지 URL (verify_runs 기반)
400
+ let badgeEntitlements = null;
401
+ try {
402
+ badgeEntitlements = await (0, entitlement_js_1.getEntitlements)();
403
+ }
404
+ catch { /* ignore */ }
405
+ const badgePlan = badgeEntitlements?.plan ?? "free";
406
+ const badgeIsPro = badgePlan === "pro" || badgePlan === "team";
407
+ if (badgeIsPro) {
408
+ const { LAXY_API_URL } = await Promise.resolve().then(() => __importStar(require("./auth.js")));
409
+ const repoId = (0, auth_js_1.getOrCreateRepoId)(args.projectDir);
410
+ const dynamicUrl = `${LAXY_API_URL}/api/badge/${repoId}`;
411
+ console.log(`![Laxy](${dynamicUrl})`);
412
+ }
413
+ else {
414
+ const badge = (0, badge_js_1.generateBadge)(content.grade);
415
+ console.log(badge);
416
+ }
417
+ exitGracefully(0);
418
+ return;
419
+ }
420
+ let config;
421
+ try {
422
+ config = (0, config_js_1.loadConfig)({
423
+ dir: args.projectDir,
424
+ configPath: args.configPath,
425
+ ciMode: args.ciMode,
426
+ cliFlags: {
427
+ failOn: args.failOn,
428
+ skipLighthouse: args.skipLighthouse,
429
+ },
430
+ });
431
+ }
432
+ catch (err) {
433
+ console.error(`Config error: ${err instanceof Error ? err.message : String(err)}`);
434
+ exitGracefully(2);
435
+ return;
436
+ }
437
+ let detected;
438
+ try {
439
+ detected = (0, detect_js_1.detect)(args.projectDir);
440
+ }
441
+ catch (err) {
442
+ console.error(`Detection error: ${err instanceof Error ? err.message : String(err)}`);
443
+ exitGracefully(2);
444
+ return;
445
+ }
446
+ const buildCmd = config.build_command || detected.buildCmd;
447
+ const devCmd = config.dev_command || detected.devCmd;
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
+ }
459
+ }
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})`);
469
+ }
470
+ let buildResult;
471
+ if (useExistingServer) {
472
+ // Skip build when using an external server
473
+ buildResult = { success: true, durationMs: 0, errors: [] };
474
+ }
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
+ }
486
+ }
487
+ let scores;
488
+ let lighthouseResult = null;
489
+ let lighthouseErrorCount = 0;
490
+ const adjustedThresholds = {
491
+ performance: config.ciMode ? config.thresholds.performance - 10 : config.thresholds.performance,
492
+ accessibility: config.thresholds.accessibility,
493
+ seo: config.thresholds.seo,
494
+ bestPractices: config.thresholds.bestPractices,
495
+ };
496
+ let entitlements = null;
497
+ try {
498
+ entitlements = await (0, entitlement_js_1.getEntitlements)();
499
+ (0, entitlement_js_1.printPlanBanner)(entitlements);
500
+ }
501
+ catch {
502
+ // Ignore entitlement errors and keep the free feature set.
503
+ }
504
+ const features = entitlements ?? {
505
+ plan: "free",
506
+ // All verification features run on every plan
507
+ // Automation features ??not available on Free
508
+ github_actions: false,
509
+ queue_priority: false,
510
+ parallel_execution: false,
511
+ };
512
+ let effectiveFeatures = features;
513
+ if (args.planOverride) {
514
+ try {
515
+ effectiveFeatures = (0, entitlement_js_1.applyPlanOverride)(features, args.planOverride);
516
+ console.log(` Plan override: ${(0, entitlement_js_1.normalizePlan)(features.plan)} -> ${effectiveFeatures.plan} (verification behavior is unchanged)`);
517
+ }
518
+ catch (overrideErr) {
519
+ console.error(`Plan override error: ${overrideErr instanceof Error ? overrideErr.message : String(overrideErr)}`);
520
+ exitGracefully(2);
521
+ return;
522
+ }
523
+ }
524
+ // Always run 3 Lighthouse passes for median score
525
+ if (config.lighthouse_runs < 3) {
526
+ config = { ...config, lighthouse_runs: 3 };
527
+ }
528
+ let multiViewportScores = null;
529
+ let allViewportsOk = false;
530
+ let e2eResult;
531
+ let crossBrowserResults;
532
+ let e2eCoverageGaps = [];
533
+ let e2eConsoleErrors = [];
534
+ let e2eStabilityPassed = true;
535
+ let visualDiffResult = null;
536
+ let securityAuditResult = null;
537
+ let mobileLighthouseScores = null;
538
+ let brokenLinksResult = null;
539
+ const failureEvidence = [];
540
+ if (buildResult.success) {
541
+ let servePid;
542
+ try {
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
+ }
548
+ const verifyUrl = `http://127.0.0.1:${port}/`;
549
+ const verificationTier = (0, index_js_1.planToVerificationTier)(effectiveFeatures.plan);
550
+ if (!args.skipLighthouse) {
551
+ try {
552
+ const lhResult = await (0, lighthouse_js_1.runLighthouse)(port, config.lighthouse_runs);
553
+ lighthouseErrorCount = lhResult.errors.length;
554
+ scores = lhResult.scores ?? undefined;
555
+ if (scores) {
556
+ lighthouseResult = {
557
+ performance: scores.performance,
558
+ accessibility: scores.accessibility,
559
+ seo: scores.seo,
560
+ bestPractices: scores.bestPractices,
561
+ runs: config.lighthouse_runs,
562
+ };
563
+ }
564
+ }
565
+ catch (lhErr) {
566
+ console.error(`Lighthouse error: ${lhErr instanceof Error ? lhErr.message : String(lhErr)}`);
567
+ }
568
+ }
569
+ if (!args.skipLighthouse) {
570
+ try {
571
+ multiViewportScores = await (0, multi_viewport_js_1.runMultiViewportLighthouse)(port);
572
+ (0, multi_viewport_js_1.printMultiViewportResults)(multiViewportScores, adjustedThresholds);
573
+ allViewportsOk = (0, multi_viewport_js_1.allViewportsPass)(multiViewportScores, adjustedThresholds);
574
+ if (multiViewportScores.screenshotDiffs) {
575
+ for (const diff of multiViewportScores.screenshotDiffs) {
576
+ if (!diff.baselineCreated && diff.diffPercent > 10) {
577
+ failureEvidence.push(`Viewport screenshot: ${diff.viewport} diff ${diff.diffPercent}% exceeds 10% threshold`);
578
+ }
579
+ }
580
+ }
581
+ }
582
+ catch (mvErr) {
583
+ console.error(`Multi-viewport error: ${mvErr instanceof Error ? mvErr.message : String(mvErr)}`);
584
+ }
585
+ }
586
+ // Security audit (npm audit)
587
+ try {
588
+ securityAuditResult = await (0, security_audit_js_1.runSecurityAudit)(args.projectDir);
589
+ if (securityAuditResult.critical > 0 || securityAuditResult.high > 0) {
590
+ failureEvidence.push(`Security: ${securityAuditResult.summary}`);
591
+ }
592
+ }
593
+ catch (secErr) {
594
+ console.error(`Security audit error: ${secErr instanceof Error ? secErr.message : String(secErr)}`);
595
+ }
596
+ const crawlEnabled = args.crawl || config.crawl;
597
+ const crawlOpts = crawlEnabled
598
+ ? { enabled: true, maxDepth: config.max_crawl_depth, maxPages: config.max_crawl_pages }
599
+ : undefined;
600
+ let lastE2EScenarios;
601
+ try {
602
+ const e2eRuns = await (0, e2e_js_1.runVerifyE2E)(verifyUrl, verificationTier, config.scenarios, crawlOpts);
603
+ lastE2EScenarios = e2eRuns.scenarios;
604
+ e2eResult = {
605
+ passed: e2eRuns.passed,
606
+ failed: e2eRuns.failed,
607
+ total: e2eRuns.results.length,
608
+ results: e2eRuns.results,
609
+ };
610
+ e2eCoverageGaps = e2eRuns.coverageGaps;
611
+ e2eConsoleErrors = e2eRuns.consoleErrors;
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
621
+ if (e2eRuns.crawlResult && e2eRuns.crawlResult.totalLinks > 0) {
622
+ try {
623
+ brokenLinksResult = await (0, broken_links_js_1.auditBrokenLinks)(e2eRuns.crawlResult, verifyUrl);
624
+ if (brokenLinksResult.hasBrokenLinks) {
625
+ failureEvidence.push(`Broken links: ${brokenLinksResult.summary}`);
626
+ }
627
+ }
628
+ catch (blErr) {
629
+ console.error(`Broken links audit error: ${blErr instanceof Error ? blErr.message : String(blErr)}`);
630
+ }
631
+ }
632
+ // E2E stability: run a second time if first run passed all
633
+ if (e2eRuns.passed === e2eRuns.results.length && e2eRuns.results.length > 0) {
634
+ console.log(" Running stability pass (run 2/2)...");
635
+ const e2eRuns2 = await (0, e2e_js_1.runVerifyE2E)(verifyUrl, verificationTier, config.scenarios, crawlOpts);
636
+ if (e2eRuns2.passed < e2eRuns2.results.length) {
637
+ e2eStabilityPassed = false;
638
+ e2eCoverageGaps.push("Stability check failed on second run");
639
+ const failedNames = e2eRuns2.results
640
+ .filter((r) => !r.passed)
641
+ .map((r) => r.name)
642
+ .join(", ");
643
+ failureEvidence.push(`E2E stability: second run failed (${failedNames})`);
644
+ }
645
+ else {
646
+ console.log(" Stability pass: OK (2/2 runs passed)");
647
+ }
648
+ }
649
+ if (e2eRuns.coverageGaps.length > 0) {
650
+ console.error(`E2E coverage warning: ${e2eRuns.coverageGaps.join(" ")}`);
651
+ }
652
+ if (e2eRuns.consoleErrors.length > 0) {
653
+ console.error(`E2E console errors: ${e2eRuns.consoleErrors.length} detected`);
654
+ }
655
+ }
656
+ catch (e2eErr) {
657
+ console.error(`E2E error: ${e2eErr instanceof Error ? e2eErr.message : String(e2eErr)}`);
658
+ }
659
+ // Cross-browser E2E via Playwright (if non-chromium browsers configured)
660
+ const extraBrowsers = (config.browsers || []).filter((b) => b !== "chromium" && ["firefox", "webkit"].includes(b));
661
+ if (extraBrowsers.length > 0 && lastE2EScenarios && lastE2EScenarios.length > 0) {
662
+ const pwAvailable = await (0, playwright_runner_js_1.isPlaywrightAvailable)();
663
+ if (pwAvailable) {
664
+ try {
665
+ crossBrowserResults = await (0, playwright_runner_js_1.runPlaywrightE2E)(verifyUrl, lastE2EScenarios, extraBrowsers);
666
+ for (const cbr of crossBrowserResults) {
667
+ console.log(` Cross-browser ${cbr.browser}: ${cbr.passed}/${cbr.results.length} passed`);
668
+ if (cbr.failed > 0) {
669
+ const failedNames = cbr.results.filter(r => !r.passed).map(r => r.name).join(", ");
670
+ failureEvidence.push(`Cross-browser ${cbr.browser}: ${failedNames} failed`);
671
+ }
672
+ }
673
+ }
674
+ catch (cbErr) {
675
+ console.error(`Cross-browser error: ${cbErr instanceof Error ? cbErr.message : String(cbErr)}`);
676
+ }
677
+ }
678
+ else {
679
+ console.log(" Note: Cross-browser testing requires playwright. Run: npm install -D playwright && npx playwright install");
680
+ }
681
+ }
682
+ try {
683
+ visualDiffResult = await (0, visual_diff_js_1.runVisualDiff)(args.projectDir, verifyUrl, "verify");
684
+ }
685
+ catch (visualErr) {
686
+ console.error(`Visual diff error: ${visualErr instanceof Error ? visualErr.message : String(visualErr)}`);
687
+ }
688
+ }
689
+ catch (serveErr) {
690
+ console.error(`Dev server error: ${serveErr instanceof Error ? serveErr.message : String(serveErr)}`);
691
+ }
692
+ finally {
693
+ if (servePid) {
694
+ await cleanupActiveDevServer();
695
+ }
696
+ }
697
+ }
698
+ const verificationTier = (0, index_js_1.planToVerificationTier)(effectiveFeatures.plan);
699
+ const viewportSummary = summarizeViewportIssues(multiViewportScores, adjustedThresholds);
700
+ failureEvidence.push(...buildResult.errors.slice(0, 3).map((error) => `Build: ${error}`), ...(lighthouseErrorCount > 0 ? [`Lighthouse: ${lighthouseErrorCount} run error(s) were recorded during collection.`] : []), ...(e2eResult
701
+ ? e2eResult.results
702
+ .filter((scenario) => !scenario.passed)
703
+ .slice(0, 2)
704
+ .map((scenario) => `E2E: ${scenario.name}${scenario.error ? ` - ${scenario.error}` : ""}`)
705
+ : []), ...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
706
+ ? [
707
+ `Visual diff: ${(0, visual_diff_js_1.formatVisualDiffSummary)(visualDiffResult)}`,
708
+ ]
709
+ : []));
710
+ const verificationReport = (0, index_js_1.buildVerificationReport)({
711
+ buildSuccess: buildResult.success,
712
+ buildErrors: buildResult.errors,
713
+ e2ePassed: e2eResult?.passed,
714
+ e2eTotal: e2eResult?.total,
715
+ e2eCoverageGaps,
716
+ e2eConsoleErrorCount: e2eConsoleErrors.length,
717
+ e2eStabilityPassed,
718
+ lighthouseSkipped: args.skipLighthouse,
719
+ lighthouseErrorCount,
720
+ viewportIssues: multiViewportScores ? viewportSummary.count : undefined,
721
+ multiViewportPassed: multiViewportScores ? allViewportsOk : undefined,
722
+ multiViewportSummary: multiViewportScores ? viewportSummary.summary : undefined,
723
+ visualDiffVerdict: visualDiffResult?.verdict,
724
+ visualDiffPercentage: visualDiffResult?.diffPercentage,
725
+ hasVisualBaseline: visualDiffResult?.hasBaseline,
726
+ lighthouseScores: scores,
727
+ mobileLighthouseScores: mobileLighthouseScores ?? undefined,
728
+ securityAudit: securityAuditResult
729
+ ? {
730
+ totalVulnerabilities: securityAuditResult.totalVulnerabilities,
731
+ critical: securityAuditResult.critical,
732
+ high: securityAuditResult.high,
733
+ summary: securityAuditResult.summary,
734
+ }
735
+ : undefined,
736
+ brokenLinksAudit: brokenLinksResult
737
+ ? {
738
+ checkedCount: brokenLinksResult.checkedCount,
739
+ brokenCount: brokenLinksResult.brokenLinks.length,
740
+ summary: brokenLinksResult.summary,
741
+ }
742
+ : undefined,
743
+ failureEvidence,
744
+ }, {
745
+ tier: verificationTier,
746
+ thresholds: adjustedThresholds,
747
+ });
748
+ const verificationView = (0, index_js_1.getTierVerificationView)(verificationReport);
749
+ const unifiedGrade = verificationReport.grade;
750
+ const exitCode = shouldFailVerificationResult(verificationReport, config.fail_on) ? 1 : 0;
751
+ const resultObj = {
752
+ grade: unifiedGrade.charAt(0).toUpperCase() + unifiedGrade.slice(1),
753
+ timestamp: new Date().toISOString(),
754
+ build: {
755
+ success: buildResult.success,
756
+ durationMs: buildResult.durationMs,
757
+ errors: buildResult.errors,
758
+ },
759
+ e2e: e2eResult,
760
+ crossBrowser: crossBrowserResults,
761
+ lighthouse: lighthouseResult,
762
+ mobileLighthouse: mobileLighthouseScores,
763
+ security: securityAuditResult,
764
+ visualDiff: visualDiffResult,
765
+ thresholds: adjustedThresholds,
766
+ ciMode: config.ciMode,
767
+ framework: detected.framework,
768
+ exitCode,
769
+ config_fail_on: config.fail_on,
770
+ _plan: effectiveFeatures.plan,
771
+ _verbose_failure: true,
772
+ _failure_analysis: true,
773
+ verification: {
774
+ tier: verificationTier,
775
+ report: verificationReport,
776
+ view: verificationView,
777
+ },
778
+ };
779
+ const markdownReportPath = (0, report_markdown_js_1.getMarkdownReportPath)(args.projectDir);
780
+ if ((0, report_markdown_js_1.shouldWriteMarkdownReport)(resultObj)) {
781
+ const markdownReport = (0, report_markdown_js_1.buildMarkdownReport)(args.projectDir, resultObj);
782
+ fs.writeFileSync(markdownReportPath, markdownReport, "utf-8");
783
+ resultObj.markdownReportPath = markdownReportPath;
784
+ }
785
+ else if (fs.existsSync(markdownReportPath)) {
786
+ fs.rmSync(markdownReportPath, { force: true });
787
+ }
788
+ const inGitHubActions = !!process.env.GITHUB_ACTIONS;
789
+ if (inGitHubActions) {
790
+ try {
791
+ if (process.env.GITHUB_EVENT_NAME === "pull_request") {
792
+ let trendDelta = null;
793
+ const baseSnapshot = (0, trend_js_1.loadBaseSnapshot)((0, trend_js_1.getBaseResultPath)(args.projectDir));
794
+ if (baseSnapshot) {
795
+ const currentSnapshot = {
796
+ grade: resultObj.grade,
797
+ lighthouse: resultObj.lighthouse,
798
+ e2e: resultObj.e2e ? { passed: resultObj.e2e.passed, total: resultObj.e2e.total } : null,
799
+ timestamp: resultObj.timestamp,
800
+ };
801
+ trendDelta = (0, trend_js_1.computeTrendDelta)(currentSnapshot, baseSnapshot);
802
+ }
803
+ await (0, comment_js_1.postPRComment)(resultObj, trendDelta);
804
+ resultObj.github = { status: "comment_posted", grade: resultObj.grade };
805
+ }
806
+ await (0, status_js_1.createStatusCheck)({ grade: resultObj.grade, exitCode: resultObj.exitCode });
807
+ resultObj.github ??= { status: "status_set", grade: resultObj.grade };
808
+ }
809
+ catch (ghErr) {
810
+ console.error(`GitHub API warning: ${ghErr instanceof Error ? ghErr.message : String(ghErr)}`);
811
+ }
812
+ }
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
+ }
855
+ if (args.format === "json") {
856
+ console.log(JSON.stringify(resultObj, null, 2));
857
+ }
858
+ else {
859
+ consoleOutput(resultObj);
860
+ }
861
+ if (inGitHubActions && process.env.GITHUB_OUTPUT) {
862
+ fs.appendFileSync(process.env.GITHUB_OUTPUT, `grade=${resultObj.grade}\n`);
863
+ }
864
+ exitGracefully(exitCode);
865
+ }
866
+ run().catch((err) => {
867
+ console.error(`Fatal error: ${err instanceof Error ? err.message : String(err)}`);
868
+ exitGracefully(1);
869
+ });