wcag-a11y 0.3.5 → 0.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 CHANGED
@@ -56,7 +56,7 @@ Generating AI fixes for 7 violations...
56
56
 
57
57
  The **prompt** at the end of each fix is what you copy into Cursor, Copilot, or Claude. It includes the affected selectors, the WCAG rule, and exactly what needs to change — no rewriting needed.
58
58
 
59
- With `--report`, the full output is also saved to `a11y-report.md`.
59
+ The full output is saved to `a11y-report.md` by default. Pass `--no-report` to skip.
60
60
 
61
61
  ---
62
62
 
@@ -69,17 +69,22 @@ npm install -g wcag-a11y
69
69
  ## Setup
70
70
 
71
71
  ```bash
72
- wcag-a11y init # Gemini (free, default)
73
- wcag-a11y init --provider openai # OpenAI
74
- wcag-a11y init --provider ollama # Local — no API key needed
72
+ wcag-a11y init # Gemini (free, default)
73
+ wcag-a11y init --provider openai # OpenAI
74
+ wcag-a11y init --provider anthropic # Anthropic Claude
75
+ wcag-a11y init --provider mistral # Mistral
76
+ wcag-a11y init --provider groq # Groq (fast inference)
77
+ wcag-a11y init --provider cohere # Cohere
78
+ wcag-a11y init --provider xai # xAI Grok
79
+ wcag-a11y init --provider deepseek # DeepSeek
80
+ wcag-a11y init --provider together # Together AI (open-source models)
81
+ wcag-a11y init --provider perplexity # Perplexity
82
+ wcag-a11y init --provider azure-openai # Azure OpenAI
83
+ wcag-a11y init --provider ollama # Local — no API key needed
75
84
  ```
76
85
 
77
86
  Each command creates an `a11y.config.json` pre-wired for that provider. Fill in your API key, then scan.
78
87
 
79
- **Get a free Gemini key:** https://aistudio.google.com
80
- **Get an OpenAI key:** https://platform.openai.com/api-keys
81
- **Ollama (local):** install from https://ollama.com, then run `ollama serve`
82
-
83
88
  ---
84
89
 
85
90
  ## Commands
@@ -91,13 +96,13 @@ Scan a built-in page with 10 intentional violations. No dev server or config req
91
96
  ```bash
92
97
  wcag-a11y demo # violations + AI fixes (default, requires config)
93
98
  wcag-a11y demo --no-ai # violations only, no AI — faster
94
- wcag-a11y demo --report # + save a11y-report.md
99
+ wcag-a11y demo --no-report # skip saving a11y-report.md
95
100
  ```
96
101
 
97
102
  | Flag | Description |
98
103
  |---|---|
99
104
  | `--no-ai` | Skip AI fix generation — prints violations only, no prompts |
100
- | `-r, --report` | Save the full report to `a11y-report.md` in the current directory |
105
+ | `--no-report` | Skip saving report to `a11y-report.md` (report is saved by default) |
101
106
 
102
107
  ---
103
108
 
@@ -113,7 +118,7 @@ wcag-a11y init --provider ollama # Ollama (local)
113
118
 
114
119
  | Flag | Description |
115
120
  |---|---|
116
- | `--provider <name>` | Which provider to configure: `gemini` (default), `openai`, or `ollama`. Determines which fields are written to the config file. |
121
+ | `--provider <name>` | Which provider to configure. Valid values: `gemini` (default), `openai`, `anthropic`, `mistral`, `groq`, `cohere`, `xai`, `deepseek`, `together`, `perplexity`, `azure-openai`, `ollama`. Determines which fields are written to the config file. |
117
122
 
118
123
  ---
119
124
 
@@ -124,8 +129,9 @@ Scan a running dev server for accessibility violations.
124
129
  ```bash
125
130
  wcag-a11y scan -u http://localhost:3000
126
131
  wcag-a11y scan -u http://localhost:3000 --pages / /about /contact
127
- wcag-a11y scan -u http://localhost:3000 --crawl --report
132
+ wcag-a11y scan -u http://localhost:3000 --crawl
128
133
  wcag-a11y scan -u http://localhost:3000 --no-ai --ci
134
+ wcag-a11y scan -u http://localhost:3000 --terminal --fast-mode
129
135
  ```
130
136
 
131
137
  | Flag | Default | Description |
@@ -133,12 +139,14 @@ wcag-a11y scan -u http://localhost:3000 --no-ai --ci
133
139
  | `-u, --url <url>` | required | Base URL of your running dev server |
134
140
  | `-p, --pages <pages...>` | `/` | One or more paths to scan. Separate with spaces: `--pages / /about /contact` |
135
141
  | `-c, --crawl` | off | Follow same-origin links and scan all reachable pages automatically |
136
- | `-r, --report` | off | Save the full scan output to `a11y-report.md` |
142
+ | `--no-report` | on | Skip saving scan output to `a11y-report.md` (report is saved by default) |
137
143
  | `--no-ai` | on | Skip AI fix generation — scan runs faster and prints violations only |
138
- | `--no-explain` | off | Print only the ready-to-paste prompt for each fix, without the AI explanation |
144
+ | `--no-explain` | on | Print only the ready-to-paste prompt for each fix, without the AI explanation |
145
+ | `--terminal` | off | Print violations summary and AI fix prompts to terminal |
146
+ | `--fast-mode` | off | Output only AI fix prompts — no summaries, explanations, or progress messages |
139
147
  | `--group <strategy>` | `rule` | `rule` (default) groups all violations of the same type into one fix prompt. `none` produces a separate prompt per element. Use `none` when violations of the same rule need different fixes |
140
148
  | `--ci` | off | Exit with code `1` if any violations are found. Use this to fail a CI pipeline |
