@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
@@ -0,0 +1,18 @@
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 { Command } from "commander";
8
+ import type { VertaauxConfig } from "../../config/schema.js";
9
+ import type { AuditCommandOptions } from "./types.js";
10
+ /**
11
+ * Execute the audit command — exported as handleAudit for CommandRunnerView.
12
+ */
13
+ export declare function handleAudit(rawUrl: string, options: AuditCommandOptions, config: VertaauxConfig): Promise<void>;
14
+ /**
15
+ * Register the audit command with the Commander program.
16
+ */
17
+ export declare function registerAuditCommand(program: Command): void;
18
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/audit/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AA+D7D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAuFtD;;GAEG;AACH,wBAAsB,WAAW,CAC/B,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,mBAAmB,EAC5B,MAAM,EAAE,cAAc,GACrB,OAAO,CAAC,IAAI,CAAC,CA6Sf;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAyP3D"}
@@ -0,0 +1,564 @@
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 } 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 } = 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
53
+ const gateConfig = buildQualityGateConfig(config, options);
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 options with precedence: flags > env > config > defaults
118
+ const mode = options.mode || config.mode || "basic";
119
+ const timeout = options.timeout || config.timeout || 60000;
120
+ const interval = options.interval || config.interval || 5000;
121
+ const wait = options.wait ?? true; // Default to waiting for completion
122
+ const quiet = options.quiet ?? false;
123
+ const interactive = options.interactive ?? false;
124
+ // Interactive mode validation
125
+ if (interactive) {
126
+ if (!wait) {
127
+ throw new Error("--interactive requires --wait (audit must complete before interactive mode)");
128
+ }
129
+ if (!isInteractive()) {
130
+ throw new Error("Interactive mode requires a terminal. Use --format json in CI or piped environments.");
131
+ }
132
+ }
133
+ // Resolve fail-fast mode: --strict > --continue-on-error > auto-detect
134
+ let failFast;
135
+ if (options.strict && options.continueOnError) {
136
+ writeOutput(renderWarning({ message: "--strict and --continue-on-error both set — --strict takes precedence" }));
137
+ failFast = true;
138
+ }
139
+ else if (options.strict) {
140
+ failFast = true;
141
+ }
142
+ else if (options.continueOnError) {
143
+ failFast = false;
144
+ }
145
+ else {
146
+ failFast = isCI() || !tuiIsTTY();
147
+ }
148
+ // Resolve API settings
149
+ const base = resolveApiBase(options.base);
150
+ const apiKey = getApiKey(config.apiKey);
151
+ const sdkClient = createClient({ base: options.base, apiKey: config.apiKey });
152
+ // Resolve output format using per-command registry
153
+ // --json flag is a shorthand for --format json (convenient for piping)
154
+ const machineMode = options.machine || false;
155
+ const explicitFormat = options.json ? "json" : (options.format || config.output?.format);
156
+ const validatedFormat = resolveCommandFormat("audit", explicitFormat, machineMode);
157
+ const format = validatedFormat;
158
+ const formatter = createOutput(format);
159
+ const groupBy = options.groupBy || config.output?.groupBy || "severity";
160
+ // Keyboard handler for abort
161
+ let keyboard = null;
162
+ let aborted = false;
163
+ // Create renderer for step-list display
164
+ const renderer = createRenderer("auto");
165
+ keyboard = createKeyboardHandler();
166
+ keyboard.on("quit", () => {
167
+ aborted = true;
168
+ renderer.dispose();
169
+ keyboard?.dispose();
170
+ writeOutput("\nAudit aborted by user.\n");
171
+ process.exitCode = ExitCode.ERROR;
172
+ });
173
+ keyboard.start();
174
+ const auditStartTime = Date.now();
175
+ const baseState = {
176
+ phase: strings.audit.run.action,
177
+ phaseIndex: 1,
178
+ phaseTotal: 3,
179
+ url: targetUrl,
180
+ mode,
181
+ progress: {},
182
+ totals: {},
183
+ issueCount: 0,
184
+ scorePreview: null,
185
+ verbose: false,
186
+ elapsed: 0,
187
+ };
188
+ let auditResult = null;
189
+ let createdJobId;
190
+ const steps = [
191
+ {
192
+ id: "fetch",
193
+ actionText: isLocalUrl(targetUrl)
194
+ ? strings.audit.localFetch.action
195
+ : strings.audit.run.action,
196
+ summaryText: isLocalUrl(targetUrl)
197
+ ? strings.audit.localFetch.done()
198
+ : strings.audit.localAnalyze.done(),
199
+ run: async () => {
200
+ if (aborted)
201
+ return;
202
+ if (isLocalUrl(targetUrl)) {
203
+ // Local URL — capture HTML on this machine, send to /analyze
204
+ // Note: informational messages moved to step summaryText to avoid
205
+ // writing to stderr during ComposedRenderer step execution.
206
+ let captured;
207
+ try {
208
+ captured = await captureLocalPage(targetUrl, { timeoutMs: timeout });
209
+ }
210
+ catch (captureErr) {
211
+ throw new Error(`Cannot reach ${targetUrl}. Ensure your local server is running.\n` +
212
+ ` ${captureErr instanceof Error ? captureErr.message : String(captureErr)}`);
213
+ }
214
+ const created = await apiRequest(base, "/analyze", {
215
+ method: "POST",
216
+ body: { html: captured.html, url: targetUrl, mode },
217
+ }, apiKey);
218
+ auditResult = created;
219
+ createdJobId = created.job_id;
220
+ }
221
+ else {
222
+ // Public URL — send to cloud API
223
+ const created = await sdkClient.audits.create({ url: targetUrl, mode: mode });
224
+ auditResult = created;
225
+ createdJobId = created.job_id;
226
+ // If not waiting, just output the job info and stop processing.
227
+ // Return early so the "wait" and "analyze" steps are skipped — the
228
+ // quality gate must NOT run for --no-wait jobs since the audit may
229
+ // not be complete yet (or may be from a sync server that returns the
230
+ // completed result immediately).
231
+ if (!wait) {
232
+ renderer.finish({
233
+ url: targetUrl,
234
+ mode,
235
+ overallScore: 0,
236
+ scores: {},
237
+ issueCount: 0,
238
+ passed: true,
239
+ elapsed: Date.now() - auditStartTime,
240
+ });
241
+ keyboard?.dispose();
242
+ outputFireAndForget(created, format, formatter, options, quiet);
243
+ return;
244
+ }
245
+ }
246
+ },
247
+ },
248
+ {
249
+ id: "wait",
250
+ actionText: strings.audit.wait.action,
251
+ summaryText: strings.audit.wait.done(0),
252
+ run: async () => {
253
+ if (aborted || !wait || !auditResult)
254
+ return;
255
+ const created = auditResult;
256
+ if (created.status === "failed") {
257
+ throw new Error(created.error || "Audit failed on server");
258
+ }
259
+ const isSyncResponse = created.status === "completed" && created.scores;
260
+ if (isSyncResponse) {
261
+ // Already done — nothing to wait for
262
+ return;
263
+ }
264
+ if (!created.job_id) {
265
+ throw new Error("Audit response missing job_id");
266
+ }
267
+ const result = await waitForAudit(sdkClient, created.job_id, timeout, interval, (progress, status) => {
268
+ if (aborted)
269
+ return;
270
+ const phase = mapStatusToPhase(status);
271
+ renderer.update({
272
+ ...baseState,
273
+ phase,
274
+ phaseIndex: phaseIndex(phase),
275
+ phaseTotal: phaseTotal(),
276
+ progress: { audit: progress },
277
+ totals: { audit: 100 },
278
+ elapsed: Date.now() - auditStartTime,
279
+ });
280
+ });
281
+ auditResult = result;
282
+ },
283
+ },
284
+ {
285
+ id: "analyze",
286
+ actionText: strings.audit.localAnalyze.action,
287
+ summaryText: strings.audit.run.done(0, targetUrl),
288
+ run: async () => {
289
+ // No-op: analysis output is written AFTER renderer.finish()
290
+ // to avoid breaking ComposedRenderer's cursor arithmetic.
291
+ if (aborted || !auditResult || !wait)
292
+ return;
293
+ },
294
+ },
295
+ ];
296
+ const { success, states } = await runSteps(steps, {
297
+ failFast,
298
+ onStateChange: (stepStates) => {
299
+ renderer.update({
300
+ ...baseState,
301
+ stepStates,
302
+ elapsed: Date.now() - auditStartTime,
303
+ });
304
+ },
305
+ });
306
+ if (success && auditResult) {
307
+ const resolvedResult = auditResult;
308
+ const overallScore = getOverallScoreFromResult(resolvedResult);
309
+ renderer.finish({
310
+ url: targetUrl,
311
+ mode,
312
+ overallScore: overallScore ?? 0,
313
+ scores: extractNumericScores(resolvedResult.scores),
314
+ issueCount: countTotalIssues(resolvedResult.issues),
315
+ passed: (overallScore ?? 0) >= 70,
316
+ elapsed: Date.now() - auditStartTime,
317
+ });
318
+ }
319
+ else {
320
+ renderer.finish({
321
+ url: targetUrl,
322
+ mode,
323
+ overallScore: 0,
324
+ scores: {},
325
+ issueCount: 0,
326
+ passed: false,
327
+ elapsed: Date.now() - auditStartTime,
328
+ });
329
+ }
330
+ keyboard?.dispose();
331
+ if (!success) {
332
+ const failedStep = states.find((s) => s.status === "failed");
333
+ const reason = failedStep?.failReason || "(no reason captured)";
334
+ process.stderr.write(renderError({
335
+ message: `Audit failed — ${reason}`,
336
+ suggestion: "vertaa doctor",
337
+ }) + "\n");
338
+ process.exitCode = ExitCode.ERROR;
339
+ return;
340
+ }
341
+ // Run post-audit analysis AFTER renderer.finish() so output
342
+ // doesn't break ComposedRenderer's cursor arithmetic
343
+ if (auditResult && wait && !aborted) {
344
+ await runPostAuditAnalysis({
345
+ result: auditResult,
346
+ createdJobId,
347
+ targetUrl,
348
+ format,
349
+ formatter,
350
+ groupBy,
351
+ options,
352
+ config,
353
+ interactive,
354
+ quiet,
355
+ });
356
+ }
357
+ }
358
+ /**
359
+ * Register the audit command with the Commander program.
360
+ */
361
+ export function registerAuditCommand(program) {
362
+ program
363
+ .command("audit [url]")
364
+ .description("Run UX and accessibility audit. Localhost and private URLs are " +
365
+ "captured locally and analyzed via static HTML analysis.")
366
+ .option("-u, --url <url>", "URL to audit")
367
+ .option("--repo <repo>", "GitHub repository to audit (owner/repo)")
368
+ .option("--storybook <url>", "Storybook URL to audit")
369
+ .option("--routes <routes>", "Comma-separated list of routes to audit")
370
+ .option("--auth-profile <profile>", "Authentication profile for protected pages")
371
+ .option("--mode <mode>", "Audit depth: basic|standard|deep", parseMode, "basic")
372
+ .option("--format <format>", "Output format: json|sarif|junit|html|human (default: human in terminal, auto-detected in CI)")
373
+ .option("--json", "Shorthand for --format json (convenient for piping)")
374
+ .option("-o, --output <path>", "Output file path")
375
+ .option("--group-by <field>", "Group issues by: severity|category|route", parseGroupBy)
376
+ .option("--wait", "Wait for audit completion (default)")
377
+ .option("--no-wait", "Don't wait for audit completion")
378
+ .option("--severity <levels>", "Filter issues by severity: error|warning|info (comma-separated)")
379
+ .option("--category <categories>", "Filter issues by category (comma-separated)")
380
+ .option("--fail-on <severity>", "Exit 1 if new issues at or above severity: error|warning|info|none", parseFailOn)
381
+ .option("--threshold <score>", "Exit 3 if overall score below threshold (0-100)", parseThreshold)
382
+ .option("--max-new-errors <n>", "Maximum allowed new error-severity issues (default: 0)", (v) => validateNumeric(v, "max-new-errors", { min: 0, integer: true }))
383
+ .option("--max-new-warnings <n>", "Maximum allowed new warning-severity issues (default: unlimited)", (v) => validateNumeric(v, "max-new-warnings", { min: 0, integer: true }))
384
+ .option("--fail-on-existing", "Also fail on existing issues (legacy mode)")
385
+ .option("--bypass-labels <labels>", "Comma-separated PR labels that bypass quality gate")
386
+ .option("--baseline <path>", "Path to baseline file for new issue detection")
387
+ .option("--timeout <ms>", "Wait timeout in milliseconds (1-300000)", parseTimeout)
388
+ .option("--interval <ms>", "Poll interval in milliseconds (1-300000)", parseInterval)
389
+ .option("--interactive", "Step through issues interactively (requires --wait)")
390
+ // Repro artifact options (CLI-17)
391
+ .option("--save-trace", "Save Playwright trace for debugging")
392
+ .option("--save-har", "Save HAR network log")
393
+ .option("--screenshots", "Save page screenshots")
394
+ .option("--dom-snapshots", "Save DOM snapshots")
395
+ // Performance options (CLI-18)
396
+ .option("--concurrency <n>", "Number of concurrent audits (1-50, default: 3)", parseConcurrency)
397
+ .option("--cache", "Enable route caching to speed up repeated audits")
398
+ // CI artifact bundling (CICD-08)
399
+ .option("--upload-artifacts", "Save all outputs to .vertaaux/artifacts/ for CI upload")
400
+ .option("--job-id <id>", "Job identifier for artifact directory naming")
401
+ // Incremental mode (CICD-07)
402
+ .option("--incremental", "Only audit routes changed in PR")
403
+ .option("--base-branch <branch>", "Base branch for comparison (default: auto-detect)")
404
+ // Budget mode (CICD-13)
405
+ .option("--budget <mode>", "Budget mode: quick|standard|full", parseBudget)
406
+ // Monorepo options (CICD-16)
407
+ .option("--workspace <name>", "Audit specific workspace in monorepo")
408
+ .option("--all-workspaces", "Audit all workspaces in monorepo")
409
+ .option("--parallel", "Run workspace audits in parallel")
410
+ .option("--detect-matrix", "Output CI matrix config for monorepo (JSON)")
411
+ // Caching options (CICD-14)
412
+ .option("--no-cache", "Disable route caching")
413
+ .option("--cache-dir <path>", "Custom cache directory")
414
+ // Logging options (CICD-18)
415
+ .option("--json-logs", "Output structured JSON logs for CI")
416
+ // Policy options (CICD-17)
417
+ .option("--policy <file>", "Path to policy file (default: auto-detect vertaa.policy.yml)")
418
+ .option("--explain", "Append AI explanation to audit results")
419
+ .option("--strict", "Fail immediately on first step error")
420
+ .option("--continue-on-error", "Continue on step errors even in CI")
421
+ .action(async (urlArg, cmdOptions, command) => {
422
+ try {
423
+ // Initialize structured logger
424
+ const logger = createLogger({
425
+ json: cmdOptions.jsonLogs || false,
426
+ level: cmdOptions.quiet ? "error" : "info",
427
+ });
428
+ // Load config (supports --config global option)
429
+ const globalOpts = command.optsWithGlobals();
430
+ const config = await resolveConfig(globalOpts.config);
431
+ // Propagate global --machine and --base flags to command options
432
+ cmdOptions.machine = globalOpts.machine || false;
433
+ cmdOptions.base = globalOpts.base || cmdOptions.base;
434
+ // Handle monorepo detection and matrix output (CICD-16)
435
+ if (cmdOptions.detectMatrix || cmdOptions.allWorkspaces || cmdOptions.workspace) {
436
+ const monorepo = await detectMonorepo();
437
+ if (monorepo.type === "none") {
438
+ if (cmdOptions.detectMatrix) {
439
+ // Output empty matrix for non-monorepo
440
+ process.stdout.write(JSON.stringify({ include: [] }) + "\n");
441
+ process.exit(ExitCode.SUCCESS);
442
+ }
443
+ // Not a monorepo, continue with normal audit
444
+ }
445
+ else {
446
+ logger.info(`Detected ${monorepo.type} monorepo`, {
447
+ workspaces: monorepo.workspaces.length,
448
+ });
449
+ const apps = getAuditableApps(monorepo);
450
+ logger.info(`Found ${apps.length} auditable apps`, {
451
+ apps: apps.map((a) => a.name),
452
+ });
453
+ // Output matrix config for CI
454
+ if (cmdOptions.detectMatrix) {
455
+ const matrix = generateMatrixConfig(apps);
456
+ process.stdout.write(JSON.stringify(matrix) + "\n");
457
+ process.exit(ExitCode.SUCCESS);
458
+ }
459
+ // Audit specific workspace
460
+ if (cmdOptions.workspace) {
461
+ const workspace = monorepo.workspaces.find((w) => w.name === cmdOptions.workspace);
462
+ if (!workspace) {
463
+ writeOutput(`Error: Workspace "${cmdOptions.workspace}" not found\nAvailable: ${monorepo.workspaces.map((w) => w.name).join(", ")}\n`);
464
+ process.exit(ExitCode.ERROR);
465
+ }
466
+ // Use workspace URL or infer from type
467
+ if (workspace.url) {
468
+ urlArg = workspace.url;
469
+ logger.info(`Auditing workspace`, { workspace: workspace.name, url: urlArg });
470
+ }
471
+ }
472
+ // Audit all workspaces — not yet implemented (placeholder removed to prevent false-green CI results)
473
+ if (cmdOptions.allWorkspaces) {
474
+ // TODO: Implement real workspace audit execution (iterate apps, call executeAudit per workspace)
475
+ writeOutput(colorize(strings.audit.errors.allWorkspacesNotImplemented, severityPalette.warning) +
476
+ "\n" +
477
+ apps.map((app) => dim(` vertaa audit ${app.url || app.name}`)).join("\n") +
478
+ "\n");
479
+ process.exit(ExitCode.ERROR);
480
+ }
481
+ }
482
+ }
483
+ // Handle incremental mode (CICD-07)
484
+ if (cmdOptions.incremental) {
485
+ const baseBranch = validateBranchName(cmdOptions.baseBranch || detectBaseBranch());
486
+ const changedResult = await getChangedRoutes({
487
+ baseBranch,
488
+ routePatterns: [], // Use default patterns
489
+ });
490
+ if (!changedResult.hasChanges) {
491
+ writeOutput(colorize(strings.audit.noRouteChanges, brand.lime) + "\n");
492
+ process.exit(ExitCode.SUCCESS);
493
+ }
494
+ // Log which routes will be audited
495
+ writeOutput(dim(`Incremental mode: Auditing ${changedResult.routes.length} changed routes`) + "\n");
496
+ for (const route of changedResult.routes) {
497
+ writeOutput(dim(` - ${route}`) + "\n");
498
+ }
499
+ // If routes were detected, use them instead of URL argument
500
+ if (!config.defaultUrl) {
501
+ writeOutput("Error: --incremental requires defaultUrl in config to construct full URLs.\n");
502
+ process.exit(ExitCode.ERROR);
503
+ }
504
+ // Set routes option for executeAudit
505
+ cmdOptions.routes = changedResult.routes.join(",");
506
+ }
507
+ // Handle budget mode (CICD-13)
508
+ if (cmdOptions.budget) {
509
+ const budgetConfig = getBudgetConfig(cmdOptions.budget);
510
+ // Apply budget constraints to options
511
+ if (!cmdOptions.concurrency) {
512
+ cmdOptions.concurrency = budgetConfig.concurrency;
513
+ }
514
+ if (!cmdOptions.timeout) {
515
+ cmdOptions.timeout = budgetConfig.maxTime;
516
+ }
517
+ writeOutput(dim(`Budget mode: ${cmdOptions.budget} (max ${budgetConfig.maxPages} pages, ${budgetConfig.maxTime / 1000}s timeout, ${budgetConfig.concurrency} concurrent)`) + "\n");
518
+ }
519
+ // Resolve target URL
520
+ // Priority: positional > --url > --repo > --storybook > --routes > config default
521
+ let targetUrl;
522
+ if (urlArg) {
523
+ targetUrl = urlArg;
524
+ }
525
+ else if (cmdOptions.url) {
526
+ targetUrl = cmdOptions.url;
527
+ }
528
+ else if (cmdOptions.repo) {
529
+ // For repo audits, construct a special URL or handle differently
530
+ // For now, we'll just pass it as a marker
531
+ targetUrl = `repo:${cmdOptions.repo}`;
532
+ }
533
+ else if (cmdOptions.storybook) {
534
+ targetUrl = cmdOptions.storybook;
535
+ }
536
+ else if (cmdOptions.routes) {
537
+ // Routes require a base URL from config
538
+ if (!config.defaultUrl) {
539
+ writeOutput("Error: --routes requires defaultUrl in config or a base URL.\n");
540
+ process.exit(ExitCode.ERROR);
541
+ }
542
+ // For routes, we'll handle them as comma-separated paths
543
+ const routes = cmdOptions.routes.split(",").map((r) => r.trim());
544
+ targetUrl = `${config.defaultUrl}${routes[0]}`; // First route for now
545
+ }
546
+ else if (config.defaultUrl) {
547
+ targetUrl = config.defaultUrl;
548
+ }
549
+ if (!targetUrl) {
550
+ writeOutput("Error: URL is required. Provide as argument, --url flag, or defaultUrl in config.\n");
551
+ process.exit(ExitCode.ERROR);
552
+ }
553
+ await handleAudit(targetUrl, cmdOptions, config);
554
+ }
555
+ catch (error) {
556
+ process.stderr.write(renderError({
557
+ message: error instanceof Error ? error.message : String(error),
558
+ suggestion: "vertaa doctor",
559
+ exitCode: ExitCode.ERROR,
560
+ }) + "\n");
561
+ process.exit(ExitCode.ERROR);
562
+ }
563
+ });
564
+ }
@@ -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;AAG3E,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"}