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,200 @@
1
+ /**
2
+ * Format converters for accessibility reports
3
+ * Converts AllyReport to various output formats (SARIF, JUnit, CSV)
4
+ */
5
+ import { relative } from 'path';
6
+ /**
7
+ * Map axe-core severity to SARIF level
8
+ */
9
+ function severityToSarifLevel(severity) {
10
+ switch (severity) {
11
+ case 'critical':
12
+ case 'serious':
13
+ return 'error';
14
+ case 'moderate':
15
+ return 'warning';
16
+ case 'minor':
17
+ default:
18
+ return 'note';
19
+ }
20
+ }
21
+ /**
22
+ * Convert AllyReport to SARIF 2.1.0 format for GitHub Code Scanning integration
23
+ */
24
+ export function convertToSarif(report) {
25
+ // Collect unique rules from all violations
26
+ const ruleMap = new Map();
27
+ for (const result of report.results) {
28
+ for (const violation of result.violations) {
29
+ if (!ruleMap.has(violation.id)) {
30
+ ruleMap.set(violation.id, { violation, severity: violation.impact });
31
+ }
32
+ }
33
+ }
34
+ // Build rules array
35
+ const rules = [];
36
+ const ruleIndexMap = new Map();
37
+ Array.from(ruleMap.entries()).forEach(([ruleId, { violation, severity }]) => {
38
+ ruleIndexMap.set(ruleId, rules.length);
39
+ rules.push({
40
+ id: ruleId,
41
+ name: ruleId,
42
+ shortDescription: { text: violation.help },
43
+ fullDescription: { text: violation.description },
44
+ helpUri: violation.helpUrl,
45
+ defaultConfiguration: {
46
+ level: severityToSarifLevel(severity),
47
+ },
48
+ properties: {
49
+ tags: violation.tags,
50
+ },
51
+ });
52
+ });
53
+ // Build results array
54
+ const results = [];
55
+ for (const scanResult of report.results) {
56
+ const fileUri = scanResult.file
57
+ ? relative(process.cwd(), scanResult.file)
58
+ : scanResult.url;
59
+ for (const violation of scanResult.violations) {
60
+ const ruleIndex = ruleIndexMap.get(violation.id) ?? 0;
61
+ // Create a result for each affected node
62
+ for (const node of violation.nodes) {
63
+ const locations = [
64
+ {
65
+ physicalLocation: {
66
+ artifactLocation: {
67
+ uri: fileUri,
68
+ uriBaseId: '%SRCROOT%',
69
+ },
70
+ region: {
71
+ startLine: 1, // Line info not available from axe-core
72
+ snippet: { text: node.html },
73
+ },
74
+ },
75
+ },
76
+ ];
77
+ results.push({
78
+ ruleId: violation.id,
79
+ ruleIndex,
80
+ level: severityToSarifLevel(violation.impact),
81
+ message: {
82
+ text: `${violation.help}. ${node.failureSummary}`,
83
+ },
84
+ locations,
85
+ });
86
+ }
87
+ }
88
+ }
89
+ return {
90
+ $schema: 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json',
91
+ version: '2.1.0',
92
+ runs: [
93
+ {
94
+ tool: {
95
+ driver: {
96
+ name: 'ally',
97
+ version: '1.0.0',
98
+ informationUri: 'https://github.com/forbiddenlink/ally',
99
+ rules,
100
+ },
101
+ },
102
+ results,
103
+ },
104
+ ],
105
+ };
106
+ }
107
+ /**
108
+ * Escape XML special characters
109
+ */
110
+ function escapeXml(str) {
111
+ return str
112
+ .replace(/&/g, '&')
113
+ .replace(/</g, '&lt;')
114
+ .replace(/>/g, '&gt;')
115
+ .replace(/"/g, '&quot;')
116
+ .replace(/'/g, '&apos;');
117
+ }
118
+ /**
119
+ * Convert AllyReport to JUnit XML format for CI/CD tools (Jenkins, GitLab, etc.)
120
+ * Each violation is represented as a test failure
121
+ */
122
+ export function convertToJunit(report) {
123
+ // Count total tests (violations) and failures
124
+ let totalTests = 0;
125
+ let totalFailures = 0;
126
+ // Collect all violations with their file context
127
+ const testcases = [];
128
+ for (const result of report.results) {
129
+ const fileUri = result.file
130
+ ? relative(process.cwd(), result.file)
131
+ : result.url || 'unknown';
132
+ for (const violation of result.violations) {
133
+ for (const node of violation.nodes) {
134
+ totalTests++;
135
+ totalFailures++;
136
+ const wcagTags = violation.tags.filter(t => t.startsWith('wcag')).join(', ');
137
+ const failureMessage = escapeXml(`${violation.help}. ${node.failureSummary || ''}`);
138
+ const failureDetails = escapeXml(`File: ${fileUri}\n` +
139
+ `Selector: ${node.target.join(' > ')}\n` +
140
+ `HTML: ${node.html}\n` +
141
+ `WCAG: ${wcagTags}\n` +
142
+ `Help: ${violation.helpUrl}`);
143
+ testcases.push(` <testcase name="${escapeXml(violation.id)}" classname="${escapeXml(violation.impact)}" time="0">\n` +
144
+ ` <failure message="${failureMessage}" type="${escapeXml(violation.impact)}">\n` +
145
+ `${failureDetails}\n` +
146
+ ` </failure>\n` +
147
+ ` </testcase>`);
148
+ }
149
+ }
150
+ }
151
+ // Build the JUnit XML
152
+ const xml = [
153
+ '<?xml version="1.0" encoding="UTF-8"?>',
154
+ `<testsuites name="ally-a11y" tests="${totalTests}" failures="${totalFailures}" errors="0" time="0">`,
155
+ ` <testsuite name="accessibility" tests="${totalTests}" failures="${totalFailures}" errors="0" skipped="0" time="0">`,
156
+ ...testcases,
157
+ ' </testsuite>',
158
+ '</testsuites>',
159
+ ].join('\n');
160
+ return xml;
161
+ }
162
+ /**
163
+ * Escape a value for CSV output
164
+ */
165
+ function escapeCsv(value) {
166
+ // If value contains comma, quote, or newline, wrap in quotes and escape internal quotes
167
+ if (value.includes(',') || value.includes('"') || value.includes('\n') || value.includes('\r')) {
168
+ return '"' + value.replace(/"/g, '""') + '"';
169
+ }
170
+ return value;
171
+ }
172
+ /**
173
+ * Convert AllyReport to CSV format
174
+ * Headers: file,violation_id,impact,description,selector,wcag,help_url
175
+ */
176
+ export function convertToCsv(report) {
177
+ const headers = ['file', 'violation_id', 'impact', 'description', 'selector', 'wcag', 'help_url'];
178
+ const rows = [headers.join(',')];
179
+ for (const result of report.results) {
180
+ const fileUri = result.file
181
+ ? relative(process.cwd(), result.file)
182
+ : result.url || 'unknown';
183
+ for (const violation of result.violations) {
184
+ const wcagTags = violation.tags.filter(t => t.startsWith('wcag')).join('; ');
185
+ for (const node of violation.nodes) {
186
+ const row = [
187
+ escapeCsv(fileUri),
188
+ escapeCsv(violation.id),
189
+ escapeCsv(violation.impact),
190
+ escapeCsv(violation.help),
191
+ escapeCsv(node.target.join(' > ')),
192
+ escapeCsv(wcagTags),
193
+ escapeCsv(violation.helpUrl),
194
+ ];
195
+ rows.push(row.join(','));
196
+ }
197
+ }
198
+ }
199
+ return rows.join('\n');
200
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * GitHub Copilot CLI integration utilities
3
+ */
4
+ export interface CopilotConfig {
5
+ available: boolean;
6
+ command: string;
7
+ version?: string;
8
+ }
9
+ /**
10
+ * Check if GitHub Copilot CLI is installed and available
11
+ */
12
+ export declare function checkCopilotCli(): CopilotConfig;
13
+ /**
14
+ * Generate a Copilot CLI prompt for fixing an accessibility issue
15
+ */
16
+ export declare function generateFixPrompt(file: string, issue: string, html: string, suggestedFix?: string): string;
17
+ /**
18
+ * Invoke Copilot CLI to fix an issue (with approval flow)
19
+ */
20
+ export declare function invokeCopilotFix(file: string, prompt: string, options?: {
21
+ allowEdits?: boolean;
22
+ }): Promise<{
23
+ success: boolean;
24
+ output: string;
25
+ }>;
26
+ /**
27
+ * Print Copilot CLI installation instructions
28
+ */
29
+ export declare function printCopilotInstructions(): void;
30
+ /**
31
+ * Check if MCP server is configured
32
+ */
33
+ export declare function checkMcpConfig(): {
34
+ configured: boolean;
35
+ path: string;
36
+ };
@@ -0,0 +1,139 @@
1
+ /**
2
+ * GitHub Copilot CLI integration utilities
3
+ */
4
+ import { spawn, execSync } from 'child_process';
5
+ import { existsSync } from 'fs';
6
+ import { resolve } from 'path';
7
+ import { homedir } from 'os';
8
+ import chalk from 'chalk';
9
+ /**
10
+ * Check if GitHub Copilot CLI is installed and available
11
+ */
12
+ export function checkCopilotCli() {
13
+ const commands = ['copilot', 'gh copilot'];
14
+ for (const cmd of commands) {
15
+ try {
16
+ const result = execSync(`${cmd} --version 2>/dev/null`, {
17
+ encoding: 'utf-8',
18
+ timeout: 5000,
19
+ });
20
+ return {
21
+ available: true,
22
+ command: cmd,
23
+ version: result.trim(),
24
+ };
25
+ }
26
+ catch {
27
+ // Command not found, try next
28
+ }
29
+ }
30
+ return {
31
+ available: false,
32
+ command: '',
33
+ };
34
+ }
35
+ /**
36
+ * Generate a Copilot CLI prompt for fixing an accessibility issue
37
+ */
38
+ export function generateFixPrompt(file, issue, html, suggestedFix) {
39
+ let prompt = `Fix this accessibility issue in ${file}:
40
+
41
+ Issue: ${issue}
42
+
43
+ Current code:
44
+ ${html}
45
+ `;
46
+ if (suggestedFix) {
47
+ prompt += `
48
+ Suggested fix pattern:
49
+ ${suggestedFix}
50
+ `;
51
+ }
52
+ prompt += `
53
+ Requirements:
54
+ - Make the minimal change needed to fix the accessibility issue
55
+ - Preserve existing functionality and styling
56
+ - Follow WCAG 2.1 AA guidelines
57
+ - Use semantic HTML where possible
58
+ `;
59
+ return prompt;
60
+ }
61
+ /**
62
+ * Invoke Copilot CLI to fix an issue (with approval flow)
63
+ */
64
+ export async function invokeCopilotFix(file, prompt, options = {}) {
65
+ const config = checkCopilotCli();
66
+ if (!config.available) {
67
+ return {
68
+ success: false,
69
+ output: 'GitHub Copilot CLI is not installed. Install it with: npm install -g @github/copilot-cli',
70
+ };
71
+ }
72
+ return new Promise((resolve) => {
73
+ const args = ['-p', prompt];
74
+ if (options.allowEdits) {
75
+ args.unshift('--allow-edits');
76
+ }
77
+ // Split command if it contains spaces (e.g., "gh copilot")
78
+ const cmdParts = config.command.split(' ');
79
+ const cmd = cmdParts[0];
80
+ const cmdArgs = [...cmdParts.slice(1), ...args];
81
+ const copilot = spawn(cmd, cmdArgs, {
82
+ stdio: ['inherit', 'pipe', 'pipe'],
83
+ });
84
+ let stdout = '';
85
+ let stderr = '';
86
+ copilot.stdout?.on('data', (data) => {
87
+ stdout += data.toString();
88
+ process.stdout.write(data);
89
+ });
90
+ copilot.stderr?.on('data', (data) => {
91
+ stderr += data.toString();
92
+ process.stderr.write(data);
93
+ });
94
+ copilot.on('close', (code) => {
95
+ resolve({
96
+ success: code === 0,
97
+ output: stdout || stderr,
98
+ });
99
+ });
100
+ copilot.on('error', (error) => {
101
+ resolve({
102
+ success: false,
103
+ output: error.message,
104
+ });
105
+ });
106
+ });
107
+ }
108
+ /**
109
+ * Print Copilot CLI installation instructions
110
+ */
111
+ export function printCopilotInstructions() {
112
+ console.log();
113
+ console.log(chalk.yellow('GitHub Copilot CLI not detected.'));
114
+ console.log();
115
+ console.log(chalk.bold('To enable AI-powered fixes, install Copilot CLI:'));
116
+ console.log();
117
+ console.log(chalk.cyan(' npm install -g @github/copilot-cli'));
118
+ console.log(chalk.cyan(' copilot auth login'));
119
+ console.log();
120
+ console.log(chalk.dim('Once installed, run `ally fix` again for AI-assisted fixes.'));
121
+ console.log(chalk.dim('For now, showing manual fix suggestions below.'));
122
+ console.log();
123
+ }
124
+ /**
125
+ * Check if MCP server is configured
126
+ */
127
+ export function checkMcpConfig() {
128
+ const home = homedir();
129
+ const configPaths = [
130
+ '.copilot/mcp-config.json',
131
+ ...(home ? [resolve(home, '.copilot/mcp-config.json')] : []),
132
+ ];
133
+ for (const configPath of configPaths) {
134
+ if (existsSync(configPath)) {
135
+ return { configured: true, path: configPath };
136
+ }
137
+ }
138
+ return { configured: false, path: '' };
139
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Smart project detection for zero-config scanning
3
+ */
4
+ export type ProjectType = 'next' | 'nuxt' | 'react' | 'vue' | 'svelte' | 'angular' | 'html' | 'unknown';
5
+ export interface ProjectInfo {
6
+ type: ProjectType;
7
+ srcDir: string;
8
+ publicDir: string;
9
+ patterns: string[];
10
+ }
11
+ /**
12
+ * Detect the project type and return appropriate scan patterns
13
+ */
14
+ export declare function detectProject(cwd: string): Promise<ProjectInfo>;
15
+ /**
16
+ * Count files matching the detected patterns
17
+ */
18
+ export declare function countProjectFiles(cwd: string, patterns: string[]): Promise<number>;
19
+ /**
20
+ * Get a human-readable description of the project type
21
+ */
22
+ export declare function getProjectDescription(type: ProjectType): string;
@@ -0,0 +1,197 @@
1
+ /**
2
+ * Smart project detection for zero-config scanning
3
+ */
4
+ import { readFile } from 'fs/promises';
5
+ import { existsSync } from 'fs';
6
+ import { resolve, join } from 'path';
7
+ import { glob } from 'glob';
8
+ /**
9
+ * Detect the project type and return appropriate scan patterns
10
+ */
11
+ export async function detectProject(cwd) {
12
+ const packageJsonPath = resolve(cwd, 'package.json');
13
+ // Try to detect from package.json first
14
+ if (existsSync(packageJsonPath)) {
15
+ try {
16
+ const content = await readFile(packageJsonPath, 'utf-8');
17
+ const pkg = JSON.parse(content);
18
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
19
+ // Check for frameworks in priority order
20
+ if ('next' in allDeps) {
21
+ return detectNextProject(cwd);
22
+ }
23
+ if ('nuxt' in allDeps) {
24
+ return detectNuxtProject(cwd);
25
+ }
26
+ if ('@angular/core' in allDeps) {
27
+ return {
28
+ type: 'angular',
29
+ srcDir: 'src',
30
+ publicDir: 'public',
31
+ patterns: ['src/**/*.html', 'src/**/*.component.html'],
32
+ };
33
+ }
34
+ if ('svelte' in allDeps || '@sveltejs/kit' in allDeps) {
35
+ return {
36
+ type: 'svelte',
37
+ srcDir: 'src',
38
+ publicDir: 'static',
39
+ patterns: ['src/**/*.svelte'],
40
+ };
41
+ }
42
+ if ('vue' in allDeps) {
43
+ return {
44
+ type: 'vue',
45
+ srcDir: 'src',
46
+ publicDir: 'public',
47
+ patterns: ['src/**/*.vue'],
48
+ };
49
+ }
50
+ if ('react' in allDeps || 'react-dom' in allDeps) {
51
+ return {
52
+ type: 'react',
53
+ srcDir: 'src',
54
+ publicDir: 'public',
55
+ patterns: ['src/**/*.tsx', 'src/**/*.jsx'],
56
+ };
57
+ }
58
+ }
59
+ catch {
60
+ // Failed to parse package.json, fall through to directory detection
61
+ }
62
+ }
63
+ // No package.json or no recognized framework - check for common directories
64
+ return await detectStaticSite(cwd);
65
+ }
66
+ /**
67
+ * Detect Next.js project structure
68
+ */
69
+ async function detectNextProject(cwd) {
70
+ // Check for app router (Next.js 13+)
71
+ const hasAppDir = existsSync(join(cwd, 'app'));
72
+ const hasSrcAppDir = existsSync(join(cwd, 'src', 'app'));
73
+ // Check for pages router
74
+ const hasPagesDir = existsSync(join(cwd, 'pages'));
75
+ const hasSrcPagesDir = existsSync(join(cwd, 'src', 'pages'));
76
+ const patterns = [];
77
+ let srcDir = '.';
78
+ if (hasSrcAppDir || hasSrcPagesDir) {
79
+ srcDir = 'src';
80
+ }
81
+ if (hasAppDir || hasSrcAppDir) {
82
+ const appPath = hasSrcAppDir ? 'src/app' : 'app';
83
+ patterns.push(`${appPath}/**/*.tsx`, `${appPath}/**/*.jsx`);
84
+ }
85
+ if (hasPagesDir || hasSrcPagesDir) {
86
+ const pagesPath = hasSrcPagesDir ? 'src/pages' : 'pages';
87
+ patterns.push(`${pagesPath}/**/*.tsx`, `${pagesPath}/**/*.jsx`);
88
+ }
89
+ // Also scan components directory if it exists
90
+ if (existsSync(join(cwd, 'components'))) {
91
+ patterns.push('components/**/*.tsx', 'components/**/*.jsx');
92
+ }
93
+ else if (existsSync(join(cwd, 'src', 'components'))) {
94
+ patterns.push('src/components/**/*.tsx', 'src/components/**/*.jsx');
95
+ }
96
+ // Include public HTML files
97
+ patterns.push('public/**/*.html');
98
+ return {
99
+ type: 'next',
100
+ srcDir,
101
+ publicDir: 'public',
102
+ patterns: patterns.length > 0 ? patterns : ['**/*.tsx', '**/*.jsx', 'public/**/*.html'],
103
+ };
104
+ }
105
+ /**
106
+ * Detect Nuxt project structure
107
+ */
108
+ async function detectNuxtProject(cwd) {
109
+ const patterns = [];
110
+ // Nuxt 3 uses pages/ and components/
111
+ if (existsSync(join(cwd, 'pages'))) {
112
+ patterns.push('pages/**/*.vue');
113
+ }
114
+ if (existsSync(join(cwd, 'components'))) {
115
+ patterns.push('components/**/*.vue');
116
+ }
117
+ if (existsSync(join(cwd, 'layouts'))) {
118
+ patterns.push('layouts/**/*.vue');
119
+ }
120
+ // App.vue at root
121
+ if (existsSync(join(cwd, 'app.vue'))) {
122
+ patterns.push('app.vue');
123
+ }
124
+ // Public HTML files
125
+ patterns.push('public/**/*.html');
126
+ return {
127
+ type: 'nuxt',
128
+ srcDir: '.',
129
+ publicDir: 'public',
130
+ patterns: patterns.length > 0 ? patterns : ['**/*.vue', 'public/**/*.html'],
131
+ };
132
+ }
133
+ /**
134
+ * Detect static HTML site
135
+ */
136
+ async function detectStaticSite(cwd) {
137
+ // Check for common static site directories
138
+ const staticDirs = ['public', 'dist', 'build', 'out', '_site', 'www'];
139
+ for (const dir of staticDirs) {
140
+ const dirPath = join(cwd, dir);
141
+ if (existsSync(dirPath)) {
142
+ const htmlFiles = await glob('**/*.html', { cwd: dirPath, absolute: false });
143
+ if (htmlFiles.length > 0) {
144
+ return {
145
+ type: 'html',
146
+ srcDir: dir,
147
+ publicDir: dir,
148
+ patterns: [`${dir}/**/*.html`, `${dir}/**/*.htm`],
149
+ };
150
+ }
151
+ }
152
+ }
153
+ // Check for HTML files in the root
154
+ const rootHtmlFiles = await glob('*.html', { cwd, absolute: false });
155
+ if (rootHtmlFiles.length > 0) {
156
+ return {
157
+ type: 'html',
158
+ srcDir: '.',
159
+ publicDir: '.',
160
+ patterns: ['**/*.html', '**/*.htm'],
161
+ };
162
+ }
163
+ // Unknown project type - default to scanning for HTML files
164
+ return {
165
+ type: 'unknown',
166
+ srcDir: '.',
167
+ publicDir: '.',
168
+ patterns: ['**/*.html', '**/*.htm'],
169
+ };
170
+ }
171
+ /**
172
+ * Count files matching the detected patterns
173
+ */
174
+ export async function countProjectFiles(cwd, patterns) {
175
+ const files = await glob(patterns, {
176
+ cwd,
177
+ ignore: ['**/node_modules/**', '**/dist/**', '**/build/**', '**/.git/**'],
178
+ absolute: false,
179
+ });
180
+ return files.length;
181
+ }
182
+ /**
183
+ * Get a human-readable description of the project type
184
+ */
185
+ export function getProjectDescription(type) {
186
+ const descriptions = {
187
+ next: 'Next.js project',
188
+ nuxt: 'Nuxt project',
189
+ react: 'React project',
190
+ vue: 'Vue project',
191
+ svelte: 'Svelte project',
192
+ angular: 'Angular project',
193
+ html: 'Static HTML site',
194
+ unknown: 'Unknown project type',
195
+ };
196
+ return descriptions[type];
197
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Enhanced Error Messages System
3
+ *
4
+ * Provides actionable, helpful error messages with:
5
+ * - Clear explanations of what went wrong
6
+ * - Step-by-step remediation guidance
7
+ * - Code examples
8
+ * - Links to documentation
9
+ * - Context-specific suggestions
10
+ */
11
+ export interface EnhancedError {
12
+ title: string;
13
+ message: string;
14
+ remediation: string[];
15
+ example?: string;
16
+ docs?: string;
17
+ tip?: string;
18
+ }
19
+ /**
20
+ * Error messages for common issues
21
+ */
22
+ export declare const ERROR_MESSAGES: Record<string, EnhancedError>;
23
+ /**
24
+ * Get enhanced error message
25
+ */
26
+ export declare function getEnhancedError(errorCode: string): EnhancedError | null;
27
+ /**
28
+ * Format enhanced error for console output
29
+ */
30
+ export declare function formatEnhancedError(error: EnhancedError): string;
31
+ /**
32
+ * Common error patterns and their codes
33
+ */
34
+ export declare function detectErrorCode(error: Error): string | null;
35
+ /**
36
+ * Show enhanced error and exit
37
+ */
38
+ export declare function showEnhancedErrorAndExit(errorCode: string, exitCode?: number): never;
39
+ /**
40
+ * Throw enhanced error (for backwards compatibility)
41
+ */
42
+ export declare function throwEnhancedError(errorCode: string, originalError?: Error): never;
43
+ /**
44
+ * Handle error with enhanced message
45
+ */
46
+ export declare function handleErrorWithEnhancement(error: Error): void;