@vertaaux/cli 0.2.3 → 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 (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +58 -2
  3. package/dist/auth/device-flow.js +6 -8
  4. package/dist/commands/audit.d.ts +2 -0
  5. package/dist/commands/audit.d.ts.map +1 -1
  6. package/dist/commands/audit.js +165 -6
  7. package/dist/commands/compare.d.ts +20 -0
  8. package/dist/commands/compare.d.ts.map +1 -0
  9. package/dist/commands/compare.js +335 -0
  10. package/dist/commands/doc.d.ts +18 -0
  11. package/dist/commands/doc.d.ts.map +1 -0
  12. package/dist/commands/doc.js +161 -0
  13. package/dist/commands/download.d.ts.map +1 -1
  14. package/dist/commands/download.js +9 -8
  15. package/dist/commands/explain.d.ts +14 -33
  16. package/dist/commands/explain.d.ts.map +1 -1
  17. package/dist/commands/explain.js +277 -179
  18. package/dist/commands/fix-plan.d.ts +15 -0
  19. package/dist/commands/fix-plan.d.ts.map +1 -0
  20. package/dist/commands/fix-plan.js +182 -0
  21. package/dist/commands/patch-review.d.ts +14 -0
  22. package/dist/commands/patch-review.d.ts.map +1 -0
  23. package/dist/commands/patch-review.js +200 -0
  24. package/dist/commands/release-notes.d.ts +17 -0
  25. package/dist/commands/release-notes.d.ts.map +1 -0
  26. package/dist/commands/release-notes.js +145 -0
  27. package/dist/commands/suggest.d.ts +18 -0
  28. package/dist/commands/suggest.d.ts.map +1 -0
  29. package/dist/commands/suggest.js +152 -0
  30. package/dist/commands/triage.d.ts +17 -0
  31. package/dist/commands/triage.d.ts.map +1 -0
  32. package/dist/commands/triage.js +205 -0
  33. package/dist/commands/upload.d.ts.map +1 -1
  34. package/dist/commands/upload.js +8 -7
  35. package/dist/index.js +62 -25
  36. package/dist/output/formats.d.ts.map +1 -1
  37. package/dist/output/formats.js +14 -0
  38. package/dist/output/human.d.ts +1 -10
  39. package/dist/output/human.d.ts.map +1 -1
  40. package/dist/output/human.js +26 -98
  41. package/dist/prompts/command-catalog.d.ts +46 -0
  42. package/dist/prompts/command-catalog.d.ts.map +1 -0
  43. package/dist/prompts/command-catalog.js +187 -0
  44. package/dist/ui/spinner.d.ts +10 -35
  45. package/dist/ui/spinner.d.ts.map +1 -1
  46. package/dist/ui/spinner.js +11 -58
  47. package/dist/ui/table.d.ts +1 -18
  48. package/dist/ui/table.d.ts.map +1 -1
  49. package/dist/ui/table.js +56 -163
  50. package/dist/utils/ai-error.d.ts +48 -0
  51. package/dist/utils/ai-error.d.ts.map +1 -0
  52. package/dist/utils/ai-error.js +190 -0
  53. package/dist/utils/detect-env.d.ts +6 -8
  54. package/dist/utils/detect-env.d.ts.map +1 -1
  55. package/dist/utils/detect-env.js +6 -25
  56. package/dist/utils/stdin.d.ts +50 -0
  57. package/dist/utils/stdin.d.ts.map +1 -0
  58. package/dist/utils/stdin.js +93 -0
  59. package/package.json +9 -5
@@ -0,0 +1,335 @@
1
+ /**
2
+ * Compare command for VertaaUX CLI (upgraded).
3
+ *
4
+ * Two modes:
5
+ * 1. URL comparison (backward compat): `vertaa compare <urlA> <urlB> --wait`
6
+ * Runs two audits and shows a score/category delta table.
7
+ * 2. File-based LLM comparison: `vertaa compare --before old.json --after new.json`
8
+ * Sends both audit JSONs to the LLM compare endpoint for a narrative analysis.
9
+ *
10
+ * When --before/--after are provided, the LLM mode is used automatically.
11
+ * When positional URLs are given, the legacy comparison mode is used.
12
+ *
13
+ * Examples:
14
+ * vertaa compare https://a.com https://b.com --wait
15
+ * vertaa compare --before baseline.json --after current.json
16
+ * vertaa compare --before baseline.json --after current.json --verbose
17
+ */
18
+ import chalk from "chalk";
19
+ import { ExitCode } from "../utils/exit-codes.js";
20
+ import { resolveApiBase, getApiKey, apiRequest, waitForAudit, } from "../utils/client.js";
21
+ import { resolveConfig } from "../config/loader.js";
22
+ import { writeJsonOutput, writeOutput } from "../output/envelope.js";
23
+ import { resolveCommandFormat } from "../output/formats.js";
24
+ import { createSpinner, succeedSpinner, failSpinner } from "../ui/spinner.js";
25
+ import { readJsonInput } from "../utils/stdin.js";
26
+ import { handleAiCommandError, AI_TIMEOUT_MS } from "../utils/ai-error.js";
27
+ // ---------------------------------------------------------------------------
28
+ // Helpers
29
+ // ---------------------------------------------------------------------------
30
+ function normalizeIssues(issues) {
31
+ let list;
32
+ if (Array.isArray(issues)) {
33
+ list = issues;
34
+ }
35
+ else if (issues && typeof issues === "object") {
36
+ list = Object.values(issues).flatMap((v) => Array.isArray(v) ? v : []);
37
+ }
38
+ else {
39
+ return [];
40
+ }
41
+ return list.map((raw) => {
42
+ const i = raw;
43
+ return {
44
+ id: i.id || i.ruleId || i.rule_id || null,
45
+ title: i.title || i.description || null,
46
+ description: i.description || null,
47
+ severity: i.severity || null,
48
+ category: i.category || null,
49
+ selector: i.selector || null,
50
+ wcag_reference: i.wcag_reference || null,
51
+ recommendation: i.recommendation || i.recommended_fix || null,
52
+ };
53
+ });
54
+ }
55
+ function toNumber(value) {
56
+ if (typeof value === "number" && Number.isFinite(value))
57
+ return value;
58
+ return null;
59
+ }
60
+ function getOverallScore(scores) {
61
+ if (!scores)
62
+ return null;
63
+ const direct = toNumber(scores.overall ?? scores.ux ?? scores.total);
64
+ if (direct !== null)
65
+ return direct;
66
+ const numeric = Object.values(scores)
67
+ .map((v) => toNumber(v))
68
+ .filter((v) => v !== null);
69
+ if (numeric.length === 0)
70
+ return null;
71
+ const avg = numeric.reduce((sum, v) => sum + v, 0) / numeric.length;
72
+ return Math.round(avg);
73
+ }
74
+ function buildAuditPayload(result, fallbackJobId) {
75
+ const issues = normalizeIssues(result.issues);
76
+ return {
77
+ job_id: result.job_id || fallbackJobId || null,
78
+ url: result.url || null,
79
+ scores: result.scores || null,
80
+ issues,
81
+ };
82
+ }
83
+ // ---------------------------------------------------------------------------
84
+ // Formatters — Legacy URL comparison
85
+ // ---------------------------------------------------------------------------
86
+ function formatLegacyCompareHuman(data) {
87
+ const lines = [];
88
+ lines.push(chalk.bold("Audit Comparison"));
89
+ lines.push(` URL A: ${chalk.cyan(data.urlA)}`);
90
+ lines.push(` URL B: ${chalk.cyan(data.urlB)}`);
91
+ lines.push("");
92
+ // Overall scores
93
+ const deltaStr = data.delta !== null
94
+ ? (data.delta >= 0 ? chalk.green(`+${data.delta}`) : chalk.red(`${data.delta}`))
95
+ : chalk.dim("n/a");
96
+ lines.push(chalk.bold("Overall Scores"));
97
+ lines.push(` A: ${data.overallA ?? "n/a"} B: ${data.overallB ?? "n/a"} Delta: ${deltaStr}`);
98
+ lines.push("");
99
+ // Category deltas
100
+ const entries = Object.entries(data.categoryDeltas);
101
+ if (entries.length > 0) {
102
+ lines.push(chalk.bold("Category Scores"));
103
+ for (const [key, vals] of entries) {
104
+ const d = vals.delta !== null
105
+ ? (vals.delta >= 0 ? chalk.green(`+${vals.delta}`) : chalk.red(`${vals.delta}`))
106
+ : chalk.dim("n/a");
107
+ lines.push(` ${key}: ${vals.a ?? "n/a"} → ${vals.b ?? "n/a"} (${d})`);
108
+ }
109
+ lines.push("");
110
+ }
111
+ // Issue counts
112
+ lines.push(chalk.bold("Issue Counts"));
113
+ lines.push(` A: ${data.issuesA} B: ${data.issuesB}`);
114
+ return lines.join("\n");
115
+ }
116
+ // ---------------------------------------------------------------------------
117
+ // Formatters — LLM comparison
118
+ // ---------------------------------------------------------------------------
119
+ function formatLlmCompareHuman(data, verbose) {
120
+ const lines = [];
121
+ lines.push(chalk.bold(data.headline));
122
+ lines.push("");
123
+ // Score delta
124
+ const d = data.score_delta.overall;
125
+ const deltaStr = d >= 0 ? chalk.green(`+${d}`) : chalk.red(`${d}`);
126
+ lines.push(`Overall delta: ${deltaStr}`);
127
+ if (data.score_delta.categories && verbose) {
128
+ for (const [cat, val] of Object.entries(data.score_delta.categories)) {
129
+ const catDelta = val >= 0 ? chalk.green(`+${val}`) : chalk.red(`${val}`);
130
+ lines.push(` ${cat}: ${catDelta}`);
131
+ }
132
+ }
133
+ lines.push("");
134
+ // Improvements
135
+ if (data.improvements.length > 0) {
136
+ lines.push(chalk.green.bold(`Improvements (${data.improvements.length})`));
137
+ for (const item of data.improvements) {
138
+ lines.push(` ${chalk.green("+")} ${item}`);
139
+ }
140
+ lines.push("");
141
+ }
142
+ // Regressions
143
+ if (data.regressions.length > 0) {
144
+ lines.push(chalk.red.bold(`Regressions (${data.regressions.length})`));
145
+ for (const item of data.regressions) {
146
+ lines.push(` ${chalk.red("-")} ${item}`);
147
+ }
148
+ lines.push("");
149
+ }
150
+ lines.push(chalk.dim(`Unchanged: ${data.unchanged}`));
151
+ if (verbose) {
152
+ lines.push("");
153
+ lines.push(chalk.bold("Analysis"));
154
+ lines.push(data.narrative);
155
+ }
156
+ return lines.join("\n");
157
+ }
158
+ // ---------------------------------------------------------------------------
159
+ // Command Registration
160
+ // ---------------------------------------------------------------------------
161
+ export function registerCompareCommand(program) {
162
+ program
163
+ .command("compare [urlA] [urlB]")
164
+ .description("Compare two audits — by URL (run audits) or by file (LLM analysis)")
165
+ .option("--before <path>", "Baseline audit JSON file")
166
+ .option("--after <path>", "Current audit JSON file")
167
+ .option("--mode <mode>", "Audit depth: basic|standard|deep", "basic")
168
+ .option("--wait", "Wait for audits to complete (URL mode)")
169
+ .option("--timeout <ms>", "Wait timeout in milliseconds", "60000")
170
+ .option("--interval <ms>", "Poll interval in milliseconds", "5000")
171
+ .option("--fail-on-score <n>", "Exit non-zero if score below n (0-100)")
172
+ .option("-f, --format <format>", "Output format: json | human")
173
+ .addHelpText("after", `
174
+ Examples:
175
+ vertaa compare https://a.com https://b.com --wait
176
+ vertaa compare --before baseline.json --after current.json
177
+ vertaa compare --before baseline.json --after current.json --verbose
178
+ `)
179
+ .action(async (urlA, urlB, options, command) => {
180
+ try {
181
+ const globalOpts = command.optsWithGlobals();
182
+ const config = await resolveConfig(globalOpts.config);
183
+ const machineMode = globalOpts.machine || false;
184
+ const verbose = globalOpts.verbose || false;
185
+ const format = resolveCommandFormat("compare", options.format, machineMode);
186
+ // Decide mode: file-based LLM or URL-based legacy
187
+ const isFileMode = options.before || options.after;
188
+ if (isFileMode) {
189
+ await runLlmCompare(options, globalOpts, config, format, verbose);
190
+ }
191
+ else if (urlA && urlB) {
192
+ await runUrlCompare(urlA, urlB, options, globalOpts, config, format);
193
+ }
194
+ else {
195
+ console.error("Error: Provide either two URLs or --before/--after files.");
196
+ console.error("Usage:");
197
+ console.error(" vertaa compare https://a.com https://b.com --wait");
198
+ console.error(" vertaa compare --before baseline.json --after current.json");
199
+ process.exit(ExitCode.ERROR);
200
+ }
201
+ }
202
+ catch (error) {
203
+ console.error("Error:", error instanceof Error ? error.message : String(error));
204
+ process.exit(ExitCode.ERROR);
205
+ }
206
+ });
207
+ }
208
+ // ---------------------------------------------------------------------------
209
+ // LLM-based comparison (new)
210
+ // ---------------------------------------------------------------------------
211
+ async function runLlmCompare(options, globalOpts, config, format, verbose) {
212
+ if (!options.before || !options.after) {
213
+ console.error("Error: Both --before and --after are required for file-based comparison.");
214
+ process.exit(ExitCode.ERROR);
215
+ }
216
+ const beforeInput = await readJsonInput(options.before);
217
+ const afterInput = await readJsonInput(options.after);
218
+ if (!beforeInput || !afterInput) {
219
+ console.error("Error: Could not read audit files.");
220
+ process.exit(ExitCode.ERROR);
221
+ }
222
+ function extractPayload(input) {
223
+ const data = input;
224
+ const inner = (data.data && typeof data.data === "object" ? data.data : data);
225
+ const issues = normalizeIssues(inner.issues);
226
+ return {
227
+ job_id: inner.job_id || null,
228
+ url: inner.url || null,
229
+ scores: inner.scores || null,
230
+ issues,
231
+ };
232
+ }
233
+ const beforePayload = extractPayload(beforeInput);
234
+ const afterPayload = extractPayload(afterInput);
235
+ const base = resolveApiBase(globalOpts.base);
236
+ const apiKey = getApiKey(config.apiKey);
237
+ const spinner = createSpinner("Comparing audits...");
238
+ try {
239
+ const response = await Promise.race([
240
+ apiRequest(base, "/cli/ai/compare", { method: "POST", body: { before: beforePayload, after: afterPayload } }, apiKey),
241
+ new Promise((_, reject) => setTimeout(() => reject(new Error("LLM request timed out")), AI_TIMEOUT_MS)),
242
+ ]);
243
+ succeedSpinner(spinner, "Comparison complete");
244
+ if (format === "json") {
245
+ writeJsonOutput(response.data, "compare");
246
+ }
247
+ else {
248
+ writeOutput(formatLlmCompareHuman(response.data, verbose));
249
+ }
250
+ }
251
+ catch (error) {
252
+ handleAiCommandError(error, "compare", spinner);
253
+ }
254
+ }
255
+ // ---------------------------------------------------------------------------
256
+ // URL-based comparison (legacy backward compat)
257
+ // ---------------------------------------------------------------------------
258
+ async function runUrlCompare(urlA, urlB, options, globalOpts, config, format) {
259
+ const base = resolveApiBase(globalOpts.base);
260
+ const apiKey = getApiKey(config.apiKey);
261
+ const mode = options.mode || "basic";
262
+ const spinner = createSpinner("Starting audits...");
263
+ const jobA = await apiRequest(base, "/audit", {
264
+ method: "POST",
265
+ body: { url: urlA, mode },
266
+ }, apiKey);
267
+ const jobB = await apiRequest(base, "/audit", {
268
+ method: "POST",
269
+ body: { url: urlB, mode },
270
+ }, apiKey);
271
+ if (!options.wait) {
272
+ succeedSpinner(spinner, "Audits queued");
273
+ const payload = { job_a: jobA, job_b: jobB };
274
+ if (format === "json") {
275
+ writeJsonOutput(payload, "compare");
276
+ }
277
+ else {
278
+ writeOutput(`Audit comparison queued:\n Job A: ${jobA.job_id}\n Job B: ${jobB.job_id}`);
279
+ }
280
+ return;
281
+ }
282
+ if (!jobA.job_id || !jobB.job_id) {
283
+ failSpinner(spinner, "Missing job IDs");
284
+ throw new Error("Compare response missing job_id");
285
+ }
286
+ const timeout = parseInt(options.timeout || "60000", 10);
287
+ const interval = parseInt(options.interval || "5000", 10);
288
+ const [resultA, resultB] = await Promise.all([
289
+ waitForAudit(base, jobA.job_id, timeout, interval, apiKey),
290
+ waitForAudit(base, jobB.job_id, timeout, interval, apiKey),
291
+ ]);
292
+ succeedSpinner(spinner, "Audits complete");
293
+ const overallA = getOverallScore(resultA.scores);
294
+ const overallB = getOverallScore(resultB.scores);
295
+ const delta = overallA !== null && overallB !== null ? overallB - overallA : null;
296
+ const scoresA = (resultA.scores || {});
297
+ const scoresB = (resultB.scores || {});
298
+ const keys = new Set([...Object.keys(scoresA), ...Object.keys(scoresB)]);
299
+ const categoryDeltas = {};
300
+ for (const key of keys) {
301
+ const a = toNumber(scoresA[key]);
302
+ const b = toNumber(scoresB[key]);
303
+ categoryDeltas[key] = {
304
+ a,
305
+ b,
306
+ delta: a !== null && b !== null ? b - a : null,
307
+ };
308
+ }
309
+ const issuesA = normalizeIssues(resultA.issues).length;
310
+ const issuesB = normalizeIssues(resultB.issues).length;
311
+ const compareData = {
312
+ urlA,
313
+ urlB,
314
+ jobA: resultA.job_id || "",
315
+ jobB: resultB.job_id || "",
316
+ overallA,
317
+ overallB,
318
+ delta,
319
+ categoryDeltas,
320
+ issuesA,
321
+ issuesB,
322
+ };
323
+ const failOnScore = options.failOnScore ? parseInt(options.failOnScore, 10) : undefined;
324
+ if (failOnScore !== undefined &&
325
+ ((overallA !== null && overallA < failOnScore) ||
326
+ (overallB !== null && overallB < failOnScore))) {
327
+ process.exitCode = 1;
328
+ }
329
+ if (format === "json") {
330
+ writeJsonOutput(compareData, "compare");
331
+ }
332
+ else {
333
+ writeOutput(formatLegacyCompareHuman(compareData));
334
+ }
335
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Doc command for VertaaUX CLI.
3
+ *
4
+ * Accepts full audit JSON (via stdin, --file, or --job) and calls the
5
+ * LLM doc endpoint to produce a "Team Playbook" markdown document from
6
+ * recurring findings — patterns, root causes, correct implementations,
7
+ * and copy/paste checklists.
8
+ *
9
+ * Default output is markdown; use --format json for structured output.
10
+ *
11
+ * Examples:
12
+ * vertaa audit https://example.com --json | vertaa doc
13
+ * vertaa doc --job abc123
14
+ * vertaa doc --file audit.json --team "Frontend Team"
15
+ */
16
+ import { Command } from "commander";
17
+ export declare function registerDocCommand(program: Command): void;
18
+ //# sourceMappingURL=doc.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"doc.d.ts","sourceRoot":"","sources":["../../src/commands/doc.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAqFpC,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAoHzD"}
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Doc command for VertaaUX CLI.
3
+ *
4
+ * Accepts full audit JSON (via stdin, --file, or --job) and calls the
5
+ * LLM doc endpoint to produce a "Team Playbook" markdown document from
6
+ * recurring findings — patterns, root causes, correct implementations,
7
+ * and copy/paste checklists.
8
+ *
9
+ * Default output is markdown; use --format json for structured output.
10
+ *
11
+ * Examples:
12
+ * vertaa audit https://example.com --json | vertaa doc
13
+ * vertaa doc --job abc123
14
+ * vertaa doc --file audit.json --team "Frontend Team"
15
+ */
16
+ import chalk from "chalk";
17
+ import { ExitCode } from "../utils/exit-codes.js";
18
+ import { resolveApiBase, getApiKey, apiRequest } from "../utils/client.js";
19
+ import { resolveConfig } from "../config/loader.js";
20
+ import { writeJsonOutput, writeOutput } from "../output/envelope.js";
21
+ import { resolveCommandFormat } from "../output/formats.js";
22
+ import { createSpinner, succeedSpinner } from "../ui/spinner.js";
23
+ import { readJsonInput } from "../utils/stdin.js";
24
+ import { handleAiCommandError, AI_TIMEOUT_MS } from "../utils/ai-error.js";
25
+ // ---------------------------------------------------------------------------
26
+ // Helpers
27
+ // ---------------------------------------------------------------------------
28
+ function normalizeIssues(issues) {
29
+ let list;
30
+ if (Array.isArray(issues)) {
31
+ list = issues;
32
+ }
33
+ else if (issues && typeof issues === "object") {
34
+ list = Object.values(issues).flatMap((v) => Array.isArray(v) ? v : []);
35
+ }
36
+ else {
37
+ return [];
38
+ }
39
+ return list.map((raw) => {
40
+ const i = raw;
41
+ return {
42
+ id: i.id || i.ruleId || i.rule_id || null,
43
+ title: i.title || i.description || null,
44
+ description: i.description || null,
45
+ severity: i.severity || null,
46
+ category: i.category || null,
47
+ selector: i.selector || null,
48
+ wcag_reference: i.wcag_reference || null,
49
+ recommendation: i.recommendation || i.recommended_fix || null,
50
+ };
51
+ });
52
+ }
53
+ // ---------------------------------------------------------------------------
54
+ // Formatters
55
+ // ---------------------------------------------------------------------------
56
+ function formatDocHuman(data) {
57
+ const lines = [];
58
+ lines.push(chalk.bold(data.title));
59
+ lines.push(chalk.dim("─".repeat(40)));
60
+ lines.push(chalk.dim(`Sections: ${data.sections.join(", ")}`));
61
+ lines.push("");
62
+ lines.push(data.content);
63
+ return lines.join("\n");
64
+ }
65
+ // ---------------------------------------------------------------------------
66
+ // Command Registration
67
+ // ---------------------------------------------------------------------------
68
+ export function registerDocCommand(program) {
69
+ program
70
+ .command("doc")
71
+ .description("Generate a Team Playbook document from recurring audit findings")
72
+ .option("--job <job-id>", "Fetch audit data from a job ID")
73
+ .option("--file <path>", "Load audit JSON from file")
74
+ .option("--team <name>", "Team name for the playbook header")
75
+ .option("-f, --format <format>", "Output format: json | markdown")
76
+ .addHelpText("after", `
77
+ Examples:
78
+ vertaa audit https://example.com --json | vertaa doc
79
+ vertaa doc --job abc123
80
+ vertaa doc --file audit.json --team "Frontend Team"
81
+ `)
82
+ .action(async (options, command) => {
83
+ try {
84
+ const globalOpts = command.optsWithGlobals();
85
+ const config = await resolveConfig(globalOpts.config);
86
+ const machineMode = globalOpts.machine || false;
87
+ const format = resolveCommandFormat("doc", options.format, machineMode);
88
+ // Resolve audit data
89
+ let auditPayload;
90
+ if (options.job) {
91
+ const base = resolveApiBase(globalOpts.base);
92
+ const apiKey = getApiKey(config.apiKey);
93
+ const result = await apiRequest(base, `/audit/${options.job}`, { method: "GET" }, apiKey);
94
+ const issues = normalizeIssues(result.issues);
95
+ auditPayload = {
96
+ job_id: result.job_id || options.job,
97
+ url: result.url || null,
98
+ scores: result.scores || null,
99
+ issues,
100
+ };
101
+ }
102
+ else {
103
+ const input = await readJsonInput(options.file);
104
+ if (!input) {
105
+ console.error("Error: No audit data provided.");
106
+ console.error("Usage:");
107
+ console.error(" vertaa audit https://example.com --json | vertaa doc");
108
+ console.error(" vertaa doc --job <job-id>");
109
+ console.error(" vertaa doc --file audit.json");
110
+ process.exit(ExitCode.ERROR);
111
+ }
112
+ const data = input;
113
+ const innerData = (data.data && typeof data.data === "object" ? data.data : data);
114
+ const issues = normalizeIssues(innerData.issues);
115
+ auditPayload = {
116
+ job_id: innerData.job_id || null,
117
+ url: innerData.url || null,
118
+ scores: innerData.scores || null,
119
+ issues,
120
+ };
121
+ }
122
+ if (!Array.isArray(auditPayload.issues) ||
123
+ auditPayload.issues.length === 0) {
124
+ console.error("Error: No issues found in audit data.");
125
+ process.exit(ExitCode.ERROR);
126
+ }
127
+ // Auth check
128
+ const base = resolveApiBase(globalOpts.base);
129
+ const apiKey = getApiKey(config.apiKey);
130
+ // Call LLM doc API
131
+ const spinner = createSpinner("Generating team playbook...");
132
+ try {
133
+ const response = await Promise.race([
134
+ apiRequest(base, "/cli/ai/doc", {
135
+ method: "POST",
136
+ body: {
137
+ audit: auditPayload,
138
+ teamName: options.team || null,
139
+ },
140
+ }, apiKey),
141
+ new Promise((_, reject) => setTimeout(() => reject(new Error("LLM request timed out")), AI_TIMEOUT_MS)),
142
+ ]);
143
+ succeedSpinner(spinner, "Playbook ready");
144
+ if (format === "json") {
145
+ writeJsonOutput(response.data, "doc");
146
+ }
147
+ else {
148
+ // Default markdown output — just emit the content directly
149
+ writeOutput(format === "markdown" ? response.data.content : formatDocHuman(response.data));
150
+ }
151
+ }
152
+ catch (error) {
153
+ handleAiCommandError(error, "doc", spinner);
154
+ }
155
+ }
156
+ catch (error) {
157
+ console.error("Error:", error instanceof Error ? error.message : String(error));
158
+ process.exit(ExitCode.ERROR);
159
+ }
160
+ });
161
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"download.d.ts","sourceRoot":"","sources":["../../src/commands/download.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAsNpC;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAkB9D"}
1
+ {"version":3,"file":"download.d.ts","sourceRoot":"","sources":["../../src/commands/download.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAuNpC;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAkB9D"}
@@ -7,7 +7,7 @@
7
7
  import fs from "fs";
8
8
  import path from "path";
9
9
  import chalk from "chalk";
10
- import ora from "ora";
10
+ import { createSpinner, succeedSpinner, failSpinner, warnSpinner, infoSpinner } from "../ui/spinner.js";
11
11
  import { loadToken } from "../auth/token-store.js";
12
12
  import { getCIToken } from "../auth/ci-token.js";
13
13
  import { resolveApiBase } from "../utils/client.js";
@@ -47,7 +47,8 @@ async function handleDownload(jobId, options) {
47
47
  const config = await resolveConfig(options.configPath);
48
48
  const apiBase = resolveApiBase(options.base);
49
49
  const outputDir = options.output || DEFAULT_OUTPUT_DIR;
50
- const spinner = ora(`Downloading audit ${jobId}...`).start();
50
+ const spinner = createSpinner(`Downloading audit ${jobId}...`);
51
+ spinner.start();
51
52
  try {
52
53
  // Build request URL
53
54
  let url = `${apiBase}/sync/download/${jobId}`;
@@ -83,14 +84,14 @@ async function handleDownload(jobId, options) {
83
84
  if (isInteractive()) {
84
85
  const overwrite = await confirmAction(`File ${auditPath} already exists. Overwrite?`, false);
85
86
  if (!overwrite) {
86
- spinner.info("Skipping audit results (file exists).");
87
+ infoSpinner(spinner, "Skipping audit results (file exists).");
87
88
  }
88
89
  else {
89
90
  fs.writeFileSync(auditPath, JSON.stringify(result.audit, null, 2), "utf-8");
90
91
  }
91
92
  }
92
93
  else {
93
- spinner.warn(`Skipping ${auditPath} (already exists, use --force to overwrite)`);
94
+ warnSpinner(spinner, `Skipping ${auditPath} (already exists, use --force to overwrite)`);
94
95
  }
95
96
  }
96
97
  else {
@@ -105,14 +106,14 @@ async function handleDownload(jobId, options) {
105
106
  if (isInteractive()) {
106
107
  const overwrite = await confirmAction(`Baseline file already exists. Overwrite?`, false);
107
108
  if (!overwrite) {
108
- spinner.info("Skipping baseline (file exists).");
109
+ infoSpinner(spinner, "Skipping baseline (file exists).");
109
110
  }
110
111
  else {
111
112
  await saveBaseline(result.baseline, baselinePath);
112
113
  }
113
114
  }
114
115
  else {
115
- spinner.warn(`Skipping baseline (already exists, use --force to overwrite)`);
116
+ warnSpinner(spinner, `Skipping baseline (already exists, use --force to overwrite)`);
116
117
  }
117
118
  }
118
119
  else {
@@ -144,7 +145,7 @@ async function handleDownload(jobId, options) {
144
145
  }
145
146
  }
146
147
  }
147
- spinner.succeed("Download complete!");
148
+ succeedSpinner(spinner, "Download complete!");
148
149
  console.error("");
149
150
  console.error(` Job ID: ${result.job_id}`);
150
151
  console.error(` Output: ${resolvedOutputDir}`);
@@ -160,7 +161,7 @@ async function handleDownload(jobId, options) {
160
161
  console.error("");
161
162
  }
162
163
  catch (error) {
163
- spinner.fail("Download failed");
164
+ failSpinner(spinner, "Download failed");
164
165
  console.error(chalk.red("Error:"), error instanceof Error ? error.message : String(error));
165
166
  process.exit(ExitCode.ERROR);
166
167
  }
@@ -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 { Command } from "commander";
17
19
  import type { Issue } from "../baseline/hash.js";
@@ -19,44 +21,23 @@ import type { Issue } from "../baseline/hash.js";
19
21
  * Extended issue type with additional evidence fields.
20
22
  */
21
23
  export interface EvidenceIssue extends Issue {
22
- /** DOM snippet showing the issue */
23
24
  html?: string;
24
- /** Element HTML snippet */
25
25
  element?: string;
26
- /** Screenshot path or URL */
27
26
  screenshot?: string;
28
- /** Help URL for the rule */
29
27
  helpUrl?: string;
30
28
  }
31
29
  /**
32
30
  * Format a full evidence bundle for display.
33
- *
34
- * @param issue - Issue with evidence
35
- * @returns Formatted string for terminal display
36
31
  */
37
32
  export declare function formatEvidenceBundle(issue: EvidenceIssue): string;
38
- /**
39
- * Format evidence as JSON.
40
- */
41
33
  export declare function formatEvidenceJson(issue: EvidenceIssue): string;
42
- /**
43
- * Export for reuse in fix-wizard.
44
- *
45
- * @param issue - Issue to explain
46
- * @returns Formatted evidence string
47
- */
34
+ /** Exported for reuse in fix-wizard. */
48
35
  export declare function explainIssue(issue: EvidenceIssue): string;
49
- /**
50
- * Command options for explain.
51
- */
52
36
  export interface ExplainCommandOptions {
53
37
  job?: string;
54
38
  file?: string;
55
- format?: "json" | "human";
39
+ format?: string;
56
40
  base?: string;
57
41
  }
58
- /**
59
- * Register the explain command with the Commander program.
60
- */
61
42
  export declare function registerExplainCommand(program: Command): void;
62
43
  //# sourceMappingURL=explain.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"explain.d.ts","sourceRoot":"","sources":["../../src/commands/explain.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AASpC,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AAEjD;;GAEG;AACH,MAAM,WAAW,aAAc,SAAQ,KAAK;IAC1C,oCAAoC;IACpC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,2BAA2B;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,6BAA6B;IAC7B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,4BAA4B;IAC5B,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAyED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,aAAa,GAAG,MAAM,CAyDjE;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,aAAa,GAAG,MAAM,CAgB/D;AAUD;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,aAAa,GAAG,MAAM,CAEzD;AAED;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAgK7D"}
1
+ {"version":3,"file":"explain.d.ts","sourceRoot":"","sources":["../../src/commands/explain.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAYpC,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AAMjD;;GAEG;AACH,MAAM,WAAW,aAAc,SAAQ,KAAK;IAC1C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAiFD;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,aAAa,GAAG,MAAM,CAyCjE;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,aAAa,GAAG,MAAM,CAmB/D;AAED,wCAAwC;AACxC,wBAAgB,YAAY,CAAC,KAAK,EAAE,aAAa,GAAG,MAAM,CAEzD;AAqRD,MAAM,WAAW,qBAAqB;IACpC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA+C7D"}