@vertaaux/cli 0.3.3 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (227) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/MIGRATION.md +239 -0
  3. package/README.md +34 -16
  4. package/dist/app/interactive-app.d.ts +101 -0
  5. package/dist/app/interactive-app.d.ts.map +1 -0
  6. package/dist/app/interactive-app.js +309 -0
  7. package/dist/app/layout/canvas.d.ts +23 -0
  8. package/dist/app/layout/canvas.d.ts.map +1 -0
  9. package/dist/app/layout/canvas.js +36 -0
  10. package/dist/app/layout/footer.d.ts +31 -0
  11. package/dist/app/layout/footer.d.ts.map +1 -0
  12. package/dist/app/layout/footer.js +41 -0
  13. package/dist/app/layout/header.d.ts +20 -0
  14. package/dist/app/layout/header.d.ts.map +1 -0
  15. package/dist/app/layout/header.js +27 -0
  16. package/dist/app/menu/categories.d.ts +20 -0
  17. package/dist/app/menu/categories.d.ts.map +1 -0
  18. package/dist/app/menu/categories.js +181 -0
  19. package/dist/app/menu/filter.d.ts +17 -0
  20. package/dist/app/menu/filter.d.ts.map +1 -0
  21. package/dist/app/menu/filter.js +33 -0
  22. package/dist/app/menu/menu-view.d.ts +35 -0
  23. package/dist/app/menu/menu-view.d.ts.map +1 -0
  24. package/dist/app/menu/menu-view.js +230 -0
  25. package/dist/app/menu/recent.d.ts +24 -0
  26. package/dist/app/menu/recent.d.ts.map +1 -0
  27. package/dist/app/menu/recent.js +49 -0
  28. package/dist/app/types.d.ts +43 -0
  29. package/dist/app/types.d.ts.map +1 -0
  30. package/dist/app/types.js +7 -0
  31. package/dist/app/views/command-runner.d.ts +36 -0
  32. package/dist/app/views/command-runner.d.ts.map +1 -0
  33. package/dist/app/views/command-runner.js +372 -0
  34. package/dist/app/views/help-overlay.d.ts +21 -0
  35. package/dist/app/views/help-overlay.d.ts.map +1 -0
  36. package/dist/app/views/help-overlay.js +45 -0
  37. package/dist/auth/ci-token.d.ts +14 -2
  38. package/dist/auth/ci-token.d.ts.map +1 -1
  39. package/dist/auth/ci-token.js +15 -30
  40. package/dist/auth/device-flow.d.ts +2 -1
  41. package/dist/auth/device-flow.d.ts.map +1 -1
  42. package/dist/auth/device-flow.js +13 -10
  43. package/dist/auth/token-store.d.ts.map +1 -1
  44. package/dist/auth/token-store.js +12 -2
  45. package/dist/baseline/diff.d.ts +2 -2
  46. package/dist/baseline/diff.d.ts.map +1 -1
  47. package/dist/baseline/diff.js +15 -34
  48. package/dist/commands/a11y.d.ts +9 -0
  49. package/dist/commands/a11y.d.ts.map +1 -0
  50. package/dist/commands/a11y.js +76 -0
  51. package/dist/commands/audit/artifacts.d.ts +27 -0
  52. package/dist/commands/audit/artifacts.d.ts.map +1 -0
  53. package/dist/commands/audit/artifacts.js +158 -0
  54. package/dist/commands/audit/ci-detection.d.ts +18 -0
  55. package/dist/commands/audit/ci-detection.d.ts.map +1 -0
  56. package/dist/commands/audit/ci-detection.js +71 -0
  57. package/dist/commands/audit/explain.d.ts +11 -0
  58. package/dist/commands/audit/explain.d.ts.map +1 -0
  59. package/dist/commands/audit/explain.js +45 -0
  60. package/dist/commands/audit/filters.d.ts +17 -0
  61. package/dist/commands/audit/filters.d.ts.map +1 -0
  62. package/dist/commands/audit/filters.js +40 -0
  63. package/dist/commands/audit/index.d.ts +18 -0
  64. package/dist/commands/audit/index.d.ts.map +1 -0
  65. package/dist/commands/audit/index.js +564 -0
  66. package/dist/commands/audit/output.d.ts +32 -0
  67. package/dist/commands/audit/output.d.ts.map +1 -0
  68. package/dist/commands/audit/output.js +130 -0
  69. package/dist/commands/audit/policy.d.ts +19 -0
  70. package/dist/commands/audit/policy.d.ts.map +1 -0
  71. package/dist/commands/audit/policy.js +102 -0
  72. package/dist/commands/audit/scoring.d.ts +23 -0
  73. package/dist/commands/audit/scoring.d.ts.map +1 -0
  74. package/dist/commands/audit/scoring.js +70 -0
  75. package/dist/commands/audit/types.d.ts +88 -0
  76. package/dist/commands/audit/types.d.ts.map +1 -0
  77. package/dist/commands/audit/types.js +8 -0
  78. package/dist/commands/audit.d.ts +2 -60
  79. package/dist/commands/audit.d.ts.map +1 -1
  80. package/dist/commands/audit.js +2 -1038
  81. package/dist/commands/baseline.d.ts +1 -0
  82. package/dist/commands/baseline.d.ts.map +1 -1
  83. package/dist/commands/baseline.js +205 -121
  84. package/dist/commands/comment.d.ts +22 -0
  85. package/dist/commands/comment.d.ts.map +1 -1
  86. package/dist/commands/comment.js +122 -58
  87. package/dist/commands/compare.d.ts +17 -0
  88. package/dist/commands/compare.d.ts.map +1 -1
  89. package/dist/commands/compare.js +287 -180
  90. package/dist/commands/diff.d.ts +5 -0
  91. package/dist/commands/diff.d.ts.map +1 -1
  92. package/dist/commands/diff.js +168 -141
  93. package/dist/commands/doc.d.ts +10 -0
  94. package/dist/commands/doc.d.ts.map +1 -1
  95. package/dist/commands/doc.js +134 -76
  96. package/dist/commands/doctor.d.ts +2 -0
  97. package/dist/commands/doctor.d.ts.map +1 -1
  98. package/dist/commands/doctor.js +164 -17
  99. package/dist/commands/download.d.ts +10 -0
  100. package/dist/commands/download.d.ts.map +1 -1
  101. package/dist/commands/download.js +169 -112
  102. package/dist/commands/explain.d.ts +5 -0
  103. package/dist/commands/explain.d.ts.map +1 -1
  104. package/dist/commands/explain.js +241 -155
  105. package/dist/commands/fix-all.d.ts +25 -0
  106. package/dist/commands/fix-all.d.ts.map +1 -0
  107. package/dist/commands/fix-all.js +206 -0
  108. package/dist/commands/fix-plan.d.ts +9 -0
  109. package/dist/commands/fix-plan.d.ts.map +1 -1
  110. package/dist/commands/fix-plan.js +152 -89
  111. package/dist/commands/fix.d.ts +17 -0
  112. package/dist/commands/fix.d.ts.map +1 -0
  113. package/dist/commands/fix.js +111 -0
  114. package/dist/commands/init.d.ts +11 -0
  115. package/dist/commands/init.d.ts.map +1 -1
  116. package/dist/commands/init.js +94 -42
  117. package/dist/commands/login.d.ts +18 -0
  118. package/dist/commands/login.d.ts.map +1 -1
  119. package/dist/commands/login.js +268 -95
  120. package/dist/commands/patch-review.d.ts +11 -0
  121. package/dist/commands/patch-review.d.ts.map +1 -1
  122. package/dist/commands/patch-review.js +159 -97
  123. package/dist/commands/policy.d.ts +31 -0
  124. package/dist/commands/policy.d.ts.map +1 -1
  125. package/dist/commands/policy.js +269 -124
  126. package/dist/commands/release-notes.d.ts +10 -0
  127. package/dist/commands/release-notes.d.ts.map +1 -1
  128. package/dist/commands/release-notes.js +127 -73
  129. package/dist/commands/scan.d.ts +13 -0
  130. package/dist/commands/scan.d.ts.map +1 -0
  131. package/dist/commands/scan.js +133 -0
  132. package/dist/commands/status.d.ts +9 -0
  133. package/dist/commands/status.d.ts.map +1 -0
  134. package/dist/commands/status.js +81 -0
  135. package/dist/commands/suggest.d.ts +10 -0
  136. package/dist/commands/suggest.d.ts.map +1 -1
  137. package/dist/commands/suggest.js +153 -82
  138. package/dist/commands/triage.d.ts +35 -0
  139. package/dist/commands/triage.d.ts.map +1 -1
  140. package/dist/commands/triage.js +206 -81
  141. package/dist/commands/upload.d.ts +9 -0
  142. package/dist/commands/upload.d.ts.map +1 -1
  143. package/dist/commands/upload.js +140 -101
  144. package/dist/commands/verify.d.ts +13 -0
  145. package/dist/commands/verify.d.ts.map +1 -0
  146. package/dist/commands/verify.js +118 -0
  147. package/dist/index.d.ts +3 -2
  148. package/dist/index.d.ts.map +1 -1
  149. package/dist/index.js +125 -990
  150. package/dist/interactive/fix-wizard.d.ts +3 -0
  151. package/dist/interactive/fix-wizard.d.ts.map +1 -1
  152. package/dist/interactive/fix-wizard.js +130 -112
  153. package/dist/interactive/init-wizard.d.ts +3 -1
  154. package/dist/interactive/init-wizard.d.ts.map +1 -1
  155. package/dist/interactive/init-wizard.js +207 -138
  156. package/dist/interactive/prompts.d.ts +7 -3
  157. package/dist/interactive/prompts.d.ts.map +1 -1
  158. package/dist/interactive/prompts.js +44 -23
  159. package/dist/output/envelope.d.ts +2 -0
  160. package/dist/output/envelope.d.ts.map +1 -1
  161. package/dist/output/envelope.js +18 -2
  162. package/dist/output/factory.d.ts +9 -1
  163. package/dist/output/factory.d.ts.map +1 -1
  164. package/dist/output/html.d.ts +2 -1
  165. package/dist/output/html.d.ts.map +1 -1
  166. package/dist/output/html.js +3 -2
  167. package/dist/output/human.d.ts +9 -1
  168. package/dist/output/human.d.ts.map +1 -1
  169. package/dist/output/human.js +17 -2
  170. package/dist/output/json.d.ts +2 -1
  171. package/dist/output/json.d.ts.map +1 -1
  172. package/dist/output/junit.d.ts +2 -1
  173. package/dist/output/junit.d.ts.map +1 -1
  174. package/dist/output/sarif.d.ts +2 -1
  175. package/dist/output/sarif.d.ts.map +1 -1
  176. package/dist/types.d.ts +74 -0
  177. package/dist/types.d.ts.map +1 -0
  178. package/dist/types.js +5 -0
  179. package/dist/ui/banner.d.ts +34 -0
  180. package/dist/ui/banner.d.ts.map +1 -1
  181. package/dist/ui/banner.js +97 -5
  182. package/dist/ui/diagnostics.d.ts +9 -4
  183. package/dist/ui/diagnostics.d.ts.map +1 -1
  184. package/dist/ui/diagnostics.js +32 -82
  185. package/dist/ui/strings.d.ts +373 -0
  186. package/dist/ui/strings.d.ts.map +1 -0
  187. package/dist/ui/strings.js +499 -0
  188. package/dist/ui/table.d.ts +0 -2
  189. package/dist/ui/table.d.ts.map +1 -1
  190. package/dist/ui/table.js +3 -4
  191. package/dist/utils/api-client.d.ts +46 -0
  192. package/dist/utils/api-client.d.ts.map +1 -0
  193. package/dist/utils/api-client.js +170 -0
  194. package/dist/utils/client.d.ts +29 -18
  195. package/dist/utils/client.d.ts.map +1 -1
  196. package/dist/utils/client.js +102 -12
  197. package/dist/utils/formatters.d.ts +38 -0
  198. package/dist/utils/formatters.d.ts.map +1 -0
  199. package/dist/utils/formatters.js +277 -0
  200. package/dist/utils/local-capture.d.ts +25 -0
  201. package/dist/utils/local-capture.d.ts.map +1 -0
  202. package/dist/utils/local-capture.js +57 -0
  203. package/dist/utils/url-classify.d.ts +18 -0
  204. package/dist/utils/url-classify.d.ts.map +1 -0
  205. package/dist/utils/url-classify.js +106 -0
  206. package/node_modules/@vertaaux/tui/dist/index.cjs +713 -20
  207. package/node_modules/@vertaaux/tui/dist/index.cjs.map +1 -1
  208. package/node_modules/@vertaaux/tui/dist/index.d.cts +361 -4
  209. package/node_modules/@vertaaux/tui/dist/index.d.ts +361 -4
  210. package/node_modules/@vertaaux/tui/dist/index.js +689 -21
  211. package/node_modules/@vertaaux/tui/dist/index.js.map +1 -1
  212. package/package.json +13 -5
  213. package/dist/commands/client.d.ts +0 -14
  214. package/dist/commands/client.d.ts.map +0 -1
  215. package/dist/commands/client.js +0 -362
  216. package/dist/commands/drift.d.ts +0 -15
  217. package/dist/commands/drift.d.ts.map +0 -1
  218. package/dist/commands/drift.js +0 -309
  219. package/dist/commands/protect.d.ts +0 -16
  220. package/dist/commands/protect.d.ts.map +0 -1
  221. package/dist/commands/protect.js +0 -323
  222. package/dist/commands/report.d.ts +0 -15
  223. package/dist/commands/report.d.ts.map +0 -1
  224. package/dist/commands/report.js +0 -214
  225. package/dist/policy/sync.d.ts +0 -67
  226. package/dist/policy/sync.d.ts.map +0 -1
  227. package/dist/policy/sync.js +0 -147
