@vertaaux/cli 0.2.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 (198) hide show
  1. package/README.md +345 -0
  2. package/dist/auth/ci-token.d.ts +49 -0
  3. package/dist/auth/ci-token.d.ts.map +1 -0
  4. package/dist/auth/ci-token.js +83 -0
  5. package/dist/auth/device-flow.d.ts +66 -0
  6. package/dist/auth/device-flow.d.ts.map +1 -0
  7. package/dist/auth/device-flow.js +156 -0
  8. package/dist/auth/token-store.d.ts +53 -0
  9. package/dist/auth/token-store.d.ts.map +1 -0
  10. package/dist/auth/token-store.js +78 -0
  11. package/dist/baseline/diff.d.ts +57 -0
  12. package/dist/baseline/diff.d.ts.map +1 -0
  13. package/dist/baseline/diff.js +152 -0
  14. package/dist/baseline/hash.d.ts +54 -0
  15. package/dist/baseline/hash.d.ts.map +1 -0
  16. package/dist/baseline/hash.js +66 -0
  17. package/dist/baseline/manager.d.ts +89 -0
  18. package/dist/baseline/manager.d.ts.map +1 -0
  19. package/dist/baseline/manager.js +157 -0
  20. package/dist/cache/index.d.ts +8 -0
  21. package/dist/cache/index.d.ts.map +1 -0
  22. package/dist/cache/index.js +7 -0
  23. package/dist/cache/route-cache.d.ts +119 -0
  24. package/dist/cache/route-cache.d.ts.map +1 -0
  25. package/dist/cache/route-cache.js +213 -0
  26. package/dist/ci/changed-routes.d.ts +95 -0
  27. package/dist/ci/changed-routes.d.ts.map +1 -0
  28. package/dist/ci/changed-routes.js +304 -0
  29. package/dist/ci/github-api.d.ts +68 -0
  30. package/dist/ci/github-api.d.ts.map +1 -0
  31. package/dist/ci/github-api.js +138 -0
  32. package/dist/ci/gitlab-api.d.ts +75 -0
  33. package/dist/ci/gitlab-api.d.ts.map +1 -0
  34. package/dist/ci/gitlab-api.js +180 -0
  35. package/dist/ci/index.d.ts +6 -0
  36. package/dist/ci/index.d.ts.map +1 -0
  37. package/dist/ci/index.js +4 -0
  38. package/dist/commands/audit.d.ts +58 -0
  39. package/dist/commands/audit.d.ts.map +1 -0
  40. package/dist/commands/audit.js +862 -0
  41. package/dist/commands/baseline.d.ts +22 -0
  42. package/dist/commands/baseline.d.ts.map +1 -0
  43. package/dist/commands/baseline.js +210 -0
  44. package/dist/commands/comment.d.ts +14 -0
  45. package/dist/commands/comment.d.ts.map +1 -0
  46. package/dist/commands/comment.js +363 -0
  47. package/dist/commands/diff.d.ts +24 -0
  48. package/dist/commands/diff.d.ts.map +1 -0
  49. package/dist/commands/diff.js +196 -0
  50. package/dist/commands/doctor.d.ts +58 -0
  51. package/dist/commands/doctor.d.ts.map +1 -0
  52. package/dist/commands/doctor.js +338 -0
  53. package/dist/commands/download.d.ts +12 -0
  54. package/dist/commands/download.d.ts.map +1 -0
  55. package/dist/commands/download.js +183 -0
  56. package/dist/commands/explain.d.ts +62 -0
  57. package/dist/commands/explain.d.ts.map +1 -0
  58. package/dist/commands/explain.js +302 -0
  59. package/dist/commands/init.d.ts +12 -0
  60. package/dist/commands/init.d.ts.map +1 -0
  61. package/dist/commands/init.js +212 -0
  62. package/dist/commands/login.d.ts +14 -0
  63. package/dist/commands/login.d.ts.map +1 -0
  64. package/dist/commands/login.js +222 -0
  65. package/dist/commands/policy.d.ts +13 -0
  66. package/dist/commands/policy.d.ts.map +1 -0
  67. package/dist/commands/policy.js +347 -0
  68. package/dist/commands/upload.d.ts +12 -0
  69. package/dist/commands/upload.d.ts.map +1 -0
  70. package/dist/commands/upload.js +158 -0
  71. package/dist/config/defaults.d.ts +21 -0
  72. package/dist/config/defaults.d.ts.map +1 -0
  73. package/dist/config/defaults.js +49 -0
  74. package/dist/config/loader.d.ts +66 -0
  75. package/dist/config/loader.d.ts.map +1 -0
  76. package/dist/config/loader.js +167 -0
  77. package/dist/config/schema.d.ts +55 -0
  78. package/dist/config/schema.d.ts.map +1 -0
  79. package/dist/config/schema.js +6 -0
  80. package/dist/index.d.ts +9 -0
  81. package/dist/index.d.ts.map +1 -0
  82. package/dist/index.js +1090 -0
  83. package/dist/interactive/fix-wizard.d.ts +44 -0
  84. package/dist/interactive/fix-wizard.d.ts.map +1 -0
  85. package/dist/interactive/fix-wizard.js +286 -0
  86. package/dist/interactive/init-wizard.d.ts +32 -0
  87. package/dist/interactive/init-wizard.d.ts.map +1 -0
  88. package/dist/interactive/init-wizard.js +193 -0
  89. package/dist/interactive/prompts.d.ts +62 -0
  90. package/dist/interactive/prompts.d.ts.map +1 -0
  91. package/dist/interactive/prompts.js +78 -0
  92. package/dist/monorepo/detector.d.ts +70 -0
  93. package/dist/monorepo/detector.d.ts.map +1 -0
  94. package/dist/monorepo/detector.js +278 -0
  95. package/dist/monorepo/index.d.ts +9 -0
  96. package/dist/monorepo/index.d.ts.map +1 -0
  97. package/dist/monorepo/index.js +8 -0
  98. package/dist/monorepo/workspace.d.ts +142 -0
  99. package/dist/monorepo/workspace.d.ts.map +1 -0
  100. package/dist/monorepo/workspace.js +171 -0
  101. package/dist/output/envelope.d.ts +21 -0
  102. package/dist/output/envelope.d.ts.map +1 -0
  103. package/dist/output/envelope.js +27 -0
  104. package/dist/output/factory.d.ts +73 -0
  105. package/dist/output/factory.d.ts.map +1 -0
  106. package/dist/output/factory.js +60 -0
  107. package/dist/output/formats.d.ts +11 -0
  108. package/dist/output/formats.d.ts.map +1 -0
  109. package/dist/output/formats.js +41 -0
  110. package/dist/output/html.d.ts +45 -0
  111. package/dist/output/html.d.ts.map +1 -0
  112. package/dist/output/html.js +607 -0
  113. package/dist/output/human.d.ts +41 -0
  114. package/dist/output/human.d.ts.map +1 -0
  115. package/dist/output/human.js +274 -0
  116. package/dist/output/json.d.ts +42 -0
  117. package/dist/output/json.d.ts.map +1 -0
  118. package/dist/output/json.js +37 -0
  119. package/dist/output/junit.d.ts +56 -0
  120. package/dist/output/junit.d.ts.map +1 -0
  121. package/dist/output/junit.js +135 -0
  122. package/dist/output/markdown.d.ts +77 -0
  123. package/dist/output/markdown.d.ts.map +1 -0
  124. package/dist/output/markdown.js +411 -0
  125. package/dist/output/sarif.d.ts +160 -0
  126. package/dist/output/sarif.d.ts.map +1 -0
  127. package/dist/output/sarif.js +207 -0
  128. package/dist/policy/evaluator.d.ts +111 -0
  129. package/dist/policy/evaluator.d.ts.map +1 -0
  130. package/dist/policy/evaluator.js +362 -0
  131. package/dist/policy/index.d.ts +15 -0
  132. package/dist/policy/index.d.ts.map +1 -0
  133. package/dist/policy/index.js +11 -0
  134. package/dist/policy/loader.d.ts +97 -0
  135. package/dist/policy/loader.d.ts.map +1 -0
  136. package/dist/policy/loader.js +281 -0
  137. package/dist/policy/schema.d.ts +297 -0
  138. package/dist/policy/schema.d.ts.map +1 -0
  139. package/dist/policy/schema.js +230 -0
  140. package/dist/quality-gate/evaluator.d.ts +58 -0
  141. package/dist/quality-gate/evaluator.d.ts.map +1 -0
  142. package/dist/quality-gate/evaluator.js +274 -0
  143. package/dist/quality-gate/index.d.ts +10 -0
  144. package/dist/quality-gate/index.d.ts.map +1 -0
  145. package/dist/quality-gate/index.js +7 -0
  146. package/dist/quality-gate/types.d.ts +103 -0
  147. package/dist/quality-gate/types.d.ts.map +1 -0
  148. package/dist/quality-gate/types.js +23 -0
  149. package/dist/templates/azure-devops.d.ts +25 -0
  150. package/dist/templates/azure-devops.d.ts.map +1 -0
  151. package/dist/templates/azure-devops.js +109 -0
  152. package/dist/templates/circleci.d.ts +28 -0
  153. package/dist/templates/circleci.d.ts.map +1 -0
  154. package/dist/templates/circleci.js +86 -0
  155. package/dist/templates/github-actions.d.ts +81 -0
  156. package/dist/templates/github-actions.d.ts.map +1 -0
  157. package/dist/templates/github-actions.js +393 -0
  158. package/dist/templates/gitlab-ci.d.ts +26 -0
  159. package/dist/templates/gitlab-ci.d.ts.map +1 -0
  160. package/dist/templates/gitlab-ci.js +70 -0
  161. package/dist/templates/index.d.ts +72 -0
  162. package/dist/templates/index.d.ts.map +1 -0
  163. package/dist/templates/index.js +112 -0
  164. package/dist/templates/jenkins.d.ts +26 -0
  165. package/dist/templates/jenkins.d.ts.map +1 -0
  166. package/dist/templates/jenkins.js +110 -0
  167. package/dist/ui/banner.d.ts +31 -0
  168. package/dist/ui/banner.d.ts.map +1 -0
  169. package/dist/ui/banner.js +84 -0
  170. package/dist/ui/diagnostics.d.ts +39 -0
  171. package/dist/ui/diagnostics.d.ts.map +1 -0
  172. package/dist/ui/diagnostics.js +153 -0
  173. package/dist/ui/spinner.d.ts +61 -0
  174. package/dist/ui/spinner.d.ts.map +1 -0
  175. package/dist/ui/spinner.js +101 -0
  176. package/dist/ui/table.d.ts +63 -0
  177. package/dist/ui/table.d.ts.map +1 -0
  178. package/dist/ui/table.js +236 -0
  179. package/dist/utils/client.d.ts +82 -0
  180. package/dist/utils/client.d.ts.map +1 -0
  181. package/dist/utils/client.js +128 -0
  182. package/dist/utils/detect-env.d.ts +59 -0
  183. package/dist/utils/detect-env.d.ts.map +1 -0
  184. package/dist/utils/detect-env.js +115 -0
  185. package/dist/utils/exit-codes.d.ts +47 -0
  186. package/dist/utils/exit-codes.d.ts.map +1 -0
  187. package/dist/utils/exit-codes.js +61 -0
  188. package/dist/utils/logger.d.ts +87 -0
  189. package/dist/utils/logger.d.ts.map +1 -0
  190. package/dist/utils/logger.js +185 -0
  191. package/dist/utils/sanitize.d.ts +36 -0
  192. package/dist/utils/sanitize.d.ts.map +1 -0
  193. package/dist/utils/sanitize.js +64 -0
  194. package/dist/utils/validators.d.ts +41 -0
  195. package/dist/utils/validators.d.ts.map +1 -0
  196. package/dist/utils/validators.js +123 -0
  197. package/package.json +63 -0
  198. package/schemas/vertaaux.config.schema.json +103 -0
