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 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>
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { cli } from '../src/cli.js';
4
+
5
+ cli(process.argv);
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
+ }