@vertaaux/cli 0.2.3 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +58 -2
  3. package/dist/auth/device-flow.js +6 -8
  4. package/dist/commands/audit.d.ts +2 -0
  5. package/dist/commands/audit.d.ts.map +1 -1
  6. package/dist/commands/audit.js +165 -6
  7. package/dist/commands/compare.d.ts +20 -0
  8. package/dist/commands/compare.d.ts.map +1 -0
  9. package/dist/commands/compare.js +335 -0
  10. package/dist/commands/doc.d.ts +18 -0
  11. package/dist/commands/doc.d.ts.map +1 -0
  12. package/dist/commands/doc.js +161 -0
  13. package/dist/commands/download.d.ts.map +1 -1
  14. package/dist/commands/download.js +9 -8
  15. package/dist/commands/explain.d.ts +14 -33
  16. package/dist/commands/explain.d.ts.map +1 -1
  17. package/dist/commands/explain.js +277 -179
  18. package/dist/commands/fix-plan.d.ts +15 -0
  19. package/dist/commands/fix-plan.d.ts.map +1 -0
  20. package/dist/commands/fix-plan.js +182 -0
  21. package/dist/commands/patch-review.d.ts +14 -0
  22. package/dist/commands/patch-review.d.ts.map +1 -0
  23. package/dist/commands/patch-review.js +200 -0
  24. package/dist/commands/release-notes.d.ts +17 -0
  25. package/dist/commands/release-notes.d.ts.map +1 -0
  26. package/dist/commands/release-notes.js +145 -0
  27. package/dist/commands/suggest.d.ts +18 -0
  28. package/dist/commands/suggest.d.ts.map +1 -0
  29. package/dist/commands/suggest.js +152 -0
  30. package/dist/commands/triage.d.ts +17 -0
  31. package/dist/commands/triage.d.ts.map +1 -0
  32. package/dist/commands/triage.js +205 -0
  33. package/dist/commands/upload.d.ts.map +1 -1
  34. package/dist/commands/upload.js +8 -7
  35. package/dist/index.js +62 -25
  36. package/dist/output/formats.d.ts.map +1 -1
  37. package/dist/output/formats.js +14 -0
  38. package/dist/output/human.d.ts +1 -10
  39. package/dist/output/human.d.ts.map +1 -1
  40. package/dist/output/human.js +26 -98
  41. package/dist/prompts/command-catalog.d.ts +46 -0
  42. package/dist/prompts/command-catalog.d.ts.map +1 -0
  43. package/dist/prompts/command-catalog.js +187 -0
  44. package/dist/ui/spinner.d.ts +10 -35
  45. package/dist/ui/spinner.d.ts.map +1 -1
  46. package/dist/ui/spinner.js +11 -58
  47. package/dist/ui/table.d.ts +1 -18
  48. package/dist/ui/table.d.ts.map +1 -1
  49. package/dist/ui/table.js +56 -163
  50. package/dist/utils/ai-error.d.ts +48 -0
  51. package/dist/utils/ai-error.d.ts.map +1 -0
  52. package/dist/utils/ai-error.js +190 -0
  53. package/dist/utils/detect-env.d.ts +6 -8
  54. package/dist/utils/detect-env.d.ts.map +1 -1
  55. package/dist/utils/detect-env.js +6 -25
  56. package/dist/utils/stdin.d.ts +50 -0
  57. package/dist/utils/stdin.d.ts.map +1 -0
  58. package/dist/utils/stdin.js +93 -0
  59. package/package.json +9 -5
@@ -0,0 +1,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
  }
package/dist/index.js CHANGED
@@ -10,6 +10,7 @@ import fs from "fs";
10
10
  import path from "path";
11
11
  import { fileURLToPath } from "url";
12
12
  import { Command } from "commander";
13
+ import { setColorEnabled, shouldUseColor } from "@vertaaux/tui";
13
14
  import { showBanner, getVersion } from "./ui/banner.js";