package/dist/index.js ADDED
@@ -0,0 +1,1090 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * VertaaUX CLI - UX audits, accessibility checks, and CI gating.
4
+ *
5
+ * Refactored to use Commander.js for command parsing with config file support.
6
+ * Maintains backward compatibility with existing command syntax.
7
+ */
8
+ import dotenv from "dotenv";
9
+ import fs from "fs";
10
+ import path from "path";
11
+ import { fileURLToPath } from "url";
12
+ import { Command } from "commander";
13
+ import { showBanner, getVersion } from "./ui/banner.js";
14
+ import { registerAuditCommand } from "./commands/audit.js";
15
+ import { registerBaselineCommand } from "./commands/baseline.js";
16
+ import { registerCommentCommand } from "./commands/comment.js";
17
+ import { registerDiffCommand } from "./commands/diff.js";
18
+ import { registerExplainCommand } from "./commands/explain.js";
19
+ import { registerLoginCommand } from "./commands/login.js";
20
+ import { registerInitCommand } from "./commands/init.js";
21
+ import { registerUploadCommand } from "./commands/upload.js";
22
+ import { registerDownloadCommand } from "./commands/download.js";
23
+ import { registerPolicyCommand } from "./commands/policy.js";
24
+ import { registerDoctorCommand } from "./commands/doctor.js";
25
+ import { ExitCode } from "./utils/exit-codes.js";
26
+ import { formatCommanderError } from "./ui/diagnostics.js";
27
+ import { parseMode, parseTimeout, parseInterval, parseScore } from "./utils/validators.js";
28
+ import { isInteractive, selectAction } from "./interactive/prompts.js";
29
+ const __filename = fileURLToPath(import.meta.url);
30
+ const __dirname = path.dirname(__filename);
31
+ // Load environment variables from multiple locations
32
+ const envCandidates = [
33
+ path.resolve(process.cwd(), ".env.local"),
34
+ path.resolve(process.cwd(), ".env"),
35
+ path.resolve(__dirname, "../.env.local"),
36
+ path.resolve(__dirname, "../.env"),
37
+ path.resolve(__dirname, "../../.env.local"),
38
+ path.resolve(__dirname, "../../.env"),
39
+ ];
40
+ // Silence any dotenv stdout output during loading
41
+ const originalLog = console.log;
42
+ console.log = (...args) => {
43
+ const msg = String(args[0] || "");
44
+ if (msg.includes("[dotenv"))
45
+ return; // suppress dotenv banners (e.g. [dotenv@17.2.3])
46
+ originalLog.apply(console, args);
47
+ };
48
+ for (const candidate of envCandidates) {
49
+ if (fs.existsSync(candidate)) {
50
+ dotenv.config({ path: candidate, override: false });
51
+ }
52
+ }
53
+ console.log = originalLog;
54
+ const DEFAULT_API_BASE = "https://vertaaux.ai/v1";
55
+ // ============================================================================
56
+ // Legacy helper functions (kept for backward compatibility with other commands)
57
+ // ============================================================================
58
+ function parseArgs(args) {
59
+ const positional = [];
60
+ const flags = {};
61
+ for (let i = 0; i < args.length; i++) {
62
+ const arg = args[i];
63
+ if (arg === "--") {
64
+ positional.push(...args.slice(i + 1));
65
+ break;
66
+ }
67
+ if (arg.startsWith("--")) {
68
+ const [rawKey, rawValue] = arg.slice(2).split("=", 2);
69
+ if (rawValue !== undefined) {
70
+ flags[rawKey] = rawValue;
71
+ continue;
72
+ }
73
+ const next = args[i + 1];
74
+ if (next && !next.startsWith("-")) {
75
+ flags[rawKey] = next;
76
+ i += 1;
77
+ }
78
+ else {
79
+ flags[rawKey] = true;
80
+ }
81
+ continue;
82
+ }
83
+ if (arg.startsWith("-") && arg.length > 1) {
84
+ const key = arg.slice(1);
85
+ if (key === "u") {
86
+ const next = args[i + 1];
87
+ if (next && !next.startsWith("-")) {
88
+ flags.url = next;
89
+ i += 1;
90
+ }
91
+ else {
92
+ flags.url = true;
93
+ }
94
+ continue;
95
+ }
96
+ if (key === "b") {
97
+ const next = args[i + 1];
98
+ if (next && !next.startsWith("-")) {
99
+ flags.base = next;
100
+ i += 1;
101
+ }
102
+ else {
103
+ flags.base = true;
104
+ }
105
+ continue;
106
+ }
107
+ if (key === "f") {
108
+ const next = args[i + 1];
109
+ if (next && !next.startsWith("-")) {
110
+ flags.format = next;
111
+ i += 1;
112
+ }
113
+ else {
114
+ flags.format = true;
115
+ }
116
+ continue;
117
+ }
118
+ if (key === "h") {
119
+ flags.help = true;
120
+ continue;
121
+ }
122
+ if (key === "q") {
123
+ flags.quiet = true;
124
+ continue;
125
+ }
126
+ const next = args[i + 1];
127
+ if (next && !next.startsWith("-")) {
128
+ flags[key] = next;
129
+ i += 1;
130
+ }
131
+ else {
132
+ flags[key] = true;
133
+ }
134
+ continue;
135
+ }
136
+ positional.push(arg);
137
+ }
138
+ return { positional, flags };
139
+ }
140
+ function getString(flags, key) {
141
+ const value = flags[key];
142
+ if (typeof value === "string")
143
+ return value;
144
+ // Fallback: try camelCase version (Commander.js converts --multi-word to multiWord)
145
+ const camelKey = key.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
146
+ if (camelKey !== key) {
147
+ const camelValue = flags[camelKey];
148
+ if (typeof camelValue === "string")
149
+ return camelValue;
150
+ }
151
+ return undefined;
152
+ }
153
+ function getBool(flags, key) {
154
+ if (flags[key] === true)
155
+ return true;
156
+ // Fallback: try camelCase version (Commander.js converts --multi-word to multiWord)
157
+ const camelKey = key.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
158
+ if (camelKey !== key)
159
+ return flags[camelKey] === true;
160
+ return false;
161
+ }
162
+ function getNumber(flags, key) {
163
+ const raw = getString(flags, key);
164
+ if (raw === undefined)
165
+ return undefined;
166
+ const parsed = Number(raw);
167
+ if (Number.isNaN(parsed)) {
168
+ throw new Error(`Invalid --${key} value: ${raw}`);
169
+ }
170
+ return parsed;
171
+ }
172
+ function resolveFormat(flags) {
173
+ if (getBool(flags, "json"))
174
+ return "json";
175
+ if (getBool(flags, "md") || getBool(flags, "markdown"))
176
+ return "md";
177
+ const raw = getString(flags, "format") || "json";
178
+ if (raw === "md" || raw === "markdown")
179
+ return "md";
180
+ return "json";
181
+ }
182
+ function resolveApiBase(flags) {
183
+ const raw = getString(flags, "base") || process.env.VERTAAUX_API_BASE || DEFAULT_API_BASE;
184
+ return raw.replace(/\/$/, "");
185
+ }
186
+ function getApiKey() {
187
+ const key = process.env.VERTAAUX_API_KEY;
188
+ if (!key) {
189
+ throw new Error("VERTAAUX_API_KEY is required");
190
+ }
191
+ return key;
192
+ }
193
+ async function apiRequest(base, reqPath, options) {
194
+ const apiKey = getApiKey();
195
+ const url = `${base}${reqPath}`;
196
+ const res = await fetch(url, {
197
+ method: options.method,
198
+ headers: {
199
+ "Content-Type": "application/json",
200
+ "X-API-Key": apiKey,
201
+ },
202
+ body: options.body ? JSON.stringify(options.body) : undefined,
203
+ });
204
+ if (!res.ok) {
205
+ let detail = res.statusText;
206
+ try {
207
+ const data = (await res.json());
208
+ detail =
209
+ (data && (data.error || data.message)) ||
210
+ JSON.stringify(data) ||
211
+ detail;
212
+ }
213
+ catch {
214
+ try {
215
+ detail = await res.text();
216
+ }
217
+ catch {
218
+ // ignore
219
+ }
220
+ }
221
+ throw new Error(`HTTP ${res.status}: ${detail}`);
222
+ }
223
+ return (await res.json().catch(() => ({})));
224
+ }
225
+ function sleep(ms) {
226
+ return new Promise((resolve) => setTimeout(resolve, ms));
227
+ }
228
+ async function waitForAudit(base, jobId, timeoutMs, intervalMs) {
229
+ const start = Date.now();
230
+ while (true) {
231
+ const status = await apiRequest(base, `/audit/${jobId}`, {
232
+ method: "GET",
233
+ });
234
+ if (status.status === "completed")
235
+ return status;
236
+ if (status.status === "failed")
237
+ return status;
238
+ if (Date.now() - start > timeoutMs) {
239
+ throw new Error(`Timed out waiting for audit ${jobId}`);
240
+ }
241
+ await sleep(intervalMs);
242
+ }
243
+ }
244
+ function normalizeIssues(issues) {
245
+ if (Array.isArray(issues))
246
+ return issues;
247
+ if (issues && typeof issues === "object") {
248
+ const values = Object.values(issues);
249
+ return values.flatMap((value) => (Array.isArray(value) ? value : []));
250
+ }
251
+ return [];
252
+ }
253
+ function toNumber(value) {
254
+ if (typeof value === "number" && Number.isFinite(value))
255
+ return value;
256
+ return null;
257
+ }
258
+ function getOverallScore(scores) {
259
+ if (!scores)
260
+ return null;
261
+ const direct = toNumber(scores.overall ?? scores.ux ?? scores.total);
262
+ if (direct !== null)
263
+ return direct;
264
+ const numeric = Object.values(scores)
265
+ .map((value) => toNumber(value))
266
+ .filter((value) => value !== null);
267
+ if (numeric.length === 0)
268
+ return null;
269
+ const avg = numeric.reduce((sum, value) => sum + value, 0) / numeric.length;
270
+ return Math.round(avg);
271
+ }
272
+ function getCategoryScore(scores, key) {
273
+ if (!scores)
274
+ return null;
275
+ return toNumber(scores[key]);
276
+ }
277
+ function formatScoresTable(scores) {
278
+ if (!scores)
279
+ return "No scores available.";
280
+ const entries = Object.entries(scores).filter(([, value]) => typeof value === "number");
281
+ if (entries.length === 0)
282
+ return "No scores available.";
283
+ const lines = ["| Category | Score |", "| --- | --- |"];
284
+ for (const [key, value] of entries) {
285
+ lines.push(`| ${key} | ${value} |`);
286
+ }
287
+ return lines.join("\n");
288
+ }
289
+ function formatIssuesList(issues, limit = 5) {
290
+ if (!issues.length)
291
+ return "No issues.";
292
+ const lines = issues.slice(0, limit).map((issue, index) => {
293
+ const label = issue.title || issue.id || "Issue";
294
+ const severity = issue.severity ? issue.severity.toUpperCase() : "INFO";
295
+ return `${index + 1}. [${severity}] ${label}`;
296
+ });
297
+ if (issues.length > limit) {
298
+ lines.push(`...and ${issues.length - limit} more`);
299
+ }
300
+ return lines.join("\n");
301
+ }
302
+ function formatAuditMarkdown(result) {
303
+ const status = result.status || "unknown";
304
+ const lines = [`## Audit Results`, `- Status: ${status}`];
305
+ if (result.job_id)
306
+ lines.push(`- Job ID: ${result.job_id}`);
307
+ if (result.url)
308
+ lines.push(`- URL: ${result.url}`);
309
+ if (result.mode)
310
+ lines.push(`- Mode: ${result.mode}`);
311
+ if (typeof result.progress === "number" && status !== "completed") {
312
+ lines.push(`- Progress: ${result.progress}%`);
313
+ }
314
+ if (status !== "completed")
315
+ return lines.join("\n");
316
+ const overall = getOverallScore(result.scores);
317
+ if (overall !== null)
318
+ lines.push(`- Overall score: ${overall}`);
319
+ lines.push("\n### Scores");
320
+ lines.push(formatScoresTable(result.scores));
321
+ const issues = normalizeIssues(result.issues);
322
+ lines.push("\n### Issues");
323
+ lines.push(`Total issues: ${issues.length}`);
324
+ lines.push(formatIssuesList(issues));
325
+ return lines.join("\n");
326
+ }
327
+ function formatA11yMarkdown(result) {
328
+ const lines = [`## Accessibility Audit`, `- Status: ${result.status || "unknown"}`];
329
+ if (result.url)
330
+ lines.push(`- URL: ${result.url}`);
331
+ if (result.mode)
332
+ lines.push(`- Mode: ${result.mode}`);
333
+ if (result.status !== "completed")
334
+ return lines.join("\n");
335
+ const a11yScore = getCategoryScore(result.scores, "accessibility");
336
+ if (a11yScore !== null)
337
+ lines.push(`- Accessibility score: ${a11yScore}`);
338
+ const issues = normalizeIssues(result.issues).filter((issue) => {
339
+ const category = (issue.category || "").toLowerCase();
340
+ return (category.includes("accessibility") ||
341
+ category.includes("a11y") ||
342
+ category.includes("wcag"));
343
+ });
344
+ lines.push("\n### Accessibility Issues");
345
+ lines.push(`Total accessibility issues: ${issues.length}`);
346
+ lines.push(formatIssuesList(issues));
347
+ return lines.join("\n");
348
+ }
349
+ function formatCompareMarkdown(compare) {
350
+ const lines = ["## Audit Comparison"];
351
+ lines.push(`- URL A: ${compare.urlA}`);
352
+ lines.push(`- URL B: ${compare.urlB}`);
353
+ lines.push(`- Job A: ${compare.jobA}`);
354
+ lines.push(`- Job B: ${compare.jobB}`);
355
+ lines.push("\n### Overall Scores");
356
+ lines.push("| Site | Score | Delta |");
357
+ lines.push("| --- | --- | --- |");
358
+ lines.push(`| A | ${compare.overallA ?? "n/a"} | ${compare.delta ?? "n/a"} |`);
359
+ lines.push(`| B | ${compare.overallB ?? "n/a"} | ${compare.delta ?? "n/a"} |`);
360
+ lines.push("\n### Category Scores");
361
+ lines.push("| Category | A | B | Delta |");
362
+ lines.push("| --- | --- | --- | --- |");
363
+ for (const [key, values] of Object.entries(compare.categoryDeltas)) {
364
+ lines.push(`| ${key} | ${values.a ?? "n/a"} | ${values.b ?? "n/a"} | ${values.delta ?? "n/a"} |`);
365
+ }
366
+ lines.push("\n### Issue Counts");
367
+ lines.push(`- A: ${compare.issuesA}`);
368
+ lines.push(`- B: ${compare.issuesB}`);
369
+ return lines.join("\n");
370
+ }
371
+ function formatExplainMarkdown(issue) {
372
+ const lines = ["## Issue Explanation"];
373
+ if (issue.id)
374
+ lines.push(`- ID: ${issue.id}`);
375
+ if (issue.severity)
376
+ lines.push(`- Severity: ${issue.severity}`);
377
+ if (issue.category)
378
+ lines.push(`- Category: ${issue.category}`);
379
+ if (issue.selector)
380
+ lines.push(`- Selector: ${issue.selector}`);
381
+ if (issue.description) {
382
+ lines.push("\n### Description");
383
+ lines.push(issue.description);
384
+ }
385
+ const recommendation = issue.recommendation || issue.recommended_fix;
386
+ if (recommendation) {
387
+ lines.push("\n### Recommendation");
388
+ lines.push(recommendation);
389
+ }
390
+ if (issue.wcag_reference) {
391
+ lines.push("\n### WCAG Reference");
392
+ lines.push(issue.wcag_reference);
393
+ }
394
+ return lines.join("\n");
395
+ }
396
+ function formatPatchMarkdown(patch) {
397
+ if (!patch)
398
+ return "No patch generated.";
399
+ const lines = [
400
+ "## Patch Generated",
401
+ "",
402
+ `**Confidence:** ${patch.confidence.label} (${patch.confidence.percentage}%)`,
403
+ `**Classification:** ${patch.classification}`,
404
+ "",
405
+ "### Explanation",
406
+ patch.explanation,
407
+ "",
408
+ "### Search",
409
+ "```",
410
+ patch.search,
411
+ "```",
412
+ "",
413
+ "### Replace",
414
+ "```",
415
+ patch.replace,
416
+ "```",
417
+ ];
418
+ return lines.join("\n");
419
+ }
420
+ function formatVerifyMarkdown(result) {
421
+ if (!result.success || !result.verification) {
422
+ return `## Verification Failed\n\n${result.error?.message || "Unknown error"}`;
423
+ }
424
+ const v = result.verification;
425
+ const deltaSign = v.score_delta.delta >= 0 ? "+" : "";
426
+ const lines = [
427
+ "## Verification Result",
428
+ "",
429
+ `**Issue Fixed:** ${v.issue_fixed ? "Yes" : "No"}`,
430
+ `**Score Delta:** ${v.score_delta.before} -> ${v.score_delta.after} (${deltaSign}${v.score_delta.delta})`,
431
+ `**Component:** ${v.component_audited}`,
432
+ `**Duration:** ${v.duration_ms}ms`,
433
+ ];
434
+ if (v.regressions.length > 0) {
435
+ lines.push("", "### Regressions");
436
+ for (const r of v.regressions) {
437
+ lines.push(`- [${r.impact.toUpperCase()}] ${r.rule_id}: ${r.description}`);
438
+ }
439
+ }
440
+ else {
441
+ lines.push("", "### Regressions", "None");
442
+ }
443
+ if (v.error) {
444
+ lines.push("", `**Warning:** ${v.error}`);
445
+ }
446
+ return lines.join("\n");
447
+ }
448
+ // Auto-fixable issue types (from lib/patch/classifier.ts)
449
+ const AUTO_FIXABLE_TYPES = new Set([
450
+ "label",
451
+ "input-missing-label",
452
+ "form-field-label",
453
+ "button-name",
454
+ "missing-button-name",
455
+ "link-name",
456
+ "html-has-lang",
457
+ "missing-lang-attribute",
458
+ "html-lang-valid",
459
+ "document-title",
460
+ "bypass",
461
+ "region",
462
+ "landmark-one-main",
463
+ "color-contrast",
464
+ "autocomplete-valid",
465
+ "aria-required-attr",
466
+ "aria-valid-attr",
467
+ "aria-valid-attr-value",
468
+ ]);
469
+ function isAutoFixable(issueType) {
470
+ const normalized = issueType.toLowerCase().replace(/^(axe|wcag)-/, "");
471
+ return AUTO_FIXABLE_TYPES.has(normalized);
472
+ }
473
+ function formatBatchMarkdown(results) {
474
+ const lines = [
475
+ "## Batch Patch Results",
476
+ "",
477
+ `**Total Requested:** ${results.totalRequested}`,
478
+ `**Succeeded:** ${results.successes.length}`,
479
+ `**Failed:** ${results.failures.length}`,
480
+ `**Skipped:** ${results.totalSkipped}`,
481
+ ];
482
+ if (results.successes.length > 0) {
483
+ lines.push("", "### Successful Patches");
484
+ for (const s of results.successes) {
485
+ const conf = s.patch?.confidence;
486
+ const cls = s.patch?.classification || "unknown";
487
+ const confStr = conf ? `${conf.label} (${conf.percentage}%)` : "n/a";
488
+ lines.push(`- ${s.issueId}: ${confStr} [${cls}]`);
489
+ }
490
+ }
491
+ if (results.failures.length > 0) {
492
+ lines.push("", "### Failed");
493
+ for (const f of results.failures) {
494
+ const details = f.details ? ` - ${f.details}` : "";
495
+ lines.push(`- ${f.issueId}: ${f.reason}${details}`);
496
+ }
497
+ }
498
+ return lines.join("\n");
499
+ }
500
+ function printOutput(format, data, markdown) {
501
+ if (format === "md") {
502
+ console.log(markdown || "");
503
+ return;
504
+ }
505
+ console.log(JSON.stringify(data, null, 2));
506
+ }
507
+ // ============================================================================
508
+ // Legacy command handlers (a11y, scan, compare, status, explain, fix, etc.)
509
+ // ============================================================================
510
+ async function runAuditCommand(base, url, flags, label) {
511
+ const mode = getString(flags, "mode") || "basic";
512
+ const timeout = getNumber(flags, "timeout");
513
+ const userAgent = getString(flags, "user-agent");
514
+ const engineVersion = getString(flags, "engine-version");
515
+ const failOnScore = getNumber(flags, "fail-on-score");
516
+ const created = await apiRequest(base, "/audit", {
517
+ method: "POST",
518
+ body: {
519
+ url,
520
+ mode,
521
+ timeout,
522
+ user_agent: userAgent,
523
+ engine_version: engineVersion,
524
+ fail_on_score: failOnScore,
525
+ },
526
+ });
527
+ const format = resolveFormat(flags);
528
+ if (!getBool(flags, "wait")) {
529
+ const markdown = `## Audit queued\n- Job ID: ${created.job_id}\n- URL: ${created.url}\n- Mode: ${created.mode}`;
530
+ printOutput(format, created, markdown);
531
+ return;
532
+ }
533
+ if (!created.job_id) {
534
+ throw new Error("Audit response missing job_id");
535
+ }
536
+ const waitTimeout = getNumber(flags, "timeout") || 60000;
537
+ const interval = getNumber(flags, "interval") || 5000;
538
+ const result = await waitForAudit(base, created.job_id, waitTimeout, interval);
539
+ if (label === "a11y") {
540
+ const a11yScore = getCategoryScore(result.scores, "accessibility");
541
+ if (failOnScore !== undefined && a11yScore !== null && a11yScore < failOnScore) {
542
+ process.exitCode = 1;
543
+ }
544
+ const issues = normalizeIssues(result.issues).filter((issue) => {
545
+ const category = (issue.category || "").toLowerCase();
546
+ return (category.includes("accessibility") ||
547
+ category.includes("a11y") ||
548
+ category.includes("wcag"));
549
+ });
550
+ const payload = {
551
+ ...result,
552
+ accessibility_score: a11yScore,
553
+ accessibility_issues: issues,
554
+ };
555
+ printOutput(format, payload, formatA11yMarkdown(result));
556
+ return;
557
+ }
558
+ const overall = getOverallScore(result.scores);
559
+ if (failOnScore !== undefined && overall !== null && overall < failOnScore) {
560
+ process.exitCode = 1;
561
+ }
562
+ printOutput(format, result, formatAuditMarkdown(result));
563
+ }
564
+ async function runCompareCommand(base, urls, flags) {
565
+ const mode = getString(flags, "mode") || "basic";
566
+ const wait = getBool(flags, "wait");
567
+ const format = resolveFormat(flags);
568
+ const [urlA, urlB] = urls;
569
+ const jobA = await apiRequest(base, "/audit", {
570
+ method: "POST",
571
+ body: { url: urlA, mode },
572
+ });
573
+ const jobB = await apiRequest(base, "/audit", {
574
+ method: "POST",
575
+ body: { url: urlB, mode },
576
+ });
577
+ if (!wait) {
578
+ const payload = { job_a: jobA, job_b: jobB };
579
+ const markdown = `## Audit comparison queued\n- Job A: ${jobA.job_id}\n- Job B: ${jobB.job_id}`;
580
+ printOutput(format, payload, markdown);
581
+ return;
582
+ }
583
+ if (!jobA.job_id || !jobB.job_id) {
584
+ throw new Error("Compare response missing job_id");
585
+ }
586
+ const timeout = getNumber(flags, "timeout") || 60000;
587
+ const interval = getNumber(flags, "interval") || 5000;
588
+ const resultA = await waitForAudit(base, jobA.job_id, timeout, interval);
589
+ const resultB = await waitForAudit(base, jobB.job_id, timeout, interval);
590
+ const overallA = getOverallScore(resultA.scores);
591
+ const overallB = getOverallScore(resultB.scores);
592
+ const delta = overallA !== null && overallB !== null ? overallB - overallA : null;
593
+ const scoresA = resultA.scores || {};
594
+ const scoresB = resultB.scores || {};
595
+ const keys = new Set([...Object.keys(scoresA), ...Object.keys(scoresB)]);
596
+ const categoryDeltas = {};
597
+ for (const key of keys) {
598
+ const a = toNumber(scoresA[key]);
599
+ const b = toNumber(scoresB[key]);
600
+ categoryDeltas[key] = {
601
+ a,
602
+ b,
603
+ delta: a !== null && b !== null ? b - a : null,
604
+ };
605
+ }
606
+ const issuesA = normalizeIssues(resultA.issues).length;
607
+ const issuesB = normalizeIssues(resultB.issues).length;
608
+ const compare = {
609
+ urlA,
610
+ urlB,
611
+ jobA: resultA.job_id || "",
612
+ jobB: resultB.job_id || "",
613
+ overallA,
614
+ overallB,
615
+ delta,
616
+ categoryDeltas,
617
+ issuesA,
618
+ issuesB,
619
+ };
620
+ const failOnScore = getNumber(flags, "fail-on-score");
621
+ if (failOnScore !== undefined &&
622
+ ((overallA !== null && overallA < failOnScore) ||
623
+ (overallB !== null && overallB < failOnScore))) {
624
+ process.exitCode = 1;
625
+ }
626
+ printOutput(format, compare, formatCompareMarkdown(compare));
627
+ }
628
+ async function runFixCommand(base, jobId, issueId, flags) {
629
+ const fileContent = getString(flags, "file-content");
630
+ const format = resolveFormat(flags);
631
+ if (!fileContent) {
632
+ throw new Error("fix requires --file-content <code>");
633
+ }
634
+ const isTTYOutput = process.stdout.isTTY && !getBool(flags, "json");
635
+ if (isTTYOutput) {
636
+ process.stderr.write("Generating patch...\r");
637
+ }
638
+ const result = await apiRequest(base, "/patch", {
639
+ method: "POST",
640
+ body: {
641
+ job_id: jobId,
642
+ issue_id: issueId,
643
+ file_content: fileContent,
644
+ },
645
+ });
646
+ if (isTTYOutput) {
647
+ process.stderr.write(" \r");
648
+ }
649
+ if (!result.success) {
650
+ process.exitCode = 1;
651
+ }
652
+ if (format === "json") {
653
+ console.log(JSON.stringify(result, null, 2));
654
+ }
655
+ else {
656
+ if (result.success && result.patch) {
657
+ console.log(formatPatchMarkdown(result.patch));
658
+ }
659
+ else {
660
+ console.log(`## Patch Generation Failed\n\n${result.error?.message || "Unknown error"}`);
661
+ }
662
+ }
663
+ }
664
+ async function runVerifyCommand(base, flags) {
665
+ const search = getString(flags, "search");
666
+ const replace = getString(flags, "replace");
667
+ const issueId = getString(flags, "issue");
668
+ const url = getString(flags, "url");
669
+ const selector = getString(flags, "selector");
670
+ const timeoutMs = getNumber(flags, "timeout");
671
+ const format = resolveFormat(flags);
672
+ if (!search || !replace || !issueId || !url || !selector) {
673
+ throw new Error("verify requires --search, --replace, --issue, --url, and --selector");
674
+ }
675
+ const isTTYOutput = process.stdout.isTTY && !getBool(flags, "json");
676
+ if (isTTYOutput) {
677
+ process.stderr.write("Running verification...\r");
678
+ }
679
+ const result = await apiRequest(base, "/verify", {
680
+ method: "POST",
681
+ body: {
682
+ patch: {
683
+ search,
684
+ replace,
685
+ issue_id: issueId,
686
+ },
687
+ url,
688
+ selector,
689
+ timeout_ms: timeoutMs || 60000,
690
+ },
691
+ });
692
+ if (isTTYOutput) {
693
+ process.stderr.write(" \r");
694
+ }
695
+ // Exit code: 1 if verification failed OR issue not fixed OR regressions
696
+ if (!result.success) {
697
+ process.exitCode = 1;
698
+ }
699
+ else if (result.verification &&
700
+ (!result.verification.issue_fixed || result.verification.regressions.length > 0)) {
701
+ process.exitCode = 1;
702
+ }
703
+ if (format === "json") {
704
+ console.log(JSON.stringify(result, null, 2));
705
+ }
706
+ else {
707
+ console.log(formatVerifyMarkdown(result));
708
+ }
709
+ }
710
+ const BATCH_LIMIT = 10;
711
+ async function runFixAllCommand(base, jobId, flags) {
712
+ const fileContent = getString(flags, "file-content");
713
+ const autoFixOnly = getBool(flags, "auto-fix-only");
714
+ const format = resolveFormat(flags);
715
+ if (!fileContent) {
716
+ throw new Error("fix-all requires --file-content <code>");
717
+ }
718
+ const isTTYOutput = process.stdout.isTTY && !getBool(flags, "json");
719
+ // Fetch audit to get issues
720
+ if (isTTYOutput) {
721
+ process.stderr.write("Fetching audit...\r");
722
+ }
723
+ const audit = await apiRequest(base, `/audit/${jobId}`, {
724
+ method: "GET",
725
+ });
726
+ if (audit.status !== "completed") {
727
+ throw new Error(`Audit ${jobId} is not completed (status: ${audit.status})`);
728
+ }
729
+ // Normalize and filter issues
730
+ let issues = normalizeIssues(audit.issues);
731
+ const totalRequested = issues.length;
732
+ if (autoFixOnly) {
733
+ issues = issues.filter((issue) => {
734
+ const typeId = issue.id || "";
735
+ return isAutoFixable(typeId);
736
+ });
737
+ }
738
+ // Respect batch limit
739
+ const totalSkipped = Math.max(0, issues.length - BATCH_LIMIT);
740
+ const issuesToProcess = issues.slice(0, BATCH_LIMIT);
741
+ if (isTTYOutput) {
742
+ process.stderr.write(" \r");
743
+ }
744
+ const results = {
745
+ successes: [],
746
+ failures: [],
747
+ totalRequested,
748
+ totalSkipped,
749
+ };
750
+ // Process issues sequentially to avoid overwhelming LLM API
751
+ for (let i = 0; i < issuesToProcess.length; i++) {
752
+ const issue = issuesToProcess[i];
753
+ const curIssueId = issue.id || `issue-${i}`;
754
+ if (isTTYOutput) {
755
+ process.stderr.write(`Generating patch ${i + 1}/${issuesToProcess.length}...\r`);
756
+ }
757
+ try {
758
+ const patchResult = await apiRequest(base, "/patch", {
759
+ method: "POST",
760
+ body: {
761
+ job_id: jobId,
762
+ issue_id: curIssueId,
763
+ file_content: fileContent,
764
+ },
765
+ });
766
+ if (patchResult.success && patchResult.patch) {
767
+ results.successes.push({
768
+ issueId: curIssueId,
769
+ patch: patchResult.patch,
770
+ });
771
+ }
772
+ else {
773
+ results.failures.push({
774
+ issueId: curIssueId,
775
+ reason: patchResult.error?.code || "PATCH_FAILED",
776
+ details: patchResult.error?.message,
777
+ });
778
+ }
779
+ }
780
+ catch (err) {
781
+ results.failures.push({
782
+ issueId: curIssueId,
783
+ reason: "REQUEST_ERROR",
784
+ details: err instanceof Error ? err.message : String(err),
785
+ });
786
+ }
787
+ }
788
+ // Clear progress line
789
+ if (isTTYOutput) {
790
+ process.stderr.write(" \r");
791
+ }
792
+ // Exit code 1 if any failures
793
+ if (results.failures.length > 0) {
794
+ process.exitCode = 1;
795
+ }
796
+ if (format === "json") {
797
+ console.log(JSON.stringify(results, null, 2));
798
+ }
799
+ else {
800
+ console.log(formatBatchMarkdown(results));
801
+ }
802
+ }
803
+ async function runExplainCommand(base, flags) {
804
+ const format = resolveFormat(flags);
805
+ const issueJson = getString(flags, "issue");
806
+ const issueFile = getString(flags, "file");
807
+ const jobId = getString(flags, "job") || getString(flags, "job-id");
808
+ const issueId = getString(flags, "issue-id") || getString(flags, "id");
809
+ let issue = null;
810
+ if (issueJson) {
811
+ issue = JSON.parse(issueJson);
812
+ }
813
+ else if (issueFile) {
814
+ const raw = fs.readFileSync(issueFile, "utf-8");
815
+ issue = JSON.parse(raw);
816
+ }
817
+ else if (jobId && issueId) {
818
+ const result = await apiRequest(base, `/audit/${jobId}`, {
819
+ method: "GET",
820
+ });
821
+ const issues = normalizeIssues(result.issues);
822
+ issue = issues.find((item) => item.id === issueId) || null;
823
+ if (!issue) {
824
+ throw new Error(`Issue ${issueId} not found in job ${jobId}`);
825
+ }
826
+ }
827
+ if (!issue) {
828
+ throw new Error("Provide --issue, --file, or --job with --issue-id");
829
+ }
830
+ const payload = { issue };
831
+ printOutput(format, payload, formatExplainMarkdown(issue));
832
+ }
833
+ // ============================================================================
834
+ // Commander.js CLI setup
835
+ // ============================================================================
836
+ const version = getVersion();
837
+ const program = new Command();
838
+ program
839
+ .name("vertaa")
840
+ .description("VertaaUX CLI for UX audits, accessibility checks, and CI gating")
841
+ .version(version, "-v, --version", "Output the version number")
842
+ .enablePositionalOptions()
843
+ .option("-b, --base <url>", "API base URL")
844
+ .option("-c, --config <path>", "Path to config file (overrides auto-detection)")
845
+ .option("-q, --quiet", "Suppress banner and non-essential output")
846
+ .option("--no-banner", "Hide the V-mark banner")
847
+ .option("--machine", "Strict machine-readable output (JSON stdout, diagnostics stderr)")
848
+ .configureOutput({
849
+ outputError: (str, _write) => {
850
+ const formatted = formatCommanderError(str);
851
+ process.stderr.write(formatted + "\n");
852
+ },
853
+ })
854
+ .exitOverride((err) => {
855
+ if (err.code === "commander.invalidArgument" ||
856
+ err.code === "commander.missingArgument" ||
857
+ err.code === "commander.missingMandatoryOptionValue" ||
858
+ err.code === "commander.optionMissingArgument") {
859
+ process.exit(2);
860
+ }
861
+ // For other Commander errors (help, version), use Commander's default exit code
862
+ process.exit(err.exitCode);
863
+ })
864
+ .hook("preAction", (thisCommand) => {
865
+ const opts = thisCommand.optsWithGlobals();
866
+ const machineMode = opts.machine || false;
867
+ if (machineMode) {
868
+ opts.quiet = true;
869
+ opts.banner = false;
870
+ }
871
+ showBanner({
872
+ version,
873
+ quiet: opts.quiet || machineMode,
874
+ noBanner: opts.banner === false || machineMode,
875
+ });
876
+ });
877
+ // Register commands (new Commander.js implementations)
878
+ registerAuditCommand(program);
879
+ registerBaselineCommand(program);
880
+ registerCommentCommand(program);
881
+ registerDiffCommand(program);
882
+ registerExplainCommand(program);
883
+ registerLoginCommand(program);
884
+ registerInitCommand(program);
885
+ registerUploadCommand(program);
886
+ registerDownloadCommand(program);
887
+ registerPolicyCommand(program);
888
+ registerDoctorCommand(program);
889
+ // Legacy commands using old argument parsing
890
+ // These will be migrated in subsequent plans
891
+ program
892
+ .command("a11y <url>")
893
+ .description("Run accessibility-focused audit (alias for audit with a11y filter)")
894
+ .option("--mode <mode>", "Audit depth: basic|standard|deep", parseMode, "basic")
895
+ .option("--wait", "Wait for audit completion")
896
+ .option("--timeout <ms>", "Wait timeout in milliseconds (1-300000)", parseTimeout, 60000)
897
+ .option("--interval <ms>", "Poll interval in milliseconds (1-300000)", parseInterval, 5000)
898
+ .option("--fail-on-score <n>", "Exit non-zero if score below n (0-100)", parseScore)
899
+ .action(async (url, cmdOptions) => {
900
+ try {
901
+ const base = resolveApiBase(cmdOptions);
902
+ await runAuditCommand(base, url, { ...cmdOptions, wait: cmdOptions.wait ?? true }, "a11y");
903
+ }
904
+ catch (error) {
905
+ process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}\n`);
906
+ process.exit(ExitCode.ERROR);
907
+ }
908
+ });
909
+ program
910
+ .command("scan <url>")
911
+ .description("Run UX scan (alias for audit)")
912
+ .option("--mode <mode>", "Audit depth: basic|standard|deep", parseMode, "basic")
913
+ .option("--wait", "Wait for audit completion")
914
+ .option("--timeout <ms>", "Wait timeout in milliseconds (1-300000)", parseTimeout, 60000)
915
+ .option("--interval <ms>", "Poll interval in milliseconds (1-300000)", parseInterval, 5000)
916
+ .option("--fail-on-score <n>", "Exit non-zero if score below n (0-100)", parseScore)
917
+ .action(async (url, cmdOptions) => {
918
+ try {
919
+ const base = resolveApiBase(cmdOptions);
920
+ await runAuditCommand(base, url, { ...cmdOptions, wait: cmdOptions.wait ?? true }, "scan");
921
+ }
922
+ catch (error) {
923
+ process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}\n`);
924
+ process.exit(ExitCode.ERROR);
925
+ }
926
+ });
927
+ program
928
+ .command("compare <urlA> <urlB>")
929
+ .description("Compare audits of two URLs")
930
+ .option("--mode <mode>", "Audit depth: basic|standard|deep", parseMode, "basic")
931
+ .option("--wait", "Wait for audits to complete")
932
+ .option("--timeout <ms>", "Wait timeout in milliseconds (1-300000)", parseTimeout, 60000)
933
+ .option("--interval <ms>", "Poll interval in milliseconds (1-300000)", parseInterval, 5000)
934
+ .option("--fail-on-score <n>", "Exit non-zero if score below n (0-100)", parseScore)
935
+ .action(async (urlA, urlB, cmdOptions) => {
936
+ try {
937
+ const base = resolveApiBase(cmdOptions);
938
+ await runCompareCommand(base, [urlA, urlB], cmdOptions);
939
+ }
940
+ catch (error) {
941
+ process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}\n`);
942
+ process.exit(ExitCode.ERROR);
943
+ }
944
+ });
945
+ program
946
+ .command("status <jobId>")
947
+ .description("Get status of an audit job")
948
+ .option("--fail-on-score <n>", "Exit non-zero if score below n (0-100)", parseScore)
949
+ .action(async (jobId, cmdOptions) => {
950
+ try {
951
+ const base = resolveApiBase(cmdOptions);
952
+ const result = await apiRequest(base, `/audit/${jobId}`, {
953
+ method: "GET",
954
+ });
955
+ const format = resolveFormat(cmdOptions);
956
+ const overall = getOverallScore(result.scores);
957
+ const payload = { ...result, overall_score: overall };
958
+ const markdown = formatAuditMarkdown(result);
959
+ const failOnScore = getNumber(cmdOptions, "fail-on-score");
960
+ if (failOnScore !== undefined && overall !== null && overall < failOnScore) {
961
+ process.exitCode = 1;
962
+ }
963
+ printOutput(format, payload, markdown);
964
+ }
965
+ catch (error) {
966
+ process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}\n`);
967
+ process.exit(ExitCode.ERROR);
968
+ }
969
+ });
970
+ // Legacy explain command removed - using new registerExplainCommand
971
+ program
972
+ .command("fix <jobId>")
973
+ .description("Generate a fix patch for an issue")
974
+ .requiredOption("--issue <id>", "Issue ID to fix")
975
+ .requiredOption("--file-content <code>", "Source code content")
976
+ .action(async (jobId, cmdOptions) => {
977
+ try {
978
+ const base = resolveApiBase(cmdOptions);
979
+ const issueId = getString(cmdOptions, "issue");
980
+ if (!issueId)
981
+ throw new Error("--issue is required");
982
+ await runFixCommand(base, jobId, issueId, cmdOptions);
983
+ }
984
+ catch (error) {
985
+ process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}\n`);
986
+ process.exit(ExitCode.ERROR);
987
+ }
988
+ });
989
+ program
990
+ .command("fix-all <jobId>")
991
+ .description("Generate fix patches for all issues in an audit")
992
+ .requiredOption("--file-content <code>", "Source code content")
993
+ .option("--auto-fix-only", "Only process auto-fixable issues")
994
+ .action(async (jobId, cmdOptions) => {
995
+ try {
996
+ const base = resolveApiBase(cmdOptions);
997
+ await runFixAllCommand(base, jobId, cmdOptions);
998
+ }
999
+ catch (error) {
1000
+ process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}\n`);
1001
+ process.exit(ExitCode.ERROR);
1002
+ }
1003
+ });
1004
+ program
1005
+ .command("verify")
1006
+ .description("Verify that a patch fixes an issue")
1007
+ .requiredOption("--search <text>", "Search pattern")
1008
+ .requiredOption("--replace <text>", "Replace pattern")
1009
+ .requiredOption("--issue <id>", "Issue ID")
1010
+ .requiredOption("--url <url>", "Target URL")
1011
+ .requiredOption("--selector <css>", "CSS selector")
1012
+ .option("--timeout <ms>", "Timeout in milliseconds (1-300000)", parseTimeout, 60000)
1013
+ .action(async (cmdOptions) => {
1014
+ try {
1015
+ const base = resolveApiBase(cmdOptions);
1016
+ await runVerifyCommand(base, cmdOptions);
1017
+ }
1018
+ catch (error) {
1019
+ process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}\n`);
1020
+ process.exit(ExitCode.ERROR);
1021
+ }
1022
+ });
1023
+ /**
1024
+ * Interactive menu when no command given in TTY.
1025
+ */
1026
+ async function showInteractiveMenu() {
1027
+ const action = await selectAction("What would you like to do?", [
1028
+ {
1029
+ name: "Run audit",
1030
+ value: "audit",
1031
+ description: "Audit a URL for UX and accessibility issues",
1032
+ },
1033
+ {
1034
+ name: "Initialize project",
1035
+ value: "init",
1036
+ description: "Create .vertaaux.yml configuration",
1037
+ },
1038
+ {
1039
+ name: "Check health",
1040
+ value: "doctor",
1041
+ description: "Run diagnostics on CLI configuration and connectivity",
1042
+ },
1043
+ {
1044
+ name: "View help",
1045
+ value: "help",
1046
+ description: "Show all available commands",
1047
+ },
1048
+ {
1049
+ name: "Exit",
1050
+ value: "exit",
1051
+ },
1052
+ ]);
1053
+ switch (action) {
1054
+ case "audit":
1055
+ // Re-parse with audit command
1056
+ process.argv.push("audit");
1057
+ program.parse();
1058
+ break;
1059
+ case "init":
1060
+ // Re-parse with init command
1061
+ process.argv.push("init");
1062
+ program.parse();
1063
+ break;
1064
+ case "doctor":
1065
+ process.argv.push("doctor");
1066
+ program.parse();
1067
+ break;
1068
+ case "help":
1069
+ program.outputHelp();
1070
+ break;
1071
+ case "exit":
1072
+ process.exit(0);
1073
+ break;
1074
+ }
1075
+ }
1076
+ // Parse and execute
1077
+ // If no command and TTY, show interactive menu
1078
+ const args = process.argv.slice(2);
1079
+ const hasCommand = args.length > 0 && !args[0].startsWith("-");
1080
+ if (!hasCommand && isInteractive()) {
1081
+ // Show banner first
1082
+ showBanner({ version, quiet: false, noBanner: false });
1083
+ showInteractiveMenu().catch((error) => {
1084
+ process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}\n`);
1085
+ process.exit(ExitCode.ERROR);
1086
+ });
1087
+ }
1088
+ else {
1089
+ program.parse();
1090
+ }