@@ -12,15 +12,15 @@
12
12
  * vertaa triage --job abc123
13
13
  * vertaa triage --file audit.json --verbose
14
14
  */
15
- import chalk from "chalk";
15
+ import { bold, dim, colorize, boldColor, brand, severity as severityPalette, runSteps, createRenderer, renderWarning, renderError, isCI, isTTY, isStdinTTY, ScrollingViewport } from "@vertaaux/tui";
16
16
  import { ExitCode } from "../utils/exit-codes.js";
17
- import { resolveApiBase, getApiKey, apiRequest } from "../utils/client.js";
17
+ import { resolveApiBase, getApiKey, apiRequest, createClient } from "../utils/client.js";
18
18
  import { resolveConfig } from "../config/loader.js";
19
19
  import { writeJsonOutput, writeOutput } from "../output/envelope.js";
20
20
  import { resolveCommandFormat } from "../output/formats.js";
21
- import { createSpinner, succeedSpinner } from "../ui/spinner.js";
22
21
  import { readJsonInput } from "../utils/stdin.js";
23
- import { handleAiCommandError, AI_TIMEOUT_MS } from "../utils/ai-error.js";
22
+ import { AI_TIMEOUT_MS } from "../utils/ai-error.js";
23
+ import { strings } from "../ui/strings.js";
24
24
  // ---------------------------------------------------------------------------
