laxy-verify 1.3.3 → 1.4.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.
package/README.md CHANGED
@@ -199,7 +199,7 @@ For Free accounts, the CLI prints a tip instead:
199
199
 
200
200
  ```text
201
201
  Tip: Pro tracks your last 30 runs so you can see if Performance or Grade is improving.
202
- https://laxy.app/pricing
202
+ https://laxy-blue.vercel.app/pricing
203
203
  ```
204
204
 
205
205
  ## The decision it helps you make
@@ -432,8 +432,26 @@ It is a pre-merge and pre-release verification layer, not your entire quality sy
432
432
  - a frontend app with a runnable build flow
433
433
  - optional: `playwright` if your project already uses it
434
434
 
435
+ ## Auto-update check
436
+
437
+ When you run `npx laxy-verify .` interactively, the CLI checks npm for a newer version. If an update is available, it prompts:
438
+
439
+ ```text
440
+ laxy-verify update available: 1.3.3 → 1.4.0
441
+ Update now? (y/N):
442
+ ```
443
+
444
+ Press `y` to update and re-run automatically. The check is skipped in CI environments and non-interactive terminals.
445
+
435
446
  ## Changelog
436
447
 
448
+ ### v1.4.0 - Auto-update check and evidence fix
449
+
450
+ - **Auto-update prompt** - the CLI now checks for newer versions on npm at startup and offers a one-step update when running interactively. Skipped automatically in CI and non-TTY environments
451
+ - **Evidence output fix** - fixed `screenshotDiffs: Pundefined Aundefined` in the evidence section caused by iterating non-Lighthouse fields in multi-viewport results
452
+ - **URL fix** - corrected the pricing tip URL to point to the live domain
453
+ - Removed leftover `[Pro+]` and `[Pro]` labels from multi-viewport and mobile Lighthouse output
454
+
437
455
  ### v1.3.3 - Cleaner output and route validation
438
456
 
439
457
  - Removed `Pro` and `Pro+` labels from verification logs so `npx laxy-verify .` prints plan-neutral output