141
- | `--provider <name>` | from config | Override the AI provider for this run: `gemini`, `openai`, or `ollama`. Does not modify the config file |
149
+ | `--provider <name>` | from config | Override the AI provider for this run. See [AI Providers](#ai-providers) for valid names. Does not modify the config file |
142
150
 
143
151
  ---
144
152
 
@@ -158,11 +166,15 @@ wcag-a11y fix -u http://localhost:3000 --apply
158
166
 
159
167
  # Auto-discover pages + write fixes
160
168
  wcag-a11y fix -u http://localhost:3000 --crawl --apply
169
+
170
+ # Skip rescanning — load violations from an existing report
171
+ wcag-a11y fix --from-report
172
+ wcag-a11y fix --from-report ./reports/a11y-report.md --apply
161
173
  ```
162
174
 
163
175
  **How it works:**
164
176
 
165
- 1. Runs the same scan as `wcag-a11y scan`
177
+ 1. Runs the same scan as `wcag-a11y scan` (or loads an existing report with `--from-report`)
166
178
  2. For each violation, locates the source file — checks `violation.source` (React dev mode) first, then falls back to grepping `./src` for unique identifiers in the HTML snippet (`id=`, `name=`, `for=`, local `src=`, text content)
167
179
  3. Groups violations by file (multiple violations in the same file → one AI call)
168
180
  4. Sends the full file content + violation list to your configured AI provider and asks for the corrected file
@@ -186,30 +198,51 @@ src/components/Navbar.jsx — 2 violation(s)
186
198
 
187
199
  | Flag | Default | Description |
188
200
  |---|---|---|
189
- | `-u, --url <url>` | required | Base URL of your running dev server |
201
+ | `-u, --url <url>` | | Base URL of your running dev server. Required unless `--from-report` is used |
190
202
  | `-p, --pages <pages...>` | `/` | Specific pages to scan |
191
203
  | `-c, --crawl` | off | Auto-discover pages by following same-origin links |
204
+ | `--from-report [path]` | `a11y-report.md` | Load violations from an existing report instead of scanning. Useful when you already ran `scan --report` and just want to apply fixes |
192
205
  | `--apply` | off | Write patched files to disk (dry-run without this flag) |
193
- | `--provider <name>` | from config | Override AI provider for this run: `gemini`, `openai`, or `ollama` |
206
+ | `--provider <name>` | from config | Override AI provider for this run. See [AI Providers](#ai-providers) for valid names |
194
207
 
195
208
  > **Tip:** Always run without `--apply` first to review the diff. The dry-run is safe — nothing is written to disk.
196
209
 
210
+ **Common workflow:** run `scan --report` to generate a report for review, then run `fix --from-report --apply` to patch the files — no second browser crawl needed.
211
+
212
+ ```bash
213
+ wcag-a11y scan -u http://localhost:3000 # generates a11y-report.md automatically
214
+ wcag-a11y fix --from-report --apply # patch files from that report
215
+ ```
216
+
197
217
  ---
198
218
 
199
219
  ## AI Providers
200
220
 
201
- | Provider | Model | Cost | API Key |
202
- |---|---|---|---|
203
- | `gemini` (default) | `gemini-2.5-flash` | Free tier | [aistudio.google.com](https://aistudio.google.com) |
204
- | `openai` | `gpt-4o-mini` | Pay-per-use | [platform.openai.com](https://platform.openai.com/api-keys) |
205
- | `ollama` | `llama3` | Free (local) | None — run `ollama serve` |
221
+ 12 providers are supported. Set your provider in `a11y.config.json` or override per-run with `--provider <name>`.
206
222
 
207
- Set your provider in `a11y.config.json` or override it per-run with `--provider`. If the AI response is unparseable, the tool generates a fix prompt directly from the violation data so you always get something actionable.
223
+ | Provider | `--provider` name | Default model | API key source |
224
+ |---|---|---|---|
225
+ | Google Gemini | `gemini` *(default)* | `gemini-2.5-flash` | [aistudio.google.com](https://aistudio.google.com) — free tier |
226
+ | OpenAI | `openai` | `gpt-4o-mini` | [platform.openai.com/api-keys](https://platform.openai.com/api-keys) |
227
+ | Anthropic | `anthropic` | `claude-sonnet-4-6` | [console.anthropic.com](https://console.anthropic.com) |
228
+ | Mistral | `mistral` | `mistral-large-latest` | [console.mistral.ai](https://console.mistral.ai) |
229
+ | Groq | `groq` | `llama-3.3-70b-versatile` | [console.groq.com](https://console.groq.com) |
230
+ | Cohere | `cohere` | `command-r-plus` | [dashboard.cohere.com](https://dashboard.cohere.com) |
231
+ | xAI | `xai` | `grok-2` | [console.x.ai](https://console.x.ai) |
232
+ | DeepSeek | `deepseek` | `deepseek-chat` | [platform.deepseek.com](https://platform.deepseek.com) |
233
+ | Together AI | `together` | `meta-llama/Llama-3-70b-chat-hf` | [api.together.xyz](https://api.together.xyz) |
234
+ | Perplexity | `perplexity` | `llama-3.1-sonar-large-128k-online` | [perplexity.ai/settings/api](https://www.perplexity.ai/settings/api) |
235
+ | Azure OpenAI | `azure-openai` | *(your deployment)* | [portal.azure.com](https://portal.azure.com) |
236
+ | Ollama | `ollama` | `llama3` | None — run `ollama serve` locally |
237
+
238
+ All models are configurable. Change the model field in `a11y.config.json` to use any model your API key has access to. If the AI response is unparseable, the tool generates a fix prompt directly from the violation data so you always get something actionable.
208
239
 
209
240
  ---
210
241
 
211
242
  ## Config (`a11y.config.json`)
212
243
 
244
+ Only the fields for your active `provider` are required. This file is gitignored by default.
245
+
213
246
  ```json
214
247
  {
215
248
  "provider": "gemini",
@@ -220,13 +253,40 @@ Set your provider in `a11y.config.json` or override it per-run with `--provider`
220
253
  "openaiApiKey": "YOUR_OPENAI_API_KEY",
221
254
  "openaiModel": "gpt-4o-mini",
222
255
 
256
+ "anthropicApiKey": "YOUR_ANTHROPIC_API_KEY",
257
+ "anthropicModel": "claude-sonnet-4-6",
258
+
259
+ "mistralApiKey": "YOUR_MISTRAL_API_KEY",
260
+ "mistralModel": "mistral-large-latest",
261
+
262
+ "groqApiKey": "YOUR_GROQ_API_KEY",
263
+ "groqModel": "llama-3.3-70b-versatile",
264
+
265
+ "cohereApiKey": "YOUR_COHERE_API_KEY",
266
+ "cohereModel": "command-r-plus",
267
+
268
+ "xaiApiKey": "YOUR_XAI_API_KEY",
269
+ "xaiModel": "grok-2",
270
+
271
+ "deepseekApiKey": "YOUR_DEEPSEEK_API_KEY",
272
+ "deepseekModel": "deepseek-chat",
273
+
274
+ "togetherApiKey": "YOUR_TOGETHER_API_KEY",
275
+ "togetherModel": "meta-llama/Llama-3-70b-chat-hf",
276
+
277
+ "perplexityApiKey": "YOUR_PERPLEXITY_API_KEY",
278
+ "perplexityModel": "llama-3.1-sonar-large-128k-online",
279
+
280
+ "azureOpenaiApiKey": "YOUR_AZURE_KEY",
281
+ "azureOpenaiEndpoint": "https://YOUR_RESOURCE.openai.azure.com",
282
+ "azureOpenaiDeployment": "YOUR_DEPLOYMENT_NAME",
283
+ "azureOpenaiApiVersion": "2024-10-01-preview",
284
+
223
285
  "ollamaBaseUrl": "http://localhost:11434",
224
286
  "ollamaModel": "llama3"
225
287
  }
226
288
  ```
227
289
 
228
- Only the fields for your active provider are required. This file is gitignored by default.
229
-
230
290
  ---
231
291
 
232
292
  ## What it checks
@@ -0,0 +1,59 @@
1
+ import { buildPrompt } from './prompt.js';
2
+ import { buildPatchPrompt } from './patch-prompt.js';
3
+ import { groupViolations } from './group.js';
4
+ import { BaseAIProvider } from './base.js';
5
+ export class AnthropicProvider extends BaseAIProvider {
6
+ apiKey;
7
+ model;
8
+ constructor(apiKey, model = 'claude-sonnet-4-6') {
9
+ super();
10
+ this.apiKey = apiKey;
11
+ this.model = model;
12
+ }
13
+ get headers() {
14
+ return {
15
+ 'Content-Type': 'application/json',
16
+ 'x-api-key': this.apiKey,
17
+ 'anthropic-version': '2023-06-01',
18
+ };
19
+ }
20
+ async generateFixes(violations, strategy = 'rule', framework) {
21
+ if (violations.length === 0)
22
+ return [];
23
+ const groups = groupViolations(violations, strategy);
24
+ const prompt = buildPrompt(groups, framework);
25
+ const response = await fetch('https://api.anthropic.com/v1/messages', {
26
+ method: 'POST',
27
+ headers: this.headers,
28
+ body: JSON.stringify({
29
+ model: this.model,
30
+ max_tokens: 8192,
31
+ temperature: 0.2,
32
+ messages: [{ role: 'user', content: prompt }],
33
+ }),
34
+ });
35
+ if (!response.ok) {
36
+ throw new Error(`Anthropic API error: ${response.status} ${await response.text()}`);
37
+ }
38
+ const data = await response.json();
39
+ const text = data.content?.[0]?.text ?? '[]';
40
+ return this.parse(text, groups, framework);
41
+ }
42
+ async generateFilePatch(fileContent, violations, filePath, framework) {
43
+ const prompt = buildPatchPrompt(fileContent, violations, filePath, framework);
44
+ const response = await fetch('https://api.anthropic.com/v1/messages', {
45
+ method: 'POST',
46
+ headers: this.headers,
47
+ body: JSON.stringify({
48
+ model: this.model,
49
+ max_tokens: 16384,
50
+ temperature: 0.1,
51
+ messages: [{ role: 'user', content: prompt }],
52
+ }),
53
+ });
54
+ if (!response.ok)
55
+ throw new Error(`Anthropic API error: ${response.status} ${await response.text()}`);
56
+ const data = await response.json();
57
+ return data.content?.[0]?.text ?? '';
58
+ }
59
+ }
@@ -0,0 +1,17 @@
1
+ import { OpenAICompatProvider } from './openai-compat.js';
2
+ export class AzureOpenAIProvider extends OpenAICompatProvider {
3
+ apiVersion;
4
+ constructor(endpoint, apiKey, deployment, apiVersion = '2024-10-01-preview') {
5
+ super(`${endpoint}/openai/deployments/${deployment}`, apiKey, deployment, 'Azure OpenAI');
6
+ this.apiVersion = apiVersion;
7
+ }
8
+ buildUrl(path) {
9
+ return `${this.baseUrl}${path}?api-version=${this.apiVersion}`;
10
+ }
11
+ buildHeaders() {
12
+ return {
13
+ 'Content-Type': 'application/json',
14
+ 'api-key': this.apiKey,
15
+ };
16
+ }
17
+ }
@@ -0,0 +1,39 @@
1
+ import { fallbackExplanation } from './fallback-explanation.js';
2
+ export class BaseAIProvider {
3
+ parse(text, groups, framework) {
4
+ try {
5
+ const jsonMatch = text.match(/\[[\s\S]*\]/);
6
+ const fixes = jsonMatch ? JSON.parse(jsonMatch[0]) : [];
7
+ return groups.map((g) => {
8
+ const found = fixes.find((f) => f.ruleId === g.ruleId);
9
+ return found
10
+ ? { ...found, selectors: g.selectors, instanceCount: g.count }
11
+ : this.fallbackFix(g, framework);
12
+ });
13
+ }
14
+ catch {
15
+ return groups.map((g) => this.fallbackFix(g, framework));
16
+ }
17
+ }
18
+ fallback(groups, framework) {
19
+ return groups.map((g) => this.fallbackFix(g, framework));
20
+ }
21
+ fallbackFix(g, framework) {
22
+ const v = g.representative;
23
+ const selectorList = g.selectors.map((s) => `- ${s}`).join('\n');
24
+ const explanation = fallbackExplanation(g.ruleId, g.description, g.wcag, g.level);
25
+ const fwNote = framework ? `This project uses ${framework}. ` : '';
26
+ const prompt = g.count > 1
27
+ ? `${fwNote}Fix WCAG 2.1 SC ${g.wcag} (Level ${g.level}) — ${g.description}\n\nAffected elements (${g.count} instances):\n${selectorList}\n\nRepresentative HTML:\n\`${v.html.slice(0, 300)}\`\n\nApply the fix to all ${g.count} instances in the codebase to comply with WCAG 2.1 SC ${g.wcag}.`
28
+ : `${fwNote}Fix WCAG 2.1 SC ${g.wcag} (Level ${g.level}) — ${g.description}\n\nAffected element:\n- Selector: \`${g.selectors[0]}\`\n- HTML: \`${v.html.slice(0, 300)}\`\n\nApply the fix to comply with WCAG 2.1 SC ${g.wcag}.`;
29
+ return {
30
+ ruleId: g.ruleId,
31
+ selectors: g.selectors,
32
+ instanceCount: g.count,
33
+ explanation,
34
+ fixedCode: v.html,
35
+ wcagReference: `WCAG 2.1 SC ${g.wcag}`,
36
+ optimalPrompt: prompt,
37
+ };
38
+ }
39
+ }
@@ -0,0 +1,58 @@
1
+ import { buildPrompt } from './prompt.js';
2
+ import { buildPatchPrompt } from './patch-prompt.js';
3
+ import { groupViolations } from './group.js';
4
+ import { BaseAIProvider } from './base.js';
5
+ export class CohereProvider extends BaseAIProvider {
6
+ apiKey;
7
+ model;
8
+ constructor(apiKey, model = 'command-r-plus') {
9
+ super();
10
+ this.apiKey = apiKey;
11
+ this.model = model;
12
+ }
13
+ get headers() {
14
+ return {
15
+ 'Content-Type': 'application/json',
16
+ 'Authorization': `Bearer ${this.apiKey}`,
17
+ };
18
+ }
19
+ async generateFixes(violations, strategy = 'rule', framework) {
20
+ if (violations.length === 0)
21
+ return [];
22
+ const groups = groupViolations(violations, strategy);
23
+ const prompt = buildPrompt(groups, framework);
24
+ const response = await fetch('https://api.cohere.com/v2/chat', {
25
+ method: 'POST',
26
+ headers: this.headers,
27
+ body: JSON.stringify({
28
+ model: this.model,
29
+ messages: [{ role: 'user', content: prompt }],
30
+ max_tokens: 8192,
31
+ temperature: 0.2,
32
+ }),
33
+ });
34
+ if (!response.ok) {
35
+ throw new Error(`Cohere API error: ${response.status} ${await response.text()}`);
36
+ }
37
+ const data = await response.json();
38
+ const text = data.message?.content?.[0]?.text ?? '[]';
39
+ return this.parse(text, groups, framework);
40
+ }
41
+ async generateFilePatch(fileContent, violations, filePath, framework) {
42
+ const prompt = buildPatchPrompt(fileContent, violations, filePath, framework);
43
+ const response = await fetch('https://api.cohere.com/v2/chat', {
44
+ method: 'POST',
45
+ headers: this.headers,
46
+ body: JSON.stringify({
47
+ model: this.model,
48
+ messages: [{ role: 'user', content: prompt }],
49
+ max_tokens: 16384,
50
+ temperature: 0.1,
51
+ }),
52
+ });
53
+ if (!response.ok)
54
+ throw new Error(`Cohere API error: ${response.status} ${await response.text()}`);
55
+ const data = await response.json();
56
+ return data.message?.content?.[0]?.text ?? '';
57
+ }
58
+ }
@@ -0,0 +1,6 @@
1
+ import { OpenAICompatProvider } from './openai-compat.js';
2
+ export class DeepSeekProvider extends OpenAICompatProvider {
3
+ constructor(apiKey, model = 'deepseek-chat') {
4
+ super('https://api.deepseek.com/v1', apiKey, model, 'DeepSeek');
5
+ }
6
+ }
package/dist/ai/gemini.js CHANGED
@@ -1,21 +1,24 @@
1
1
  import { buildPrompt } from './prompt.js';
