@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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024-2026 Digitaltableteur Tmi, trading as VertaaUX
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -67,12 +67,27 @@ vertaa whoami
67
67
 
68
68
  | Command | Description |
69
69
  |---------|-------------|
70
- | `explain <finding-id>` | Show evidence bundle for a finding |
70
+ | `explain [finding-id]` | AI-powered audit summary, or evidence for a specific finding |
71
71
  | `comment` | Generate PR comment from audit results |
72
72
  | `fix <job-id>` | Generate a fix patch for an issue |
73
73
  | `fix-all <job-id>` | Generate fix patches for all issues |
74
74
  | `verify` | Verify that a patch fixes an issue |
75
75
 
76
+ ### AI Intelligence
77
+
78
+ | Command | Description |
79
+ |---------|-------------|
80
+ | `suggest <intent>` | Convert natural language to exact CLI command(s) |
81
+ | `explain` | AI-powered audit summary (also: evidence for a single finding) |
82
+ | `triage` | Prioritize findings into P0/P1/P2 buckets with effort estimates |
83
+ | `fix-plan` | Structured remediation plan with ordered steps |
84
+ | `patch-review` | Review a diff for safety (SAFE/UNSAFE/NEEDS_REVIEW verdict) |
85
+ | `release-notes` | Generate developer + PM release notes from audit diff |
86
+ | `compare` | Before/after audit narrative with score deltas (also: URL comparison) |
87
+ | `doc` | Generate a Team Playbook from recurring findings |
88
+
89
+ All AI commands require authentication (`vertaa login` or `VERTAAUX_API_KEY`). They accept input via stdin pipe, `--file`, or `--job`.
90
+
76
91
  ### Utility
77
92
 
78
93
  | Command | Description |
@@ -92,7 +107,7 @@ vertaa whoami
92
107
  |---------|-----------|
93
108
  | `a11y <url>` | Accessibility-focused audit (filters for a11y issues) |
94
109
  | `scan <url>` | UX scan (alias for audit) |
95
- | `compare <urlA> <urlB>` | Compare audits of two URLs |
110
+ | `compare <urlA> <urlB>` | Compare audits of two URLs (also supports `--before`/`--after` for LLM-powered comparison) |
96
111
 
97
112
  ## Output Formats
98
113
 
@@ -105,6 +120,13 @@ Formats are **per-command**, not global. Each command supports a different set o
105
120
  | `explain` | `human`, `json` | `human` |
106
121
  | `policy show` | `json`, `yaml` | `yaml` |
107
122
  | `diff` | `human`, `json` | `human` |
123
+ | `suggest` | `human`, `json` | `human` |
124
+ | `triage` | `human`, `json` | `human` |
125
+ | `fix-plan` | `human`, `json` | `human` |
126
+ | `patch-review` | `human`, `json` | `human` |
127
+ | `release-notes` | `human`, `json`, `markdown` | `markdown` |
128
+ | `compare` | `human`, `json` | `human` |
129
+ | `doc` | `json`, `markdown` | `markdown` |
108
130
 
109
131
  Usage:
110
132
 
@@ -146,6 +168,37 @@ vertaa audit https://example.com --format json | jq '.data.scores'
146
168
  vertaa audit https://example.com --format json > results.json
