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 +19 -1
- package/dist/auto-fix.d.ts +66 -0
- package/dist/auto-fix.js +176 -0
- package/dist/check-run.d.ts +58 -0
- package/dist/check-run.js +139 -0
- package/dist/cli.js +146 -13
- package/dist/entitlement.d.ts +2 -0
- package/dist/entitlement.js +4 -0
- package/dist/update-check.d.ts +6 -0
- package/dist/update-check.js +138 -0
- package/package.json +1 -1
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>;
|
package/dist/auto-fix.js
ADDED
|
@@ -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
|
|
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
|
-
|
|
1161
|
-
|
|
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) {
|
package/dist/entitlement.d.ts
CHANGED
|
@@ -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;
|
package/dist/entitlement.js
CHANGED
|
@@ -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,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
|
+
}
|