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,367 @@
1
+ /**
2
+ * ally health command - Quick accessibility overview like npm audit
3
+ */
4
+ import { readFile } from 'fs/promises';
5
+ import { existsSync } from 'fs';
6
+ import { resolve } from 'path';
7
+ import chalk from 'chalk';
8
+ import boxen from 'boxen';
9
+ import { printBanner, createSpinner, printError, printInfo, } from '../utils/ui.js';
10
+ import { AccessibilityScanner, findHtmlFiles, createReport, DEFAULT_STANDARD, DEFAULT_TIMEOUT, DEFAULT_BATCH_SIZE, } from '../utils/scanner.js';
11
+ import { loadConfig, loadIgnorePatterns } from '../utils/config.js';
12
+ const CATEGORY_CONFIG = {
13
+ 'html-structure': {
14
+ name: 'HTML Structure',
15
+ icon: '1',
16
+ patterns: [
17
+ /^heading-order$/,
18
+ /^document-title$/,
19
+ /^html-has-lang$/,
20
+ /^html-lang-valid$/,
21
+ /^bypass$/,
22
+ /^landmark-/,
23
+ /^region$/,
24
+ /^page-has-heading-one$/,
25
+ /^meta-viewport$/,
26
+ /^frame-title$/,
27
+ /^valid-lang$/,
28
+ ],
29
+ },
30
+ 'color-contrast': {
31
+ name: 'Color Contrast',
32
+ icon: '2',
33
+ patterns: [
34
+ /^color-contrast/,
35
+ /^link-in-text-block$/,
36
+ ],
37
+ },
38
+ 'keyboard-nav': {
39
+ name: 'Keyboard Nav',
40
+ icon: '3',
41
+ patterns: [
42
+ /^tabindex$/,
43
+ /^focus-/,
44
+ /^scrollable-region-focusable$/,
45
+ /^accesskey/,
46
+ /^skip-link$/,
47
+ ],
48
+ },
49
+ 'aria-usage': {
50
+ name: 'ARIA Usage',
51
+ icon: '4',
52
+ patterns: [
53
+ /^aria-/,
54
+ /^role-/,
55
+ ],
56
+ },
57
+ 'forms': {
58
+ name: 'Forms',
59
+ icon: '5',
60
+ patterns: [
61
+ /^label$/,
62
+ /^label-/,
63
+ /^autocomplete-valid$/,
64
+ /^input-/,
65
+ /^select-/,
66
+ /^form-field-multiple-labels$/,
67
+ ],
68
+ },
69
+ 'images': {
70
+ name: 'Images',
71
+ icon: '6',
72
+ patterns: [
73
+ /^image-alt$/,
74
+ /^svg-img-alt$/,
75
+ /^image-redundant-alt$/,
76
+ /^object-alt$/,
77
+ /^area-alt$/,
78
+ ],
79
+ },
80
+ };
81
+ /**
82
+ * Severity penalty points
83
+ */
84
+ const SEVERITY_PENALTY = {
85
+ critical: 25,
86
+ serious: 15,
87
+ moderate: 10,
88
+ minor: 5,
89
+ };
90
+ /**
91
+ * Determine which category a violation belongs to based on its ID
92
+ */
93
+ function categorizeViolation(violationId) {
94
+ for (const [category, config] of Object.entries(CATEGORY_CONFIG)) {
95
+ for (const pattern of config.patterns) {
96
+ if (pattern.test(violationId)) {
97
+ return category;
98
+ }
99
+ }
100
+ }
101
+ return null;
102
+ }
103
+ /**
104
+ * Calculate scores by category from violations
105
+ */
106
+ function calculateCategoryScores(violations) {
107
+ // Initialize category data
108
+ const categoryData = {
109
+ 'html-structure': { penalty: 0, violations: 0 },
110
+ 'color-contrast': { penalty: 0, violations: 0 },
111
+ 'keyboard-nav': { penalty: 0, violations: 0 },
112
+ 'aria-usage': { penalty: 0, violations: 0 },
113
+ 'forms': { penalty: 0, violations: 0 },
114
+ 'images': { penalty: 0, violations: 0 },
115
+ };
116
+ // Process each violation
117
+ for (const violation of violations) {
118
+ const category = categorizeViolation(violation.id);
119
+ if (category) {
120
+ const penalty = SEVERITY_PENALTY[violation.impact] || 5;
121
+ // Count each node instance, capped at 10 per violation
122
+ const nodeCount = Math.min(violation.nodes.length, 10);
123
+ categoryData[category].penalty += penalty * nodeCount;
124
+ categoryData[category].violations += nodeCount;
125
+ }
126
+ }
127
+ // Convert to scores
128
+ const categoryOrder = ['html-structure', 'color-contrast', 'keyboard-nav', 'aria-usage', 'forms', 'images'];
129
+ return categoryOrder.map((category) => {
130
+ const config = CATEGORY_CONFIG[category];
131
+ const data = categoryData[category];
132
+ // Score starts at 100, subtract penalties, minimum 0
133
+ const score = Math.max(0, 100 - Math.min(data.penalty, 100));
134
+ return {
135
+ category,
136
+ name: config.name,
137
+ score: Math.round(score),
138
+ violations: data.violations,
139
+ icon: config.icon,
140
+ };
141
+ });
142
+ }
143
+ /**
144
+ * Get score label based on value
145
+ */
146
+ function getScoreLabel(score) {
147
+ if (score >= 90)
148
+ return 'Excellent';
149
+ if (score >= 75)
150
+ return 'Good';
151
+ if (score >= 50)
152
+ return 'Needs Work';
153
+ return 'Poor';
154
+ }
155
+ /**
156
+ * Get score icon based on value
157
+ */
158
+ function getScoreIcon(score) {
159
+ if (score >= 75)
160
+ return chalk.green('\u2713'); // checkmark
161
+ if (score >= 50)
162
+ return chalk.yellow('\u26A0'); // warning
163
+ return chalk.red('\u2717'); // x mark
164
+ }
165
+ /**
166
+ * Generate top recommendations from violations
167
+ */
168
+ function generateRecommendations(violations, categoryScores) {
169
+ // Group violations by category and count
170
+ const categoryViolations = new Map();
171
+ for (const violation of violations) {
172
+ const category = categorizeViolation(violation.id);
173
+ if (!category)
174
+ continue;
175
+ if (!categoryViolations.has(category)) {
176
+ categoryViolations.set(category, new Map());
177
+ }
178
+ const catMap = categoryViolations.get(category);
179
+ const existing = catMap.get(violation.id);
180
+ const nodeCount = violation.nodes.length;
181
+ if (existing) {
182
+ existing.count += nodeCount;
183
+ }
184
+ else {
185
+ catMap.set(violation.id, { count: nodeCount, description: violation.help });
186
+ }
187
+ }
188
+ // Sort categories by score (lowest first) to prioritize recommendations
189
+ const sortedCategories = [...categoryScores].sort((a, b) => a.score - b.score);
190
+ const recommendations = [];
191
+ for (const catScore of sortedCategories) {
192
+ if (catScore.score >= 90)
193
+ continue; // Skip excellent categories
194
+ const catViolations = categoryViolations.get(catScore.category);
195
+ if (!catViolations)
196
+ continue;
197
+ // Get the most common violation in this category
198
+ let topViolation = null;
199
+ for (const [id, data] of catViolations.entries()) {
200
+ if (!topViolation || data.count > topViolation.count) {
201
+ topViolation = { id, count: data.count, description: data.description };
202
+ }
203
+ }
204
+ if (topViolation) {
205
+ recommendations.push({
206
+ message: `Fix ${topViolation.count} ${topViolation.description.toLowerCase()}`,
207
+ category: catScore.category,
208
+ count: topViolation.count,
209
+ });
210
+ }
211
+ if (recommendations.length >= 3)
212
+ break; // Limit to 3 recommendations
213
+ }
214
+ return recommendations;
215
+ }
216
+ /**
217
+ * Get the lowest scoring category for the fix command suggestion
218
+ */
219
+ function getWorstCategory(categoryScores) {
220
+ let worst = categoryScores[0];
221
+ for (const score of categoryScores) {
222
+ if (score.score < worst.score) {
223
+ worst = score;
224
+ }
225
+ }
226
+ return worst.category;
227
+ }
228
+ /**
229
+ * Render the health check box
230
+ */
231
+ function renderHealthBox(overallScore, categoryScores, recommendations) {
232
+ const boxWidth = 48;
233
+ const scoreLabel = getScoreLabel(overallScore);
234
+ const scoreColor = overallScore >= 75 ? chalk.green : overallScore >= 50 ? chalk.yellow : chalk.red;
235
+ // Build content lines
236
+ const lines = [];
237
+ // Header
238
+ lines.push(chalk.bold(' Accessibility Health Check'));
239
+ lines.push('');
240
+ // Overall score
241
+ lines.push(` Overall Score: ${scoreColor.bold(`${overallScore}/100`)} (${scoreLabel})`);
242
+ lines.push('');
243
+ // Category scores
244
+ for (const cat of categoryScores) {
245
+ const icon = getScoreIcon(cat.score);
246
+ const nameWidth = 18;
247
+ const paddedName = cat.name.padEnd(nameWidth);
248
+ const scoreStr = `${cat.score}/100`;
249
+ lines.push(` ${icon} ${paddedName} ${chalk.dim('|')} ${scoreStr}`);
250
+ }
251
+ lines.push('');
252
+ // Recommendations
253
+ if (recommendations.length > 0) {
254
+ lines.push(chalk.bold(' Top Recommendations:'));
255
+ recommendations.forEach((rec, i) => {
256
+ lines.push(` ${i + 1}. ${rec.message}`);
257
+ });
258
+ lines.push('');
259
+ }
260
+ // Fix command suggestion
261
+ const worstCategory = getWorstCategory(categoryScores);
262
+ lines.push(chalk.dim(` Run: ally fix --category ${worstCategory}`));
263
+ return lines.join('\n');
264
+ }
265
+ export async function healthCommand(options = {}) {
266
+ printBanner();
267
+ const { path: targetPath = '.', standard = DEFAULT_STANDARD, input, } = options;
268
+ let report = null;
269
+ // Check if we should use existing scan results
270
+ if (input) {
271
+ const inputPath = resolve(input);
272
+ if (!existsSync(inputPath)) {
273
+ printError(`Input file not found: ${input}`);
274
+ return;
275
+ }
276
+ const spinner = createSpinner('Loading scan results...');
277
+ spinner.start();
278
+ try {
279
+ const content = await readFile(inputPath, 'utf-8');
280
+ report = JSON.parse(content);
281
+ spinner.succeed('Loaded existing scan results');
282
+ }
283
+ catch (error) {
284
+ spinner.fail('Failed to load scan results');
285
+ printError(error instanceof Error ? error.message : String(error));
286
+ return;
287
+ }
288
+ }
289
+ else {
290
+ // Check for existing .ally/scan.json
291
+ const defaultScanPath = resolve(targetPath, '.ally', 'scan.json');
292
+ if (existsSync(defaultScanPath)) {
293
+ const spinner = createSpinner('Loading existing scan results...');
294
+ spinner.start();
295
+ try {
296
+ const content = await readFile(defaultScanPath, 'utf-8');
297
+ report = JSON.parse(content);
298
+ spinner.succeed('Using existing scan from .ally/scan.json');
299
+ printInfo('Run `ally scan` to update results');
300
+ }
301
+ catch {
302
+ // Fall through to new scan
303
+ }
304
+ }
305
+ // If no existing results, run a scan
306
+ if (!report) {
307
+ const spinner = createSpinner('Scanning project for accessibility issues...');
308
+ spinner.start();
309
+ try {
310
+ // Load config
311
+ const { config } = await loadConfig();
312
+ const { patterns: ignorePatterns } = await loadIgnorePatterns();
313
+ const allIgnorePatterns = [...ignorePatterns, ...(config.scan?.ignore ?? [])];
314
+ // Find HTML files
315
+ const absolutePath = resolve(targetPath);
316
+ const htmlFiles = await findHtmlFiles(absolutePath, allIgnorePatterns);
317
+ if (htmlFiles.length === 0) {
318
+ spinner.fail('No HTML files found');
319
+ printInfo('Use `ally scan --url <url>` to scan a live URL');
320
+ return;
321
+ }
322
+ spinner.text = `Scanning ${htmlFiles.length} files...`;
323
+ // Scan files
324
+ const scanner = new AccessibilityScanner(DEFAULT_TIMEOUT);
325
+ await scanner.init();
326
+ try {
327
+ const { results } = await scanner.scanHtmlFilesParallel(htmlFiles, standard, DEFAULT_BATCH_SIZE);
328
+ report = createReport(results);
329
+ spinner.succeed(`Scanned ${results.length} files`);
330
+ }
331
+ finally {
332
+ await scanner.close();
333
+ }
334
+ }
335
+ catch (error) {
336
+ spinner.fail('Scan failed');
337
+ printError(error instanceof Error ? error.message : String(error));
338
+ return;
339
+ }
340
+ }
341
+ }
342
+ if (!report) {
343
+ printError('No scan results available');
344
+ return;
345
+ }
346
+ // Collect all violations from the report
347
+ const allViolations = [];
348
+ for (const result of report.results) {
349
+ allViolations.push(...result.violations);
350
+ }
351
+ // Calculate category scores
352
+ const categoryScores = calculateCategoryScores(allViolations);
353
+ // Calculate overall score (average of category scores)
354
+ const overallScore = Math.round(categoryScores.reduce((sum, cat) => sum + cat.score, 0) / categoryScores.length);
355
+ // Generate recommendations
356
+ const recommendations = generateRecommendations(allViolations, categoryScores);
357
+ // Render the health box
358
+ const content = renderHealthBox(overallScore, categoryScores, recommendations);
359
+ const borderColor = overallScore >= 75 ? 'green' : overallScore >= 50 ? 'yellow' : 'red';
360
+ console.log(boxen(content, {
361
+ padding: { top: 1, bottom: 1, left: 0, right: 0 },
362
+ borderStyle: 'round',
363
+ borderColor,
364
+ }));
365
+ console.log();
366
+ }
367
+ export default healthCommand;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * ally history command - View scan history and progress
3
+ */
4
+ interface HistoryCommandOptions {
5
+ limit?: number;
6
+ branch?: string;
7
+ verbose?: boolean;
8
+ }
9
+ export declare function historyCommand(options?: HistoryCommandOptions): Promise<void>;
10
+ export {};
@@ -0,0 +1,191 @@
1
+ /**
2
+ * ally history command - View scan history and progress
3
+ */
4
+ import chalk from 'chalk';
5
+ import boxen from 'boxen';
6
+ import { loadHistory, getStats, timeAgo, } from '../utils/history-tracking.js';
7
+ import { printError, printInfo } from '../utils/ui.js';
8
+ /**
9
+ * Display a single history entry
10
+ */
11
+ function printEntry(entry, index, total) {
12
+ const num = total - index;
13
+ const scoreColor = entry.score >= 90 ? chalk.green :
14
+ entry.score >= 75 ? chalk.yellow :
15
+ entry.score >= 50 ? chalk.yellow.dim :
16
+ chalk.red;
17
+ const branchInfo = entry.branch ? chalk.dim(` [${entry.branch}`) + (entry.commit ? chalk.dim(`@${entry.commit}]`) : chalk.dim(']')) : '';
18
+ console.log(chalk.dim(`#${num}`) +
19
+ ' ' +
20
+ chalk.cyan(timeAgo(entry.timestamp)) +
21
+ ' ' +
22
+ scoreColor(`${entry.score}/100`) +
23
+ ' ' +
24
+ chalk.dim(`(${entry.totalViolations} issues, ${entry.filesScanned} files)`) +
25
+ branchInfo);
26
+ }
27
+ /**
28
+ * Display stats summary
29
+ */
30
+ function printStats(stats) {
31
+ const scoreColor = stats.currentScore >= 90 ? chalk.green.bold :
32
+ stats.currentScore >= 75 ? chalk.yellow.bold :
33
+ stats.currentScore >= 50 ? chalk.yellow :
34
+ chalk.red.bold;
35
+ const trendIcon = stats.trend === 'improving' ? chalk.green('↗ improving') :
36
+ stats.trend === 'declining' ? chalk.red('↘ declining') :
37
+ chalk.blue('→ stable');
38
+ const changeText = stats.scoreChange !== null
39
+ ? stats.scoreChange > 0
40
+ ? chalk.green(`+${stats.scoreChange}`)
41
+ : stats.scoreChange < 0
42
+ ? chalk.red(`${stats.scoreChange}`)
43
+ : chalk.dim('no change')
44
+ : chalk.dim('first scan');
45
+ const fixedText = stats.totalFixed !== null
46
+ ? stats.totalFixed > 0
47
+ ? chalk.green(`${stats.totalFixed} fixed`)
48
+ : stats.totalFixed < 0
49
+ ? chalk.red(`${Math.abs(stats.totalFixed)} added`)
50
+ : chalk.dim('no change')
51
+ : chalk.dim('no data');
52
+ const streakText = stats.streak > 0
53
+ ? chalk.green(`${stats.streak} scan${stats.streak === 1 ? '' : 's'}`)
54
+ : chalk.dim('0 scans');
55
+ const summary = `
56
+ ${chalk.bold('Current Score:')} ${scoreColor(stats.currentScore.toString())}${chalk.dim('/100')} ${changeText}
57
+
58
+ ${chalk.bold('Trend:')} ${trendIcon}
59
+ ${chalk.bold('Streak:')} ${streakText} improving/stable
60
+
61
+ ${chalk.bold('Statistics:')}
62
+ Best: ${chalk.green(stats.bestScore?.toString() ?? 'N/A')}${stats.bestScore ? '/100' : ''}
63
+ Average: ${chalk.yellow(stats.averageScore?.toString() ?? 'N/A')}${stats.averageScore ? '/100' : ''}
64
+ Worst: ${chalk.red(stats.worstScore?.toString() ?? 'N/A')}${stats.worstScore ? '/100' : ''}
65
+
66
+ ${chalk.bold('Progress:')}
67
+ Total scans: ${chalk.cyan(stats.totalScans.toString())}
68
+ Issues fixed: ${fixedText}
69
+ First scan: ${chalk.dim(timeAgo(stats.firstScan))}
70
+ Last scan: ${chalk.dim(timeAgo(stats.lastScan))}
71
+ `;
72
+ console.log(boxen(summary.trim(), {
73
+ padding: 1,
74
+ margin: 1,
75
+ borderStyle: 'round',
76
+ borderColor: stats.currentScore >= 75 ? 'green' : stats.currentScore >= 50 ? 'yellow' : 'red',
77
+ title: '📊 Accessibility Progress',
78
+ titleAlignment: 'center',
79
+ }));
80
+ }
81
+ /**
82
+ * Create ASCII sparkline from scores
83
+ */
84
+ function createSparkline(entries, width = 50) {
85
+ if (entries.length === 0)
86
+ return '';
87
+ const scores = entries.map(e => e.score);
88
+ const min = Math.min(...scores);
89
+ const max = Math.max(...scores);
90
+ const range = max - min || 1;
91
+ const bars = '▁▂▃▄▅▆▇█';
92
+ // Sample scores to fit width
93
+ const step = Math.max(1, Math.ceil(scores.length / width));
94
+ const sampledScores = [];
95
+ for (let i = 0; i < scores.length; i += step) {
96
+ sampledScores.push(scores[i]);
97
+ }
98
+ return sampledScores
99
+ .map(score => {
100
+ const normalized = (score - min) / range;
101
+ const index = Math.min(bars.length - 1, Math.floor(normalized * bars.length));
102
+ const char = bars[index];
103
+ // Color based on score
104
+ if (score >= 90)
105
+ return chalk.green(char);
106
+ if (score >= 75)
107
+ return chalk.yellow(char);
108
+ if (score >= 50)
109
+ return chalk.yellow.dim(char);
110
+ return chalk.red(char);
111
+ })
112
+ .join('');
113
+ }
114
+ export async function historyCommand(options = {}) {
115
+ const { limit = 10, branch, verbose = false } = options;
116
+ // Load history
117
+ const history = await loadHistory();
118
+ if (history.entries.length === 0) {
119
+ printInfo('No scan history yet. Run `ally scan` to start tracking progress.');
120
+ return;
121
+ }
122
+ // Get stats
123
+ const stats = getStats(history);
124
+ if (!stats) {
125
+ printError('Unable to calculate statistics');
126
+ return;
127
+ }
128
+ // Print stats summary
129
+ printStats(stats);
130
+ // Filter by branch if requested
131
+ let entries = history.entries;
132
+ if (branch) {
133
+ entries = entries.filter(e => e.branch === branch);
134
+ if (entries.length === 0) {
135
+ printError(`No scans found for branch: ${branch}`);
136
+ return;
137
+ }
138
+ printInfo(`Showing scans for branch: ${branch}`);
139
+ }
140
+ // Print sparkline
141
+ if (entries.length > 1) {
142
+ console.log();
143
+ console.log(chalk.bold('Score History:'));
144
+ console.log(createSparkline(entries));
145
+ console.log(chalk.dim(`${entries[0].score}${' '.repeat(42)}${entries[entries.length - 1].score}`));
146
+ console.log();
147
+ }
148
+ // Print recent entries
149
+ console.log(chalk.bold(`Recent Scans (last ${Math.min(limit, entries.length)}):`));
150
+ console.log();
151
+ const recentEntries = entries.slice(-limit).reverse();
152
+ recentEntries.forEach((entry, index) => {
153
+ printEntry(entry, index, entries.length);
154
+ });
155
+ // Motivational message
156
+ console.log();
157
+ const motivation = getMotivationalMessage(stats);
158
+ if (motivation) {
159
+ console.log(chalk.cyan('💡 ' + motivation));
160
+ }
161
+ }
162
+ /**
163
+ * Get motivational message based on stats
164
+ */
165
+ function getMotivationalMessage(stats) {
166
+ if (stats.currentScore === 100) {
167
+ return 'Perfect score! Your site is fully accessible. 🎉';
168
+ }
169
+ if (stats.trend === 'improving' && stats.streak >= 3) {
170
+ return `Great momentum! You've improved for ${stats.streak} consecutive scans. Keep it up!`;
171
+ }
172
+ if (stats.trend === 'improving') {
173
+ return 'You\'re on the right track! Keep fixing those issues.';
174
+ }
175
+ if (stats.trend === 'declining') {
176
+ return 'Score is declining. Time to address those new violations!';
177
+ }
178
+ if (stats.totalFixed && stats.totalFixed > 10) {
179
+ return `Awesome! You've fixed ${stats.totalFixed} violations since starting.`;
180
+ }
181
+ if (stats.currentScore >= 90) {
182
+ return 'Almost perfect! Just a few more fixes to go.';
183
+ }
184
+ if (stats.currentScore >= 75) {
185
+ return 'Good accessibility! Focus on critical and serious issues.';
186
+ }
187
+ if (stats.currentScore >= 50) {
188
+ return 'Making progress! Prioritize high-impact violations first.';
189
+ }
190
+ return 'Start by fixing critical violations - they block the most users.';
191
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * ally init command - Sets up ally configuration in a project
3
+ */
4
+ interface InitOptions {
5
+ force?: boolean;
6
+ hooks?: boolean;
7
+ }
8
+ export declare function initCommand(options?: InitOptions): Promise<void>;
9
+ export default initCommand;