@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,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Report generation command for VertaaUX CLI.
|
|
3
|
+
*
|
|
4
|
+
* Generates consolidated multi-client reports by delegating
|
|
5
|
+
* data aggregation to the server-side consolidated report API.
|
|
6
|
+
* The CLI handles formatting only -- no data computation.
|
|
7
|
+
*
|
|
8
|
+
* Implements 46-06: CLI report command.
|
|
9
|
+
*/
|
|
10
|
+
import chalk from "chalk";
|
|
11
|
+
import fs from "fs";
|
|
12
|
+
import { resolveApiBase, getApiKey, apiRequest } from "../utils/client.js";
|
|
13
|
+
/**
|
|
14
|
+
* Resolve API connection settings.
|
|
15
|
+
*/
|
|
16
|
+
function resolveConnection() {
|
|
17
|
+
return {
|
|
18
|
+
base: resolveApiBase(),
|
|
19
|
+
apiKey: getApiKey(),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Get a traffic-light status indicator for a client based on scores and policy.
|
|
24
|
+
*/
|
|
25
|
+
function getStatusIndicator(client) {
|
|
26
|
+
// Red: any URL with score < 50 or policy violations
|
|
27
|
+
const hasLowScore = client.urls.some((u) => u.overall !== null && u.overall < 50);
|
|
28
|
+
if (hasLowScore || client.policyViolations > 0) {
|
|
29
|
+
return chalk.red("\u25cf");
|
|
30
|
+
}
|
|
31
|
+
// Yellow: some URLs below threshold (< 70)
|
|
32
|
+
const hasMediumScore = client.urls.some((u) => u.overall !== null && u.overall < 70);
|
|
33
|
+
if (hasMediumScore) {
|
|
34
|
+
return chalk.yellow("\u25cf");
|
|
35
|
+
}
|
|
36
|
+
// Green: all URLs passing or above 70
|
|
37
|
+
return chalk.green("\u25cf");
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Get trend arrow for display.
|
|
41
|
+
*/
|
|
42
|
+
function getTrendArrow(trend) {
|
|
43
|
+
switch (trend) {
|
|
44
|
+
case "improving":
|
|
45
|
+
return chalk.green("\u2191");
|
|
46
|
+
case "declining":
|
|
47
|
+
return chalk.red("\u2193");
|
|
48
|
+
case "stable":
|
|
49
|
+
return chalk.dim("-");
|
|
50
|
+
default:
|
|
51
|
+
return chalk.dim("?");
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Pad a string to the right to a given width.
|
|
56
|
+
*/
|
|
57
|
+
function padRight(str, width) {
|
|
58
|
+
if (str.length >= width)
|
|
59
|
+
return str;
|
|
60
|
+
return str + " ".repeat(width - str.length);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Truncate a string to a maximum length with ellipsis.
|
|
64
|
+
*/
|
|
65
|
+
function truncate(str, maxLen) {
|
|
66
|
+
if (str.length <= maxLen)
|
|
67
|
+
return str;
|
|
68
|
+
return str.slice(0, maxLen - 1) + "\u2026";
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Format the consolidated report as human-readable output.
|
|
72
|
+
*/
|
|
73
|
+
function formatHumanReport(report) {
|
|
74
|
+
const lines = [];
|
|
75
|
+
// Header
|
|
76
|
+
lines.push("");
|
|
77
|
+
lines.push(chalk.bold(`Consolidated Report: ${report.orgName}`));
|
|
78
|
+
lines.push(chalk.dim(`Generated: ${new Date(report.generatedAt).toISOString().split("T")[0]}`));
|
|
79
|
+
lines.push("");
|
|
80
|
+
// Per-client sections
|
|
81
|
+
for (const client of report.clients) {
|
|
82
|
+
const indicator = getStatusIndicator(client);
|
|
83
|
+
const avgStr = client.averageScore !== null
|
|
84
|
+
? String(client.averageScore)
|
|
85
|
+
: "n/a";
|
|
86
|
+
lines.push(`${indicator} ${chalk.bold(client.name)} (avg: ${avgStr})`);
|
|
87
|
+
// URL table
|
|
88
|
+
const urlWidth = 40;
|
|
89
|
+
const scoreWidth = 8;
|
|
90
|
+
const policyWidth = 10;
|
|
91
|
+
const trendWidth = 6;
|
|
92
|
+
const header = " " +
|
|
93
|
+
padRight("URL", urlWidth) +
|
|
94
|
+
padRight("Score", scoreWidth) +
|
|
95
|
+
padRight("Policy", policyWidth) +
|
|
96
|
+
padRight("Trend", trendWidth);
|
|
97
|
+
lines.push(chalk.dim(header));
|
|
98
|
+
lines.push(chalk.dim(" " + "-".repeat(urlWidth + scoreWidth + policyWidth + trendWidth)));
|
|
99
|
+
for (const u of client.urls) {
|
|
100
|
+
const scoreStr = u.overall !== null ? String(u.overall) : "n/a";
|
|
101
|
+
const policyStr = u.policyStatus === "passing"
|
|
102
|
+
? chalk.green("pass")
|
|
103
|
+
: u.policyStatus === "failing"
|
|
104
|
+
? chalk.red("fail")
|
|
105
|
+
: chalk.dim("none");
|
|
106
|
+
const trendStr = getTrendArrow(u.trend);
|
|
107
|
+
const row = " " +
|
|
108
|
+
padRight(truncate(u.url, urlWidth - 2), urlWidth) +
|
|
109
|
+
padRight(scoreStr, scoreWidth) +
|
|
110
|
+
padRight(policyStr, policyWidth) +
|
|
111
|
+
trendStr;
|
|
112
|
+
lines.push(row);
|
|
113
|
+
}
|
|
114
|
+
lines.push("");
|
|
115
|
+
}
|
|
116
|
+
// Summary
|
|
117
|
+
lines.push(chalk.bold("Summary"));
|
|
118
|
+
lines.push(` Clients: ${report.summary.totalClients} | URLs: ${report.summary.totalUrls} | Avg Score: ${report.summary.averageScore ?? "n/a"}`);
|
|
119
|
+
lines.push(` Policies: ${chalk.green(String(report.summary.passingPolicies) + " passing")} ${chalk.red(String(report.summary.failingPolicies) + " failing")}`);
|
|
120
|
+
lines.push("");
|
|
121
|
+
return lines.join("\n");
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Format the consolidated report as CSV.
|
|
125
|
+
*/
|
|
126
|
+
function formatCsvReport(report) {
|
|
127
|
+
const lines = [];
|
|
128
|
+
// Header
|
|
129
|
+
lines.push("Client,URL,Overall,Accessibility,Performance,UX,Policy,Trend");
|
|
130
|
+
// Data rows
|
|
131
|
+
for (const client of report.clients) {
|
|
132
|
+
for (const u of client.urls) {
|
|
133
|
+
const overall = u.overall !== null ? String(u.overall) : "";
|
|
134
|
+
const accessibility = u.scores.accessibility !== undefined
|
|
135
|
+
? String(u.scores.accessibility)
|
|
136
|
+
: "";
|
|
137
|
+
const performance = u.scores.performance !== undefined
|
|
138
|
+
? String(u.scores.performance)
|
|
139
|
+
: "";
|
|
140
|
+
const ux = u.scores.ux !== undefined ? String(u.scores.ux) : "";
|
|
141
|
+
const policy = u.policyStatus;
|
|
142
|
+
const trend = u.trend;
|
|
143
|
+
// Escape client name and URL for CSV (wrap in quotes if contains comma)
|
|
144
|
+
const escapeCsv = (s) => s.includes(",") || s.includes('"')
|
|
145
|
+
? `"${s.replace(/"/g, '""')}"`
|
|
146
|
+
: s;
|
|
147
|
+
lines.push([
|
|
148
|
+
escapeCsv(client.name),
|
|
149
|
+
escapeCsv(u.url),
|
|
150
|
+
overall,
|
|
151
|
+
accessibility,
|
|
152
|
+
performance,
|
|
153
|
+
ux,
|
|
154
|
+
policy,
|
|
155
|
+
trend,
|
|
156
|
+
].join(","));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return lines.join("\n") + "\n";
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Register the report command with the Commander program.
|
|
163
|
+
*/
|
|
164
|
+
export function registerReportCommand(program) {
|
|
165
|
+
program
|
|
166
|
+
.command("report")
|
|
167
|
+
.description("Generate consolidated reports")
|
|
168
|
+
.option("--clients", "Generate multi-client consolidated report")
|
|
169
|
+
.option("--client <names>", "Comma-separated client names/slugs (default: all)")
|
|
170
|
+
.option("--format <format>", "Output format: human|json|csv", "human")
|
|
171
|
+
.option("--output <path>", "Write report to file instead of stdout")
|
|
172
|
+
.action(async (options) => {
|
|
173
|
+
try {
|
|
174
|
+
// Currently only consolidated client reports are supported
|
|
175
|
+
if (!options.clients) {
|
|
176
|
+
process.stderr.write(chalk.yellow("The report command requires --clients flag.\n"));
|
|
177
|
+
process.stderr.write(chalk.dim("Usage: vertaa report --clients [--client <names>] [--format human|json|csv]\n"));
|
|
178
|
+
process.exit(1);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const { base, apiKey } = resolveConnection();
|
|
182
|
+
// Build API URL with optional client filter
|
|
183
|
+
let apiPath = "/reports/consolidated";
|
|
184
|
+
if (options.client) {
|
|
185
|
+
const encoded = encodeURIComponent(options.client);
|
|
186
|
+
apiPath += `?clients=${encoded}`;
|
|
187
|
+
}
|
|
188
|
+
const report = await apiRequest(base, apiPath, { method: "GET" }, apiKey);
|
|
189
|
+
// Format output
|
|
190
|
+
let output;
|
|
191
|
+
if (options.format === "json") {
|
|
192
|
+
output = JSON.stringify(report, null, 2) + "\n";
|
|
193
|
+
}
|
|
194
|
+
else if (options.format === "csv") {
|
|
195
|
+
output = formatCsvReport(report);
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
output = formatHumanReport(report);
|
|
199
|
+
}
|
|
200
|
+
// Write to file or stdout
|
|
201
|
+
if (options.output) {
|
|
202
|
+
fs.writeFileSync(options.output, output, "utf-8");
|
|
203
|
+
process.stderr.write(chalk.green(`Report written to ${options.output}\n`));
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
process.stdout.write(output);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
process.stderr.write(chalk.red(`Error: ${error instanceof Error ? error.message : String(error)}\n`));
|
|
211
|
+
process.exit(1);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Suggest command for VertaaUX CLI.
|
|
3
|
+
*
|
|
4
|
+
* Converts natural language intent into exact CLI command(s).
|
|
5
|
+
* Uses local command catalog matching first, with API fallback
|
|
6
|
+
* for complex intents.
|
|
7
|
+
*
|
|
8
|
+
* Examples:
|
|
9
|
+
* vertaa suggest "check contrast issues"
|
|
10
|
+
* vertaa suggest "compare two pages"
|
|
11
|
+
* vertaa suggest "set up CI quality gate"
|
|
12
|
+
*/
|
|
13
|
+
import { Command } from "commander";
|
|
14
|
+
/**
|
|
15
|
+
* Register the suggest command with the Commander program.
|
|
16
|
+
*/
|
|
17
|
+
export declare function registerSuggestCommand(program: Command): void;
|
|
18
|
+
//# sourceMappingURL=suggest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"suggest.d.ts","sourceRoot":"","sources":["../../src/commands/suggest.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAoDpC;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAqH7D"}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Suggest command for VertaaUX CLI.
|
|
3
|
+
*
|
|
4
|
+
* Converts natural language intent into exact CLI command(s).
|
|
5
|
+
* Uses local command catalog matching first, with API fallback
|
|
6
|
+
* for complex intents.
|
|
7
|
+
*
|
|
8
|
+
* Examples:
|
|
9
|
+
* vertaa suggest "check contrast issues"
|
|
10
|
+
* vertaa suggest "compare two pages"
|
|
11
|
+
* vertaa suggest "set up CI quality gate"
|
|
12
|
+
*/
|
|
13
|
+
import chalk from "chalk";
|
|
14
|
+
import { ExitCode } from "../utils/exit-codes.js";
|
|
15
|
+
import { resolveApiBase, getApiKey, hasApiKey, apiRequest } from "../utils/client.js";
|
|
16
|
+
import { resolveConfig } from "../config/loader.js";
|
|
17
|
+
import { writeJsonOutput, writeOutput } from "../output/envelope.js";
|
|
18
|
+
import { resolveCommandFormat } from "../output/formats.js";
|
|
19
|
+
import { createSpinner, succeedSpinner, failSpinner } from "../ui/spinner.js";
|
|
20
|
+
import { findMatches } from "../prompts/command-catalog.js";
|
|
21
|
+
import { AI_TIMEOUT_MS } from "../utils/ai-error.js";
|
|
22
|
+
/**
|
|
23
|
+
* Format suggestion for human-readable output.
|
|
24
|
+
*/
|
|
25
|
+
function formatSuggestHuman(results) {
|
|
26
|
+
const lines = [];
|
|
27
|
+
for (const result of results) {
|
|
28
|
+
lines.push(` ${chalk.cyan.bold("$")} ${chalk.bold(result.command)}`);
|
|
29
|
+
lines.push(` ${chalk.dim(result.explanation)}`);
|
|
30
|
+
lines.push("");
|
|
31
|
+
}
|
|
32
|
+
if (results.length === 0) {
|
|
33
|
+
lines.push(chalk.yellow("No matching commands found."));
|
|
34
|
+
lines.push(chalk.dim("Try: vertaa --help"));
|
|
35
|
+
}
|
|
36
|
+
return lines.join("\n");
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Format suggestion for JSON output.
|
|
40
|
+
*/
|
|
41
|
+
function formatSuggestJson(results) {
|
|
42
|
+
return {
|
|
43
|
+
suggestions: results.map((r) => ({
|
|
44
|
+
command: r.command,
|
|
45
|
+
explanation: r.explanation,
|
|
46
|
+
source: r.source,
|
|
47
|
+
confidence: r.confidence,
|
|
48
|
+
})),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Register the suggest command with the Commander program.
|
|
53
|
+
*/
|
|
54
|
+
export function registerSuggestCommand(program) {
|
|
55
|
+
program
|
|
56
|
+
.command("suggest <intent...>")
|
|
57
|
+
.description("Convert natural language to exact CLI command(s)")
|
|
58
|
+
.option("-f, --format <format>", "Output format: json | human")
|
|
59
|
+
.addHelpText("after", `
|
|
60
|
+
Examples:
|
|
61
|
+
vertaa suggest "check accessibility"
|
|
62
|
+
vertaa suggest "audit my site for CI"
|
|
63
|
+
vertaa suggest "compare two pages"
|
|
64
|
+
vertaa suggest "what failed in my audit"
|
|
65
|
+
`)
|
|
66
|
+
.action(async (intentParts, options, command) => {
|
|
67
|
+
try {
|
|
68
|
+
const globalOpts = command.optsWithGlobals();
|
|
69
|
+
const config = await resolveConfig(globalOpts.config);
|
|
70
|
+
const machineMode = globalOpts.machine || false;
|
|
71
|
+
const format = resolveCommandFormat("suggest", options.format, machineMode);
|
|
72
|
+
const intent = intentParts.join(" ");
|
|
73
|
+
// Step 1: Local fuzzy match against command catalog
|
|
74
|
+
const localMatches = findMatches(intent);
|
|
75
|
+
let results;
|
|
76
|
+
if (localMatches.length > 0 && localMatches[0].score >= 0.2) {
|
|
77
|
+
// Good local matches — use them
|
|
78
|
+
results = localMatches.map((m) => ({
|
|
79
|
+
command: m.entry.command,
|
|
80
|
+
explanation: m.entry.description,
|
|
81
|
+
source: "local",
|
|
82
|
+
confidence: Math.round(m.score * 100),
|
|
83
|
+
}));
|
|
84
|
+
}
|
|
85
|
+
else if (hasApiKey(config)) {
|
|
86
|
+
// No strong local match — try API
|
|
87
|
+
const spinner = createSpinner("Thinking...");
|
|
88
|
+
try {
|
|
89
|
+
const base = resolveApiBase(globalOpts.base);
|
|
90
|
+
const apiKey = getApiKey(config.apiKey);
|
|
91
|
+
const response = await Promise.race([
|
|
92
|
+
apiRequest(base, "/cli/ai/suggest", {
|
|
93
|
+
method: "POST",
|
|
94
|
+
body: { intent },
|
|
95
|
+
}, apiKey),
|
|
96
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("LLM request timed out")), AI_TIMEOUT_MS)),
|
|
97
|
+
]);
|
|
98
|
+
succeedSpinner(spinner, "Done");
|
|
99
|
+
if (response.data?.suggestions?.length) {
|
|
100
|
+
results = response.data.suggestions.map((s) => ({
|
|
101
|
+
command: s.command,
|
|
102
|
+
explanation: s.explanation,
|
|
103
|
+
source: "api",
|
|
104
|
+
confidence: 80,
|
|
105
|
+
}));
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
// API returned nothing — fall back to partial local matches
|
|
109
|
+
results = localMatches.map((m) => ({
|
|
110
|
+
command: m.entry.command,
|
|
111
|
+
explanation: m.entry.description,
|
|
112
|
+
source: "local",
|
|
113
|
+
confidence: Math.round(m.score * 100),
|
|
114
|
+
}));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
// Suggest degrades gracefully — fall back to local catalog
|
|
119
|
+
// instead of hard exit via handleAiCommandError
|
|
120
|
+
failSpinner(spinner, "API unavailable — using local catalog");
|
|
121
|
+
// API failed — use whatever local matches we have
|
|
122
|
+
results = localMatches.map((m) => ({
|
|
123
|
+
command: m.entry.command,
|
|
124
|
+
explanation: m.entry.description,
|
|
125
|
+
source: "local",
|
|
126
|
+
confidence: Math.round(m.score * 100),
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
// No API key — use partial local matches
|
|
132
|
+
results = localMatches.map((m) => ({
|
|
133
|
+
command: m.entry.command,
|
|
134
|
+
explanation: m.entry.description,
|
|
135
|
+
source: "local",
|
|
136
|
+
confidence: Math.round(m.score * 100),
|
|
137
|
+
}));
|
|
138
|
+
}
|
|
139
|
+
// Output
|
|
140
|
+
if (format === "json") {
|
|
141
|
+
writeJsonOutput(formatSuggestJson(results), "suggest");
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
writeOutput(formatSuggestHuman(results));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
console.error("Error:", error instanceof Error ? error.message : String(error));
|
|
149
|
+
process.exit(ExitCode.ERROR);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Triage command for VertaaUX CLI.
|
|
3
|
+
*
|
|
4
|
+
* Accepts full audit JSON (via stdin, --file, or --job) and calls the
|
|
5
|
+
* LLM triage endpoint to produce P0/P1/P2 priority buckets with effort
|
|
6
|
+
* estimates and a quick-wins list.
|
|
7
|
+
*
|
|
8
|
+
* Default output shows bucket counts; --verbose expands each bucket.
|
|
9
|
+
*
|
|
10
|
+
* Examples:
|
|
11
|
+
* vertaa audit https://example.com --json | vertaa triage
|
|
12
|
+
* vertaa triage --job abc123
|
|
13
|
+
* vertaa triage --file audit.json --verbose
|
|
14
|
+
*/
|
|
15
|
+
import { Command } from "commander";
|
|
16
|
+
export declare function registerTriageCommand(program: Command): void;
|
|
17
|
+
//# sourceMappingURL=triage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"triage.d.ts","sourceRoot":"","sources":["../../src/commands/triage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAuJpC,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA+G5D"}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Triage command for VertaaUX CLI.
|
|
3
|
+
*
|
|
4
|
+
* Accepts full audit JSON (via stdin, --file, or --job) and calls the
|
|
5
|
+
* LLM triage endpoint to produce P0/P1/P2 priority buckets with effort
|
|
6
|
+
* estimates and a quick-wins list.
|
|
7
|
+
*
|
|
8
|
+
* Default output shows bucket counts; --verbose expands each bucket.
|
|
9
|
+
*
|
|
10
|
+
* Examples:
|
|
11
|
+
* vertaa audit https://example.com --json | vertaa triage
|
|
12
|
+
* vertaa triage --job abc123
|
|
13
|
+
* vertaa triage --file audit.json --verbose
|
|
14
|
+
*/
|
|
15
|
+
import chalk from "chalk";
|
|
16
|
+
import { ExitCode } from "../utils/exit-codes.js";
|
|
17
|
+
import { resolveApiBase, getApiKey, apiRequest } from "../utils/client.js";
|
|
18
|
+
import { resolveConfig } from "../config/loader.js";
|
|
19
|
+
import { writeJsonOutput, writeOutput } from "../output/envelope.js";
|
|
20
|
+
import { resolveCommandFormat } from "../output/formats.js";
|
|
21
|
+
import { createSpinner, succeedSpinner } from "../ui/spinner.js";
|
|
22
|
+
import { readJsonInput } from "../utils/stdin.js";
|
|
23
|
+
import { handleAiCommandError, AI_TIMEOUT_MS } from "../utils/ai-error.js";
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Helpers
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
function normalizeIssues(issues) {
|
|
28
|
+
let list;
|
|
29
|
+
if (Array.isArray(issues)) {
|
|
30
|
+
list = issues;
|
|
31
|
+
}
|
|
32
|
+
else if (issues && typeof issues === "object") {
|
|
33
|
+
list = Object.values(issues).flatMap((v) => Array.isArray(v) ? v : []);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
return list.map((raw) => {
|
|
39
|
+
const i = raw;
|
|
40
|
+
return {
|
|
41
|
+
id: i.id || i.ruleId || i.rule_id || null,
|
|
42
|
+
title: i.title || i.description || null,
|
|
43
|
+
description: i.description || null,
|
|
44
|
+
severity: i.severity || null,
|
|
45
|
+
category: i.category || null,
|
|
46
|
+
selector: i.selector || null,
|
|
47
|
+
wcag_reference: i.wcag_reference || null,
|
|
48
|
+
recommendation: i.recommendation || i.recommended_fix || null,
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// Formatters
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
const EFFORT_LABELS = {
|
|
56
|
+
trivial: chalk.green("trivial"),
|
|
57
|
+
small: chalk.green("small"),
|
|
58
|
+
medium: chalk.yellow("medium"),
|
|
59
|
+
large: chalk.red("large"),
|
|
60
|
+
};
|
|
61
|
+
function formatEffort(effort) {
|
|
62
|
+
return EFFORT_LABELS[effort] || chalk.dim(effort);
|
|
63
|
+
}
|
|
64
|
+
function formatTriageHuman(data, verbose) {
|
|
65
|
+
const lines = [];
|
|
66
|
+
// P0
|
|
67
|
+
lines.push(chalk.red.bold(`P0 Critical (${data.p0_critical.length})`));
|
|
68
|
+
if (verbose) {
|
|
69
|
+
for (const item of data.p0_critical) {
|
|
70
|
+
lines.push(` ${chalk.red(">")} ${chalk.bold(item.title)}${item.id ? chalk.dim(` (${item.id})`) : ""}`);
|
|
71
|
+
lines.push(` ${item.reason}`);
|
|
72
|
+
lines.push(` Effort: ${formatEffort(item.effort)}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
else if (data.p0_critical.length > 0) {
|
|
76
|
+
lines.push(` ${data.p0_critical.map((i) => i.title).join(", ")}`);
|
|
77
|
+
}
|
|
78
|
+
lines.push("");
|
|
79
|
+
// P1
|
|
80
|
+
lines.push(chalk.yellow.bold(`P1 Important (${data.p1_important.length})`));
|
|
81
|
+
if (verbose) {
|
|
82
|
+
for (const item of data.p1_important) {
|
|
83
|
+
lines.push(` ${chalk.yellow(">")} ${chalk.bold(item.title)}${item.id ? chalk.dim(` (${item.id})`) : ""}`);
|
|
84
|
+
lines.push(` ${item.reason}`);
|
|
85
|
+
lines.push(` Effort: ${formatEffort(item.effort)}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
else if (data.p1_important.length > 0) {
|
|
89
|
+
lines.push(` ${data.p1_important.map((i) => i.title).join(", ")}`);
|
|
90
|
+
}
|
|
91
|
+
lines.push("");
|
|
92
|
+
// P2
|
|
93
|
+
lines.push(chalk.cyan.bold(`P2 Nice to Have (${data.p2_nice_to_have.length})`));
|
|
94
|
+
if (verbose) {
|
|
95
|
+
for (const item of data.p2_nice_to_have) {
|
|
96
|
+
lines.push(` ${chalk.cyan(">")} ${chalk.bold(item.title)}${item.id ? chalk.dim(` (${item.id})`) : ""}`);
|
|
97
|
+
lines.push(` ${item.reason}`);
|
|
98
|
+
lines.push(` Effort: ${formatEffort(item.effort)}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
else if (data.p2_nice_to_have.length > 0) {
|
|
102
|
+
lines.push(` ${data.p2_nice_to_have.map((i) => i.title).join(", ")}`);
|
|
103
|
+
}
|
|
104
|
+
lines.push("");
|
|
105
|
+
// Quick wins
|
|
106
|
+
if (data.quick_wins.length > 0) {
|
|
107
|
+
lines.push(chalk.green.bold("Quick Wins (< 5 min each)"));
|
|
108
|
+
for (const win of data.quick_wins) {
|
|
109
|
+
lines.push(` ${chalk.green("*")} ${win}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return lines.join("\n");
|
|
113
|
+
}
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
// Command Registration
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
export function registerTriageCommand(program) {
|
|
118
|
+
program
|
|
119
|
+
.command("triage")
|
|
120
|
+
.description("Prioritize audit findings into P0/P1/P2 buckets with effort estimates")
|
|
121
|
+
.option("--job <job-id>", "Fetch audit data from a job ID")
|
|
122
|
+
.option("--file <path>", "Load audit JSON from file")
|
|
123
|
+
.option("-f, --format <format>", "Output format: json | human")
|
|
124
|
+
.addHelpText("after", `
|
|
125
|
+
Examples:
|
|
126
|
+
vertaa audit https://example.com --json | vertaa triage
|
|
127
|
+
vertaa triage --job abc123
|
|
128
|
+
vertaa triage --file audit.json --verbose
|
|
129
|
+
`)
|
|
130
|
+
.action(async (options, command) => {
|
|
131
|
+
try {
|
|
132
|
+
const globalOpts = command.optsWithGlobals();
|
|
133
|
+
const config = await resolveConfig(globalOpts.config);
|
|
134
|
+
const machineMode = globalOpts.machine || false;
|
|
135
|
+
const verbose = globalOpts.verbose || false;
|
|
136
|
+
const format = resolveCommandFormat("triage", options.format, machineMode);
|
|
137
|
+
// Resolve audit data
|
|
138
|
+
let auditPayload;
|
|
139
|
+
if (options.job) {
|
|
140
|
+
// Fetch from API
|
|
141
|
+
const base = resolveApiBase(globalOpts.base);
|
|
142
|
+
const apiKey = getApiKey(config.apiKey);
|
|
143
|
+
const result = await apiRequest(base, `/audit/${options.job}`, { method: "GET" }, apiKey);
|
|
144
|
+
const issues = normalizeIssues(result.issues);
|
|
145
|
+
auditPayload = {
|
|
146
|
+
job_id: result.job_id || options.job,
|
|
147
|
+
url: result.url || null,
|
|
148
|
+
scores: result.scores || null,
|
|
149
|
+
issues,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
// Read from stdin or --file
|
|
154
|
+
const input = await readJsonInput(options.file);
|
|
155
|
+
if (!input) {
|
|
156
|
+
console.error("Error: No audit data provided.");
|
|
157
|
+
console.error("Usage:");
|
|
158
|
+
console.error(" vertaa audit https://example.com --json | vertaa triage");
|
|
159
|
+
console.error(" vertaa triage --job <job-id>");
|
|
160
|
+
console.error(" vertaa triage --file audit.json");
|
|
161
|
+
process.exit(ExitCode.ERROR);
|
|
162
|
+
}
|
|
163
|
+
const data = input;
|
|
164
|
+
const innerData = (data.data && typeof data.data === "object" ? data.data : data);
|
|
165
|
+
const issues = normalizeIssues(innerData.issues);
|
|
166
|
+
auditPayload = {
|
|
167
|
+
job_id: innerData.job_id || null,
|
|
168
|
+
url: innerData.url || null,
|
|
169
|
+
scores: innerData.scores || null,
|
|
170
|
+
issues,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
if (!Array.isArray(auditPayload.issues) ||
|
|
174
|
+
auditPayload.issues.length === 0) {
|
|
175
|
+
console.error("Error: No issues found in audit data.");
|
|
176
|
+
process.exit(ExitCode.ERROR);
|
|
177
|
+
}
|
|
178
|
+
// Auth check
|
|
179
|
+
const base = resolveApiBase(globalOpts.base);
|
|
180
|
+
const apiKey = getApiKey(config.apiKey);
|
|
181
|
+
// Call LLM triage API
|
|
182
|
+
const spinner = createSpinner("Triaging findings...");
|
|
183
|
+
try {
|
|
184
|
+
const response = await Promise.race([
|
|
185
|
+
apiRequest(base, "/cli/ai/triage", { method: "POST", body: { audit: auditPayload } }, apiKey),
|
|
186
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("LLM request timed out")), AI_TIMEOUT_MS)),
|
|
187
|
+
]);
|
|
188
|
+
succeedSpinner(spinner, "Triage complete");
|
|
189
|
+
if (format === "json") {
|
|
190
|
+
writeJsonOutput(response.data, "triage");
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
writeOutput(formatTriageHuman(response.data, verbose));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
handleAiCommandError(error, "triage", spinner);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
console.error("Error:", error instanceof Error ? error.message : String(error));
|
|
202
|
+
process.exit(ExitCode.ERROR);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"upload.d.ts","sourceRoot":"","sources":["../../src/commands/upload.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"upload.d.ts","sourceRoot":"","sources":["../../src/commands/upload.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA0LpC;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAiB5D"}
|
package/dist/commands/upload.js
CHANGED
|
@@ -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 } 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";
|
|
@@ -45,7 +45,8 @@ async function handleUpload(jobId, options) {
|
|
|
45
45
|
const config = await resolveConfig(options.configPath);
|
|
46
46
|
const apiBase = resolveApiBase(options.base);
|
|
47
47
|
// Determine what to upload
|
|
48
|
-
const spinner =
|
|
48
|
+
const spinner = createSpinner("Preparing upload...");
|
|
49
|
+
spinner.start();
|
|
49
50
|
try {
|
|
50
51
|
// If no job ID, find the most recent local result
|
|
51
52
|
let targetJobId = jobId;
|
|
@@ -71,12 +72,12 @@ async function handleUpload(jobId, options) {
|
|
|
71
72
|
}
|
|
72
73
|
}
|
|
73
74
|
if (!targetJobId) {
|
|
74
|
-
spinner
|
|
75
|
+
failSpinner(spinner, "No job ID provided and no local results found.");
|
|
75
76
|
console.error("Run `vertaa audit --save-trace` to save local results first.");
|
|
76
77
|
process.exit(ExitCode.ERROR);
|
|
77
78
|
}
|
|
78
79
|
}
|
|
79
|
-
spinner.
|
|
80
|
+
spinner.setText(`Uploading audit ${targetJobId}...`);
|
|
80
81
|
// Prepare upload payload
|
|
81
82
|
const payload = {
|
|
82
83
|
job_id: targetJobId,
|
|
@@ -108,7 +109,7 @@ async function handleUpload(jobId, options) {
|
|
|
108
109
|
const baseline = await loadBaseline(DEFAULT_BASELINE_PATH);
|
|
109
110
|
if (baseline) {
|
|
110
111
|
payload.baseline = baseline;
|
|
111
|
-
spinner.
|
|
112
|
+
spinner.setText(`Uploading audit ${targetJobId} with baseline...`);
|
|
112
113
|
}
|
|
113
114
|
}
|
|
114
115
|
// Make API request
|
|
@@ -128,7 +129,7 @@ async function handleUpload(jobId, options) {
|
|
|
128
129
|
if (!result.success) {
|
|
129
130
|
throw new Error(result.error?.message || "Upload failed");
|
|
130
131
|
}
|
|
131
|
-
spinner
|
|
132
|
+
succeedSpinner(spinner, "Upload complete!");
|
|
132
133
|
console.error("");
|
|
133
134
|
console.error(` Job ID: ${result.job_id}`);
|
|
134
135
|
console.error(` URL: ${chalk.cyan(result.url)}`);
|
|
@@ -136,7 +137,7 @@ async function handleUpload(jobId, options) {
|
|
|
136
137
|
console.error("Share this URL with your team to view the results.");
|
|
137
138
|
}
|
|
138
139
|
catch (error) {
|
|
139
|
-
spinner
|
|
140
|
+
failSpinner(spinner, "Upload failed");
|
|
140
141
|
console.error(chalk.red("Error:"), error instanceof Error ? error.message : String(error));
|
|
141
142
|
process.exit(ExitCode.ERROR);
|
|
142
143
|
}
|