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,635 @@
1
+ /**
2
+ * ally tree command - Visualize accessibility tree for a URL
3
+ */
4
+ import puppeteer from 'puppeteer';
5
+ import chalk from 'chalk';
6
+ import boxen from 'boxen';
7
+ import { spawn } from 'child_process';
8
+ import { platform } from 'os';
9
+ import { printBanner, createSpinner, printError, printWarning, printInfo, printSuccess, } from '../utils/ui.js';
10
+ // Tree drawing characters
11
+ const TREE_CHARS = {
12
+ pipe: '\u2502', // │
13
+ tee: '\u251C', // ├
14
+ elbow: '\u2514', // └
15
+ dash: '\u2500', // ─
16
+ space: ' ',
17
+ };
18
+ // Role categories for coloring and summary
19
+ const LANDMARK_ROLES = new Set([
20
+ 'banner', 'main', 'contentinfo', 'navigation', 'complementary',
21
+ 'region', 'search', 'form', 'application'
22
+ ]);
23
+ const HEADING_ROLES = new Set(['heading']);
24
+ const INTERACTIVE_ROLES = new Set([
25
+ 'link', 'button', 'textbox', 'checkbox', 'radio', 'combobox',
26
+ 'listbox', 'menuitem', 'menuitemcheckbox', 'menuitemradio',
27
+ 'option', 'searchbox', 'slider', 'spinbutton', 'switch', 'tab',
28
+ 'treeitem', 'gridcell'
29
+ ]);
30
+ /**
31
+ * Create an empty summary object
32
+ */
33
+ function createSummary() {
34
+ return {
35
+ landmarks: new Map(),
36
+ headings: new Map(),
37
+ links: 0,
38
+ images: 0,
39
+ forms: 0,
40
+ buttons: 0,
41
+ textboxes: 0,
42
+ lists: 0,
43
+ tables: 0,
44
+ };
45
+ }
46
+ /**
47
+ * Update summary based on node
48
+ */
49
+ function updateSummary(summary, node) {
50
+ const role = node.role;
51
+ if (LANDMARK_ROLES.has(role)) {
52
+ summary.landmarks.set(role, (summary.landmarks.get(role) || 0) + 1);
53
+ }
54
+ if (role === 'heading' && node.level !== undefined) {
55
+ summary.headings.set(node.level, (summary.headings.get(node.level) || 0) + 1);
56
+ }
57
+ switch (role) {
58
+ case 'link':
59
+ summary.links++;
60
+ break;
61
+ case 'img':
62
+ case 'image':
63
+ summary.images++;
64
+ break;
65
+ case 'form':
66
+ summary.forms++;
67
+ break;
68
+ case 'button':
69
+ summary.buttons++;
70
+ break;
71
+ case 'textbox':
72
+ case 'searchbox':
73
+ summary.textboxes++;
74
+ break;
75
+ case 'list':
76
+ summary.lists++;
77
+ break;
78
+ case 'table':
79
+ summary.tables++;
80
+ break;
81
+ }
82
+ }
83
+ /**
84
+ * Colorize role based on category
85
+ */
86
+ function colorizeRole(role) {
87
+ if (LANDMARK_ROLES.has(role)) {
88
+ return chalk.blue(role);
89
+ }
90
+ if (HEADING_ROLES.has(role)) {
91
+ return chalk.yellow(role);
92
+ }
93
+ if (INTERACTIVE_ROLES.has(role)) {
94
+ return chalk.green(role);
95
+ }
96
+ return chalk.cyan(role);
97
+ }
98
+ /**
99
+ * Format node for display (role + optional level + name)
100
+ */
101
+ function formatNodeDisplay(node) {
102
+ let display = colorizeRole(node.role);
103
+ // Add level info for headings
104
+ if (node.role === 'heading' && node.level !== undefined) {
105
+ display += chalk.dim(` (level ${node.level})`);
106
+ }
107
+ // Add name if present
108
+ if (node.name && node.name.trim()) {
109
+ display += ` ${chalk.white(`"${node.name}"`)}`;
110
+ }
111
+ return display;
112
+ }
113
+ /**
114
+ * Format node properties for display (e.g., [level=2], [checked], [disabled])
115
+ */
116
+ function formatProperties(node) {
117
+ const props = [];
118
+ if (node.level !== undefined) {
119
+ props.push(`level=${node.level}`);
120
+ }
121
+ if (node.checked !== undefined) {
122
+ props.push(node.checked === 'mixed' ? 'checked=mixed' : (node.checked ? 'checked' : 'unchecked'));
123
+ }
124
+ if (node.pressed !== undefined) {
125
+ props.push(node.pressed === 'mixed' ? 'pressed=mixed' : (node.pressed ? 'pressed' : 'not pressed'));
126
+ }
127
+ if (node.selected) {
128
+ props.push('selected');
129
+ }
130
+ if (node.expanded !== undefined) {
131
+ props.push(node.expanded ? 'expanded' : 'collapsed');
132
+ }
133
+ if (node.disabled) {
134
+ props.push('disabled');
135
+ }
136
+ if (node.required) {
137
+ props.push('required');
138
+ }
139
+ if (node.invalid) {
140
+ props.push(typeof node.invalid === 'string' ? `invalid=${node.invalid}` : 'invalid');
141
+ }
142
+ if (node.focused) {
143
+ props.push('focused');
144
+ }
145
+ if (node.modal) {
146
+ props.push('modal');
147
+ }
148
+ if (node.readonly) {
149
+ props.push('readonly');
150
+ }
151
+ if (node.value !== undefined) {
152
+ const valueStr = typeof node.value === 'string' ? `"${node.value}"` : String(node.value);
153
+ props.push(`value=${valueStr}`);
154
+ }
155
+ return props.length > 0 ? ` [${props.join(', ')}]` : '';
156
+ }
157
+ /**
158
+ * Print a single tree node with proper formatting
159
+ */
160
+ function printNode(node, prefix, isLast, currentDepth, maxDepth, filterRole) {
161
+ // Check role filter
162
+ if (filterRole && node.role !== filterRole) {
163
+ // Still process children to find matching roles
164
+ let count = 0;
165
+ if (node.children && currentDepth < maxDepth) {
166
+ const childCount = node.children.length;
167
+ node.children.forEach((child, index) => {
168
+ count += printNode(child, prefix, index === childCount - 1, currentDepth + 1, maxDepth, filterRole);
169
+ });
170
+ }
171
+ return count;
172
+ }
173
+ // Build the tree prefix
174
+ const connector = isLast
175
+ ? `${TREE_CHARS.elbow}${TREE_CHARS.dash}${TREE_CHARS.dash} `
176
+ : `${TREE_CHARS.tee}${TREE_CHARS.dash}${TREE_CHARS.dash} `;
177
+ // Format the node
178
+ const role = chalk.cyan(node.role);
179
+ const name = node.name ? chalk.white(` "${node.name}"`) : '';
180
+ const props = chalk.gray(formatProperties(node));
181
+ // Print the node
182
+ if (currentDepth === 0) {
183
+ console.log(`${role}${name}${props}`);
184
+ }
185
+ else {
186
+ console.log(`${prefix}${connector}${role}${name}${props}`);
187
+ }
188
+ let nodeCount = 1;
189
+ // Process children
190
+ if (node.children && node.children.length > 0) {
191
+ if (currentDepth >= maxDepth) {
192
+ // Indicate there are more children but we're at max depth
193
+ const childPrefix = isLast
194
+ ? `${prefix} `
195
+ : `${prefix}${TREE_CHARS.pipe} `;
196
+ console.log(`${childPrefix}${chalk.gray(`... ${node.children.length} more children`)}`);
197
+ }
198
+ else {
199
+ const childPrefix = isLast
200
+ ? `${prefix} `
201
+ : `${prefix}${TREE_CHARS.pipe} `;
202
+ const childCount = node.children.length;
203
+ node.children.forEach((child, index) => {
204
+ nodeCount += printNode(child, childPrefix, index === childCount - 1, currentDepth + 1, maxDepth, filterRole);
205
+ });
206
+ }
207
+ }
208
+ return nodeCount;
209
+ }
210
+ /**
211
+ * Count total nodes in the tree
212
+ */
213
+ function countNodes(node) {
214
+ let count = 1;
215
+ if (node.children) {
216
+ for (const child of node.children) {
217
+ count += countNodes(child);
218
+ }
219
+ }
220
+ return count;
221
+ }
222
+ /**
223
+ * Find the maximum depth of the tree
224
+ */
225
+ function getMaxTreeDepth(node, currentDepth = 0) {
226
+ if (!node.children || node.children.length === 0) {
227
+ return currentDepth;
228
+ }
229
+ let maxChildDepth = currentDepth;
230
+ for (const child of node.children) {
231
+ const childDepth = getMaxTreeDepth(child, currentDepth + 1);
232
+ if (childDepth > maxChildDepth) {
233
+ maxChildDepth = childDepth;
234
+ }
235
+ }
236
+ return maxChildDepth;
237
+ }
238
+ /**
239
+ * Filter tree to only include nodes with a specific role
240
+ */
241
+ function filterByRole(node, role) {
242
+ const matches = [];
243
+ if (node.role === role) {
244
+ matches.push(node);
245
+ }
246
+ if (node.children) {
247
+ for (const child of node.children) {
248
+ matches.push(...filterByRole(child, role));
249
+ }
250
+ }
251
+ return matches;
252
+ }
253
+ /**
254
+ * Generate a screen reader announcement for a node
255
+ */
256
+ function announceNode(node) {
257
+ const role = node.role;
258
+ const name = node.name?.trim() || '';
259
+ // Skip certain roles that don't typically announce
260
+ const skipRoles = new Set(['none', 'generic', 'RootWebArea', 'InlineTextBox', 'StaticText']);
261
+ if (skipRoles.has(role)) {
262
+ // For static text, just return the name
263
+ if (role === 'StaticText' && name) {
264
+ return name;
265
+ }
266
+ return '';
267
+ }
268
+ // Handle special cases
269
+ switch (role) {
270
+ case 'heading':
271
+ if (node.level !== undefined) {
272
+ return name ? `${name}, heading level ${node.level}` : `heading level ${node.level}`;
273
+ }
274
+ return name ? `${name}, heading` : 'heading';
275
+ case 'link':
276
+ return name ? `${name}, link` : 'link';
277
+ case 'button':
278
+ return name ? `${name}, button` : 'button';
279
+ case 'img':
280
+ case 'image':
281
+ return name ? `${name}, image` : 'image';
282
+ case 'navigation':
283
+ return name ? `${name}, navigation` : 'navigation';
284
+ case 'main':
285
+ return name ? `${name}, main` : 'main';
286
+ case 'banner':
287
+ return name ? `${name}, banner` : 'banner';
288
+ case 'contentinfo':
289
+ return name ? `${name}, content info` : 'content info';
290
+ case 'complementary':
291
+ return name ? `${name}, complementary` : 'complementary';
292
+ case 'region':
293
+ return name ? `${name}, region` : 'region';
294
+ case 'search':
295
+ return name ? `${name}, search` : 'search';
296
+ case 'form':
297
+ return name ? `${name}, form` : 'form';
298
+ case 'list':
299
+ const itemCount = node.children?.filter(c => c.role === 'listitem').length || 0;
300
+ if (itemCount > 0) {
301
+ return `list with ${itemCount} item${itemCount === 1 ? '' : 's'}`;
302
+ }
303
+ return 'list';
304
+ case 'listitem':
305
+ // List items typically announce their content, not themselves
306
+ return '';
307
+ case 'textbox':
308
+ case 'searchbox':
309
+ const textboxLabel = name ? `${name}, ` : '';
310
+ const textboxType = role === 'searchbox' ? 'search edit' : 'edit';
311
+ return `${textboxLabel}${textboxType}`;
312
+ case 'checkbox':
313
+ const checkState = node.checked ? 'checked' : 'not checked';
314
+ return name ? `${name}, checkbox, ${checkState}` : `checkbox, ${checkState}`;
315
+ case 'radio':
316
+ const radioState = node.checked ? 'selected' : 'not selected';
317
+ return name ? `${name}, radio button, ${radioState}` : `radio button, ${radioState}`;
318
+ case 'combobox':
319
+ return name ? `${name}, combo box` : 'combo box';
320
+ case 'menubar':
321
+ return name ? `${name}, menu bar` : 'menu bar';
322
+ case 'menu':
323
+ return name ? `${name}, menu` : 'menu';
324
+ case 'menuitem':
325
+ return name ? `${name}, menu item` : 'menu item';
326
+ case 'tab':
327
+ const tabState = node.selected ? 'selected' : '';
328
+ return name
329
+ ? `${name}, tab${tabState ? ', ' + tabState : ''}`
330
+ : `tab${tabState ? ', ' + tabState : ''}`;
331
+ case 'tablist':
332
+ return name ? `${name}, tab list` : 'tab list';
333
+ case 'tabpanel':
334
+ return name ? `${name}, tab panel` : 'tab panel';
335
+ case 'table':
336
+ return name ? `${name}, table` : 'table';
337
+ case 'row':
338
+ return ''; // Rows don't typically announce themselves
339
+ case 'cell':
340
+ case 'gridcell':
341
+ return name || '';
342
+ case 'columnheader':
343
+ return name ? `${name}, column header` : 'column header';
344
+ case 'rowheader':
345
+ return name ? `${name}, row header` : 'row header';
346
+ case 'slider':
347
+ const sliderValue = node.valuetext || (node.value !== undefined ? String(node.value) : '');
348
+ return name
349
+ ? `${name}, slider${sliderValue ? ', ' + sliderValue : ''}`
350
+ : `slider${sliderValue ? ', ' + sliderValue : ''}`;
351
+ case 'spinbutton':
352
+ const spinValue = node.valuetext || (node.value !== undefined ? String(node.value) : '');
353
+ return name
354
+ ? `${name}, spin button${spinValue ? ', ' + spinValue : ''}`
355
+ : `spin button${spinValue ? ', ' + spinValue : ''}`;
356
+ case 'switch':
357
+ const switchState = node.checked ? 'on' : 'off';
358
+ return name ? `${name}, switch, ${switchState}` : `switch, ${switchState}`;
359
+ case 'progressbar':
360
+ const progress = node.valuetext || (node.value !== undefined ? `${node.value}%` : '');
361
+ return name
362
+ ? `${name}, progress${progress ? ', ' + progress : ''}`
363
+ : `progress${progress ? ', ' + progress : ''}`;
364
+ case 'dialog':
365
+ return name ? `${name}, dialog` : 'dialog';
366
+ case 'alert':
367
+ return name ? `alert, ${name}` : 'alert';
368
+ case 'alertdialog':
369
+ return name ? `alert dialog, ${name}` : 'alert dialog';
370
+ case 'application':
371
+ return name ? `${name}, application` : 'application';
372
+ case 'article':
373
+ return name ? `${name}, article` : 'article';
374
+ case 'figure':
375
+ return name ? `${name}, figure` : 'figure';
376
+ case 'group':
377
+ return name ? `${name}, group` : '';
378
+ case 'separator':
379
+ return 'separator';
380
+ case 'tooltip':
381
+ return name ? `tooltip, ${name}` : 'tooltip';
382
+ case 'tree':
383
+ return name ? `${name}, tree` : 'tree';
384
+ case 'treeitem':
385
+ const expanded = node.expanded !== undefined
386
+ ? (node.expanded ? ', expanded' : ', collapsed')
387
+ : '';
388
+ return name ? `${name}, tree item${expanded}` : `tree item${expanded}`;
389
+ default:
390
+ // For other roles, announce name and role if both exist
391
+ if (name) {
392
+ return `${name}, ${role}`;
393
+ }
394
+ return '';
395
+ }
396
+ }
397
+ /**
398
+ * Walk the accessibility tree and generate announcement text
399
+ */
400
+ function generateAnnouncement(node, depth = 0) {
401
+ const announcements = [];
402
+ // Get announcement for current node
403
+ const announcement = announceNode(node);
404
+ if (announcement) {
405
+ announcements.push(announcement);
406
+ }
407
+ // Process children
408
+ if (node.children) {
409
+ for (const child of node.children) {
410
+ const childAnnouncements = generateAnnouncement(child, depth + 1);
411
+ announcements.push(...childAnnouncements);
412
+ }
413
+ }
414
+ return announcements;
415
+ }
416
+ /**
417
+ * Speak text using system TTS
418
+ */
419
+ async function speakText(text) {
420
+ return new Promise((resolve, reject) => {
421
+ const os = platform();
422
+ let command;
423
+ let args;
424
+ switch (os) {
425
+ case 'darwin':
426
+ // macOS: use 'say' command
427
+ command = 'say';
428
+ args = ['-r', '180', text]; // -r 180 sets a reasonable speaking rate
429
+ break;
430
+ case 'linux':
431
+ // Linux: try espeak first, then spd-say
432
+ // We'll try espeak as it's more commonly available
433
+ command = 'espeak';
434
+ args = ['-s', '150', text]; // -s 150 sets words per minute
435
+ break;
436
+ case 'win32':
437
+ // Windows: use PowerShell TTS
438
+ command = 'powershell';
439
+ args = [
440
+ '-Command',
441
+ `Add-Type -AssemblyName System.Speech; (New-Object System.Speech.Synthesis.SpeechSynthesizer).Speak('${text.replace(/'/g, "''")}')`
442
+ ];
443
+ break;
444
+ default:
445
+ printWarning(`Text-to-speech not supported on ${os}`);
446
+ resolve();
447
+ return;
448
+ }
449
+ const process = spawn(command, args, { stdio: 'inherit' });
450
+ process.on('close', (code) => {
451
+ if (code === 0) {
452
+ resolve();
453
+ }
454
+ else if (os === 'linux') {
455
+ // Try spd-say as fallback on Linux
456
+ const fallback = spawn('spd-say', ['-r', '-20', text], { stdio: 'inherit' });
457
+ fallback.on('close', (fallbackCode) => {
458
+ if (fallbackCode === 0) {
459
+ resolve();
460
+ }
461
+ else {
462
+ printWarning('TTS not available. Install espeak or speech-dispatcher.');
463
+ resolve();
464
+ }
465
+ });
466
+ fallback.on('error', () => {
467
+ printWarning('TTS not available. Install espeak or speech-dispatcher.');
468
+ resolve();
469
+ });
470
+ }
471
+ else {
472
+ printWarning(`TTS command failed with code ${code}`);
473
+ resolve();
474
+ }
475
+ });
476
+ process.on('error', (err) => {
477
+ if (os === 'linux') {
478
+ // Try spd-say as fallback on Linux
479
+ const fallback = spawn('spd-say', ['-r', '-20', text], { stdio: 'inherit' });
480
+ fallback.on('close', (fallbackCode) => {
481
+ if (fallbackCode === 0) {
482
+ resolve();
483
+ }
484
+ else {
485
+ printWarning('TTS not available. Install espeak or speech-dispatcher.');
486
+ resolve();
487
+ }
488
+ });
489
+ fallback.on('error', () => {
490
+ printWarning('TTS not available. Install espeak or speech-dispatcher.');
491
+ resolve();
492
+ });
493
+ }
494
+ else {
495
+ printWarning(`TTS not available: ${err.message}`);
496
+ resolve();
497
+ }
498
+ });
499
+ });
500
+ }
501
+ export async function treeCommand(url, options = {}) {
502
+ printBanner();
503
+ const { depth = 5, role, json = false, speak = false, noAudio = false } = options;
504
+ // Validate URL
505
+ let targetUrl = url;
506
+ if (!targetUrl.startsWith('http://') && !targetUrl.startsWith('https://')) {
507
+ targetUrl = `https://${targetUrl}`;
508
+ }
509
+ const spinner = createSpinner(`Fetching accessibility tree from ${targetUrl}...`);
510
+ spinner.start();
511
+ let browser = null;
512
+ try {
513
+ browser = await puppeteer.launch({
514
+ headless: true,
515
+ args: ['--no-sandbox', '--disable-setuid-sandbox'],
516
+ });
517
+ const page = await browser.newPage();
518
+ await page.goto(targetUrl, { waitUntil: 'networkidle2', timeout: 30000 });
519
+ // Get the accessibility tree
520
+ const snapshot = await page.accessibility.snapshot({
521
+ interestingOnly: false, // Get all nodes, not just interesting ones
522
+ });
523
+ await page.close();
524
+ if (!snapshot) {
525
+ spinner.fail('Failed to get accessibility tree');
526
+ printError('The page may not have any accessible content.');
527
+ return;
528
+ }
529
+ spinner.succeed('Accessibility tree retrieved');
530
+ // Calculate tree stats
531
+ const totalNodes = countNodes(snapshot);
532
+ const actualDepth = getMaxTreeDepth(snapshot);
533
+ // JSON output mode
534
+ if (json) {
535
+ if (role) {
536
+ const filtered = filterByRole(snapshot, role);
537
+ console.log(JSON.stringify(filtered, null, 2));
538
+ }
539
+ else {
540
+ console.log(JSON.stringify(snapshot, null, 2));
541
+ }
542
+ return;
543
+ }
544
+ // Print header
545
+ console.log();
546
+ console.log(boxen(`Accessibility Tree for ${chalk.cyan(targetUrl)}\n${chalk.dim(`${totalNodes} nodes, depth: ${actualDepth}`)}`, {
547
+ padding: { top: 0, bottom: 0, left: 1, right: 1 },
548
+ borderStyle: 'round',
549
+ borderColor: 'cyan',
550
+ }));
551
+ console.log();
552
+ // Show warning if tree is deep
553
+ if (actualDepth > depth) {
554
+ printWarning(`Tree depth (${actualDepth}) exceeds display limit (${depth}). Use --depth to show more.`);
555
+ console.log();
556
+ }
557
+ // Filter mode
558
+ if (role) {
559
+ const matches = filterByRole(snapshot, role);
560
+ if (matches.length === 0) {
561
+ printInfo(`No nodes found with role "${role}"`);
562
+ return;
563
+ }
564
+ console.log(chalk.dim(`Found ${matches.length} node(s) with role "${role}":\n`));
565
+ for (const match of matches) {
566
+ printNode(match, '', true, 0, depth);
567
+ console.log();
568
+ }
569
+ }
570
+ else {
571
+ // Print full tree
572
+ printNode(snapshot, '', true, 0, depth);
573
+ }
574
+ console.log();
575
+ // Print legend
576
+ console.log(chalk.dim('Legend:'));
577
+ console.log(chalk.dim(` ${chalk.cyan('role')} ${chalk.white('"name"')} ${chalk.gray('[properties]')}`));
578
+ console.log();
579
+ // Screen reader simulation
580
+ if (speak) {
581
+ // Generate announcement from the tree
582
+ const treeToAnnounce = role
583
+ ? filterByRole(snapshot, role)
584
+ : [snapshot];
585
+ const allAnnouncements = [];
586
+ for (const tree of treeToAnnounce) {
587
+ const announcements = generateAnnouncement(tree);
588
+ allAnnouncements.push(...announcements);
589
+ }
590
+ // Join announcements into readable text
591
+ const announcementText = allAnnouncements.join('. ').replace(/\.\./g, '.');
592
+ // Print the announcement
593
+ console.log(chalk.yellow.bold('Screen Reader Announcement:'));
594
+ console.log(boxen(chalk.white(`"${announcementText}"`), {
595
+ padding: { top: 0, bottom: 0, left: 1, right: 1 },
596
+ borderStyle: 'round',
597
+ borderColor: 'yellow',
598
+ }));
599
+ console.log();
600
+ // Play the announcement if audio is enabled
601
+ if (!noAudio) {
602
+ printInfo('Playing announcement...');
603
+ await speakText(announcementText);
604
+ printSuccess('Announcement complete.');
605
+ console.log();
606
+ }
607
+ }
608
+ }
609
+ catch (error) {
610
+ spinner.fail(`Failed to fetch accessibility tree`);
611
+ if (error instanceof Error) {
612
+ if (error.message.includes('net::ERR_NAME_NOT_RESOLVED')) {
613
+ printError(`Could not resolve hostname: ${targetUrl}`);
614
+ }
615
+ else if (error.message.includes('net::ERR_CONNECTION_REFUSED')) {
616
+ printError(`Connection refused: ${targetUrl}`);
617
+ }
618
+ else if (error.message.includes('Timeout')) {
619
+ printError(`Request timed out for: ${targetUrl}`);
620
+ }
621
+ else {
622
+ printError(error.message);
623
+ }
624
+ }
625
+ else {
626
+ printError(String(error));
627
+ }
628
+ }
629
+ finally {
630
+ if (browser) {
631
+ await browser.close();
632
+ }
633
+ }
634
+ }
635
+ export default treeCommand;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * ally triage command - Interactive issue categorization and prioritization
3
+ */
4
+ export interface TriageResult {
5
+ fix: string[];
6
+ ignore: string[];
7
+ backlog: string[];
8
+ }
9
+ interface TriageOptions {
10
+ input?: string;
11
+ }
12
+ export declare function triageCommand(options?: TriageOptions): Promise<void>;
13
+ export default triageCommand;