147
169
  ```
148
170
 
171
+ ### Pipeline Examples
172
+
173
+ Chain commands with Unix pipes for powerful workflows:
174
+
175
+ ```bash
176
+ # Audit and get an AI-powered summary
177
+ vertaa audit https://example.com --json | vertaa explain
178
+
179
+ # Audit and explain with full evidence per issue
180
+ vertaa audit https://example.com --json | vertaa explain --verbose
181
+
182
+ # Audit, triage, and get a fix plan
183
+ vertaa audit https://example.com --json | vertaa triage --verbose
184
+ vertaa audit https://example.com --json | vertaa fix-plan --json
185
+
186
+ # Review a PR diff for safety against audit findings
187
+ gh pr diff 123 | vertaa patch-review --job <audit-job-id>
188
+
189
+ # Generate release notes from a diff between two audits
190
+ vertaa diff --job-a abc --job-b def --json | vertaa release-notes
191
+
192
+ # Compare two audit snapshots with LLM narrative
193
+ vertaa compare --before baseline.json --after current.json
194
+
195
+ # Convert natural language to a CLI command
196
+ vertaa suggest "check contrast on my site"
197
+
198
+ # Generate a team playbook from audit findings
199
+ vertaa audit https://example.com --json | vertaa doc --team "Frontend"
200
+ ```
201
+
149
202
  ## Global Options
150
203
 
151
204
  These options work with any command:
@@ -157,6 +210,9 @@ These options work with any command:
157
210
  | `-q, --quiet` | Suppress banner and non-essential output |
158
211
  | `--no-banner` | Hide the V-mark banner |
159
212
  | `--machine` | Strict machine-readable output mode |
213
+ | `--dry-run` | Show what would happen without executing |
214
+ | `-y, --yes` | Auto-confirm all interactive prompts |
215
+ | `--verbose` | Expand output with additional details |
160
216
  | `-v, --version` | Show version number |
161
217
  | `-h, --help` | Show help for command |
162
218
 
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * @see https://datatracker.ietf.org/doc/html/rfc8628
8
8
  */
9
- import ora from "ora";
9
+ import { createSpinner, succeedSpinner } from "../ui/spinner.js";
10
10
  /**
11
11
  * Format remaining time as MM:SS.
12
12
  */
@@ -91,10 +91,8 @@ async function pollForToken(clientId, deviceCode, intervalSeconds, expiresInSeco
91
91
  };
92
92
  process.on("SIGINT", onSigint);
93
93
  // Start spinner with countdown
94
- const spinner = ora({
95
- text: `Waiting for authorization... (${formatRemaining(Math.round(timeoutMs / 1000))} remaining)`,
96
- stream: process.stderr,
97
- }).start();
94
+ const spinner = createSpinner(`Waiting for authorization... (${formatRemaining(Math.round(timeoutMs / 1000))} remaining)`);
95
+ spinner.start();
98
96
  try {
99
97
  while (true) {
100
98
  if (cancelled) {
@@ -107,7 +105,7 @@ async function pollForToken(clientId, deviceCode, intervalSeconds, expiresInSeco
107
105
  throw new Error("Authorization timed out. Please try again.");
108
106
  }
109
107
  // Update spinner with countdown
110
- spinner.text = `Waiting for authorization... (${formatRemaining(Math.ceil(remaining / 1000))} remaining)`;
108
+ spinner.setText(`Waiting for authorization... (${formatRemaining(Math.ceil(remaining / 1000))} remaining)`);
111
109
  // Wait for poll interval (cancellable)
112
110
  await sleep(interval, () => cancelled);
113
111
  if (cancelled) {
@@ -128,7 +126,7 @@ async function pollForToken(clientId, deviceCode, intervalSeconds, expiresInSeco
128
126
  // Success
129
127
  if (response.ok) {
130
128
  const tokens = (await response.json());
131
- spinner.succeed("Authorization successful!");
129
+ succeedSpinner(spinner, "Authorization successful!");
132
130
  return {
133
131
  accessToken: tokens.access_token,
134
132
  refreshToken: tokens.refresh_token,
@@ -156,7 +154,7 @@ async function pollForToken(clientId, deviceCode, intervalSeconds, expiresInSeco
156
154
  }
157
155
  finally {
158
156
  process.removeListener("SIGINT", onSigint);
159
- if (spinner.isSpinning) {
157
+ if (spinner.isRunning) {
160
158
  spinner.stop();
161
159
  }
162
160
  }
@@ -47,9 +47,11 @@ export interface AuditCommandOptions {
47
47
  noCache?: boolean;
48
48
  cacheDir?: string;
49
49
  jsonLogs?: boolean;
50
+ explain?: boolean;
50
51
  base?: string;
51
52
  quiet?: boolean;
52
53
  machine?: boolean;
54
+ dashboard?: boolean;
53
55
  }
54
56
  /**
55
57
  * Register the audit command with the Commander program.
@@ -1 +1 @@
1
- {"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../../src/commands/audit.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA+EpC,MAAM,WAAW,mBAAmB;IAElC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IAGrB,IAAI,CAAC,EAAE,OAAO,GAAG,UAAU,GAAG,MAAM,CAAC;IACrC,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAGlB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,UAAU,GAAG,UAAU,GAAG,OAAO,CAAC;IAG5C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAGlB,MAAM,CAAC,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,CAAC;IAC/C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAGlB,WAAW,CAAC,EAAE,OAAO,CAAC;IAGtB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,YAAY,CAAC,EAAE,OAAO,CAAC;IAGvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,OAAO,CAAC;IAGhB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;IAGf,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IAGpB,MAAM,CAAC,EAAE,OAAO,GAAG,UAAU,GAAG,MAAM,CAAC;IAGvC,MAAM,CAAC,EAAE,MAAM,CAAC;IAGhB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,YAAY,CAAC,EAAE,OAAO,CAAC;IAGvB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAGlB,QAAQ,CAAC,EAAE,OAAO,CAAC;IAGnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AA2tBD;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAiT3D"}
1
+ {"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../../src/commands/audit.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAyFpC,MAAM,WAAW,mBAAmB;IAElC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IAGrB,IAAI,CAAC,EAAE,OAAO,GAAG,UAAU,GAAG,MAAM,CAAC;IACrC,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAGlB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,UAAU,GAAG,UAAU,GAAG,OAAO,CAAC;IAG5C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAGlB,MAAM,CAAC,EAAE,OAAO,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,CAAC;IAC/C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAGlB,WAAW,CAAC,EAAE,OAAO,CAAC;IAGtB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,YAAY,CAAC,EAAE,OAAO,CAAC;IAGvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,OAAO,CAAC;IAGhB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;IAGf,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IAGpB,MAAM,CAAC,EAAE,OAAO,GAAG,UAAU,GAAG,MAAM,CAAC;IAGvC,MAAM,CAAC,EAAE,MAAM,CAAC;IAGhB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,YAAY,CAAC,EAAE,OAAO,CAAC;IAGvB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAGlB,QAAQ,CAAC,EAAE,OAAO,CAAC;IAGnB,OAAO,CAAC,EAAE,OAAO,CAAC;IAGlB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAu4BD;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAkT3D"}
@@ -16,6 +16,7 @@ import { createEnvelope, writeJsonOutput, writeOutput as writeStdout } from "../
16
16
  import { resolveCommandFormat } from "../output/formats.js";
17
17
  import { getVersion } from "../ui/banner.js";
18
18
  import { createSpinner, updateSpinner, succeedSpinner, failSpinner, } from "../ui/spinner.js";
19
+ import { createRenderer, createKeyboardHandler, AuditPhase, phaseIndex, phaseTotal, } from "@vertaaux/tui";
19
20
  import { runFixWizard } from "../interactive/fix-wizard.js";
20
21
  import { isInteractive } from "../interactive/prompts.js";
21
22
  import { evaluateQualityGate, DEFAULT_QUALITY_GATE_CONFIG, } from "../quality-gate/index.js";
@@ -385,12 +386,31 @@ async function executeAudit(targetUrl, options, config) {
385
386
  const format = validatedFormat;
386
387
  const formatter = createOutput(format);
387
388
  const groupBy = options.groupBy || config.output?.groupBy || "severity";
388
- // Create spinner for progress (only in TTY mode with wait)
389
- const spinner = wait && isTTY() && !quiet
389
+ // Determine UI mode: dashboard (full-screen) vs spinner (inline)
390
+ const useDashboard = wait && !quiet && !machineMode && options.dashboard !== false;
391
+ const useSpinner = wait && isTTY() && !quiet && !useDashboard;
392
+ // Create dashboard renderer or fallback spinner
393
+ let renderer = null;
394
+ let keyboard = null;
395
+ let aborted = false;
396
+ const spinner = useSpinner
390
397
  ? createSpinner(`Auditing ${targetUrl}...`)
391
398
  : null;
399
+ if (useDashboard) {
400
+ renderer = createRenderer("auto");
401
+ keyboard = createKeyboardHandler();
402
+ keyboard.on("quit", () => {
403
+ aborted = true;
404
+ renderer?.dispose();
405
+ keyboard?.dispose();
406
+ process.stderr.write("\nAudit aborted by user.\n");
407
+ process.exitCode = ExitCode.ERROR;
408
+ });
409
+ keyboard.start();
410
+ }
411
+ const auditStartTime = Date.now();
392
412
  try {
393
- // Start spinner
413
+ // Start spinner (dashboard renders on first update)
394
414
  spinner?.start();
395
415
  // Create audit job
396
416
  const created = await apiRequest(base, "/audit", {
@@ -400,6 +420,8 @@ async function executeAudit(targetUrl, options, config) {
400
420
  // If not waiting, just output the job info
401
421
  if (!wait) {
402
422
  spinner?.stop();
423
+ renderer?.dispose();
424
+ keyboard?.dispose();
403
425
  if (format === "json") {
404
426
  if (options.output) {
405
427
  const output = JSON.stringify(createEnvelope(created, "audit"), null, 2);
@@ -429,12 +451,45 @@ async function executeAudit(targetUrl, options, config) {
429
451
  if (!created.job_id) {
430
452
  throw new Error("Audit response missing job_id");
431
453
  }
432
- const result = await waitForAudit(base, created.job_id, timeout, interval, apiKey, (progress) => {
454
+ const result = await waitForAudit(base, created.job_id, timeout, interval, apiKey, (progress, status) => {
455
+ if (aborted)
456
+ return;
457
+ if (renderer) {
458
+ const phase = mapStatusToPhase(status);
459
+ const state = {
460
+ phase,
461
+ phaseIndex: phaseIndex(phase),
462
+ phaseTotal: phaseTotal(),
463
+ url: targetUrl,
464
+ mode,
465
+ progress: { audit: progress },
466
+ totals: { audit: 100 },
467
+ issueCount: 0,
468
+ scorePreview: null,
469
+ verbose: false,
470
+ elapsed: Date.now() - auditStartTime,
471
+ };
472
+ renderer.update(state);
473
+ }
433
474
  if (spinner) {
434
475
  updateSpinner(spinner, `Auditing ${targetUrl}`, progress, 100);
435
476
  }
436
477
  });
437
- // Stop spinner with success
478
+ // Finish dashboard or spinner
479
+ if (renderer) {
480
+ const overallScore = getOverallScoreFromResult(result);
481
+ const summaryResult = {
482
+ url: targetUrl,
483
+ mode,
484
+ overallScore: overallScore ?? 0,
485
+ scores: extractNumericScores(result.scores),
486
+ issueCount: countTotalIssues(result.issues),
487
+ passed: (overallScore ?? 0) >= 70,
488
+ elapsed: Date.now() - auditStartTime,
489
+ };
490
+ renderer.finish(summaryResult);
491
+ keyboard?.dispose();
492
+ }
438
493
  if (spinner) {
439
494
  succeedSpinner(spinner, `Audit complete: ${targetUrl}`);
440
495
  }
@@ -548,6 +603,41 @@ async function executeAudit(targetUrl, options, config) {
548
603
  writeStdout(output);
549
604
  }
550
605
  }
606
+ // Inline AI explanation (--explain flag, PROG-04)
607
+ if (options.explain && issues.length > 0) {
608
+ try {
609
+ const explainBase = resolveApiBase(options.base);
610
+ const explainKey = getApiKey(config.apiKey);
611
+ const explainIssues = issues.map((i) => ({
612
+ id: i.id || null,
613
+ title: i.title || i.description || null,
614
+ description: i.description || null,
615
+ severity: i.severity || null,
616
+ category: i.category || null,
617
+ selector: i.selector || null,
618
+ wcag_reference: i.wcag_reference || null,
619
+ recommendation: i.recommendation || i.recommended_fix || null,
620
+ }));
621
+ const explainPayload = {
622
+ job_id: result.job_id || null,
623
+ url: targetUrl || null,
624
+ scores: result.scores || null,
625
+ issues: explainIssues,
626
+ };
627
+ const explainSpinner = createSpinner("Generating AI explanation...");
628
+ const explainResponse = await apiRequest(explainBase, "/cli/ai/explain", { method: "POST", body: { audit: explainPayload } }, explainKey);
629
+ succeedSpinner(explainSpinner, "Explanation ready");
630
+ console.error("");
631
+ console.error(chalk.bold("AI Explanation"));
632
+ console.error(chalk.dim("─".repeat(40)));
633
+ for (const bullet of explainResponse.data.summary) {
634
+ console.error(` ${chalk.cyan("*")} ${bullet}`);
635
+ }
636
+ }
637
+ catch (explainErr) {
638
+ console.error(chalk.dim(`\n(AI explanation unavailable: ${explainErr instanceof Error ? explainErr.message : String(explainErr)})`));
639
+ }
640
+ }
551
641
  // Output quality gate result
552
642
  if (!quiet) {
553
643
  console.error(""); // Blank line before gate result
@@ -595,13 +685,81 @@ async function executeAudit(targetUrl, options, config) {
595
685
  }
596
686
  }
597
687
  catch (error) {
598
- // Stop spinner with failure
688
+ // Stop dashboard or spinner with failure
689
+ renderer?.dispose();
690
+ keyboard?.dispose();
599
691
  if (spinner) {
600
692
  failSpinner(spinner, `Audit failed: ${error instanceof Error ? error.message : String(error)}`);
601
693
  }
602
694
  throw error;
603
695
  }
604
696
  }
697
+ /**
698
+ * Map API audit status to TUI phase name.
699
+ */
700
+ function mapStatusToPhase(status) {
701
+ switch (status) {
702
+ case "queued":
703
+ case "pending":
704
+ return AuditPhase.Connecting;
705
+ case "crawling":
706
+ return AuditPhase.Crawling;
707
+ case "running":
708
+ case "analyzing":
709
+ return AuditPhase.Analyzing;
710
+ case "scoring":
711
+ return AuditPhase.Scoring;
712
+ case "completed":
713
+ return AuditPhase.Done;
714
+ case "failed":
715
+ return AuditPhase.Failed;
716
+ default:
717
+ return AuditPhase.Analyzing;
718
+ }
719
+ }
720
+ /**
721
+ * Extract overall score from audit result.
722
+ */
723
+ function getOverallScoreFromResult(result) {
724
+ if (!result.scores)
725
+ return null;
726
+ const scores = result.scores;
727
+ const direct = scores.overall ?? scores.ux ?? scores.total;
728
+ if (typeof direct === "number" && Number.isFinite(direct))
729
+ return direct;
730
+ const numeric = Object.values(scores)
731
+ .filter((v) => typeof v === "number" && Number.isFinite(v));
732
+ if (numeric.length === 0)
733
+ return null;
734
+ return Math.round(numeric.reduce((a, b) => a + b, 0) / numeric.length);
735
+ }
736
+ /**
737
+ * Extract numeric scores from result scores object.
738
+ */
739
+ function extractNumericScores(scores) {
740
+ if (!scores)
741
+ return {};
742
+ const result = {};
743
+ for (const [key, value] of Object.entries(scores)) {
744
+ if (typeof value === "number" && key !== "overall") {
745
+ result[key] = value;
746
+ }
747
+ }
748
+ return result;
749
+ }
750
+ /**
751
+ * Count total issues from various result formats.
752
+ */
753
+ function countTotalIssues(issues) {
754
+ if (Array.isArray(issues))
755
+ return issues.length;
756
+ if (issues && typeof issues === "object") {
757
+ return Object.values(issues)
758
+ .flatMap((v) => (Array.isArray(v) ? v : []))
759
+ .length;
760
+ }
761
+ return 0;
762
+ }
605
763
  /**
606
764
  * Register the audit command with the Commander program.
607
765
  */
@@ -660,6 +818,7 @@ export function registerAuditCommand(program) {
660
818
  .option("--json-logs", "Output structured JSON logs for CI")
661
819
  // Policy options (CICD-17)
662
820
  .option("--policy <file>", "Path to policy file (default: auto-detect vertaa.policy.yml)")
821
+ .option("--explain", "Append AI explanation to audit results")
663
822
  .action(async (urlArg, cmdOptions, command) => {
664
823
  try {
665
824
  // Initialize structured logger
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Compare command for VertaaUX CLI (upgraded).
3
+ *
4
+ * Two modes:
5
+ * 1. URL comparison (backward compat): `vertaa compare <urlA> <urlB> --wait`
6
+ * Runs two audits and shows a score/category delta table.
7
+ * 2. File-based LLM comparison: `vertaa compare --before old.json --after new.json`
8
+ * Sends both audit JSONs to the LLM compare endpoint for a narrative analysis.
9
+ *
10
+ * When --before/--after are provided, the LLM mode is used automatically.
11
+ * When positional URLs are given, the legacy comparison mode is used.
12
+ *
13
+ * Examples:
14
+ * vertaa compare https://a.com https://b.com --wait
15
+ * vertaa compare --before baseline.json --after current.json
16
+ * vertaa compare --before baseline.json --after current.json --verbose
17
+ */
18
+ import { Command } from "commander";
19
+ export declare function registerCompareCommand(program: Command): void;
20
+ //# sourceMappingURL=compare.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compare.d.ts","sourceRoot":"","sources":["../../src/commands/compare.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAkNpC,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAmE7D"}