14
15
  import { registerAuditCommand } from "./commands/audit.js";
15
16
  import { registerBaselineCommand } from "./commands/baseline.js";
@@ -22,6 +23,13 @@ import { registerUploadCommand } from "./commands/upload.js";
22
23
  import { registerDownloadCommand } from "./commands/download.js";
23
24
  import { registerPolicyCommand } from "./commands/policy.js";
24
25
  import { registerDoctorCommand } from "./commands/doctor.js";
26
+ import { registerSuggestCommand } from "./commands/suggest.js";
27
+ import { registerTriageCommand } from "./commands/triage.js";
28
+ import { registerFixPlanCommand } from "./commands/fix-plan.js";
29
+ import { registerPatchReviewCommand } from "./commands/patch-review.js";
30
+ import { registerCompareCommand } from "./commands/compare.js";
31
+ import { registerReleaseNotesCommand } from "./commands/release-notes.js";
32
+ import { registerDocCommand } from "./commands/doc.js";
25
33
  import { ExitCode } from "./utils/exit-codes.js";
26
34
  import { formatCommanderError } from "./ui/diagnostics.js";
27
35
  import { parseMode, parseTimeout, parseInterval, parseScore } from "./utils/validators.js";
@@ -625,7 +633,7 @@ async function runCompareCommand(base, urls, flags) {
625
633
  }
626
634
  printOutput(format, compare, formatCompareMarkdown(compare));
627
635
  }