@@ -0,0 +1,66 @@
1
+ export interface AutoFixContext {
2
+ grade: string;
3
+ blockers: Array<{
4
+ title: string;
5
+ severity: string;
6
+ action: string;
7
+ }>;
8
+ lighthouseScores: {
9
+ performance: number;
10
+ accessibility: number;
11
+ seo: number;
12
+ bestPractices: number;
13
+ } | null;
14
+ thresholds: {
15
+ performance: number;
16
+ accessibility: number;
17
+ seo: number;
18
+ bestPractices: number;
19
+ };
20
+ buildErrors: string[];
21
+ e2eFailed: number;
22
+ e2eTotal: number;
23
+ securitySummary?: string;
24
+ /** Source file snippets keyed by relative path */
25
+ fileSnippets: Array<{
26
+ path: string;
27
+ content: string;
28
+ }>;
29
+ }
30
+ export interface CodeFix {
31
+ /** Relative file path to apply the fix to */
32
+ filePath: string;
33
+ /** Description of what this fix does */
34
+ description: string;
35
+ /** The complete replacement content for the file (or section) */
36
+ code: string;
37
+ /** Whether this is a full-file replacement or a targeted patch */
38
+ type: "full" | "patch";
39
+ /** Line range for patch-type fixes */
40
+ startLine?: number;
41
+ endLine?: number;
42
+ }
43
+ export interface AutoFixResult {
44
+ rootCause: string;
45
+ fixes: CodeFix[];
46
+ confidence: "high" | "medium" | "low";
47
+ }
48
+ /**
49
+ * Collect relevant source file snippets for AI context.
50
+ * Picks up to 5 files that are most likely related to the failures.
51
+ */
52
+ export declare function collectFileSnippets(projectDir: string, buildErrors: string[]): Array<{
53
+ path: string;
54
+ content: string;
55
+ }>;
56
+ /**
57
+ * Apply a code fix to the project directory.
58
+ */
59
+ export declare function applyCodeFix(projectDir: string, fix: CodeFix): {
60
+ success: boolean;
61
+ error?: string;
62
+ };
63
+ /**
64
+ * Request AI auto-fix from the Laxy API.
65
+ */
66
+ export declare function requestAutoFix(context: AutoFixContext): Promise<AutoFixResult | null>;
@@ -0,0 +1,176 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.collectFileSnippets = collectFileSnippets;
37
+ exports.applyCodeFix = applyCodeFix;
38
+ exports.requestAutoFix = requestAutoFix;
39
+ /**
40
+ * AI Auto-Fix client (Pro feature).
41
+ *
42
+ * Sends verification failure context plus relevant source files
43
+ * to the Laxy API, which returns code fix suggestions that can
44
+ * be applied directly or pushed as a PR.
45
+ */
46
+ const fs = __importStar(require("node:fs"));
47
+ const path = __importStar(require("node:path"));
48
+ const auth_js_1 = require("./auth.js");
49
+ /**
50
+ * Collect relevant source file snippets for AI context.
51
+ * Picks up to 5 files that are most likely related to the failures.
52
+ */
53
+ function collectFileSnippets(projectDir, buildErrors) {
54
+ const snippets = [];
55
+ const seen = new Set();
56
+ // Extract file paths from build errors
57
+ const errorFilePattern = /(?:^|\n)(\.?[^\s:]+(?:\/[^\s:]+)*\.[a-zA-Z]+)(?::|\s)/g;
58
+ const errorFiles = new Set();
59
+ for (const err of buildErrors) {
60
+ let match;
61
+ const localPattern = new RegExp(errorFilePattern.source, "g");
62
+ while ((match = localPattern.exec(err)) !== null) {
63
+ errorFiles.add(match[1]);
64
+ }
65
+ }
66
+ // Add files from build errors first
67
+ for (const filePath of errorFiles) {
68
+ if (seen.has(filePath) || snippets.length >= 5)
69
+ continue;
70
+ const absPath = path.join(projectDir, filePath);
71
+ if (fs.existsSync(absPath)) {
72
+ try {
73
+ const content = fs.readFileSync(absPath, "utf-8");
74
+ if (content.length <= 20000) {
75
+ snippets.push({ path: filePath, content: content.slice(0, 4000) });
76
+ seen.add(filePath);
77
+ }
78
+ }
79
+ catch { /* ignore */ }
80
+ }
81
+ }
82
+ // Fill remaining slots with common config/entry files
83
+ const commonFiles = [
84
+ "package.json",
85
+ "tsconfig.json",
86
+ "vite.config.ts",
87
+ "vite.config.js",
88
+ "next.config.js",
89
+ "next.config.mjs",
90
+ "src/App.tsx",
91
+ "src/App.jsx",
92
+ "src/main.tsx",
93
+ "src/main.jsx",
94
+ "src/pages/index.tsx",
95
+ "src/pages/_app.tsx",
96
+ "app/page.tsx",
97
+ "app/layout.tsx",
98
+ "index.html",
99
+ ];
100
+ for (const filePath of commonFiles) {
101
+ if (seen.has(filePath) || snippets.length >= 5)
102
+ continue;
103
+ const absPath = path.join(projectDir, filePath);
104
+ if (fs.existsSync(absPath)) {
105
+ try {
106
+ const content = fs.readFileSync(absPath, "utf-8");
107
+ if (content.length <= 20000) {
108
+ snippets.push({ path: filePath, content: content.slice(0, 4000) });
109
+ seen.add(filePath);
110
+ }
111
+ }
112
+ catch { /* ignore */ }
113
+ }
114
+ }
115
+ return snippets;
116
+ }
117
+ /**
118
+ * Apply a code fix to the project directory.
119
+ */
120
+ function applyCodeFix(projectDir, fix) {
121
+ const absPath = path.resolve(projectDir, fix.filePath);
122
+ // Prevent path traversal outside the project directory
123
+ if (!absPath.startsWith(path.resolve(projectDir) + path.sep) && absPath !== path.resolve(projectDir)) {
124
+ return { success: false, error: `Path traversal detected: ${fix.filePath}` };
125
+ }
126
+ if (fix.type === "full") {
127
+ try {
128
+ fs.writeFileSync(absPath, fix.code, "utf-8");
129
+ return { success: true };
130
+ }
131
+ catch (err) {
132
+ return { success: false, error: err instanceof Error ? err.message : "Write failed" };
133
+ }
134
+ }
135
+ // Patch type: replace specific lines
136
+ if (!fs.existsSync(absPath)) {
137
+ return { success: false, error: `File not found: ${fix.filePath}` };
138
+ }
139
+ try {
140
+ const lines = fs.readFileSync(absPath, "utf-8").split("\n");
141
+ const start = (fix.startLine ?? 1) - 1;
142
+ const end = fix.endLine ?? lines.length;
143
+ const newLines = fix.code.split("\n");
144
+ lines.splice(start, end - start, ...newLines);
145
+ fs.writeFileSync(absPath, lines.join("\n"), "utf-8");
146
+ return { success: true };
147
+ }
148
+ catch (err) {
149
+ return { success: false, error: err instanceof Error ? err.message : "Patch failed" };
150
+ }
151
+ }
152
+ /**
153
+ * Request AI auto-fix from the Laxy API.
154
+ */
155
+ async function requestAutoFix(context) {
156
+ const token = (0, auth_js_1.loadToken)();
157
+ if (!token)
158
+ return null;
159
+ try {
160
+ const res = await fetch(`${auth_js_1.LAXY_API_URL}/api/v1/auto-fix`, {
161
+ method: "POST",
162
+ headers: {
163
+ Authorization: `Bearer ${token}`,
164
+ "Content-Type": "application/json",
165
+ },
166
+ body: JSON.stringify(context),
167
+ signal: AbortSignal.timeout(60_000),
168
+ });
169
+ if (!res.ok)
170
+ return null;
171
+ return (await res.json());
172
+ }
173
+ catch {
174
+ return null;
175
+ }
176
+ }
@@ -0,0 +1,58 @@
1
+ import type { TrendDelta } from "./trend.js";
2
+ interface CheckRunResult {
3
+ grade: string;
4
+ verdict: string;
5
+ exitCode: number;
6
+ lighthouse: {
7
+ performance: number;
8
+ accessibility: number;
9
+ seo: number;
10
+ bestPractices: number;
11
+ runs: number;
12
+ } | null;
13
+ thresholds: {
14
+ performance: number;
15
+ accessibility: number;
16
+ seo: number;
17
+ bestPractices: number;
18
+ };
19
+ e2e?: {
20
+ passed: number;
21
+ failed: number;
22
+ total: number;
23
+ } | null;
24
+ security?: {
25
+ critical: number;
26
+ high: number;
27
+ total: number;
28
+ } | null;
29
+ build?: {
30
+ success: boolean;
31
+ durationMs: number;
32
+ };
33
+ blockers: Array<{
34
+ title: string;
35
+ severity: string;
36
+ }>;
37
+ typecheck?: {
38
+ errorCount: number;
39
+ } | null;
40
+ secretScan?: {
41
+ findingsCount: number;
42
+ } | null;
43
+ bundleSize?: {
44
+ advisory: string;
45
+ } | null;
46
+ a11yDeep?: {
47
+ criticalCount: number;
48
+ seriousCount: number;
49
+ } | null;
50
+ brokenLinks?: {
51
+ broken: number;
52
+ checked: number;
53
+ } | null;
54
+ consoleErrors?: number;
55
+ config_fail_on: string;
56
+ }
57
+ export declare function createCheckRun(result: CheckRunResult, trend?: TrendDelta | null): Promise<void>;
58
+ export {};
@@ -0,0 +1,139 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createCheckRun = createCheckRun;
4
+ /**
5
+ * GitHub Check Run API integration (Pro feature).
6
+ *
7
+ * Creates a rich Check Run on the PR that appears in the merge box
8
+ * with expandable details: grade, blockers, Lighthouse scores, E2E,
9
+ * security, and verification verdict.
10
+ */
11
+ const github_js_1 = require("./github.js");
12
+ const trend_js_1 = require("./trend.js");
13
+ function renderCheckRunSummary(result, trend) {
14
+ const lines = [];
15
+ const emoji = result.grade === "Gold" ? "🏆" :
16
+ result.grade === "Silver" ? "✅" :
17
+ result.grade === "Bronze" ? "🔨" : "⚠️";
18
+ lines.push(`## ${emoji} ${result.grade} — ${result.verdict}`);
19
+ lines.push("");
20
+ // Trend table
21
+ if (trend) {
22
+ lines.push("### vs Base Branch");
23
+ lines.push("| Check | This PR | Base | Delta |");
24
+ lines.push("|---|---|---|---|");
25
+ const gradeUp = (0, trend_js_1.gradeIndex)(trend.grade.current) > (0, trend_js_1.gradeIndex)(trend.grade.base);
26
+ const gradeDown = (0, trend_js_1.gradeIndex)(trend.grade.current) < (0, trend_js_1.gradeIndex)(trend.grade.base);
27
+ const gradeDelta = gradeUp ? " ↑" : gradeDown ? " ↓" : "";
28
+ lines.push(`| Grade | **${trend.grade.current}** | ${trend.grade.base} |${gradeDelta} |`);
29
+ const fmtD = (d) => d === null ? "—" : d > 0 ? `+${d}` : d < 0 ? `${d}` : "—";
30
+ if (trend.performance.current !== null)
31
+ lines.push(`| Performance | ${trend.performance.current} | ${trend.performance.base ?? "—"} | ${fmtD(trend.performance.delta)} |`);
32
+ if (trend.accessibility.current !== null)
33
+ lines.push(`| Accessibility | ${trend.accessibility.current} | ${trend.accessibility.base ?? "—"} | ${fmtD(trend.accessibility.delta)} |`);
34
+ if (trend.seo.current !== null)
35
+ lines.push(`| SEO | ${trend.seo.current} | ${trend.seo.base ?? "—"} | ${fmtD(trend.seo.delta)} |`);
36
+ if (trend.bestPractices.current !== null)
37
+ lines.push(`| Best Practices | ${trend.bestPractices.current} | ${trend.bestPractices.base ?? "—"} | ${fmtD(trend.bestPractices.delta)} |`);
38
+ if (trend.e2e.current !== null)
39
+ lines.push(`| E2E | ${trend.e2e.current} | ${trend.e2e.base ?? "—"} | — |`);
40
+ lines.push("");
41
+ }
42
+ // Build
43
+ if (result.build) {
44
+ const dur = (result.build.durationMs / 1000).toFixed(1);
45
+ lines.push(`### Build: ${result.build.success ? "✅ Passed" : "❌ Failed"} (${dur}s)`);
46
+ lines.push("");
47
+ }
48
+ // Lighthouse
49
+ if (result.lighthouse) {
50
+ const lh = result.lighthouse;
51
+ const t = result.thresholds;
52
+ lines.push("### Lighthouse");
53
+ lines.push("| Category | Score | Threshold | Status |");
54
+ lines.push("|---|---|---|---|");
55
+ lines.push(`| Performance | ${lh.performance} | ≥${t.performance} | ${lh.performance >= t.performance ? "✅" : "❌"} |`);
56
+ lines.push(`| Accessibility | ${lh.accessibility} | ≥${t.accessibility} | ${lh.accessibility >= t.accessibility ? "✅" : "❌"} |`);
57
+ lines.push(`| SEO | ${lh.seo} | ≥${t.seo} | ${lh.seo >= t.seo ? "✅" : "❌"} |`);
58
+ lines.push(`| Best Practices | ${lh.bestPractices} | ≥${t.bestPractices} | ${lh.bestPractices >= t.bestPractices ? "✅" : "❌"} |`);
59
+ lines.push("");
60
+ }
61
+ // E2E
62
+ if (result.e2e && result.e2e.total > 0) {
63
+ const e2e = result.e2e;
64
+ lines.push(`### E2E: ${e2e.failed === 0 ? "✅" : "❌"} ${e2e.passed}/${e2e.total} passed`);
65
+ lines.push("");
66
+ }
67
+ // Blockers
68
+ if (result.blockers.length > 0) {
69
+ lines.push("### Blockers");
70
+ for (const b of result.blockers.slice(0, 8)) {
71
+ lines.push(`- **[${b.severity}]** ${b.title}`);
72
+ }
73
+ lines.push("");
74
+ }
75
+ // Extra checks
76
+ const extras = [];
77
+ if (result.security && result.security.total > 0)
78
+ extras.push(`Security: ${result.security.critical} critical, ${result.security.high} high`);
79
+ if (result.typecheck && result.typecheck.errorCount > 0)
80
+ extras.push(`TypeScript: ${result.typecheck.errorCount} errors`);
81
+ if (result.secretScan && result.secretScan.findingsCount > 0)
82
+ extras.push(`Secrets: ${result.secretScan.findingsCount} findings`);
83
+ if (result.a11yDeep && (result.a11yDeep.criticalCount > 0 || result.a11yDeep.seriousCount > 0))
84
+ extras.push(`WCAG: ${result.a11yDeep.criticalCount} critical, ${result.a11yDeep.seriousCount} serious`);
85
+ if (result.brokenLinks && result.brokenLinks.broken > 0)
86
+ extras.push(`Broken links: ${result.brokenLinks.broken}/${result.brokenLinks.checked}`);
87
+ if (result.consoleErrors && result.consoleErrors > 0)
88
+ extras.push(`Console errors: ${result.consoleErrors}`);
89
+ if (extras.length > 0) {
90
+ lines.push("### Additional findings");
91
+ for (const e of extras)
92
+ lines.push(`- ${e}`);
93
+ lines.push("");
94
+ }
95
+ lines.push(`**Fail-on threshold**: ${result.config_fail_on ?? "bronze"}`);
96
+ lines.push("");
97
+ lines.push("---");
98
+ lines.push("[laxy-verify docs](https://github.com/SUNgm24/Laxy/tree/main/laxy-verify)");
99
+ return lines.join("\n");
100
+ }
101
+ async function createCheckRun(result, trend) {
102
+ const ctx = (0, github_js_1.getGitHubContext)();
103
+ if (!ctx)
104
+ return;
105
+ const [owner, repo] = ctx.repository.split("/");
106
+ const conclusion = result.exitCode === 0 ? "success" : "failure";
107
+ const title = result.exitCode === 0
108
+ ? `Laxy Verify: ${result.grade} — no blockers`
109
+ : `Laxy Verify: ${result.grade} — ${result.blockers.length} blocker(s)`;
110
+ const summary = renderCheckRunSummary(result, trend);
111
+ // GitHub Check Runs API
112
+ const url = `https://api.github.com/repos/${owner}/${repo}/check-runs`;
113
+ try {
114
+ const res = await fetch(url, {
115
+ method: "POST",
116
+ headers: {
117
+ Authorization: `Bearer ${ctx.token}`,
118
+ Accept: "application/vnd.github.v3+json",
119
+ "Content-Type": "application/json",
120
+ },
121
+ body: JSON.stringify({
122
+ name: "laxy-verify",
123
+ head_sha: ctx.sha,
124
+ status: "completed",
125
+ conclusion,
126
+ output: {
127
+ title,
128
+ summary,
129
+ },
130
+ }),
131
+ });
132
+ if (!res.ok) {
133
+ console.warn(`GitHub Check Run API returned ${res.status}; falling back to status check.`);
134
+ }
135
+ }
136
+ catch (err) {
137
+ console.warn(`GitHub Check Run request failed: ${err instanceof Error ? err.message : String(err)}`);
138
+ }
139
+ }
package/dist/cli.js CHANGED
@@ -48,6 +48,7 @@ const grade_js_1 = require("./grade.js");
48
48
  const init_js_1 = require("./init.js");
