@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
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
 
@@ -1 +1 @@
1
- {"version":3,"file":"device-flow.d.ts","sourceRoot":"","sources":["../../src/auth/device-flow.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,+BAA+B;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,2BAA2B;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,yCAAyC;IACzC,gBAAgB,EAAE,MAAM,CAAC;IACzB,oDAAoD;IACpD,yBAAyB,CAAC,EAAE,MAAM,CAAC;IACnC,8CAA8C;IAC9C,QAAQ,EAAE,MAAM,CAAC;IACjB,iCAAiC;IACjC,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,mBAAmB;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,oCAAoC;IACpC,UAAU,EAAE,MAAM,CAAC;IACnB,gCAAgC;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,kDAAkD;IAClD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,oBAAoB;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,iCAAiC;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,sCAAsC;IACtC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,2CAA2C;IAC3C,SAAS,EAAE,MAAM,CAAC;CACnB;AAmCD;;;;;;;;;;;;GAYG;AACH,wBAAsB,eAAe,CACnC,QAAQ,EAAE,MAAM,EAChB,QAAQ,GAAE,MAA0B,GACnC,OAAO,CAAC,gBAAgB,CAAC,CA2B3B"}
1
+ {"version":3,"file":"device-flow.d.ts","sourceRoot":"","sources":["../../src/auth/device-flow.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,+BAA+B;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,2BAA2B;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,yCAAyC;IACzC,gBAAgB,EAAE,MAAM,CAAC;IACzB,oDAAoD;IACpD,yBAAyB,CAAC,EAAE,MAAM,CAAC;IACnC,8CAA8C;IAC9C,QAAQ,EAAE,MAAM,CAAC;IACjB,iCAAiC;IACjC,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,mBAAmB;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,oCAAoC;IACpC,UAAU,EAAE,MAAM,CAAC;IACnB,gCAAgC;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,kDAAkD;IAClD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,oBAAoB;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,iCAAiC;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,sCAAsC;IACtC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,2CAA2C;IAC3C,SAAS,EAAE,MAAM,CAAC;CACnB;AAmCD;;;;;;;;;;;;GAYG;AACH,wBAAsB,eAAe,CACnC,QAAQ,EAAE,MAAM,EAChB,QAAQ,GAAE,MAA0B,GACnC,OAAO,CAAC,gBAAgB,CAAC,CA6B3B"}
@@ -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
  */
@@ -50,6 +50,7 @@ export async function startDeviceFlow(clientId, authBase = DEFAULT_AUTH_BASE) {
50
50
  console.log(` Or open: ${deviceCodeResponse.verification_uri_complete}`);
51
51
  console.log("\n");
52
52
  }
53
+ console.log(" Press Ctrl+C to cancel.\n");
53
54
  // Step 3: Poll for token with countdown
54
55
  const tokens = await pollForToken(clientId, deviceCodeResponse.device_code, deviceCodeResponse.interval, deviceCodeResponse.expires_in, authBase);
55
56
  return tokens;
@@ -83,13 +84,20 @@ async function pollForToken(clientId, deviceCode, intervalSeconds, expiresInSeco
83
84
  const startTime = Date.now();
84
85
  const timeoutMs = Math.min(expiresInSeconds, DEFAULT_TIMEOUT_SECONDS) * 1000;
85
86
  let interval = intervalSeconds * 1000; // Convert to milliseconds
87
+ let cancelled = false;
88
+ // Handle Ctrl+C gracefully
89
+ const onSigint = () => {
90
+ cancelled = true;
91
+ };
92
+ process.on("SIGINT", onSigint);
86
93
  // Start spinner with countdown
87
- const spinner = ora({
88
- text: `Waiting for authorization... (${formatRemaining(Math.round(timeoutMs / 1000))} remaining)`,
89
- stream: process.stderr,
90
- }).start();
94
+ const spinner = createSpinner(`Waiting for authorization... (${formatRemaining(Math.round(timeoutMs / 1000))} remaining)`);
95
+ spinner.start();
91
96
  try {
92
97
  while (true) {
98
+ if (cancelled) {
99
+ throw new Error("Login cancelled.");
100
+ }
93
101
  // Check timeout
94
102
  const elapsed = Date.now() - startTime;
95
103
  const remaining = Math.max(0, timeoutMs - elapsed);
@@ -97,9 +105,12 @@ async function pollForToken(clientId, deviceCode, intervalSeconds, expiresInSeco
97
105
  throw new Error("Authorization timed out. Please try again.");
98
106
  }
99
107
  // Update spinner with countdown
100
- spinner.text = `Waiting for authorization... (${formatRemaining(Math.ceil(remaining / 1000))} remaining)`;
101
- // Wait for poll interval
102
- await sleep(interval);
108
+ spinner.setText(`Waiting for authorization... (${formatRemaining(Math.ceil(remaining / 1000))} remaining)`);
109
+ // Wait for poll interval (cancellable)
110
+ await sleep(interval, () => cancelled);
111
+ if (cancelled) {
112
+ throw new Error("Login cancelled.");
113
+ }
103
114
  // Poll token endpoint
104
115
  const response = await fetch(url, {
105
116
  method: "POST",
@@ -115,7 +126,7 @@ async function pollForToken(clientId, deviceCode, intervalSeconds, expiresInSeco
115
126
  // Success
116
127
  if (response.ok) {
117
128
  const tokens = (await response.json());
118
- spinner.succeed("Authorization successful!");
129
+ succeedSpinner(spinner, "Authorization successful!");
119
130
  return {
120
131
  accessToken: tokens.access_token,
121
132
  refreshToken: tokens.refresh_token,
@@ -142,15 +153,36 @@ async function pollForToken(clientId, deviceCode, intervalSeconds, expiresInSeco
142
153
  }
143
154
  }
144
155
  finally {
145
- // Ensure spinner is stopped
146
- if (spinner.isSpinning) {
156
+ process.removeListener("SIGINT", onSigint);
157
+ if (spinner.isRunning) {
147
158
  spinner.stop();
148
159
  }
149
160
  }
150
161
  }
151
162
  /**
152
- * Sleep for specified milliseconds.
163
+ * Sleep for specified milliseconds, with early cancellation support.
153
164
  */
154
- function sleep(ms) {
155
- return new Promise((resolve) => setTimeout(resolve, ms));
165
+ function sleep(ms, isCancelled) {
166
+ return new Promise((resolve) => {
167
+ if (isCancelled?.()) {
168
+ resolve();
169
+ return;
170
+ }
171
+ let check;
172
+ const timer = setTimeout(() => {
173
+ if (check)
174
+ clearInterval(check);
175
+ resolve();
176
+ }, ms);
177
+ // Check cancellation every 200ms to respond quickly to Ctrl+C
178
+ if (isCancelled) {
179
+ check = setInterval(() => {
180
+ if (isCancelled()) {
181
+ clearTimeout(timer);
182
+ clearInterval(check);
183
+ resolve();
184
+ }
185
+ }, 200);
186
+ }
187
+ });
156
188
  }
@@ -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;AA8EpC,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;AA4tBD;;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"}
@@ -14,7 +14,9 @@ import { resolveApiBase, getApiKey, apiRequest, waitForAudit, } from "../utils/c
14
14
  import { createOutput, formatSarif, formatAuditHtml } from "../output/factory.js";
15
15
  import { createEnvelope, writeJsonOutput, writeOutput as writeStdout } from "../output/envelope.js";
16
16
  import { resolveCommandFormat } from "../output/formats.js";
17
+ import { getVersion } from "../ui/banner.js";
17
18
  import { createSpinner, updateSpinner, succeedSpinner, failSpinner, } from "../ui/spinner.js";
19
+ import { createRenderer, createKeyboardHandler, AuditPhase, phaseIndex, phaseTotal, } from "@vertaaux/tui";
18
20
  import { runFixWizard } from "../interactive/fix-wizard.js";
19
21
  import { isInteractive } from "../interactive/prompts.js";
20
22
  import { evaluateQualityGate, DEFAULT_QUALITY_GATE_CONFIG, } from "../quality-gate/index.js";
@@ -29,7 +31,6 @@ import semver from "semver";
29
31
  // Artifact directory
30
32
  const ARTIFACTS_DIR = ".vertaaux/artifacts";
31
33
  // CLI version for policy version requirements (read from package.json)
32
- import { getVersion } from "../ui/banner.js";
33
34
  const CLI_VERSION = getVersion();
34
35
  /**
35
36
  * Detect current branch from CI environment or git.
@@ -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
  */
@@ -615,7 +773,7 @@ export function registerAuditCommand(program) {
615
773
  .option("--routes <routes>", "Comma-separated list of routes to audit")
616
774
  .option("--auth-profile <profile>", "Authentication profile for protected pages")
617
775
  .option("--mode <mode>", "Audit depth: basic|standard|deep", parseMode, "basic")
618
- .option("--format <format>", "Output format: json|sarif|junit|html|human|auto")
776
+ .option("--format <format>", "Output format: json|sarif|junit|html|human (default: human in terminal, auto-detected in CI)")
619
777
  .option("-o, --output <path>", "Output file path")
620
778
  .option("--group-by <field>", "Group issues by: severity|category|route", parseGroupBy)
621
779
  .option("--wait", "Wait for audit completion (default)")
@@ -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,14 @@
1
+ /**
2
+ * Client management commands for VertaaUX CLI.
3
+ *
4
+ * Provides agency client CRUD operations and batch auditing
5
+ * across client URL portfolios with bounded concurrency.
6
+ *
7
+ * Implements 46-06: CLI client management and batch audit.
8
+ */
9
+ import { Command } from "commander";
10
+ /**
11
+ * Register the client command with the Commander program.
12
+ */
13
+ export declare function registerClientCommand(program: Command): void;
14
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/commands/client.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA0HpC;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAwX5D"}