@vertaaux/cli 0.2.3 → 0.3.1
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.
- package/LICENSE +21 -0
- package/README.md +58 -2
- package/dist/auth/device-flow.js +6 -8
- package/dist/commands/audit.d.ts +2 -0
- package/dist/commands/audit.d.ts.map +1 -1
- package/dist/commands/audit.js +165 -6
- package/dist/commands/compare.d.ts +20 -0
- package/dist/commands/compare.d.ts.map +1 -0
- package/dist/commands/compare.js +335 -0
- package/dist/commands/doc.d.ts +18 -0
- package/dist/commands/doc.d.ts.map +1 -0
- package/dist/commands/doc.js +161 -0
- package/dist/commands/download.d.ts.map +1 -1
- package/dist/commands/download.js +9 -8
- package/dist/commands/explain.d.ts +14 -33
- package/dist/commands/explain.d.ts.map +1 -1
- package/dist/commands/explain.js +277 -179
- package/dist/commands/fix-plan.d.ts +15 -0
- package/dist/commands/fix-plan.d.ts.map +1 -0
- package/dist/commands/fix-plan.js +182 -0
- package/dist/commands/patch-review.d.ts +14 -0
- package/dist/commands/patch-review.d.ts.map +1 -0
- package/dist/commands/patch-review.js +200 -0
- package/dist/commands/release-notes.d.ts +17 -0
- package/dist/commands/release-notes.d.ts.map +1 -0
- package/dist/commands/release-notes.js +145 -0
- package/dist/commands/suggest.d.ts +18 -0
- package/dist/commands/suggest.d.ts.map +1 -0
- package/dist/commands/suggest.js +152 -0
- package/dist/commands/triage.d.ts +17 -0
- package/dist/commands/triage.d.ts.map +1 -0
- package/dist/commands/triage.js +205 -0
- package/dist/commands/upload.d.ts.map +1 -1
- package/dist/commands/upload.js +8 -7
- package/dist/index.js +62 -25
- package/dist/output/formats.d.ts.map +1 -1
- package/dist/output/formats.js +14 -0
- package/dist/output/human.d.ts +1 -10
- package/dist/output/human.d.ts.map +1 -1
- package/dist/output/human.js +26 -98
- package/dist/prompts/command-catalog.d.ts +46 -0
- package/dist/prompts/command-catalog.d.ts.map +1 -0
- package/dist/prompts/command-catalog.js +187 -0
- package/dist/ui/spinner.d.ts +10 -35
- package/dist/ui/spinner.d.ts.map +1 -1
- package/dist/ui/spinner.js +11 -58
- package/dist/ui/table.d.ts +1 -18
- package/dist/ui/table.d.ts.map +1 -1
- package/dist/ui/table.js +56 -163
- package/dist/utils/ai-error.d.ts +48 -0
- package/dist/utils/ai-error.d.ts.map +1 -0
- package/dist/utils/ai-error.js +190 -0
- package/dist/utils/detect-env.d.ts +6 -8
- package/dist/utils/detect-env.d.ts.map +1 -1
- package/dist/utils/detect-env.js +6 -25
- package/dist/utils/stdin.d.ts +50 -0
- package/dist/utils/stdin.d.ts.map +1 -0
- package/dist/utils/stdin.js +93 -0
- package/node_modules/@vertaaux/tui/dist/index.cjs +1157 -0
- package/node_modules/@vertaaux/tui/dist/index.cjs.map +1 -0
- package/node_modules/@vertaaux/tui/dist/index.d.cts +609 -0
- package/node_modules/@vertaaux/tui/dist/index.d.ts +609 -0
- package/node_modules/@vertaaux/tui/dist/index.js +1100 -0
- package/node_modules/@vertaaux/tui/dist/index.js.map +1 -0
- package/node_modules/@vertaaux/tui/package.json +64 -0
- package/package.json +12 -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
|
|
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
|
|
package/dist/auth/device-flow.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @see https://datatracker.ietf.org/doc/html/rfc8628
|
|
8
8
|
*/
|
|
9
|
-
import
|
|
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 =
|
|
95
|
-
|
|
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.
|
|
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
|
|
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.
|
|
157
|
+
if (spinner.isRunning) {
|
|
160
158
|
spinner.stop();
|
|
161
159
|
}
|
|
162
160
|
}
|
package/dist/commands/audit.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/commands/audit.js
CHANGED
|
@@ -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
|
-
//
|
|
389
|
-
const
|
|
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
|
-
//
|
|
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"}
|