49
49
  const badge_js_1 = require("./badge.js");
50
50
  const comment_js_1 = require("./comment.js");
51
+ const check_run_js_1 = require("./check-run.js");
51
52
  const status_js_1 = require("./status.js");
52
53
  const auth_js_1 = require("./auth.js");
53
54
  const entitlement_js_1 = require("./entitlement.js");
@@ -68,8 +69,10 @@ const vitals_budget_js_1 = require("./vitals-budget.js");
68
69
  const index_js_1 = require("./verification-core/index.js");
69
70
  const trend_js_1 = require("./trend.js");
70
71
  const ai_analysis_js_1 = require("./ai-analysis.js");
72
+ const auto_fix_js_1 = require("./auto-fix.js");
71
73
  const compare_env_js_1 = require("./compare-env.js");
72
74
  const route_discovery_js_1 = require("./route-discovery.js");
75
+ const update_check_js_1 = require("./update-check.js");
73
76
  const package_json_1 = __importDefault(require("../package.json"));
74
77
  let activeDevServerPid;
75
78
  let activeDevServerCleanup = null;
@@ -173,6 +176,7 @@ function parseArgs() {
173
176
  a11yDeep: flags["a11y-deep"] !== undefined,
174
177
  seoDeep: flags["seo-deep"] !== undefined,
175
178
  vitalsBudget: flags["vitals-budget"] !== undefined,
179
+ autoFix: flags["auto-fix"] !== undefined,
176
180
  };
