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
package/dist/cli.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * ally - Your codebase's accessibility ally
4
+ *
5
+ * Scans, explains, and fixes accessibility issues using GitHub Copilot CLI.
6
+ */
7
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,528 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * ally - Your codebase's accessibility ally
4
+ *
5
+ * Scans, explains, and fixes accessibility issues using GitHub Copilot CLI.
6
+ */
7
+ import { Command } from 'commander';
8
+ import { createRequire } from 'module';
9
+ import { handleErrorWithEnhancement } from './utils/enhanced-errors.js';
10
+ // Read version from package.json to stay in sync
11
+ const require = createRequire(import.meta.url);
12
+ const { version } = require('../package.json');
13
+ import { scanCommand } from './commands/scan.js';
14
+ import { explainCommand } from './commands/explain.js';
15
+ import { fixCommand } from './commands/fix.js';
16
+ import { reportCommand } from './commands/report.js';
17
+ import { initCommand } from './commands/init.js';
18
+ import { statsCommand } from './commands/stats.js';
19
+ import { badgeCommand } from './commands/badge.js';
20
+ import { watchCommand } from './commands/watch.js';
21
+ import { learnCommand } from './commands/learn.js';
22
+ import { crawlCommand } from './commands/crawl.js';
23
+ import { treeCommand } from './commands/tree.js';
24
+ import { triageCommand } from './commands/triage.js';
25
+ import { prCheckCommand } from './commands/pr-check.js';
26
+ import { completionCommand } from './commands/completion.js';
27
+ import { doctorCommand } from './commands/doctor.js';
28
+ import { healthCommand } from './commands/health.js';
29
+ import { scanStorybookCommand } from './commands/scan-storybook.js';
30
+ import { auditPaletteCommand } from './commands/audit-palette.js';
31
+ const program = new Command();
32
+ program
33
+ .name('ally')
34
+ .description('Your codebase\'s accessibility ally. Scans, explains, and fixes a11y issues using GitHub Copilot CLI.')
35
+ .version(version);
36
+ // ally scan [path]
37
+ program
38
+ .command('scan [path]')
39
+ .description('Scan files for accessibility violations')
40
+ .option('-o, --output <dir>', 'Output directory for results', '.ally')
41
+ .option('-u, --url <url>', 'Scan a URL instead of files')
42
+ .option('-j, --json', 'Output raw JSON results')
43
+ .option('-f, --format <type>', 'Output format: json, sarif, junit, or csv')
44
+ .option('-v, --verbose', 'Show all violations including minor ones')
45
+ .option('-t, --threshold <number>', 'Exit with error if violations exceed threshold (for CI)')
46
+ .option('--ci', 'CI mode: minimal output, exit code based on violations')
47
+ .option('-F, --fail-on <severities>', 'Fail only on specified severities (comma-separated: critical,serious,moderate,minor)')
48
+ .option('-S, --simulate <type>', 'Simulate color blindness (deuteranopia, protanopia, tritanopia)', (value) => {
49
+ const valid = ['deuteranopia', 'protanopia', 'tritanopia'];
50
+ if (!valid.includes(value)) {
51
+ throw new Error(`Invalid simulation type: ${value}. Valid options: ${valid.join(', ')}`);
52
+ }
53
+ return value;
54
+ })
55
+ .option('-s, --standard <level>', 'WCAG standard to test against (default: wcag22aa)', (value) => {
56
+ const valid = ['wcag2a', 'wcag2aa', 'wcag2aaa', 'wcag21a', 'wcag21aa', 'wcag21aaa', 'wcag22aa', 'section508', 'best-practice'];
57
+ if (!valid.includes(value)) {
58
+ throw new Error(`Invalid standard: ${value}. Valid options: ${valid.join(', ')}`);
59
+ }
60
+ return value;
61
+ })
62
+ .option('-T, --timeout <ms>', 'Page load timeout in milliseconds (default: 30000)')
63
+ .option('-B, --browser <browser>', 'Browser engine: chromium, firefox, webkit (default: chromium)', (value) => {
64
+ const valid = ['chromium', 'firefox', 'webkit'];
65
+ if (!valid.includes(value)) {
66
+ throw new Error(`Invalid browser: ${value}. Valid options: ${valid.join(', ')}`);
67
+ }
68
+ return value;
69
+ })
70
+ .option('--experimental-apca', 'Show APCA Lc values alongside WCAG 2.x contrast ratios (experimental)')
71
+ .option('--max-files <number>', 'Limit scan to first N files (useful for large projects)')
72
+ .option('--baseline', 'Set current scan as accessibility baseline (for regression detection)')
73
+ .option('--compare-baseline', 'Compare against saved baseline and show improvements/regressions')
74
+ .option('--fail-on-regression', 'Exit with error if accessibility regressions detected (with --compare-baseline)')
75
+ .option('--no-cache', 'Do not use cache, rescan all files')
76
+ .action(async (path, options) => {
77
+ try {
78
+ // Parse timeout if provided
79
+ if (options.timeout !== undefined) {
80
+ const timeout = parseInt(options.timeout, 10);
81
+ if (isNaN(timeout) || timeout < 1000) {
82
+ console.error('Error: --timeout must be at least 1000 (1 second)');
83
+ process.exit(1);
84
+ }
85
+ options.timeout = timeout;
86
+ }
87
+ // Parse threshold if provided
88
+ if (options.threshold !== undefined) {
89
+ const threshold = parseInt(options.threshold, 10);
90
+ if (isNaN(threshold) || threshold < 0) {
91
+ console.error('Error: --threshold must be a non-negative number');
92
+ process.exit(1);
93
+ }
94
+ options.threshold = threshold;
95
+ }
96
+ // Parse max-files if provided
97
+ if (options.maxFiles !== undefined) {
98
+ const maxFiles = parseInt(options.maxFiles, 10);
99
+ if (isNaN(maxFiles) || maxFiles < 1) {
100
+ console.error('Error: --max-files must be at least 1');
101
+ process.exit(1);
102
+ }
103
+ options.maxFiles = maxFiles;
104
+ }
105
+ const report = await scanCommand(path, options);
106
+ // CI threshold check
107
+ if (options.threshold !== undefined && report) {
108
+ // If --fail-on is set, filter violations before checking threshold
109
+ let violationCount;
110
+ if (options.failOn) {
111
+ const severities = options.failOn.split(',').map((s) => s.trim().toLowerCase());
112
+ const validSeverities = ['critical', 'serious', 'moderate', 'minor'];
113
+ const invalidSeverities = severities.filter((s) => !validSeverities.includes(s));
114
+ if (invalidSeverities.length > 0) {
115
+ console.error(`Error: Invalid severity values: ${invalidSeverities.join(', ')}`);
116
+ console.error(`Valid values are: ${validSeverities.join(', ')}`);
117
+ process.exit(1);
118
+ }
119
+ // Count violations matching specified severities
120
+ violationCount = report.results.reduce((count, result) => {
121
+ return count + result.violations.filter(v => severities.includes(v.impact)).length;
122
+ }, 0);
123
+ }
124
+ else {
125
+ violationCount = report.summary.totalViolations;
126
+ }
127
+ if (violationCount > options.threshold) {
128
+ const severityNote = options.failOn ? ` (filtered by: ${options.failOn})` : '';
129
+ console.error(`\nCI FAILED: ${violationCount} violations${severityNote} exceed threshold of ${options.threshold}`);
130
+ process.exit(1);
131
+ }
132
+ }
133
+ }
134
+ catch (error) {
135
+ if (error instanceof Error) {
136
+ handleErrorWithEnhancement(error);
137
+ }
138
+ else {
139
+ console.error('Scan failed:', error);
140
+ }
141
+ process.exit(1);
142
+ }
143
+ });
144
+ // ally explain
145
+ program
146
+ .command('explain')
147
+ .description('Get plain-language explanations of violations using Copilot')
148
+ .option('-i, --input <file>', 'Path to scan results', '.ally/scan.json')
149
+ .option('-s, --severity <level>', 'Filter by severity (critical, serious, moderate, minor)')
150
+ .option('-l, --limit <number>', 'Maximum number of issues to explain', '10')
151
+ .option('--ai', 'Use GitHub Copilot CLI for AI-powered explanations')
152
+ .action(async (options) => {
153
+ try {
154
+ const limit = parseInt(options.limit, 10);
155
+ if (isNaN(limit) || limit < 1) {
156
+ console.error('Error: --limit must be a positive number');
157
+ process.exit(1);
158
+ }
159
+ await explainCommand({
160
+ ...options,
161
+ limit,
162
+ });
163
+ }
164
+ catch (error) {
165
+ console.error('Explain failed:', error);
166
+ process.exit(1);
167
+ }
168
+ });
169
+ // ally fix
170
+ program
171
+ .command('fix')
172
+ .description('Fix accessibility issues using Copilot CLI agentic mode')
173
+ .option('-i, --input <file>', 'Path to scan results', '.ally/scan.json')
174
+ .option('-s, --severity <level>', 'Filter by severity (critical, serious, moderate, minor)')
175
+ .option('-a, --auto', 'Automatically apply all fixes without prompting')
176
+ .option('-d, --dry-run', 'Show what would be fixed without making changes')
177
+ .option('-y, --yes', 'Skip interactive review, apply all suggested fixes')
178
+ .action(async (options) => {
179
+ try {
180
+ await fixCommand(options);
181
+ }
182
+ catch (error) {
183
+ console.error('Fix failed:', error);
184
+ process.exit(1);
185
+ }
186
+ });
187
+ // ally report
188
+ program
189
+ .command('report')
190
+ .description('Generate accessibility report (ACCESSIBILITY.md)')
191
+ .option('-i, --input <file>', 'Path to scan results', '.ally/scan.json')
192
+ .option('-o, --output <file>', 'Output file path', 'ACCESSIBILITY.md')
193
+ .option('-f, --format <type>', 'Report format (markdown, json, html)', 'markdown')
194
+ .action(async (options) => {
195
+ try {
196
+ await reportCommand(options);
197
+ }
198
+ catch (error) {
199
+ console.error('Report generation failed:', error);
200
+ process.exit(1);
201
+ }
202
+ });
203
+ // ally init
204
+ program
205
+ .command('init')
206
+ .description('Initialize ally in your project')
207
+ .option('-f, --force', 'Overwrite existing configuration')
208
+ .option('-H, --hooks', 'Set up pre-commit hooks for accessibility checks')
209
+ .action(async (options) => {
210
+ try {
211
+ await initCommand(options);
212
+ }
213
+ catch (error) {
214
+ console.error('Initialization failed:', error);
215
+ process.exit(1);
216
+ }
217
+ });
218
+ // ally stats
219
+ program
220
+ .command('stats')
221
+ .description('Show accessibility progress dashboard')
222
+ .action(async () => {
223
+ try {
224
+ await statsCommand();
225
+ }
226
+ catch (error) {
227
+ console.error('Stats failed:', error);
228
+ process.exit(1);
229
+ }
230
+ });
231
+ // ally history
232
+ program
233
+ .command('history')
234
+ .description('View scan history and progress over time')
235
+ .option('-l, --limit <number>', 'Number of recent scans to show', '10')
236
+ .option('-b, --branch <name>', 'Filter by git branch')
237
+ .option('-v, --verbose', 'Show detailed information')
238
+ .action(async (options) => {
239
+ try {
240
+ const { historyCommand } = await import('./commands/history.js');
241
+ const limit = parseInt(options.limit, 10);
242
+ await historyCommand({ limit, branch: options.branch, verbose: options.verbose });
243
+ }
244
+ catch (error) {
245
+ console.error('History failed:', error);
246
+ process.exit(1);
247
+ }
248
+ });
249
+ // ally badge
250
+ program
251
+ .command('badge')
252
+ .description('Generate accessibility score badge for README')
253
+ .option('-i, --input <file>', 'Path to scan results', '.ally/scan.json')
254
+ .option('-f, --format <type>', 'Output format (url, markdown, svg)', 'url')
255
+ .option('-o, --output <file>', 'Save badge to file (primarily for SVG)')
256
+ .action(async (options) => {
257
+ try {
258
+ await badgeCommand(options);
259
+ }
260
+ catch (error) {
261
+ console.error('Badge generation failed:', error);
262
+ process.exit(1);
263
+ }
264
+ });
265
+ // ally watch
266
+ program
267
+ .command('watch [path]')
268
+ .description('Watch for file changes and run accessibility scans continuously')
269
+ .option('-d, --debounce <ms>', 'Debounce time in milliseconds', '500')
270
+ .option('--clear', 'Clear console between scans')
271
+ .option('--fix-on-save', 'Automatically apply high-confidence fixes (≥90%) when files change')
272
+ .action(async (path, options) => {
273
+ try {
274
+ const debounce = parseInt(options.debounce, 10);
275
+ if (isNaN(debounce) || debounce < 0) {
276
+ console.error('Error: --debounce must be a non-negative number');
277
+ process.exit(1);
278
+ }
279
+ await watchCommand(path, {
280
+ ...options,
281
+ debounce,
282
+ });
283
+ }
284
+ catch (error) {
285
+ console.error('Watch failed:', error);
286
+ process.exit(1);
287
+ }
288
+ });
289
+ // ally learn
290
+ program
291
+ .command('learn [violation-id]')
292
+ .description('Learn about WCAG accessibility criteria in depth')
293
+ .option('-l, --list', 'List all known violation types')
294
+ .action(async (violationId, options) => {
295
+ try {
296
+ await learnCommand(violationId, options);
297
+ }
298
+ catch (error) {
299
+ console.error('Learn failed:', error);
300
+ process.exit(1);
301
+ }
302
+ });
303
+ // ally crawl
304
+ program
305
+ .command('crawl <url>')
306
+ .description('Crawl entire website by following links and scan each page')
307
+ .option('-d, --depth <number>', 'Maximum crawl depth', '2')
308
+ .option('-l, --limit <number>', 'Maximum pages to scan', '10')
309
+ .option('--same-origin', 'Only follow links to same origin (default: true)', true)
310
+ .option('--no-same-origin', 'Follow links to any origin')
311
+ .option('-o, --output <dir>', 'Output directory for results', '.ally')
312
+ .action(async (url, options) => {
313
+ try {
314
+ // Parse numeric options
315
+ const depth = parseInt(options.depth, 10);
316
+ if (isNaN(depth) || depth < 0) {
317
+ console.error('Error: --depth must be a non-negative number');
318
+ process.exit(1);
319
+ }
320
+ const limit = parseInt(options.limit, 10);
321
+ if (isNaN(limit) || limit < 1) {
322
+ console.error('Error: --limit must be a positive number');
323
+ process.exit(1);
324
+ }
325
+ await crawlCommand(url, {
326
+ depth,
327
+ limit,
328
+ sameOrigin: options.sameOrigin,
329
+ output: options.output,
330
+ });
331
+ }
332
+ catch (error) {
333
+ console.error('Crawl failed:', error);
334
+ process.exit(1);
335
+ }
336
+ });
337
+ // ally tree
338
+ program
339
+ .command('tree <url>')
340
+ .description('Display accessibility tree for a URL')
341
+ .option('-d, --depth <number>', 'Maximum tree depth to display', '5')
342
+ .option('-r, --role <role>', 'Filter to specific ARIA role')
343
+ .option('-j, --json', 'Output as JSON')
344
+ .option('-s, --speak', 'Simulate screen reader announcement')
345
+ .option('--no-audio', 'Print announcement but skip TTS playback (use with --speak)')
346
+ .action(async (url, options) => {
347
+ try {
348
+ const depth = parseInt(options.depth, 10);
349
+ if (isNaN(depth) || depth < 1) {
350
+ console.error('Error: --depth must be a positive number');
351
+ process.exit(1);
352
+ }
353
+ await treeCommand(url, {
354
+ depth,
355
+ role: options.role,
356
+ json: options.json,
357
+ speak: options.speak,
358
+ noAudio: !options.audio, // Commander converts --no-audio to audio: false
359
+ });
360
+ }
361
+ catch (error) {
362
+ console.error('Tree failed:', error);
363
+ process.exit(1);
364
+ }
365
+ });
366
+ // ally triage
367
+ program
368
+ .command('triage')
369
+ .description('Interactively categorize and prioritize accessibility issues')
370
+ .option('-i, --input <file>', 'Path to scan results', '.ally/scan.json')
371
+ .action(async (options) => {
372
+ try {
373
+ await triageCommand(options);
374
+ }
375
+ catch (error) {
376
+ console.error('Triage failed:', error);
377
+ process.exit(1);
378
+ }
379
+ });
380
+ // ally pr-check
381
+ program
382
+ .command('pr-check')
383
+ .description('Post accessibility results to GitHub PR')
384
+ .option('-i, --input <file>', 'Path to scan results', '.ally/scan.json')
385
+ .option('-p, --pr <number>', 'PR number (auto-detected in GitHub Actions)')
386
+ .option('--no-comment', 'Skip posting PR comment')
387
+ .option('-F, --fail-on <severities>', 'Fail on specified severities (comma-separated)')
388
+ .action(async (options) => {
389
+ try {
390
+ const prNumber = options.pr ? parseInt(options.pr, 10) : undefined;
391
+ await prCheckCommand({
392
+ ...options,
393
+ pr: prNumber,
394
+ });
395
+ }
396
+ catch (error) {
397
+ console.error('PR check failed:', error);
398
+ process.exit(1);
399
+ }
400
+ });
401
+ // ally completion
402
+ program
403
+ .command('completion [shell]')
404
+ .description('Generate shell completion script (bash, zsh, fish)')
405
+ .action(async (shell) => {
406
+ try {
407
+ await completionCommand(shell);
408
+ }
409
+ catch (error) {
410
+ console.error('Completion failed:', error);
411
+ process.exit(1);
412
+ }
413
+ });
414
+ // ally doctor
415
+ program
416
+ .command('doctor')
417
+ .description('Diagnose installation and configuration issues')
418
+ .action(async () => {
419
+ try {
420
+ await doctorCommand();
421
+ }
422
+ catch (error) {
423
+ console.error('Doctor failed:', error);
424
+ process.exit(1);
425
+ }
426
+ });
427
+ // ally health
428
+ program
429
+ .command('health')
430
+ .description('Quick accessibility health check (like npm audit)')
431
+ .option('-p, --path <path>', 'Path to scan', '.')
432
+ .option('-s, --standard <level>', 'WCAG standard to test against (default: wcag22aa)', (value) => {
433
+ const valid = ['wcag2a', 'wcag2aa', 'wcag2aaa', 'wcag21a', 'wcag21aa', 'wcag21aaa', 'wcag22aa', 'section508', 'best-practice'];
434
+ if (!valid.includes(value)) {
435
+ throw new Error(`Invalid standard: ${value}. Valid options: ${valid.join(', ')}`);
436
+ }
437
+ return value;
438
+ })
439
+ .option('-i, --input <file>', 'Use existing scan results instead of scanning')
440
+ .action(async (options) => {
441
+ try {
442
+ await healthCommand(options);
443
+ }
444
+ catch (error) {
445
+ console.error('Health check failed:', error);
446
+ process.exit(1);
447
+ }
448
+ });
449
+ // ally scan-storybook
450
+ program
451
+ .command('scan-storybook')
452
+ .description('Scan Storybook stories for accessibility issues')
453
+ .option('-u, --url <url>', 'Storybook URL (default: http://localhost:6006)', 'http://localhost:6006')
454
+ .option('-T, --timeout <ms>', 'Page load timeout in milliseconds (default: 10000)', '10000')
455
+ .option('-f, --filter <pattern>', 'Filter stories by name pattern')
456
+ .option('-F, --format <format>', 'Output format (default, json, csv)', 'default')
457
+ .option('-o, --output <dir>', 'Output directory for results', '.ally')
458
+ .option('-s, --standard <level>', 'WCAG standard to test against (default: wcag22aa)', (value) => {
459
+ const valid = ['wcag2a', 'wcag2aa', 'wcag2aaa', 'wcag21a', 'wcag21aa', 'wcag21aaa', 'wcag22aa', 'section508', 'best-practice'];
460
+ if (!valid.includes(value)) {
461
+ throw new Error(`Invalid standard: ${value}. Valid options: ${valid.join(', ')}`);
462
+ }
463
+ return value;
464
+ })
465
+ .action(async (options) => {
466
+ try {
467
+ // Parse timeout if provided
468
+ const timeout = parseInt(options.timeout, 10);
469
+ if (isNaN(timeout) || timeout < 1000) {
470
+ console.error('Error: --timeout must be at least 1000 (1 second)');
471
+ process.exit(1);
472
+ }
473
+ // Validate format
474
+ const validFormats = ['default', 'json', 'csv'];
475
+ if (!validFormats.includes(options.format)) {
476
+ console.error(`Error: --format must be one of: ${validFormats.join(', ')}`);
477
+ process.exit(1);
478
+ }
479
+ await scanStorybookCommand({
480
+ url: options.url,
481
+ timeout,
482
+ filter: options.filter,
483
+ format: options.format,
484
+ output: options.output,
485
+ standard: options.standard,
486
+ });
487
+ }
488
+ catch (error) {
489
+ console.error('Storybook scan failed:', error);
490
+ process.exit(1);
491
+ }
492
+ });
493
+ // ally audit-palette
494
+ program
495
+ .command('audit-palette <file>')
496
+ .description('Audit a design system color palette for contrast issues')
497
+ .option('-f, --format <format>', 'Output format (default, json, csv)', 'default')
498
+ .option('-l, --level <level>', 'Minimum WCAG level to pass (aa, aaa)', 'aa')
499
+ .option('--large-text', 'Use large text thresholds (3:1 for AA)')
500
+ .option('--apca', 'Include APCA contrast values in output')
501
+ .action(async (file, options) => {
502
+ try {
503
+ // Validate level
504
+ const validLevels = ['aa', 'aaa'];
505
+ if (!validLevels.includes(options.level)) {
506
+ console.error(`Error: --level must be one of: ${validLevels.join(', ')}`);
507
+ process.exit(1);
508
+ }
509
+ // Validate format
510
+ const validFormats = ['default', 'json', 'csv'];
511
+ if (!validFormats.includes(options.format)) {
512
+ console.error(`Error: --format must be one of: ${validFormats.join(', ')}`);
513
+ process.exit(1);
514
+ }
515
+ await auditPaletteCommand(file, {
516
+ format: options.format,
517
+ level: options.level,
518
+ largeText: options.largeText,
519
+ apca: options.apca,
520
+ });
521
+ }
522
+ catch (error) {
523
+ console.error('Palette audit failed:', error);
524
+ process.exit(1);
525
+ }
526
+ });
527
+ // Parse and run
528
+ program.parse();
@@ -0,0 +1,18 @@
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
+ type OutputFormat = 'default' | 'json' | 'csv';
10
+ type WcagLevel = 'aa' | 'aaa';
11
+ interface AuditPaletteOptions {
12
+ format?: OutputFormat;
13
+ level?: WcagLevel;
14
+ largeText?: boolean;
15
+ apca?: boolean;
16
+ }
17
+ export declare function auditPaletteCommand(filePath: string, options?: AuditPaletteOptions): Promise<void>;
18
+ export default auditPaletteCommand;