@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.
- package/LICENSE +21 -0
- package/README.md +58 -2
- package/dist/auth/device-flow.d.ts.map +1 -1
- package/dist/auth/device-flow.js +46 -14
- package/dist/commands/audit.d.ts +2 -0
- package/dist/commands/audit.d.ts.map +1 -1
- package/dist/commands/audit.js +167 -8
- package/dist/commands/client.d.ts +14 -0
- package/dist/commands/client.d.ts.map +1 -0
- package/dist/commands/client.js +362 -0
- 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/drift.d.ts +15 -0
- package/dist/commands/drift.d.ts.map +1 -0
- package/dist/commands/drift.js +309 -0
- 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/protect.d.ts +16 -0
- package/dist/commands/protect.d.ts.map +1 -0
- package/dist/commands/protect.js +323 -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/report.d.ts +15 -0
- package/dist/commands/report.d.ts.map +1 -0
- package/dist/commands/report.js +214 -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 +18 -2
- 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/policy/sync.d.ts +67 -0
- package/dist/policy/sync.d.ts.map +1 -0
- package/dist/policy/sync.js +147 -0
- 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/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
|
|
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,
|
|
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"}
|
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
|
*/
|
|
@@ -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 =
|
|
88
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
146
|
-
if (spinner.
|
|
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) =>
|
|
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
|
}
|
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
|
@@ -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
|
-
//
|
|
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
|
*/
|
|
@@ -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
|
|
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"}
|