@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,323 @@
1
+ /**
2
+ * Protect command for VertaaUX CLI.
3
+ *
4
+ * Implements the 3-step conversion flow from the command line:
5
+ * 1. Audit - Captures scores (from latest audit or --input)
6
+ * 2. Protect - Generates policy YAML with score thresholds
7
+ * 3. Monitor - Outputs CI snippet for continuous quality gating
8
+ *
9
+ * Usage: vertaa protect [url] [options]
10
+ */
11
+ import chalk from "chalk";
12
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
13
+ import { resolve, dirname } from "node:path";
14
+ import { stringify } from "yaml";
15
+ import { generatePolicyFromScores } from "@vertaaux/quality-control";
16
+ import { generateTemplate } from "../templates/index.js";
17
+ import { ExitCode } from "../utils/exit-codes.js";
18
+ import { normalizeIssues, generateFingerprint } from "@vertaaux/quality-control";
19
+ /**
20
+ * Path to the latest audit results cache.
21
+ */
22
+ const LATEST_AUDIT_PATH = ".vertaaux/latest-audit.json";
23
+ /**
24
+ * Default baseline file path.
25
+ */
26
+ const DEFAULT_BASELINE_PATH = ".vertaaux/baseline.json";
27
+ /**
28
+ * Maximum age (in ms) for a cached audit to be considered "recent" (1 hour).
29
+ */
30
+ const MAX_AUDIT_AGE_MS = 60 * 60 * 1000;
31
+ /**
32
+ * Extract numeric scores from audit scores object.
33
+ */
34
+ function extractNumericScores(scores) {
35
+ if (!scores)
36
+ return {};
37
+ const result = {};
38
+ for (const [key, value] of Object.entries(scores)) {
39
+ if (typeof value === "number" && Number.isFinite(value)) {
40
+ result[key] = value;
41
+ }
42
+ }
43
+ return result;
44
+ }
45
+ /**
46
+ * Get the rule ID from an issue for fingerprinting.
47
+ */
48
+ function getRuleId(issue) {
49
+ return issue.ruleId || issue.rule_id || issue.id || "unknown";
50
+ }
51
+ /**
52
+ * Register the protect command with the Commander program.
53
+ */
54
+ export function registerProtectCommand(program) {
55
+ program
56
+ .command("protect [url]")
57
+ .description("Lock current scores as policy thresholds (audit -> protect -> monitor)")
58
+ .option("--buffer <points>", "Points below current scores for threshold", "5")
59
+ .option("--ci <provider>", "CI provider: github|gitlab|circleci|azure|jenkins", "github")
60
+ .option("--output <path>", "Policy output path", "vertaa.policy.yml")
61
+ .option("--no-baseline", "Skip implicit baseline creation")
62
+ .option("--no-ci-snippet", "Skip CI snippet generation")
63
+ .option("--input <path>", "Use existing audit results JSON instead of running audit")
64
+ .option("--format <format>", "Output format: human|json", "human")
65
+ .option("--force", "Overwrite existing policy file without prompting")
66
+ .action(async (urlArg, options) => {
67
+ try {
68
+ await executeProtect(urlArg, options);
69
+ }
70
+ catch (error) {
71
+ process.stderr.write(chalk.red(`Error: ${error instanceof Error ? error.message : String(error)}\n`));
72
+ process.exit(ExitCode.ERROR);
73
+ }
74
+ });
75
+ }
76
+ /**
77
+ * Execute the protect flow.
78
+ */
79
+ async function executeProtect(urlArg, options) {
80
+ const buffer = parseInt(options.buffer, 10);
81
+ if (isNaN(buffer) || buffer < 0) {
82
+ process.stderr.write(chalk.red("Error: --buffer must be a non-negative integer\n"));
83
+ process.exit(ExitCode.ERROR);
84
+ }
85
+ // ─── Step 1: Get audit results ──────────────────────────────────────
86
+ let auditData;
87
+ let auditUrl;
88
+ if (options.input) {
89
+ // Read from explicit input file
90
+ const inputPath = resolve(process.cwd(), options.input);
91
+ if (!existsSync(inputPath)) {
92
+ process.stderr.write(chalk.red(`Error: Input file not found: ${inputPath}\n`));
93
+ process.exit(ExitCode.ERROR);
94
+ }
95
+ try {
96
+ const raw = readFileSync(inputPath, "utf-8");
97
+ auditData = JSON.parse(raw);
98
+ }
99
+ catch (err) {
100
+ process.stderr.write(chalk.red(`Error: Failed to parse input file: ${err instanceof Error ? err.message : String(err)}\n`));
101
+ process.exit(ExitCode.ERROR);
102
+ return; // TypeScript flow control
103
+ }
104
+ auditUrl = auditData.url || urlArg || "unknown";
105
+ }
106
+ else {
107
+ // Check latest-audit.json
108
+ const latestPath = resolve(process.cwd(), LATEST_AUDIT_PATH);
109
+ if (!existsSync(latestPath)) {
110
+ if (urlArg) {
111
+ process.stderr.write(chalk.red("Error: No recent audit found.\n\n"));
112
+ process.stderr.write(`Run ${chalk.cyan(`vertaa audit ${urlArg}`)} first, then ${chalk.cyan(`vertaa protect ${urlArg}`)}\n`);
113
+ }
114
+ else {
115
+ process.stderr.write(chalk.red("Error: No audit results found.\n\n"));
116
+ process.stderr.write(`Run ${chalk.cyan("vertaa audit <url>")} first, then ${chalk.cyan("vertaa protect <url>")}\n`);
117
+ process.stderr.write(`Or provide results directly: ${chalk.cyan("vertaa protect --input <audit.json>")}\n`);
118
+ }
119
+ process.exit(ExitCode.ERROR);
120
+ return;
121
+ }
122
+ try {
123
+ const raw = readFileSync(latestPath, "utf-8");
124
+ auditData = JSON.parse(raw);
125
+ }
126
+ catch (err) {
127
+ process.stderr.write(chalk.red(`Error: Failed to parse latest audit: ${err instanceof Error ? err.message : String(err)}\n`));
128
+ process.exit(ExitCode.ERROR);
129
+ return;
130
+ }
131
+ // If URL provided, check it matches the latest audit
132
+ if (urlArg && auditData.url && auditData.url !== urlArg) {
133
+ // Check if the latest audit is for a different URL
134
+ process.stderr.write(chalk.yellow(`Warning: Latest audit is for ${auditData.url}, not ${urlArg}\n`));
135
+ process.stderr.write(`Run ${chalk.cyan(`vertaa audit ${urlArg}`)} first, then ${chalk.cyan(`vertaa protect ${urlArg}`)}\n`);
136
+ process.exit(ExitCode.ERROR);
137
+ return;
138
+ }
139
+ // Check if the audit is recent enough (within 1 hour)
140
+ const auditTimestamp = auditData.completed_at || auditData.created_at;
141
+ if (auditTimestamp) {
142
+ const auditAge = Date.now() - new Date(auditTimestamp).getTime();
143
+ if (auditAge > MAX_AUDIT_AGE_MS) {
144
+ const auditAuditUrl = auditData.url || "the URL";
145
+ process.stderr.write(chalk.yellow(`Warning: Latest audit is ${Math.round(auditAge / 60000)} minutes old.\n`));
146
+ process.stderr.write(`Run ${chalk.cyan(`vertaa audit ${auditAuditUrl}`)} for fresh results, then ${chalk.cyan(`vertaa protect ${auditAuditUrl}`)}\n`);
147
+ process.exit(ExitCode.ERROR);
148
+ return;
149
+ }
150
+ }
151
+ auditUrl = auditData.url || urlArg || "unknown";
152
+ }
153
+ // Extract numeric scores
154
+ const scores = extractNumericScores(auditData.scores);
155
+ if (Object.keys(scores).length === 0) {
156
+ process.stderr.write(chalk.red("Error: Audit results do not contain scores.\n"));
157
+ process.stderr.write("Ensure the audit completed successfully before running protect.\n");
158
+ process.exit(ExitCode.ERROR);
159
+ }
160
+ // ─── Step 2: Generate policy ────────────────────────────────────────
161
+ const policy = generatePolicyFromScores({
162
+ scores,
163
+ buffer,
164
+ url: auditUrl,
165
+ });
166
+ const today = new Date().toISOString().slice(0, 10);
167
+ const header = [
168
+ `# Generated by vertaa protect on ${today}`,
169
+ `# Thresholds set ${buffer} points below current scores`,
170
+ "",
171
+ ].join("\n");
172
+ const yamlContent = header + stringify(policy, { lineWidth: 120 });
173
+ // ─── Step 3: Write policy file ──────────────────────────────────────
174
+ const outputPath = resolve(process.cwd(), options.output);
175
+ if (existsSync(outputPath) && !options.force) {
176
+ process.stderr.write(chalk.yellow(`Policy file already exists at ${options.output}. Use --force to overwrite.\n`));
177
+ process.exit(ExitCode.ERROR);
178
+ }
179
+ // Ensure directory exists
180
+ const outputDir = dirname(outputPath);
181
+ if (!existsSync(outputDir)) {
182
+ mkdirSync(outputDir, { recursive: true });
183
+ }
184
+ writeFileSync(outputPath, yamlContent, "utf-8");
185
+ // ─── Step 4: Create implicit baseline ───────────────────────────────
186
+ let baselineCreated = false;
187
+ if (options.baseline !== false) {
188
+ const baselinePath = resolve(process.cwd(), DEFAULT_BASELINE_PATH);
189
+ // Check if an explicit baseline already exists
190
+ if (existsSync(baselinePath)) {
191
+ try {
192
+ const existingRaw = readFileSync(baselinePath, "utf-8");
193
+ const existing = JSON.parse(existingRaw);
194
+ if (existing.source && existing.source !== "implicit") {
195
+ process.stderr.write(chalk.dim("Explicit baseline exists, keeping it (implicit baseline skipped)\n"));
196
+ }
197
+ else {
198
+ // Overwrite implicit baseline with fresh one
199
+ const baseline = createImplicitBaseline(auditUrl, scores, auditData.issues);
200
+ const baselineDir = dirname(baselinePath);
201
+ if (!existsSync(baselineDir)) {
202
+ mkdirSync(baselineDir, { recursive: true });
203
+ }
204
+ writeFileSync(baselinePath, JSON.stringify(baseline, null, 2) + "\n", "utf-8");
205
+ baselineCreated = true;
206
+ }
207
+ }
208
+ catch {
209
+ // If we can't parse existing baseline, overwrite it
210
+ const baseline = createImplicitBaseline(auditUrl, scores, auditData.issues);
211
+ const baselineDir = dirname(baselinePath);
212
+ if (!existsSync(baselineDir)) {
213
+ mkdirSync(baselineDir, { recursive: true });
214
+ }
215
+ writeFileSync(baselinePath, JSON.stringify(baseline, null, 2) + "\n", "utf-8");
216
+ baselineCreated = true;
217
+ }
218
+ }
219
+ else {
220
+ // No baseline exists -- create one
221
+ const baseline = createImplicitBaseline(auditUrl, scores, auditData.issues);
222
+ const baselineDir = dirname(baselinePath);
223
+ if (!existsSync(baselineDir)) {
224
+ mkdirSync(baselineDir, { recursive: true });
225
+ }
226
+ writeFileSync(baselinePath, JSON.stringify(baseline, null, 2) + "\n", "utf-8");
227
+ baselineCreated = true;
228
+ }
229
+ }
230
+ // ─── Step 5: Generate CI snippet ───────────────────────────────────
231
+ let ciSnippetContent = null;
232
+ let ciSnippetPath = null;
233
+ if (options.ciSnippet !== false) {
234
+ const template = generateTemplate(options.ci, {
235
+ auditUrl: auditUrl !== "unknown" ? auditUrl : undefined,
236
+ threshold: policy.assertions.overall_score,
237
+ failOn: "error",
238
+ uploadSarif: true,
239
+ postComment: true,
240
+ });
241
+ if (template) {
242
+ ciSnippetContent = template.content;
243
+ ciSnippetPath = template.path;
244
+ }
245
+ }
246
+ // ─── Step 6: Output ────────────────────────────────────────────────
247
+ if (options.format === "json") {
248
+ // JSON output to stdout
249
+ const output = {
250
+ url: auditUrl,
251
+ policy,
252
+ policyPath: options.output,
253
+ baseline: baselineCreated
254
+ ? { path: DEFAULT_BASELINE_PATH, created: true }
255
+ : { path: DEFAULT_BASELINE_PATH, created: false },
256
+ ciSnippet: ciSnippetContent
257
+ ? { provider: options.ci, path: ciSnippetPath, content: ciSnippetContent }
258
+ : null,
259
+ };
260
+ process.stdout.write(JSON.stringify(output, null, 2) + "\n");
261
+ }
262
+ else {
263
+ // Human-readable output to stderr (status) and stdout (CI snippet)
264
+ process.stderr.write("\n" + chalk.green.bold("Score protected!") + "\n\n");
265
+ process.stderr.write(chalk.bold("What happened:") + "\n");
266
+ process.stderr.write(` ${chalk.cyan("1. Audit")} ${chalk.dim("-")} Scores captured for ${chalk.underline(auditUrl)}\n`);
267
+ process.stderr.write(` ${chalk.cyan("2. Protect")} ${chalk.dim("-")} Policy saved to ${chalk.underline(options.output)}\n`);
268
+ process.stderr.write(` ${chalk.cyan("3. Monitor")} ${chalk.dim("-")} CI snippet ready (paste into your pipeline)\n`);
269
+ // Policy thresholds
270
+ process.stderr.write("\n" + chalk.bold("Policy thresholds:") + "\n");
271
+ process.stderr.write(` Overall: ${chalk.yellow(String(policy.assertions.overall_score))}\n`);
272
+ if (policy.budgets) {
273
+ for (const [category, budget] of Object.entries(policy.budgets)) {
274
+ const currentScore = scores[category];
275
+ const scoreInfo = currentScore !== undefined
276
+ ? chalk.dim(` (current: ${currentScore})`)
277
+ : "";
278
+ process.stderr.write(` ${category}: ${chalk.yellow(String(budget.min_score))}${scoreInfo}\n`);
279
+ }
280
+ }
281
+ if (baselineCreated) {
282
+ process.stderr.write("\n" +
283
+ chalk.dim(`Baseline saved to ${DEFAULT_BASELINE_PATH}\n`));
284
+ }
285
+ // CI snippet
286
+ if (ciSnippetContent && ciSnippetPath) {
287
+ process.stderr.write("\n" +
288
+ chalk.bold(`Next step: Add this to your CI pipeline (${ciSnippetPath}):`) +
289
+ "\n");
290
+ process.stderr.write(chalk.dim("---") + "\n");
291
+ process.stdout.write(ciSnippetContent + "\n");
292
+ process.stderr.write(chalk.dim("---") + "\n");
293
+ }
294
+ }
295
+ }
296
+ /**
297
+ * Create an implicit baseline from audit data.
298
+ *
299
+ * Produces a BaselineFile with source: "implicit" marker
300
+ * so it can be distinguished from explicit user-created baselines.
301
+ */
302
+ function createImplicitBaseline(url, scores, rawIssues) {
303
+ const now = new Date().toISOString();
304
+ const issues = normalizeIssues(rawIssues);
305
+ const baselineIssues = issues.map((issue) => ({
306
+ fingerprint: generateFingerprint(issue),
307
+ ruleId: getRuleId(issue),
308
+ severity: issue.severity || "warning",
309
+ category: issue.category || "general",
310
+ description: issue.description || issue.title || "",
311
+ selector: issue.selector,
312
+ baselinedAt: now,
313
+ }));
314
+ return {
315
+ version: 1,
316
+ created: now,
317
+ updated: now,
318
+ url,
319
+ source: "implicit",
320
+ scores,
321
+ issues: baselineIssues,
322
+ };
323
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Release-notes command for VertaaUX CLI.
3
+ *
4
+ * Reads diff data (via stdin, --file, or from two --job-a/--job-b jobs)
5
+ * and calls the LLM release-notes endpoint to produce developer-facing
6
+ * and PM-facing release notes.
7
+ *
8
+ * Default output is markdown; use --format json for structured output.
9
+ *
10
+ * Examples:
11
+ * vertaa diff --job-a abc --job-b def --json | vertaa release-notes
12
+ * vertaa release-notes --file diff.json
13
+ * vertaa release-notes --job-a abc --job-b def
14
+ */
15
+ import { Command } from "commander";
16
+ export declare function registerReleaseNotesCommand(program: Command): void;
17
+ //# sourceMappingURL=release-notes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"release-notes.d.ts","sourceRoot":"","sources":["../../src/commands/release-notes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA0EpC,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAgHlE"}
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Release-notes command for VertaaUX CLI.
3
+ *
4
+ * Reads diff data (via stdin, --file, or from two --job-a/--job-b jobs)
5
+ * and calls the LLM release-notes endpoint to produce developer-facing
6
+ * and PM-facing release notes.
7
+ *
8
+ * Default output is markdown; use --format json for structured output.
9
+ *
10
+ * Examples:
11
+ * vertaa diff --job-a abc --job-b def --json | vertaa release-notes
12
+ * vertaa release-notes --file diff.json
13
+ * vertaa release-notes --job-a abc --job-b def
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
+ // Formatters
26
+ // ---------------------------------------------------------------------------
27
+ function formatReleaseNotesHuman(data) {
28
+ const lines = [];
29
+ lines.push(chalk.bold(data.headline));
30
+ lines.push("");
31
+ lines.push(chalk.bold.blue("Developer Notes"));
32
+ lines.push(chalk.dim("─".repeat(40)));
33
+ lines.push(data.developer_notes);
34
+ lines.push("");
35
+ lines.push(chalk.bold.green("PM / User-Facing Notes"));
36
+ lines.push(chalk.dim("─".repeat(40)));
37
+ lines.push(data.pm_notes);
38
+ return lines.join("\n");
39
+ }
40
+ function formatReleaseNotesMarkdown(data) {
41
+ const lines = [];
42
+ lines.push(`# ${data.headline}`);
43
+ lines.push("");
44
+ lines.push("## Developer Notes");
45
+ lines.push("");
46
+ lines.push(data.developer_notes);
47
+ lines.push("");
48
+ lines.push("## User-Facing Notes");
49
+ lines.push("");
50
+ lines.push(data.pm_notes);
51
+ return lines.join("\n");
52
+ }
53
+ // ---------------------------------------------------------------------------
54
+ // Command Registration
55
+ // ---------------------------------------------------------------------------
56
+ export function registerReleaseNotesCommand(program) {
57
+ program
58
+ .command("release-notes")
59
+ .description("Generate developer + PM release notes from audit diff data")
60
+ .option("--file <path>", "Load diff JSON from file")
61
+ .option("--job-a <id>", "First audit job ID (baseline)")
62
+ .option("--job-b <id>", "Second audit job ID (current)")
63
+ .option("-f, --format <format>", "Output format: json | human | markdown")
64
+ .addHelpText("after", `
65
+ Examples:
66
+ vertaa diff --job-a abc --job-b def --json | vertaa release-notes
67
+ vertaa release-notes --file diff.json
68
+ vertaa release-notes --job-a abc --job-b def
69
+ `)
70
+ .action(async (options, command) => {
71
+ try {
72
+ const globalOpts = command.optsWithGlobals();
73
+ const config = await resolveConfig(globalOpts.config);
74
+ const machineMode = globalOpts.machine || false;
75
+ const format = resolveCommandFormat("release-notes", options.format, machineMode);
76
+ // Resolve diff data
77
+ let diffPayload;
78
+ if (options.jobA && options.jobB) {
79
+ // Fetch diff from two jobs via the diff API
80
+ const base = resolveApiBase(globalOpts.base);
81
+ const apiKey = getApiKey(config.apiKey);
82
+ const diffResult = await apiRequest(base, `/diff?job_a=${encodeURIComponent(options.jobA)}&job_b=${encodeURIComponent(options.jobB)}`, { method: "GET" }, apiKey);
83
+ diffPayload = diffResult.data;
84
+ }
85
+ else {
86
+ // Read from stdin or --file
87
+ const input = await readJsonInput(options.file);
88
+ if (!input) {
89
+ console.error("Error: No diff data provided.");
90
+ console.error("Usage:");
91
+ console.error(" vertaa diff --job-a abc --job-b def --json | vertaa release-notes");
92
+ console.error(" vertaa release-notes --file diff.json");
93
+ console.error(" vertaa release-notes --job-a abc --job-b def");
94
+ process.exit(ExitCode.ERROR);
95
+ }
96
+ const data = input;
97
+ const innerData = (data.data && typeof data.data === "object" ? data.data : data);
98
+ // Validate shape
99
+ if (!Array.isArray(innerData.new) || !Array.isArray(innerData.fixed)) {
100
+ console.error("Error: Invalid diff data. Expected { new: [...], fixed: [...], summary: {...} }");
101
+ console.error("Pipe from: vertaa diff --job-a abc --job-b def --json | vertaa release-notes");
102
+ process.exit(ExitCode.ERROR);
103
+ }
104
+ diffPayload = {
105
+ new: innerData.new,
106
+ fixed: innerData.fixed,
107
+ present: innerData.present || null,
108
+ summary: innerData.summary || {
109
+ newCount: innerData.new.length,
110
+ fixedCount: innerData.fixed.length,
111
+ presentCount: Array.isArray(innerData.present) ? innerData.present.length : null,
112
+ },
113
+ };
114
+ }
115
+ // Auth check
116
+ const base = resolveApiBase(globalOpts.base);
117
+ const apiKey = getApiKey(config.apiKey);
118
+ // Call LLM release-notes API
119
+ const spinner = createSpinner("Generating release notes...");
120
+ try {
121
+ const response = await Promise.race([
122
+ apiRequest(base, "/cli/ai/release-notes", { method: "POST", body: { diff: diffPayload } }, apiKey),
123
+ new Promise((_, reject) => setTimeout(() => reject(new Error("LLM request timed out")), AI_TIMEOUT_MS)),
124
+ ]);
125
+ succeedSpinner(spinner, "Notes ready");
126
+ if (format === "json") {
127
+ writeJsonOutput(response.data, "release-notes");
128
+ }
129
+ else if (format === "markdown") {
130
+ writeOutput(formatReleaseNotesMarkdown(response.data));
131
+ }
132
+ else {
133
+ writeOutput(formatReleaseNotesHuman(response.data));
134
+ }
135
+ }
136
+ catch (error) {
137
+ handleAiCommandError(error, "release-notes", spinner);
138
+ }
139
+ }
140
+ catch (error) {
141
+ console.error("Error:", error instanceof Error ? error.message : String(error));
142
+ process.exit(ExitCode.ERROR);
143
+ }
144
+ });
145
+ }
@@ -0,0 +1,15 @@
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 { Command } from "commander";
11
+ /**
12
+ * Register the report command with the Commander program.
13
+ */
14
+ export declare function registerReportCommand(program: Command): void;
15
+ //# sourceMappingURL=report.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"report.d.ts","sourceRoot":"","sources":["../../src/commands/report.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAwPpC;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAyF5D"}