25
25
  // Helpers
26
26
  // ---------------------------------------------------------------------------
@@ -53,21 +53,21 @@ function normalizeIssues(issues) {
53
53
  // Formatters
54
54
  // ---------------------------------------------------------------------------
55
55
  const EFFORT_LABELS = {
56
- trivial: chalk.green("trivial"),
57
- small: chalk.green("small"),
58
- medium: chalk.yellow("medium"),
59
- large: chalk.red("large"),
56
+ trivial: colorize("trivial", brand.lime),
57
+ small: colorize("small", brand.lime),
58
+ medium: colorize("medium", severityPalette.warning),
59
+ large: colorize("large", severityPalette.error),
60
60
  };
61
61
  function formatEffort(effort) {
62
- return EFFORT_LABELS[effort] || chalk.dim(effort);
62
+ return EFFORT_LABELS[effort] || dim(effort);
63
63
  }
64
64
  function formatTriageHuman(data, verbose) {
65
65
  const lines = [];
66
66
  // P0
67
- lines.push(chalk.red.bold(`P0 Critical (${data.p0_critical.length})`));
67
+ lines.push(boldColor(`P0 Critical (${data.p0_critical.length})`, severityPalette.error));
68
68
  if (verbose) {
69
69
  for (const item of data.p0_critical) {
70
- lines.push(` ${chalk.red(">")} ${chalk.bold(item.title)}${item.id ? chalk.dim(` (${item.id})`) : ""}`);
70
+ lines.push(` ${colorize(">", severityPalette.error)} ${bold(item.title)}${item.id ? dim(` (${item.id})`) : ""}`);
71
71
  lines.push(` ${item.reason}`);
72
72
  lines.push(` Effort: ${formatEffort(item.effort)}`);
73
73
  }
@@ -77,10 +77,10 @@ function formatTriageHuman(data, verbose) {
77
77
  }
78
78
  lines.push("");
79
79
  // P1
80
- lines.push(chalk.yellow.bold(`P1 Important (${data.p1_important.length})`));
80
+ lines.push(boldColor(`P1 Important (${data.p1_important.length})`, severityPalette.warning));
81
81
  if (verbose) {
82
82
  for (const item of data.p1_important) {
83
- lines.push(` ${chalk.yellow(">")} ${chalk.bold(item.title)}${item.id ? chalk.dim(` (${item.id})`) : ""}`);
83
+ lines.push(` ${colorize(">", severityPalette.warning)} ${bold(item.title)}${item.id ? dim(` (${item.id})`) : ""}`);
84
84
  lines.push(` ${item.reason}`);
85
85
  lines.push(` Effort: ${formatEffort(item.effort)}`);
86
86
  }
@@ -90,10 +90,10 @@ function formatTriageHuman(data, verbose) {
90
90
  }
91
91
  lines.push("");
92
92
  // P2
93
- lines.push(chalk.cyan.bold(`P2 Nice to Have (${data.p2_nice_to_have.length})`));
93
+ lines.push(boldColor(`P2 Nice to Have (${data.p2_nice_to_have.length})`, brand.cyan));
94
94
  if (verbose) {
95
95
  for (const item of data.p2_nice_to_have) {
96
- lines.push(` ${chalk.cyan(">")} ${chalk.bold(item.title)}${item.id ? chalk.dim(` (${item.id})`) : ""}`);
96
+ lines.push(` ${colorize(">", brand.cyan)} ${bold(item.title)}${item.id ? dim(` (${item.id})`) : ""}`);
97
97
  lines.push(` ${item.reason}`);
98
98
  lines.push(` Effort: ${formatEffort(item.effort)}`);
99
99
  }
@@ -104,14 +104,185 @@ function formatTriageHuman(data, verbose) {
104
104
  lines.push("");
105
105
  // Quick wins
106
106
  if (data.quick_wins.length > 0) {
107
- lines.push(chalk.green.bold("Quick Wins (< 5 min each)"));
107
+ lines.push(boldColor("Quick Wins (< 5 min each)", brand.lime));
108
108
  for (const win of data.quick_wins) {
109
- lines.push(` ${chalk.green("*")} ${win}`);
109
+ lines.push(` ${colorize("*", brand.lime)} ${win}`);
110
110
  }
111
111
  }
112
112
  return lines.join("\n");
113
113
  }
114
114
  // ---------------------------------------------------------------------------
115
+ // Viewport helpers
116
+ // ---------------------------------------------------------------------------
117
+ /**
118
+ * Flatten triage p0/p1/p2 buckets into a ViewportItem array.
119
+ *
120
+ * Order: p0_critical → p1_important → p2_nice_to_have (preserves API order within buckets).
121
+ * Severity mapping: p0 → "critical", p1 → "warning", p2 → "moderate".
122
+ */
123
+ export function buildViewportItems(data) {
124
+ const items = [];
125
+ for (const item of data.p0_critical) {
126
+ items.push({
127
+ id: item.id ?? item.title,
128
+ severity: "critical",
129
+ title: item.title,
130
+ effort: item.effort,
131
+ detail: item.reason,
132
+ });
133
+ }
134
+ for (const item of data.p1_important) {
135
+ items.push({
136
+ id: item.id ?? item.title,
137
+ severity: "warning",
138
+ title: item.title,
139
+ effort: item.effort,
140
+ detail: item.reason,
141
+ });
142
+ }
143
+ for (const item of data.p2_nice_to_have) {
144
+ items.push({
145
+ id: item.id ?? item.title,
146
+ severity: "moderate",
147
+ title: item.title,
148
+ effort: item.effort,
149
+ detail: item.reason,
150
+ });
151
+ }
152
+ return items;
153
+ }
154
+ export async function handleTriage(opts) {
155
+ const format = resolveCommandFormat("triage", opts.format, opts.machine || false);
156
+ const verbose = opts.verbose || false;
157
+ const config = { apiKey: opts.apiKey };
158
+ // Resolve fail-fast mode: --strict > --continue-on-error > auto-detect
159
+ let failFast;
160
+ if (opts.strict && opts.continueOnError) {
161
+ writeOutput(renderWarning({ message: "--strict and --continue-on-error both set — --strict takes precedence" }));
162
+ failFast = true;
163
+ }
164
+ else if (opts.strict) {
165
+ failFast = true;
166
+ }
167
+ else if (opts.continueOnError) {
168
+ failFast = false;
169
+ }
170
+ else {
171
+ failFast = isCI() || !isTTY();
172
+ }
173
+ // Resolve audit data
174
+ let auditPayload;
175
+ if (opts.job) {
176
+ // Fetch from API using SDK typed client
177
+ const sdkClient = createClient({ base: opts.base, apiKey: config.apiKey });
178
+ const result = await sdkClient.audits.retrieve(opts.job);
179
+ const issues = normalizeIssues(result.issues);
180
+ auditPayload = {
181
+ job_id: result.job_id || opts.job,
182
+ url: result.url || null,
183
+ scores: result.scores || null,
184
+ issues,
185
+ };
186
+ }
187
+ else {
188
+ // Read from stdin or --file
189
+ const input = await readJsonInput(opts.file);
190
+ if (!input) {
191
+ throw new Error("No audit data provided.\nUsage:\n vertaa audit https://example.com --json | vertaa triage\n vertaa triage --job <job-id>\n vertaa triage --file audit.json");
192
+ }
193
+ const data = input;
194
+ const innerData = (data.data && typeof data.data === "object" ? data.data : data);
195
+ const issues = normalizeIssues(innerData.issues);
196
+ auditPayload = {
197
+ job_id: innerData.job_id || null,
198
+ url: innerData.url || null,
199
+ scores: innerData.scores || null,
200
+ issues,
201
+ };
202
+ }
203
+ if (!Array.isArray(auditPayload.issues) ||
204
+ auditPayload.issues.length === 0) {
205
+ throw new Error("No issues found in audit data.");
206
+ }
207
+ // Auth check
208
+ const base = resolveApiBase(opts.base);
209
+ const apiKey = getApiKey(config.apiKey);
210
+ const ctx = { triageData: null };
211
+ const renderer = createRenderer("auto");
212
+ const baseState = {
213
+ phase: "triage",
214
+ phaseIndex: 1,
215
+ phaseTotal: 1,
216
+ url: auditPayload.url || "",
217
+ mode: "triage",
218
+ progress: {},
219
+ totals: {},
220
+ issueCount: 0,
221
+ scorePreview: null,
222
+ verbose: false,
223
+ elapsed: 0,
224
+ };
225
+ const steps = [
226
+ {
227
+ id: "triage",
228
+ actionText: strings.triage.run.action,
229
+ summaryText: strings.triage.run.done(0, 0, 0),
230
+ run: async () => {
231
+ const response = await Promise.race([
232
+ apiRequest(base, "/cli/ai/triage", { method: "POST", body: { audit: auditPayload } }, apiKey),
233
+ new Promise((_, reject) => setTimeout(() => reject(new Error("LLM request timed out")), AI_TIMEOUT_MS)),
234
+ ]);
235
+ ctx.triageData = response.data;
236
+ },
237
+ },
238
+ ];
239
+ const { success, states } = await runSteps(steps, {
240
+ failFast,
241
+ onStateChange: (stepStates) => {
242
+ renderer.update({ ...baseState, stepStates });
243
+ },
244
+ });
245
+ renderer.finish({
246
+ url: auditPayload.url || "",
247
+ mode: "triage",
248
+ overallScore: success ? 100 : 0,
249
+ scores: {},
250
+ issueCount: 0,
251
+ passed: success,
252
+ elapsed: 0,
253
+ });
254
+ if (!success) {
255
+ const failedStep = states.find((s) => s.status === "failed");
256
+ process.stderr.write(renderError({
257
+ message: strings.triage.errors.triageFailed(failedStep?.failReason ?? "unknown error"),
258
+ suggestion: "vertaa doctor",
259
+ }) + "\n");
260
+ process.exit(ExitCode.ERROR);
261
+ }
262
+ const triageData = ctx.triageData;
263
+ if (!triageData) {
264
+ process.exit(ExitCode.ERROR);
265
+ }
266
+ if (format === "json") {
267
+ writeJsonOutput(triageData, "triage");
268
+ }
269
+ else if (isTTY() && isStdinTTY() && !isCI()) {
270
+ // Interactive viewport navigation — arrow-key navigable list
271
+ const viewportItems = buildViewportItems(triageData);
272
+ if (viewportItems.length > 0) {
273
+ const viewport = new ScrollingViewport({ items: viewportItems });
274
+ await viewport.run();
275
+ }
276
+ else {
277
+ writeOutput(formatTriageHuman(triageData, verbose));
278
+ }
279
+ }
280
+ else {
281
+ // Non-TTY / CI / piped-stdin fallback: current text output
282
+ writeOutput(formatTriageHuman(triageData, verbose));
283
+ }
284
+ }
285
+ // ---------------------------------------------------------------------------
115
286
  // Command Registration
116
287
  // ---------------------------------------------------------------------------
117
288
  export function registerTriageCommand(program) {
@@ -121,6 +292,8 @@ export function registerTriageCommand(program) {
121
292
  .option("--job <job-id>", "Fetch audit data from a job ID")
122
293
  .option("--file <path>", "Load audit JSON from file")
123
294
  .option("-f, --format <format>", "Output format: json | human")
295
+ .option("--strict", "Fail immediately on first step error")
296
+ .option("--continue-on-error", "Continue on step errors even in CI")
124
297
  .addHelpText("after", `
125
298
  Examples:
126
299
  vertaa audit https://example.com --json | vertaa triage
@@ -133,72 +306,24 @@ Examples:
133
306
  const config = await resolveConfig(globalOpts.config);
134
307
  const machineMode = globalOpts.machine || false;
135
308
  const verbose = globalOpts.verbose || false;
136
- const format = resolveCommandFormat("triage", options.format, machineMode);
137
- // Resolve audit data
138
- let auditPayload;
139
- if (options.job) {
140
- // Fetch from API
141
- const base = resolveApiBase(globalOpts.base);
142
- const apiKey = getApiKey(config.apiKey);
143
- const result = await apiRequest(base, `/audit/${options.job}`, { method: "GET" }, apiKey);
144
- const issues = normalizeIssues(result.issues);
145
- auditPayload = {
146
- job_id: result.job_id || options.job,
147
- url: result.url || null,
148
- scores: result.scores || null,
149
- issues,
150
- };
151
- }
152
- else {
153
- // Read from stdin or --file
154
- const input = await readJsonInput(options.file);
155
- if (!input) {
156
- console.error("Error: No audit data provided.");
157
- console.error("Usage:");
158
- console.error(" vertaa audit https://example.com --json | vertaa triage");
159
- console.error(" vertaa triage --job <job-id>");
160
- console.error(" vertaa triage --file audit.json");
161
- process.exit(ExitCode.ERROR);
162
- }
163
- const data = input;
164
- const innerData = (data.data && typeof data.data === "object" ? data.data : data);
165
- const issues = normalizeIssues(innerData.issues);
166
- auditPayload = {
167
- job_id: innerData.job_id || null,
168
- url: innerData.url || null,
169
- scores: innerData.scores || null,
170
- issues,
171
- };
172
- }
173
- if (!Array.isArray(auditPayload.issues) ||
174
- auditPayload.issues.length === 0) {
175
- console.error("Error: No issues found in audit data.");
176
- process.exit(ExitCode.ERROR);
177
- }
178
- // Auth check
179
- const base = resolveApiBase(globalOpts.base);
180
- const apiKey = getApiKey(config.apiKey);
181
- // Call LLM triage API
182
- const spinner = createSpinner("Triaging findings...");
183
- try {
184
- const response = await Promise.race([
185
- apiRequest(base, "/cli/ai/triage", { method: "POST", body: { audit: auditPayload } }, apiKey),
186
- new Promise((_, reject) => setTimeout(() => reject(new Error("LLM request timed out")), AI_TIMEOUT_MS)),
187
- ]);
188
- succeedSpinner(spinner, "Triage complete");
189
- if (format === "json") {
190
- writeJsonOutput(response.data, "triage");
191
- }
192
- else {
193
- writeOutput(formatTriageHuman(response.data, verbose));
194
- }
195
- }
196
- catch (error) {
197
- handleAiCommandError(error, "triage", spinner);
198
- }
309
+ await handleTriage({
310
+ job: options.job,
311
+ file: options.file,
312
+ format: options.format,
313
+ strict: options.strict,
314
+ continueOnError: options.continueOnError,
315
+ machine: machineMode,
316
+ verbose,
317
+ base: globalOpts.base,
318
+ apiKey: config.apiKey,
319
+ });
199
320
  }
200
321
  catch (error) {
201
- console.error("Error:", error instanceof Error ? error.message : String(error));
322
+ process.stderr.write(renderError({
323
+ message: error instanceof Error ? error.message : String(error),
324
+ suggestion: "vertaa doctor",
325
+ exitCode: ExitCode.ERROR,
326
+ }) + "\n");
202
327
  process.exit(ExitCode.ERROR);
203
328
  }
204
329
  });
@@ -5,6 +5,15 @@
5
5
  * Enables sharing results across team members and CI runs.
6
6
  */
7
7
  import { Command } from "commander";
8
+ /**
9
+ * Handle the upload command.
10
+ */
11
+ export declare function handleUpload(jobId: string | undefined, options: {
12
+ baseline?: boolean;
13
+ project?: string;
14
+ base?: string;
15
+ configPath?: string;
16
+ }): Promise<void>;
8
17
  /**
9
18
  * Register the upload command with the Commander program.
10
19
  */
@@ -1 +1 @@
1
- {"version":3,"file":"upload.d.ts","sourceRoot":"","sources":["../../src/commands/upload.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA0LpC;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAiB5D"}
1
+ {"version":3,"file":"upload.d.ts","sourceRoot":"","sources":["../../src/commands/upload.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA2CpC;;GAEG;AACH,wBAAsB,YAAY,CAChC,KAAK,EAAE,MAAM,GAAG,SAAS,EACzB,OAAO,EAAE;IACP,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,GACA,OAAO,CAAC,IAAI,CAAC,CAwKf;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA4B5D"}
@@ -6,14 +6,15 @@
6
6
  */
7
7
  import fs from "fs";
8
8
  import path from "path";
9
- import chalk from "chalk";
10
- import { createSpinner, succeedSpinner, failSpinner } from "../ui/spinner.js";
9
+ import { colorize, brand, renderError, runSteps, createRenderer } from "@vertaaux/tui";
11
10
  import { loadToken } from "../auth/token-store.js";
12
11
  import { getCIToken } from "../auth/ci-token.js";
13
- import { resolveApiBase } from "../utils/client.js";
12
+ import { apiRequest, resolveApiBase } from "../utils/client.js";
14
13
  import { resolveConfig } from "../config/loader.js";
15
14
  import { loadBaseline, DEFAULT_BASELINE_PATH } from "../baseline/manager.js";
16
15
  import { ExitCode } from "../utils/exit-codes.js";
16
+ import { writeOutput } from "../output/envelope.js";
17
+ import { strings } from "../ui/strings.js";
17
18
  /**
18
19
  * Artifacts directory.
19
20
  */
@@ -33,113 +34,141 @@ async function getAuthToken() {
33
34
  /**
34
35
  * Handle the upload command.
35
36
  */
36
- async function handleUpload(jobId, options) {
37
+ export async function handleUpload(jobId, options) {
37
38
  // Get auth token
38
39
  const token = await getAuthToken();
39
40
  if (!token) {
40
- console.error(chalk.red("Error: Not authenticated."));
41
- console.error("Run `vertaa login` to authenticate or set VERTAAUX_TOKEN environment variable.");
41
+ process.stderr.write(renderError({
42
+ message: strings.upload.errors.notAuthenticated,
43
+ suggestion: "vertaa login",
44
+ exitCode: ExitCode.ERROR,
45
+ }) + "\n");
42
46
  process.exit(ExitCode.ERROR);
43
47
  }
44
48
  // Load config for API base (supports --config global option)
45
- const config = await resolveConfig(options.configPath);
49
+ await resolveConfig(options.configPath);
46
50
  const apiBase = resolveApiBase(options.base);
47
- // Determine what to upload
48
- const spinner = createSpinner("Preparing upload...");
49
- spinner.start();
50
- try {
51
- // If no job ID, find the most recent local result
52
- let targetJobId = jobId;
53
- let localResultPath;
54
- if (!targetJobId) {
55
- // Look for recent results in artifacts directory
56
- const artifactsPath = path.resolve(process.cwd(), ARTIFACTS_DIR);
57
- if (fs.existsSync(artifactsPath)) {
58
- const dirs = fs.readdirSync(artifactsPath).filter((d) => {
59
- const stat = fs.statSync(path.join(artifactsPath, d));
60
- return stat.isDirectory();
61
- });
62
- if (dirs.length > 0) {
63
- // Get most recent by mtime
64
- const sorted = dirs
65
- .map((d) => ({
66
- name: d,
67
- mtime: fs.statSync(path.join(artifactsPath, d)).mtime,
68
- }))
69
- .sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
70
- targetJobId = sorted[0].name;
71
- localResultPath = path.join(artifactsPath, targetJobId);
51
+ const renderer = createRenderer("auto");
52
+ const baseState = {
53
+ phase: "upload",
54
+ phaseIndex: 1,
55
+ phaseTotal: 1,
56
+ url: "",
57
+ mode: "upload",
58
+ progress: {},
59
+ totals: {},
60
+ issueCount: 0,
61
+ scorePreview: null,
62
+ verbose: false,
63
+ elapsed: 0,
64
+ };
65
+ const startTime = Date.now();
66
+ const ctx = {
67
+ uploadResult: null,
68
+ artifactCount: 1,
69
+ };
70
+ const steps = [
71
+ {
72
+ id: "read",
73
+ actionText: strings.upload.run.action,
74
+ summaryText: "File loaded",
75
+ run: async () => {
76
+ // If no job ID, find the most recent local result
77
+ let targetJobId = jobId;
78
+ let localResultPath;
79
+ if (!targetJobId) {
80
+ // Look for recent results in artifacts directory
81
+ const artifactsPath = path.resolve(process.cwd(), ARTIFACTS_DIR);
82
+ if (fs.existsSync(artifactsPath)) {
83
+ const dirs = fs.readdirSync(artifactsPath).filter((d) => {
84
+ const stat = fs.statSync(path.join(artifactsPath, d));
85
+ return stat.isDirectory();
86
+ });
87
+ if (dirs.length > 0) {
88
+ // Get most recent by mtime
89
+ const sorted = dirs
90
+ .map((d) => ({
91
+ name: d,
92
+ mtime: fs.statSync(path.join(artifactsPath, d)).mtime,
93
+ }))
94
+ .sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
95
+ targetJobId = sorted[0].name;
96
+ localResultPath = path.join(artifactsPath, targetJobId);
97
+ }
98
+ }
99
+ if (!targetJobId) {
100
+ throw new Error(strings.upload.noJobId);
101
+ }
72
102
  }
73
- }
74
- if (!targetJobId) {
75
- failSpinner(spinner, "No job ID provided and no local results found.");
76
- console.error("Run `vertaa audit --save-trace` to save local results first.");
77
- process.exit(ExitCode.ERROR);
78
- }
79
- }
80
- spinner.setText(`Uploading audit ${targetJobId}...`);
81
- // Prepare upload payload
82
- const payload = {
83
- job_id: targetJobId,
84
- project: options.project || path.basename(process.cwd()),
85
- };
86
- // Add local results if available
87
- if (localResultPath && fs.existsSync(localResultPath)) {
88
- const files = fs.readdirSync(localResultPath);
89
- const artifacts = {};
90
- for (const file of files) {
91
- const filePath = path.join(localResultPath, file);
92
- const stat = fs.statSync(filePath);
93
- // Skip large files (> 10MB)
94
- if (stat.size > 10 * 1024 * 1024) {
95
- continue;
103
+ // Prepare upload payload
104
+ const payload = {
105
+ job_id: targetJobId,
106
+ project: options.project || path.basename(process.cwd()),
107
+ };
108
+ // Add local results if available
109
+ if (localResultPath && fs.existsSync(localResultPath)) {
110
+ const files = fs.readdirSync(localResultPath);
111
+ const artifacts = {};
112
+ for (const file of files) {
113
+ const filePath = path.join(localResultPath, file);
114
+ const stat = fs.statSync(filePath);
115
+ // Skip large files (> 10MB)
116
+ if (stat.size > 10 * 1024 * 1024) {
117
+ continue;
118
+ }
119
+ // Read file content (base64 for binary)
120
+ const content = fs.readFileSync(filePath);
121
+ const ext = path.extname(file).toLowerCase();
122
+ const isBinary = [".png", ".jpg", ".jpeg", ".gif", ".zip"].includes(ext);
123
+ artifacts[file] = isBinary
124
+ ? content.toString("base64")
125
+ : content.toString("utf-8");
126
+ }
127
+ payload.artifacts = artifacts;
128
+ ctx.artifactCount = Object.keys(artifacts).length;
96
129
  }
97
- // Read file content (base64 for binary)
98
- const content = fs.readFileSync(filePath);
99
- const ext = path.extname(file).toLowerCase();
100
- const isBinary = [".png", ".jpg", ".jpeg", ".gif", ".zip"].includes(ext);
101
- artifacts[file] = isBinary
102
- ? content.toString("base64")
103
- : content.toString("utf-8");
104
- }
105
- payload.artifacts = artifacts;
106
- }
107
- // Upload baseline if requested
108
- if (options.baseline) {
109
- const baseline = await loadBaseline(DEFAULT_BASELINE_PATH);
110
- if (baseline) {
111
- payload.baseline = baseline;
112
- spinner.setText(`Uploading audit ${targetJobId} with baseline...`);
113
- }
114
- }
115
- // Make API request
116
- const response = await fetch(`${apiBase}/sync/upload`, {
117
- method: "POST",
118
- headers: {
119
- "Content-Type": "application/json",
120
- "X-API-Key": token,
130
+ // Upload baseline if requested
131
+ if (options.baseline) {
132
+ const baseline = await loadBaseline(DEFAULT_BASELINE_PATH);
133
+ if (baseline) {
134
+ payload.baseline = baseline;
135
+ }
136
+ }
137
+ // Make API request via apiRequest() — auth header constructed in client.ts only
138
+ const result = await apiRequest(apiBase, "/sync/upload", { method: "POST", body: payload }, token);
139
+ if (!result.success) {
140
+ throw new Error(result.error?.message || "Upload failed");
141
+ }
142
+ ctx.uploadResult = result;
121
143
  },
122
- body: JSON.stringify(payload),
123
- });
124
- if (!response.ok) {
125
- const error = await response.json().catch(() => ({ error: { message: response.statusText } }));
126
- throw new Error(error.error?.message || `HTTP ${response.status}`);
127
- }
128
- const result = (await response.json());
129
- if (!result.success) {
130
- throw new Error(result.error?.message || "Upload failed");
131
- }
132
- succeedSpinner(spinner, "Upload complete!");
133
- console.error("");
134
- console.error(` Job ID: ${result.job_id}`);
135
- console.error(` URL: ${chalk.cyan(result.url)}`);
136
- console.error("");
137
- console.error("Share this URL with your team to view the results.");
144
+ },
145
+ ];
146
+ const { success, states } = await runSteps(steps, {
147
+ failFast: true,
148
+ onStateChange: (stepStates) => {
149
+ renderer.update({ ...baseState, stepStates, elapsed: Date.now() - startTime });
150
+ },
151
+ });
152
+ renderer.finish({ url: "", mode: "upload", overallScore: 0, scores: {}, issueCount: 0, passed: success, elapsed: Date.now() - startTime });
153
+ if (!success) {
154
+ const failed = states.find(s => s.status === "failed");
155
+ process.stderr.write(renderError({
156
+ message: failed?.failReason || "Command failed",
157
+ suggestion: "vertaa doctor",
158
+ }) + "\n");
159
+ process.exitCode = ExitCode.ERROR;
138
160
  }
139
- catch (error) {
140
- failSpinner(spinner, "Upload failed");
141
- console.error(chalk.red("Error:"), error instanceof Error ? error.message : String(error));
142
- process.exit(ExitCode.ERROR);
161
+ const { uploadResult, artifactCount } = ctx;
162
+ if (uploadResult) {
163
+ const lines = [
164
+ strings.upload.run.done(artifactCount),
165
+ "",
166
+ ` Job ID: ${uploadResult.job_id}`,
167
+ ` URL: ${colorize(uploadResult.url, brand.cyan)}`,
168
+ "",
169
+ strings.upload.shareUrl,
170
+ ];
171
+ writeOutput(lines.join("\n"));
143
172
  }
144
173
  }
145
174
  /**
@@ -153,7 +182,17 @@ export function registerUploadCommand(program) {
153
182
  .option("--project <name>", "Cloud project name (default: current directory name)")
154
183
  .option("-b, --base <url>", "API base URL")
155
184
  .action(async (jobId, options, command) => {
156
- const globalOpts = command.optsWithGlobals();
157
- await handleUpload(jobId, { ...options, configPath: globalOpts.config });
185
+ try {
186
+ const globalOpts = command.optsWithGlobals();
187
+ await handleUpload(jobId, { ...options, configPath: globalOpts.config });
188
+ }
189
+ catch (error) {
190
+ process.stderr.write(renderError({
191
+ message: error instanceof Error ? error.message : String(error),
192
+ suggestion: "vertaa doctor",
193
+ exitCode: ExitCode.ERROR,
194
+ }) + "\n");
195
+ process.exit(ExitCode.ERROR);
196
+ }
158
197
  });
159
198
  }