@wcag-audit/cli 1.0.0-alpha.4 → 1.0.0-alpha.6
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/package.json +1 -1
- package/src/cli.js +1 -1
- package/src/commands/doctor.js +1 -1
- package/src/commands/init.js +39 -3
- package/src/commands/scan.js +2 -2
- package/src/license/request-free.js +46 -0
- package/src/license/request-free.test.js +57 -0
- package/src/output/agents-md.js +1 -1
- package/src/output/cursor-rules.js +1 -1
- package/src/output/markdown.test.js +2 -2
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -18,7 +18,7 @@ const program = new Command();
|
|
|
18
18
|
program
|
|
19
19
|
.name("wcag-audit")
|
|
20
20
|
.description("WCAG 2.1/2.2 auditor for web projects — with AI-ready fixes for Cursor / Claude Code / Windsurf.")
|
|
21
|
-
.version("1.0.0-alpha.
|
|
21
|
+
.version("1.0.0-alpha.6");
|
|
22
22
|
|
|
23
23
|
// ── init ─────────────────────────────────────────────────────────
|
|
24
24
|
program
|
package/src/commands/doctor.js
CHANGED
|
@@ -5,7 +5,7 @@ import { readGlobalConfig } from "../config/global.js";
|
|
|
5
5
|
import { validateLicense } from "../license/validate.js";
|
|
6
6
|
import { detectFramework } from "../discovery/registry.js";
|
|
7
7
|
|
|
8
|
-
const CLI_VERSION = "1.0.0-alpha.
|
|
8
|
+
const CLI_VERSION = "1.0.0-alpha.6";
|
|
9
9
|
|
|
10
10
|
export async function runDoctor({ cwd = process.cwd(), log = console.log } = {}) {
|
|
11
11
|
const checks = [];
|
package/src/commands/init.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import enquirer from "enquirer";
|
|
2
2
|
import { randomUUID } from "crypto";
|
|
3
3
|
import { validateLicense } from "../license/validate.js";
|
|
4
|
+
import { requestFreeLicense } from "../license/request-free.js";
|
|
4
5
|
import { writeGlobalConfig } from "../config/global.js";
|
|
5
6
|
|
|
6
|
-
const CLI_VERSION = "1.0.0-alpha.
|
|
7
|
+
const CLI_VERSION = "1.0.0-alpha.6";
|
|
7
8
|
|
|
8
9
|
// runInit can be called two ways:
|
|
9
10
|
// 1. Interactive (no answers) — uses enquirer to prompt
|
|
@@ -13,7 +14,7 @@ export async function runInit({ answers, log = console.log } = {}) {
|
|
|
13
14
|
log("Welcome to WCAG Audit CLI");
|
|
14
15
|
log("");
|
|
15
16
|
|
|
16
|
-
const resolved = answers || (await promptUser());
|
|
17
|
+
const resolved = answers || (await promptUser({ log }));
|
|
17
18
|
|
|
18
19
|
// 1. Validate license
|
|
19
20
|
const machineId = randomUUID();
|
|
@@ -68,7 +69,42 @@ function defaultModelFor(provider) {
|
|
|
68
69
|
}[provider] || "claude-sonnet-4-6";
|
|
69
70
|
}
|
|
70
71
|
|
|
71
|
-
async function promptUser() {
|
|
72
|
+
async function promptUser({ log = console.log } = {}) {
|
|
73
|
+
// Step 1: Has a key? Or request free?
|
|
74
|
+
const { hasKey } = await enquirer.prompt([
|
|
75
|
+
{
|
|
76
|
+
type: "confirm",
|
|
77
|
+
name: "hasKey",
|
|
78
|
+
message: "Do you already have a license key?",
|
|
79
|
+
initial: true,
|
|
80
|
+
},
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
if (!hasKey) {
|
|
84
|
+
// Free-tier flow: ask for email, request a free key, wait for them
|
|
85
|
+
// to paste it from their inbox.
|
|
86
|
+
const { email } = await enquirer.prompt([
|
|
87
|
+
{
|
|
88
|
+
type: "input",
|
|
89
|
+
name: "email",
|
|
90
|
+
message: "Enter your email for a free license (1 audit/day):",
|
|
91
|
+
validate: (v) => (/@.+\./.test(v) ? true : "Enter a valid email"),
|
|
92
|
+
},
|
|
93
|
+
]);
|
|
94
|
+
log("");
|
|
95
|
+
log(`Requesting free license for ${email}...`);
|
|
96
|
+
const machineId = randomUUID();
|
|
97
|
+
const result = await requestFreeLicense({ email, machineId });
|
|
98
|
+
if (!result.ok) {
|
|
99
|
+
log(`✗ ${result.error}`);
|
|
100
|
+
log(" You can try again or skip and use --help to paste a key later.");
|
|
101
|
+
throw new Error(result.error);
|
|
102
|
+
}
|
|
103
|
+
log(`✓ ${result.message}`);
|
|
104
|
+
log("");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Step 2: Paste the key (works for both free and paid)
|
|
72
108
|
const answers = await enquirer.prompt([
|
|
73
109
|
{
|
|
74
110
|
type: "input",
|
package/src/commands/scan.js
CHANGED
|
@@ -17,7 +17,7 @@ import { renderCursorRules } from "../output/cursor-rules.js";
|
|
|
17
17
|
import { upsertAgentsMd } from "../output/agents-md.js";
|
|
18
18
|
import { hashContent, readCacheEntry, writeCacheEntry } from "../cache/route-cache.js";
|
|
19
19
|
|
|
20
|
-
const CLI_VERSION = "1.0.0-alpha.
|
|
20
|
+
const CLI_VERSION = "1.0.0-alpha.6";
|
|
21
21
|
|
|
22
22
|
export async function runScan({
|
|
23
23
|
cwd = process.cwd(),
|
|
@@ -230,7 +230,7 @@ export async function runScan({
|
|
|
230
230
|
if (outputs.has("markdown")) {
|
|
231
231
|
const md = renderFindingsMarkdown({
|
|
232
232
|
projectName,
|
|
233
|
-
generator: `@
|
|
233
|
+
generator: `@wcag-audit/cli v${CLI_VERSION}`,
|
|
234
234
|
totalPages: routes.length,
|
|
235
235
|
wcagVersion: globalCfg.defaults.wcagVersion,
|
|
236
236
|
levels: globalCfg.defaults.conformanceLevel,
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Free-tier license request. POSTs to /v1/license/free which creates a
|
|
2
|
+
// User + License row (tier: "free") and emails the key to the user.
|
|
3
|
+
// The key is NEVER returned in the response body — it's mailed out,
|
|
4
|
+
// so users have to paste it from their inbox. This matches the
|
|
5
|
+
// Chrome extension free-tier flow.
|
|
6
|
+
|
|
7
|
+
const DEFAULT_API_URL = "https://wcagaudit.io/api/v1/license/free";
|
|
8
|
+
|
|
9
|
+
export async function requestFreeLicense({ email, machineId, apiUrl } = {}) {
|
|
10
|
+
const url = apiUrl || process.env.WCAG_LICENSE_FREE_URL || DEFAULT_API_URL;
|
|
11
|
+
|
|
12
|
+
const trimmedEmail = (email || "").trim();
|
|
13
|
+
if (!trimmedEmail || !trimmedEmail.includes("@")) {
|
|
14
|
+
return {
|
|
15
|
+
ok: false,
|
|
16
|
+
error: "A valid email address is required",
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const res = await fetch(url, {
|
|
22
|
+
method: "POST",
|
|
23
|
+
headers: { "content-type": "application/json" },
|
|
24
|
+
body: JSON.stringify({ email: trimmedEmail, machineId }),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const body = await res.json().catch(() => ({}));
|
|
28
|
+
|
|
29
|
+
if (!res.ok) {
|
|
30
|
+
return {
|
|
31
|
+
ok: false,
|
|
32
|
+
error: body.error || `Free license request failed (HTTP ${res.status})`,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
ok: true,
|
|
38
|
+
message: body.message || "Check your email for your free license key",
|
|
39
|
+
};
|
|
40
|
+
} catch (err) {
|
|
41
|
+
return {
|
|
42
|
+
ok: false,
|
|
43
|
+
error: `Network error: ${err.message}`,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
2
|
+
import { requestFreeLicense } from "./request-free.js";
|
|
3
|
+
|
|
4
|
+
describe("requestFreeLicense", () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
global.fetch = vi.fn();
|
|
7
|
+
});
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
vi.restoreAllMocks();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("rejects invalid email without hitting the network", async () => {
|
|
13
|
+
const result = await requestFreeLicense({ email: "not-an-email", machineId: "m1" });
|
|
14
|
+
expect(result.ok).toBe(false);
|
|
15
|
+
expect(result.error).toMatch(/email/i);
|
|
16
|
+
expect(global.fetch).not.toHaveBeenCalled();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("rejects empty email", async () => {
|
|
20
|
+
const result = await requestFreeLicense({ email: "", machineId: "m1" });
|
|
21
|
+
expect(result.ok).toBe(false);
|
|
22
|
+
expect(global.fetch).not.toHaveBeenCalled();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("returns ok when API returns 200", async () => {
|
|
26
|
+
global.fetch.mockResolvedValueOnce({
|
|
27
|
+
ok: true,
|
|
28
|
+
json: async () => ({ ok: true, message: "Check your email for your free license key" }),
|
|
29
|
+
});
|
|
30
|
+
const result = await requestFreeLicense({ email: "sai@example.com", machineId: "m1" });
|
|
31
|
+
expect(result.ok).toBe(true);
|
|
32
|
+
expect(result.message).toMatch(/email/i);
|
|
33
|
+
const [url, init] = global.fetch.mock.calls[0];
|
|
34
|
+
expect(url).toMatch(/\/v1\/license\/free$/);
|
|
35
|
+
const body = JSON.parse(init.body);
|
|
36
|
+
expect(body.email).toBe("sai@example.com");
|
|
37
|
+
expect(body.machineId).toBe("m1");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("returns error on non-2xx", async () => {
|
|
41
|
+
global.fetch.mockResolvedValueOnce({
|
|
42
|
+
ok: false,
|
|
43
|
+
status: 429,
|
|
44
|
+
json: async () => ({ error: "Rate limit exceeded" }),
|
|
45
|
+
});
|
|
46
|
+
const result = await requestFreeLicense({ email: "sai@example.com", machineId: "m1" });
|
|
47
|
+
expect(result.ok).toBe(false);
|
|
48
|
+
expect(result.error).toMatch(/rate limit/i);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("returns error when fetch throws", async () => {
|
|
52
|
+
global.fetch.mockRejectedValueOnce(new Error("ECONNREFUSED"));
|
|
53
|
+
const result = await requestFreeLicense({ email: "sai@example.com", machineId: "m1" });
|
|
54
|
+
expect(result.ok).toBe(false);
|
|
55
|
+
expect(result.error).toMatch(/network/i);
|
|
56
|
+
});
|
|
57
|
+
});
|
package/src/output/agents-md.js
CHANGED
|
@@ -11,7 +11,7 @@ function buildSection(body) {
|
|
|
11
11
|
WCAG_SECTION_START,
|
|
12
12
|
"## WCAG Accessibility Fixes",
|
|
13
13
|
"",
|
|
14
|
-
"The latest @
|
|
14
|
+
"The latest @wcag-audit/cli scan produced the fixes below.",
|
|
15
15
|
"Apply them when editing the affected files.",
|
|
16
16
|
"",
|
|
17
17
|
body.trimEnd(),
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
export function renderCursorRules({ findings, projectName }) {
|
|
7
7
|
const frontMatter = [
|
|
8
8
|
"---",
|
|
9
|
-
"description: WCAG accessibility fixes generated by @
|
|
9
|
+
"description: WCAG accessibility fixes generated by @wcag-audit/cli",
|
|
10
10
|
`globs: ${JSON.stringify(["src/app/**/*.tsx", "src/app/**/*.ts", "src/components/**/*.tsx"])}`,
|
|
11
11
|
"alwaysApply: false",
|
|
12
12
|
"---",
|
|
@@ -18,14 +18,14 @@ describe("renderFindingsMarkdown", () => {
|
|
|
18
18
|
it("includes a header with project + generator metadata", () => {
|
|
19
19
|
const md = renderFindingsMarkdown({
|
|
20
20
|
projectName: "my-nextjs-app",
|
|
21
|
-
generator: "@
|
|
21
|
+
generator: "@wcag-audit/cli v1.0.0-alpha.1",
|
|
22
22
|
totalPages: 47,
|
|
23
23
|
wcagVersion: "2.2",
|
|
24
24
|
levels: ["A", "AA"],
|
|
25
25
|
findings: [],
|
|
26
26
|
});
|
|
27
27
|
expect(md).toMatch(/# WCAG Audit Findings — my-nextjs-app/);
|
|
28
|
-
expect(md).toMatch(/@
|
|
28
|
+
expect(md).toMatch(/@wcag-audit\/cli v1\.0\.0-alpha\.1/);
|
|
29
29
|
expect(md).toMatch(/Total pages audited: 47/);
|
|
30
30
|
expect(md).toMatch(/WCAG 2\.2 Level A \+ AA/);
|
|
31
31
|
expect(md).toMatch(/No issues found!/);
|