@vertaaux/cli 0.4.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (248) hide show
  1. package/CHANGELOG.md +116 -0
  2. package/MIGRATION.md +239 -0
  3. package/README.md +62 -17
  4. package/dist/app/interactive-app.d.ts +103 -0
  5. package/dist/app/interactive-app.d.ts.map +1 -0
  6. package/dist/app/interactive-app.js +328 -0
  7. package/dist/app/layout/canvas.d.ts +23 -0
  8. package/dist/app/layout/canvas.d.ts.map +1 -0
  9. package/dist/app/layout/canvas.js +36 -0
  10. package/dist/app/layout/footer.d.ts +31 -0
  11. package/dist/app/layout/footer.d.ts.map +1 -0
  12. package/dist/app/layout/footer.js +41 -0
  13. package/dist/app/layout/header.d.ts +20 -0
  14. package/dist/app/layout/header.d.ts.map +1 -0
  15. package/dist/app/layout/header.js +27 -0
  16. package/dist/app/menu/categories.d.ts +20 -0
  17. package/dist/app/menu/categories.d.ts.map +1 -0
  18. package/dist/app/menu/categories.js +166 -0
  19. package/dist/app/menu/filter.d.ts +17 -0
  20. package/dist/app/menu/filter.d.ts.map +1 -0
  21. package/dist/app/menu/filter.js +33 -0
  22. package/dist/app/menu/menu-view.d.ts +35 -0
  23. package/dist/app/menu/menu-view.d.ts.map +1 -0
  24. package/dist/app/menu/menu-view.js +230 -0
  25. package/dist/app/menu/recent.d.ts +24 -0
  26. package/dist/app/menu/recent.d.ts.map +1 -0
  27. package/dist/app/menu/recent.js +49 -0
  28. package/dist/app/types.d.ts +43 -0
  29. package/dist/app/types.d.ts.map +1 -0
  30. package/dist/app/types.js +7 -0
  31. package/dist/app/views/command-runner.d.ts +36 -0
  32. package/dist/app/views/command-runner.d.ts.map +1 -0
  33. package/dist/app/views/command-runner.js +415 -0
  34. package/dist/app/views/help-overlay.d.ts +21 -0
  35. package/dist/app/views/help-overlay.d.ts.map +1 -0
  36. package/dist/app/views/help-overlay.js +46 -0
  37. package/dist/auth/ci-token.d.ts +8 -2
  38. package/dist/auth/ci-token.d.ts.map +1 -1
  39. package/dist/auth/ci-token.js +15 -30
  40. package/dist/auth/device-flow.d.ts +2 -1
  41. package/dist/auth/device-flow.d.ts.map +1 -1
  42. package/dist/auth/device-flow.js +13 -10
  43. package/dist/auth/token-store.d.ts.map +1 -1
  44. package/dist/auth/token-store.js +12 -2
  45. package/dist/baseline/diff.d.ts +2 -2
  46. package/dist/baseline/diff.d.ts.map +1 -1
  47. package/dist/baseline/diff.js +15 -34
  48. package/dist/commands/a11y.d.ts +11 -0
  49. package/dist/commands/a11y.d.ts.map +1 -0
  50. package/dist/commands/a11y.js +149 -0
  51. package/dist/commands/audit/artifacts.d.ts +27 -0
  52. package/dist/commands/audit/artifacts.d.ts.map +1 -0
  53. package/dist/commands/audit/artifacts.js +158 -0
  54. package/dist/commands/audit/ci-detection.d.ts +18 -0
  55. package/dist/commands/audit/ci-detection.d.ts.map +1 -0
  56. package/dist/commands/audit/ci-detection.js +71 -0
  57. package/dist/commands/audit/explain.d.ts +11 -0
  58. package/dist/commands/audit/explain.d.ts.map +1 -0
  59. package/dist/commands/audit/explain.js +45 -0
  60. package/dist/commands/audit/filters.d.ts +17 -0
  61. package/dist/commands/audit/filters.d.ts.map +1 -0
  62. package/dist/commands/audit/filters.js +40 -0
  63. package/dist/commands/audit/index.d.ts +18 -0
  64. package/dist/commands/audit/index.d.ts.map +1 -0
  65. package/dist/commands/audit/index.js +589 -0
  66. package/dist/commands/audit/output.d.ts +32 -0
  67. package/dist/commands/audit/output.d.ts.map +1 -0
  68. package/dist/commands/audit/output.js +129 -0
  69. package/dist/commands/audit/policy.d.ts +27 -0
  70. package/dist/commands/audit/policy.d.ts.map +1 -0
  71. package/dist/commands/audit/policy.js +147 -0
  72. package/dist/commands/audit/scoring.d.ts +23 -0
  73. package/dist/commands/audit/scoring.d.ts.map +1 -0
  74. package/dist/commands/audit/scoring.js +70 -0
  75. package/dist/commands/audit/types.d.ts +89 -0
  76. package/dist/commands/audit/types.d.ts.map +1 -0
  77. package/dist/commands/audit/types.js +8 -0
  78. package/dist/commands/audit.d.ts +2 -60
  79. package/dist/commands/audit.d.ts.map +1 -1
  80. package/dist/commands/audit.js +2 -1097
  81. package/dist/commands/baseline.d.ts +2 -0
  82. package/dist/commands/baseline.d.ts.map +1 -1
  83. package/dist/commands/baseline.js +221 -123
  84. package/dist/commands/comment.d.ts +22 -0
  85. package/dist/commands/comment.d.ts.map +1 -1
  86. package/dist/commands/comment.js +127 -62
  87. package/dist/commands/compare.d.ts +17 -0
  88. package/dist/commands/compare.d.ts.map +1 -1
  89. package/dist/commands/compare.js +288 -181
  90. package/dist/commands/diff.d.ts +7 -0
  91. package/dist/commands/diff.d.ts.map +1 -1
  92. package/dist/commands/diff.js +181 -143
  93. package/dist/commands/doc.d.ts +10 -0
  94. package/dist/commands/doc.d.ts.map +1 -1
  95. package/dist/commands/doc.js +135 -77
  96. package/dist/commands/doctor.d.ts +2 -0
  97. package/dist/commands/doctor.d.ts.map +1 -1
  98. package/dist/commands/doctor.js +166 -19
  99. package/dist/commands/download.d.ts +10 -0
  100. package/dist/commands/download.d.ts.map +1 -1
  101. package/dist/commands/download.js +169 -112
  102. package/dist/commands/explain.d.ts +5 -0
  103. package/dist/commands/explain.d.ts.map +1 -1
  104. package/dist/commands/explain.js +242 -156
  105. package/dist/commands/fix-all.d.ts +25 -0
  106. package/dist/commands/fix-all.d.ts.map +1 -0
  107. package/dist/commands/fix-all.js +206 -0
  108. package/dist/commands/fix-plan.d.ts +9 -0
  109. package/dist/commands/fix-plan.d.ts.map +1 -1
  110. package/dist/commands/fix-plan.js +154 -90
  111. package/dist/commands/fix.d.ts +17 -0
  112. package/dist/commands/fix.d.ts.map +1 -0
  113. package/dist/commands/fix.js +111 -0
  114. package/dist/commands/init.d.ts +11 -0
  115. package/dist/commands/init.d.ts.map +1 -1
  116. package/dist/commands/init.js +94 -42
  117. package/dist/commands/login.d.ts +18 -0
  118. package/dist/commands/login.d.ts.map +1 -1
  119. package/dist/commands/login.js +263 -92
  120. package/dist/commands/patch-review.d.ts +11 -0
  121. package/dist/commands/patch-review.d.ts.map +1 -1
  122. package/dist/commands/patch-review.js +160 -98
  123. package/dist/commands/policy.d.ts +31 -0
  124. package/dist/commands/policy.d.ts.map +1 -1
  125. package/dist/commands/policy.js +270 -125
  126. package/dist/commands/release-notes.d.ts +10 -0
  127. package/dist/commands/release-notes.d.ts.map +1 -1
  128. package/dist/commands/release-notes.js +128 -74
  129. package/dist/commands/scan.d.ts +13 -0
  130. package/dist/commands/scan.d.ts.map +1 -0
  131. package/dist/commands/scan.js +133 -0
  132. package/dist/commands/status.d.ts +9 -0
  133. package/dist/commands/status.d.ts.map +1 -0
  134. package/dist/commands/status.js +81 -0
  135. package/dist/commands/suggest.d.ts +10 -0
  136. package/dist/commands/suggest.d.ts.map +1 -1
  137. package/dist/commands/suggest.js +180 -83
  138. package/dist/commands/triage.d.ts +35 -0
  139. package/dist/commands/triage.d.ts.map +1 -1
  140. package/dist/commands/triage.js +207 -82
  141. package/dist/commands/upload.d.ts +9 -0
  142. package/dist/commands/upload.d.ts.map +1 -1
  143. package/dist/commands/upload.js +140 -101
  144. package/dist/commands/verify.d.ts +13 -0
  145. package/dist/commands/verify.d.ts.map +1 -0
  146. package/dist/commands/verify.js +118 -0
  147. package/dist/config/schema.d.ts +4 -0
  148. package/dist/config/schema.d.ts.map +1 -1
  149. package/dist/index.d.ts +3 -2
  150. package/dist/index.d.ts.map +1 -1
  151. package/dist/index.js +127 -991
  152. package/dist/interactive/fix-wizard.d.ts +3 -0
  153. package/dist/interactive/fix-wizard.d.ts.map +1 -1
  154. package/dist/interactive/fix-wizard.js +130 -112
  155. package/dist/interactive/init-wizard.d.ts +3 -1
  156. package/dist/interactive/init-wizard.d.ts.map +1 -1
  157. package/dist/interactive/init-wizard.js +207 -138
  158. package/dist/interactive/prompts.d.ts +7 -3
  159. package/dist/interactive/prompts.d.ts.map +1 -1
  160. package/dist/interactive/prompts.js +44 -23
  161. package/dist/output/envelope.d.ts +9 -0
  162. package/dist/output/envelope.d.ts.map +1 -1
  163. package/dist/output/envelope.js +37 -3
  164. package/dist/output/factory.d.ts +2 -1
  165. package/dist/output/factory.d.ts.map +1 -1
  166. package/dist/output/html.d.ts +2 -1
  167. package/dist/output/html.d.ts.map +1 -1
  168. package/dist/output/html.js +3 -2
  169. package/dist/output/human.d.ts +2 -1
  170. package/dist/output/human.d.ts.map +1 -1
  171. package/dist/output/human.js +3 -2
  172. package/dist/output/json.d.ts +2 -1
  173. package/dist/output/json.d.ts.map +1 -1
  174. package/dist/output/junit.d.ts +2 -1
  175. package/dist/output/junit.d.ts.map +1 -1
  176. package/dist/output/sarif.d.ts +2 -1
  177. package/dist/output/sarif.d.ts.map +1 -1
  178. package/dist/policy/schema.d.ts +137 -0
  179. package/dist/policy/schema.d.ts.map +1 -1
  180. package/dist/policy/schema.js +107 -0
  181. package/dist/prompts/command-catalog.js +9 -9
  182. package/dist/types.d.ts +74 -0
  183. package/dist/types.d.ts.map +1 -0
  184. package/dist/types.js +5 -0
  185. package/dist/ui/banner.d.ts +34 -0
  186. package/dist/ui/banner.d.ts.map +1 -1
  187. package/dist/ui/banner.js +97 -5
  188. package/dist/ui/diagnostics.d.ts +9 -4
  189. package/dist/ui/diagnostics.d.ts.map +1 -1
  190. package/dist/ui/diagnostics.js +32 -82
  191. package/dist/ui/strings.d.ts +373 -0
  192. package/dist/ui/strings.d.ts.map +1 -0
  193. package/dist/ui/strings.js +499 -0
  194. package/dist/ui/table.d.ts +0 -2
  195. package/dist/ui/table.d.ts.map +1 -1
  196. package/dist/ui/table.js +3 -4
  197. package/dist/utils/api-client.d.ts +46 -0
  198. package/dist/utils/api-client.d.ts.map +1 -0
  199. package/dist/utils/api-client.js +170 -0
  200. package/dist/utils/client.d.ts +29 -18
  201. package/dist/utils/client.d.ts.map +1 -1
  202. package/dist/utils/client.js +104 -12
  203. package/dist/utils/formatters.d.ts +38 -0
  204. package/dist/utils/formatters.d.ts.map +1 -0
  205. package/dist/utils/formatters.js +277 -0
  206. package/dist/utils/root-args.d.ts +12 -0
  207. package/dist/utils/root-args.d.ts.map +1 -0
  208. package/dist/utils/root-args.js +44 -0
  209. package/dist/utils/stdin.d.ts +7 -0
  210. package/dist/utils/stdin.d.ts.map +1 -1
  211. package/dist/utils/stdin.js +32 -2
  212. package/dist/utils/url-classify.d.ts.map +1 -1
  213. package/dist/utils/url-classify.js +24 -3
  214. package/node_modules/@vertaaux/tui/dist/index.cjs +1216 -27
  215. package/node_modules/@vertaaux/tui/dist/index.cjs.map +1 -1
  216. package/node_modules/@vertaaux/tui/dist/index.d.cts +361 -4
  217. package/node_modules/@vertaaux/tui/dist/index.d.ts +361 -4
  218. package/node_modules/@vertaaux/tui/dist/index.js +1189 -27
  219. package/node_modules/@vertaaux/tui/dist/index.js.map +1 -1
  220. package/node_modules/@vertaaux/tui/package.json +2 -3
  221. package/node_modules/chalk/license +9 -0
  222. package/node_modules/chalk/package.json +83 -0
  223. package/node_modules/chalk/readme.md +297 -0
  224. package/node_modules/chalk/source/index.d.ts +325 -0
  225. package/node_modules/chalk/source/index.js +225 -0
  226. package/node_modules/chalk/source/utilities.js +33 -0
  227. package/node_modules/chalk/source/vendor/ansi-styles/index.d.ts +236 -0
  228. package/node_modules/chalk/source/vendor/ansi-styles/index.js +223 -0
  229. package/node_modules/chalk/source/vendor/supports-color/browser.d.ts +1 -0
  230. package/node_modules/chalk/source/vendor/supports-color/browser.js +34 -0
  231. package/node_modules/chalk/source/vendor/supports-color/index.d.ts +55 -0
  232. package/node_modules/chalk/source/vendor/supports-color/index.js +190 -0
  233. package/package.json +20 -5
  234. package/dist/commands/client.d.ts +0 -14
  235. package/dist/commands/client.d.ts.map +0 -1
  236. package/dist/commands/client.js +0 -362
  237. package/dist/commands/drift.d.ts +0 -15
  238. package/dist/commands/drift.d.ts.map +0 -1
  239. package/dist/commands/drift.js +0 -309
  240. package/dist/commands/protect.d.ts +0 -16
  241. package/dist/commands/protect.d.ts.map +0 -1
  242. package/dist/commands/protect.js +0 -323
  243. package/dist/commands/report.d.ts +0 -15
  244. package/dist/commands/report.d.ts.map +0 -1
  245. package/dist/commands/report.js +0 -214
  246. package/dist/policy/sync.d.ts +0 -67
  247. package/dist/policy/sync.d.ts.map +0 -1
  248. package/dist/policy/sync.js +0 -147
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,46 @@ 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
+ import { getRootCommandToken, hasHelpOrVersionFlag } from "./utils/root-args.js";
52
+ // Detect best symbol tier for this terminal (nerd > unicode > ascii)
53
+ setSymbolTier(detectSymbolTier());
54
+ // ============================================================================
55
+ // Global error handlers — installed before anything else runs
56
+ // Ensures no user ever sees a raw Node.js stack trace
57
+ // ============================================================================
58
+ process.on("uncaughtException", (err) => {
59
+ process.stderr.write(renderError({
60
+ message: err.message || "An unexpected error occurred",
61
+ suggestion: "vertaa doctor",
62
+ exitCode: ExitCode.ERROR,
63
+ }) + "\n");
64
+ process.exit(ExitCode.ERROR);
65
+ });
66
+ process.on("unhandledRejection", (reason) => {
67
+ const message = reason instanceof Error ? reason.message : String(reason);
68
+ process.stderr.write(renderError({
69
+ message,
70
+ suggestion: "vertaa doctor",
71
+ exitCode: ExitCode.ERROR,
72
+ }) + "\n");
73
+ process.exit(ExitCode.ERROR);
74
+ });
37
75
  const __filename = fileURLToPath(import.meta.url);
