@vertaaux/cli 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +58 -2
  3. package/dist/auth/device-flow.d.ts.map +1 -1
  4. package/dist/auth/device-flow.js +46 -14
  5. package/dist/commands/audit.d.ts +2 -0
  6. package/dist/commands/audit.d.ts.map +1 -1
  7. package/dist/commands/audit.js +167 -8
  8. package/dist/commands/client.d.ts +14 -0
  9. package/dist/commands/client.d.ts.map +1 -0
  10. package/dist/commands/client.js +362 -0
  11. package/dist/commands/compare.d.ts +20 -0
  12. package/dist/commands/compare.d.ts.map +1 -0
  13. package/dist/commands/compare.js +335 -0
  14. package/dist/commands/doc.d.ts +18 -0
  15. package/dist/commands/doc.d.ts.map +1 -0
  16. package/dist/commands/doc.js +161 -0
  17. package/dist/commands/download.d.ts.map +1 -1
  18. package/dist/commands/download.js +9 -8
  19. package/dist/commands/drift.d.ts +15 -0
  20. package/dist/commands/drift.d.ts.map +1 -0
  21. package/dist/commands/drift.js +309 -0
  22. package/dist/commands/explain.d.ts +14 -33
  23. package/dist/commands/explain.d.ts.map +1 -1
  24. package/dist/commands/explain.js +277 -179
  25. package/dist/commands/fix-plan.d.ts +15 -0
  26. package/dist/commands/fix-plan.d.ts.map +1 -0
  27. package/dist/commands/fix-plan.js +182 -0
  28. package/dist/commands/patch-review.d.ts +14 -0
  29. package/dist/commands/patch-review.d.ts.map +1 -0
  30. package/dist/commands/patch-review.js +200 -0
  31. package/dist/commands/protect.d.ts +16 -0
  32. package/dist/commands/protect.d.ts.map +1 -0
  33. package/dist/commands/protect.js +323 -0
  34. package/dist/commands/release-notes.d.ts +17 -0
  35. package/dist/commands/release-notes.d.ts.map +1 -0
  36. package/dist/commands/release-notes.js +145 -0
  37. package/dist/commands/report.d.ts +15 -0
  38. package/dist/commands/report.d.ts.map +1 -0
  39. package/dist/commands/report.js +214 -0
  40. package/dist/commands/suggest.d.ts +18 -0
  41. package/dist/commands/suggest.d.ts.map +1 -0
  42. package/dist/commands/suggest.js +152 -0
  43. package/dist/commands/triage.d.ts +17 -0
  44. package/dist/commands/triage.d.ts.map +1 -0
  45. package/dist/commands/triage.js +205 -0
  46. package/dist/commands/upload.d.ts.map +1 -1
  47. package/dist/commands/upload.js +8 -7
  48. package/dist/index.js +62 -25
  49. package/dist/output/formats.d.ts.map +1 -1
  50. package/dist/output/formats.js +18 -2
  51. package/dist/output/human.d.ts +1 -10
  52. package/dist/output/human.d.ts.map +1 -1
  53. package/dist/output/human.js +26 -98
  54. package/dist/policy/sync.d.ts +67 -0
  55. package/dist/policy/sync.d.ts.map +1 -0
  56. package/dist/policy/sync.js +147 -0
  57. package/dist/prompts/command-catalog.d.ts +46 -0
  58. package/dist/prompts/command-catalog.d.ts.map +1 -0
  59. package/dist/prompts/command-catalog.js +187 -0
  60. package/dist/ui/spinner.d.ts +10 -35
  61. package/dist/ui/spinner.d.ts.map +1 -1
  62. package/dist/ui/spinner.js +11 -58
  63. package/dist/ui/table.d.ts +1 -18
  64. package/dist/ui/table.d.ts.map +1 -1
  65. package/dist/ui/table.js +56 -163
  66. package/dist/utils/ai-error.d.ts +48 -0
  67. package/dist/utils/ai-error.d.ts.map +1 -0
  68. package/dist/utils/ai-error.js +190 -0
  69. package/dist/utils/detect-env.d.ts +6 -8
  70. package/dist/utils/detect-env.d.ts.map +1 -1
  71. package/dist/utils/detect-env.js +6 -25
  72. package/dist/utils/stdin.d.ts +50 -0
  73. package/dist/utils/stdin.d.ts.map +1 -0
  74. package/dist/utils/stdin.js +93 -0
  75. package/package.json +11 -7
@@ -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;AAyLpC;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAiB5D"}
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"}
@@ -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 } 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 = ora("Preparing upload...").start();
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.fail("No job ID provided and no local results found.");
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.text = `Uploading audit ${targetJobId}...`;
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.text = `Uploading audit ${targetJobId} with baseline...`;
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.succeed("Upload complete!");
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.fail("Upload failed");
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
  }