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,613 @@
1
+ /**
2
+ * ally audit-palette command - Audits a design system color palette for contrast issues
3
+ *
4
+ * Supports:
5
+ * - Tailwind config files (tailwind.config.js, tailwind.config.ts)
6
+ * - CSS variable files (*.css)
7
+ * - JSON palette files (*.json)
8
+ */
9
+ import { readFile } from 'fs/promises';
10
+ import { existsSync } from 'fs';
11
+ import { resolve, extname } from 'path';
12
+ import chalk from 'chalk';
13
+ import boxen from 'boxen';
14
+ import { printBanner, createSpinner, printError, printInfo, printWarning, } from '../utils/ui.js';
15
+ // ============================================================================
16
+ // Color Parsing Utilities
17
+ // ============================================================================
18
+ /**
19
+ * Parse a hex color string to RGB values
20
+ */
21
+ function hexToRgb(hex) {
22
+ // Remove # if present
23
+ const cleanHex = hex.replace(/^#/, '');
24
+ // Support 3-char and 6-char hex
25
+ let fullHex;
26
+ if (cleanHex.length === 3) {
27
+ fullHex = cleanHex
28
+ .split('')
29
+ .map((c) => c + c)
30
+ .join('');
31
+ }
32
+ else if (cleanHex.length === 6) {
33
+ fullHex = cleanHex;
34
+ }
35
+ else {
36
+ return null;
37
+ }
38
+ const num = parseInt(fullHex, 16);
39
+ if (isNaN(num))
40
+ return null;
41
+ return {
42
+ r: (num >> 16) & 255,
43
+ g: (num >> 8) & 255,
44
+ b: num & 255,
45
+ };
46
+ }
47
+ /**
48
+ * Convert RGB to hex string
49
+ */
50
+ function rgbToHex(r, g, b) {
51
+ const toHex = (n) => Math.round(Math.max(0, Math.min(255, n)))
52
+ .toString(16)
53
+ .padStart(2, '0');
54
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}`.toUpperCase();
55
+ }
56
+ /**
57
+ * Check if a string looks like a valid hex color
58
+ */
59
+ function isValidHexColor(value) {
60
+ return /^#?([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.test(value);
61
+ }
62
+ /**
63
+ * Normalize hex color to uppercase with #
64
+ */
65
+ function normalizeHex(hex) {
66
+ const cleanHex = hex.replace(/^#/, '').toUpperCase();
67
+ if (cleanHex.length === 3) {
68
+ return ('#' +
69
+ cleanHex
70
+ .split('')
71
+ .map((c) => c + c)
72
+ .join(''));
73
+ }
74
+ return '#' + cleanHex;
75
+ }
76
+ // ============================================================================
77
+ // Contrast Calculation (WCAG 2.x)
78
+ // ============================================================================
79
+ /**
80
+ * Calculate relative luminance per WCAG 2.x spec
81
+ * https://www.w3.org/WAI/GL/wiki/Relative_luminance
82
+ */
83
+ function relativeLuminance(r, g, b) {
84
+ const sRGB = [r, g, b].map((c) => {
85
+ const s = c / 255;
86
+ return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
87
+ });
88
+ return 0.2126 * sRGB[0] + 0.7152 * sRGB[1] + 0.0722 * sRGB[2];
89
+ }
90
+ /**
91
+ * Calculate WCAG 2.x contrast ratio between two colors
92
+ */
93
+ function wcagContrastRatio(fg, bg) {
94
+ const l1 = relativeLuminance(fg.r, fg.g, fg.b);
95
+ const l2 = relativeLuminance(bg.r, bg.g, bg.b);
96
+ const lighter = Math.max(l1, l2);
97
+ const darker = Math.min(l1, l2);
98
+ return (lighter + 0.05) / (darker + 0.05);
99
+ }
100
+ // ============================================================================
101
+ // APCA Contrast Calculation
102
+ // ============================================================================
103
+ /**
104
+ * APCA (Accessible Perceptual Contrast Algorithm) calculation
105
+ * Based on APCA W3 specification
106
+ * https://github.com/Myndex/SAPC-APCA
107
+ *
108
+ * Returns Lc value (Lightness contrast):
109
+ * - Positive values: light text on dark background
110
+ * - Negative values: dark text on light background
111
+ * - Magnitude indicates contrast level (higher = more contrast)
112
+ * - |Lc| >= 60 is recommended for body text
113
+ * - |Lc| >= 45 is recommended for large text
114
+ */
115
+ function calcAPCA(fg, bg) {
116
+ // sRGB to Y (luminance) conversion with APCA coefficients
117
+ const sRGBtoY = (rgb) => {
118
+ // Piecewise sRGB decode
119
+ const mainTRC = 2.4;
120
+ const decode = (c) => {
121
+ const s = c / 255;
122
+ return s <= 0.04045
123
+ ? s / 12.92
124
+ : Math.pow((s + 0.055) / 1.055, mainTRC);
125
+ };
126
+ // APCA uses slightly different coefficients than WCAG
127
+ const Rco = 0.2126729;
128
+ const Gco = 0.7151522;
129
+ const Bco = 0.0721750;
130
+ return Rco * decode(rgb.r) + Gco * decode(rgb.g) + Bco * decode(rgb.b);
131
+ };
132
+ // Soft clamp function
133
+ const softClamp = (y) => {
134
+ const blkThrs = 0.022;
135
+ const blkClmp = 1.414;
136
+ return y > blkThrs ? y : y + Math.pow(blkThrs - y, blkClmp);
137
+ };
138
+ // Calculate luminance values
139
+ let Ytxt = sRGBtoY(fg);
140
+ let Ybg = sRGBtoY(bg);
141
+ // Apply soft clamp
142
+ Ytxt = softClamp(Ytxt);
143
+ Ybg = softClamp(Ybg);
144
+ // APCA constants for contrast calculation
145
+ const normBG = 0.56;
146
+ const normTXT = 0.57;
147
+ const revTXT = 0.62;
148
+ const revBG = 0.65;
149
+ const scaleBoW = 1.14;
150
+ const scaleWoB = 1.14;
151
+ const loBoWoffset = 0.027;
152
+ const loWoBoffset = 0.027;
153
+ const loClip = 0.1;
154
+ const deltaYmin = 0.0005;
155
+ // Calculate contrast
156
+ let SAPC = 0;
157
+ let outputContrast = 0;
158
+ // Check for adequate difference
159
+ if (Math.abs(Ybg - Ytxt) < deltaYmin) {
160
+ return 0;
161
+ }
162
+ // Calculate polarity-dependent contrast
163
+ if (Ybg > Ytxt) {
164
+ // Dark text on light background
165
+ SAPC = (Math.pow(Ybg, normBG) - Math.pow(Ytxt, normTXT)) * scaleBoW;
166
+ outputContrast = SAPC < loClip ? 0 : SAPC - loBoWoffset;
167
+ }
168
+ else {
169
+ // Light text on dark background
170
+ SAPC = (Math.pow(Ybg, revBG) - Math.pow(Ytxt, revTXT)) * scaleWoB;
171
+ outputContrast = SAPC > -loClip ? 0 : SAPC + loWoBoffset;
172
+ }
173
+ // Return as Lc value (multiply by 100)
174
+ return Math.round(outputContrast * 100);
175
+ }
176
+ // ============================================================================
177
+ // File Parsers
178
+ // ============================================================================
179
+ /**
180
+ * Parse Tailwind config file to extract colors
181
+ */
182
+ async function parseTailwindConfig(filePath) {
183
+ const content = await readFile(filePath, 'utf-8');
184
+ const colors = [];
185
+ // Extract the colors object using regex (handles both JS and TS)
186
+ // This is a simplified parser - a real implementation might use esbuild/swc
187
+ const colorsMatch = content.match(/colors\s*:\s*\{([\s\S]*?)\n\s*\}/);
188
+ if (!colorsMatch) {
189
+ // Try theme.extend.colors pattern
190
+ const extendMatch = content.match(/extend\s*:\s*\{[\s\S]*?colors\s*:\s*\{([\s\S]*?)\n\s*\}\s*\}/);
191
+ if (extendMatch) {
192
+ parseColorObject(extendMatch[1], '', colors);
193
+ }
194
+ return colors;
195
+ }
196
+ parseColorObject(colorsMatch[1], '', colors);
197
+ return colors;
198
+ }
199
+ /**
200
+ * Recursively parse a color object string
201
+ */
202
+ function parseColorObject(content, prefix, colors) {
203
+ // Match simple color definitions: name: '#hex' or 'name': '#hex'
204
+ const simplePattern = /['"]?(\w+)['"]?\s*:\s*['"]?(#[A-Fa-f0-9]{3,6})['"]?/g;
205
+ let match;
206
+ while ((match = simplePattern.exec(content)) !== null) {
207
+ const name = prefix ? `${prefix}-${match[1]}` : match[1];
208
+ const hex = normalizeHex(match[2]);
209
+ const rgb = hexToRgb(hex);
210
+ if (rgb) {
211
+ colors.push({ name, hex, rgb });
212
+ }
213
+ }
214
+ // Match nested objects: name: { ... }
215
+ const nestedPattern = /['"]?(\w+)['"]?\s*:\s*\{([^{}]+)\}/g;
216
+ while ((match = nestedPattern.exec(content)) !== null) {
217
+ const nestedPrefix = prefix ? `${prefix}-${match[1]}` : match[1];
218
+ parseColorObject(match[2], nestedPrefix, colors);
219
+ }
220
+ }
221
+ /**
222
+ * Parse CSS file to extract color variables
223
+ */
224
+ async function parseCssFile(filePath) {
225
+ const content = await readFile(filePath, 'utf-8');
226
+ const colors = [];
227
+ // Match CSS custom properties with color values
228
+ // --color-name: #hex;
229
+ const pattern = /--([\w-]+)\s*:\s*(#[A-Fa-f0-9]{3,6})\s*;/g;
230
+ let match;
231
+ while ((match = pattern.exec(content)) !== null) {
232
+ const name = match[1];
233
+ const hex = normalizeHex(match[2]);
234
+ const rgb = hexToRgb(hex);
235
+ if (rgb) {
236
+ colors.push({ name, hex, rgb });
237
+ }
238
+ }
239
+ return colors;
240
+ }
241
+ /**
242
+ * Parse JSON palette file to extract colors
243
+ */
244
+ async function parseJsonFile(filePath) {
245
+ const content = await readFile(filePath, 'utf-8');
246
+ const data = JSON.parse(content);
247
+ const colors = [];
248
+ // Look for colors in common structures
249
+ const colorObjects = data.colors || data.palette || data.theme?.colors || data;
250
+ function extractColors(obj, prefix = '') {
251
+ for (const [key, value] of Object.entries(obj)) {
252
+ if (typeof value === 'string' && isValidHexColor(value)) {
253
+ const name = prefix ? `${prefix}-${key}` : key;
254
+ const hex = normalizeHex(value);
255
+ const rgb = hexToRgb(hex);
256
+ if (rgb) {
257
+ colors.push({ name, hex, rgb });
258
+ }
259
+ }
260
+ else if (typeof value === 'object' && value !== null) {
261
+ extractColors(value, prefix ? `${prefix}-${key}` : key);
262
+ }
263
+ }
264
+ }
265
+ if (typeof colorObjects === 'object' && colorObjects !== null) {
266
+ extractColors(colorObjects);
267
+ }
268
+ return colors;
269
+ }
270
+ /**
271
+ * Detect file type and parse colors accordingly
272
+ */
273
+ async function parseColorFile(filePath) {
274
+ const ext = extname(filePath).toLowerCase();
275
+ if (filePath.includes('tailwind.config')) {
276
+ return parseTailwindConfig(filePath);
277
+ }
278
+ switch (ext) {
279
+ case '.css':
280
+ return parseCssFile(filePath);
281
+ case '.json':
282
+ return parseJsonFile(filePath);
283
+ case '.js':
284
+ case '.ts':
285
+ case '.mjs':
286
+ case '.cjs':
287
+ return parseTailwindConfig(filePath);
288
+ default:
289
+ throw new Error(`Unsupported file type: ${ext}. Supported: .css, .json, .js, .ts`);
290
+ }
291
+ }
292
+ // ============================================================================
293
+ // Contrast Analysis
294
+ // ============================================================================
295
+ /**
296
+ * Test all foreground/background combinations
297
+ */
298
+ function analyzeContrastPairs(colors, level, largeText) {
299
+ const results = [];
300
+ const failing = [];
301
+ const passing = [];
302
+ // Thresholds based on WCAG level and text size
303
+ const aaThreshold = largeText ? 3.0 : 4.5;
304
+ const aaaThreshold = largeText ? 4.5 : 7.0;
305
+ const requiredThreshold = level === 'aaa' ? aaaThreshold : aaThreshold;
306
+ // Test all pairs (excluding same color)
307
+ for (const bg of colors) {
308
+ for (const fg of colors) {
309
+ if (bg.hex === fg.hex)
310
+ continue;
311
+ const wcagRatio = wcagContrastRatio(fg.rgb, bg.rgb);
312
+ const apcaLc = calcAPCA(fg.rgb, bg.rgb);
313
+ const wcagAA = wcagRatio >= aaThreshold;
314
+ const wcagAAA = wcagRatio >= aaaThreshold;
315
+ const result = {
316
+ background: bg,
317
+ foreground: fg,
318
+ wcagRatio: Math.round(wcagRatio * 10) / 10,
319
+ wcagAA,
320
+ wcagAAA,
321
+ apcaLc,
322
+ };
323
+ results.push(result);
324
+ const passesLevel = level === 'aaa' ? wcagAAA : wcagAA;
325
+ if (passesLevel) {
326
+ passing.push(result);
327
+ }
328
+ else {
329
+ failing.push(result);
330
+ }
331
+ }
332
+ }
333
+ // Generate fix suggestions for failing pairs
334
+ const suggestions = generateSuggestions(failing, colors, requiredThreshold);
335
+ return {
336
+ totalColors: colors.length,
337
+ totalCombinations: results.length,
338
+ wcagAAPassing: results.filter((r) => r.wcagAA).length,
339
+ wcagAAAPassing: results.filter((r) => r.wcagAAA).length,
340
+ failing,
341
+ passing,
342
+ suggestions,
343
+ };
344
+ }
345
+ /**
346
+ * Generate fix suggestions by finding nearest passing color
347
+ */
348
+ function generateSuggestions(failing, allColors, threshold) {
349
+ const suggestions = [];
350
+ const processed = new Set();
351
+ for (const fail of failing.slice(0, 10)) {
352
+ // Limit suggestions
353
+ const key = `${fail.background.name}+${fail.foreground.name}`;
354
+ if (processed.has(key))
355
+ continue;
356
+ processed.add(key);
357
+ // Find alternative foreground colors that pass
358
+ let bestAlternative = null;
359
+ for (const candidate of allColors) {
360
+ if (candidate.hex === fail.foreground.hex)
361
+ continue;
362
+ if (candidate.hex === fail.background.hex)
363
+ continue;
364
+ const ratio = wcagContrastRatio(candidate.rgb, fail.background.rgb);
365
+ if (ratio >= threshold) {
366
+ // Prefer colors with similar lightness to original
367
+ if (!bestAlternative ||
368
+ Math.abs(relativeLuminance(candidate.rgb.r, candidate.rgb.g, candidate.rgb.b) -
369
+ relativeLuminance(fail.foreground.rgb.r, fail.foreground.rgb.g, fail.foreground.rgb.b)) <
370
+ Math.abs(relativeLuminance(bestAlternative.color.rgb.r, bestAlternative.color.rgb.g, bestAlternative.color.rgb.b) -
371
+ relativeLuminance(fail.foreground.rgb.r, fail.foreground.rgb.g, fail.foreground.rgb.b))) {
372
+ bestAlternative = { color: candidate, ratio };
373
+ }
374
+ }
375
+ }
376
+ if (bestAlternative) {
377
+ suggestions.push({
378
+ background: fail.background.name,
379
+ foreground: fail.foreground.name,
380
+ suggestedForeground: bestAlternative.color.name,
381
+ originalRatio: fail.wcagRatio,
382
+ newRatio: Math.round(bestAlternative.ratio * 10) / 10,
383
+ });
384
+ }
385
+ }
386
+ return suggestions;
387
+ }
388
+ // ============================================================================
389
+ // Output Formatting
390
+ // ============================================================================
391
+ /**
392
+ * Format results as default terminal output
393
+ */
394
+ function formatDefaultOutput(result, showApca, level) {
395
+ // Summary box
396
+ const aaPercent = Math.round((result.wcagAAPassing / result.totalCombinations) * 100);
397
+ const aaaPercent = Math.round((result.wcagAAAPassing / result.totalCombinations) * 100);
398
+ const summaryContent = `
399
+ ${chalk.bold(' Palette Audit Results')}
400
+
401
+ Combinations tested: ${result.totalCombinations}
402
+ WCAG AA passing: ${result.wcagAAPassing} (${aaPercent}%)
403
+ WCAG AAA passing: ${result.wcagAAAPassing} (${aaaPercent}%)
404
+ `;
405
+ const borderColor = aaPercent >= 80 ? 'green' : aaPercent >= 50 ? 'yellow' : 'red';
406
+ console.log(boxen(summaryContent.trim(), {
407
+ padding: 1,
408
+ borderStyle: 'round',
409
+ borderColor,
410
+ }));
411
+ // Failing combinations table
412
+ if (result.failing.length > 0) {
413
+ console.log();
414
+ console.log(chalk.bold(`Failing Combinations (WCAG ${level.toUpperCase()}):`));
415
+ console.log();
416
+ // Table header
417
+ const bgCol = 'Background'.padEnd(14);
418
+ const fgCol = 'Foreground'.padEnd(14);
419
+ const ratioCol = 'Ratio'.padEnd(7);
420
+ const wcagCol = 'WCAG'.padEnd(8);
421
+ const apcaCol = showApca ? 'APCA Lc'.padEnd(11) : '';
422
+ console.log(chalk.dim(`+${'-'.repeat(14)}+${'-'.repeat(14)}+${'-'.repeat(7)}+${'-'.repeat(8)}+${showApca ? '-'.repeat(11) + '+' : ''}`));
423
+ console.log(chalk.dim('|') +
424
+ chalk.bold(bgCol) +
425
+ chalk.dim('|') +
426
+ chalk.bold(fgCol) +
427
+ chalk.dim('|') +
428
+ chalk.bold(ratioCol) +
429
+ chalk.dim('|') +
430
+ chalk.bold(wcagCol) +
431
+ chalk.dim('|') +
432
+ (showApca ? chalk.bold(apcaCol) + chalk.dim('|') : ''));
433
+ console.log(chalk.dim(`+${'-'.repeat(14)}+${'-'.repeat(14)}+${'-'.repeat(7)}+${'-'.repeat(8)}+${showApca ? '-'.repeat(11) + '+' : ''}`));
434
+ // Show up to 15 failing pairs
435
+ for (const fail of result.failing.slice(0, 15)) {
436
+ const bg = fail.background.name.substring(0, 12).padEnd(14);
437
+ const fg = fail.foreground.name.substring(0, 12).padEnd(14);
438
+ const ratio = `${fail.wcagRatio}:1`.padEnd(7);
439
+ const wcag = chalk.red('Fail').padEnd(8 + 10); // Account for ANSI codes
440
+ const apca = showApca ? `Lc ${fail.apcaLc}`.padEnd(11) : '';
441
+ console.log(chalk.dim('|') +
442
+ bg +
443
+ chalk.dim('|') +
444
+ fg +
445
+ chalk.dim('|') +
446
+ ratio +
447
+ chalk.dim('|') +
448
+ wcag +
449
+ chalk.dim('|') +
450
+ (showApca ? apca + chalk.dim('|') : ''));
451
+ }
452
+ console.log(chalk.dim(`+${'-'.repeat(14)}+${'-'.repeat(14)}+${'-'.repeat(7)}+${'-'.repeat(8)}+${showApca ? '-'.repeat(11) + '+' : ''}`));
453
+ if (result.failing.length > 15) {
454
+ console.log(chalk.dim(` ... and ${result.failing.length - 15} more failing pairs`));
455
+ }
456
+ }
457
+ // Suggestions
458
+ if (result.suggestions.length > 0) {
459
+ console.log();
460
+ console.log(chalk.bold('Suggested Fixes:'));
461
+ for (const sug of result.suggestions) {
462
+ console.log(chalk.yellow('*') +
463
+ ` ${sug.background} + ${sug.foreground}: Use ${chalk.green(sug.suggestedForeground)} instead (${sug.newRatio}:1)`);
464
+ }
465
+ }
466
+ // Safe pairs for text
467
+ const safePairs = result.passing
468
+ .filter((p) => p.wcagAAA)
469
+ .sort((a, b) => b.wcagRatio - a.wcagRatio)
470
+ .slice(0, 5);
471
+ if (safePairs.length > 0) {
472
+ console.log();
473
+ console.log(chalk.bold('Safe Pairs for Text:'));
474
+ for (const pair of safePairs) {
475
+ const label = pair.wcagAAA ? chalk.green('AAA') : chalk.yellow('AA');
476
+ const apca = showApca ? chalk.dim(` (Lc ${pair.apcaLc})`) : '';
477
+ console.log(chalk.green('*') +
478
+ ` ${pair.background.name} + ${pair.foreground.name}: ${pair.wcagRatio}:1 ${label}${apca}`);
479
+ }
480
+ }
481
+ }
482
+ /**
483
+ * Format results as JSON
484
+ */
485
+ function formatJsonOutput(result) {
486
+ const output = {
487
+ summary: {
488
+ totalColors: result.totalColors,
489
+ totalCombinations: result.totalCombinations,
490
+ wcagAAPassing: result.wcagAAPassing,
491
+ wcagAAAPassing: result.wcagAAAPassing,
492
+ wcagAAPercent: Math.round((result.wcagAAPassing / result.totalCombinations) * 100),
493
+ wcagAAAPercent: Math.round((result.wcagAAAPassing / result.totalCombinations) * 100),
494
+ },
495
+ failing: result.failing.map((f) => ({
496
+ background: { name: f.background.name, hex: f.background.hex },
497
+ foreground: { name: f.foreground.name, hex: f.foreground.hex },
498
+ wcagRatio: f.wcagRatio,
499
+ wcagAA: f.wcagAA,
500
+ wcagAAA: f.wcagAAA,
501
+ apcaLc: f.apcaLc,
502
+ })),
503
+ passing: result.passing.map((p) => ({
504
+ background: { name: p.background.name, hex: p.background.hex },
505
+ foreground: { name: p.foreground.name, hex: p.foreground.hex },
506
+ wcagRatio: p.wcagRatio,
507
+ wcagAA: p.wcagAA,
508
+ wcagAAA: p.wcagAAA,
509
+ apcaLc: p.apcaLc,
510
+ })),
511
+ suggestions: result.suggestions,
512
+ };
513
+ console.log(JSON.stringify(output, null, 2));
514
+ }
515
+ /**
516
+ * Format results as CSV
517
+ */
518
+ function formatCsvOutput(result) {
519
+ const headers = [
520
+ 'background_name',
521
+ 'background_hex',
522
+ 'foreground_name',
523
+ 'foreground_hex',
524
+ 'wcag_ratio',
525
+ 'wcag_aa',
526
+ 'wcag_aaa',
527
+ 'apca_lc',
528
+ ];
529
+ console.log(headers.join(','));
530
+ const allResults = [...result.failing, ...result.passing];
531
+ for (const r of allResults) {
532
+ const row = [
533
+ r.background.name,
534
+ r.background.hex,
535
+ r.foreground.name,
536
+ r.foreground.hex,
537
+ r.wcagRatio,
538
+ r.wcagAA,
539
+ r.wcagAAA,
540
+ r.apcaLc,
541
+ ];
542
+ console.log(row.join(','));
543
+ }
544
+ }
545
+ // ============================================================================
546
+ // Main Command
547
+ // ============================================================================
548
+ export async function auditPaletteCommand(filePath, options = {}) {
549
+ const { format = 'default', level = 'aa', largeText = false, apca = false, } = options;
550
+ // Only print banner for default format
551
+ if (format === 'default') {
552
+ printBanner();
553
+ }
554
+ const absolutePath = resolve(filePath);
555
+ // Check file exists
556
+ if (!existsSync(absolutePath)) {
557
+ printError(`File not found: ${filePath}`);
558
+ process.exit(1);
559
+ }
560
+ // Parse colors
561
+ let spinner = null;
562
+ if (format === 'default') {
563
+ spinner = createSpinner('Analyzing color palette...');
564
+ spinner.start();
565
+ }
566
+ let colors;
567
+ try {
568
+ colors = await parseColorFile(absolutePath);
569
+ }
570
+ catch (error) {
571
+ if (spinner)
572
+ spinner.fail('Failed to parse color file');
573
+ printError(error instanceof Error ? error.message : String(error));
574
+ process.exit(1);
575
+ }
576
+ if (colors.length === 0) {
577
+ if (spinner)
578
+ spinner.fail('No colors found in file');
579
+ printWarning('The file does not contain any recognizable color definitions.');
580
+ printInfo('Supported formats:');
581
+ printInfo(' - Tailwind: colors: { primary: "#hex" }');
582
+ printInfo(' - CSS: --color-name: #hex;');
583
+ printInfo(' - JSON: { "colors": { "name": "#hex" } }');
584
+ process.exit(1);
585
+ }
586
+ if (format === 'default' && spinner) {
587
+ spinner.text = `Found ${colors.length} colors. Testing ${colors.length * (colors.length - 1)} combinations...`;
588
+ }
589
+ // Analyze contrast
590
+ const result = analyzeContrastPairs(colors, level, largeText);
591
+ if (spinner) {
592
+ spinner.succeed(`Found ${colors.length} colors in palette`);
593
+ console.log();
594
+ }
595
+ // Output results
596
+ switch (format) {
597
+ case 'json':
598
+ formatJsonOutput(result);
599
+ break;
600
+ case 'csv':
601
+ formatCsvOutput(result);
602
+ break;
603
+ default:
604
+ formatDefaultOutput(result, apca, level);
605
+ break;
606
+ }
607
+ // Exit with error if there are failing combinations
608
+ if (result.failing.length > 0 && format === 'default') {
609
+ console.log();
610
+ printWarning(`${result.failing.length} color combinations fail WCAG ${level.toUpperCase()}`);
611
+ }
612
+ }
613
+ export default auditPaletteCommand;
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * ally auto-pr - THE KILLER FEATURE
4
+ * Automated accessibility PR creation: scan → fix → commit → push → PR
5
+ */
6
+ interface AutoPROptions {
7
+ fix?: boolean;
8
+ branch?: string;
9
+ token?: string;
10
+ title?: string;
11
+ draft?: boolean;
12
+ remote?: string;
13
+ }
14
+ /**
15
+ * Main auto-pr command - THE KILLER FEATURE
16
+ * Scans for accessibility issues, fixes them, and creates a GitHub PR
17
+ */
18
+ export declare function autoPRCommand(options: AutoPROptions): Promise<void>;
19
+ export {};