a11y-pilot 1.0.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/LICENSE +21 -0
- package/README.md +281 -0
- package/bin/a11y-pilot.js +5 -0
- package/package.json +67 -0
- package/src/cli.js +324 -0
- package/src/copilot-bridge.js +332 -0
- package/src/parsers/html-parser.js +124 -0
- package/src/parsers/jsx-parser.js +134 -0
- package/src/reporter.js +281 -0
- package/src/rules/anchor-content.js +46 -0
- package/src/rules/button-content.js +37 -0
- package/src/rules/form-label.js +47 -0
- package/src/rules/heading-order.js +51 -0
- package/src/rules/img-alt.js +40 -0
- package/src/rules/index.js +43 -0
- package/src/rules/no-autofocus.js +32 -0
- package/src/rules/no-div-button.js +54 -0
- package/src/rules/semantic-nav.js +75 -0
- package/src/scanner.js +106 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 a11y-pilot
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
# 🛫 a11y-pilot
|
|
2
|
+
|
|
3
|
+
> **Your accessibility co-pilot for the terminal.**
|
|
4
|
+
> AI-powered CLI that scans frontend codebases for accessibility issues and auto-fixes them using GitHub Copilot CLI.
|
|
5
|
+
|
|
6
|
+
<p align="center">
|
|
7
|
+
<img src="docs/banner.png" alt="a11y-pilot banner" width="700" />
|
|
8
|
+
</p>
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## What is a11y-pilot?
|
|
13
|
+
|
|
14
|
+
**a11y-pilot** is a CLI tool that works like ESLint for accessibility. Run it in any frontend project directory to:
|
|
15
|
+
|
|
16
|
+
1. **Scan** your HTML, JSX, and TSX files for accessibility violations
|
|
17
|
+
2. **Report** issues with file, line number, severity, and WCAG references
|
|
18
|
+
3. **Auto-fix** issues by invoking GitHub Copilot CLI as the refactoring engine
|
|
19
|
+
|
|
20
|
+
It catches real problems that affect real users — missing alt text, non-semantic buttons, unlabeled form inputs, broken heading hierarchies, and more.
|
|
21
|
+
|
|
22
|
+
## Why?
|
|
23
|
+
|
|
24
|
+
**95%+ of websites fail basic accessibility checks** ([WebAIM Million Report](https://webaim.org/projects/million/)). Most developers don't skip accessibility on purpose — they just don't have the tools that make it easy. a11y-pilot brings a11y linting right into your terminal workflow, and uses Copilot CLI to fix issues intelligently.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Quick Start
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# Clone the repo
|
|
32
|
+
git clone https://github.com/YOUR_USERNAME/a11y-pilot.git
|
|
33
|
+
cd a11y-pilot
|
|
34
|
+
npm install
|
|
35
|
+
|
|
36
|
+
# Scan the test fixtures
|
|
37
|
+
node bin/a11y-pilot.js scan test/fixtures/
|
|
38
|
+
|
|
39
|
+
# Scan your own project
|
|
40
|
+
node bin/a11y-pilot.js scan /path/to/your/frontend/src
|
|
41
|
+
|
|
42
|
+
# Show Copilot CLI fix commands
|
|
43
|
+
node bin/a11y-pilot.js scan ./src --fix
|
|
44
|
+
|
|
45
|
+
# Auto-fix with Copilot CLI (requires copilot CLI installed)
|
|
46
|
+
node bin/a11y-pilot.js fix ./src
|
|
47
|
+
|
|
48
|
+
# Or use the scan command with --auto-fix
|
|
49
|
+
node bin/a11y-pilot.js scan ./src --auto-fix
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Install Globally (optional)
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
npm link
|
|
56
|
+
a11y-pilot scan ./src
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Commands
|
|
62
|
+
|
|
63
|
+
### `scan [path]`
|
|
64
|
+
|
|
65
|
+
Scan files for accessibility issues.
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
a11y-pilot scan # Scan current directory
|
|
69
|
+
a11y-pilot scan ./src/components # Scan specific directory
|
|
70
|
+
a11y-pilot scan ./src/App.tsx # Scan a single file
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**Flags:**
|
|
74
|
+
|
|
75
|
+
| Flag | Description |
|
|
76
|
+
|------|-------------|
|
|
77
|
+
| `-r, --rules <rules>` | Comma-separated rule IDs to check (e.g., `img-alt,no-div-button`) |
|
|
78
|
+
| `-f, --format <format>` | Output format: `text` (default) or `json` |
|
|
79
|
+
| `--fix` | Show Copilot CLI commands for each issue |
|
|
80
|
+
| `--auto-fix` | Invoke Copilot CLI to auto-fix issues |
|
|
81
|
+
| `--dry-run` | Preview what auto-fix would do (no changes) |
|
|
82
|
+
| `--one-by-one` | Fix issues individually instead of batching |
|
|
83
|
+
|
|
84
|
+
### `fix [path]`
|
|
85
|
+
|
|
86
|
+
Convenience command — scans and auto-fixes in one step.
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
a11y-pilot fix ./src
|
|
90
|
+
a11y-pilot fix ./src --dry-run
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### `rules`
|
|
94
|
+
|
|
95
|
+
List all available accessibility rules.
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
a11y-pilot rules
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Rules
|
|
104
|
+
|
|
105
|
+
a11y-pilot ships with 8 high-impact accessibility rules:
|
|
106
|
+
|
|
107
|
+
| Rule | Severity | WCAG | What it catches |
|
|
108
|
+
|------|----------|------|----------------|
|
|
109
|
+
| `img-alt` | error | 1.1.1 | `<img>` without `alt` attribute |
|
|
110
|
+
| `button-content` | error | 4.1.2 | Empty `<button>` with no text/aria-label |
|
|
111
|
+
| `no-div-button` | error | 4.1.2, 2.1.1 | `<div>`/`<span>` with `onClick` but no `role`/`tabIndex` |
|
|
112
|
+
| `form-label` | error | 1.3.1, 4.1.2 | `<input>` without associated label or `aria-label` |
|
|
113
|
+
| `heading-order` | warning | 1.3.1 | Skipped heading levels (h1 → h3) |
|
|
114
|
+
| `anchor-content` | error | 2.4.4, 4.1.2 | `<a>` with no text content or `aria-label` |
|
|
115
|
+
| `no-autofocus` | warning | 3.2.1 | Usage of `autoFocus` attribute |
|
|
116
|
+
| `semantic-nav` | warning | 1.3.1, 2.4.1 | Navigation links not wrapped in `<nav>` |
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Copilot CLI Integration
|
|
121
|
+
|
|
122
|
+
a11y-pilot uses GitHub Copilot CLI as its AI-powered fix engine. When you run `--auto-fix` or `a11y-pilot fix`:
|
|
123
|
+
|
|
124
|
+
1. **Detection** — a11y-pilot scans your code and identifies issues
|
|
125
|
+
2. **Prompt Engineering** — For each issue, it builds a precise, context-rich prompt
|
|
126
|
+
3. **Copilot Invocation** — It spawns `copilot` CLI with the fix prompt
|
|
127
|
+
4. **Intelligent Fixing** — Copilot CLI reads your file, understands the context, and makes the fix
|
|
128
|
+
5. **Progress Reporting** — You see real-time status of each fix
|
|
129
|
+
|
|
130
|
+
### How it works under the hood
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
|
|
134
|
+
│ a11y-pilot │────▶│ Issue detected │────▶│ Build prompt │
|
|
135
|
+
│ scanner │ │ (rule engine) │ │ (context-rich) │
|
|
136
|
+
└─────────────────┘ └──────────────────┘ └────────┬────────┘
|
|
137
|
+
│
|
|
138
|
+
▼
|
|
139
|
+
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
|
|
140
|
+
│ File fixed! │◀────│ Copilot applies │◀────│ copilot CLI │
|
|
141
|
+
│ ✔ Report │ │ the fix │ │ invoked │
|
|
142
|
+
└─────────────────┘ └──────────────────┘ └─────────────────┘
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Prerequisites
|
|
146
|
+
|
|
147
|
+
- [GitHub Copilot CLI](https://github.com/github/copilot-cli) installed and authenticated
|
|
148
|
+
- Node.js 18+
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
# Install Copilot CLI
|
|
152
|
+
# See: https://github.com/github/copilot-cli
|
|
153
|
+
|
|
154
|
+
# Authenticate
|
|
155
|
+
copilot auth login
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## JSON Output (CI-friendly)
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
a11y-pilot scan ./src --format json
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Outputs structured JSON:
|
|
167
|
+
|
|
168
|
+
```json
|
|
169
|
+
{
|
|
170
|
+
"version": "1.0.0",
|
|
171
|
+
"timestamp": "2026-02-14T10:00:00.000Z",
|
|
172
|
+
"summary": {
|
|
173
|
+
"filesScanned": 23,
|
|
174
|
+
"filesWithIssues": 5,
|
|
175
|
+
"totalErrors": 12,
|
|
176
|
+
"totalWarnings": 3,
|
|
177
|
+
"totalIssues": 15
|
|
178
|
+
},
|
|
179
|
+
"files": {
|
|
180
|
+
"src/Hero.tsx": [
|
|
181
|
+
{
|
|
182
|
+
"ruleId": "img-alt",
|
|
183
|
+
"severity": "error",
|
|
184
|
+
"message": "<img> is missing the `alt` attribute",
|
|
185
|
+
"line": 12,
|
|
186
|
+
"fix": "Add alt=\"descriptive text\" or alt=\"\" if decorative",
|
|
187
|
+
"copilotCommand": "copilot \"In file src/Hero.tsx at line 12...\""
|
|
188
|
+
}
|
|
189
|
+
]
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## Supported File Types
|
|
197
|
+
|
|
198
|
+
| Extension | Parser |
|
|
199
|
+
|-----------|--------|
|
|
200
|
+
| `.jsx` | Babel (JSX) |
|
|
201
|
+
| `.tsx` | Babel (TSX + TypeScript) |
|
|
202
|
+
| `.html`, `.htm` | htmlparser2 |
|
|
203
|
+
| `.vue` | htmlparser2 (template) |
|
|
204
|
+
| `.svelte` | htmlparser2 (template) |
|
|
205
|
+
| `.astro` | htmlparser2 (template) |
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## Project Structure
|
|
210
|
+
|
|
211
|
+
```
|
|
212
|
+
a11y-pilot/
|
|
213
|
+
├── bin/
|
|
214
|
+
│ └── a11y-pilot.js # CLI entry point
|
|
215
|
+
├── src/
|
|
216
|
+
│ ├── cli.js # Commander setup & orchestration
|
|
217
|
+
│ ├── scanner.js # File discovery & walking
|
|
218
|
+
│ ├── parsers/
|
|
219
|
+
│ │ ├── jsx-parser.js # JSX/TSX AST parsing
|
|
220
|
+
│ │ └── html-parser.js # HTML parsing
|
|
221
|
+
│ ├── rules/
|
|
222
|
+
│ │ ├── index.js # Rule registry
|
|
223
|
+
│ │ ├── img-alt.js # Missing alt attributes
|
|
224
|
+
│ │ ├── button-content.js # Empty buttons
|
|
225
|
+
│ │ ├── no-div-button.js # Non-semantic click handlers
|
|
226
|
+
│ │ ├── form-label.js # Unlabeled form inputs
|
|
227
|
+
│ │ ├── heading-order.js # Heading hierarchy
|
|
228
|
+
│ │ ├── anchor-content.js # Empty links
|
|
229
|
+
│ │ ├── no-autofocus.js # autoFocus anti-pattern
|
|
230
|
+
│ │ └── semantic-nav.js # Missing <nav> landmarks
|
|
231
|
+
│ ├── reporter.js # Terminal output (colors/formatting)
|
|
232
|
+
│ └── copilot-bridge.js # Copilot CLI invocation engine
|
|
233
|
+
├── test/
|
|
234
|
+
│ └── fixtures/ # Sample files with a11y issues
|
|
235
|
+
├── docs/
|
|
236
|
+
│ └── PLAN.md # Project plan
|
|
237
|
+
├── package.json
|
|
238
|
+
├── LICENSE
|
|
239
|
+
└── README.md
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## Built With GitHub Copilot CLI
|
|
245
|
+
|
|
246
|
+
This entire project was built using GitHub Copilot CLI as a core development tool. Copilot CLI was used for:
|
|
247
|
+
|
|
248
|
+
- Scaffolding the project structure
|
|
249
|
+
- Writing parser logic for JSX/TSX AST traversal
|
|
250
|
+
- Implementing accessibility rules with WCAG references
|
|
251
|
+
- Debugging edge cases in HTML parsing
|
|
252
|
+
- Generating test fixtures
|
|
253
|
+
- Writing documentation
|
|
254
|
+
|
|
255
|
+
See the [submission article](#) for terminal screenshots and a full walkthrough.
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## License
|
|
260
|
+
|
|
261
|
+
MIT — see [LICENSE](LICENSE) for details.
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
## Contributing
|
|
266
|
+
|
|
267
|
+
Contributions welcome! Ideas for new rules, parser improvements, or better Copilot CLI integration are all appreciated.
|
|
268
|
+
|
|
269
|
+
```bash
|
|
270
|
+
# Run tests
|
|
271
|
+
npm test
|
|
272
|
+
|
|
273
|
+
# Scan the test fixtures during development
|
|
274
|
+
node bin/a11y-pilot.js scan test/fixtures/
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
<p align="center">
|
|
280
|
+
<strong>a11y-pilot</strong> — Making the web accessible, one terminal command at a time. 🛫
|
|
281
|
+
</p>
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "a11y-pilot",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "AI-powered accessibility scanner that uses GitHub Copilot CLI to auto-fix a11y issues in frontend codebases",
|
|
5
|
+
"main": "src/cli.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"a11y-pilot": "./bin/a11y-pilot.js"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"files": [
|
|
11
|
+
"bin/",
|
|
12
|
+
"src/",
|
|
13
|
+
"LICENSE",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"start": "node bin/a11y-pilot.js",
|
|
18
|
+
"test": "vitest run",
|
|
19
|
+
"test:watch": "vitest",
|
|
20
|
+
"prepublishOnly": "node bin/a11y-pilot.js --help"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"accessibility",
|
|
24
|
+
"a11y",
|
|
25
|
+
"copilot",
|
|
26
|
+
"cli",
|
|
27
|
+
"scanner",
|
|
28
|
+
"wcag",
|
|
29
|
+
"aria",
|
|
30
|
+
"semantic-html",
|
|
31
|
+
"github-copilot",
|
|
32
|
+
"lint",
|
|
33
|
+
"a11y-lint",
|
|
34
|
+
"copilot-cli",
|
|
35
|
+
"keyboard-navigation",
|
|
36
|
+
"screen-reader"
|
|
37
|
+
],
|
|
38
|
+
"author": "Safvan <github.com/Safvan-tsy>",
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "https://github.com/Safvan-tsy/a11y-pilot.git"
|
|
43
|
+
},
|
|
44
|
+
"homepage": "https://github.com/Safvan-tsy/a11y-pilot#readme",
|
|
45
|
+
"bugs": {
|
|
46
|
+
"url": "https://github.com/Safvan-tsy/a11y-pilot/issues"
|
|
47
|
+
},
|
|
48
|
+
"engines": {
|
|
49
|
+
"node": ">=18.0.0"
|
|
50
|
+
},
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"@babel/parser": "^7.29.0",
|
|
53
|
+
"@babel/traverse": "^7.29.0",
|
|
54
|
+
"boxen": "^8.0.1",
|
|
55
|
+
"chalk": "^5.6.2",
|
|
56
|
+
"commander": "^14.0.3",
|
|
57
|
+
"diff": "^8.0.3",
|
|
58
|
+
"figures": "^6.1.0",
|
|
59
|
+
"gradient-string": "^3.0.0",
|
|
60
|
+
"htmlparser2": "^10.1.0",
|
|
61
|
+
"log-symbols": "^7.0.1",
|
|
62
|
+
"ora": "^9.3.0"
|
|
63
|
+
},
|
|
64
|
+
"devDependencies": {
|
|
65
|
+
"vitest": "^4.0.18"
|
|
66
|
+
}
|
|
67
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { walkDir, readFileSafe, getFileType, relativePath } from './scanner.js';
|
|
5
|
+
import { parseJSX, collectJSXElements, collectHeadings } from './parsers/jsx-parser.js';
|
|
6
|
+
import { parseHTML, collectHTMLHeadings } from './parsers/html-parser.js';
|
|
7
|
+
import { allRules, getRules } from './rules/index.js';
|
|
8
|
+
import {
|
|
9
|
+
printBanner,
|
|
10
|
+
printScanStart,
|
|
11
|
+
printFileIssues,
|
|
12
|
+
printSummary,
|
|
13
|
+
printFixPrompt,
|
|
14
|
+
printRulesList,
|
|
15
|
+
printJSON,
|
|
16
|
+
printError,
|
|
17
|
+
printInfo,
|
|
18
|
+
} from './reporter.js';
|
|
19
|
+
import { autoFixAll, generateFixCommand } from './copilot-bridge.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Analyze a single file and return all issues found
|
|
23
|
+
* @param {string} filePath - Absolute file path
|
|
24
|
+
* @param {object[]} rules - Rules to check
|
|
25
|
+
* @returns {object[]} Issues found
|
|
26
|
+
*/
|
|
27
|
+
function analyzeFile(filePath, rules) {
|
|
28
|
+
const code = readFileSafe(filePath);
|
|
29
|
+
if (!code) return [];
|
|
30
|
+
|
|
31
|
+
const fileType = getFileType(filePath);
|
|
32
|
+
let elements = [];
|
|
33
|
+
let headings = [];
|
|
34
|
+
|
|
35
|
+
// Parse based on file type
|
|
36
|
+
if (fileType === 'jsx') {
|
|
37
|
+
const ast = parseJSX(code, filePath);
|
|
38
|
+
if (!ast) return [];
|
|
39
|
+
elements = collectJSXElements(ast, code);
|
|
40
|
+
headings = collectHeadings(elements);
|
|
41
|
+
} else if (fileType === 'html') {
|
|
42
|
+
elements = parseHTML(code);
|
|
43
|
+
headings = collectHTMLHeadings(elements);
|
|
44
|
+
} else {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const issues = [];
|
|
49
|
+
|
|
50
|
+
// Run element-level rules
|
|
51
|
+
for (const element of elements) {
|
|
52
|
+
for (const rule of rules) {
|
|
53
|
+
const issue = rule.check(element);
|
|
54
|
+
if (issue) {
|
|
55
|
+
issues.push(issue);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Run file-level rules
|
|
61
|
+
for (const rule of rules) {
|
|
62
|
+
// Heading order check
|
|
63
|
+
if (rule.id === 'heading-order' && rule.checkHeadings) {
|
|
64
|
+
const headingIssues = rule.checkHeadings(headings);
|
|
65
|
+
issues.push(...headingIssues);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Semantic nav check
|
|
69
|
+
if (rule.id === 'semantic-nav' && rule.checkNavPatterns) {
|
|
70
|
+
const navIssues = rule.checkNavPatterns(elements, code);
|
|
71
|
+
issues.push(...navIssues);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Sort by line number
|
|
76
|
+
issues.sort((a, b) => a.line - b.line);
|
|
77
|
+
|
|
78
|
+
return issues;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Main CLI entry point
|
|
83
|
+
* @param {string[]} argv
|
|
84
|
+
*/
|
|
85
|
+
export function cli(argv) {
|
|
86
|
+
const program = new Command();
|
|
87
|
+
|
|
88
|
+
program
|
|
89
|
+
.name('a11y-pilot')
|
|
90
|
+
.description(
|
|
91
|
+
'AI-powered accessibility scanner that uses GitHub Copilot CLI to auto-fix a11y issues'
|
|
92
|
+
)
|
|
93
|
+
.version('1.0.0');
|
|
94
|
+
|
|
95
|
+
// ─── scan command ──────────────────────────────────────────────────────────
|
|
96
|
+
program
|
|
97
|
+
.command('scan')
|
|
98
|
+
.description('Scan files for accessibility issues')
|
|
99
|
+
.argument('[path]', 'Path to scan (file or directory)', '.')
|
|
100
|
+
.option('-r, --rules <rules>', 'Comma-separated list of rule IDs to check')
|
|
101
|
+
.option('-f, --format <format>', 'Output format: text or json', 'text')
|
|
102
|
+
.option('--fix', 'Show Copilot CLI fix commands for each issue')
|
|
103
|
+
.option('--auto-fix', 'Automatically invoke Copilot CLI to fix issues')
|
|
104
|
+
.option('--dry-run', 'Show what auto-fix would do without executing')
|
|
105
|
+
.option('--one-by-one', 'Fix issues one at a time (instead of batching per file)')
|
|
106
|
+
.action(async (targetPath, options) => {
|
|
107
|
+
const absolutePath = path.resolve(process.cwd(), targetPath);
|
|
108
|
+
|
|
109
|
+
// Get rules
|
|
110
|
+
const ruleIds = options.rules ? options.rules.split(',').map(s => s.trim()) : null;
|
|
111
|
+
const rules = getRules(ruleIds);
|
|
112
|
+
|
|
113
|
+
if (rules.length === 0) {
|
|
114
|
+
printError('No matching rules found. Run `a11y-pilot rules` to see available rules.');
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// JSON format doesn't get the banner
|
|
119
|
+
if (options.format !== 'json') {
|
|
120
|
+
printBanner();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Discover files
|
|
124
|
+
const files = walkDir(absolutePath);
|
|
125
|
+
|
|
126
|
+
if (files.length === 0) {
|
|
127
|
+
if (options.format !== 'json') {
|
|
128
|
+
printError(`No scannable files found in ${targetPath}`);
|
|
129
|
+
printInfo('Supported extensions: .html, .htm, .jsx, .tsx, .vue, .astro, .svelte');
|
|
130
|
+
}
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (options.format !== 'json') {
|
|
135
|
+
printScanStart(files.length);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Analyze all files
|
|
139
|
+
const allIssues = new Map(); // filePath → issues[]
|
|
140
|
+
let totalErrors = 0;
|
|
141
|
+
let totalWarnings = 0;
|
|
142
|
+
let filesWithIssues = 0;
|
|
143
|
+
|
|
144
|
+
for (const file of files) {
|
|
145
|
+
const issues = analyzeFile(file, rules);
|
|
146
|
+
|
|
147
|
+
if (issues.length > 0) {
|
|
148
|
+
allIssues.set(file, issues);
|
|
149
|
+
filesWithIssues++;
|
|
150
|
+
totalErrors += issues.filter(i => i.severity === 'error').length;
|
|
151
|
+
totalWarnings += issues.filter(i => i.severity === 'warning').length;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ─── Output results ─────────────────────────────────────────────────
|
|
156
|
+
if (options.format === 'json') {
|
|
157
|
+
const report = {
|
|
158
|
+
version: '1.0.0',
|
|
159
|
+
timestamp: new Date().toISOString(),
|
|
160
|
+
summary: {
|
|
161
|
+
filesScanned: files.length,
|
|
162
|
+
filesWithIssues,
|
|
163
|
+
totalErrors,
|
|
164
|
+
totalWarnings,
|
|
165
|
+
totalIssues: totalErrors + totalWarnings,
|
|
166
|
+
},
|
|
167
|
+
files: {},
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
for (const [filePath, issues] of allIssues) {
|
|
171
|
+
report.files[relativePath(filePath)] = issues.map(i => ({
|
|
172
|
+
ruleId: i.ruleId,
|
|
173
|
+
severity: i.severity,
|
|
174
|
+
message: i.message,
|
|
175
|
+
line: i.line,
|
|
176
|
+
fix: i.fix,
|
|
177
|
+
copilotCommand: generateFixCommand(filePath, i),
|
|
178
|
+
}));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
printJSON(report);
|
|
182
|
+
process.exit(totalErrors > 0 ? 1 : 0);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Text output
|
|
187
|
+
if (options.autoFix || options.fix) {
|
|
188
|
+
// Show issues first
|
|
189
|
+
for (const [filePath, issues] of allIssues) {
|
|
190
|
+
printFileIssues(relativePath(filePath), issues);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
printSummary({
|
|
194
|
+
errors: totalErrors,
|
|
195
|
+
warnings: totalWarnings,
|
|
196
|
+
files: filesWithIssues,
|
|
197
|
+
totalFiles: files.length,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Auto-fix mode (Option B — the main event!)
|
|
202
|
+
if (options.autoFix) {
|
|
203
|
+
if (allIssues.size === 0) {
|
|
204
|
+
// Nothing to fix
|
|
205
|
+
process.exit(0);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const { totalFixed, totalFailed } = await autoFixAll(allIssues, {
|
|
210
|
+
dryRun: options.dryRun,
|
|
211
|
+
oneByOne: options.oneByOne,
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
process.exit(totalFailed > 0 ? 1 : 0);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// --fix mode: show copilot CLI commands
|
|
219
|
+
if (options.fix) {
|
|
220
|
+
if (allIssues.size > 0) {
|
|
221
|
+
console.log('');
|
|
222
|
+
printInfo('Copilot CLI fix commands:\n');
|
|
223
|
+
|
|
224
|
+
for (const [filePath, issues] of allIssues) {
|
|
225
|
+
for (const issue of issues) {
|
|
226
|
+
printFixPrompt(relativePath(filePath), issue);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
process.exit(totalErrors > 0 ? 1 : 0);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Default: just show issues
|
|
236
|
+
for (const [filePath, issues] of allIssues) {
|
|
237
|
+
printFileIssues(relativePath(filePath), issues);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
printSummary({
|
|
241
|
+
errors: totalErrors,
|
|
242
|
+
warnings: totalWarnings,
|
|
243
|
+
files: filesWithIssues,
|
|
244
|
+
totalFiles: files.length,
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
process.exit(totalErrors > 0 ? 1 : 0);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// ─── rules command ─────────────────────────────────────────────────────────
|
|
251
|
+
program
|
|
252
|
+
.command('rules')
|
|
253
|
+
.description('List all available accessibility rules')
|
|
254
|
+
.action(() => {
|
|
255
|
+
printRulesList(allRules);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// ─── fix command (convenience alias) ───────────────────────────────────────
|
|
259
|
+
program
|
|
260
|
+
.command('fix')
|
|
261
|
+
.description('Scan and auto-fix issues using GitHub Copilot CLI')
|
|
262
|
+
.argument('[path]', 'Path to scan (file or directory)', '.')
|
|
263
|
+
.option('-r, --rules <rules>', 'Comma-separated list of rule IDs to check')
|
|
264
|
+
.option('--dry-run', 'Show what would be fixed without executing')
|
|
265
|
+
.option('--one-by-one', 'Fix issues one at a time')
|
|
266
|
+
.action(async (targetPath, options) => {
|
|
267
|
+
// Delegate to scan with --auto-fix
|
|
268
|
+
const absolutePath = path.resolve(process.cwd(), targetPath);
|
|
269
|
+
const ruleIds = options.rules ? options.rules.split(',').map(s => s.trim()) : null;
|
|
270
|
+
const rules = getRules(ruleIds);
|
|
271
|
+
|
|
272
|
+
printBanner();
|
|
273
|
+
|
|
274
|
+
const files = walkDir(absolutePath);
|
|
275
|
+
if (files.length === 0) {
|
|
276
|
+
printError(`No scannable files found in ${targetPath}`);
|
|
277
|
+
process.exit(1);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
printScanStart(files.length);
|
|
281
|
+
|
|
282
|
+
const allIssuesMap = new Map();
|
|
283
|
+
let totalErrors = 0;
|
|
284
|
+
let totalWarnings = 0;
|
|
285
|
+
let filesWithIssues = 0;
|
|
286
|
+
|
|
287
|
+
for (const file of files) {
|
|
288
|
+
const issues = analyzeFile(file, rules);
|
|
289
|
+
if (issues.length > 0) {
|
|
290
|
+
allIssuesMap.set(file, issues);
|
|
291
|
+
filesWithIssues++;
|
|
292
|
+
totalErrors += issues.filter(i => i.severity === 'error').length;
|
|
293
|
+
totalWarnings += issues.filter(i => i.severity === 'warning').length;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Print issues
|
|
298
|
+
for (const [filePath, issues] of allIssuesMap) {
|
|
299
|
+
printFileIssues(relativePath(filePath), issues);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
printSummary({
|
|
303
|
+
errors: totalErrors,
|
|
304
|
+
warnings: totalWarnings,
|
|
305
|
+
files: filesWithIssues,
|
|
306
|
+
totalFiles: files.length,
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
if (allIssuesMap.size === 0) {
|
|
310
|
+
process.exit(0);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Auto-fix
|
|
315
|
+
const { totalFixed, totalFailed } = await autoFixAll(allIssuesMap, {
|
|
316
|
+
dryRun: options.dryRun,
|
|
317
|
+
oneByOne: options.oneByOne,
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
process.exit(totalFailed > 0 ? 1 : 0);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
program.parse(argv);
|
|
324
|
+
}
|