2
2
  import { buildPatchPrompt } from './patch-prompt.js';
3
3
  import { groupViolations } from './group.js';
4
- import { fallbackExplanation } from './fallback-explanation.js';
5
- export class GeminiProvider {
4
+ import { BaseAIProvider } from './base.js';
5
+ export class GeminiProvider extends BaseAIProvider {
6
6
  apiKey;
7
7
  model;
8
8
  constructor(apiKey, model = 'gemini-2.5-flash') {
9
+ super();
9
10
  this.apiKey = apiKey;
10
11
  this.model = model;
11
12
  }
13
+ buildUrl(model) {
14
+ return `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${this.apiKey}`;
15
+ }
12
16
  async generateFixes(violations, strategy = 'rule', framework) {
13
17
  if (violations.length === 0)
14
18
  return [];
15
19
  const groups = groupViolations(violations, strategy);
16
20
  const prompt = buildPrompt(groups, framework);
17
- const url = `https://generativelanguage.googleapis.com/v1beta/models/${this.model}:generateContent?key=${this.apiKey}`;
18
- const response = await fetch(url, {
21
+ const response = await fetch(this.buildUrl(this.model), {
19
22
  method: 'POST',
20
23
  headers: { 'Content-Type': 'application/json' },
21
24
  body: JSON.stringify({
@@ -32,8 +35,7 @@ export class GeminiProvider {
32
35
  }
33
36
  async generateFilePatch(fileContent, violations, filePath, framework) {
34
37
  const prompt = buildPatchPrompt(fileContent, violations, filePath, framework);
35
- const url = `https://generativelanguage.googleapis.com/v1beta/models/${this.model}:generateContent?key=${this.apiKey}`;
36
- const response = await fetch(url, {
38
+ const response = await fetch(this.buildUrl(this.model), {
37
39
  method: 'POST',
38
40
  headers: { 'Content-Type': 'application/json' },
39
41
  body: JSON.stringify({
@@ -46,37 +48,4 @@ export class GeminiProvider {
46
48
  const data = await response.json();
47
49
  return data.candidates?.[0]?.content?.parts?.[0]?.text ?? '';
48
50
  }
49
- parse(text, groups, framework) {
50
- try {
51
- const jsonMatch = text.match(/\[[\s\S]*\]/);
52
- const fixes = jsonMatch ? JSON.parse(jsonMatch[0]) : [];
53
- return groups.map((g) => {
54
- const found = fixes.find((f) => f.ruleId === g.ruleId);
55
- return found
56
- ? { ...found, selectors: g.selectors, instanceCount: g.count }
57
- : this.fallbackFix(g, framework);
58
- });
59
- }
60
- catch {
61
- return groups.map((g) => this.fallbackFix(g, framework));
62
- }
63
- }
64
- fallbackFix(g, framework) {
65
- const v = g.representative;
66
- const selectorList = g.selectors.map((s) => `- ${s}`).join('\n');
67
- const explanation = fallbackExplanation(g.ruleId, g.description, g.wcag, g.level);
68
- const fwNote = framework ? `This project uses ${framework}. ` : '';
69
- const prompt = g.count > 1
70
- ? `${fwNote}Fix WCAG 2.1 SC ${g.wcag} (Level ${g.level}) — ${g.description}\n\nAffected elements (${g.count} instances):\n${selectorList}\n\nRepresentative HTML:\n\`${v.html.slice(0, 300)}\`\n\nApply the fix to all ${g.count} instances in the codebase to comply with WCAG 2.1 SC ${g.wcag}.`
71
- : `${fwNote}Fix WCAG 2.1 SC ${g.wcag} (Level ${g.level}) — ${g.description}\n\nAffected element:\n- Selector: \`${g.selectors[0]}\`\n- HTML: \`${v.html.slice(0, 300)}\`\n\nApply the fix to comply with WCAG 2.1 SC ${g.wcag}.`;
72
- return {
73
- ruleId: g.ruleId,
74
- selectors: g.selectors,
75
- instanceCount: g.count,
76
- explanation,
77
- fixedCode: v.html,
78
- wcagReference: `WCAG 2.1 SC ${g.wcag}`,
79
- optimalPrompt: prompt,
80
- };
81
- }
82
51
  }
@@ -0,0 +1,6 @@
1
+ import { OpenAICompatProvider } from './openai-compat.js';
2
+ export class GroqProvider extends OpenAICompatProvider {
3
+ constructor(apiKey, model = 'llama-3.3-70b-versatile') {
4
+ super('https://api.groq.com/openai/v1', apiKey, model, 'Groq');
5
+ }
6
+ }
package/dist/ai/index.js CHANGED
@@ -1,20 +1,84 @@
1
1
  import { GeminiProvider } from './gemini.js';
2
2
  import { OllamaProvider } from './ollama.js';
3
3
  import { OpenAIProvider } from './openai.js';
4
+ import { AnthropicProvider } from './anthropic.js';
5
+ import { MistralProvider } from './mistral.js';
6
+ import { GroqProvider } from './groq.js';
7
+ import { CohereProvider } from './cohere.js';
8
+ import { XAIProvider } from './xai.js';
9
+ import { DeepSeekProvider } from './deepseek.js';
10
+ import { TogetherProvider } from './together.js';
11
+ import { PerplexityProvider } from './perplexity.js';
12
+ import { AzureOpenAIProvider } from './azure-openai.js';
4
13
  export function createAIProvider(config) {
5
- if (config.provider === 'ollama') {
6
- return new OllamaProvider(config.ollamaBaseUrl, config.ollamaModel);
14
+ switch (config.provider) {
15
+ case 'ollama':
16
+ return new OllamaProvider(config.ollamaBaseUrl, config.ollamaModel);
17
+ case 'openai':
18
+ if (!config.openaiApiKey) {
19
+ console.error('No OpenAI API key found. Add "openaiApiKey" to a11y.config.json.');
20
+ process.exit(1);
21
+ }
22
+ return new OpenAIProvider(config.openaiApiKey, config.openaiModel);
23
+ case 'anthropic':
24
+ if (!config.anthropicApiKey) {
25
+ console.error('No Anthropic API key found. Add "anthropicApiKey" to a11y.config.json.');
26
+ process.exit(1);
27
+ }
28
+ return new AnthropicProvider(config.anthropicApiKey, config.anthropicModel);
29
+ case 'mistral':
30
+ if (!config.mistralApiKey) {
31
+ console.error('No Mistral API key found. Add "mistralApiKey" to a11y.config.json.');
32
+ process.exit(1);
33
+ }
34
+ return new MistralProvider(config.mistralApiKey, config.mistralModel);
35
+ case 'groq':
36
+ if (!config.groqApiKey) {
37
+ console.error('No Groq API key found. Add "groqApiKey" to a11y.config.json.');
38
+ process.exit(1);
39
+ }
40
+ return new GroqProvider(config.groqApiKey, config.groqModel);
41
+ case 'cohere':
42
+ if (!config.cohereApiKey) {
43
+ console.error('No Cohere API key found. Add "cohereApiKey" to a11y.config.json.');
44
+ process.exit(1);
45
+ }
46
+ return new CohereProvider(config.cohereApiKey, config.cohereModel);
47
+ case 'xai':
48
+ if (!config.xaiApiKey) {
49
+ console.error('No xAI API key found. Add "xaiApiKey" to a11y.config.json.');
50
+ process.exit(1);
51
+ }
52
+ return new XAIProvider(config.xaiApiKey, config.xaiModel);
53
+ case 'deepseek':
54
+ if (!config.deepseekApiKey) {
55
+ console.error('No DeepSeek API key found. Add "deepseekApiKey" to a11y.config.json.');
56
+ process.exit(1);
57
+ }
58
+ return new DeepSeekProvider(config.deepseekApiKey, config.deepseekModel);
59
+ case 'together':
60
+ if (!config.togetherApiKey) {
61
+ console.error('No Together AI API key found. Add "togetherApiKey" to a11y.config.json.');
62
+ process.exit(1);
63
+ }
64
+ return new TogetherProvider(config.togetherApiKey, config.togetherModel);
65
+ case 'perplexity':
66
+ if (!config.perplexityApiKey) {
67
+ console.error('No Perplexity API key found. Add "perplexityApiKey" to a11y.config.json.');
68
+ process.exit(1);
69
+ }
70
+ return new PerplexityProvider(config.perplexityApiKey, config.perplexityModel);
71
+ case 'azure-openai':
72
+ if (!config.azureOpenaiApiKey || !config.azureOpenaiEndpoint || !config.azureOpenaiDeployment) {
73
+ console.error('Azure OpenAI requires "azureOpenaiApiKey", "azureOpenaiEndpoint", and "azureOpenaiDeployment" in a11y.config.json.');
74
+ process.exit(1);
75
+ }
76
+ return new AzureOpenAIProvider(config.azureOpenaiEndpoint, config.azureOpenaiApiKey, config.azureOpenaiDeployment, config.azureOpenaiApiVersion);
77
+ default:
78
+ if (!config.apiKey) {
79
+ console.error('No API key found. Run: wcag-a11y init');
80
+ process.exit(1);
81
+ }
82
+ return new GeminiProvider(config.apiKey, config.model);
7
83
  }
8
- if (config.provider === 'openai') {
9
- if (!config.openaiApiKey) {
10
- console.error('No OpenAI API key found. Add "openaiApiKey" to a11y.config.json.');
11
- process.exit(1);
12
- }
13
- return new OpenAIProvider(config.openaiApiKey, config.openaiModel);
14
- }
15
- if (!config.apiKey) {
16
- console.error('No API key found. Run: wcag-a11y init');
17
- process.exit(1);
18
- }
19
- return new GeminiProvider(config.apiKey, config.model);
20
84
  }
@@ -0,0 +1,6 @@
1
+ import { OpenAICompatProvider } from './openai-compat.js';
2
+ export class MistralProvider extends OpenAICompatProvider {
3
+ constructor(apiKey, model = 'mistral-large-latest') {
4
+ super('https://api.mistral.ai/v1', apiKey, model, 'Mistral');
5
+ }
6
+ }
package/dist/ai/ollama.js CHANGED
@@ -1,11 +1,12 @@
1
1
  import { buildPrompt } from './prompt.js';
2
2
  import { buildPatchPrompt } from './patch-prompt.js';
3
3
  import { groupViolations } from './group.js';
4
- import { fallbackExplanation } from './fallback-explanation.js';
5
- export class OllamaProvider {
4
+ import { BaseAIProvider } from './base.js';
5
+ export class OllamaProvider extends BaseAIProvider {
6
6
  baseUrl;
7
7
  model;
8
8
  constructor(baseUrl = 'http://localhost:11434', model = 'llama3') {
9
+ super();
9
10
  this.baseUrl = baseUrl;
10
11
  this.model = model;
11
12
  }
@@ -52,25 +53,4 @@ export class OllamaProvider {
52
53
  const data = await response.json();
53
54
  return data.response ?? '';
54
55
  }
55
- fallback(groups, framework) {
56
- return groups.map((g) => this.fallbackFix(g, framework));
57
- }
58
- fallbackFix(g, framework) {
59
- const v = g.representative;
60
- const selectorList = g.selectors.map((s) => `- ${s}`).join('\n');
61
- const explanation = fallbackExplanation(g.ruleId, g.description, g.wcag, g.level);
62
- const fwNote = framework ? `This project uses ${framework}. ` : '';
63
- const prompt = g.count > 1
64
- ? `${fwNote}Fix WCAG 2.1 SC ${g.wcag} (Level ${g.level}) — ${g.description}\n\nAffected elements (${g.count} instances):\n${selectorList}\n\nRepresentative HTML:\n\`${v.html.slice(0, 300)}\`\n\nApply the fix to all ${g.count} instances in the codebase to comply with WCAG 2.1 SC ${g.wcag}.`
65
- : `${fwNote}Fix WCAG 2.1 SC ${g.wcag} (Level ${g.level}) — ${g.description}\n\nAffected element:\n- Selector: \`${g.selectors[0]}\`\n- HTML: \`${v.html.slice(0, 300)}\`\n\nApply the fix to comply with WCAG 2.1 SC ${g.wcag}.`;
66
- return {
67
- ruleId: g.ruleId,
68
- selectors: g.selectors,
69
- instanceCount: g.count,
70
- explanation,
71
- fixedCode: v.html,
72
- wcagReference: `WCAG 2.1 SC ${g.wcag}`,
73
- optimalPrompt: prompt,
74
- };
75
- }
76
56
  }
@@ -0,0 +1,65 @@
1
+ import { buildPrompt } from './prompt.js';
2
+ import { buildPatchPrompt } from './patch-prompt.js';
3
+ import { groupViolations } from './group.js';
4
+ import { BaseAIProvider } from './base.js';
5
+ export class OpenAICompatProvider extends BaseAIProvider {
6
+ baseUrl;
7
+ apiKey;
8
+ model;
9
+ providerName;
10
+ constructor(baseUrl, apiKey, model, providerName = 'API') {
11
+ super();
12
+ this.baseUrl = baseUrl;
13
+ this.apiKey = apiKey;
14
+ this.model = model;
15
+ this.providerName = providerName;
16
+ }
17
+ buildUrl(path) {
18
+ return `${this.baseUrl}${path}`;
19
+ }
20
+ buildHeaders() {
21
+ return {
22
+ 'Content-Type': 'application/json',
23
+ 'Authorization': `Bearer ${this.apiKey}`,
24
+ };
25
+ }
26
+ async generateFixes(violations, strategy = 'rule', framework) {
27
+ if (violations.length === 0)
28
+ return [];
29
+ const groups = groupViolations(violations, strategy);
30
+ const prompt = buildPrompt(groups, framework);
31
+ const response = await fetch(this.buildUrl('/chat/completions'), {
32
+ method: 'POST',
33
+ headers: this.buildHeaders(),
34
+ body: JSON.stringify({
35
+ model: this.model,
36
+ messages: [{ role: 'user', content: prompt }],
37
+ temperature: 0.2,
38
+ max_tokens: 8192,
39
+ }),
40
+ });
41
+ if (!response.ok) {
42
+ throw new Error(`${this.providerName} API error: ${response.status} ${await response.text()}`);
43
+ }
44
+ const data = await response.json();
45
+ const text = data.choices?.[0]?.message?.content ?? '[]';
46
+ return this.parse(text, groups, framework);
47
+ }
48
+ async generateFilePatch(fileContent, violations, filePath, framework) {
49
+ const prompt = buildPatchPrompt(fileContent, violations, filePath, framework);
50
+ const response = await fetch(this.buildUrl('/chat/completions'), {
51
+ method: 'POST',
52
+ headers: this.buildHeaders(),
53
+ body: JSON.stringify({
54
+ model: this.model,
55
+ messages: [{ role: 'user', content: prompt }],
56
+ temperature: 0.1,
57
+ max_tokens: 16384,
58
+ }),
59
+ });
60
+ if (!response.ok)
61
+ throw new Error(`${this.providerName} API error: ${response.status} ${await response.text()}`);
62
+ const data = await response.json();
63
+ return data.choices?.[0]?.message?.content ?? '';
64
+ }
65
+ }
package/dist/ai/openai.js CHANGED
@@ -1,87 +1,6 @@
1
- import { buildPrompt } from './prompt.js';
2
- import { buildPatchPrompt } from './patch-prompt.js';
3
- import { groupViolations } from './group.js';
4
- import { fallbackExplanation } from './fallback-explanation.js';
5
- export class OpenAIProvider {
6
- apiKey;
7
- model;
1
+ import { OpenAICompatProvider } from './openai-compat.js';
2
+ export class OpenAIProvider extends OpenAICompatProvider {
8
3
  constructor(apiKey, model = 'gpt-4o-mini') {
9
- this.apiKey = apiKey;
10
- this.model = model;
11
- }
12
- async generateFixes(violations, strategy = 'rule', framework) {
13
- if (violations.length === 0)
14
- return [];
15
- const groups = groupViolations(violations, strategy);
16
- const prompt = buildPrompt(groups, framework);
17
- const response = await fetch('https://api.openai.com/v1/chat/completions', {
18
- method: 'POST',
19
- headers: {
20
- 'Content-Type': 'application/json',
21
- 'Authorization': `Bearer ${this.apiKey}`,
22
- },
23
- body: JSON.stringify({
24
- model: this.model,
25
- messages: [{ role: 'user', content: prompt }],
26
- temperature: 0.2,
27
- max_tokens: 8192,
28
- }),
29
- });
30
- if (!response.ok) {
31
- throw new Error(`OpenAI API error: ${response.status} ${await response.text()}`);
32
- }
33
- const data = await response.json();
34
- const text = data.choices?.[0]?.message?.content ?? '[]';
35
- return this.parse(text, groups, framework);
36
- }
37
- async generateFilePatch(fileContent, violations, filePath, framework) {
38
- const prompt = buildPatchPrompt(fileContent, violations, filePath, framework);
39
- const response = await fetch('https://api.openai.com/v1/chat/completions', {
40
- method: 'POST',
41
- headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this.apiKey}` },
42
- body: JSON.stringify({
43
- model: this.model,
44
- messages: [{ role: 'user', content: prompt }],
45
- temperature: 0.1,
46
- max_tokens: 16384,
47
- }),
48
- });
49
- if (!response.ok)
50
- throw new Error(`OpenAI API error: ${response.status} ${await response.text()}`);
51
- const data = await response.json();
52
- return data.choices?.[0]?.message?.content ?? '';
53
- }
54
- parse(text, groups, framework) {
55
- try {
56
- const jsonMatch = text.match(/\[[\s\S]*\]/);
57
- const fixes = jsonMatch ? JSON.parse(jsonMatch[0]) : [];
58
- return groups.map((g) => {
59
- const found = fixes.find((f) => f.ruleId === g.ruleId);
60
- return found
61
- ? { ...found, selectors: g.selectors, instanceCount: g.count }
62
- : this.fallbackFix(g, framework);
63
- });
64
- }
65
- catch {
66
- return groups.map((g) => this.fallbackFix(g, framework));
67
- }
68
- }
69
- fallbackFix(g, framework) {
70
- const v = g.representative;
71
- const selectorList = g.selectors.map((s) => `- ${s}`).join('\n');
72
- const explanation = fallbackExplanation(g.ruleId, g.description, g.wcag, g.level);
73
- const fwNote = framework ? `This project uses ${framework}. ` : '';
74
- const prompt = g.count > 1
75
- ? `${fwNote}Fix WCAG 2.1 SC ${g.wcag} (Level ${g.level}) — ${g.description}\n\nAffected elements (${g.count} instances):\n${selectorList}\n\nRepresentative HTML:\n\`${v.html.slice(0, 300)}\`\n\nApply the fix to all ${g.count} instances in the codebase to comply with WCAG 2.1 SC ${g.wcag}.`
76
- : `${fwNote}Fix WCAG 2.1 SC ${g.wcag} (Level ${g.level}) — ${g.description}\n\nAffected element:\n- Selector: \`${g.selectors[0]}\`\n- HTML: \`${v.html.slice(0, 300)}\`\n\nApply the fix to comply with WCAG 2.1 SC ${g.wcag}.`;
77
- return {
78
- ruleId: g.ruleId,
79
- selectors: g.selectors,
80
- instanceCount: g.count,
81
- explanation,
82
- fixedCode: v.html,
83
- wcagReference: `WCAG 2.1 SC ${g.wcag}`,
84
- optimalPrompt: prompt,
85
- };
4
+ super('https://api.openai.com/v1', apiKey, model, 'OpenAI');
86
5
  }
87
6
  }
@@ -0,0 +1,6 @@
1
+ import { OpenAICompatProvider } from './openai-compat.js';
2
+ export class PerplexityProvider extends OpenAICompatProvider {
3
+ constructor(apiKey, model = 'llama-3.1-sonar-large-128k-online') {
4
+ super('https://api.perplexity.ai', apiKey, model, 'Perplexity');
5
+ }
6
+ }
@@ -0,0 +1,6 @@
1
+ import { OpenAICompatProvider } from './openai-compat.js';
2
+ export class TogetherProvider extends OpenAICompatProvider {
3
+ constructor(apiKey, model = 'meta-llama/Llama-3-70b-chat-hf') {
4
+ super('https://api.together.xyz/v1', apiKey, model, 'Together AI');
5
+ }
6
+ }
package/dist/ai/xai.js ADDED
@@ -0,0 +1,6 @@
1
+ import { OpenAICompatProvider } from './openai-compat.js';
2
+ export class XAIProvider extends OpenAICompatProvider {
3
+ constructor(apiKey, model = 'grok-2') {
4
+ super('https://api.x.ai/v1', apiKey, model, 'xAI');
5
+ }
6
+ }
package/dist/cli.js CHANGED
@@ -13,11 +13,11 @@ const program = new Command();
13
13
  program
14
14
  .name('wcag-a11y')
15
15
  .description('WCAG 2.1/2.2 accessibility auditor with AI-powered fixes')
16
- .version('0.3.5');
16
+ .version('0.4.0');
17
17
  program
18
18
  .command('init')
19
19
  .description('Create a11y.config.json in the current directory')
20
- .option('--provider <name>', 'AI provider to configure (gemini|openai|ollama)', 'gemini')
20
+ .option('--provider <name>', 'AI provider to configure (gemini|openai|ollama|anthropic|mistral|groq|cohere|xai|deepseek|together|perplexity|azure-openai)', 'gemini')
21
21
  .action((opts) => {
22
22
  initConfig(opts.provider);
23
23
  });
@@ -34,7 +34,7 @@ program
34
34
  .option('--fast-mode', 'Output only AI fix prompts — no summaries or explanations', false)
35
35
  .option('--group <strategy>', 'Group violations by rule or show individually (rule|none)', 'rule')
36
36
  .option('--ci', 'Exit with code 1 if any violations are found (for CI/CD pipelines)', false)
37
- .option('--provider <name>', 'Override the AI provider from config (gemini|openai|ollama)')
37
+ .option('--provider <name>', 'Override the AI provider from config (gemini|openai|ollama|anthropic|mistral|groq|cohere|xai|deepseek|together|perplexity|azure-openai)')
38
38
  .action(async (opts) => {
39
39
  try {
40
40
  console.log(`\nScanning ${opts.url}...`);
@@ -77,21 +77,30 @@ program
77
77
  program
78
78
  .command('fix')
79
79
  .description('Scan for violations and apply AI fixes directly to source files')
80
- .requiredOption('-u, --url <url>', 'Base URL of your dev server (e.g. http://localhost:3000)')
80
+ .option('-u, --url <url>', 'Base URL of your dev server (e.g. http://localhost:3000)')
81
81
  .option('-p, --pages <pages...>', 'Specific pages to scan', ['/'])
82
82
  .option('-c, --crawl', 'Auto-discover pages by following same-origin links', false)
83
+ .option('--from-report [path]', 'Use an existing report instead of scanning (default: a11y-report.md)')
83
84
  .option('--apply', 'Write fixes to source files (default: dry-run, shows diff only)', false)
84
- .option('--provider <name>', 'Override the AI provider from config (gemini|openai|ollama)')
85
+ .option('--provider <name>', 'Override the AI provider from config (gemini|openai|ollama|anthropic|mistral|groq|cohere|xai|deepseek|together|perplexity|azure-openai)')
85
86
  .action(async (opts) => {
87
+ if (!opts.url && !opts.fromReport) {
88
+ console.error('\nError: provide --url <url> to scan, or --from-report [path] to load an existing report.');
89
+ process.exit(1);
90
+ }
86
91
  try {
87
92
  const config = loadConfig();
88
93
  if (opts.provider)
89
94
  config.provider = opts.provider;
90
95
  const provider = createAIProvider(config);
96
+ const reportPath = opts.fromReport
97
+ ? (opts.fromReport === true ? 'a11y-report.md' : opts.fromReport)
98
+ : undefined;
91
99
  await runFix({
92
100
  url: opts.url,
93
101
  pages: opts.pages,
94
102
  crawl: opts.crawl,
103
+ reportPath,
95
104
  apply: opts.apply,
96
105
  provider,
97
106
  srcDir: resolve(process.cwd(), 'src'),
package/dist/config.js CHANGED
@@ -7,6 +7,15 @@ const DEFAULTS = {
7
7
  ollamaBaseUrl: 'http://localhost:11434',
8
8
  ollamaModel: 'llama3',
9
9
  openaiModel: 'gpt-4o-mini',
10
+ anthropicModel: 'claude-sonnet-4-6',
11
+ mistralModel: 'mistral-large-latest',
12
+ groqModel: 'llama-3.3-70b-versatile',
13
+ cohereModel: 'command-r-plus',
14
+ xaiModel: 'grok-2',
15
+ deepseekModel: 'deepseek-chat',
16
+ togetherModel: 'meta-llama/Llama-3-70b-chat-hf',
17
+ perplexityModel: 'llama-3.1-sonar-large-128k-online',
18
+ azureOpenaiApiVersion: '2024-10-01-preview',
10
19
  };
11
20
  export function loadConfig() {
12
21
  const configPath = join(process.cwd(), CONFIG_FILE);
@@ -30,6 +39,48 @@ const STARTER_CONFIGS = {
30
39
  { provider: 'ollama', ollamaBaseUrl: 'http://localhost:11434', ollamaModel: 'llama3' },
31
40
  `Created ${CONFIG_FILE} — run \`ollama serve\` to start the local model server`,
32
41
  ],
42
+ anthropic: [
43
+ { provider: 'anthropic', anthropicApiKey: 'YOUR_ANTHROPIC_API_KEY', anthropicModel: 'claude-sonnet-4-6' },
44
+ `Created ${CONFIG_FILE} — add your Anthropic API key from https://console.anthropic.com`,
45
+ ],
46
+ mistral: [
47
+ { provider: 'mistral', mistralApiKey: 'YOUR_MISTRAL_API_KEY', mistralModel: 'mistral-large-latest' },
48
+ `Created ${CONFIG_FILE} — add your Mistral API key from https://console.mistral.ai`,
49
+ ],
50
+ groq: [
51
+ { provider: 'groq', groqApiKey: 'YOUR_GROQ_API_KEY', groqModel: 'llama-3.3-70b-versatile' },
52
+ `Created ${CONFIG_FILE} — add your Groq API key from https://console.groq.com`,
53
+ ],
54
+ cohere: [
55
+ { provider: 'cohere', cohereApiKey: 'YOUR_COHERE_API_KEY', cohereModel: 'command-r-plus' },
56
+ `Created ${CONFIG_FILE} — add your Cohere API key from https://dashboard.cohere.com`,
57
+ ],
58
+ xai: [
59
+ { provider: 'xai', xaiApiKey: 'YOUR_XAI_API_KEY', xaiModel: 'grok-2' },
60
+ `Created ${CONFIG_FILE} — add your xAI API key from https://console.x.ai`,
61
+ ],
62
+ deepseek: [
63
+ { provider: 'deepseek', deepseekApiKey: 'YOUR_DEEPSEEK_API_KEY', deepseekModel: 'deepseek-chat' },
64
+ `Created ${CONFIG_FILE} — add your DeepSeek API key from https://platform.deepseek.com`,
65
+ ],
66
+ together: [
67
+ { provider: 'together', togetherApiKey: 'YOUR_TOGETHER_API_KEY', togetherModel: 'meta-llama/Llama-3-70b-chat-hf' },
68
+ `Created ${CONFIG_FILE} — add your Together AI API key from https://api.together.xyz`,
69
+ ],
70
+ perplexity: [
71
+ { provider: 'perplexity', perplexityApiKey: 'YOUR_PERPLEXITY_API_KEY', perplexityModel: 'llama-3.1-sonar-large-128k-online' },
72
+ `Created ${CONFIG_FILE} — add your Perplexity API key from https://www.perplexity.ai/settings/api`,
73
+ ],
74
+ 'azure-openai': [
75
+ {
76
+ provider: 'azure-openai',
77
+ azureOpenaiApiKey: 'YOUR_AZURE_OPENAI_API_KEY',
78
+ azureOpenaiEndpoint: 'https://YOUR_RESOURCE.openai.azure.com',
79
+ azureOpenaiDeployment: 'YOUR_DEPLOYMENT_NAME',
80
+ azureOpenaiApiVersion: '2024-10-01-preview',
81
+ },
82
+ `Created ${CONFIG_FILE} — fill in your Azure OpenAI endpoint, deployment, and API key from https://portal.azure.com`,
83
+ ],
33
84
  };
34
85
  export function initConfig(provider = 'gemini') {
35
86
  const configPath = join(process.cwd(), CONFIG_FILE);
package/dist/fixer.js CHANGED
@@ -5,15 +5,32 @@ import { crawl } from './crawler.js';
5
5
  const SOURCE_EXTS = new Set(['.jsx', '.tsx', '.js', '.ts', '.vue', '.svelte', '.html']);
6
6
  const SKIP_DIRS = new Set(['node_modules', 'dist', 'build', '.git', '.next', '.nuxt', 'coverage', 'public']);
7
7
  export async function runFix(opts) {
8
- console.log(`\nScanning ${opts.url}...`);
9
- const result = await crawl({ url: opts.url, pages: opts.pages, crawl: opts.crawl });
10
- const { framework } = result;
11
- if (result.totalViolations === 0) {
12
- console.log(chalk.green('\nNo violations found.'));
13
- return;
8
+ let allViolations;
9
+ let framework;
10
+ if (opts.reportPath) {
11
+ const absReport = resolve(process.cwd(), opts.reportPath);
12
+ if (!existsSync(absReport)) {
13
+ throw new Error(`Report file not found: ${opts.reportPath}`);
14
+ }
15
+ console.log(`\nLoading violations from ${opts.reportPath}...`);
16
+ allViolations = parseReportViolations(absReport);
17
+ if (allViolations.length === 0) {
18
+ console.log(chalk.green('\nNo violations found in report.'));
19
+ return;
20
+ }
21
+ console.log(`Found ${allViolations.length} violation(s) in report. Locating source files...\n`);
22
+ }
23
+ else {
24
+ console.log(`\nScanning ${opts.url}...`);
25
+ const result = await crawl({ url: opts.url, pages: opts.pages, crawl: opts.crawl });
26
+ framework = result.framework;
27
+ if (result.totalViolations === 0) {
28
+ console.log(chalk.green('\nNo violations found.'));
29
+ return;
30
+ }
31
+ console.log(`Found ${result.totalViolations} violation(s). Locating source files...\n`);
32
+ allViolations = result.pages.flatMap((p) => p.violations);
14
33
  }
15
- console.log(`Found ${result.totalViolations} violation(s). Locating source files...\n`);
16
- const allViolations = result.pages.flatMap((p) => p.violations);
17
34
  // Group violations by resolved source file path
18
35
  const fileGroups = new Map();
19
36
  let unlocated = 0;
@@ -97,6 +114,57 @@ export async function runFix(opts) {
97
114
  console.log(chalk.yellow(`${unlocated} violation(s) could not be located in source files.`));
98
115
  }
99
116
  }
117
+ function parseReportViolations(reportPath) {
118
+ const content = readFileSync(reportPath, 'utf8');
119
+ const violations = [];
120
+ const pageParts = content.split(/^## Page:/m).slice(1);
121
+ for (const part of pageParts) {
122
+ const pageUrl = part.split('\n')[0].trim();
123
+ if (part.includes('✅ No violations found'))
124
+ continue;
125
+ for (const group of part.split(/^---$/m)) {
126
+ const headingMatch = group.match(/^###\s+\S+\s+\[(\w+)\]\s+(.+)$/m);
127
+ if (!headingMatch)
128
+ continue;
129
+ const impact = headingMatch[1].toLowerCase();
130
+ const description = headingMatch[2].trim();
131
+ const ruleMatch = group.match(/\*\*Rule:\*\*\s+`([^`]+)`/);
132
+ if (!ruleMatch)
133
+ continue;
134
+ const ruleId = ruleMatch[1];
135
+ let wcag = '';
136
+ let level = 'A';
137
+ const wcagStd = group.match(/\*\*WCAG:\*\*\s+SC\s+([\d.]+)\s+\(Level\s+([A-Z]+)\)/);
138
+ if (wcagStd) {
139
+ wcag = wcagStd[1];
140
+ level = wcagStd[2];
141
+ }
142
+ else {
143
+ const wcagAI = group.match(/\*\*WCAG:\*\*\s+WCAG\s+[\d.]+\s+SC\s+([\d.]+)/);
144
+ if (wcagAI)
145
+ wcag = wcagAI[1];
146
+ }
147
+ const repMatch = group.match(/\*\*Representative element:\*\*\s*\n`([^`]+)`(?:\s+—\s+`([^`]+)`)?/);
148
+ if (!repMatch)
149
+ continue;
150
+ const selector = repMatch[1];
151
+ const source = repMatch[2];
152
+ const htmlMatch = group.match(/\*\*Representative element:\*\*[\s\S]*?```html\n([\s\S]*?)\n```/);
153
+ const html = htmlMatch?.[1] ?? '';
154
+ violations.push({ ruleId, wcag, level, impact, description, selector, html, page: pageUrl, ...(source ? { source } : {}) });
155
+ const alsoSection = group.match(/\*\*Also affects[^*]*\*\*([\s\S]*?)(?:\n\n\*\*|\n---|\n```|$)/);
156
+ if (alsoSection) {
157
+ for (const line of alsoSection[1].split('\n')) {
158
+ const m = line.match(/^-\s+`([^`]+)`(?:\s+—\s+`([^`]+)`)?/);
159
+ if (!m)
160
+ continue;
161
+ violations.push({ ruleId, wcag, level, impact, description, selector: m[1], html: '', page: pageUrl, ...(m[2] ? { source: m[2] } : {}) });
162
+ }
163
+ }
164
+ }
165
+ }
166
+ return violations;
167
+ }
100
168
  export function findSourceFile(violation, srcDir) {
101
169
  // Priority 1: React dev-mode source annotation
102
170
  if (violation.source) {
package/package.json CHANGED
@@ -1,7 +1,25 @@
1
1
  {
2
2
  "name": "wcag-a11y",
3
- "version": "0.3.5",
4
- "description": "WCAG 2.1/2.2 accessibility auditor with AI-powered fixes",
3
+ "version": "0.4.0",
4
+ "description": "WCAG 2.1/2.2 accessibility auditor with AI-powered fixes. Crawls your dev server with Playwright, runs 40+ checks, and uses AI (12 providers) to generate fix prompts or patch source files directly.",
5
+ "keywords": [
6
+ "wcag",
7
+ "accessibility",
8
+ "a11y",
9
+ "wcag2",
10
+ "wcag21",
11
+ "wcag22",
12
+ "aria",
13
+ "playwright",
14
+ "ai",
15
+ "audit",
16
+ "fix",
17
+ "cli",
18
+ "gemini",
19
+ "openai",
20
+ "anthropic",
21
+ "screen-reader"
22
+ ],
5
23
  "type": "module",
6
24
  "bin": {
7
25
  "wcag-a11y": "dist/cli.js"