@vertaaux/cli 0.2.2 → 0.3.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/LICENSE +21 -0
- package/README.md +58 -2
- package/dist/auth/device-flow.d.ts.map +1 -1
- package/dist/auth/device-flow.js +46 -14
- package/dist/commands/audit.d.ts +2 -0
- package/dist/commands/audit.d.ts.map +1 -1
- package/dist/commands/audit.js +167 -8
- package/dist/commands/client.d.ts +14 -0
- package/dist/commands/client.d.ts.map +1 -0
- package/dist/commands/client.js +362 -0
- package/dist/commands/compare.d.ts +20 -0
- package/dist/commands/compare.d.ts.map +1 -0
- package/dist/commands/compare.js +335 -0
- package/dist/commands/doc.d.ts +18 -0
- package/dist/commands/doc.d.ts.map +1 -0
- package/dist/commands/doc.js +161 -0
- package/dist/commands/download.d.ts.map +1 -1
- package/dist/commands/download.js +9 -8
- package/dist/commands/drift.d.ts +15 -0
- package/dist/commands/drift.d.ts.map +1 -0
- package/dist/commands/drift.js +309 -0
- package/dist/commands/explain.d.ts +14 -33
- package/dist/commands/explain.d.ts.map +1 -1
- package/dist/commands/explain.js +277 -179
- package/dist/commands/fix-plan.d.ts +15 -0
- package/dist/commands/fix-plan.d.ts.map +1 -0
- package/dist/commands/fix-plan.js +182 -0
- package/dist/commands/patch-review.d.ts +14 -0
- package/dist/commands/patch-review.d.ts.map +1 -0
- package/dist/commands/patch-review.js +200 -0
- package/dist/commands/protect.d.ts +16 -0
- package/dist/commands/protect.d.ts.map +1 -0
- package/dist/commands/protect.js +323 -0
- package/dist/commands/release-notes.d.ts +17 -0
- package/dist/commands/release-notes.d.ts.map +1 -0
- package/dist/commands/release-notes.js +145 -0
- package/dist/commands/report.d.ts +15 -0
- package/dist/commands/report.d.ts.map +1 -0
- package/dist/commands/report.js +214 -0
- package/dist/commands/suggest.d.ts +18 -0
- package/dist/commands/suggest.d.ts.map +1 -0
- package/dist/commands/suggest.js +152 -0
- package/dist/commands/triage.d.ts +17 -0
- package/dist/commands/triage.d.ts.map +1 -0
- package/dist/commands/triage.js +205 -0
- package/dist/commands/upload.d.ts.map +1 -1
- package/dist/commands/upload.js +8 -7
- package/dist/index.js +62 -25
- package/dist/output/formats.d.ts.map +1 -1
- package/dist/output/formats.js +18 -2
- package/dist/output/human.d.ts +1 -10
- package/dist/output/human.d.ts.map +1 -1
- package/dist/output/human.js +26 -98
- package/dist/policy/sync.d.ts +67 -0
- package/dist/policy/sync.d.ts.map +1 -0
- package/dist/policy/sync.js +147 -0
- package/dist/prompts/command-catalog.d.ts +46 -0
- package/dist/prompts/command-catalog.d.ts.map +1 -0
- package/dist/prompts/command-catalog.js +187 -0
- package/dist/ui/spinner.d.ts +10 -35
- package/dist/ui/spinner.d.ts.map +1 -1
- package/dist/ui/spinner.js +11 -58
- package/dist/ui/table.d.ts +1 -18
- package/dist/ui/table.d.ts.map +1 -1
- package/dist/ui/table.js +56 -163
- package/dist/utils/ai-error.d.ts +48 -0
- package/dist/utils/ai-error.d.ts.map +1 -0
- package/dist/utils/ai-error.js +190 -0
- package/dist/utils/detect-env.d.ts +6 -8
- package/dist/utils/detect-env.d.ts.map +1 -1
- package/dist/utils/detect-env.js +6 -25
- package/dist/utils/stdin.d.ts +50 -0
- package/dist/utils/stdin.d.ts.map +1 -0
- package/dist/utils/stdin.js +93 -0
- package/package.json +11 -7
package/dist/commands/explain.js
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Explain command for VertaaUX CLI.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* -
|
|
6
|
-
* -
|
|
7
|
-
* -
|
|
8
|
-
* - Recommendation
|
|
9
|
-
* - Related artifacts (screenshots, DOM)
|
|
4
|
+
* Two modes:
|
|
5
|
+
* 1. **AI mode** (no finding-id): Accepts full audit JSON via stdin, --file,
|
|
6
|
+
* or --job. Calls the LLM explain endpoint to produce a 3-bullet summary
|
|
7
|
+
* and per-issue explanations. Use --verbose for full evidence per issue.
|
|
10
8
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
9
|
+
* 2. **Evidence mode** (with finding-id): Shows the full evidence bundle for
|
|
10
|
+
* a specific finding (backward compatible with existing usage).
|
|
11
|
+
*
|
|
12
|
+
* Examples:
|
|
13
|
+
* vertaa audit https://example.com --json | vertaa explain
|
|
14
|
+
* vertaa explain --job abc123
|
|
15
|
+
* vertaa explain --file audit.json --verbose
|
|
16
|
+
* vertaa explain color-contrast-001 --job abc123 (legacy mode)
|
|
15
17
|
*/
|
|
16
18
|
import fs from "fs";
|
|
17
19
|
import path from "path";
|
|
@@ -21,47 +23,39 @@ import { resolveApiBase, getApiKey, apiRequest } from "../utils/client.js";
|
|
|
21
23
|
import { resolveConfig } from "../config/loader.js";
|
|
22
24
|
import { writeJsonOutput, writeOutput } from "../output/envelope.js";
|
|
23
25
|
import { resolveCommandFormat } from "../output/formats.js";
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
import { createSpinner, succeedSpinner } from "../ui/spinner.js";
|
|
27
|
+
import { readJsonInput } from "../utils/stdin.js";
|
|
28
|
+
import { handleAiCommandError, AI_TIMEOUT_MS } from "../utils/ai-error.js";
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Constants
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
27
32
|
const RECENT_AUDITS_PATH = ".vertaaux/recent.json";
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Helpers
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
31
36
|
function loadRecentAudits() {
|
|
32
37
|
const filePath = path.resolve(process.cwd(), RECENT_AUDITS_PATH);
|
|
33
|
-
if (!fs.existsSync(filePath))
|
|
38
|
+
if (!fs.existsSync(filePath))
|
|
34
39
|
return [];
|
|
35
|
-
}
|
|
36
40
|
try {
|
|
37
|
-
|
|
38
|
-
return JSON.parse(content);
|
|
41
|
+
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
39
42
|
}
|
|
40
43
|
catch {
|
|
41
44
|
return [];
|
|
42
45
|
}
|
|
43
46
|
}
|
|
44
|
-
/**
|
|
45
|
-
* Normalize issues from various API response formats.
|
|
46
|
-
*/
|
|
47
47
|
function normalizeIssues(issues) {
|
|
48
48
|
if (Array.isArray(issues))
|
|
49
49
|
return issues;
|
|
50
50
|
if (issues && typeof issues === "object") {
|
|
51
51
|
const values = Object.values(issues);
|
|
52
|
-
return values.flatMap((
|
|
52
|
+
return values.flatMap((v) => (Array.isArray(v) ? v : []));
|
|
53
53
|
}
|
|
54
54
|
return [];
|
|
55
55
|
}
|
|
56
|
-
/**
|
|
57
|
-
* Get display-friendly rule ID from issue.
|
|
58
|
-
*/
|
|
59
56
|
function getRuleId(issue) {
|
|
60
57
|
return issue.ruleId || issue.rule_id || issue.id || "unknown";
|
|
61
58
|
}
|
|
62
|
-
/**
|
|
63
|
-
* Get severity label with appropriate color.
|
|
64
|
-
*/
|
|
65
59
|
function coloredSeverity(severity) {
|
|
66
60
|
const sev = (severity || "info").toLowerCase();
|
|
67
61
|
switch (sev) {
|
|
@@ -78,64 +72,57 @@ function coloredSeverity(severity) {
|
|
|
78
72
|
return chalk.dim(sev.toUpperCase());
|
|
79
73
|
}
|
|
80
74
|
}
|
|
75
|
+
function truncate(str, maxLength) {
|
|
76
|
+
if (str.length <= maxLength)
|
|
77
|
+
return str;
|
|
78
|
+
return str.slice(0, maxLength - 3) + "...";
|
|
79
|
+
}
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// Evidence mode formatters (backward compat)
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
81
83
|
/**
|
|
82
84
|
* Format a full evidence bundle for display.
|
|
83
|
-
*
|
|
84
|
-
* @param issue - Issue with evidence
|
|
85
|
-
* @returns Formatted string for terminal display
|
|
86
85
|
*/
|
|
87
86
|
export function formatEvidenceBundle(issue) {
|
|
88
87
|
const lines = [];
|
|
89
|
-
// Header
|
|
90
88
|
const ruleId = getRuleId(issue);
|
|
91
89
|
const severity = coloredSeverity(issue.severity);
|
|
92
90
|
lines.push(chalk.bold(`ISSUE: ${ruleId} (${severity})`));
|
|
93
91
|
lines.push("");
|
|
94
|
-
// Description
|
|
95
92
|
lines.push(chalk.cyan.bold("DESCRIPTION"));
|
|
96
93
|
lines.push(issue.description || issue.title || "No description available");
|
|
97
94
|
lines.push("");
|
|
98
|
-
// Selector
|
|
99
95
|
if (issue.selector) {
|
|
100
96
|
lines.push(chalk.cyan.bold("SELECTOR"));
|
|
101
97
|
lines.push(chalk.gray(issue.selector));
|
|
102
98
|
lines.push("");
|
|
103
99
|
}
|
|
104
|
-
// WCAG Reference
|
|
105
100
|
if (issue.wcag_reference) {
|
|
106
101
|
lines.push(chalk.cyan.bold("WCAG REFERENCE"));
|
|
107
102
|
lines.push(issue.wcag_reference);
|
|
108
103
|
lines.push("");
|
|
109
104
|
}
|
|
110
|
-
// Recommendation
|
|
111
105
|
const recommendation = issue.recommendation || issue.recommended_fix;
|
|
112
106
|
if (recommendation) {
|
|
113
107
|
lines.push(chalk.cyan.bold("RECOMMENDATION"));
|
|
114
108
|
lines.push(recommendation);
|
|
115
109
|
lines.push("");
|
|
116
110
|
}
|
|
117
|
-
// Evidence section
|
|
118
111
|
const hasEvidence = issue.screenshot || issue.html || issue.element || issue.helpUrl;
|
|
119
112
|
if (hasEvidence) {
|
|
120
113
|
lines.push(chalk.cyan.bold("EVIDENCE"));
|
|
121
|
-
if (issue.screenshot)
|
|
114
|
+
if (issue.screenshot)
|
|
122
115
|
lines.push(`- Screenshot: ${chalk.underline(issue.screenshot)}`);
|
|
123
|
-
}
|
|
124
116
|
if (issue.html || issue.element) {
|
|
125
|
-
|
|
126
|
-
lines.push(`- DOM Snapshot: ${chalk.dim(truncate(snippet, 100))}`);
|
|
117
|
+
lines.push(`- DOM Snapshot: ${chalk.dim(truncate(issue.html || issue.element || "", 100))}`);
|
|
127
118
|
}
|
|
128
|
-
if (issue.helpUrl)
|
|
119
|
+
if (issue.helpUrl)
|
|
129
120
|
lines.push(`- Help: ${chalk.underline(issue.helpUrl)}`);
|
|
130
|
-
}
|
|
131
121
|
}
|
|
132
122
|
return lines.join("\n");
|
|
133
123
|
}
|
|
134
|
-
/**
|
|
135
|
-
* Format evidence as JSON.
|
|
136
|
-
*/
|
|
137
124
|
export function formatEvidenceJson(issue) {
|
|
138
|
-
|
|
125
|
+
return JSON.stringify({
|
|
139
126
|
ruleId: getRuleId(issue),
|
|
140
127
|
severity: issue.severity,
|
|
141
128
|
category: issue.category,
|
|
@@ -148,150 +135,261 @@ export function formatEvidenceJson(issue) {
|
|
|
148
135
|
html: issue.html || issue.element,
|
|
149
136
|
helpUrl: issue.helpUrl,
|
|
150
137
|
},
|
|
151
|
-
};
|
|
152
|
-
return JSON.stringify(output, null, 2);
|
|
153
|
-
}
|
|
154
|
-
/**
|
|
155
|
-
* Truncate string with ellipsis.
|
|
156
|
-
*/
|
|
157
|
-
function truncate(str, maxLength) {
|
|
158
|
-
if (str.length <= maxLength)
|
|
159
|
-
return str;
|
|
160
|
-
return str.slice(0, maxLength - 3) + "...";
|
|
138
|
+
}, null, 2);
|
|
161
139
|
}
|
|
162
|
-
/**
|
|
163
|
-
* Export for reuse in fix-wizard.
|
|
164
|
-
*
|
|
165
|
-
* @param issue - Issue to explain
|
|
166
|
-
* @returns Formatted evidence string
|
|
167
|
-
*/
|
|
140
|
+
/** Exported for reuse in fix-wizard. */
|
|
168
141
|
export function explainIssue(issue) {
|
|
169
142
|
return formatEvidenceBundle(issue);
|
|
170
143
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
// AI mode formatters
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
function formatAiExplainHuman(response, verbose, issues) {
|
|
148
|
+
const lines = [];
|
|
149
|
+
// 3-bullet summary
|
|
150
|
+
lines.push(chalk.bold("Summary"));
|
|
151
|
+
for (const bullet of response.summary) {
|
|
152
|
+
lines.push(` ${chalk.cyan(">")} ${bullet}`);
|
|
153
|
+
}
|
|
154
|
+
lines.push("");
|
|
155
|
+
// Issues grouped by severity
|
|
156
|
+
lines.push(chalk.bold("Issues"));
|
|
157
|
+
lines.push("");
|
|
158
|
+
for (const issue of response.issues) {
|
|
159
|
+
const severity = coloredSeverity(issue.severity);
|
|
160
|
+
lines.push(` ${severity} ${chalk.bold(issue.title)}${issue.id ? chalk.dim(` (${issue.id})`) : ""}`);
|
|
161
|
+
lines.push(` ${issue.explanation}`);
|
|
162
|
+
lines.push(` ${chalk.green("Fix:")} ${issue.fix}`);
|
|
163
|
+
if (verbose && issues) {
|
|
164
|
+
// Find the matching original issue for full evidence
|
|
165
|
+
const original = issues.find((i) => getRuleId(i) === issue.id || i.id === issue.id);
|
|
166
|
+
if (original) {
|
|
167
|
+
if (original.selector)
|
|
168
|
+
lines.push(` ${chalk.dim("Selector:")} ${original.selector}`);
|
|
169
|
+
if (original.wcag_reference)
|
|
170
|
+
lines.push(` ${chalk.dim("WCAG:")} ${original.wcag_reference}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
lines.push("");
|
|
174
|
+
}
|
|
175
|
+
return lines.join("\n");
|
|
176
|
+
}
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
// Evidence mode handler (backward compat)
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
async function handleEvidenceMode(findingId, options, config, format) {
|
|
181
|
+
let issue = null;
|
|
182
|
+
if (options.file) {
|
|
183
|
+
const filePath = path.resolve(process.cwd(), options.file);
|
|
184
|
+
if (!fs.existsSync(filePath)) {
|
|
185
|
+
console.error(`Error: File not found: ${filePath}`);
|
|
186
|
+
process.exit(ExitCode.ERROR);
|
|
187
|
+
}
|
|
188
|
+
const data = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
189
|
+
let issues;
|
|
190
|
+
if (Array.isArray(data)) {
|
|
191
|
+
issues = data;
|
|
192
|
+
}
|
|
193
|
+
else if (data.issues) {
|
|
194
|
+
issues = normalizeIssues(data.issues);
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
if (data.id === findingId || data.ruleId === findingId)
|
|
198
|
+
issue = data;
|
|
199
|
+
issues = [];
|
|
200
|
+
}
|
|
201
|
+
if (!issue) {
|
|
202
|
+
issue = issues.find((i) => i.id === findingId ||
|
|
203
|
+
getRuleId(i) === findingId ||
|
|
204
|
+
i.id?.startsWith(findingId) ||
|
|
205
|
+
getRuleId(i).startsWith(findingId)) || null;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
else if (options.job) {
|
|
209
|
+
const base = resolveApiBase(options.base);
|
|
210
|
+
const apiKey = getApiKey(config.apiKey);
|
|
211
|
+
const result = await apiRequest(base, `/audit/${options.job}`, { method: "GET" }, apiKey);
|
|
212
|
+
const issues = normalizeIssues(result.issues);
|
|
213
|
+
issue = issues.find((i) => i.id === findingId ||
|
|
214
|
+
getRuleId(i) === findingId ||
|
|
215
|
+
i.id?.startsWith(findingId) ||
|
|
216
|
+
getRuleId(i).startsWith(findingId)) || null;
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
const recent = loadRecentAudits();
|
|
220
|
+
if (recent.length === 0) {
|
|
221
|
+
console.error("Error: No recent audits found. Provide --job or --file option.");
|
|
222
|
+
process.exit(ExitCode.ERROR);
|
|
223
|
+
}
|
|
224
|
+
const base = resolveApiBase(options.base);
|
|
225
|
+
const apiKey = getApiKey(config.apiKey);
|
|
226
|
+
for (const audit of recent) {
|
|
227
|
+
try {
|
|
228
|
+
const result = await apiRequest(base, `/audit/${audit.jobId}`, { method: "GET" }, apiKey);
|
|
229
|
+
const issues = normalizeIssues(result.issues);
|
|
230
|
+
issue = issues.find((i) => i.id === findingId ||
|
|
231
|
+
getRuleId(i) === findingId ||
|
|
232
|
+
i.id?.startsWith(findingId) ||
|
|
233
|
+
getRuleId(i).startsWith(findingId)) || null;
|
|
234
|
+
if (issue)
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (!issue) {
|
|
243
|
+
console.error(`Error: Finding "${findingId}" not found.`);
|
|
244
|
+
if (options.job)
|
|
245
|
+
console.error(`Looked in job: ${options.job}`);
|
|
246
|
+
else if (options.file)
|
|
247
|
+
console.error(`Looked in file: ${options.file}`);
|
|
248
|
+
else
|
|
249
|
+
console.error("Try specifying --job or --file to narrow the search.");
|
|
250
|
+
process.exit(ExitCode.ERROR);
|
|
251
|
+
}
|
|
252
|
+
if (format === "json") {
|
|
253
|
+
writeJsonOutput({
|
|
254
|
+
ruleId: getRuleId(issue),
|
|
255
|
+
severity: issue.severity,
|
|
256
|
+
category: issue.category,
|
|
257
|
+
description: issue.description || issue.title,
|
|
258
|
+
selector: issue.selector,
|
|
259
|
+
wcagReference: issue.wcag_reference,
|
|
260
|
+
recommendation: issue.recommendation || issue.recommended_fix,
|
|
261
|
+
evidence: {
|
|
262
|
+
screenshot: issue.screenshot,
|
|
263
|
+
html: issue.html || issue.element,
|
|
264
|
+
helpUrl: issue.helpUrl,
|
|
265
|
+
},
|
|
266
|
+
}, "explain");
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
writeOutput(formatEvidenceBundle(issue));
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
// ---------------------------------------------------------------------------
|
|
273
|
+
// AI mode handler
|
|
274
|
+
// ---------------------------------------------------------------------------
|
|
275
|
+
async function handleAiMode(options, config, format, verbose) {
|
|
276
|
+
// Read audit JSON from stdin, --file, or --job
|
|
277
|
+
let auditData = null;
|
|
278
|
+
let rawIssues = [];
|
|
279
|
+
if (options.job) {
|
|
280
|
+
// Fetch full audit from API
|
|
281
|
+
const base = resolveApiBase(options.base, undefined);
|
|
282
|
+
const apiKey = getApiKey(config.apiKey);
|
|
283
|
+
const result = await apiRequest(base, `/audit/${options.job}`, { method: "GET" }, apiKey);
|
|
284
|
+
rawIssues = normalizeIssues(result.issues);
|
|
285
|
+
auditData = {
|
|
286
|
+
job_id: result.job_id || options.job,
|
|
287
|
+
url: result.url || null,
|
|
288
|
+
scores: result.scores || null,
|
|
289
|
+
issues: rawIssues.map((i) => ({
|
|
290
|
+
id: i.id || getRuleId(i),
|
|
291
|
+
title: i.title || i.description || null,
|
|
292
|
+
description: i.description || null,
|
|
293
|
+
severity: i.severity || null,
|
|
294
|
+
category: i.category || null,
|
|
295
|
+
selector: i.selector || null,
|
|
296
|
+
wcag_reference: i.wcag_reference || null,
|
|
297
|
+
recommendation: i.recommendation || i.recommended_fix || null,
|
|
298
|
+
})),
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
// Read from stdin or --file
|
|
303
|
+
const input = await readJsonInput(options.file);
|
|
304
|
+
if (!input) {
|
|
305
|
+
console.error("Error: No audit data provided.");
|
|
306
|
+
console.error("Usage:");
|
|
307
|
+
console.error(" vertaa audit https://example.com --json | vertaa explain");
|
|
308
|
+
console.error(" vertaa explain --file audit.json");
|
|
309
|
+
console.error(" vertaa explain --job <job-id>");
|
|
310
|
+
console.error(" vertaa explain <finding-id> --job <job-id>");
|
|
311
|
+
process.exit(ExitCode.ERROR);
|
|
312
|
+
}
|
|
313
|
+
const data = input;
|
|
314
|
+
// Handle the envelope format { meta: {...}, data: {...} }
|
|
315
|
+
const innerData = (data.data && typeof data.data === "object" ? data.data : data);
|
|
316
|
+
rawIssues = normalizeIssues(innerData.issues);
|
|
317
|
+
auditData = {
|
|
318
|
+
job_id: innerData.job_id || null,
|
|
319
|
+
url: innerData.url || null,
|
|
320
|
+
scores: innerData.scores || null,
|
|
321
|
+
issues: rawIssues.map((i) => ({
|
|
322
|
+
id: i.id || getRuleId(i),
|
|
323
|
+
title: i.title || i.description || null,
|
|
324
|
+
description: i.description || null,
|
|
325
|
+
severity: i.severity || null,
|
|
326
|
+
category: i.category || null,
|
|
327
|
+
selector: i.selector || null,
|
|
328
|
+
wcag_reference: i.wcag_reference || null,
|
|
329
|
+
recommendation: i.recommendation || i.recommended_fix || null,
|
|
330
|
+
})),
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
if (!auditData ||
|
|
334
|
+
!Array.isArray(auditData.issues) ||
|
|
335
|
+
auditData.issues.length === 0) {
|
|
336
|
+
console.error("Error: No issues found in audit data.");
|
|
337
|
+
process.exit(ExitCode.ERROR);
|
|
338
|
+
}
|
|
339
|
+
// Call the LLM explain API
|
|
340
|
+
const base = resolveApiBase(options.base, undefined);
|
|
341
|
+
const apiKey = getApiKey(config.apiKey);
|
|
342
|
+
const spinner = createSpinner("Analyzing findings...");
|
|
343
|
+
try {
|
|
344
|
+
const response = await Promise.race([
|
|
345
|
+
apiRequest(base, "/cli/ai/explain", { method: "POST", body: { audit: auditData } }, apiKey),
|
|
346
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("LLM request timed out")), AI_TIMEOUT_MS)),
|
|
347
|
+
]);
|
|
348
|
+
succeedSpinner(spinner, "Analysis complete");
|
|
349
|
+
if (format === "json") {
|
|
350
|
+
writeJsonOutput(response.data, "explain");
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
writeOutput(formatAiExplainHuman(response.data, verbose, rawIssues));
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
catch (error) {
|
|
357
|
+
handleAiCommandError(error, "explain", spinner);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
174
360
|
export function registerExplainCommand(program) {
|
|
175
361
|
program
|
|
176
|
-
.command("explain
|
|
177
|
-
.description("
|
|
178
|
-
.option("--job <job-id>", "Job ID
|
|
179
|
-
.option("--file <path>", "Load
|
|
180
|
-
.option("-f, --format <format>", "Output format: json | human"
|
|
362
|
+
.command("explain [finding-id]")
|
|
363
|
+
.description("Explain audit findings with AI, or show evidence for a specific finding")
|
|
364
|
+
.option("--job <job-id>", "Job ID (for fetching audit data or finding)")
|
|
365
|
+
.option("--file <path>", "Load audit JSON from file")
|
|
366
|
+
.option("-f, --format <format>", "Output format: json | human")
|
|
367
|
+
.addHelpText("after", `
|
|
368
|
+
Modes:
|
|
369
|
+
AI mode (no finding-id):
|
|
370
|
+
vertaa audit https://example.com --json | vertaa explain
|
|
371
|
+
vertaa explain --job <job-id>
|
|
372
|
+
vertaa explain --file audit.json
|
|
373
|
+
vertaa explain --file audit.json --verbose
|
|
374
|
+
|
|
375
|
+
Evidence mode (with finding-id):
|
|
376
|
+
vertaa explain color-contrast-001 --job <job-id>
|
|
377
|
+
vertaa explain axe-label --file results.json
|
|
378
|
+
`)
|
|
181
379
|
.action(async (findingId, options, command) => {
|
|
182
380
|
try {
|
|
183
|
-
// Load config (supports --config global option)
|
|
184
381
|
const globalOpts = command.optsWithGlobals();
|
|
185
382
|
const config = await resolveConfig(globalOpts.config);
|
|
186
|
-
// Validate format using per-command registry
|
|
187
383
|
const machineMode = globalOpts.machine || false;
|
|
384
|
+
const verbose = globalOpts.verbose || false;
|
|
188
385
|
const format = resolveCommandFormat("explain", options.format, machineMode);
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
const filePath = path.resolve(process.cwd(), options.file);
|
|
193
|
-
if (!fs.existsSync(filePath)) {
|
|
194
|
-
console.error(`Error: File not found: ${filePath}`);
|
|
195
|
-
process.exit(ExitCode.ERROR);
|
|
196
|
-
}
|
|
197
|
-
const content = fs.readFileSync(filePath, "utf-8");
|
|
198
|
-
const data = JSON.parse(content);
|
|
199
|
-
// Handle different file structures
|
|
200
|
-
let issues;
|
|
201
|
-
if (Array.isArray(data)) {
|
|
202
|
-
issues = data;
|
|
203
|
-
}
|
|
204
|
-
else if (data.issues) {
|
|
205
|
-
issues = normalizeIssues(data.issues);
|
|
206
|
-
}
|
|
207
|
-
else {
|
|
208
|
-
// Single issue object
|
|
209
|
-
if (data.id === findingId || data.ruleId === findingId) {
|
|
210
|
-
issue = data;
|
|
211
|
-
}
|
|
212
|
-
issues = [];
|
|
213
|
-
}
|
|
214
|
-
if (!issue) {
|
|
215
|
-
issue =
|
|
216
|
-
issues.find((i) => i.id === findingId ||
|
|
217
|
-
getRuleId(i) === findingId ||
|
|
218
|
-
i.id?.startsWith(findingId) ||
|
|
219
|
-
getRuleId(i).startsWith(findingId)) || null;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
else if (options.job) {
|
|
223
|
-
// Fetch from API
|
|
224
|
-
const base = resolveApiBase(options.base);
|
|
225
|
-
const apiKey = getApiKey(config.apiKey);
|
|
226
|
-
const result = await apiRequest(base, `/audit/${options.job}`, { method: "GET" }, apiKey);
|
|
227
|
-
const issues = normalizeIssues(result.issues);
|
|
228
|
-
issue =
|
|
229
|
-
issues.find((i) => i.id === findingId ||
|
|
230
|
-
getRuleId(i) === findingId ||
|
|
231
|
-
i.id?.startsWith(findingId) ||
|
|
232
|
-
getRuleId(i).startsWith(findingId)) || null;
|
|
233
|
-
}
|
|
234
|
-
else {
|
|
235
|
-
// Try recent audits
|
|
236
|
-
const recent = loadRecentAudits();
|
|
237
|
-
if (recent.length === 0) {
|
|
238
|
-
console.error("Error: No recent audits found. Provide --job or --file option.");
|
|
239
|
-
process.exit(ExitCode.ERROR);
|
|
240
|
-
}
|
|
241
|
-
// Try each recent audit
|
|
242
|
-
const base = resolveApiBase(options.base);
|
|
243
|
-
const apiKey = getApiKey(config.apiKey);
|
|
244
|
-
for (const audit of recent) {
|
|
245
|
-
try {
|
|
246
|
-
const result = await apiRequest(base, `/audit/${audit.jobId}`, { method: "GET" }, apiKey);
|
|
247
|
-
const issues = normalizeIssues(result.issues);
|
|
248
|
-
issue =
|
|
249
|
-
issues.find((i) => i.id === findingId ||
|
|
250
|
-
getRuleId(i) === findingId ||
|
|
251
|
-
i.id?.startsWith(findingId) ||
|
|
252
|
-
getRuleId(i).startsWith(findingId)) || null;
|
|
253
|
-
if (issue)
|
|
254
|
-
break;
|
|
255
|
-
}
|
|
256
|
-
catch {
|
|
257
|
-
// Try next audit
|
|
258
|
-
continue;
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
if (!issue) {
|
|
263
|
-
console.error(`Error: Finding "${findingId}" not found.`);
|
|
264
|
-
if (options.job) {
|
|
265
|
-
console.error(`Looked in job: ${options.job}`);
|
|
266
|
-
}
|
|
267
|
-
else if (options.file) {
|
|
268
|
-
console.error(`Looked in file: ${options.file}`);
|
|
269
|
-
}
|
|
270
|
-
else {
|
|
271
|
-
console.error("Try specifying --job or --file to narrow the search.");
|
|
272
|
-
}
|
|
273
|
-
process.exit(ExitCode.ERROR);
|
|
274
|
-
}
|
|
275
|
-
// Output
|
|
276
|
-
if (format === "json") {
|
|
277
|
-
const evidenceData = {
|
|
278
|
-
ruleId: getRuleId(issue),
|
|
279
|
-
severity: issue.severity,
|
|
280
|
-
category: issue.category,
|
|
281
|
-
description: issue.description || issue.title,
|
|
282
|
-
selector: issue.selector,
|
|
283
|
-
wcagReference: issue.wcag_reference,
|
|
284
|
-
recommendation: issue.recommendation || issue.recommended_fix,
|
|
285
|
-
evidence: {
|
|
286
|
-
screenshot: issue.screenshot,
|
|
287
|
-
html: issue.html || issue.element,
|
|
288
|
-
helpUrl: issue.helpUrl,
|
|
289
|
-
},
|
|
290
|
-
};
|
|
291
|
-
writeJsonOutput(evidenceData, "explain");
|
|
386
|
+
if (findingId) {
|
|
387
|
+
// Backward-compatible evidence mode
|
|
388
|
+
await handleEvidenceMode(findingId, options, config, format);
|
|
292
389
|
}
|
|
293
390
|
else {
|
|
294
|
-
|
|
391
|
+
// AI-powered explain mode
|
|
392
|
+
await handleAiMode(options, config, format, verbose);
|
|
295
393
|
}
|
|
296
394
|
}
|
|
297
395
|
catch (error) {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fix-plan command for VertaaUX CLI.
|
|
3
|
+
*
|
|
4
|
+
* Accepts full audit JSON (via stdin, --file, or --job) and calls the
|
|
5
|
+
* LLM fix-plan endpoint to produce a structured remediation plan with
|
|
6
|
+
* ordered steps, effort estimates, and code hints.
|
|
7
|
+
*
|
|
8
|
+
* Examples:
|
|
9
|
+
* vertaa audit https://example.com --json | vertaa fix-plan
|
|
10
|
+
* vertaa fix-plan --job abc123
|
|
11
|
+
* vertaa fix-plan --file audit.json --json
|
|
12
|
+
*/
|
|
13
|
+
import { Command } from "commander";
|
|
14
|
+
export declare function registerFixPlanCommand(program: Command): void;
|
|
15
|
+
//# sourceMappingURL=fix-plan.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fix-plan.d.ts","sourceRoot":"","sources":["../../src/commands/fix-plan.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAuIpC,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA6G7D"}
|