@vertaaux/cli 0.4.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (248) hide show
  1. package/CHANGELOG.md +116 -0
  2. package/MIGRATION.md +239 -0
  3. package/README.md +62 -17
  4. package/dist/app/interactive-app.d.ts +103 -0
  5. package/dist/app/interactive-app.d.ts.map +1 -0
  6. package/dist/app/interactive-app.js +328 -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 +166 -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 +415 -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 +46 -0
  37. package/dist/auth/ci-token.d.ts +8 -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 +11 -0
  49. package/dist/commands/a11y.d.ts.map +1 -0
  50. package/dist/commands/a11y.js +149 -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 +589 -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 +129 -0
  69. package/dist/commands/audit/policy.d.ts +27 -0
  70. package/dist/commands/audit/policy.d.ts.map +1 -0
  71. package/dist/commands/audit/policy.js +147 -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 +89 -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 -1097
  81. package/dist/commands/baseline.d.ts +2 -0
  82. package/dist/commands/baseline.d.ts.map +1 -1
  83. package/dist/commands/baseline.js +221 -123
  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 +127 -62
  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 +288 -181
  90. package/dist/commands/diff.d.ts +7 -0
  91. package/dist/commands/diff.d.ts.map +1 -1
  92. package/dist/commands/diff.js +181 -143
  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 +135 -77
  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 +166 -19
  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 +242 -156
  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 +154 -90
  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 +263 -92
  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 +160 -98
  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 +270 -125
  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 +128 -74
  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 +180 -83
  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 +207 -82
  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/config/schema.d.ts +4 -0
  148. package/dist/config/schema.d.ts.map +1 -1
  149. package/dist/index.d.ts +3 -2
  150. package/dist/index.d.ts.map +1 -1
  151. package/dist/index.js +127 -991
  152. package/dist/interactive/fix-wizard.d.ts +3 -0
  153. package/dist/interactive/fix-wizard.d.ts.map +1 -1
  154. package/dist/interactive/fix-wizard.js +130 -112
  155. package/dist/interactive/init-wizard.d.ts +3 -1
  156. package/dist/interactive/init-wizard.d.ts.map +1 -1
  157. package/dist/interactive/init-wizard.js +207 -138
  158. package/dist/interactive/prompts.d.ts +7 -3
  159. package/dist/interactive/prompts.d.ts.map +1 -1
  160. package/dist/interactive/prompts.js +44 -23
  161. package/dist/output/envelope.d.ts +9 -0
  162. package/dist/output/envelope.d.ts.map +1 -1
  163. package/dist/output/envelope.js +37 -3
  164. package/dist/output/factory.d.ts +2 -1
  165. package/dist/output/factory.d.ts.map +1 -1
  166. package/dist/output/html.d.ts +2 -1
  167. package/dist/output/html.d.ts.map +1 -1
  168. package/dist/output/html.js +3 -2
  169. package/dist/output/human.d.ts +2 -1
  170. package/dist/output/human.d.ts.map +1 -1
  171. package/dist/output/human.js +3 -2
  172. package/dist/output/json.d.ts +2 -1
  173. package/dist/output/json.d.ts.map +1 -1
  174. package/dist/output/junit.d.ts +2 -1
  175. package/dist/output/junit.d.ts.map +1 -1
  176. package/dist/output/sarif.d.ts +2 -1
  177. package/dist/output/sarif.d.ts.map +1 -1
  178. package/dist/policy/schema.d.ts +137 -0
  179. package/dist/policy/schema.d.ts.map +1 -1
  180. package/dist/policy/schema.js +107 -0
  181. package/dist/prompts/command-catalog.js +9 -9
  182. package/dist/types.d.ts +74 -0
  183. package/dist/types.d.ts.map +1 -0
  184. package/dist/types.js +5 -0
  185. package/dist/ui/banner.d.ts +34 -0
  186. package/dist/ui/banner.d.ts.map +1 -1
  187. package/dist/ui/banner.js +97 -5
  188. package/dist/ui/diagnostics.d.ts +9 -4
  189. package/dist/ui/diagnostics.d.ts.map +1 -1
  190. package/dist/ui/diagnostics.js +32 -82
  191. package/dist/ui/strings.d.ts +373 -0
  192. package/dist/ui/strings.d.ts.map +1 -0
  193. package/dist/ui/strings.js +499 -0
  194. package/dist/ui/table.d.ts +0 -2
  195. package/dist/ui/table.d.ts.map +1 -1
  196. package/dist/ui/table.js +3 -4
  197. package/dist/utils/api-client.d.ts +46 -0
  198. package/dist/utils/api-client.d.ts.map +1 -0
  199. package/dist/utils/api-client.js +170 -0
  200. package/dist/utils/client.d.ts +29 -18
  201. package/dist/utils/client.d.ts.map +1 -1
  202. package/dist/utils/client.js +104 -12
  203. package/dist/utils/formatters.d.ts +38 -0
  204. package/dist/utils/formatters.d.ts.map +1 -0
  205. package/dist/utils/formatters.js +277 -0
  206. package/dist/utils/root-args.d.ts +12 -0
  207. package/dist/utils/root-args.d.ts.map +1 -0
  208. package/dist/utils/root-args.js +44 -0
  209. package/dist/utils/stdin.d.ts +7 -0
  210. package/dist/utils/stdin.d.ts.map +1 -1
  211. package/dist/utils/stdin.js +32 -2
  212. package/dist/utils/url-classify.d.ts.map +1 -1
  213. package/dist/utils/url-classify.js +24 -3
  214. package/node_modules/@vertaaux/tui/dist/index.cjs +1216 -27
  215. package/node_modules/@vertaaux/tui/dist/index.cjs.map +1 -1
  216. package/node_modules/@vertaaux/tui/dist/index.d.cts +361 -4
  217. package/node_modules/@vertaaux/tui/dist/index.d.ts +361 -4
  218. package/node_modules/@vertaaux/tui/dist/index.js +1189 -27
  219. package/node_modules/@vertaaux/tui/dist/index.js.map +1 -1
  220. package/node_modules/@vertaaux/tui/package.json +2 -3
  221. package/node_modules/chalk/license +9 -0
  222. package/node_modules/chalk/package.json +83 -0
  223. package/node_modules/chalk/readme.md +297 -0
  224. package/node_modules/chalk/source/index.d.ts +325 -0
  225. package/node_modules/chalk/source/index.js +225 -0
  226. package/node_modules/chalk/source/utilities.js +33 -0
  227. package/node_modules/chalk/source/vendor/ansi-styles/index.d.ts +236 -0
  228. package/node_modules/chalk/source/vendor/ansi-styles/index.js +223 -0
  229. package/node_modules/chalk/source/vendor/supports-color/browser.d.ts +1 -0
  230. package/node_modules/chalk/source/vendor/supports-color/browser.js +34 -0
  231. package/node_modules/chalk/source/vendor/supports-color/index.d.ts +55 -0
  232. package/node_modules/chalk/source/vendor/supports-color/index.js +190 -0
  233. package/package.json +20 -5
  234. package/dist/commands/client.d.ts +0 -14
  235. package/dist/commands/client.d.ts.map +0 -1
  236. package/dist/commands/client.js +0 -362
  237. package/dist/commands/drift.d.ts +0 -15
  238. package/dist/commands/drift.d.ts.map +0 -1
  239. package/dist/commands/drift.js +0 -309
  240. package/dist/commands/protect.d.ts +0 -16
  241. package/dist/commands/protect.d.ts.map +0 -1
  242. package/dist/commands/protect.js +0 -323
  243. package/dist/commands/report.d.ts +0 -15
  244. package/dist/commands/report.d.ts.map +0 -1
  245. package/dist/commands/report.js +0 -214
  246. package/dist/policy/sync.d.ts +0 -67
  247. package/dist/policy/sync.d.ts.map +0 -1
  248. package/dist/policy/sync.js +0 -147
