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