@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
|
@@ -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;
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drift detection command for VertaaUX CLI.
|
|
3
|
+
*
|
|
4
|
+
* Compares current audit scores against a baseline and reports
|
|
5
|
+
* per-category regressions with delta magnitudes. Used in local
|
|
6
|
+
* workflows and CI pipelines to detect score regressions before merging.
|
|
7
|
+
*
|
|
8
|
+
* Implements DRIFT-01: CLI drift check command.
|
|
9
|
+
*/
|
|
10
|
+
import { Command } from "commander";
|
|
11
|
+
/**
|
|
12
|
+
* Register the drift command with the Commander program.
|
|
13
|
+
*/
|
|
14
|
+
export declare function registerDriftCommand(program: Command): void;
|
|
15
|
+
//# sourceMappingURL=drift.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"drift.d.ts","sourceRoot":"","sources":["../../src/commands/drift.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAyNpC;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAsK3D"}
|