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.
- package/ACCESSIBILITY.md +205 -0
- package/LICENSE +21 -0
- package/README.md +940 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.js +528 -0
- package/dist/commands/audit-palette.d.ts +18 -0
- package/dist/commands/audit-palette.js +613 -0
- package/dist/commands/auto-pr.d.ts +19 -0
- package/dist/commands/auto-pr.js +434 -0
- package/dist/commands/badge.d.ts +11 -0
- package/dist/commands/badge.js +143 -0
- package/dist/commands/completion.d.ts +4 -0
- package/dist/commands/completion.js +185 -0
- package/dist/commands/crawl.d.ts +12 -0
- package/dist/commands/crawl.js +249 -0
- package/dist/commands/doctor.d.ts +5 -0
- package/dist/commands/doctor.js +233 -0
- package/dist/commands/explain.d.ts +12 -0
- package/dist/commands/explain.js +233 -0
- package/dist/commands/fix.d.ts +13 -0
- package/dist/commands/fix.js +668 -0
- package/dist/commands/health.d.ts +11 -0
- package/dist/commands/health.js +367 -0
- package/dist/commands/history.d.ts +10 -0
- package/dist/commands/history.js +191 -0
- package/dist/commands/init.d.ts +9 -0
- package/dist/commands/init.js +164 -0
- package/dist/commands/learn.d.ts +8 -0
- package/dist/commands/learn.js +592 -0
- package/dist/commands/pr-check.d.ts +12 -0
- package/dist/commands/pr-check.js +270 -0
- package/dist/commands/report.d.ts +11 -0
- package/dist/commands/report.js +375 -0
- package/dist/commands/scan-storybook.d.ts +18 -0
- package/dist/commands/scan-storybook.js +402 -0
- package/dist/commands/scan.d.ts +25 -0
- package/dist/commands/scan.js +673 -0
- package/dist/commands/stats.d.ts +5 -0
- package/dist/commands/stats.js +137 -0
- package/dist/commands/tree.d.ts +12 -0
- package/dist/commands/tree.js +635 -0
- package/dist/commands/triage.d.ts +13 -0
- package/dist/commands/triage.js +327 -0
- package/dist/commands/watch.d.ts +17 -0
- package/dist/commands/watch.js +302 -0
- package/dist/types/index.d.ts +60 -0
- package/dist/types/index.js +4 -0
- package/dist/utils/baseline.d.ts +62 -0
- package/dist/utils/baseline.js +169 -0
- package/dist/utils/browser.d.ts +78 -0
- package/dist/utils/browser.js +239 -0
- package/dist/utils/cache.d.ts +76 -0
- package/dist/utils/cache.js +178 -0
- package/dist/utils/config.d.ts +102 -0
- package/dist/utils/config.js +237 -0
- package/dist/utils/converters.d.ts +77 -0
- package/dist/utils/converters.js +200 -0
- package/dist/utils/copilot.d.ts +36 -0
- package/dist/utils/copilot.js +139 -0
- package/dist/utils/detect.d.ts +22 -0
- package/dist/utils/detect.js +197 -0
- package/dist/utils/enhanced-errors.d.ts +46 -0
- package/dist/utils/enhanced-errors.js +295 -0
- package/dist/utils/errors.d.ts +31 -0
- package/dist/utils/errors.js +149 -0
- package/dist/utils/fix-patterns.d.ts +56 -0
- package/dist/utils/fix-patterns.js +529 -0
- package/dist/utils/history-tracking.d.ts +94 -0
- package/dist/utils/history-tracking.js +230 -0
- package/dist/utils/history.d.ts +42 -0
- package/dist/utils/history.js +255 -0
- package/dist/utils/impact-scores.d.ts +44 -0
- package/dist/utils/impact-scores.js +257 -0
- package/dist/utils/retry.d.ts +24 -0
- package/dist/utils/retry.js +76 -0
- package/dist/utils/scanner.d.ts +74 -0
- package/dist/utils/scanner.js +606 -0
- package/dist/utils/scanner.test.d.ts +4 -0
- package/dist/utils/scanner.test.js +162 -0
- package/dist/utils/ui.d.ts +44 -0
- package/dist/utils/ui.js +276 -0
- package/mcp-server/dist/index.d.ts +8 -0
- package/mcp-server/dist/index.js +1923 -0
- 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, '<')
|
|
114
|
+
.replace(/>/g, '>')
|
|
115
|
+
.replace(/"/g, '"')
|
|
116
|
+
.replace(/'/g, ''');
|
|
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;
|