@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wcag-audit/cli",
3
- "version": "1.0.0-alpha.4",
3
+ "version": "1.0.0-alpha.6",
4
4
  "description": "Project-aware WCAG 2.1/2.2 auditor with AI-ready fix prompts for vibe-coding tools (Cursor, Claude Code, Windsurf).",
5
5
  "type": "module",
6
6
  "bin": {
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.4");
21
+ .version("1.0.0-alpha.6");
22
22
 
23
23
  // ── init ─────────────────────────────────────────────────────────
24
24
  program
@@ -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.4";
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 = [];
@@ -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.4";
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",
@@ -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.4";
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: `@wcagaudit/cli v${CLI_VERSION}`,
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
+ });
@@ -11,7 +11,7 @@ function buildSection(body) {
11
11
  WCAG_SECTION_START,
12
12
  "## WCAG Accessibility Fixes",
13
13
  "",
14
- "The latest @wcagaudit/cli scan produced the fixes below.",
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 @wcagaudit/cli",
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: "@wcagaudit/cli v1.0.0-alpha.1",
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(/@wcagaudit\/cli v1\.0\.0-alpha\.1/);
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!/);