laxy-verify 1.3.3 → 1.4.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
@@ -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,35 @@ 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 { spawnSync } = await Promise.resolve().then(() => __importStar(require("node:child_process")));
466
+ try {
467
+ const reExecResult = spawnSync("npx", ["laxy-verify", ...process.argv.slice(2)], {
468
+ stdio: "inherit",
469
+ cwd: process.cwd(),
470
+ });
471
+ if (reExecResult.error)
472
+ throw reExecResult.error;
473
+ if (reExecResult.status !== 0) {
474
+ const err = new Error("Re-exec failed");
475
+ err.status = reExecResult.status;
476
+ throw err;
477
+ }
478
+ exitGracefully(0);
479
+ return;
480
+ }
481
+ catch (reExecErr) {
482
+ // If re-exec fails, the child already printed errors — propagate exit code
483
+ const code = reExecErr.status ?? 1;
484
+ exitGracefully(code);
485
+ return;
486
+ }
487
+ }
488
+ }
453
489
  if (args.init) {
454
490
  let initEntitlements = null;
455
491
  try {
@@ -625,6 +661,8 @@ async function run() {
625
661
  compare_env: false,
626
662
  share_result: false,
627
663
  history_trend: false,
664
+ pr_decoration: false,
665
+ auto_fix: false,
628
666
  };
629
667
  let effectiveFeatures = features;
630
668
  if (args.planOverride) {
@@ -1134,7 +1172,6 @@ async function run() {
1134
1172
  if ((0, report_markdown_js_1.shouldWriteMarkdownReport)(resultObj)) {
1135
1173
  const markdownReport = (0, report_markdown_js_1.buildMarkdownReport)(args.projectDir, resultObj);
1136
1174
  fs.writeFileSync(markdownReportPath, markdownReport, "utf-8");
1137
- resultObj.markdownReportPath = markdownReportPath;
1138
1175
  }
1139
1176
  else if (fs.existsSync(markdownReportPath)) {
1140
1177
  fs.rmSync(markdownReportPath, { force: true });
@@ -1142,8 +1179,8 @@ async function run() {
1142
1179
  const inGitHubActions = !!process.env.GITHUB_ACTIONS;
1143
1180
  if (inGitHubActions) {
1144
1181
  try {
1182
+ let trendDelta = null;
1145
1183
  if (process.env.GITHUB_EVENT_NAME === "pull_request") {
1146
- let trendDelta = null;
1147
1184
  const baseSnapshot = (0, trend_js_1.loadBaseSnapshot)((0, trend_js_1.getBaseResultPath)(args.projectDir));
1148
1185
  if (baseSnapshot) {
1149
1186
  const currentSnapshot = {
@@ -1154,11 +1191,41 @@ async function run() {
1154
1191
  };
1155
1192
  trendDelta = (0, trend_js_1.computeTrendDelta)(currentSnapshot, baseSnapshot);
1156
1193
  }
1157
- await (0, comment_js_1.postPRComment)(resultObj, trendDelta);
1158
- resultObj.github = { status: "comment_posted", grade: resultObj.grade };
1159
1194
  }
1160
- await (0, status_js_1.createStatusCheck)({ grade: resultObj.grade, exitCode: resultObj.exitCode });
1161
- resultObj.github ??= { status: "status_set", grade: resultObj.grade };
1195
+ // Pro: Check Run (rich merge-box integration) + enhanced PR comment
1196
+ if (effectiveFeatures.pr_decoration) {
1197
+ if (process.env.GITHUB_EVENT_NAME === "pull_request") {
1198
+ await (0, check_run_js_1.createCheckRun)({
1199
+ grade: resultObj.grade,
1200
+ verdict: verificationReport.verdict,
1201
+ exitCode: resultObj.exitCode,
1202
+ lighthouse: resultObj.lighthouse,
1203
+ thresholds: adjustedThresholds,
1204
+ e2e: resultObj.e2e,
1205
+ security: securityAuditResult ? { critical: securityAuditResult.critical, high: securityAuditResult.high, total: securityAuditResult.totalVulnerabilities } : undefined,
1206
+ build: { success: buildResult.success, durationMs: buildResult.durationMs },
1207
+ blockers: verificationView.blockers.slice(0, 10).map((b) => ({ title: b.title, severity: b.severity })),
1208
+ typecheck: typecheckResult ? { errorCount: typecheckResult.errorCount } : undefined,
1209
+ secretScan: secretScanResult ? { findingsCount: secretScanResult.findings.length } : undefined,
1210
+ bundleSize: bundleSizeResult ? { advisory: bundleSizeResult.advisory } : undefined,
1211
+ a11yDeep: a11yDeepResult ? { criticalCount: a11yDeepResult.criticalCount, seriousCount: a11yDeepResult.seriousCount } : undefined,
1212
+ brokenLinks: brokenLinksResult ? { broken: brokenLinksResult.brokenLinks.length, checked: brokenLinksResult.checkedCount } : undefined,
1213
+ consoleErrors: e2eConsoleErrors.length,
1214
+ config_fail_on: config.fail_on,
1215
+ }, trendDelta);
1216
+ await (0, comment_js_1.postPRComment)(resultObj, trendDelta);
1217
+ resultObj.github = { status: "check_run_created", grade: resultObj.grade };
1218
+ }
1219
+ }
1220
+ else {
1221
+ // Free: basic status check + simple PR comment
1222
+ if (process.env.GITHUB_EVENT_NAME === "pull_request") {
1223
+ await (0, comment_js_1.postPRComment)(resultObj, trendDelta);
1224
+ resultObj.github = { status: "comment_posted", grade: resultObj.grade };
1225
+ }
1226
+ await (0, status_js_1.createStatusCheck)({ grade: resultObj.grade, exitCode: resultObj.exitCode });
1227
+ resultObj.github ??= { status: "status_set", grade: resultObj.grade };
1228
+ }
1162
1229
  }
1163
1230
  catch (ghErr) {
1164
1231
  console.error(`GitHub API warning: ${ghErr instanceof Error ? ghErr.message : String(ghErr)}`);
@@ -1263,6 +1330,79 @@ async function run() {
1263
1330
  // AI analysis errors are non-critical
1264
1331
  }
1265
1332
  }
1333
+ // Pro: AI Auto-Fix — collect source files, request code fixes, optionally apply
1334
+ if (effectiveFeatures.auto_fix &&
1335
+ args.autoFix &&
1336
+ verificationReport.verdict !== "client-ready" &&
1337
+ verificationReport.verdict !== "release-ready" &&
1338
+ args.format !== "json") {
1339
+ try {
1340
+ console.log("\n Requesting AI Auto-Fix...");
1341
+ console.log(" Note: Source files will be sent to an external AI provider for analysis.");
1342
+ const fileSnippets = (0, auto_fix_js_1.collectFileSnippets)(args.projectDir, buildResult.errors);
1343
+ const autoFixResult = await (0, auto_fix_js_1.requestAutoFix)({
1344
+ grade: unifiedGrade,
1345
+ blockers: verificationView.blockers.map((b) => ({
1346
+ title: b.title,
1347
+ severity: b.severity,
1348
+ action: b.action,
1349
+ })),
1350
+ lighthouseScores: scores ?? null,
1351
+ thresholds: adjustedThresholds,
1352
+ buildErrors: buildResult.errors.slice(0, 3),
1353
+ e2eFailed: e2eResult ? e2eResult.failed : 0,
1354
+ e2eTotal: e2eResult ? e2eResult.total : 0,
1355
+ securitySummary: securityAuditResult?.summary,
1356
+ fileSnippets,
1357
+ });
1358
+ if (autoFixResult && autoFixResult.fixes.length > 0) {
1359
+ console.log(`\n AI Auto-Fix (Pro) — confidence: ${autoFixResult.confidence}`);
1360
+ console.log(` Root cause: ${autoFixResult.rootCause}`);
1361
+ console.log(` Suggested fixes:`);
1362
+ for (const fix of autoFixResult.fixes) {
1363
+ console.log(` - ${fix.filePath}: ${fix.description} (${fix.type})`);
1364
+ }
1365
+ // In CI with GITHUB_ACTIONS: write fixes as patch files for PR creation
1366
+ if (inGitHubActions) {
1367
+ const patchDir = path.join(args.projectDir, ".laxy-fixes");
1368
+ fs.mkdirSync(patchDir, { recursive: true });
1369
+ for (const fix of autoFixResult.fixes) {
1370
+ const fixPath = path.join(patchDir, fix.filePath.replace(/\//g, "_") + ".patch.json");
1371
+ fs.writeFileSync(fixPath, JSON.stringify(fix, null, 2), "utf-8");
1372
+ }
1373
+ console.log(` Fixes written to .laxy-fixes/ for PR automation.`);
1374
+ }
1375
+ else {
1376
+ // Interactive: apply fixes to local project
1377
+ console.log(`\n Applying fixes to local project...`);
1378
+ let applied = 0;
1379
+ for (const fix of autoFixResult.fixes) {
1380
+ const result = (0, auto_fix_js_1.applyCodeFix)(args.projectDir, fix);
1381
+ if (result.success) {
1382
+ console.log(` ✓ ${fix.filePath}`);
1383
+ applied++;
1384
+ }
1385
+ else {
1386
+ console.log(` ✗ ${fix.filePath}: ${result.error}`);
1387
+ }
1388
+ }
1389
+ if (applied > 0) {
1390
+ console.log(`\n ${applied}/${autoFixResult.fixes.length} fix(es) applied. Rerun verification to check results.`);
1391
+ }
1392
+ }
1393
+ }
1394
+ else if (autoFixResult) {
1395
+ console.log(`\n AI Auto-Fix: ${autoFixResult.rootCause}`);
1396
+ console.log(` No concrete code fixes could be generated (confidence: ${autoFixResult.confidence}).`);
1397
+ }
1398
+ }
1399
+ catch {
1400
+ // Auto-fix errors are non-critical
1401
+ }
1402
+ }
1403
+ else if (args.autoFix && !effectiveFeatures.auto_fix) {
1404
+ console.log("\n [warn] --auto-fix requires a logged-in Pro account. Skipping AI auto-fix.");
1405
+ }
1266
1406
  if (args.format === "json") {
1267
1407
  console.log(JSON.stringify(resultObj, null, 2));
1268
1408
  }
@@ -1276,7 +1416,7 @@ async function run() {
1276
1416
  }
1277
1417
  if (!effectiveFeatures.history_trend) {
1278
1418
  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`);
1419
+ console.log(` https://laxy-blue.vercel.app/pricing`);
1280
1420
  }
1281
1421
  }
1282
1422
  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,17 +1,16 @@
1
1
  {
2
2
  "name": "laxy-verify",
3
- "version": "1.3.3",
3
+ "version": "1.4.1",
4
4
  "description": "Frontend verification CLI for build checks, Lighthouse, E2E, and release readiness",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",
7
- "homepage": "https://github.com/SUNgm24/Laxy/tree/main/laxy-verify#readme",
7
+ "homepage": "https://github.com/SUNgm24/laxy-verify#readme",
8
8
  "repository": {
9
9
  "type": "git",
10
- "url": "git+https://github.com/SUNgm24/Laxy.git",
11
- "directory": "laxy-verify"
10
+ "url": "git+https://github.com/SUNgm24/laxy-verify.git"
12
11
  },
13
12
  "bugs": {
14
- "url": "https://github.com/SUNgm24/Laxy/issues"
13
+ "url": "https://github.com/SUNgm24/laxy-verify/issues"
15
14
  },
16
15
  "keywords": [
17
16
  "frontend",