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,270 @@
1
+ /**
2
+ * ally pr-check command - Post accessibility results to GitHub PR
3
+ */
4
+ import { readFile, writeFile } from 'fs/promises';
5
+ import { existsSync } from 'fs';
6
+ import { resolve } from 'path';
7
+ import { execSync } from 'child_process';
8
+ import chalk from 'chalk';
9
+ import boxen from 'boxen';
10
+ import { printBanner, createSpinner, printError, printSuccess, printInfo, printWarning, } from '../utils/ui.js';
11
+ import { suggestInit } from '../utils/errors.js';
12
+ /**
13
+ * Get current GitHub repo from git remote
14
+ */
15
+ function getGitHubRepo() {
16
+ try {
17
+ const remote = execSync('git remote get-url origin', { encoding: 'utf-8' }).trim();
18
+ // Parse GitHub URL (supports both HTTPS and SSH)
19
+ // https://github.com/owner/repo.git
20
+ // git@github.com:owner/repo.git
21
+ const httpsMatch = remote.match(/github\.com\/([^/]+)\/([^/.]+)/);
22
+ const sshMatch = remote.match(/github\.com:([^/]+)\/([^/.]+)/);
23
+ const match = httpsMatch || sshMatch;
24
+ if (match) {
25
+ return {
26
+ owner: match[1],
27
+ repo: match[2].replace(/\.git$/, ''),
28
+ };
29
+ }
30
+ }
31
+ catch {
32
+ // Not in a git repo or no remote
33
+ }
34
+ return null;
35
+ }
36
+ /**
37
+ * Get current PR number from environment or git
38
+ */
39
+ function getCurrentPR() {
40
+ // Check GitHub Actions environment
41
+ if (process.env.GITHUB_EVENT_NAME === 'pull_request') {
42
+ const eventPath = process.env.GITHUB_EVENT_PATH;
43
+ if (eventPath && existsSync(eventPath)) {
44
+ try {
45
+ const event = JSON.parse(require('fs').readFileSync(eventPath, 'utf-8'));
46
+ return event.pull_request?.number || null;
47
+ }
48
+ catch {
49
+ // Ignore
50
+ }
51
+ }
52
+ }
53
+ // Check GITHUB_REF for PR number
54
+ const ref = process.env.GITHUB_REF;
55
+ if (ref) {
56
+ const prMatch = ref.match(/refs\/pull\/(\d+)/);
57
+ if (prMatch) {
58
+ return parseInt(prMatch[1], 10);
59
+ }
60
+ }
61
+ return null;
62
+ }
63
+ /**
64
+ * Format severity for display
65
+ */
66
+ function getSeverityEmoji(severity) {
67
+ const emojis = {
68
+ critical: ':red_circle:',
69
+ serious: ':orange_circle:',
70
+ moderate: ':yellow_circle:',
71
+ minor: ':blue_circle:',
72
+ };
73
+ return emojis[severity] || ':white_circle:';
74
+ }
75
+ /**
76
+ * Generate markdown comment for PR
77
+ */
78
+ function generatePRComment(report) {
79
+ const { summary } = report;
80
+ const scoreEmoji = summary.score >= 90 ? ':star:' : summary.score >= 75 ? ':white_check_mark:' : summary.score >= 50 ? ':warning:' : ':x:';
81
+ let comment = `## ${scoreEmoji} Accessibility Report
82
+
83
+ **Score:** ${summary.score}/100
84
+
85
+ | Severity | Count |
86
+ |----------|-------|
87
+ | :red_circle: Critical | ${summary.bySeverity.critical || 0} |
88
+ | :orange_circle: Serious | ${summary.bySeverity.serious || 0} |
89
+ | :yellow_circle: Moderate | ${summary.bySeverity.moderate || 0} |
90
+ | :blue_circle: Minor | ${summary.bySeverity.minor || 0} |
91
+
92
+ `;
93
+ if (summary.topIssues.length > 0) {
94
+ comment += `### Top Issues\n\n`;
95
+ for (const issue of summary.topIssues.slice(0, 5)) {
96
+ const emoji = getSeverityEmoji(issue.severity);
97
+ comment += `- ${emoji} **${issue.id}**: ${issue.description} (${issue.count} occurrences)\n`;
98
+ }
99
+ comment += '\n';
100
+ }
101
+ if (summary.totalViolations > 0) {
102
+ comment += `<details>
103
+ <summary>How to fix</summary>
104
+
105
+ \`\`\`bash
106
+ # Get detailed explanations
107
+ ally explain
108
+
109
+ # Apply AI-powered fixes
110
+ ally fix
111
+ \`\`\`
112
+
113
+ </details>
114
+
115
+ `;
116
+ }
117
+ comment += `---
118
+ *Generated by [ally](https://github.com/forbiddenlink/ally) :robot:*`;
119
+ return comment;
120
+ }
121
+ /**
122
+ * Post comment to GitHub PR using gh CLI
123
+ */
124
+ async function postPRComment(repo, prNumber, comment) {
125
+ try {
126
+ // Write comment to temp file to avoid shell escaping issues
127
+ const tempFile = resolve('.ally/pr-comment.md');
128
+ await writeFile(tempFile, comment);
129
+ // Use gh CLI to post comment
130
+ execSync(`gh pr comment ${prNumber} --repo ${repo.owner}/${repo.repo} --body-file "${tempFile}"`, { encoding: 'utf-8', stdio: 'pipe' });
131
+ return true;
132
+ }
133
+ catch (error) {
134
+ if (error instanceof Error) {
135
+ // Check for common errors
136
+ if (error.message.includes('gh: command not found')) {
137
+ printError('GitHub CLI (gh) not found. Install it from https://cli.github.com/');
138
+ }
139
+ else if (error.message.includes('not logged in')) {
140
+ printError('Not logged in to GitHub CLI. Run: gh auth login');
141
+ }
142
+ else {
143
+ printError(`Failed to post comment: ${error.message}`);
144
+ }
145
+ }
146
+ return false;
147
+ }
148
+ }
149
+ /**
150
+ * Create GitHub check annotations for violations
151
+ */
152
+ function generateAnnotations(report) {
153
+ const annotations = [];
154
+ for (const result of report.results) {
155
+ const file = result.file || 'unknown';
156
+ for (const violation of result.violations) {
157
+ for (const node of violation.nodes) {
158
+ // Try to extract line number from HTML context
159
+ const annotation = {
160
+ path: file,
161
+ start_line: 1, // Would need source map for actual line
162
+ end_line: 1,
163
+ annotation_level: violation.impact === 'critical' || violation.impact === 'serious' ? 'failure' : 'warning',
164
+ message: `${violation.id}: ${violation.help}\n\nElement: ${node.html.substring(0, 100)}...`,
165
+ title: violation.description,
166
+ };
167
+ annotations.push(JSON.stringify(annotation));
168
+ }
169
+ }
170
+ }
171
+ return annotations.join('\n');
172
+ }
173
+ export async function prCheckCommand(options = {}) {
174
+ printBanner();
175
+ const { input = '.ally/scan.json', pr, comment = true, failOn } = options;
176
+ // Load scan results
177
+ const spinner = createSpinner('Loading scan results...');
178
+ spinner.start();
179
+ const reportPath = resolve(input);
180
+ if (!existsSync(reportPath)) {
181
+ spinner.stop();
182
+ suggestInit(reportPath);
183
+ return;
184
+ }
185
+ let report;
186
+ try {
187
+ const content = await readFile(reportPath, 'utf-8');
188
+ report = JSON.parse(content);
189
+ spinner.succeed('Loaded scan results');
190
+ }
191
+ catch (error) {
192
+ spinner.fail('Failed to load scan results');
193
+ printError(error instanceof Error ? error.message : String(error));
194
+ return;
195
+ }
196
+ // Get GitHub repo info
197
+ const repo = getGitHubRepo();
198
+ if (!repo) {
199
+ printWarning('Not a GitHub repository. Skipping PR integration.');
200
+ console.log();
201
+ printInfo('This command works best in a GitHub repository with a PR open.');
202
+ return;
203
+ }
204
+ // Get PR number
205
+ const prNumber = pr || getCurrentPR();
206
+ if (!prNumber) {
207
+ printWarning('No PR number detected. Use --pr <number> to specify.');
208
+ console.log();
209
+ printInfo('Run this command from a GitHub Actions PR workflow or specify --pr manually.');
210
+ // Still show the report summary
211
+ console.log();
212
+ console.log(boxen(`${chalk.bold('Accessibility Report')}\n\n` +
213
+ `Score: ${report.summary.score}/100\n` +
214
+ `Violations: ${report.summary.totalViolations}`, {
215
+ padding: 1,
216
+ borderStyle: 'round',
217
+ borderColor: report.summary.score >= 75 ? 'green' : 'yellow',
218
+ }));
219
+ return;
220
+ }
221
+ console.log();
222
+ printInfo(`Posting results to ${repo.owner}/${repo.repo}#${prNumber}`);
223
+ // Post PR comment
224
+ if (comment) {
225
+ const commentSpinner = createSpinner('Posting PR comment...');
226
+ commentSpinner.start();
227
+ const prComment = generatePRComment(report);
228
+ const success = await postPRComment(repo, prNumber, prComment);
229
+ if (success) {
230
+ commentSpinner.succeed('Posted accessibility report to PR');
231
+ }
232
+ else {
233
+ commentSpinner.fail('Failed to post PR comment');
234
+ }
235
+ }
236
+ // Print summary
237
+ console.log();
238
+ const scoreColor = report.summary.score >= 90 ? chalk.green
239
+ : report.summary.score >= 75 ? chalk.yellow
240
+ : chalk.red;
241
+ console.log(boxen(`${chalk.bold('Accessibility Check Complete')}\n\n` +
242
+ `Score: ${scoreColor(`${report.summary.score}/100`)}\n\n` +
243
+ `Critical: ${report.summary.bySeverity.critical || 0}\n` +
244
+ `Serious: ${report.summary.bySeverity.serious || 0}\n` +
245
+ `Moderate: ${report.summary.bySeverity.moderate || 0}\n` +
246
+ `Minor: ${report.summary.bySeverity.minor || 0}`, {
247
+ padding: 1,
248
+ borderStyle: 'round',
249
+ borderColor: report.summary.score >= 75 ? 'green' : 'yellow',
250
+ }));
251
+ // Check fail conditions
252
+ if (failOn) {
253
+ const severities = failOn.split(',').map((s) => s.trim().toLowerCase());
254
+ let shouldFail = false;
255
+ for (const severity of severities) {
256
+ if (report.summary.bySeverity[severity] && report.summary.bySeverity[severity] > 0) {
257
+ shouldFail = true;
258
+ break;
259
+ }
260
+ }
261
+ if (shouldFail) {
262
+ console.log();
263
+ printError(`Failing due to ${failOn} violations`);
264
+ process.exit(1);
265
+ }
266
+ }
267
+ console.log();
268
+ printSuccess('PR check complete!');
269
+ }
270
+ export default prCheckCommand;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * ally report command - Generates ACCESSIBILITY.md report
3
+ */
4
+ type ReportFormat = 'markdown' | 'html' | 'json' | 'sarif' | 'junit' | 'csv' | 'all';
5
+ interface ReportOptions {
6
+ input?: string;
7
+ output?: string;
8
+ format?: ReportFormat;
9
+ }
10
+ export declare function reportCommand(options?: ReportOptions): Promise<void>;
11
+ export default reportCommand;
@@ -0,0 +1,375 @@
1
+ /**
2
+ * ally report command - Generates ACCESSIBILITY.md report
3
+ */
4
+ import { readFile, writeFile } from 'fs/promises';
5
+ import { existsSync } from 'fs';
6
+ import { resolve, dirname, basename, relative } from 'path';
7
+ import chalk from 'chalk';
8
+ import { printBanner, createSpinner, printError, printInfo, printSuccess, } from '../utils/ui.js';
9
+ import { suggestInit, suggestRescan } from '../utils/errors.js';
10
+ import { convertToSarif, convertToJunit, convertToCsv } from '../utils/converters.js';
11
+ export async function reportCommand(options = {}) {
12
+ printBanner();
13
+ const { input = '.ally/scan.json', output = 'ACCESSIBILITY.md', format = 'markdown', } = options;
14
+ // Load scan results
15
+ const spinner = createSpinner('Loading scan results...');
16
+ spinner.start();
17
+ const reportPath = resolve(input);
18
+ if (!existsSync(reportPath)) {
19
+ spinner.stop();
20
+ suggestInit(reportPath);
21
+ return;
22
+ }
23
+ let report;
24
+ try {
25
+ const content = await readFile(reportPath, 'utf-8');
26
+ report = JSON.parse(content);
27
+ spinner.succeed('Loaded scan results');
28
+ }
29
+ catch (error) {
30
+ spinner.fail('Failed to load scan results');
31
+ if (error instanceof SyntaxError) {
32
+ suggestRescan(reportPath);
33
+ }
34
+ else {
35
+ printError(error instanceof Error ? error.message : String(error));
36
+ }
37
+ return;
38
+ }
39
+ // Generate report
40
+ const generateSpinner = createSpinner('Generating report...');
41
+ generateSpinner.start();
42
+ // Handle batch export (--format all)
43
+ if (format === 'all') {
44
+ const outputDir = dirname(resolve(output));
45
+ const generatedFiles = [];
46
+ try {
47
+ // Generate all formats
48
+ const formats = [
49
+ { name: 'ACCESSIBILITY.md', ext: 'md', content: generateMarkdownReport(report) },
50
+ { name: 'accessibility.html', ext: 'html', content: generateHtmlReport(report) },
51
+ { name: 'accessibility.json', ext: 'json', content: JSON.stringify(report, null, 2) },
52
+ { name: 'accessibility.sarif', ext: 'sarif', content: JSON.stringify(convertToSarif(report), null, 2) },
53
+ { name: 'accessibility.junit.xml', ext: 'xml', content: convertToJunit(report) },
54
+ { name: 'accessibility.csv', ext: 'csv', content: convertToCsv(report) },
55
+ ];
56
+ for (const { name, content } of formats) {
57
+ const filePath = resolve(outputDir, name);
58
+ await writeFile(filePath, content);
59
+ generatedFiles.push(name);
60
+ }
61
+ generateSpinner.succeed('Generated all report formats');
62
+ console.log();
63
+ console.log(chalk.green(`\u2713 Generated ${generatedFiles.length} report files:`));
64
+ for (const file of generatedFiles) {
65
+ console.log(chalk.dim(` \u2022 ${file}`));
66
+ }
67
+ }
68
+ catch (error) {
69
+ generateSpinner.fail('Failed to write reports');
70
+ printError(error instanceof Error ? error.message : String(error));
71
+ return;
72
+ }
73
+ }
74
+ else {
75
+ // Single format export
76
+ let reportContent;
77
+ switch (format) {
78
+ case 'json':
79
+ reportContent = JSON.stringify(report, null, 2);
80
+ break;
81
+ case 'html':
82
+ reportContent = generateHtmlReport(report);
83
+ break;
84
+ case 'sarif':
85
+ reportContent = JSON.stringify(convertToSarif(report), null, 2);
86
+ break;
87
+ case 'junit':
88
+ reportContent = convertToJunit(report);
89
+ break;
90
+ case 'csv':
91
+ reportContent = convertToCsv(report);
92
+ break;
93
+ case 'markdown':
94
+ default:
95
+ reportContent = generateMarkdownReport(report);
96
+ }
97
+ // Adjust output filename based on format
98
+ let outputPath = resolve(output);
99
+ if (format === 'sarif' && !output.endsWith('.sarif')) {
100
+ outputPath = resolve(dirname(output), 'accessibility.sarif');
101
+ }
102
+ else if (format === 'junit' && !output.endsWith('.xml')) {
103
+ outputPath = resolve(dirname(output), 'accessibility.junit.xml');
104
+ }
105
+ else if (format === 'csv' && !output.endsWith('.csv')) {
106
+ outputPath = resolve(dirname(output), 'accessibility.csv');
107
+ }
108
+ else if (format === 'json' && !output.endsWith('.json')) {
109
+ outputPath = resolve(dirname(output), 'accessibility.json');
110
+ }
111
+ else if (format === 'html' && !output.endsWith('.html')) {
112
+ outputPath = resolve(dirname(output), 'accessibility.html');
113
+ }
114
+ try {
115
+ await writeFile(outputPath, reportContent);
116
+ generateSpinner.succeed(`Report generated: ${outputPath}`);
117
+ }
118
+ catch (error) {
119
+ generateSpinner.fail('Failed to write report');
120
+ printError(error instanceof Error ? error.message : String(error));
121
+ return;
122
+ }
123
+ console.log();
124
+ printSuccess('Accessibility report ready!');
125
+ printInfo(`Add ${basename(outputPath)} to your repository to document your a11y compliance`);
126
+ }
127
+ // Generate badge URL
128
+ const badgeColor = report.summary.score >= 90 ? 'brightgreen'
129
+ : report.summary.score >= 75 ? 'green'
130
+ : report.summary.score >= 50 ? 'yellow'
131
+ : 'red';
132
+ console.log();
133
+ console.log(chalk.bold('Badge for README:'));
134
+ console.log(chalk.dim(`![Accessibility Score](https://img.shields.io/badge/a11y_score-${report.summary.score}%25-${badgeColor})`));
135
+ }
136
+ function generateMarkdownReport(report) {
137
+ const { summary } = report;
138
+ const scanDate = new Date(report.scanDate).toLocaleDateString();
139
+ // Score emoji
140
+ const scoreEmoji = summary.score >= 90 ? '🌟'
141
+ : summary.score >= 75 ? '✅'
142
+ : summary.score >= 50 ? '⚠️'
143
+ : '❌';
144
+ let md = `# Accessibility Report ${scoreEmoji}
145
+
146
+ > Generated by [ally](https://github.com/forbiddenlink/ally) on ${scanDate}
147
+
148
+ ## Score: ${summary.score}/100
149
+
150
+ ![Accessibility Score](https://img.shields.io/badge/a11y_score-${summary.score}%25-${getScoreColor(summary.score)})
151
+
152
+ ## Summary
153
+
154
+ | Severity | Count |
155
+ |----------|-------|
156
+ | 🔴 Critical | ${summary.bySeverity.critical || 0} |
157
+ | 🟠 Serious | ${summary.bySeverity.serious || 0} |
158
+ | 🟡 Moderate | ${summary.bySeverity.moderate || 0} |
159
+ | 🔵 Minor | ${summary.bySeverity.minor || 0} |
160
+ | **Total** | **${summary.totalViolations}** |
161
+
162
+ ## Top Issues
163
+
164
+ `;
165
+ if (summary.topIssues.length > 0) {
166
+ for (const issue of summary.topIssues) {
167
+ const severityIcon = getSeverityIcon(issue.severity);
168
+ md += `### ${severityIcon} ${issue.description}\n\n`;
169
+ md += `- **Occurrences:** ${issue.count}\n`;
170
+ md += `- **Rule ID:** \`${issue.id}\`\n\n`;
171
+ }
172
+ }
173
+ else {
174
+ md += `No issues found! 🎉\n\n`;
175
+ }
176
+ // Files scanned
177
+ md += `## Files Scanned\n\n`;
178
+ md += `Total: ${report.totalFiles} files\n\n`;
179
+ if (report.results.length > 0) {
180
+ md += `| File | Issues |\n`;
181
+ md += `|------|--------|\n`;
182
+ for (const result of report.results) {
183
+ // Convert absolute paths to relative for cleaner reports
184
+ let fileName;
185
+ if (result.file) {
186
+ fileName = relative(process.cwd(), result.file) || result.file;
187
+ }
188
+ else if (result.url) {
189
+ fileName = result.url;
190
+ }
191
+ else {
192
+ fileName = 'unknown';
193
+ }
194
+ const issueCount = result.violations.length;
195
+ const icon = issueCount === 0 ? '✅' : issueCount < 5 ? '⚠️' : '❌';
196
+ md += `| ${icon} ${fileName} | ${issueCount} |\n`;
197
+ }
198
+ }
199
+ // WCAG Compliance
200
+ md += `\n## WCAG 2.1 Compliance\n\n`;
201
+ const wcagViolations = new Set();
202
+ for (const result of report.results) {
203
+ for (const violation of result.violations) {
204
+ for (const tag of violation.tags) {
205
+ if (tag.startsWith('wcag')) {
206
+ wcagViolations.add(tag);
207
+ }
208
+ }
209
+ }
210
+ }
211
+ if (wcagViolations.size > 0) {
212
+ md += `The following WCAG criteria have violations:\n\n`;
213
+ for (const criterion of Array.from(wcagViolations).sort()) {
214
+ md += `- \`${criterion}\`\n`;
215
+ }
216
+ }
217
+ else {
218
+ md += `No WCAG violations detected! ✅\n`;
219
+ }
220
+ // Next steps
221
+ md += `\n## Next Steps\n\n`;
222
+ if (summary.totalViolations > 0) {
223
+ md += `1. Run \`ally explain\` to understand each issue\n`;
224
+ md += `2. Run \`ally fix\` to apply automated fixes\n`;
225
+ md += `3. Re-scan with \`ally scan\` to verify fixes\n`;
226
+ }
227
+ else {
228
+ md += `Your site is looking great! Consider:\n`;
229
+ md += `- Testing with real screen readers (NVDA, VoiceOver)\n`;
230
+ md += `- Running manual keyboard navigation tests\n`;
231
+ md += `- Getting user feedback from people with disabilities\n`;
232
+ }
233
+ // Footer
234
+ md += `\n---\n\n`;
235
+ md += `*This report was automatically generated. Automated testing can only catch ~50% of accessibility issues. Manual testing is recommended.*\n`;
236
+ return md;
237
+ }
238
+ function generateHtmlReport(report) {
239
+ const { summary } = report;
240
+ return `<!DOCTYPE html>
241
+ <html lang="en">
242
+ <head>
243
+ <meta charset="UTF-8">
244
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
245
+ <title>Accessibility Report</title>
246
+ <style>
247
+ :root {
248
+ --critical: #dc2626;
249
+ --serious: #ea580c;
250
+ --moderate: #ca8a04;
251
+ --minor: #2563eb;
252
+ --pass: #16a34a;
253
+ }
254
+ body {
255
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
256
+ max-width: 800px;
257
+ margin: 0 auto;
258
+ padding: 2rem;
259
+ line-height: 1.6;
260
+ }
261
+ .score {
262
+ font-size: 4rem;
263
+ font-weight: bold;
264
+ text-align: center;
265
+ padding: 2rem;
266
+ border-radius: 1rem;
267
+ background: ${getScoreGradient(summary.score)};
268
+ color: white;
269
+ }
270
+ .severity-grid {
271
+ display: grid;
272
+ grid-template-columns: repeat(4, 1fr);
273
+ gap: 1rem;
274
+ margin: 2rem 0;
275
+ }
276
+ .severity-card {
277
+ padding: 1rem;
278
+ border-radius: 0.5rem;
279
+ text-align: center;
280
+ }
281
+ .severity-card.critical { background: var(--critical); color: white; }
282
+ .severity-card.serious { background: var(--serious); color: white; }
283
+ .severity-card.moderate { background: var(--moderate); color: white; }
284
+ .severity-card.minor { background: var(--minor); color: white; }
285
+ table {
286
+ width: 100%;
287
+ border-collapse: collapse;
288
+ margin: 1rem 0;
289
+ }
290
+ th, td {
291
+ padding: 0.75rem;
292
+ text-align: left;
293
+ border-bottom: 1px solid #e5e7eb;
294
+ }
295
+ th { background: #f9fafb; }
296
+ </style>
297
+ </head>
298
+ <body>
299
+ <h1>Accessibility Report</h1>
300
+ <div class="score">${summary.score}/100</div>
301
+
302
+ <h2>Violations by Severity</h2>
303
+ <div class="severity-grid">
304
+ <div class="severity-card critical">
305
+ <div style="font-size: 2rem;">${summary.bySeverity.critical || 0}</div>
306
+ <div>Critical</div>
307
+ </div>
308
+ <div class="severity-card serious">
309
+ <div style="font-size: 2rem;">${summary.bySeverity.serious || 0}</div>
310
+ <div>Serious</div>
311
+ </div>
312
+ <div class="severity-card moderate">
313
+ <div style="font-size: 2rem;">${summary.bySeverity.moderate || 0}</div>
314
+ <div>Moderate</div>
315
+ </div>
316
+ <div class="severity-card minor">
317
+ <div style="font-size: 2rem;">${summary.bySeverity.minor || 0}</div>
318
+ <div>Minor</div>
319
+ </div>
320
+ </div>
321
+
322
+ <h2>Top Issues</h2>
323
+ <table>
324
+ <thead>
325
+ <tr>
326
+ <th>Issue</th>
327
+ <th>Severity</th>
328
+ <th>Count</th>
329
+ </tr>
330
+ </thead>
331
+ <tbody>
332
+ ${summary.topIssues.map(issue => `
333
+ <tr>
334
+ <td>${issue.description}</td>
335
+ <td>${issue.severity}</td>
336
+ <td>${issue.count}</td>
337
+ </tr>
338
+ `).join('')}
339
+ </tbody>
340
+ </table>
341
+
342
+ <footer>
343
+ <p><em>Generated by ally on ${new Date(report.scanDate).toLocaleDateString()}</em></p>
344
+ </footer>
345
+ </body>
346
+ </html>`;
347
+ }
348
+ function getScoreColor(score) {
349
+ if (score >= 90)
350
+ return 'brightgreen';
351
+ if (score >= 75)
352
+ return 'green';
353
+ if (score >= 50)
354
+ return 'yellow';
355
+ return 'red';
356
+ }
357
+ function getScoreGradient(score) {
358
+ if (score >= 90)
359
+ return 'linear-gradient(135deg, #16a34a, #22c55e)';
360
+ if (score >= 75)
361
+ return 'linear-gradient(135deg, #65a30d, #84cc16)';
362
+ if (score >= 50)
363
+ return 'linear-gradient(135deg, #ca8a04, #eab308)';
364
+ return 'linear-gradient(135deg, #dc2626, #ef4444)';
365
+ }
366
+ function getSeverityIcon(severity) {
367
+ const icons = {
368
+ critical: '🔴',
369
+ serious: '🟠',
370
+ moderate: '🟡',
371
+ minor: '🔵',
372
+ };
373
+ return icons[severity] || '⚪';
374
+ }
375
+ export default reportCommand;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * ally scan-storybook command - Scans Storybook stories for accessibility issues
3
+ */
4
+ import { type WcagStandard } from '../utils/scanner.js';
5
+ import type { AllyReport } from '../types/index.js';
6
+ interface ScanStorybookOptions {
7
+ url?: string;
8
+ timeout?: number;
9
+ filter?: string;
10
+ format?: 'default' | 'json' | 'csv';
11
+ output?: string;
12
+ standard?: WcagStandard;
13
+ }
14
+ /**
15
+ * Main scan-storybook command
16
+ */
17
+ export declare function scanStorybookCommand(options?: ScanStorybookOptions): Promise<AllyReport | null>;
18
+ export default scanStorybookCommand;