ally-a11y 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.
Files changed (84) hide show
  1. package/ACCESSIBILITY.md +205 -0
  2. package/LICENSE +21 -0
  3. package/README.md +940 -0
  4. package/dist/cli.d.ts +7 -0
  5. package/dist/cli.js +528 -0
  6. package/dist/commands/audit-palette.d.ts +18 -0
  7. package/dist/commands/audit-palette.js +613 -0
  8. package/dist/commands/auto-pr.d.ts +19 -0
  9. package/dist/commands/auto-pr.js +434 -0
  10. package/dist/commands/badge.d.ts +11 -0
  11. package/dist/commands/badge.js +143 -0
  12. package/dist/commands/completion.d.ts +4 -0
  13. package/dist/commands/completion.js +185 -0
  14. package/dist/commands/crawl.d.ts +12 -0
  15. package/dist/commands/crawl.js +249 -0
  16. package/dist/commands/doctor.d.ts +5 -0
  17. package/dist/commands/doctor.js +233 -0
  18. package/dist/commands/explain.d.ts +12 -0
  19. package/dist/commands/explain.js +233 -0
  20. package/dist/commands/fix.d.ts +13 -0
  21. package/dist/commands/fix.js +668 -0
  22. package/dist/commands/health.d.ts +11 -0
  23. package/dist/commands/health.js +367 -0
  24. package/dist/commands/history.d.ts +10 -0
  25. package/dist/commands/history.js +191 -0
  26. package/dist/commands/init.d.ts +9 -0
  27. package/dist/commands/init.js +164 -0
  28. package/dist/commands/learn.d.ts +8 -0
  29. package/dist/commands/learn.js +592 -0
  30. package/dist/commands/pr-check.d.ts +12 -0
  31. package/dist/commands/pr-check.js +270 -0
  32. package/dist/commands/report.d.ts +11 -0
  33. package/dist/commands/report.js +375 -0
  34. package/dist/commands/scan-storybook.d.ts +18 -0
  35. package/dist/commands/scan-storybook.js +402 -0
  36. package/dist/commands/scan.d.ts +25 -0
  37. package/dist/commands/scan.js +673 -0
  38. package/dist/commands/stats.d.ts +5 -0
  39. package/dist/commands/stats.js +137 -0
  40. package/dist/commands/tree.d.ts +12 -0
  41. package/dist/commands/tree.js +635 -0
  42. package/dist/commands/triage.d.ts +13 -0
  43. package/dist/commands/triage.js +327 -0
  44. package/dist/commands/watch.d.ts +17 -0
  45. package/dist/commands/watch.js +302 -0
  46. package/dist/types/index.d.ts +60 -0
  47. package/dist/types/index.js +4 -0
  48. package/dist/utils/baseline.d.ts +62 -0
  49. package/dist/utils/baseline.js +169 -0
  50. package/dist/utils/browser.d.ts +78 -0
  51. package/dist/utils/browser.js +239 -0
  52. package/dist/utils/cache.d.ts +76 -0
  53. package/dist/utils/cache.js +178 -0
  54. package/dist/utils/config.d.ts +102 -0
  55. package/dist/utils/config.js +237 -0
  56. package/dist/utils/converters.d.ts +77 -0
  57. package/dist/utils/converters.js +200 -0
  58. package/dist/utils/copilot.d.ts +36 -0
  59. package/dist/utils/copilot.js +139 -0
  60. package/dist/utils/detect.d.ts +22 -0
  61. package/dist/utils/detect.js +197 -0
  62. package/dist/utils/enhanced-errors.d.ts +46 -0
  63. package/dist/utils/enhanced-errors.js +295 -0
  64. package/dist/utils/errors.d.ts +31 -0
  65. package/dist/utils/errors.js +149 -0
  66. package/dist/utils/fix-patterns.d.ts +56 -0
  67. package/dist/utils/fix-patterns.js +529 -0
  68. package/dist/utils/history-tracking.d.ts +94 -0
  69. package/dist/utils/history-tracking.js +230 -0
  70. package/dist/utils/history.d.ts +42 -0
  71. package/dist/utils/history.js +255 -0
  72. package/dist/utils/impact-scores.d.ts +44 -0
  73. package/dist/utils/impact-scores.js +257 -0
  74. package/dist/utils/retry.d.ts +24 -0
  75. package/dist/utils/retry.js +76 -0
  76. package/dist/utils/scanner.d.ts +74 -0
  77. package/dist/utils/scanner.js +606 -0
  78. package/dist/utils/scanner.test.d.ts +4 -0
  79. package/dist/utils/scanner.test.js +162 -0
  80. package/dist/utils/ui.d.ts +44 -0
  81. package/dist/utils/ui.js +276 -0
  82. package/mcp-server/dist/index.d.ts +8 -0
  83. package/mcp-server/dist/index.js +1923 -0
  84. package/package.json +88 -0
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Tests for the accessibility scanner
3
+ */
4
+ import { describe, it, before, after } from 'node:test';
5
+ import assert from 'node:assert';
6
+ import { resolve } from 'path';
7
+ import { AccessibilityScanner, calculateScore, generateSummary, createReport, findHtmlFiles, } from '../../dist/utils/scanner.js';
8
+ describe('AccessibilityScanner', () => {
9
+ let scanner;
10
+ before(async () => {
11
+ scanner = new AccessibilityScanner();
12
+ await scanner.init();
13
+ });
14
+ after(async () => {
15
+ await scanner.close();
16
+ });
17
+ it('should scan HTML file with violations', async () => {
18
+ const testFile = resolve(process.cwd(), 'test-fixtures/bad-a11y.html');
19
+ const result = await scanner.scanHtmlFile(testFile);
20
+ assert.ok(result.violations.length > 0, 'Should find violations');
21
+ assert.ok(result.timestamp, 'Should have timestamp');
22
+ assert.ok(result.passes > 0, 'Should have some passing rules');
23
+ });
24
+ it('should scan clean HTML file', async () => {
25
+ const testFile = resolve(process.cwd(), 'test-fixtures/good-a11y.html');
26
+ const result = await scanner.scanHtmlFile(testFile);
27
+ assert.strictEqual(result.violations.length, 0, 'Should find no violations');
28
+ });
29
+ it('should scan HTML string', async () => {
30
+ const html = `
31
+ <!DOCTYPE html>
32
+ <html lang="en">
33
+ <head><title>Test</title></head>
34
+ <body><main><h1>Test</h1></main></body>
35
+ </html>
36
+ `;
37
+ const result = await scanner.scanHtmlString(html, 'test-string');
38
+ assert.ok(result, 'Should return result');
39
+ assert.strictEqual(result.file, 'test-string');
40
+ });
41
+ });
42
+ describe('calculateScore', () => {
43
+ it('should return 100 for no violations', () => {
44
+ const results = [{
45
+ url: 'test',
46
+ timestamp: new Date().toISOString(),
47
+ violations: [],
48
+ passes: 50,
49
+ incomplete: 0,
50
+ }];
51
+ const score = calculateScore(results);
52
+ assert.strictEqual(score, 100);
53
+ });
54
+ it('should penalize critical violations heavily', () => {
55
+ const criticalViolation = {
56
+ id: 'test',
57
+ impact: 'critical',
58
+ description: 'Test',
59
+ help: 'Test help',
60
+ helpUrl: 'https://example.com',
61
+ tags: [],
62
+ nodes: [{ html: '<div>', target: ['div'], failureSummary: '' }],
63
+ };
64
+ const results = [{
65
+ url: 'test',
66
+ timestamp: new Date().toISOString(),
67
+ violations: [criticalViolation],
68
+ passes: 50,
69
+ incomplete: 0,
70
+ }];
71
+ const score = calculateScore(results);
72
+ assert.ok(score < 100, 'Score should be reduced');
73
+ assert.ok(score <= 75, 'Critical violation should reduce score significantly');
74
+ });
75
+ it('should penalize minor violations lightly', () => {
76
+ const minorViolation = {
77
+ id: 'test',
78
+ impact: 'minor',
79
+ description: 'Test',
80
+ help: 'Test help',
81
+ helpUrl: 'https://example.com',
82
+ tags: [],
83
+ nodes: [{ html: '<div>', target: ['div'], failureSummary: '' }],
84
+ };
85
+ const results = [{
86
+ url: 'test',
87
+ timestamp: new Date().toISOString(),
88
+ violations: [minorViolation],
89
+ passes: 50,
90
+ incomplete: 0,
91
+ }];
92
+ const score = calculateScore(results);
93
+ assert.ok(score >= 95, 'Minor violation should only reduce score slightly');
94
+ });
95
+ });
96
+ describe('generateSummary', () => {
97
+ it('should count violations by severity', () => {
98
+ const results = [{
99
+ url: 'test',
100
+ timestamp: new Date().toISOString(),
101
+ violations: [
102
+ { id: 'a', impact: 'critical', description: '', help: '', helpUrl: '', tags: [], nodes: [] },
103
+ { id: 'b', impact: 'serious', description: '', help: '', helpUrl: '', tags: [], nodes: [] },
104
+ { id: 'c', impact: 'serious', description: '', help: '', helpUrl: '', tags: [], nodes: [] },
105
+ ],
106
+ passes: 0,
107
+ incomplete: 0,
108
+ }];
109
+ const summary = generateSummary(results);
110
+ assert.strictEqual(summary.bySeverity.critical, 1);
111
+ assert.strictEqual(summary.bySeverity.serious, 2);
112
+ assert.strictEqual(summary.totalViolations, 3);
113
+ });
114
+ it('should identify top issues', () => {
115
+ const results = [{
116
+ url: 'test',
117
+ timestamp: new Date().toISOString(),
118
+ violations: [
119
+ { id: 'image-alt', impact: 'critical', description: '', help: 'Images need alt', helpUrl: '', tags: [], nodes: [] },
120
+ { id: 'image-alt', impact: 'critical', description: '', help: 'Images need alt', helpUrl: '', tags: [], nodes: [] },
121
+ { id: 'button-name', impact: 'critical', description: '', help: 'Buttons need name', helpUrl: '', tags: [], nodes: [] },
122
+ ],
123
+ passes: 0,
124
+ incomplete: 0,
125
+ }];
126
+ const summary = generateSummary(results);
127
+ assert.ok(summary.topIssues.length > 0);
128
+ assert.strictEqual(summary.topIssues[0].id, 'image-alt');
129
+ assert.strictEqual(summary.topIssues[0].count, 2);
130
+ });
131
+ });
132
+ describe('findHtmlFiles', () => {
133
+ it('should find HTML files in test-fixtures', async () => {
134
+ const files = await findHtmlFiles(resolve(process.cwd(), 'test-fixtures'));
135
+ assert.ok(files.length >= 2, 'Should find at least 2 HTML files');
136
+ assert.ok(files.some(f => f.includes('bad-a11y.html')));
137
+ assert.ok(files.some(f => f.includes('good-a11y.html')));
138
+ });
139
+ it('should ignore node_modules', async () => {
140
+ const files = await findHtmlFiles(process.cwd());
141
+ const inNodeModules = files.filter(f => f.includes('node_modules'));
142
+ assert.strictEqual(inNodeModules.length, 0, 'Should not include node_modules');
143
+ });
144
+ });
145
+ describe('createReport', () => {
146
+ it('should create valid report structure', () => {
147
+ const results = [{
148
+ url: 'test.html',
149
+ file: 'test.html',
150
+ timestamp: new Date().toISOString(),
151
+ violations: [],
152
+ passes: 10,
153
+ incomplete: 0,
154
+ }];
155
+ const report = createReport(results);
156
+ assert.ok(report.version);
157
+ assert.ok(report.scanDate);
158
+ assert.strictEqual(report.totalFiles, 1);
159
+ assert.ok(report.summary);
160
+ assert.strictEqual(report.summary.score, 100);
161
+ });
162
+ });
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Terminal UI utilities for polished output
3
+ */
4
+ import { type Ora } from 'ora';
5
+ import type { ReportSummary, Violation } from '../types/index.js';
6
+ import type { ImpactScore } from './impact-scores.js';
7
+ /**
8
+ * Create a terminal hyperlink using OSC 8 escape sequences
9
+ * Format: \x1b]8;;URL\x07TEXT\x1b]8;;\x07
10
+ *
11
+ * For VS Code and other editors, use file:// URLs with line and column:
12
+ * file://path/to/file:line:column
13
+ *
14
+ * @param text - The visible text to display
15
+ * @param url - The URL to link to
16
+ * @returns The text wrapped in hyperlink escape sequences, or plain text if unsupported
17
+ */
18
+ export declare function hyperlink(text: string, url: string): string;
19
+ /**
20
+ * Create a clickable file path link that opens in the editor
21
+ *
22
+ * @param filePath - Absolute or relative path to the file
23
+ * @param line - Optional line number (1-indexed)
24
+ * @param column - Optional column number (1-indexed)
25
+ * @returns Hyperlinked file path or plain text if unsupported
26
+ */
27
+ export declare function fileLink(filePath: string, line?: number, column?: number): string;
28
+ export declare function printBanner(): void;
29
+ export declare function createSpinner(text: string): Ora;
30
+ export declare function printViolation(violation: Violation, file?: string, absolutePath?: string, impactScore?: ImpactScore): void;
31
+ export declare function printSummary(summary: ReportSummary): void;
32
+ export declare function printSuccess(message: string): void;
33
+ export declare function printError(message: string): void;
34
+ export declare function printWarning(message: string): void;
35
+ export declare function printInfo(message: string): void;
36
+ export declare function printDiff(oldCode: string, newCode: string): void;
37
+ export declare function printFixPrompt(file: string, line: number, issue: string): void;
38
+ export declare function printScoreChange(before: number, after: number): void;
39
+ /**
40
+ * Animate score counting up - creates memorable visual feedback
41
+ */
42
+ export declare function animateScoreUp(from: number, to: number): Promise<void>;
43
+ export declare function printScoreBadge(score: number): void;
44
+ export declare function printFileHeader(file: string, violationCount: number, absolutePath?: string): void;
@@ -0,0 +1,276 @@
1
+ /**
2
+ * Terminal UI utilities for polished output
3
+ */
4
+ import chalk from 'chalk';
5
+ import boxen from 'boxen';
6
+ import ora from 'ora';
7
+ // Honor NO_COLOR environment variable (https://no-color.org/)
8
+ const noColor = process.env.NO_COLOR !== undefined || process.env.ALLY_NO_COLOR !== undefined;
9
+ if (noColor) {
10
+ chalk.level = 0;
11
+ }
12
+ /**
13
+ * Check if terminal hyperlinks should be disabled
14
+ * Disabled in NO_COLOR mode, CI environments, or non-TTY
15
+ */
16
+ function supportsHyperlinks() {
17
+ if (noColor)
18
+ return false;
19
+ if (process.env.CI)
20
+ return false;
21
+ if (!process.stdout.isTTY)
22
+ return false;
23
+ // Some terminals advertise hyperlink support
24
+ if (process.env.TERM_PROGRAM === 'vscode')
25
+ return true;
26
+ if (process.env.TERM_PROGRAM === 'iTerm.app')
27
+ return true;
28
+ if (process.env.WT_SESSION)
29
+ return true; // Windows Terminal
30
+ if (process.env.KONSOLE_VERSION)
31
+ return true;
32
+ // Default to true for modern terminals
33
+ return true;
34
+ }
35
+ /**
36
+ * Create a terminal hyperlink using OSC 8 escape sequences
37
+ * Format: \x1b]8;;URL\x07TEXT\x1b]8;;\x07
38
+ *
39
+ * For VS Code and other editors, use file:// URLs with line and column:
40
+ * file://path/to/file:line:column
41
+ *
42
+ * @param text - The visible text to display
43
+ * @param url - The URL to link to
44
+ * @returns The text wrapped in hyperlink escape sequences, or plain text if unsupported
45
+ */
46
+ export function hyperlink(text, url) {
47
+ if (!supportsHyperlinks()) {
48
+ return text;
49
+ }
50
+ return `\x1b]8;;${url}\x07${text}\x1b]8;;\x07`;
51
+ }
52
+ /**
53
+ * Create a clickable file path link that opens in the editor
54
+ *
55
+ * @param filePath - Absolute or relative path to the file
56
+ * @param line - Optional line number (1-indexed)
57
+ * @param column - Optional column number (1-indexed)
58
+ * @returns Hyperlinked file path or plain text if unsupported
59
+ */
60
+ export function fileLink(filePath, line, column) {
61
+ // Build the file URL with optional line and column
62
+ let url = `file://${filePath}`;
63
+ if (line !== undefined) {
64
+ url += `:${line}`;
65
+ if (column !== undefined) {
66
+ url += `:${column}`;
67
+ }
68
+ }
69
+ // Build the display text
70
+ let displayText = filePath;
71
+ if (line !== undefined) {
72
+ displayText += `:${line}`;
73
+ if (column !== undefined) {
74
+ displayText += `:${column}`;
75
+ }
76
+ }
77
+ return hyperlink(displayText, url);
78
+ }
79
+ // Severity colors
80
+ const severityColors = {
81
+ critical: chalk.red.bold,
82
+ serious: chalk.red,
83
+ moderate: chalk.yellow,
84
+ minor: chalk.blue,
85
+ };
86
+ const severityIcons = {
87
+ critical: '!!!',
88
+ serious: '!!',
89
+ moderate: '!',
90
+ minor: 'i',
91
+ };
92
+ export function printBanner() {
93
+ const banner = chalk.cyan.bold(`
94
+ __ _ | | _ _
95
+ / _\` | | || | | |
96
+ | (_| | | || |_| |
97
+ \\__,_| |_| \\__, |
98
+ |___/ v1.0.0
99
+ `);
100
+ console.log(banner);
101
+ console.log(chalk.dim(' Your codebase\'s accessibility ally\n'));
102
+ }
103
+ export function createSpinner(text) {
104
+ return ora({
105
+ text,
106
+ color: 'cyan',
107
+ spinner: 'dots',
108
+ });
109
+ }
110
+ export function printViolation(violation, file, absolutePath, impactScore) {
111
+ const color = severityColors[violation.impact];
112
+ const icon = severityIcons[violation.impact];
113
+ // Print severity and impact score on the same line
114
+ if (impactScore) {
115
+ const scoreColor = impactScore.score >= 95 ? chalk.red.bold :
116
+ impactScore.score >= 75 ? chalk.red :
117
+ impactScore.score >= 40 ? chalk.yellow :
118
+ chalk.blue;
119
+ console.log(color(` [${icon}] ${violation.impact.toUpperCase()}`) +
120
+ scoreColor(` Impact: ${impactScore.score}/100`) +
121
+ chalk.dim(` (WCAG ${impactScore.wcagLevel})`));
122
+ }
123
+ else {
124
+ console.log(color(` [${icon}] ${violation.impact.toUpperCase()}`));
125
+ }
126
+ console.log(chalk.white(` ${violation.help}`));
127
+ // Print impact reasoning if available
128
+ if (impactScore) {
129
+ console.log(chalk.dim(` 💡 ${impactScore.reasoning}`));
130
+ console.log(chalk.dim(` 👥 Affects: `) +
131
+ chalk.cyan(impactScore.affectedUsers.join(', ')));
132
+ console.log(chalk.dim(` 📊 Estimated: `) +
133
+ chalk.cyan(impactScore.estimatedUsers));
134
+ }
135
+ if (file) {
136
+ // Use absolute path for the hyperlink if available, display relative path
137
+ const linkPath = absolutePath || file;
138
+ const linkedFile = fileLink(linkPath);
139
+ // Apply dim styling to the linked text
140
+ console.log(chalk.dim(` File: `) + chalk.dim(linkedFile));
141
+ }
142
+ violation.nodes.forEach((node, i) => {
143
+ if (i < 3) { // Limit to 3 nodes for readability
144
+ const target = node.target.join(' > ');
145
+ console.log(chalk.dim(` → ${target}`));
146
+ }
147
+ });
148
+ if (violation.nodes.length > 3) {
149
+ console.log(chalk.dim(` ... and ${violation.nodes.length - 3} more occurrences`));
150
+ }
151
+ // Make the help URL clickable too
152
+ const helpLink = hyperlink(violation.helpUrl, violation.helpUrl);
153
+ console.log(chalk.dim(` Learn more: `) + chalk.dim(helpLink));
154
+ console.log();
155
+ }
156
+ export function printSummary(summary) {
157
+ const { totalViolations, bySeverity, score } = summary;
158
+ // Score color
159
+ let scoreColor = chalk.green;
160
+ if (score < 50)
161
+ scoreColor = chalk.red;
162
+ else if (score < 75)
163
+ scoreColor = chalk.yellow;
164
+ const summaryText = `
165
+ ${chalk.bold('Accessibility Score:')} ${scoreColor.bold(`${score}/100`)}
166
+
167
+ ${chalk.red.bold(`!!! CRITICAL: ${bySeverity.critical || 0}`)}
168
+ ${chalk.red(`!! SERIOUS: ${bySeverity.serious || 0}`)}
169
+ ${chalk.yellow(`! MODERATE: ${bySeverity.moderate || 0}`)}
170
+ ${chalk.blue(`i MINOR: ${bySeverity.minor || 0}`)}
171
+
172
+ ${chalk.dim(`Total issues: ${totalViolations}`)}
173
+ `;
174
+ console.log(boxen(summaryText.trim(), {
175
+ padding: 1,
176
+ margin: 1,
177
+ borderStyle: 'round',
178
+ borderColor: score >= 75 ? 'green' : score >= 50 ? 'yellow' : 'red',
179
+ title: 'Scan Results',
180
+ titleAlignment: 'center',
181
+ }));
182
+ }
183
+ export function printSuccess(message) {
184
+ console.log(chalk.green('✓ ') + message);
185
+ }
186
+ export function printError(message) {
187
+ console.log(chalk.red('✗ ') + message);
188
+ }
189
+ export function printWarning(message) {
190
+ console.log(chalk.yellow('⚠ ') + message);
191
+ }
192
+ export function printInfo(message) {
193
+ console.log(chalk.blue('ℹ ') + message);
194
+ }
195
+ export function printDiff(oldCode, newCode) {
196
+ console.log(chalk.red(` - ${oldCode}`));
197
+ console.log(chalk.green(` + ${newCode}`));
198
+ }
199
+ export function printFixPrompt(file, line, issue) {
200
+ console.log();
201
+ console.log(chalk.cyan.bold(`📝 Fixing: ${file}:${line}`));
202
+ console.log(chalk.dim(` Issue: ${issue}`));
203
+ console.log();
204
+ }
205
+ export function printScoreChange(before, after) {
206
+ const diff = after - before;
207
+ const diffStr = diff > 0 ? chalk.green(`+${diff}`) : chalk.red(`${diff}`);
208
+ console.log(boxen(`Score: ${before} → ${chalk.bold(after.toString())} (${diffStr})`, {
209
+ padding: { top: 0, bottom: 0, left: 1, right: 1 },
210
+ borderStyle: 'round',
211
+ borderColor: diff > 0 ? 'green' : 'red',
212
+ }));
213
+ }
214
+ /**
215
+ * Animate score counting up - creates memorable visual feedback
216
+ */
217
+ export async function animateScoreUp(from, to) {
218
+ if (noColor || from >= to) {
219
+ // No animation in no-color mode or if score didn't improve
220
+ printScoreBadge(to);
221
+ return;
222
+ }
223
+ const frames = Math.min(to - from, 20); // Max 20 frames
224
+ const step = (to - from) / frames;
225
+ const delay = 50; // 50ms per frame
226
+ for (let i = 0; i <= frames; i++) {
227
+ const current = Math.round(from + (step * i));
228
+ process.stdout.write(`\r${getScoreBar(current)}`);
229
+ await sleep(delay);
230
+ }
231
+ console.log(); // New line after animation
232
+ printScoreBadge(to);
233
+ }
234
+ function sleep(ms) {
235
+ return new Promise(resolve => setTimeout(resolve, ms));
236
+ }
237
+ function getScoreBar(score) {
238
+ const width = 30;
239
+ const filled = Math.round((score / 100) * width);
240
+ const empty = width - filled;
241
+ const color = score >= 90 ? chalk.green
242
+ : score >= 75 ? chalk.yellow
243
+ : score >= 50 ? chalk.hex('#FFA500')
244
+ : chalk.red;
245
+ const bar = color('█'.repeat(filled)) + chalk.gray('░'.repeat(empty));
246
+ const scoreText = color.bold(`${score}%`);
247
+ return ` ${bar} ${scoreText}`;
248
+ }
249
+ export function printScoreBadge(score) {
250
+ const emoji = score >= 90 ? '🌟'
251
+ : score >= 75 ? '✅'
252
+ : score >= 50 ? '⚠️'
253
+ : '❌';
254
+ const color = score >= 90 ? chalk.green
255
+ : score >= 75 ? chalk.yellow
256
+ : score >= 50 ? chalk.hex('#FFA500')
257
+ : chalk.red;
258
+ console.log(boxen(`${emoji} Accessibility Score: ${color.bold(`${score}/100`)}`, {
259
+ padding: { top: 0, bottom: 0, left: 1, right: 1 },
260
+ borderStyle: 'round',
261
+ borderColor: score >= 75 ? 'green' : score >= 50 ? 'yellow' : 'red',
262
+ }));
263
+ }
264
+ export function printFileHeader(file, violationCount, absolutePath) {
265
+ const color = violationCount > 0 ? chalk.yellow : chalk.green;
266
+ // Use absolute path for the hyperlink if available
267
+ const linkPath = absolutePath || file;
268
+ const linkedFile = fileLink(linkPath);
269
+ console.log(color.bold(`\n📄 `) + color.bold(linkedFile));
270
+ if (violationCount > 0) {
271
+ console.log(chalk.dim(` ${violationCount} issue${violationCount === 1 ? '' : 's'} found`));
272
+ }
273
+ else {
274
+ console.log(chalk.green.dim(' No issues found'));
275
+ }
276
+ }
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Ally MCP Server
4
+ *
5
+ * Exposes project-specific accessibility patterns to GitHub Copilot CLI.
6
+ * This enables Copilot to generate fixes that are consistent with your codebase.
7
+ */
8
+ export {};