628
- async function runFixCommand(base, jobId, issueId, flags) {
636
+ async function runFixCommand(base, jobId, issueId, flags, globalFlags = {}) {
629
637
  const fileContent = getString(flags, "file-content");
630
638
  const format = resolveFormat(flags);
631
639
  if (!fileContent) {
@@ -649,8 +657,12 @@ async function runFixCommand(base, jobId, issueId, flags) {
649
657
  if (!result.success) {
650
658
  process.exitCode = 1;
651
659
  }
660
+ if (globalFlags.dryRun && isTTYOutput) {
661
+ process.stderr.write("[dry-run] Showing patch preview — no changes applied.\n\n");
662
+ }
652
663
  if (format === "json") {
653
- console.log(JSON.stringify(result, null, 2));
664
+ const output = globalFlags.dryRun ? { ...result, dry_run: true } : result;
665
+ console.log(JSON.stringify(output, null, 2));
654
666
  }
655
667
  else {
656
668
  if (result.success && result.patch) {
@@ -708,7 +720,7 @@ async function runVerifyCommand(base, flags) {
708
720
  }
709
721
  }
710
722
  const BATCH_LIMIT = 10;
711
- async function runFixAllCommand(base, jobId, flags) {
723
+ async function runFixAllCommand(base, jobId, flags, globalFlags = {}) {
712
724
  const fileContent = getString(flags, "file-content");
713
725
  const autoFixOnly = getBool(flags, "auto-fix-only");
714
726
  const format = resolveFormat(flags);
@@ -741,6 +753,21 @@ async function runFixAllCommand(base, jobId, flags) {
741
753
  if (isTTYOutput) {
742
754
  process.stderr.write(" \r");
743
755
  }
756
+ // --dry-run: show what would be processed and exit
757
+ if (globalFlags.dryRun) {
758
+ process.stderr.write(`[dry-run] Would generate patches for ${issuesToProcess.length} issues:\n`);
759
+ for (const issue of issuesToProcess) {
760
+ process.stderr.write(` - ${issue.id || "unknown"}: ${issue.title || issue.description || "(no title)"}\n`);
761
+ }
762
+ if (totalSkipped > 0) {
763
+ process.stderr.write(` (${totalSkipped} additional issues skipped — batch limit ${BATCH_LIMIT})\n`);
764
+ }
765
+ process.stderr.write("\nNo patches generated. Remove --dry-run to execute.\n");
766
+ return;
767
+ }
768
+ // --yes is available for future interactive confirmation steps
769
+ // Currently fix-all runs non-interactively, but --yes suppresses any
770
+ // confirmation prompts that may be added (e.g., "Apply all N patches?")
744
771
  const results = {
745
772
  successes: [],
746
773
  failures: [],
@@ -845,6 +872,13 @@ program
845
872
  .option("-q, --quiet", "Suppress banner and non-essential output")
846
873
  .option("--no-banner", "Hide the V-mark banner")
847
874
  .option("--machine", "Strict machine-readable output (JSON stdout, diagnostics stderr)")
875
+ .option("--color", "Force color output")
876
+ .option("--no-color", "Disable color output")
877
+ .option("--dashboard", "Force live dashboard during audit --wait")
878
+ .option("--no-dashboard", "Disable live dashboard (use spinner instead)")
879
+ .option("--dry-run", "Show what would happen without executing")
880
+ .option("-y, --yes", "Auto-confirm all interactive prompts")
881
+ .option("--verbose", "Expand output with additional details")
848
882
  .configureOutput({
849
883
  outputError: (str, _write) => {
850
884
  const formatted = formatCommanderError(str);
@@ -864,9 +898,20 @@ program
864
898
  .hook("preAction", (thisCommand) => {
865
899
  const opts = thisCommand.optsWithGlobals();
866
900
  const machineMode = opts.machine || false;
901
+ // Apply color settings from flags or environment
902
+ if (opts.color === false || process.env.NO_COLOR !== undefined) {
903
+ setColorEnabled(false);
904
+ }
905
+ else if (opts.color === true || process.env.FORCE_COLOR !== undefined) {
906
+ setColorEnabled(true);
907
+ }
908
+ else {
909
+ setColorEnabled(shouldUseColor());
910
+ }
867
911
  if (machineMode) {
868
912
  opts.quiet = true;
869
913
  opts.banner = false;
914
+ setColorEnabled(false);
870
915
  }
871
916
  showBanner({
872
917
  version,
@@ -886,6 +931,13 @@ registerUploadCommand(program);
886
931
  registerDownloadCommand(program);
887
932
  registerPolicyCommand(program);
888
933
  registerDoctorCommand(program);
934
+ registerSuggestCommand(program);
935
+ registerTriageCommand(program);
936
+ registerFixPlanCommand(program);
937
+ registerPatchReviewCommand(program);
938
+ registerCompareCommand(program);
939
+ registerReleaseNotesCommand(program);
940
+ registerDocCommand(program);
889
941
  // Legacy commands using old argument parsing
890
942
  // These will be migrated in subsequent plans
891
943
  program
@@ -924,24 +976,7 @@ program
924
976
  process.exit(ExitCode.ERROR);
925
977
  }
926
978
  });
927
- program
928
- .command("compare <urlA> <urlB>")
929
- .description("Compare audits of two URLs")
930
- .option("--mode <mode>", "Audit depth: basic|standard|deep", parseMode, "basic")
931
- .option("--wait", "Wait for audits to complete")
932
- .option("--timeout <ms>", "Wait timeout in milliseconds (1-300000)", parseTimeout, 60000)
933
- .option("--interval <ms>", "Poll interval in milliseconds (1-300000)", parseInterval, 5000)
934
- .option("--fail-on-score <n>", "Exit non-zero if score below n (0-100)", parseScore)
935
- .action(async (urlA, urlB, cmdOptions) => {
936
- try {
937
- const base = resolveApiBase(cmdOptions);
938
- await runCompareCommand(base, [urlA, urlB], cmdOptions);
939
- }
940
- catch (error) {
941
- process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}\n`);
942
- process.exit(ExitCode.ERROR);
943
- }
944
- });
979
+ // compare is now registered via registerCompareCommand (Phase 60)
945
980
  program
946
981
  .command("status <jobId>")
947
982
  .description("Get status of an audit job")
@@ -973,13 +1008,14 @@ program
973
1008
  .description("Generate a fix patch for an issue")
974
1009
  .requiredOption("--issue <id>", "Issue ID to fix")
975
1010
  .requiredOption("--file-content <code>", "Source code content")
976
- .action(async (jobId, cmdOptions) => {
1011
+ .action(async (jobId, cmdOptions, command) => {
977
1012
  try {
1013
+ const globalOpts = command.optsWithGlobals();
978
1014
  const base = resolveApiBase(cmdOptions);
979
1015
  const issueId = getString(cmdOptions, "issue");
980
1016
  if (!issueId)
981
1017
  throw new Error("--issue is required");
982
- await runFixCommand(base, jobId, issueId, cmdOptions);
1018
+ await runFixCommand(base, jobId, issueId, cmdOptions, { dryRun: !!globalOpts.dryRun });
983
1019
  }
984
1020
  catch (error) {
985
1021
  process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}\n`);
@@ -991,10 +1027,11 @@ program
991
1027
  .description("Generate fix patches for all issues in an audit")
992
1028
  .requiredOption("--file-content <code>", "Source code content")
993
1029
  .option("--auto-fix-only", "Only process auto-fixable issues")
994
- .action(async (jobId, cmdOptions) => {
1030
+ .action(async (jobId, cmdOptions, command) => {
995
1031
  try {
1032
+ const globalOpts = command.optsWithGlobals();
996
1033
  const base = resolveApiBase(cmdOptions);
997
- await runFixAllCommand(base, jobId, cmdOptions);
1034
+ await runFixAllCommand(base, jobId, cmdOptions, { dryRun: !!globalOpts.dryRun, yes: !!globalOpts.yes });
998
1035
  }
999
1036
  catch (error) {
1000
1037
  process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}\n`);
@@ -1 +1 @@
1
- {"version":3,"file":"formats.d.ts","sourceRoot":"","sources":["../../src/output/formats.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAM7D,CAAC;AAEF,eAAO,MAAM,sBAAsB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAMzD,CAAC;AAEF,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAclF;AAED,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,MAAM,EACf,cAAc,EAAE,MAAM,GAAG,SAAS,EAClC,WAAW,EAAE,OAAO,GACnB,MAAM,CASR"}
1
+ {"version":3,"file":"formats.d.ts","sourceRoot":"","sources":["../../src/output/formats.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAa7D,CAAC;AAEF,eAAO,MAAM,sBAAsB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAazD,CAAC;AAEF,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAclF;AAED,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,MAAM,EACf,cAAc,EAAE,MAAM,GAAG,SAAS,EAClC,WAAW,EAAE,OAAO,GACnB,MAAM,CASR"}
@@ -10,6 +10,13 @@ export const COMMAND_FORMATS = {
10
10
  explain: ["json", "human"],
11
11
  "policy-show": ["json", "yaml"],
12
12
  diff: ["json", "human"],
13
+ suggest: ["json", "human"],
14
+ triage: ["json", "human"],
15
+ "fix-plan": ["json", "human"],
16
+ "patch-review": ["json", "human"],
17
+ "release-notes": ["json", "human", "markdown"],
18
+ compare: ["json", "human"],
19
+ doc: ["json", "markdown"],
13
20
  };
14
21
  export const COMMAND_DEFAULT_FORMAT = {
15
22
  audit: "human",
@@ -17,6 +24,13 @@ export const COMMAND_DEFAULT_FORMAT = {
17
24
  explain: "human",
18
25
  "policy-show": "yaml",
19
26
  diff: "human",
27
+ suggest: "human",
28
+ triage: "human",
29
+ "fix-plan": "human",
30
+ "patch-review": "human",
31
+ "release-notes": "markdown",
32
+ compare: "human",
33
+ doc: "markdown",
20
34
  };
21
35
  export function validateFormat(command, format) {
22
36
  const allowed = COMMAND_FORMATS[command];