@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
@@ -0,0 +1,862 @@
1
+ /**
2
+ * Audit command for VertaaUX CLI.
3
+ *
4
+ * Runs UX and accessibility audits on web pages.
5
+ * Supports various targets, output formats, and CI integration.
6
+ */
7
+ import fs from "fs";
8
+ import path from "path";
9
+ import { resolveConfig } from "../config/loader.js";
10
+ import { ExitCode } from "../utils/exit-codes.js";
11
+ import { parseTimeout, parseInterval, parseConcurrency, parseThreshold, parseMode, parseFailOn, parseGroupBy, parseBudget, validateNumeric, } from "../utils/validators.js";
12
+ import { isTTY } from "../utils/detect-env.js";
13
+ import { resolveApiBase, getApiKey, apiRequest, waitForAudit, } from "../utils/client.js";
14
+ import { createOutput, formatSarif, formatAuditHtml } from "../output/factory.js";
15
+ import { createEnvelope, writeJsonOutput, writeOutput as writeStdout } from "../output/envelope.js";
16
+ import { resolveCommandFormat } from "../output/formats.js";
17
+ import { createSpinner, updateSpinner, succeedSpinner, failSpinner, } from "../ui/spinner.js";
18
+ import { runFixWizard } from "../interactive/fix-wizard.js";
19
+ import { isInteractive } from "../interactive/prompts.js";
20
+ import { evaluateQualityGate, DEFAULT_QUALITY_GATE_CONFIG, } from "../quality-gate/index.js";
21
+ import { loadBaseline } from "../baseline/manager.js";
22
+ import { getChangedRoutes, detectBaseBranch, getBudgetConfig, } from "../ci/index.js";
23
+ import { loadPolicy, resolveBranchPolicy, } from "../policy/index.js";
24
+ import { detectMonorepo, getAuditableApps, generateMatrixConfig, aggregateResults, formatAggregatedResults, } from "../monorepo/index.js";
25
+ import { createLogger } from "../utils/logger.js";
26
+ import { validateBranchName, assertPathContainment } from "../utils/sanitize.js";
27
+ import chalk from "chalk";
28
+ import semver from "semver";
29
+ // Artifact directory
30
+ const ARTIFACTS_DIR = ".vertaaux/artifacts";
31
+ // CLI version for policy version requirements
32
+ const CLI_VERSION = "0.1.0";
33
+ /**
34
+ * Detect current branch from CI environment or git.
35
+ */
36
+ function detectCurrentBranch() {
37
+ // GitHub Actions
38
+ if (process.env.GITHUB_HEAD_REF) {
39
+ return process.env.GITHUB_HEAD_REF;
40
+ }
41
+ if (process.env.GITHUB_REF_NAME) {
42
+ return process.env.GITHUB_REF_NAME;
43
+ }
44
+ // GitLab CI
45
+ if (process.env.CI_COMMIT_REF_NAME) {
46
+ return process.env.CI_COMMIT_REF_NAME;
47
+ }
48
+ // Azure DevOps
49
+ if (process.env.BUILD_SOURCEBRANCHNAME) {
50
+ return process.env.BUILD_SOURCEBRANCHNAME;
51
+ }
52
+ // CircleCI
53
+ if (process.env.CIRCLE_BRANCH) {
54
+ return process.env.CIRCLE_BRANCH;
55
+ }
56
+ // Jenkins
57
+ if (process.env.GIT_BRANCH) {
58
+ // Jenkins often includes origin/, remove it
59
+ return process.env.GIT_BRANCH.replace(/^origin\//, "");
60
+ }
61
+ // Generic CI
62
+ if (process.env.BRANCH_NAME) {
63
+ return process.env.BRANCH_NAME;
64
+ }
65
+ // Default
66
+ return "";
67
+ }
68
+ /**
69
+ * Detect PR labels from CI environment variables.
70
+ *
71
+ * Supports:
72
+ * - GitHub Actions: GITHUB_EVENT_PATH contains event JSON with labels
73
+ * - GitLab CI: CI_MERGE_REQUEST_LABELS is comma-separated
74
+ */
75
+ function detectPRLabels() {
76
+ // GitHub Actions: GITHUB_EVENT_PATH contains event JSON with labels
77
+ if (process.env.GITHUB_EVENT_PATH) {
78
+ try {
79
+ const eventContent = fs.readFileSync(process.env.GITHUB_EVENT_PATH, "utf-8");
80
+ const event = JSON.parse(eventContent);
81
+ const labels = event.pull_request?.labels;
82
+ if (Array.isArray(labels)) {
83
+ return labels.map((l) => l.name || "").filter(Boolean);
84
+ }
85
+ }
86
+ catch {
87
+ // Ignore errors reading event file
88
+ }
89
+ }
90
+ // GitLab CI: CI_MERGE_REQUEST_LABELS is comma-separated
91
+ if (process.env.CI_MERGE_REQUEST_LABELS) {
92
+ return process.env.CI_MERGE_REQUEST_LABELS.split(",")
93
+ .map((l) => l.trim())
94
+ .filter(Boolean);
95
+ }
96
+ return [];
97
+ }
98
+ /**
99
+ * Build quality gate configuration from options and config.
100
+ */
101
+ function buildQualityGateConfig(config, options) {
102
+ // Start with defaults
103
+ const gateConfig = { ...DEFAULT_QUALITY_GATE_CONFIG };
104
+ // Apply config file settings
105
+ const configGate = config.qualityGate;
106
+ if (configGate) {
107
+ if (configGate.failOn)
108
+ gateConfig.failOn = configGate.failOn;
109
+ if (configGate.thresholds) {
110
+ gateConfig.thresholds = { ...gateConfig.thresholds, ...configGate.thresholds };
111
+ }
112
+ if (configGate.maxNew) {
113
+ gateConfig.maxNew = { ...gateConfig.maxNew, ...configGate.maxNew };
114
+ }
115
+ if (configGate.failOnExisting !== undefined) {
116
+ gateConfig.failOnExisting = configGate.failOnExisting;
117
+ }
118
+ if (configGate.bypassLabels) {
119
+ gateConfig.bypassLabels = configGate.bypassLabels;
120
+ }
121
+ }
122
+ // Apply CLI flags (highest precedence)
123
+ if (options.failOn) {
124
+ gateConfig.failOn = options.failOn;
125
+ }
126
+ if (options.threshold !== undefined) {
127
+ gateConfig.thresholds.overall = options.threshold;
128
+ }
129
+ if (options.maxNewErrors !== undefined) {
130
+ gateConfig.maxNew.error = options.maxNewErrors;
131
+ }
132
+ if (options.maxNewWarnings !== undefined) {
133
+ gateConfig.maxNew.warning = options.maxNewWarnings;
134
+ }
135
+ if (options.failOnExisting !== undefined) {
136
+ gateConfig.failOnExisting = options.failOnExisting;
137
+ }
138
+ if (options.bypassLabels) {
139
+ gateConfig.bypassLabels = options.bypassLabels.split(",").map((l) => l.trim());
140
+ }
141
+ return gateConfig;
142
+ }
143
+ /**
144
+ * Normalize issues from various API response formats.
145
+ */
146
+ function normalizeIssues(issues) {
147
+ if (Array.isArray(issues))
148
+ return issues;
149
+ if (issues && typeof issues === "object") {
150
+ const values = Object.values(issues);
151
+ return values.flatMap((value) => Array.isArray(value) ? value : []);
152
+ }
153
+ return [];
154
+ }
155
+ /**
156
+ * Filter issues by severity.
157
+ */
158
+ function filterBySeverity(issues, severityFilter) {
159
+ const allowed = new Set(severityFilter.split(",").map((s) => s.trim().toLowerCase()));
160
+ // Map common aliases
161
+ if (allowed.has("error"))
162
+ allowed.add("critical");
163
+ if (allowed.has("warning"))
164
+ allowed.add("serious");
165
+ return issues.filter((issue) => {
166
+ const sev = issue.severity?.toLowerCase() || "info";
167
+ return allowed.has(sev);
168
+ });
169
+ }
170
+ /**
171
+ * Filter issues by category.
172
+ */
173
+ function filterByCategory(issues, categoryFilter) {
174
+ const allowed = new Set(categoryFilter.split(",").map((c) => c.trim().toLowerCase()));
175
+ return issues.filter((issue) => {
176
+ const cat = issue.category?.toLowerCase() || "";
177
+ return allowed.has(cat) || Array.from(allowed).some((a) => cat.includes(a));
178
+ });
179
+ }
180
+ /**
181
+ * Write output to file. Returns the resolved path if written to file, undefined otherwise.
182
+ */
183
+ function writeOutputToFile(content, outputPath, defaultPath) {
184
+ // Determine the final output path
185
+ const finalPath = outputPath || defaultPath;
186
+ if (finalPath) {
187
+ // Write to file
188
+ const resolvedPath = path.resolve(process.cwd(), finalPath);
189
+ // Ensure directory exists
190
+ const dir = path.dirname(resolvedPath);
191
+ if (!fs.existsSync(dir)) {
192
+ fs.mkdirSync(dir, { recursive: true });
193
+ }
194
+ fs.writeFileSync(resolvedPath, content, "utf-8");
195
+ return resolvedPath;
196
+ }
197
+ return undefined;
198
+ }
199
+ /**
200
+ * Get default output path based on format.
201
+ * Returns undefined for formats that should go to stdout by default.
202
+ */
203
+ function getDefaultOutputPath(format) {
204
+ switch (format) {
205
+ case "html":
206
+ return "vertaaux-report.html";
207
+ default:
208
+ return undefined;
209
+ }
210
+ }
211
+ /**
212
+ * Save repro artifacts from API response.
213
+ */
214
+ function saveReproArtifacts(jobId, response, options, quiet) {
215
+ const hasArtifactOptions = options.saveTrace || options.saveHar || options.screenshots || options.domSnapshots;
216
+ if (!hasArtifactOptions)
217
+ return;
218
+ // Create artifacts directory
219
+ const artifactsPath = path.resolve(process.cwd(), ARTIFACTS_DIR, jobId);
220
+ // Check if API response includes artifact data
221
+ const responseAny = response;
222
+ const artifacts = responseAny.artifacts;
223
+ if (!artifacts) {
224
+ if (!quiet) {
225
+ console.error("Note: Repro artifacts were requested but not available in API response.");
226
+ console.error("Artifact capture may require a 'deep' mode audit or premium plan.");
227
+ }
228
+ return;
229
+ }
230
+ // Ensure directory exists
231
+ if (!fs.existsSync(artifactsPath)) {
232
+ fs.mkdirSync(artifactsPath, { recursive: true });
233
+ }
234
+ const saved = [];
235
+ // Save trace file if available and requested
236
+ if (options.saveTrace && artifacts.trace) {
237
+ const tracePath = path.join(artifactsPath, "trace.zip");
238
+ fs.writeFileSync(tracePath, Buffer.from(artifacts.trace, "base64"));
239
+ saved.push("trace.zip");
240
+ }
241
+ // Save HAR file if available and requested
242
+ if (options.saveHar && artifacts.har) {
243
+ const harPath = path.join(artifactsPath, "network.har");
244
+ fs.writeFileSync(harPath, typeof artifacts.har === "string" ? artifacts.har : JSON.stringify(artifacts.har, null, 2));
245
+ saved.push("network.har");
246
+ }
247
+ // Save screenshots if available and requested
248
+ if (options.screenshots && artifacts.screenshots) {
249
+ const screenshots = artifacts.screenshots;
250
+ for (const screenshot of screenshots) {
251
+ const screenshotName = screenshot.name || "screenshot.png";
252
+ let screenshotPath;
253
+ try {
254
+ screenshotPath = assertPathContainment(screenshotName, artifactsPath);
255
+ }
256
+ catch {
257
+ console.error(`Security: Rejected screenshot "${screenshotName}" -- path traversal outside artifacts directory.`);
258
+ continue;
259
+ }
260
+ fs.writeFileSync(screenshotPath, Buffer.from(screenshot.data, "base64"));
261
+ saved.push(screenshotName);
262
+ }
263
+ }
264
+ // Save DOM snapshots if available and requested
265
+ if (options.domSnapshots && artifacts.domSnapshots) {
266
+ const snapshots = artifacts.domSnapshots;
267
+ for (const snapshot of snapshots) {
268
+ const snapshotName = snapshot.name || "snapshot.html";
269
+ let snapshotPath;
270
+ try {
271
+ snapshotPath = assertPathContainment(snapshotName, artifactsPath);
272
+ }
273
+ catch {
274
+ console.error(`Security: Rejected snapshot "${snapshotName}" -- path traversal outside artifacts directory.`);
275
+ continue;
276
+ }
277
+ fs.writeFileSync(snapshotPath, snapshot.html);
278
+ saved.push(snapshotName);
279
+ }
280
+ }
281
+ if (saved.length > 0 && !quiet) {
282
+ console.error(`Artifacts saved to: ${artifactsPath}`);
283
+ console.error(` - ${saved.join("\n - ")}`);
284
+ }
285
+ }
286
+ /**
287
+ * Count issues by severity level.
288
+ */
289
+ function countIssuesBySeverity(issues) {
290
+ const counts = { error: 0, warning: 0, info: 0 };
291
+ for (const issue of issues) {
292
+ const sev = (issue.severity || "info").toLowerCase();
293
+ if (sev === "error" || sev === "critical") {
294
+ counts.error++;
295
+ }
296
+ else if (sev === "warning" || sev === "serious") {
297
+ counts.warning++;
298
+ }
299
+ else {
300
+ counts.info++;
301
+ }
302
+ }
303
+ return counts;
304
+ }
305
+ /**
306
+ * Save CI artifact bundle for GitHub Actions upload.
307
+ *
308
+ * Creates a complete evidence bundle:
309
+ * - results.json: Full audit results
310
+ * - results.sarif: SARIF for Code Scanning
311
+ * - report.html: HTML report for viewing
312
+ * - manifest.json: Metadata about the bundle
313
+ */
314
+ function saveArtifactBundle(jobId, result, issues, exitCode, quiet) {
315
+ const artifactsPath = path.resolve(process.cwd(), ARTIFACTS_DIR, jobId);
316
+ // Ensure directory exists
317
+ if (!fs.existsSync(artifactsPath)) {
318
+ fs.mkdirSync(artifactsPath, { recursive: true });
319
+ }
320
+ const files = [];
321
+ // 1. Save JSON results
322
+ const jsonPath = path.join(artifactsPath, "results.json");
323
+ fs.writeFileSync(jsonPath, JSON.stringify(result, null, 2), "utf-8");
324
+ files.push("results.json");
325
+ // 2. Save SARIF for Code Scanning
326
+ const sarifContent = formatSarif(result, {
327
+ workingDirectory: process.cwd(),
328
+ });
329
+ const sarifPath = path.join(artifactsPath, "results.sarif");
330
+ fs.writeFileSync(sarifPath, sarifContent, "utf-8");
331
+ files.push("results.sarif");
332
+ // 3. Save HTML report
333
+ const htmlContent = formatAuditHtml(result, {
334
+ interactive: true,
335
+ });
336
+ const htmlPath = path.join(artifactsPath, "report.html");
337
+ fs.writeFileSync(htmlPath, htmlContent, "utf-8");
338
+ files.push("report.html");
339
+ // 4. Save manifest
340
+ const manifest = {
341
+ job_id: jobId,
342
+ timestamp: new Date().toISOString(),
343
+ files,
344
+ audit_url: result.url,
345
+ issue_count: countIssuesBySeverity(issues),
346
+ exit_code: exitCode,
347
+ };
348
+ const manifestPath = path.join(artifactsPath, "manifest.json");
349
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
350
+ files.push("manifest.json");
351
+ if (!quiet) {
352
+ console.error(`Artifacts saved to: ${artifactsPath}`);
353
+ console.error(` - ${files.join("\n - ")}`);
354
+ }
355
+ return artifactsPath;
356
+ }
357
+ /**
358
+ * Execute the audit command.
359
+ */
360
+ async function executeAudit(targetUrl, options, config) {
361
+ // Resolve options with precedence: flags > env > config > defaults
362
+ const mode = options.mode || config.mode || "basic";
363
+ const timeout = options.timeout || config.timeout || 60000;
364
+ const interval = options.interval || config.interval || 5000;
365
+ const wait = options.wait ?? true; // Default to waiting for completion
366
+ const quiet = options.quiet ?? false;
367
+ const interactive = options.interactive ?? false;
368
+ // Interactive mode validation
369
+ if (interactive) {
370
+ if (!wait) {
371
+ throw new Error("--interactive requires --wait (audit must complete before interactive mode)");
372
+ }
373
+ if (!isInteractive()) {
374
+ throw new Error("Interactive mode requires a terminal. Use --format json in CI or piped environments.");
375
+ }
376
+ }
377
+ // Resolve API settings
378
+ const base = resolveApiBase(options.base);
379
+ const apiKey = getApiKey(config.apiKey);
380
+ // Resolve output format using per-command registry
381
+ const machineMode = options.machine || false;
382
+ const explicitFormat = options.format || config.output?.format;
383
+ const validatedFormat = resolveCommandFormat("audit", explicitFormat, machineMode);
384
+ const format = validatedFormat;
385
+ const formatter = createOutput(format);
386
+ const groupBy = options.groupBy || config.output?.groupBy || "severity";
387
+ // Create spinner for progress (only in TTY mode with wait)
388
+ const spinner = wait && isTTY() && !quiet
389
+ ? createSpinner(`Auditing ${targetUrl}...`)
390
+ : null;
391
+ try {
392
+ // Start spinner
393
+ spinner?.start();
394
+ // Create audit job
395
+ const created = await apiRequest(base, "/audit", {
396
+ method: "POST",
397
+ body: { url: targetUrl, mode },
398
+ }, apiKey);
399
+ // If not waiting, just output the job info
400
+ if (!wait) {
401
+ spinner?.stop();
402
+ if (format === "json") {
403
+ if (options.output) {
404
+ const output = JSON.stringify(createEnvelope(created, "audit"), null, 2);
405
+ const filePath = writeOutputToFile(output, options.output, undefined);
406
+ if (filePath && !quiet) {
407
+ console.error(`Report written to: ${filePath}`);
408
+ }
409
+ }
410
+ else {
411
+ writeJsonOutput(created, "audit");
412
+ }
413
+ }
414
+ else {
415
+ const output = formatter.formatResult(created);
416
+ const defaultPath = getDefaultOutputPath(format);
417
+ const filePath = writeOutputToFile(output, options.output, defaultPath);
418
+ if (filePath && !quiet) {
419
+ console.error(`Report written to: ${filePath}`);
420
+ }
421
+ else if (!filePath) {
422
+ writeStdout(output);
423
+ }
424
+ }
425
+ return;
426
+ }
427
+ // Wait for completion with progress updates
428
+ if (!created.job_id) {
429
+ throw new Error("Audit response missing job_id");
430
+ }
431
+ const result = await waitForAudit(base, created.job_id, timeout, interval, apiKey, (progress) => {
432
+ if (spinner) {
433
+ updateSpinner(spinner, `Auditing ${targetUrl}`, progress, 100);
434
+ }
435
+ });
436
+ // Stop spinner with success
437
+ if (spinner) {
438
+ succeedSpinner(spinner, `Audit complete: ${targetUrl}`);
439
+ }
440
+ // Save repro artifacts if requested
441
+ if (created.job_id) {
442
+ saveReproArtifacts(created.job_id, result, options, quiet);
443
+ }
444
+ // Apply filters to issues
445
+ let issues = normalizeIssues(result.issues);
446
+ if (options.severity) {
447
+ issues = filterBySeverity(issues, options.severity);
448
+ }
449
+ if (options.category) {
450
+ issues = filterByCategory(issues, options.category);
451
+ }
452
+ // Create filtered result for output
453
+ const filteredResult = {
454
+ ...result,
455
+ issues,
456
+ };
457
+ // Load and apply policy (CICD-17)
458
+ let resolvedPolicy = null;
459
+ let policyPath = null;
460
+ try {
461
+ const policyResult = await loadPolicy(options.policy ? path.dirname(options.policy) : undefined);
462
+ if (options.policy) {
463
+ // User specified a policy file, load it specifically
464
+ const { loadPolicyFile } = await import("../policy/index.js");
465
+ resolvedPolicy = await loadPolicyFile(options.policy);
466
+ policyPath = options.policy;
467
+ }
468
+ else if (policyResult.path) {
469
+ resolvedPolicy = policyResult.policy;
470
+ policyPath = policyResult.path;
471
+ }
472
+ // If policy found, resolve branch-specific overrides
473
+ if (resolvedPolicy) {
474
+ const currentBranch = detectCurrentBranch();
475
+ if (currentBranch) {
476
+ resolvedPolicy = resolveBranchPolicy(resolvedPolicy, currentBranch);
477
+ }
478
+ if (!quiet && policyPath) {
479
+ console.error(chalk.dim(`Using policy: ${policyPath}`));
480
+ }
481
+ // Check required CLI version
482
+ if (resolvedPolicy.required_version) {
483
+ if (!semver.satisfies(CLI_VERSION, resolvedPolicy.required_version)) {
484
+ console.error(chalk.red(`Policy requires CLI version ${resolvedPolicy.required_version}, ` +
485
+ `but current version is ${CLI_VERSION}`));
486
+ process.exit(ExitCode.ERROR);
487
+ }
488
+ }
489
+ }
490
+ }
491
+ catch (error) {
492
+ // Policy loading errors should be reported but not fatal if no explicit policy specified
493
+ if (options.policy) {
494
+ throw error; // Re-throw if user explicitly specified a policy
495
+ }
496
+ if (!quiet) {
497
+ console.error(chalk.yellow(`Warning: Failed to load policy: ${error instanceof Error ? error.message : String(error)}`));
498
+ }
499
+ }
500
+ // Build quality gate config and evaluate
501
+ const gateConfig = buildQualityGateConfig(config, options);
502
+ // Load baseline if provided
503
+ const baselinePath = options.baseline || config.baseline?.path;
504
+ const baseline = baselinePath ? await loadBaseline(baselinePath) : null;
505
+ // Detect PR labels for bypass check
506
+ const prLabels = detectPRLabels();
507
+ // Evaluate quality gate
508
+ const gateResult = evaluateQualityGate({
509
+ auditResult: {
510
+ issues: issues, // Use the normalized issues array
511
+ scores: result.scores,
512
+ },
513
+ baseline,
514
+ config: gateConfig,
515
+ labels: prLabels,
516
+ });
517
+ // Use quality gate exit code
518
+ const exitCode = gateResult.exitCode;
519
+ // Format and output results
520
+ const formatOptions = {
521
+ human: {
522
+ groupBy,
523
+ showScores: true,
524
+ showSummary: true,
525
+ },
526
+ };
527
+ if (format === "json") {
528
+ if (options.output) {
529
+ const jsonStr = JSON.stringify(createEnvelope(filteredResult, "audit"), null, 2);
530
+ const filePath = writeOutputToFile(jsonStr, options.output, undefined);
531
+ if (filePath && !quiet) {
532
+ console.error(`Report written to: ${filePath}`);
533
+ }
534
+ }
535
+ else {
536
+ writeJsonOutput(filteredResult, "audit");
537
+ }
538
+ }
539
+ else {
540
+ const output = formatter.formatResult(filteredResult, formatOptions);
541
+ const defaultPath = getDefaultOutputPath(format);
542
+ const filePath = writeOutputToFile(output, options.output, defaultPath);
543
+ if (filePath && !quiet) {
544
+ console.error(`Report written to: ${filePath}`);
545
+ }
546
+ else if (!filePath) {
547
+ writeStdout(output);
548
+ }
549
+ }
550
+ // Output quality gate result
551
+ if (!quiet) {
552
+ console.error(""); // Blank line before gate result
553
+ if (gateResult.bypassed) {
554
+ console.error(chalk.yellow(`Quality gate bypassed: ${gateResult.bypassReason}`));
555
+ }
556
+ else if (gateResult.passed) {
557
+ console.error(chalk.green("Quality gate: PASSED"));
558
+ }
559
+ else {
560
+ console.error(chalk.red("Quality gate: FAILED"));
561
+ for (const violation of gateResult.violations) {
562
+ console.error(chalk.red(` - ${violation.message}`));
563
+ }
564
+ }
565
+ // Summary
566
+ console.error("");
567
+ console.error(`New issues: ${gateResult.summary.newIssues.error} errors, ` +
568
+ `${gateResult.summary.newIssues.warning} warnings, ` +
569
+ `${gateResult.summary.newIssues.info} info`);
570
+ if (gateResult.summary.fixedIssues > 0) {
571
+ console.error(chalk.green(`Fixed: ${gateResult.summary.fixedIssues} issues`));
572
+ }
573
+ if (gateResult.summary.existingIssues > 0) {
574
+ console.error(chalk.dim(`Existing (baselined): ${gateResult.summary.existingIssues} issues`));
575
+ }
576
+ }
577
+ // Save CI artifact bundle if requested
578
+ if (options.uploadArtifacts && created.job_id) {
579
+ const artifactJobId = options.jobId || created.job_id;
580
+ saveArtifactBundle(artifactJobId, result, issues, exitCode, quiet);
581
+ }
582
+ // Interactive mode: run fix wizard if there are issues
583
+ if (interactive && issues.length > 0) {
584
+ await runFixWizard(issues, {
585
+ jobId: result.job_id,
586
+ url: targetUrl,
587
+ base: options.base,
588
+ config,
589
+ });
590
+ }
591
+ // Set exit code
592
+ if (exitCode !== ExitCode.SUCCESS) {
593
+ process.exitCode = exitCode;
594
+ }
595
+ }
596
+ catch (error) {
597
+ // Stop spinner with failure
598
+ if (spinner) {
599
+ failSpinner(spinner, `Audit failed: ${error instanceof Error ? error.message : String(error)}`);
600
+ }
601
+ throw error;
602
+ }
603
+ }
604
+ /**
605
+ * Register the audit command with the Commander program.
606
+ */
607
+ export function registerAuditCommand(program) {
608
+ program
609
+ .command("audit [url]")
610
+ .description("Run UX and accessibility audit")
611
+ .option("-u, --url <url>", "URL to audit")
612
+ .option("--repo <repo>", "GitHub repository to audit (owner/repo)")
613
+ .option("--storybook <url>", "Storybook URL to audit")
614
+ .option("--routes <routes>", "Comma-separated list of routes to audit")
615
+ .option("--auth-profile <profile>", "Authentication profile for protected pages")
616
+ .option("--mode <mode>", "Audit depth: basic|standard|deep", parseMode, "basic")
617
+ .option("--format <format>", "Output format: json|sarif|junit|html|human|auto")
618
+ .option("-o, --output <path>", "Output file path")
619
+ .option("--group-by <field>", "Group issues by: severity|category|route", parseGroupBy)
620
+ .option("--wait", "Wait for audit completion (default)")
621
+ .option("--no-wait", "Don't wait for audit completion")
622
+ .option("--severity <levels>", "Filter issues by severity: error|warning|info (comma-separated)")
623
+ .option("--category <categories>", "Filter issues by category (comma-separated)")
624
+ .option("--fail-on <severity>", "Exit 1 if new issues at or above severity: error|warning|info|none", parseFailOn)
625
+ .option("--threshold <score>", "Exit 3 if overall score below threshold (0-100)", parseThreshold)
626
+ .option("--max-new-errors <n>", "Maximum allowed new error-severity issues (default: 0)", (v) => validateNumeric(v, "max-new-errors", { min: 0, integer: true }))
627
+ .option("--max-new-warnings <n>", "Maximum allowed new warning-severity issues (default: unlimited)", (v) => validateNumeric(v, "max-new-warnings", { min: 0, integer: true }))
628
+ .option("--fail-on-existing", "Also fail on existing issues (legacy mode)")
629
+ .option("--bypass-labels <labels>", "Comma-separated PR labels that bypass quality gate")
630
+ .option("--baseline <path>", "Path to baseline file for new issue detection")
631
+ .option("--timeout <ms>", "Wait timeout in milliseconds (1-300000)", parseTimeout)
632
+ .option("--interval <ms>", "Poll interval in milliseconds (1-300000)", parseInterval)
633
+ .option("--interactive", "Step through issues interactively (requires --wait)")
634
+ // Repro artifact options (CLI-17)
635
+ .option("--save-trace", "Save Playwright trace for debugging")
636
+ .option("--save-har", "Save HAR network log")
637
+ .option("--screenshots", "Save page screenshots")
638
+ .option("--dom-snapshots", "Save DOM snapshots")
639
+ // Performance options (CLI-18)
640
+ .option("--concurrency <n>", "Number of concurrent audits (1-50, default: 3)", parseConcurrency)
641
+ .option("--cache", "Enable route caching to speed up repeated audits")
642
+ // CI artifact bundling (CICD-08)
643
+ .option("--upload-artifacts", "Save all outputs to .vertaaux/artifacts/ for CI upload")
644
+ .option("--job-id <id>", "Job identifier for artifact directory naming")
645
+ // Incremental mode (CICD-07)
646
+ .option("--incremental", "Only audit routes changed in PR")
647
+ .option("--base-branch <branch>", "Base branch for comparison (default: auto-detect)")
648
+ // Budget mode (CICD-13)
649
+ .option("--budget <mode>", "Budget mode: quick|standard|full", parseBudget)
650
+ // Monorepo options (CICD-16)
651
+ .option("--workspace <name>", "Audit specific workspace in monorepo")
652
+ .option("--all-workspaces", "Audit all workspaces in monorepo")
653
+ .option("--parallel", "Run workspace audits in parallel")
654
+ .option("--detect-matrix", "Output CI matrix config for monorepo (JSON)")
655
+ // Caching options (CICD-14)
656
+ .option("--no-cache", "Disable route caching")
657
+ .option("--cache-dir <path>", "Custom cache directory")
658
+ // Logging options (CICD-18)
659
+ .option("--json-logs", "Output structured JSON logs for CI")
660
+ // Policy options (CICD-17)
661
+ .option("--policy <file>", "Path to policy file (default: auto-detect vertaa.policy.yml)")
662
+ .action(async (urlArg, cmdOptions, command) => {
663
+ try {
664
+ // Initialize structured logger
665
+ const logger = createLogger({
666
+ json: cmdOptions.jsonLogs || false,
667
+ level: cmdOptions.quiet ? "error" : "info",
668
+ });
669
+ // Load config (supports --config global option)
670
+ const globalOpts = command.optsWithGlobals();
671
+ const config = await resolveConfig(globalOpts.config);
672
+ // Propagate global --machine flag to command options
673
+ cmdOptions.machine = globalOpts.machine || false;
674
+ // Handle monorepo detection and matrix output (CICD-16)
675
+ if (cmdOptions.detectMatrix || cmdOptions.allWorkspaces || cmdOptions.workspace) {
676
+ const monorepo = await detectMonorepo();
677
+ if (monorepo.type === "none") {
678
+ if (cmdOptions.detectMatrix) {
679
+ // Output empty matrix for non-monorepo
680
+ process.stdout.write(JSON.stringify({ include: [] }) + "\n");
681
+ process.exit(ExitCode.SUCCESS);
682
+ }
683
+ // Not a monorepo, continue with normal audit
684
+ }
685
+ else {
686
+ logger.info(`Detected ${monorepo.type} monorepo`, {
687
+ workspaces: monorepo.workspaces.length,
688
+ });
689
+ const apps = getAuditableApps(monorepo);
690
+ logger.info(`Found ${apps.length} auditable apps`, {
691
+ apps: apps.map((a) => a.name),
692
+ });
693
+ // Output matrix config for CI
694
+ if (cmdOptions.detectMatrix) {
695
+ const matrix = generateMatrixConfig(apps);
696
+ process.stdout.write(JSON.stringify(matrix) + "\n");
697
+ process.exit(ExitCode.SUCCESS);
698
+ }
699
+ // Audit specific workspace
700
+ if (cmdOptions.workspace) {
701
+ const workspace = monorepo.workspaces.find((w) => w.name === cmdOptions.workspace);
702
+ if (!workspace) {
703
+ console.error(`Error: Workspace "${cmdOptions.workspace}" not found`);
704
+ console.error(`Available: ${monorepo.workspaces.map((w) => w.name).join(", ")}`);
705
+ process.exit(ExitCode.ERROR);
706
+ }
707
+ // Use workspace URL or infer from type
708
+ if (workspace.url) {
709
+ urlArg = workspace.url;
710
+ logger.info(`Auditing workspace`, { workspace: workspace.name, url: urlArg });
711
+ }
712
+ }
713
+ // Audit all workspaces
714
+ if (cmdOptions.allWorkspaces) {
715
+ console.error(chalk.dim(`Auditing ${apps.length} workspaces in ${monorepo.type} monorepo`));
716
+ const results = [];
717
+ const startTime = Date.now();
718
+ if (cmdOptions.parallel) {
719
+ // Parallel execution
720
+ const promises = apps.map(async (app) => {
721
+ const wsStartTime = Date.now();
722
+ logger.info(`Starting audit`, { workspace: app.name });
723
+ try {
724
+ // Note: In production, this would call executeAudit
725
+ // For now, we return a placeholder result
726
+ return {
727
+ workspace: app,
728
+ auditResult: { url: app.url, issues: [], status: "complete" },
729
+ duration: Date.now() - wsStartTime,
730
+ };
731
+ }
732
+ catch (error) {
733
+ return {
734
+ workspace: app,
735
+ auditResult: null,
736
+ error: error instanceof Error ? error : new Error(String(error)),
737
+ duration: Date.now() - wsStartTime,
738
+ };
739
+ }
740
+ });
741
+ results.push(...(await Promise.all(promises)));
742
+ }
743
+ else {
744
+ // Sequential execution
745
+ for (const app of apps) {
746
+ const wsStartTime = Date.now();
747
+ logger.info(`Auditing workspace`, { workspace: app.name });
748
+ try {
749
+ results.push({
750
+ workspace: app,
751
+ auditResult: { url: app.url, issues: [], status: "complete" },
752
+ duration: Date.now() - wsStartTime,
753
+ });
754
+ }
755
+ catch (error) {
756
+ results.push({
757
+ workspace: app,
758
+ auditResult: null,
759
+ error: error instanceof Error ? error : new Error(String(error)),
760
+ duration: Date.now() - wsStartTime,
761
+ });
762
+ }
763
+ }
764
+ }
765
+ // Aggregate and output results
766
+ const aggregated = aggregateResults(results);
767
+ aggregated.totalDuration = Date.now() - startTime;
768
+ if (cmdOptions.format === "json") {
769
+ writeJsonOutput(aggregated, "audit");
770
+ }
771
+ else {
772
+ writeStdout(formatAggregatedResults(aggregated));
773
+ }
774
+ // Exit with error if any failed
775
+ if (aggregated.failedAudits > 0) {
776
+ process.exit(ExitCode.ERROR);
777
+ }
778
+ if (aggregated.issuesBySeverity.error > 0) {
779
+ process.exit(ExitCode.ISSUES_FOUND);
780
+ }
781
+ process.exit(ExitCode.SUCCESS);
782
+ }
783
+ }
784
+ }
785
+ // Handle incremental mode (CICD-07)
786
+ if (cmdOptions.incremental) {
787
+ const baseBranch = validateBranchName(cmdOptions.baseBranch || detectBaseBranch());
788
+ const changedResult = await getChangedRoutes({
789
+ baseBranch,
790
+ routePatterns: [], // Use default patterns
791
+ });
792
+ if (!changedResult.hasChanges) {
793
+ console.error(chalk.green("No relevant route changes detected. Skipping audit."));
794
+ process.exit(ExitCode.SUCCESS);
795
+ }
796
+ // Log which routes will be audited
797
+ console.error(chalk.dim(`Incremental mode: Auditing ${changedResult.routes.length} changed routes`));
798
+ for (const route of changedResult.routes) {
799
+ console.error(chalk.dim(` - ${route}`));
800
+ }
801
+ // If routes were detected, use them instead of URL argument
802
+ if (!config.defaultUrl) {
803
+ console.error("Error: --incremental requires defaultUrl in config to construct full URLs.");
804
+ process.exit(ExitCode.ERROR);
805
+ }
806
+ // Set routes option for executeAudit
807
+ cmdOptions.routes = changedResult.routes.join(",");
808
+ }
809
+ // Handle budget mode (CICD-13)
810
+ if (cmdOptions.budget) {
811
+ const budgetConfig = getBudgetConfig(cmdOptions.budget);
812
+ // Apply budget constraints to options
813
+ if (!cmdOptions.concurrency) {
814
+ cmdOptions.concurrency = budgetConfig.concurrency;
815
+ }
816
+ if (!cmdOptions.timeout) {
817
+ cmdOptions.timeout = budgetConfig.maxTime;
818
+ }
819
+ console.error(chalk.dim(`Budget mode: ${cmdOptions.budget} (max ${budgetConfig.maxPages} pages, ${budgetConfig.maxTime / 1000}s timeout, ${budgetConfig.concurrency} concurrent)`));
820
+ }
821
+ // Resolve target URL
822
+ // Priority: positional > --url > --repo > --storybook > --routes > config default
823
+ let targetUrl;
824
+ if (urlArg) {
825
+ targetUrl = urlArg;
826
+ }
827
+ else if (cmdOptions.url) {
828
+ targetUrl = cmdOptions.url;
829
+ }
830
+ else if (cmdOptions.repo) {
831
+ // For repo audits, construct a special URL or handle differently
832
+ // For now, we'll just pass it as a marker
833
+ targetUrl = `repo:${cmdOptions.repo}`;
834
+ }
835
+ else if (cmdOptions.storybook) {
836
+ targetUrl = cmdOptions.storybook;
837
+ }
838
+ else if (cmdOptions.routes) {
839
+ // Routes require a base URL from config
840
+ if (!config.defaultUrl) {
841
+ console.error("Error: --routes requires defaultUrl in config or a base URL.");
842
+ process.exit(ExitCode.ERROR);
843
+ }
844
+ // For routes, we'll handle them as comma-separated paths
845
+ const routes = cmdOptions.routes.split(",").map((r) => r.trim());
846
+ targetUrl = `${config.defaultUrl}${routes[0]}`; // First route for now
847
+ }
848
+ else if (config.defaultUrl) {
849
+ targetUrl = config.defaultUrl;
850
+ }
851
+ if (!targetUrl) {
852
+ console.error("Error: URL is required. Provide as argument, --url flag, or defaultUrl in config.");
853
+ process.exit(ExitCode.ERROR);
854
+ }
855
+ await executeAudit(targetUrl, cmdOptions, config);
856
+ }
857
+ catch (error) {
858
+ console.error("Error:", error instanceof Error ? error.message : String(error));
859
+ process.exit(ExitCode.ERROR);
860
+ }
861
+ });
862
+ }