@@ -0,0 +1,589 @@
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 { colorize, dim, brand, severity as severityPalette } from "@vertaaux/tui";
8
+ import { resolveConfig } from "../../config/loader.js";
9
+ import { ExitCode } from "../../utils/exit-codes.js";
10
+ import { parseTimeout, parseInterval, parseConcurrency, parseThreshold, parseMode, parseFailOn, parseGroupBy, parseBudget, validateNumeric, } from "../../utils/validators.js";
11
+ import { resolveApiBase, getApiKey, apiRequest, waitForAudit, createClient, } from "../../utils/client.js";
12
+ import { createOutput } from "../../output/factory.js";
13
+ import { resolveCommandFormat } from "../../output/formats.js";
14
+ import { writeOutput } from "../../output/envelope.js";
15
+ import { createRenderer, createKeyboardHandler, runSteps, renderWarning, renderError, isCI, isTTY as tuiIsTTY, phaseIndex, phaseTotal, } from "@vertaaux/tui";
16
+ import { runFixWizard } from "../../interactive/fix-wizard.js";
17
+ import { isInteractive } from "../../interactive/prompts.js";
18
+ import { evaluateQualityGate, } from "../../quality-gate/index.js";
19
+ import { loadBaseline } from "../../baseline/manager.js";
20
+ import { getChangedRoutes, detectBaseBranch, getBudgetConfig, } from "../../ci/index.js";
21
+ import { detectMonorepo, getAuditableApps, generateMatrixConfig, } from "../../monorepo/index.js";
22
+ import { createLogger } from "../../utils/logger.js";
23
+ import { validateBranchName } from "../../utils/sanitize.js";
24
+ import { isLocalUrl } from "../../utils/url-classify.js";
25
+ import { captureLocalPage } from "../../utils/local-capture.js";
26
+ import { strings } from "../../ui/strings.js";
27
+ import { detectPRLabels } from "./ci-detection.js";
28
+ import { normalizeIssues, filterBySeverity, filterByCategory } from "./filters.js";
29
+ import { mapStatusToPhase, getOverallScoreFromResult, extractNumericScores, countTotalIssues } from "./scoring.js";
30
+ import { saveReproArtifacts, saveArtifactBundle } from "./artifacts.js";
31
+ import { outputFireAndForget, outputFormattedResults, outputQualityGateResult } from "./output.js";
32
+ import { loadAndResolvePolicy, buildQualityGateConfig, resolveAuditProfile } from "./policy.js";
33
+ import { runInlineExplanation } from "./explain.js";
34
+ /**
35
+ * Apply filters, evaluate quality gate, and output results after audit completes.
36
+ */
37
+ async function runPostAuditAnalysis(params) {
38
+ const { result, createdJobId, targetUrl, format, formatter, groupBy, options, config, interactive, quiet, profile } = params;
39
+ // Save repro artifacts if requested
40
+ if (createdJobId) {
41
+ saveReproArtifacts(createdJobId, result, options, quiet);
42
+ }
43
+ // Apply filters to issues
44
+ let issues = normalizeIssues(result.issues);
45
+ if (options.severity)
46
+ issues = filterBySeverity(issues, options.severity);
47
+ if (options.category)
48
+ issues = filterByCategory(issues, options.category);
49
+ const filteredResult = { ...result, issues };
50
+ // Load and apply policy (CICD-17)
51
+ await loadAndResolvePolicy(options, quiet);
52
+ // Build quality gate config and evaluate (profile applied between config and CLI flags)
53
+ const gateConfig = buildQualityGateConfig(config, options, profile);
54
+ const baselinePath = options.baseline || config.baseline?.path;
55
+ const baseline = baselinePath ? await loadBaseline(baselinePath) : null;
56
+ const prLabels = detectPRLabels();
57
+ const gateResult = evaluateQualityGate({
58
+ auditResult: {
59
+ issues,
60
+ scores: result.scores,
61
+ },
62
+ baseline,
63
+ config: gateConfig,
64
+ labels: prLabels,
65
+ });
66
+ // Format and output results
67
+ outputFormattedResults(filteredResult, format, formatter, groupBy, options, quiet);
68
+ // Inline AI explanation (--explain flag, PROG-04)
69
+ if (options.explain && issues.length > 0) {
70
+ await runInlineExplanation(issues, result, targetUrl, options, config);
71
+ }
72
+ // Output quality gate result
73
+ if (!quiet)
74
+ outputQualityGateResult(gateResult);
75
+ // Save CI artifact bundle if requested
76
+ if (options.uploadArtifacts && createdJobId) {
77
+ const artifactJobId = options.jobId || createdJobId;
78
+ saveArtifactBundle(artifactJobId, result, issues, gateResult.exitCode, quiet);
79
+ }
80
+ // Interactive mode: run fix wizard if there are issues
81
+ if (interactive && issues.length > 0) {
82
+ await runFixWizard(issues, { jobId: result.job_id, url: targetUrl, base: options.base, config });
83
+ }
84
+ // Set exit code
85
+ if (gateResult.exitCode !== ExitCode.SUCCESS) {
86
+ process.exitCode = gateResult.exitCode;
87
+ }
88
+ }
89
+ /**
90
+ * Execute the audit command — exported as handleAudit for CommandRunnerView.
91
+ */
92
+ export async function handleAudit(rawUrl, options, config) {
93
+ // Normalize URL: add https:// if no protocol specified
94
+ const targetUrl = /^https?:\/\//i.test(rawUrl) ? rawUrl : `https://${rawUrl}`;
95
+ // SECVAL-5: Reject non-http(s) schemes explicitly
96
+ try {
97
+ const parsed = new URL(targetUrl);
98
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
99
+ process.stderr.write(renderError({
100
+ message: `Unsupported URL scheme "${parsed.protocol}" — only http: and https: are allowed`,
101
+ suggestion: "Provide a URL starting with http:// or https://",
102
+ exitCode: ExitCode.ERROR,
103
+ }) + "\n");
104
+ process.exitCode = ExitCode.ERROR;
105
+ return;
106
+ }
107
+ }
108
+ catch {
109
+ process.stderr.write(renderError({
110
+ message: `Invalid URL: "${targetUrl}"`,
111
+ suggestion: "Provide a valid http:// or https:// URL",
112
+ exitCode: ExitCode.ERROR,
113
+ }) + "\n");
114
+ process.exitCode = ExitCode.ERROR;
115
+ return;
116
+ }
117
+ // Resolve audit profile (bundles categories, weights, thresholds, mode)
118
+ const profile = resolveAuditProfile(options.profile || config.profile, config.profiles);
119
+ // Resolve options with precedence: flags > config > profile > defaults
120
+ const mode = options.mode || config.mode || profile?.mode || "basic";
121
+ const timeout = options.timeout || config.timeout || 60000;
122
+ const interval = options.interval || config.interval || 5000;
123
+ const wait = options.wait ?? true; // Default to waiting for completion
124
+ const quiet = options.quiet ?? false;
125
+ const interactive = options.interactive ?? false;
126
+ // Interactive mode validation
127
+ if (interactive) {
128
+ if (!wait) {
129
+ throw new Error("--interactive requires --wait (audit must complete before interactive mode)");
130
+ }
131
+ if (!isInteractive()) {
132
+ throw new Error("Interactive mode requires a terminal. Use --format json in CI or piped environments.");
133
+ }
134
+ }
135
+ // Resolve fail-fast mode: --strict > --continue-on-error > auto-detect
136
+ let failFast;
137
+ if (options.strict && options.continueOnError) {
138
+ writeOutput(renderWarning({ message: "--strict and --continue-on-error both set — --strict takes precedence" }));
139
+ failFast = true;
140
+ }
141
+ else if (options.strict) {
142
+ failFast = true;
143
+ }
144
+ else if (options.continueOnError) {
145
+ failFast = false;
146
+ }
147
+ else {
148
+ failFast = isCI() || !tuiIsTTY();
149
+ }
150
+ // Resolve API settings
151
+ const base = resolveApiBase(options.base);
152
+ const apiKey = getApiKey(config.apiKey);
153
+ const sdkClient = createClient({ base: options.base, apiKey: config.apiKey });
154
+ // Resolve output format using per-command registry
155
+ // --json flag is a shorthand for --format json (convenient for piping)
156
+ const machineMode = options.machine || false;
157
+ const explicitFormat = options.json ? "json" : (options.format || config.output?.format);
158
+ const validatedFormat = resolveCommandFormat("audit", explicitFormat, machineMode);
159
+ const format = validatedFormat;
160
+ const formatter = createOutput(format);
161
+ const groupBy = options.groupBy || config.output?.groupBy || "severity";
162
+ // Keyboard handler for abort
163
+ let keyboard = null;
164
+ let aborted = false;
165
+ // Create renderer for step-list display
166
+ const renderer = createRenderer("auto");
167
+ keyboard = createKeyboardHandler();
168
+ keyboard.on("quit", () => {
169
+ aborted = true;
170
+ renderer.dispose();
171
+ keyboard?.dispose();
172
+ writeOutput("\nAudit aborted by user.\n");
173
+ process.exitCode = ExitCode.ERROR;
174
+ });
175
+ keyboard.start();
176
+ const auditStartTime = Date.now();
177
+ const baseState = {
178
+ phase: strings.audit.run.action,
179
+ phaseIndex: 1,
180
+ phaseTotal: 3,
181
+ url: targetUrl,
182
+ mode,
183
+ progress: {},
184
+ totals: {},
185
+ issueCount: 0,
186
+ scorePreview: null,
187
+ verbose: false,
188
+ elapsed: 0,
189
+ };
190
+ let auditResult = null;
191
+ let createdJobId;
192
+ const steps = [
193
+ {
194
+ id: "fetch",
195
+ actionText: isLocalUrl(targetUrl)
196
+ ? strings.audit.localFetch.action
197
+ : strings.audit.run.action,
198
+ summaryText: isLocalUrl(targetUrl)
199
+ ? strings.audit.localFetch.done()
200
+ : strings.audit.localAnalyze.done(),
201
+ run: async () => {
202
+ if (aborted)
203
+ return;
204
+ if (isLocalUrl(targetUrl)) {
205
+ // Local URL — capture HTML on this machine, send to /analyze
206
+ // Note: informational messages moved to step summaryText to avoid
207
+ // writing to stderr during ComposedRenderer step execution.
208
+ let captured;
209
+ try {
210
+ captured = await captureLocalPage(targetUrl, { timeoutMs: timeout });
211
+ }
212
+ catch (captureErr) {
213
+ throw new Error(`Cannot reach ${targetUrl}. Ensure your local server is running.\n` +
214
+ ` ${captureErr instanceof Error ? captureErr.message : String(captureErr)}`);
215
+ }
216
+ const created = await apiRequest(base, "/analyze", {
217
+ method: "POST",
218
+ body: { html: captured.html, url: targetUrl, mode },
219
+ }, apiKey);
220
+ auditResult = created;
221
+ createdJobId = created.job_id;
222
+ }
223
+ else {
224
+ // Public URL — send to cloud API.
225
+ //
226
+ // When the resolved profile declares a `categories` subset, we bypass
227
+ // `sdkClient.audits.create` and POST directly via `apiRequest`
228
+ // because the published @vertaaux/sdk typings don't yet include the
229
+ // `categories` field (Phase 1.5 wire format). The wire format is
230
+ // identical to the SDK's — same path, same auth — so this is a
231
+ // targeted escape hatch, not a fork. Once the SDK typings are bumped
232
+ // to include `categories?`, both branches collapse back to the SDK.
233
+ const profileCategories = profile?.categories;
234
+ const created = profileCategories && profileCategories.length > 0
235
+ ? await apiRequest(base, "/audit", {
236
+ method: "POST",
237
+ body: {
238
+ url: targetUrl,
239
+ mode: mode,
240
+ categories: profileCategories,
241
+ },
242
+ }, apiKey)
243
+ : await sdkClient.audits.create({
244
+ url: targetUrl,
245
+ mode: mode,
246
+ });
247
+ auditResult = created;
248
+ createdJobId = created.job_id;
249
+ // If not waiting, just output the job info and stop processing.
250
+ // Return early so the "wait" and "analyze" steps are skipped — the
251
+ // quality gate must NOT run for --no-wait jobs since the audit may
252
+ // not be complete yet (or may be from a sync server that returns the
253
+ // completed result immediately).
254
+ if (!wait) {
255
+ renderer.finish({
256
+ url: targetUrl,
257
+ mode,
258
+ overallScore: 0,
259
+ scores: {},
260
+ issueCount: 0,
261
+ passed: true,
262
+ elapsed: Date.now() - auditStartTime,
263
+ });
264
+ keyboard?.dispose();
265
+ outputFireAndForget(created, format, formatter, options, quiet);
266
+ return;
267
+ }
268
+ }
269
+ },
270
+ },
271
+ {
272
+ id: "wait",
273
+ actionText: strings.audit.wait.action,
274
+ summaryText: strings.audit.wait.done(0),
275
+ run: async () => {
276
+ if (aborted || !wait || !auditResult)
277
+ return;
278
+ const created = auditResult;
279
+ if (created.status === "failed") {
280
+ throw new Error(created.error || "Audit failed on server");
281
+ }
282
+ const isSyncResponse = created.status === "completed" && created.scores;
283
+ if (isSyncResponse) {
284
+ // Already done — nothing to wait for
285
+ return;
286
+ }
287
+ if (!created.job_id) {
288
+ throw new Error("Audit response missing job_id");
289
+ }
290
+ const result = await waitForAudit(sdkClient, created.job_id, timeout, interval, (progress, status) => {
291
+ if (aborted)
292
+ return;
293
+ const phase = mapStatusToPhase(status);
294
+ renderer.update({
295
+ ...baseState,
296
+ phase,
297
+ phaseIndex: phaseIndex(phase),
298
+ phaseTotal: phaseTotal(),
299
+ progress: { audit: progress },
300
+ totals: { audit: 100 },
301
+ elapsed: Date.now() - auditStartTime,
302
+ });
303
+ });
304
+ auditResult = result;
305
+ },
306
+ },
307
+ {
308
+ id: "analyze",
309
+ actionText: strings.audit.localAnalyze.action,
310
+ summaryText: strings.audit.run.done(0, targetUrl),
311
+ run: async () => {
312
+ // No-op: analysis output is written AFTER renderer.finish()
313
+ // to avoid breaking ComposedRenderer's cursor arithmetic.
314
+ if (aborted || !auditResult || !wait)
315
+ return;
316
+ },
317
+ },
318
+ ];
319
+ const { success, states } = await runSteps(steps, {
320
+ failFast,
321
+ onStateChange: (stepStates) => {
322
+ renderer.update({
323
+ ...baseState,
324
+ stepStates,
325
+ elapsed: Date.now() - auditStartTime,
326
+ });
327
+ },
328
+ });
329
+ if (success && auditResult) {
330
+ const resolvedResult = auditResult;
331
+ const overallScore = getOverallScoreFromResult(resolvedResult);
332
+ renderer.finish({
333
+ url: targetUrl,
334
+ mode,
335
+ overallScore: overallScore ?? 0,
336
+ scores: extractNumericScores(resolvedResult.scores),
337
+ issueCount: countTotalIssues(resolvedResult.issues),
338
+ passed: (overallScore ?? 0) >= 70,
339
+ elapsed: Date.now() - auditStartTime,
340
+ });
341
+ }
342
+ else {
343
+ renderer.finish({
344
+ url: targetUrl,
345
+ mode,
346
+ overallScore: 0,
347
+ scores: {},
348
+ issueCount: 0,
349
+ passed: false,
350
+ elapsed: Date.now() - auditStartTime,
351
+ });
352
+ }
353
+ keyboard?.dispose();
354
+ if (!success) {
355
+ const failedStep = states.find((s) => s.status === "failed");
356
+ const reason = failedStep?.failReason || "(no reason captured)";
357
+ process.stderr.write(renderError({
358
+ message: `Audit failed — ${reason}`,
359
+ suggestion: "vertaa doctor",
360
+ }) + "\n");
361
+ process.exitCode = ExitCode.ERROR;
362
+ return;
363
+ }
364
+ // Run post-audit analysis AFTER renderer.finish() so output
365
+ // doesn't break ComposedRenderer's cursor arithmetic
366
+ if (auditResult && wait && !aborted) {
367
+ await runPostAuditAnalysis({
368
+ result: auditResult,
369
+ createdJobId,
370
+ targetUrl,
371
+ format,
372
+ formatter,
373
+ groupBy,
374
+ options,
375
+ config,
376
+ interactive,
377
+ quiet,
378
+ profile,
379
+ });
380
+ }
381
+ }
382
+ /**
383
+ * Register the audit command with the Commander program.
384
+ */
385
+ export function registerAuditCommand(program) {
386
+ program
387
+ .command("audit [url]")
388
+ .description("Run UX and accessibility audit. Localhost and private URLs are " +
389
+ "captured locally and analyzed via static HTML analysis.")
390
+ .option("-u, --url <url>", "URL to audit")
391
+ .option("--repo <repo>", "GitHub repository to audit (owner/repo)")
392
+ .option("--storybook <url>", "Storybook URL to audit")
393
+ .option("--routes <routes>", "Comma-separated list of routes to audit")
394
+ .option("--auth-profile <profile>", "Authentication profile for protected pages")
395
+ .option("--mode <mode>", "Audit depth: basic|standard|deep", parseMode, "basic")
396
+ .option("--format <format>", "Output format: json|sarif|junit|html|human (default: human in terminal, auto-detected in CI)")
397
+ .option("--json", "Shorthand for --format json (convenient for piping)")
398
+ .option("-o, --output <path>", "Output file path")
399
+ .option("--group-by <field>", "Group issues by: severity|category|route", parseGroupBy)
400
+ .option("--wait", "Wait for audit completion (default)")
401
+ .option("--no-wait", "Don't wait for audit completion")
402
+ .option("--severity <levels>", "Filter issues by severity: error|warning|info (comma-separated)")
403
+ .option("--category <categories>", "Filter issues by category (comma-separated)")
404
+ .option("--fail-on <severity>", "Exit 1 if new issues at or above severity: error|warning|info|none", parseFailOn)
405
+ .option("--threshold <score>", "Exit 3 if overall score below threshold (0-100)", parseThreshold)
406
+ .option("--max-new-errors <n>", "Maximum allowed new error-severity issues (default: 0)", (v) => validateNumeric(v, "max-new-errors", { min: 0, integer: true }))
407
+ .option("--max-new-warnings <n>", "Maximum allowed new warning-severity issues (default: unlimited)", (v) => validateNumeric(v, "max-new-warnings", { min: 0, integer: true }))
408
+ .option("--fail-on-existing", "Also fail on existing issues (legacy mode)")
409
+ .option("--bypass-labels <labels>", "Comma-separated PR labels that bypass quality gate")
410
+ .option("--baseline <path>", "Path to baseline file for new issue detection")
411
+ .option("--timeout <ms>", "Wait timeout in milliseconds (1-300000)", parseTimeout)
412
+ .option("--interval <ms>", "Poll interval in milliseconds (1-300000)", parseInterval)
413
+ .option("--interactive", "Step through issues interactively (requires --wait)")
414
+ // Repro artifact options (CLI-17)
415
+ .option("--save-trace", "Save Playwright trace for debugging")
416
+ .option("--save-har", "Save HAR network log")
417
+ .option("--screenshots", "Save page screenshots")
418
+ .option("--dom-snapshots", "Save DOM snapshots")
419
+ // Performance options (CLI-18)
420
+ .option("--concurrency <n>", "Number of concurrent audits (1-50, default: 3)", parseConcurrency)
421
+ .option("--cache", "Enable route caching to speed up repeated audits")
422
+ // CI artifact bundling (CICD-08)
423
+ .option("--upload-artifacts", "Save all outputs to .vertaaux/artifacts/ for CI upload")
424
+ .option("--job-id <id>", "Job identifier for artifact directory naming")
425
+ // Incremental mode (CICD-07)
426
+ .option("--incremental", "Only audit routes changed in PR")
427
+ .option("--base-branch <branch>", "Base branch for comparison (default: auto-detect)")
428
+ // Budget mode (CICD-13)
429
+ .option("--budget <mode>", "Budget mode: quick|standard|full", parseBudget)
430
+ // Monorepo options (CICD-16)
431
+ .option("--workspace <name>", "Audit specific workspace in monorepo")
432
+ .option("--all-workspaces", "Audit all workspaces in monorepo")
433
+ .option("--parallel", "Run workspace audits in parallel")
434
+ .option("--detect-matrix", "Output CI matrix config for monorepo (JSON)")
435
+ // Caching options (CICD-14)
436
+ .option("--no-cache", "Disable route caching")
437
+ .option("--cache-dir <path>", "Custom cache directory")
438
+ // Logging options (CICD-18)
439
+ .option("--json-logs", "Output structured JSON logs for CI")
440
+ // Policy options (CICD-17)
441
+ .option("--policy <file>", "Path to policy file (default: auto-detect vertaa.policy.yml)")
442
+ .option("--profile <name>", "Audit profile: wcag-aa|conversion-focus|quick-ux|ci-gate|compliance")
443
+ .option("--explain", "Append AI explanation to audit results")
444
+ .option("--strict", "Fail immediately on first step error")
445
+ .option("--continue-on-error", "Continue on step errors even in CI")
446
+ .action(async (urlArg, cmdOptions, command) => {
447
+ try {
448
+ // Initialize structured logger
449
+ const logger = createLogger({
450
+ json: cmdOptions.jsonLogs || false,
451
+ level: cmdOptions.quiet ? "error" : "info",
452
+ });
453
+ // Load config (supports --config global option)
454
+ const globalOpts = command.optsWithGlobals();
455
+ const config = await resolveConfig(globalOpts.config);
456
+ // Propagate global --machine and --base flags to command options
457
+ cmdOptions.machine = globalOpts.machine || false;
458
+ cmdOptions.base = globalOpts.base || cmdOptions.base;
459
+ // Handle monorepo detection and matrix output (CICD-16)
460
+ if (cmdOptions.detectMatrix || cmdOptions.allWorkspaces || cmdOptions.workspace) {
461
+ const monorepo = await detectMonorepo();
462
+ if (monorepo.type === "none") {
463
+ if (cmdOptions.detectMatrix) {
464
+ // Output empty matrix for non-monorepo
465
+ process.stdout.write(JSON.stringify({ include: [] }) + "\n");
466
+ process.exit(ExitCode.SUCCESS);
467
+ }
468
+ // Not a monorepo, continue with normal audit
469
+ }
470
+ else {
471
+ logger.info(`Detected ${monorepo.type} monorepo`, {
472
+ workspaces: monorepo.workspaces.length,
473
+ });
474
+ const apps = getAuditableApps(monorepo);
475
+ logger.info(`Found ${apps.length} auditable apps`, {
476
+ apps: apps.map((a) => a.name),
477
+ });
478
+ // Output matrix config for CI
479
+ if (cmdOptions.detectMatrix) {
480
+ const matrix = generateMatrixConfig(apps);
481
+ process.stdout.write(JSON.stringify(matrix) + "\n");
482
+ process.exit(ExitCode.SUCCESS);
483
+ }
484
+ // Audit specific workspace
485
+ if (cmdOptions.workspace) {
486
+ const workspace = monorepo.workspaces.find((w) => w.name === cmdOptions.workspace);
487
+ if (!workspace) {
488
+ writeOutput(`Error: Workspace "${cmdOptions.workspace}" not found\nAvailable: ${monorepo.workspaces.map((w) => w.name).join(", ")}\n`);
489
+ process.exit(ExitCode.ERROR);
490
+ }
491
+ // Use workspace URL or infer from type
492
+ if (workspace.url) {
493
+ urlArg = workspace.url;
494
+ logger.info(`Auditing workspace`, { workspace: workspace.name, url: urlArg });
495
+ }
496
+ }
497
+ // Audit all workspaces — not yet implemented (placeholder removed to prevent false-green CI results)
498
+ if (cmdOptions.allWorkspaces) {
499
+ // TODO: Implement real workspace audit execution (iterate apps, call executeAudit per workspace)
500
+ writeOutput(colorize(strings.audit.errors.allWorkspacesNotImplemented, severityPalette.warning) +
501
+ "\n" +
502
+ apps.map((app) => dim(` vertaa audit ${app.url || app.name}`)).join("\n") +
503
+ "\n");
504
+ process.exit(ExitCode.ERROR);
505
+ }
506
+ }
507
+ }
508
+ // Handle incremental mode (CICD-07)
509
+ if (cmdOptions.incremental) {
510
+ const baseBranch = validateBranchName(cmdOptions.baseBranch || detectBaseBranch());
511
+ const changedResult = await getChangedRoutes({
512
+ baseBranch,
513
+ routePatterns: [], // Use default patterns
514
+ });
515
+ if (!changedResult.hasChanges) {
516
+ writeOutput(colorize(strings.audit.noRouteChanges, brand.lime) + "\n");
517
+ process.exit(ExitCode.SUCCESS);
518
+ }
519
+ // Log which routes will be audited
520
+ writeOutput(dim(`Incremental mode: Auditing ${changedResult.routes.length} changed routes`) + "\n");
521
+ for (const route of changedResult.routes) {
522
+ writeOutput(dim(` - ${route}`) + "\n");
523
+ }
524
+ // If routes were detected, use them instead of URL argument
525
+ if (!config.defaultUrl) {
526
+ writeOutput("Error: --incremental requires defaultUrl in config to construct full URLs.\n");
527
+ process.exit(ExitCode.ERROR);
528
+ }
529
+ // Set routes option for executeAudit
530
+ cmdOptions.routes = changedResult.routes.join(",");
531
+ }
532
+ // Handle budget mode (CICD-13)
533
+ if (cmdOptions.budget) {
534
+ const budgetConfig = getBudgetConfig(cmdOptions.budget);
535
+ // Apply budget constraints to options
536
+ if (!cmdOptions.concurrency) {
537
+ cmdOptions.concurrency = budgetConfig.concurrency;
538
+ }
539
+ if (!cmdOptions.timeout) {
540
+ cmdOptions.timeout = budgetConfig.maxTime;
541
+ }
542
+ writeOutput(dim(`Budget mode: ${cmdOptions.budget} (max ${budgetConfig.maxPages} pages, ${budgetConfig.maxTime / 1000}s timeout, ${budgetConfig.concurrency} concurrent)`) + "\n");
543
+ }
544
+ // Resolve target URL
545
+ // Priority: positional > --url > --repo > --storybook > --routes > config default
546
+ let targetUrl;
547
+ if (urlArg) {
548
+ targetUrl = urlArg;
549
+ }
550
+ else if (cmdOptions.url) {
551
+ targetUrl = cmdOptions.url;
552
+ }
553
+ else if (cmdOptions.repo) {
554
+ // For repo audits, construct a special URL or handle differently
555
+ // For now, we'll just pass it as a marker
556
+ targetUrl = `repo:${cmdOptions.repo}`;
557
+ }
558
+ else if (cmdOptions.storybook) {
559
+ targetUrl = cmdOptions.storybook;
560
+ }
561
+ else if (cmdOptions.routes) {
562
+ // Routes require a base URL from config
563
+ if (!config.defaultUrl) {
564
+ writeOutput("Error: --routes requires defaultUrl in config or a base URL.\n");
565
+ process.exit(ExitCode.ERROR);
566
+ }
567
+ // For routes, we'll handle them as comma-separated paths
568
+ const routes = cmdOptions.routes.split(",").map((r) => r.trim());
569
+ targetUrl = `${config.defaultUrl}${routes[0]}`; // First route for now
570
+ }
571
+ else if (config.defaultUrl) {
572
+ targetUrl = config.defaultUrl;
573
+ }
574
+ if (!targetUrl) {
575
+ writeOutput("Error: URL is required. Provide as argument, --url flag, or defaultUrl in config.\n");
576
+ process.exit(ExitCode.ERROR);
577
+ }
578
+ await handleAudit(targetUrl, cmdOptions, config);
579
+ }
580
+ catch (error) {
581
+ process.stderr.write(renderError({
582
+ message: error instanceof Error ? error.message : String(error),
583
+ suggestion: "vertaa doctor",
584
+ exitCode: ExitCode.ERROR,
585
+ }) + "\n");
586
+ process.exit(ExitCode.ERROR);
587
+ }
588
+ });
589
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Output formatting helpers for the audit command.
3
+ */
4
+ import type { AuditResponse } from "../../utils/client.js";
5
+ import { createOutput } from "../../output/factory.js";
6
+ import { evaluateQualityGate } from "../../quality-gate/index.js";
7
+ import type { OutputFormat } from "../../utils/detect-env.js";
8
+ import type { IssueLike, AuditCommandOptions } from "./types.js";
9
+ /**
10
+ * Write output to file. Returns the resolved path if written to file, undefined otherwise.
11
+ */
12
+ export declare function writeOutputToFile(content: string, outputPath: string | undefined, defaultPath?: string): string | undefined;
13
+ /**
14
+ * Get default output path based on format.
15
+ * Returns undefined for formats that should go to stdout by default.
16
+ */
17
+ export declare function getDefaultOutputPath(format: OutputFormat): string | undefined;
18
+ /**
19
+ * Output results in fire-and-forget mode (--no-wait).
20
+ */
21
+ export declare function outputFireAndForget(created: AuditResponse, format: OutputFormat, formatter: ReturnType<typeof createOutput>, options: AuditCommandOptions, quiet: boolean): void;
22
+ /**
23
+ * Print quality gate result to stderr.
24
+ */
25
+ export declare function outputQualityGateResult(gateResult: ReturnType<typeof evaluateQualityGate>): void;
26
+ /**
27
+ * Write formatted audit results to output or stdout.
28
+ */
29
+ export declare function outputFormattedResults(filteredResult: Omit<AuditResponse, "issues"> & {
30
+ issues: IssueLike[];
31
+ }, format: OutputFormat, formatter: ReturnType<typeof createOutput>, groupBy: "severity" | "category" | "route", options: AuditCommandOptions, quiet: boolean): void;
32
+ //# sourceMappingURL=output.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"output.d.ts","sourceRoot":"","sources":["../../../src/commands/audit/output.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAAE,YAAY,EAAsB,MAAM,yBAAyB,CAAC;AAE3E,OAAO,EACL,mBAAmB,EACpB,MAAM,6BAA6B,CAAC;AACrC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAC9D,OAAO,KAAK,EAAE,SAAS,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAGjE;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,WAAW,CAAC,EAAE,MAAM,GACnB,MAAM,GAAG,SAAS,CAgBpB;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,GAAG,SAAS,CAO7E;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,aAAa,EACtB,MAAM,EAAE,YAAY,EACpB,SAAS,EAAE,UAAU,CAAC,OAAO,YAAY,CAAC,EAC1C,OAAO,EAAE,mBAAmB,EAC5B,KAAK,EAAE,OAAO,GACb,IAAI,CAqBN;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CACrC,UAAU,EAAE,UAAU,CAAC,OAAO,mBAAmB,CAAC,GACjD,IAAI,CAyBN;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CACpC,cAAc,EAAE,IAAI,CAAC,aAAa,EAAE,QAAQ,CAAC,GAAG;IAAE,MAAM,EAAE,SAAS,EAAE,CAAA;CAAE,EACvE,MAAM,EAAE,YAAY,EACpB,SAAS,EAAE,UAAU,CAAC,OAAO,YAAY,CAAC,EAC1C,OAAO,EAAE,UAAU,GAAG,UAAU,GAAG,OAAO,EAC1C,OAAO,EAAE,mBAAmB,EAC5B,KAAK,EAAE,OAAO,GACb,IAAI,CA6BN"}