laxy-verify 1.2.3 → 1.3.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
@@ -188,6 +188,16 @@ Passed:
188
188
  Artifacts:
189
189
  - .laxy-result.json
190
190
  - laxy-verify-report.md
191
+
192
+ Badge (auto-updates with each run):
193
+ [![Laxy Verify](https://laxy.app/api/badge/your-repo-id)](https://laxy.app)
194
+ ```
195
+
196
+ For Free accounts, the CLI prints a tip instead:
197
+
198
+ ```text
199
+ Tip: Pro tracks your last 30 runs so you can see if Performance or Grade is improving.
200
+ https://laxy.app/pricing
191
201
  ```
192
202
 
193
203
  ## The decision it helps you make
@@ -318,9 +328,13 @@ Pro and Team accounts unlock additional capabilities on top of the same verifica
318
328
  - **Result saving and sharing** — `--share` saves the run to your dashboard and returns a shareable URL
319
329
  - **Environment comparison** — `--compare <url>` runs Lighthouse against a reference URL (e.g. staging or production) and shows score deltas between your local build and the reference
320
330
  - **AI failure analysis** — when a run ends in `hold`, Claude analyzes the failure context and returns a root cause summary with top fix suggestions
331
+ - **History trend tracking** — the last 30 runs are stored so you can see whether your grade and performance scores are improving or regressing over time
332
+ - **Dynamic README badge** — after each run, the CLI prints a Markdown badge snippet that links to your live verification status. The badge auto-updates with every run so your README always reflects current grade
321
333
  - **Team Slack / Discord alerts** — grade drops and `hold` verdicts fire webhook notifications with score deltas and blocker details
322
334
  - **Weekly team report** — automated weekly summary of verification activity across your team's repos
323
335
 
336
+ Free accounts see a hint after each run pointing to the history trend feature.
337
+
324
338
  ## Secret scan patterns
325
339
 
326
340
  When `--secret-scan` is enabled, the following patterns are detected:
@@ -418,6 +432,15 @@ It is a pre-merge and pre-release verification layer, not your entire quality sy
418
432
 
419
433
  ## Changelog
420
434
 
435
+ ### v1.3.0 — Pro history trend, dynamic badge, CLI nudge
436
+
437
+ - **History trend tracking** — Pro accounts now store the last 30 verification runs. Grade and performance scores accumulate so you can see whether your app is improving or regressing over time
438
+ - **Dynamic README badge** — after each run, Pro accounts see a Markdown badge snippet in the CLI output. The badge auto-updates with every run via the `/api/badge/:id` endpoint
439
+ - **CLI nudge** — Free accounts see a one-line tip after each run pointing to the history trend feature
440
+ - Added `generateDynamicBadgeMarkdown(repoId, apiUrl)` export to the `badge` module
441
+ - Added `share_result` and `history_trend` flags to `EntitlementFeatures` interface
442
+ - 17 test files, 88 unit tests
443
+
421
444
  ### v1.2.3 — Bug fix
422
445
 
423
446
  - Fix E2E and visual diff connection failure on Windows with Node.js 17+: `verifyUrl` now uses `localhost` instead of `127.0.0.1`. On Windows, Vite binds to `::1` (IPv6) which does not accept IPv4 connections.
@@ -1,25 +1,25 @@
1
- /**
2
- * Broken-links audit.
3
- * Uses the crawl result to find all internal links, then validates each
4
- * with an HTTP HEAD/GET request. Links that return 4xx/5xx or timeout are
5
- * reported as blockers.
6
- */
7
- import { type CrawlResult } from "../crawler.js";
8
- export interface BrokenLink {
9
- url: string;
10
- path: string;
11
- status: number;
12
- statusText: string;
13
- severity: "critical" | "high";
14
- }
15
- export interface BrokenLinksResult {
16
- brokenLinks: BrokenLink[];
17
- checkedCount: number;
18
- hasBrokenLinks: boolean;
19
- summary: string;
20
- }
21
- export interface BrokenLinksAuditOptions {
22
- extraRoutes?: string[];
23
- abortSignal?: AbortSignal;
24
- }
25
- export declare function auditBrokenLinks(crawlResult: CrawlResult | undefined, baseUrl: string, options?: BrokenLinksAuditOptions): Promise<BrokenLinksResult>;
1
+ /**
2
+ * Broken-links audit.
3
+ * Uses the crawl result to find all internal links, then validates each
4
+ * with an HTTP HEAD/GET request. Links that return 4xx/5xx or timeout are
5
+ * reported as blockers.
6
+ */
7
+ import { type CrawlResult } from "../crawler.js";
8
+ export interface BrokenLink {
9
+ url: string;
10
+ path: string;
11
+ status: number;
12
+ statusText: string;
13
+ severity: "critical" | "high";
14
+ }
15
+ export interface BrokenLinksResult {
16
+ brokenLinks: BrokenLink[];
17
+ checkedCount: number;
18
+ hasBrokenLinks: boolean;
19
+ summary: string;
20
+ }
21
+ export interface BrokenLinksAuditOptions {
22
+ extraRoutes?: string[];
23
+ abortSignal?: AbortSignal;
24
+ }
25
+ export declare function auditBrokenLinks(crawlResult: CrawlResult | undefined, baseUrl: string, options?: BrokenLinksAuditOptions): Promise<BrokenLinksResult>;
@@ -1,97 +1,97 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.auditBrokenLinks = auditBrokenLinks;
4
- const TIMEOUT_MS = 5000;
5
- const VALID_OK_STATUS = [200, 201, 202, 203, 204, 301, 302, 303, 307, 308];
6
- function isSuccessStatus(n) {
7
- return VALID_OK_STATUS.includes(n);
8
- }
9
- async function auditBrokenLinks(crawlResult, baseUrl, options = {}) {
10
- const allUrls = [];
11
- if (crawlResult) {
12
- for (const page of crawlResult.pages) {
13
- for (const href of page.internalLinks) {
14
- try {
15
- const url = new URL(href, baseUrl).href;
16
- if (!allUrls.includes(url))
17
- allUrls.push(url);
18
- }
19
- catch {
20
- // skip malformed URLs
21
- }
22
- }
23
- }
24
- }
25
- for (const route of options.extraRoutes ?? []) {
26
- try {
27
- const url = new URL(route, baseUrl).href;
28
- if (!allUrls.includes(url))
29
- allUrls.push(url);
30
- }
31
- catch {
32
- // skip malformed routes
33
- }
34
- }
35
- const uniqueUrls = allUrls;
36
- const brokenLinks = [];
37
- await Promise.all(uniqueUrls.map(async (url) => {
38
- if (options.abortSignal?.aborted)
39
- return;
40
- try {
41
- const controller = new AbortController();
42
- const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
43
- let status = 0;
44
- let statusText = "";
45
- try {
46
- const res = await fetch(url, {
47
- method: "HEAD",
48
- redirect: "follow",
49
- signal: controller.signal,
50
- });
51
- status = res.status;
52
- statusText = res.statusText;
53
- }
54
- catch {
55
- // Fall back to GET if HEAD is not allowed
56
- const controller2 = new AbortController();
57
- const timer2 = setTimeout(() => controller2.abort(), TIMEOUT_MS);
58
- try {
59
- const res = await fetch(url, {
60
- method: "GET",
61
- redirect: "follow",
62
- signal: controller2.signal,
63
- });
64
- status = res.status;
65
- statusText = res.statusText;
66
- }
67
- catch {
68
- status = 0;
69
- statusText = "timeout or network error";
70
- }
71
- clearTimeout(timer2);
72
- }
73
- clearTimeout(timer);
74
- if (!isSuccessStatus(status)) {
75
- const severity = status >= 500 ? "critical" : "high";
76
- let path = url;
77
- try {
78
- path = new URL(url).pathname;
79
- }
80
- catch { }
81
- brokenLinks.push({ url, path, status, statusText, severity });
82
- }
83
- }
84
- catch {
85
- // skip
86
- }
87
- }));
88
- const summary = brokenLinks.length === 0
89
- ? `All ${uniqueUrls.length} links OK`
90
- : `${brokenLinks.length} broken link(s) found`;
91
- return {
92
- brokenLinks,
93
- checkedCount: uniqueUrls.length,
94
- hasBrokenLinks: brokenLinks.length > 0,
95
- summary,
96
- };
97
- }
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.auditBrokenLinks = auditBrokenLinks;
4
+ const TIMEOUT_MS = 5000;
5
+ const VALID_OK_STATUS = [200, 201, 202, 203, 204, 301, 302, 303, 307, 308];
6
+ function isSuccessStatus(n) {
7
+ return VALID_OK_STATUS.includes(n);
8
+ }
9
+ async function auditBrokenLinks(crawlResult, baseUrl, options = {}) {
10
+ const allUrls = [];
11
+ if (crawlResult) {
12
+ for (const page of crawlResult.pages) {
13
+ for (const href of page.internalLinks) {
14
+ try {
15
+ const url = new URL(href, baseUrl).href;
16
+ if (!allUrls.includes(url))
17
+ allUrls.push(url);
18
+ }
19
+ catch {
20
+ // skip malformed URLs
21
+ }
22
+ }
23
+ }
24
+ }
25
+ for (const route of options.extraRoutes ?? []) {
26
+ try {
27
+ const url = new URL(route, baseUrl).href;
28
+ if (!allUrls.includes(url))
29
+ allUrls.push(url);
30
+ }
31
+ catch {
32
+ // skip malformed routes
33
+ }
34
+ }
35
+ const uniqueUrls = allUrls;
36
+ const brokenLinks = [];
37
+ await Promise.all(uniqueUrls.map(async (url) => {
38
+ if (options.abortSignal?.aborted)
39
+ return;
40
+ try {
41
+ const controller = new AbortController();
42
+ const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
43
+ let status = 0;
44
+ let statusText = "";
45
+ try {
46
+ const res = await fetch(url, {
47
+ method: "HEAD",
48
+ redirect: "follow",
49
+ signal: controller.signal,
50
+ });
51
+ status = res.status;
52
+ statusText = res.statusText;
53
+ }
54
+ catch {
55
+ // Fall back to GET if HEAD is not allowed
56
+ const controller2 = new AbortController();
57
+ const timer2 = setTimeout(() => controller2.abort(), TIMEOUT_MS);
58
+ try {
59
+ const res = await fetch(url, {
60
+ method: "GET",
61
+ redirect: "follow",
62
+ signal: controller2.signal,
63
+ });
64
+ status = res.status;
65
+ statusText = res.statusText;
66
+ }
67
+ catch {
68
+ status = 0;
69
+ statusText = "timeout or network error";
70
+ }
71
+ clearTimeout(timer2);
72
+ }
73
+ clearTimeout(timer);
74
+ if (!isSuccessStatus(status)) {
75
+ const severity = status >= 500 ? "critical" : "high";
76
+ let path = url;
77
+ try {
78
+ path = new URL(url).pathname;
79
+ }
80
+ catch { }
81
+ brokenLinks.push({ url, path, status, statusText, severity });
82
+ }
83
+ }
84
+ catch {
85
+ // skip
86
+ }
87
+ }));
88
+ const summary = brokenLinks.length === 0
89
+ ? `All ${uniqueUrls.length} links OK`
90
+ : `${brokenLinks.length} broken link(s) found`;
91
+ return {
92
+ brokenLinks,
93
+ checkedCount: uniqueUrls.length,
94
+ hasBrokenLinks: brokenLinks.length > 0,
95
+ summary,
96
+ };
97
+ }
package/dist/badge.d.ts CHANGED
@@ -1 +1,2 @@
1
- export declare function generateBadge(grade: string): string;
1
+ export declare function generateBadge(grade: string): string;
2
+ export declare function generateDynamicBadgeMarkdown(repoId: string, apiUrl: string): string;
package/dist/badge.js CHANGED
@@ -1,14 +1,18 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.generateBadge = generateBadge;
4
- function generateBadge(grade) {
5
- const gradeLower = grade.toLowerCase();
6
- const colors = {
7
- gold: "yellow",
8
- silver: "brightgreen",
9
- bronze: "blue",
10
- unverified: "lightgrey",
11
- };
12
- const color = colors[gradeLower] ?? "lightgrey";
13
- return `![Laxy Verify: ${grade}](https://img.shields.io/badge/laxy_verify-${gradeLower}-${color})`;
14
- }
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateBadge = generateBadge;
4
+ exports.generateDynamicBadgeMarkdown = generateDynamicBadgeMarkdown;
5
+ function generateBadge(grade) {
6
+ const gradeLower = grade.toLowerCase();
7
+ const colors = {
8
+ gold: "yellow",
9
+ silver: "brightgreen",
10
+ bronze: "blue",
11
+ unverified: "lightgrey",
12
+ };
13
+ const color = colors[gradeLower] ?? "lightgrey";
14
+ return `![Laxy Verify: ${grade}](https://img.shields.io/badge/laxy_verify-${gradeLower}-${color})`;
15
+ }
16
+ function generateDynamicBadgeMarkdown(repoId, apiUrl) {
17
+ return `[![Laxy Verify](${apiUrl}/api/badge/${repoId})](${apiUrl})`;
18
+ }