38
76
  const __dirname = path.dirname(__filename);
39
77
  // Load environment variables from multiple locations
@@ -45,8 +83,10 @@ const envCandidates = [
45
83
  path.resolve(__dirname, "../../.env.local"),
46
84
  path.resolve(__dirname, "../../.env"),
47
85
  ];
48
- // Silence any dotenv stdout output during loading
86
+ // Silence any dotenv stdout output during loading.
87
+ // eslint-disable-next-line no-console -- intentional: monkey-patching console.log to suppress dotenv banner output from a third-party lib
49
88
  const originalLog = console.log;
89
+ // eslint-disable-next-line no-console -- intentional: temporarily replacing console.log to filter dotenv's [dotenv@x.y.z] banners
50
90
  console.log = (...args) => {
51
91
  const msg = String(args[0] || "");
52
92
  if (msg.includes("[dotenv"))
@@ -58,805 +98,8 @@ for (const candidate of envCandidates) {
58
98
  dotenv.config({ path: candidate, override: false });
59
99
  }
60
100
  }
101
+ // eslint-disable-next-line no-console -- intentional: restoring original console.log after dotenv suppression
61
102
  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
103
  // ============================================================================
861
104
  // Commander.js CLI setup
862
105
  // ============================================================================
@@ -874,11 +117,18 @@ program
874
117
  .option("--machine", "Strict machine-readable output (JSON stdout, diagnostics stderr)")