177
181
  }
178
182
  function writeResultFile(projectDir, result) {
@@ -200,8 +204,10 @@ function uniqueRouteList(routes) {
200
204
  function summarizeViewportIssues(scores, thresholds) {
201
205
  if (!scores)
202
206
  return { count: 0 };
207
+ const viewportKeys = ["desktop", "tablet", "mobile"];
203
208
  const failed = [];
204
- for (const [label, viewportScores] of Object.entries(scores)) {
209
+ for (const label of viewportKeys) {
210
+ const viewportScores = scores[label];
205
211
  if (!viewportScores) {
206
212
  failed.push(`${label}: missing`);
207
213
  continue;
@@ -399,7 +405,7 @@ async function run() {
399
405
  --config <path> Path to .laxy.yml
400
406
  --fail-on unverified | bronze | silver | gold
401
407
  --skip-lighthouse Skip Lighthouse but still run build and E2E
402
- --port <port> Use an already-running dev server on this port (skip build & server start)
408
+ --port <port> Use an already-running dev server on this port (skip build & server start)
403
409
  --share Create a public share link for this verification result (Pro)
404
410
  --compare <url> Compare Lighthouse scores against a reference environment URL (Pro)
405
411
  --plan-override free | pro | team (testing metadata only)
@@ -413,6 +419,7 @@ async function run() {
413
419
  --a11y-deep Deep accessibility audit (axe-core)
414
420
  --seo-deep Deep SEO audit (meta, OG, JSON-LD)
415
421
  --vitals-budget Core Web Vitals budget check (LCP, CLS, INP)
422
+ --auto-fix AI code fix suggestions for blockers (Pro)
416
423
  --help Show this help
417
424
 
418
425
  Exit codes:
@@ -421,10 +428,10 @@ async function run() {
421
428
  2 Configuration error
422
429
 
423
430
  Examples:
424
- npx laxy-verify --init --run # Setup + first verification
425
- npx laxy-verify . # Run in current directory
426
- npx laxy-verify . # Auto-falls back if port 3000 is already busy
427
- npx laxy-verify . --ci # CI mode
431
+ npx laxy-verify --init --run # Setup + first verification
432
+ npx laxy-verify . # Run in current directory
433
+ npx laxy-verify . # Auto-falls back if port 3000 is already busy
434
+ npx laxy-verify . --ci # CI mode
428
435
  npx laxy-verify . --fail-on silver # Block Bronze or worse
429
436
  npx laxy-verify . --port 3001 # Use existing dev server on port 3001
430
437
  npx laxy-verify . --share # Save and share a public verification link
@@ -450,6 +457,28 @@ async function run() {
450
457
  exitGracefully(0);
451
458
  return;
452
459
  }
460
+ // Check for updates (skip in CI mode and non-interactive environments)
461
+ if (!args.ciMode && !process.env.CI && process.stdin.isTTY) {
462
+ const updated = await (0, update_check_js_1.checkForUpdates)();
463
+ if (updated) {
464
+ // Re-exec with npx to use the updated version
465
+ const { execSync } = await Promise.resolve().then(() => __importStar(require("node:child_process")));
466
+ try {
467
+ execSync(`npx laxy-verify ${process.argv.slice(2).join(" ")}`, {
468
+ stdio: "inherit",
469
+ cwd: process.cwd(),
470
+ });
471
+ exitGracefully(0);
472
+ return;
473
+ }
474
+ catch (reExecErr) {
475
+ // If re-exec fails, the child already printed errors — propagate exit code
476
+ const code = reExecErr.status ?? 1;
477
+ exitGracefully(code);
478
+ return;
479
+ }
480
+ }
481
+ }
453
482
  if (args.init) {
454
483
  let initEntitlements = null;
455
484
  try {
@@ -625,6 +654,8 @@ async function run() {
625
654
  compare_env: false,
626
655
  share_result: false,
627
656
  history_trend: false,
657
+ pr_decoration: false,
658
+ auto_fix: false,
628
659
  };
629
660
  let effectiveFeatures = features;
630
661
  if (args.planOverride) {
@@ -1134,7 +1165,6 @@ async function run() {
1134
1165
  if ((0, report_markdown_js_1.shouldWriteMarkdownReport)(resultObj)) {
1135
1166
  const markdownReport = (0, report_markdown_js_1.buildMarkdownReport)(args.projectDir, resultObj);
1136
1167
  fs.writeFileSync(markdownReportPath, markdownReport, "utf-8");
1137
- resultObj.markdownReportPath = markdownReportPath;
1138
1168
  }
1139
1169
  else if (fs.existsSync(markdownReportPath)) {
1140
1170
  fs.rmSync(markdownReportPath, { force: true });
@@ -1142,8 +1172,8 @@ async function run() {
1142
1172
  const inGitHubActions = !!process.env.GITHUB_ACTIONS;
1143
1173
  if (inGitHubActions) {
1144
1174
  try {
1175
+ let trendDelta = null;
1145
1176
  if (process.env.GITHUB_EVENT_NAME === "pull_request") {
1146
- let trendDelta = null;
1147
1177
  const baseSnapshot = (0, trend_js_1.loadBaseSnapshot)((0, trend_js_1.getBaseResultPath)(args.projectDir));
1148
1178
  if (baseSnapshot) {
1149
1179
  const currentSnapshot = {
@@ -1154,11 +1184,41 @@ async function run() {
1154
1184
  };
1155
1185
  trendDelta = (0, trend_js_1.computeTrendDelta)(currentSnapshot, baseSnapshot);
1156
1186
  }
1157
- await (0, comment_js_1.postPRComment)(resultObj, trendDelta);
1158
- resultObj.github = { status: "comment_posted", grade: resultObj.grade };
1159
1187
  }
1160
- await (0, status_js_1.createStatusCheck)({ grade: resultObj.grade, exitCode: resultObj.exitCode });
1161
- resultObj.github ??= { status: "status_set", grade: resultObj.grade };
1188
+ // Pro: Check Run (rich merge-box integration) + enhanced PR comment
1189
+ if (effectiveFeatures.pr_decoration) {
1190
+ if (process.env.GITHUB_EVENT_NAME === "pull_request") {
1191
+ await (0, check_run_js_1.createCheckRun)({
1192
+ grade: resultObj.grade,
1193
+ verdict: verificationReport.verdict,
1194
+ exitCode: resultObj.exitCode,
1195
+ lighthouse: resultObj.lighthouse,
1196
+ thresholds: adjustedThresholds,
1197
+ e2e: resultObj.e2e,
1198
+ security: securityAuditResult ? { critical: securityAuditResult.critical, high: securityAuditResult.high, total: securityAuditResult.totalVulnerabilities } : undefined,
1199
+ build: { success: buildResult.success, durationMs: buildResult.durationMs },
1200
+ blockers: verificationView.blockers.slice(0, 10).map((b) => ({ title: b.title, severity: b.severity })),
1201
+ typecheck: typecheckResult ? { errorCount: typecheckResult.errorCount } : undefined,
1202
+ secretScan: secretScanResult ? { findingsCount: secretScanResult.findings.length } : undefined,
1203
+ bundleSize: bundleSizeResult ? { advisory: bundleSizeResult.advisory } : undefined,
1204
+ a11yDeep: a11yDeepResult ? { criticalCount: a11yDeepResult.criticalCount, seriousCount: a11yDeepResult.seriousCount } : undefined,
1205
+ brokenLinks: brokenLinksResult ? { broken: brokenLinksResult.brokenLinks.length, checked: brokenLinksResult.checkedCount } : undefined,
1206
+ consoleErrors: e2eConsoleErrors.length,
1207
+ config_fail_on: config.fail_on,
1208
+ }, trendDelta);
1209
+ await (0, comment_js_1.postPRComment)(resultObj, trendDelta);
1210
+ resultObj.github = { status: "check_run_created", grade: resultObj.grade };
1211
+ }
1212
+ }
1213
+ else {
1214
+ // Free: basic status check + simple PR comment
1215
+ if (process.env.GITHUB_EVENT_NAME === "pull_request") {
1216
+ await (0, comment_js_1.postPRComment)(resultObj, trendDelta);
1217
+ resultObj.github = { status: "comment_posted", grade: resultObj.grade };
1218
+ }
1219
+ await (0, status_js_1.createStatusCheck)({ grade: resultObj.grade, exitCode: resultObj.exitCode });
1220
+ resultObj.github ??= { status: "status_set", grade: resultObj.grade };
1221
+ }
1162
1222
  }
1163
1223
  catch (ghErr) {
1164
1224
  console.error(`GitHub API warning: ${ghErr instanceof Error ? ghErr.message : String(ghErr)}`);
@@ -1263,6 +1323,79 @@ async function run() {
1263
1323
  // AI analysis errors are non-critical
1264
1324
  }
1265
1325
  }
1326
+ // Pro: AI Auto-Fix — collect source files, request code fixes, optionally apply
1327
+ if (effectiveFeatures.auto_fix &&
1328
+ args.autoFix &&
1329
+ verificationReport.verdict !== "client-ready" &&
1330
+ verificationReport.verdict !== "release-ready" &&
1331
+ args.format !== "json") {
1332
+ try {
1333
+ console.log("\n Requesting AI Auto-Fix...");
1334
+ console.log(" Note: Source files will be sent to an external AI provider for analysis.");
1335
+ const fileSnippets = (0, auto_fix_js_1.collectFileSnippets)(args.projectDir, buildResult.errors);
1336
+ const autoFixResult = await (0, auto_fix_js_1.requestAutoFix)({
1337
+ grade: unifiedGrade,
1338
+ blockers: verificationView.blockers.map((b) => ({
1339
+ title: b.title,
1340
+ severity: b.severity,
1341
+ action: b.action,
1342
+ })),
1343
+ lighthouseScores: scores ?? null,
1344
+ thresholds: adjustedThresholds,
1345
+ buildErrors: buildResult.errors.slice(0, 3),
1346
+ e2eFailed: e2eResult ? e2eResult.failed : 0,
1347
+ e2eTotal: e2eResult ? e2eResult.total : 0,
1348
+ securitySummary: securityAuditResult?.summary,
1349
+ fileSnippets,
1350
+ });
1351
+ if (autoFixResult && autoFixResult.fixes.length > 0) {
1352
+ console.log(`\n AI Auto-Fix (Pro) — confidence: ${autoFixResult.confidence}`);
1353
+ console.log(` Root cause: ${autoFixResult.rootCause}`);
1354
+ console.log(` Suggested fixes:`);
1355
+ for (const fix of autoFixResult.fixes) {
1356
+ console.log(` - ${fix.filePath}: ${fix.description} (${fix.type})`);
1357
+ }
1358
+ // In CI with GITHUB_ACTIONS: write fixes as patch files for PR creation
1359
+ if (inGitHubActions) {
1360
+ const patchDir = path.join(args.projectDir, ".laxy-fixes");
1361
+ fs.mkdirSync(patchDir, { recursive: true });
1362
+ for (const fix of autoFixResult.fixes) {
1363
+ const fixPath = path.join(patchDir, fix.filePath.replace(/\//g, "_") + ".patch.json");
1364
+ fs.writeFileSync(fixPath, JSON.stringify(fix, null, 2), "utf-8");
1365
+ }
1366
+ console.log(` Fixes written to .laxy-fixes/ for PR automation.`);
1367
+ }
1368
+ else {
1369
+ // Interactive: apply fixes to local project
1370
+ console.log(`\n Applying fixes to local project...`);
1371
+ let applied = 0;
1372
+ for (const fix of autoFixResult.fixes) {
1373
+ const result = (0, auto_fix_js_1.applyCodeFix)(args.projectDir, fix);
1374
+ if (result.success) {
1375
+ console.log(` ✓ ${fix.filePath}`);
1376
+ applied++;
1377
+ }
1378
+ else {
1379
+ console.log(` ✗ ${fix.filePath}: ${result.error}`);
1380
+ }
1381
+ }
1382
+ if (applied > 0) {
1383
+ console.log(`\n ${applied}/${autoFixResult.fixes.length} fix(es) applied. Rerun verification to check results.`);
1384
+ }
1385
+ }
1386
+ }
1387
+ else if (autoFixResult) {
1388
+ console.log(`\n AI Auto-Fix: ${autoFixResult.rootCause}`);
1389
+ console.log(` No concrete code fixes could be generated (confidence: ${autoFixResult.confidence}).`);
1390
+ }
1391
+ }
1392
+ catch {
1393
+ // Auto-fix errors are non-critical
1394
+ }
1395
+ }
1396
+ else if (args.autoFix && !effectiveFeatures.auto_fix) {
1397
+ console.log("\n [warn] --auto-fix requires a logged-in Pro account. Skipping AI auto-fix.");
1398
+ }
1266
1399
  if (args.format === "json") {
1267
1400
  console.log(JSON.stringify(resultObj, null, 2));
1268
1401
  }
@@ -1276,7 +1409,7 @@ async function run() {
1276
1409
  }
1277
1410
  if (!effectiveFeatures.history_trend) {
1278
1411
  console.log(`\n Tip: Pro tracks your last 30 runs so you can see if Performance or Grade is improving.`);
1279
- console.log(` https://laxy.app/pricing`);
1412
+ console.log(` https://laxy-blue.vercel.app/pricing`);
1280
1413
  }
1281
1414
  }
1282
1415
  if (inGitHubActions && process.env.GITHUB_OUTPUT) {
@@ -7,6 +7,8 @@ export interface EntitlementFeatures {
7
7
  compare_env: boolean;
8
8
  share_result: boolean;
9
9
  history_trend: boolean;
10
+ pr_decoration: boolean;
11
+ auto_fix: boolean;
10
12
  }
11
13
  export type TestablePlan = "free" | "pro" | "team";
12
14
  export declare function normalizePlan(plan?: string | null): TestablePlan;
@@ -22,6 +22,8 @@ const FREE_FEATURES = {
22
22
  compare_env: false,
23
23
  share_result: false,
24
24
  history_trend: false,
25
+ pr_decoration: false,
26
+ auto_fix: false,
25
27
  };
26
28
  let cache = null;
27
29
  const CACHE_TTL_MS = 5 * 60 * 1000;
@@ -80,6 +82,8 @@ function applyPlanOverride(features, overridePlan) {
80
82
  compare_env: isPro,
81
83
  share_result: isPro,
82
84
  history_trend: isPro,
85
+ pr_decoration: isPro,
86
+ auto_fix: isPro,
83
87
  // queue_priority, parallel_execution — Team only
84
88
  queue_priority: isTeam,
85
89
  parallel_execution: isTeam,
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Checks for updates and prompts the user.
3
+ * Returns true if the user updated (caller should re-exec with npx).
4
+ * Returns false if no update needed or user declined.
5
+ */
6
+ export declare function checkForUpdates(): Promise<boolean>;
@@ -0,0 +1,138 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.checkForUpdates = checkForUpdates;
37
+ /**
38
+ * Check for newer versions of laxy-verify on npm.
39
+ * Prompts the user to update if a newer version is available.
40
+ */
41
+ const node_child_process_1 = require("node:child_process");
42
+ const fs = __importStar(require("node:fs"));
43
+ const readline = __importStar(require("node:readline"));
44
+ const PACKAGE_NAME = "laxy-verify";
45
+ function getInstalledVersion() {
46
+ const pkgPath = require.resolve(`${PACKAGE_NAME}/package.json`);
47
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
48
+ return pkg.version;
49
+ }
50
+ function getLatestVersion() {
51
+ try {
52
+ const result = (0, node_child_process_1.execSync)(`npm view ${PACKAGE_NAME} version`, {
53
+ encoding: "utf-8",
54
+ timeout: 10_000,
55
+ stdio: ["pipe", "pipe", "pipe"],
56
+ });
57
+ return result.trim();
58
+ }
59
+ catch {
60
+ return null;
61
+ }
62
+ }
63
+ function compareVersions(installed, latest) {
64
+ const a = installed.split(".").map(Number);
65
+ const b = latest.split(".").map(Number);
66
+ for (let i = 0; i < 3; i++) {
67
+ if ((a[i] ?? 0) < (b[i] ?? 0))
68
+ return -1;
69
+ if ((a[i] ?? 0) > (b[i] ?? 0))
70
+ return 1;
71
+ }
72
+ return 0;
73
+ }
74
+ function promptUser(question) {
75
+ const rl = readline.createInterface({
76
+ input: process.stdin,
77
+ output: process.stdout,
78
+ });
79
+ return new Promise((resolve) => {
80
+ rl.question(question, (answer) => {
81
+ rl.close();
82
+ resolve(answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes");
83
+ });
84
+ });
85
+ }
86
+ function runUpdate() {
87
+ try {
88
+ console.log(` Updating ${PACKAGE_NAME}...`);
89
+ (0, node_child_process_1.execSync)(`npm install -g ${PACKAGE_NAME}@latest`, {
90
+ encoding: "utf-8",
91
+ timeout: 60_000,
92
+ stdio: "inherit",
93
+ });
94
+ return true;
95
+ }
96
+ catch {
97
+ // Global install failed — try local update
98
+ try {
99
+ (0, node_child_process_1.execSync)(`npm install ${PACKAGE_NAME}@latest`, {
100
+ encoding: "utf-8",
101
+ timeout: 60_000,
102
+ stdio: "inherit",
103
+ });
104
+ return true;
105
+ }
106
+ catch {
107
+ return false;
108
+ }
109
+ }
110
+ }
111
+ /**
112
+ * Checks for updates and prompts the user.
113
+ * Returns true if the user updated (caller should re-exec with npx).
114
+ * Returns false if no update needed or user declined.
115
+ */
116
+ async function checkForUpdates() {
117
+ const installed = getInstalledVersion();
118
+ const latest = getLatestVersion();
119
+ if (!latest)
120
+ return false;
121
+ if (compareVersions(installed, latest) >= 0)
122
+ return false;
123
+ console.log(`\n laxy-verify update available: ${installed} → ${latest}`);
124
+ const shouldUpdate = await promptUser(" Update now? (y/N): ");
125
+ if (!shouldUpdate) {
126
+ console.log(" Skipping update.\n");
127
+ return false;
128
+ }
129
+ const success = runUpdate();
130
+ if (success) {
131
+ console.log(` Updated to ${PACKAGE_NAME}@${latest}. Re-running verification...\n`);
132
+ return true;
133
+ }
134
+ else {
135
+ console.error(` Update failed. Continuing with ${installed}.\n`);
136
+ return false;
137
+ }
138
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "laxy-verify",
3
- "version": "1.3.3",
3
+ "version": "1.4.0",
4
4
  "description": "Frontend verification CLI for build checks, Lighthouse, E2E, and release readiness",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",