@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.
Files changed (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +58 -2
  3. package/dist/auth/device-flow.d.ts.map +1 -1
  4. package/dist/auth/device-flow.js +46 -14
  5. package/dist/commands/audit.d.ts +2 -0
  6. package/dist/commands/audit.d.ts.map +1 -1
  7. package/dist/commands/audit.js +167 -8
  8. package/dist/commands/client.d.ts +14 -0
  9. package/dist/commands/client.d.ts.map +1 -0
  10. package/dist/commands/client.js +362 -0
  11. package/dist/commands/compare.d.ts +20 -0
  12. package/dist/commands/compare.d.ts.map +1 -0
  13. package/dist/commands/compare.js +335 -0
  14. package/dist/commands/doc.d.ts +18 -0
  15. package/dist/commands/doc.d.ts.map +1 -0
  16. package/dist/commands/doc.js +161 -0
  17. package/dist/commands/download.d.ts.map +1 -1
  18. package/dist/commands/download.js +9 -8
  19. package/dist/commands/drift.d.ts +15 -0
  20. package/dist/commands/drift.d.ts.map +1 -0
  21. package/dist/commands/drift.js +309 -0
  22. package/dist/commands/explain.d.ts +14 -33
  23. package/dist/commands/explain.d.ts.map +1 -1
  24. package/dist/commands/explain.js +277 -179
  25. package/dist/commands/fix-plan.d.ts +15 -0
  26. package/dist/commands/fix-plan.d.ts.map +1 -0
  27. package/dist/commands/fix-plan.js +182 -0
  28. package/dist/commands/patch-review.d.ts +14 -0
  29. package/dist/commands/patch-review.d.ts.map +1 -0
  30. package/dist/commands/patch-review.js +200 -0
  31. package/dist/commands/protect.d.ts +16 -0
  32. package/dist/commands/protect.d.ts.map +1 -0
  33. package/dist/commands/protect.js +323 -0
  34. package/dist/commands/release-notes.d.ts +17 -0
  35. package/dist/commands/release-notes.d.ts.map +1 -0
  36. package/dist/commands/release-notes.js +145 -0
  37. package/dist/commands/report.d.ts +15 -0
  38. package/dist/commands/report.d.ts.map +1 -0
  39. package/dist/commands/report.js +214 -0
  40. package/dist/commands/suggest.d.ts +18 -0
  41. package/dist/commands/suggest.d.ts.map +1 -0
  42. package/dist/commands/suggest.js +152 -0
  43. package/dist/commands/triage.d.ts +17 -0
  44. package/dist/commands/triage.d.ts.map +1 -0
  45. package/dist/commands/triage.js +205 -0
  46. package/dist/commands/upload.d.ts.map +1 -1
  47. package/dist/commands/upload.js +8 -7
  48. package/dist/index.js +62 -25
  49. package/dist/output/formats.d.ts.map +1 -1
  50. package/dist/output/formats.js +18 -2
  51. package/dist/output/human.d.ts +1 -10
  52. package/dist/output/human.d.ts.map +1 -1
  53. package/dist/output/human.js +26 -98
  54. package/dist/policy/sync.d.ts +67 -0
  55. package/dist/policy/sync.d.ts.map +1 -0
  56. package/dist/policy/sync.js +147 -0
  57. package/dist/prompts/command-catalog.d.ts +46 -0
  58. package/dist/prompts/command-catalog.d.ts.map +1 -0
  59. package/dist/prompts/command-catalog.js +187 -0
  60. package/dist/ui/spinner.d.ts +10 -35
  61. package/dist/ui/spinner.d.ts.map +1 -1
  62. package/dist/ui/spinner.js +11 -58
  63. package/dist/ui/table.d.ts +1 -18
  64. package/dist/ui/table.d.ts.map +1 -1
  65. package/dist/ui/table.js +56 -163
  66. package/dist/utils/ai-error.d.ts +48 -0
  67. package/dist/utils/ai-error.d.ts.map +1 -0
  68. package/dist/utils/ai-error.js +190 -0
  69. package/dist/utils/detect-env.d.ts +6 -8
  70. package/dist/utils/detect-env.d.ts.map +1 -1
  71. package/dist/utils/detect-env.js +6 -25
  72. package/dist/utils/stdin.d.ts +50 -0
  73. package/dist/utils/stdin.d.ts.map +1 -0
  74. package/dist/utils/stdin.js +93 -0
  75. package/package.json +11 -7
@@ -1,17 +1,19 @@
1
1
  /**
2
2
  * Explain command for VertaaUX CLI.
3
3
  *
4
- * Shows full evidence bundle for a specific finding:
5
- * - Description
6
- * - Selector
7
- * - WCAG reference
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
- * Supports loading issues from:
12
- * - Active job via --job flag
13
- * - Local JSON file via --file flag
14
- * - Recent audits from .vertaaux/recent.json
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
- * Path to recent audits cache.
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
- * Load recent audits from cache file.
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
- const content = fs.readFileSync(filePath, "utf-8");
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((value) => Array.isArray(value) ? value : []);
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
- const snippet = issue.html || issue.element || "";
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
- const output = {
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
- * Register the explain command with the Commander program.
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 <finding-id>")
177
- .description("Show full evidence bundle for a finding")
178
- .option("--job <job-id>", "Job ID containing the finding")
179
- .option("--file <path>", "Load findings from local JSON file")
180
- .option("-f, --format <format>", "Output format: json | human", "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
- let issue = null;
190
- if (options.file) {
191
- // Load from local file
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
- writeOutput(formatEvidenceBundle(issue));
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"}