875
118
  .option("--color", "Force color output")
876
119
  .option("--no-color", "Disable color output")
120
+ .option("--plain", "Strip all color and use ASCII-only output (auto-enabled when stdout is not a TTY)")
877
121
  .option("--dashboard", "Force live dashboard during audit --wait")
878
122
  .option("--no-dashboard", "Disable live dashboard (use spinner instead)")
879
123
  .option("--dry-run", "Show what would happen without executing")
880
124
  .option("-y, --yes", "Auto-confirm all interactive prompts")
881
125
  .option("--verbose", "Expand output with additional details")
126
+ .addHelpText("after", `
127
+ Authentication:
128
+ SDK handles auth by default (reads VERTAAUX_API_KEY or ~/.vertaaux/credentials.json).
129
+ Use --api-key <key> or VERTAAUX_API_KEY env var to override for scripts and CI.
130
+ Run "vertaa login" to set up default credentials.
131
+ `)
882
132
  .configureOutput({
883
133
  outputError: (str, _write) => {
884
134
  const formatted = formatCommanderError(str);
@@ -895,31 +145,41 @@ program
895
145
  // For other Commander errors (help, version), use Commander's default exit code
896
146
  process.exit(err.exitCode);
897
147
  })
898
- .hook("preAction", (thisCommand) => {
148
+ .hook("preAction", async (thisCommand) => {
899
149
  const opts = thisCommand.optsWithGlobals();
900
150
  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);
151
+ // Plain mode: explicit --plain or auto-detect when stdout is not a TTY
152
+ // Precedence: explicit --plain > explicit --color/--no-color > NO_COLOR/FORCE_COLOR > TTY check
153
+ const explicitColor = opts.color === true;
154
+ const autoPlain = !process.stdout.isTTY && !explicitColor;
155
+ const plain = Boolean(opts.plain) || autoPlain;
156
+ if (plain && !machineMode) {
157
+ setPlainMode(true);
907
158
  }
908
159
  else {
909
- setColorEnabled(shouldUseColor());
160
+ // Apply color settings from flags or environment
161
+ if (opts.color === false || process.env.NO_COLOR !== undefined) {
162
+ setColorEnabled(false);
163
+ }
164
+ else if (opts.color === true || process.env.FORCE_COLOR !== undefined) {
165
+ setColorEnabled(true);
166
+ }
167
+ else {
168
+ setColorEnabled(shouldUseColor());
169
+ }
910
170
  }
911
171
  if (machineMode) {
912
172
  opts.quiet = true;
913
173
  opts.banner = false;
914
174
  setColorEnabled(false);
915
175
  }
916
- showBanner({
176
+ await animateBanner({
917
177
  version,
918
178
  quiet: opts.quiet || machineMode,
919
179
  noBanner: opts.banner === false || machineMode,
920
180
  });
921
181
  });
922
- // Register commands (new Commander.js implementations)
182
+ // Register modern Commander.js commands
923
183
  registerAuditCommand(program);
924
184
  registerBaselineCommand(program);
925
185
  registerCommentCommand(program);
@@ -938,187 +198,63 @@ registerPatchReviewCommand(program);
938
198
  registerCompareCommand(program);
939
199
  registerReleaseNotesCommand(program);
940
200
  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
- });
201
+ // Register legacy commands (backward compatibility)
202
+ registerA11yCommand(program);
203
+ registerScanCommand(program);
204
+ registerStatusCommand(program);
205
+ registerFixCommand(program);
206
+ registerFixAllCommand(program);
207
+ registerVerifyCommand(program);
208
+ // ============================================================================
209
+ // Interactive app (when no command given in TTY)
210
+ // ============================================================================
1060
211
  /**
1061
- * Interactive menu when no command given in TTY.
212
+ * Launch the full InteractiveApp categorized menu, search, recent commands,
213
+ * help overlay, and command dispatch within the persistent 3-section layout.
214
+ *
215
+ * Replaces the previous showInteractiveMenu() (5-item select prompt) with the
216
+ * full interactive experience designed in Phase 102.
1062
217
  */
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
- }
218
+ async function launchInteractiveApp() {
219
+ const app = new InteractiveApp();
220
+ /** Create a menu onSelect handler */
221
+ const makeOnSelect = () => {
222
+ return (command) => {
223
+ void (async () => {
224
+ await recordRecent(command);
225
+ // CommandRunnerView handles its own suspend/resume lifecycle:
226
+ // - Collects args on alt screen (raw mode, no stdin transition)
227
+ // - Suspends only when running the command
228
+ // - Resumes and returns to menu after user reads output
229
+ const runner = new CommandRunnerView(command, app, returnToMenu);
230
+ await app.setView(runner);
231
+ })();
232
+ };
233
+ };
234
+ /** Navigate back to the menu from any view */
235
+ const returnToMenu = () => {
236
+ const menu = new MenuView(makeOnSelect());
237
+ void app.setView(menu);
238
+ };
239
+ // Start with the menu
240
+ const initialMenu = new MenuView(makeOnSelect());
241
+ await app.setView(initialMenu);
242
+ await app.run();
1112
243
  }
244
+ // ============================================================================
1113
245
  // Parse and execute
1114
- // If no command and TTY, show interactive menu
246
+ // ============================================================================
1115
247
  const args = process.argv.slice(2);
1116
- 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 hasCommand = Boolean(getRootCommandToken(args));
249
+ const hasGlobalFlag = hasHelpOrVersionFlag(args);
250
+ if (!hasCommand && !hasGlobalFlag && isInteractive()) {
251
+ // InteractiveApp renders its own banner in the header section — no pre-banner needed
252
+ launchInteractiveApp().catch((error) => {
253
+ process.stderr.write(renderError({
254
+ message: error instanceof Error ? error.message : String(error),
255
+ suggestion: "vertaa doctor",
256
+ exitCode: ExitCode.ERROR,
257
+ }) + "\n");
1122
258
  process.exit(ExitCode.ERROR);
1123
259
  });
1124
260
  }