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