@vertaaux/cli 0.2.2 → 0.3.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 (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +58 -2
  3. package/dist/auth/device-flow.d.ts.map +1 -1
  4. package/dist/auth/device-flow.js +46 -14
  5. package/dist/commands/audit.d.ts +2 -0
  6. package/dist/commands/audit.d.ts.map +1 -1
  7. package/dist/commands/audit.js +167 -8
  8. package/dist/commands/client.d.ts +14 -0
  9. package/dist/commands/client.d.ts.map +1 -0
  10. package/dist/commands/client.js +362 -0
  11. package/dist/commands/compare.d.ts +20 -0
  12. package/dist/commands/compare.d.ts.map +1 -0
  13. package/dist/commands/compare.js +335 -0
  14. package/dist/commands/doc.d.ts +18 -0
  15. package/dist/commands/doc.d.ts.map +1 -0
  16. package/dist/commands/doc.js +161 -0
  17. package/dist/commands/download.d.ts.map +1 -1
  18. package/dist/commands/download.js +9 -8
  19. package/dist/commands/drift.d.ts +15 -0
  20. package/dist/commands/drift.d.ts.map +1 -0
  21. package/dist/commands/drift.js +309 -0
  22. package/dist/commands/explain.d.ts +14 -33
  23. package/dist/commands/explain.d.ts.map +1 -1
  24. package/dist/commands/explain.js +277 -179
  25. package/dist/commands/fix-plan.d.ts +15 -0
  26. package/dist/commands/fix-plan.d.ts.map +1 -0
  27. package/dist/commands/fix-plan.js +182 -0
  28. package/dist/commands/patch-review.d.ts +14 -0
  29. package/dist/commands/patch-review.d.ts.map +1 -0
  30. package/dist/commands/patch-review.js +200 -0
  31. package/dist/commands/protect.d.ts +16 -0
  32. package/dist/commands/protect.d.ts.map +1 -0
  33. package/dist/commands/protect.js +323 -0
  34. package/dist/commands/release-notes.d.ts +17 -0
  35. package/dist/commands/release-notes.d.ts.map +1 -0
  36. package/dist/commands/release-notes.js +145 -0
  37. package/dist/commands/report.d.ts +15 -0
  38. package/dist/commands/report.d.ts.map +1 -0
  39. package/dist/commands/report.js +214 -0
  40. package/dist/commands/suggest.d.ts +18 -0
  41. package/dist/commands/suggest.d.ts.map +1 -0
  42. package/dist/commands/suggest.js +152 -0
  43. package/dist/commands/triage.d.ts +17 -0
  44. package/dist/commands/triage.d.ts.map +1 -0
  45. package/dist/commands/triage.js +205 -0
  46. package/dist/commands/upload.d.ts.map +1 -1
  47. package/dist/commands/upload.js +8 -7
  48. package/dist/index.js +62 -25
  49. package/dist/output/formats.d.ts.map +1 -1
  50. package/dist/output/formats.js +18 -2
  51. package/dist/output/human.d.ts +1 -10
  52. package/dist/output/human.d.ts.map +1 -1
  53. package/dist/output/human.js +26 -98
  54. package/dist/policy/sync.d.ts +67 -0
  55. package/dist/policy/sync.d.ts.map +1 -0
  56. package/dist/policy/sync.js +147 -0
  57. package/dist/prompts/command-catalog.d.ts +46 -0
  58. package/dist/prompts/command-catalog.d.ts.map +1 -0
  59. package/dist/prompts/command-catalog.js +187 -0
  60. package/dist/ui/spinner.d.ts +10 -35
  61. package/dist/ui/spinner.d.ts.map +1 -1
  62. package/dist/ui/spinner.js +11 -58
  63. package/dist/ui/table.d.ts +1 -18
  64. package/dist/ui/table.d.ts.map +1 -1
  65. package/dist/ui/table.js +56 -163
  66. package/dist/utils/ai-error.d.ts +48 -0
  67. package/dist/utils/ai-error.d.ts.map +1 -0
  68. package/dist/utils/ai-error.js +190 -0
  69. package/dist/utils/detect-env.d.ts +6 -8
  70. package/dist/utils/detect-env.d.ts.map +1 -1
  71. package/dist/utils/detect-env.js +6 -25
  72. package/dist/utils/stdin.d.ts +50 -0
  73. package/dist/utils/stdin.d.ts.map +1 -0
  74. package/dist/utils/stdin.js +93 -0
  75. package/package.json +11 -7
package/dist/ui/table.js CHANGED
@@ -1,71 +1,18 @@
1
1
  /**
2
2
  * Table formatting utilities for CLI output.
3
3
  *
4
- * Uses cli-table3 for clean table rendering with severity-based coloring.
4
+ * Delegates to @vertaaux/tui for consistent rendering with severity-based coloring.
5
5
  * Respects terminal width and provides truncation for long content.
6
6
  */
7
- import Table from "cli-table3";
8
- import chalk from "chalk";
9
- import { shouldUseColor, getTerminalWidth } from "../utils/detect-env.js";
10
- // Severity colors matching brand palette
11
- const SEVERITY_COLORS = {
12
- critical: "#FF6B6B", // Red
13
- error: "#FF6B6B", // Red (alias)
14
- serious: "#FFD93D", // Yellow
15
- warning: "#FFD93D", // Yellow (alias)
16
- minor: "#6BCEFF", // Cyan
17
- moderate: "#6BCEFF", // Cyan (alias)
18
- info: "#888888", // Dim gray
19
- };
20
- // Severity display labels
21
- const SEVERITY_LABELS = {
22
- critical: "CRITICAL",
23
- error: "ERROR",
24
- serious: "SERIOUS",
25
- warning: "WARNING",
26
- minor: "MINOR",
27
- moderate: "MODERATE",
28
- info: "INFO",
29
- };
30
- // Severity sort order (highest first)
31
- const SEVERITY_ORDER = {
32
- critical: 0,
33
- error: 1,
34
- serious: 2,
35
- warning: 3,
36
- minor: 4,
37
- moderate: 5,
38
- info: 6,
39
- };
40
- /**
41
- * Truncate text to fit within max width, adding ellipsis if truncated.
42
- */
43
- function truncate(text, maxWidth) {
44
- if (!text)
45
- return "";
46
- if (text.length <= maxWidth)
47
- return text;
48
- return text.slice(0, maxWidth - 3) + "...";
49
- }
50
- /**
51
- * Get colored severity label.
52
- */
53
- function colorSeverity(severity) {
54
- const normalized = severity.toLowerCase();
55
- const label = SEVERITY_LABELS[normalized] || severity.toUpperCase();
56
- if (!shouldUseColor()) {
57
- return label;
58
- }
59
- const color = SEVERITY_COLORS[normalized] || "#888888";
60
- return chalk.hex(color).bold(label);
61
- }
7
+ import { renderTable, text, } from "@vertaaux/tui";
8
+ import { severityOrder, isColorEnabled, colorize, severity as severityPalette, tokens, } from "@vertaaux/tui";
62
9
  /**
63
10
  * Sort issues by severity (highest first).
64
11
  */
65
12
  function sortBySeverity(issues) {
66
13
  return [...issues].sort((a, b) => {
67
- const orderA = SEVERITY_ORDER[a.severity?.toLowerCase() || "info"] ?? 6;
68
- const orderB = SEVERITY_ORDER[b.severity?.toLowerCase() || "info"] ?? 6;
14
+ const orderA = severityOrder[a.severity?.toLowerCase() || "info"] ?? 6;
15
+ const orderB = severityOrder[b.severity?.toLowerCase() || "info"] ?? 6;
69
16
  return orderA - orderB;
70
17
  });
71
18
  }
@@ -75,10 +22,10 @@ function sortBySeverity(issues) {
75
22
  export function groupBySeverity(issues) {
76
23
  const groups = new Map();
77
24
  for (const issue of issues) {
78
- const severity = issue.severity?.toLowerCase() || "info";
79
- const group = groups.get(severity) || [];
25
+ const sev = issue.severity?.toLowerCase() || "info";
26
+ const group = groups.get(sev) || [];
80
27
  group.push(issue);
81
- groups.set(severity, group);
28
+ groups.set(sev, group);
82
29
  }
83
30
  return groups;
84
31
  }
@@ -88,149 +35,95 @@ export function groupBySeverity(issues) {
88
35
  export function groupByCategory(issues) {
89
36
  const groups = new Map();
90
37
  for (const issue of issues) {
91
- const category = issue.category || "Uncategorized";
92
- const group = groups.get(category) || [];
38
+ const cat = issue.category || "Uncategorized";
39
+ const group = groups.get(cat) || [];
93
40
  group.push(issue);
94
- groups.set(category, group);
41
+ groups.set(cat, group);
95
42
  }
96
43
  return groups;
97
44
  }
98
45
  /**
99
46
  * Format issues into a table string.
100
- *
101
- * Columns: Severity | ID | Description | Selector (optional)
102
- * Issues are sorted by severity (critical -> info).
103
- *
104
- * @param issues - Array of issues to format
105
- * @param options - Formatting options
106
- * @returns Formatted table string
107
47
  */
108
48
  export function formatIssuesTable(issues, options = {}) {
109
49
  if (!issues.length) {
110
- return shouldUseColor()
111
- ? chalk.green("No issues found.")
50
+ return isColorEnabled()
51
+ ? text.success("No issues found.")
112
52
  : "No issues found.";
113
53
  }
114
- const termWidth = getTerminalWidth();
115
- const { maxDescriptionWidth = Math.min(50, Math.floor(termWidth * 0.4)), maxSelectorWidth = Math.min(30, Math.floor(termWidth * 0.2)), showSelector = true, showCategory = false, } = options;
116
- // Calculate column widths
117
- const severityWidth = 10;
118
- const idWidth = 20;
119
- // Build header
120
- const header = ["Severity", "ID", "Description"];
121
- const colWidths = [severityWidth, idWidth, maxDescriptionWidth];
54
+ const { maxDescriptionWidth = 50, maxSelectorWidth = 30, showSelector = true, showCategory = false, } = options;
55
+ const sorted = sortBySeverity(issues);
56
+ const columns = [
57
+ { key: "severity", label: "Severity", width: 10, color: (val) => text.severityBadge(val) },
58
+ { key: "id", label: "ID", width: 20 },
59
+ { key: "description", label: "Description", width: maxDescriptionWidth },
60
+ ];
122
61
  if (showCategory) {
123
- header.push("Category");
124
- colWidths.push(15);
62
+ columns.push({ key: "category", label: "Category", width: 15 });
125
63
  }
126
64
  if (showSelector) {
127
- header.push("Selector");
128
- colWidths.push(maxSelectorWidth);
65
+ columns.push({ key: "selector", label: "Selector", width: maxSelectorWidth });
129
66
  }
130
- const useColor = shouldUseColor();
131
- const table = new Table({
132
- head: useColor
133
- ? header.map((h) => chalk.bold(h))
134
- : header,
135
- colWidths,
136
- style: {
137
- head: useColor ? ["cyan"] : [],
138
- border: useColor ? ["dim"] : [],
139
- },
140
- wordWrap: true,
141
- });
142
- // Sort and add rows
143
- const sortedIssues = sortBySeverity(issues);
144
- for (const issue of sortedIssues) {
145
- const row = [
146
- colorSeverity(issue.severity || "info"),
147
- truncate(issue.id || "-", idWidth - 2),
148
- truncate(issue.description || issue.title || "-", maxDescriptionWidth - 2),
149
- ];
150
- if (showCategory) {
151
- row.push(truncate(issue.category || "-", 13));
152
- }
153
- if (showSelector) {
154
- row.push(truncate(issue.selector || "-", maxSelectorWidth - 2));
155
- }
156
- table.push(row);
157
- }
158
- return table.toString();
67
+ const rows = sorted.map((issue) => ({
68
+ severity: issue.severity?.toLowerCase() || "info",
69
+ id: issue.id || "-",
70
+ description: issue.description || issue.title || "-",
71
+ category: issue.category || "-",
72
+ selector: issue.selector || "-",
73
+ }));
74
+ return renderTable(rows, columns);
159
75
  }
160
76
  /**
161
77
  * Format scores into a table string.
162
- *
163
- * @param scores - Object mapping category names to scores
164
- * @returns Formatted table string
165
78
  */
166
79
  export function formatScoresTable(scores) {
167
80
  const entries = Object.entries(scores).filter(([, value]) => typeof value === "number");
168
81
  if (!entries.length) {
169
- return shouldUseColor()
170
- ? chalk.dim("No scores available.")
82
+ return isColorEnabled()
83
+ ? text.dim("No scores available.")
171
84
  : "No scores available.";
172
85
  }
173
- const useColor = shouldUseColor();
174
- const table = new Table({
175
- head: useColor
176
- ? [chalk.bold("Category"), chalk.bold("Score")]
177
- : ["Category", "Score"],
178
- colWidths: [20, 10],
179
- style: {
180
- head: useColor ? ["cyan"] : [],
181
- border: useColor ? ["dim"] : [],
86
+ const columns = [
87
+ { key: "category", label: "Category", width: 20 },
88
+ {
89
+ key: "score",
90
+ label: "Score",
91
+ width: 10,
92
+ color: (val) => text.scoreBadge(Number(val)),
182
93
  },
183
- });
184
- for (const [category, score] of entries) {
185
- const numScore = score;
186
- let scoreStr = String(numScore);
187
- if (useColor) {
188
- if (numScore >= 90) {
189
- scoreStr = chalk.green.bold(scoreStr);
190
- }
191
- else if (numScore >= 70) {
192
- scoreStr = chalk.yellow(scoreStr);
193
- }
194
- else {
195
- scoreStr = chalk.red(scoreStr);
196
- }
197
- }
198
- table.push([category, scoreStr]);
199
- }
200
- return table.toString();
94
+ ];
95
+ const rows = entries.map(([category, score]) => ({
96
+ category,
97
+ score: String(score),
98
+ }));
99
+ return renderTable(rows, columns);
201
100
  }
202
101
  /**
203
102
  * Format a summary line showing issue counts by severity.
204
- *
205
- * @param issues - Array of issues
206
- * @returns Summary string like "3 critical, 5 serious, 12 minor issues"
207
103
  */
208
104
  export function formatIssueSummary(issues) {
209
105
  const counts = {};
210
106
  for (const issue of issues) {
211
- const severity = issue.severity?.toLowerCase() || "info";
212
- counts[severity] = (counts[severity] || 0) + 1;
107
+ const sev = issue.severity?.toLowerCase() || "info";
108
+ counts[sev] = (counts[sev] || 0) + 1;
213
109
  }
214
- const useColor = shouldUseColor();
215
- const parts = [];
216
- // Order: critical, serious, minor, info
217
110
  const order = ["critical", "error", "serious", "warning", "minor", "moderate", "info"];
218
- for (const severity of order) {
219
- const count = counts[severity];
111
+ const parts = [];
112
+ for (const sev of order) {
113
+ const count = counts[sev];
220
114
  if (count) {
221
- const label = count === 1 ? severity : severity;
222
- const text = `${count} ${label}`;
223
- if (useColor) {
224
- const color = SEVERITY_COLORS[severity] || "#888888";
225
- parts.push(chalk.hex(color)(text));
115
+ const label = `${count} ${sev}`;
116
+ if (isColorEnabled()) {
117
+ const hex = severityPalette[sev] || tokens.muted;
118
+ parts.push(colorize(label, hex));
226
119
  }
227
120
  else {
228
- parts.push(text);
121
+ parts.push(label);
229
122
  }
230
123
  }
231
124
  }
232
125
  if (!parts.length) {
233
- return useColor ? chalk.green("No issues") : "No issues";
126
+ return isColorEnabled() ? text.success("No issues") : "No issues";
234
127
  }
235
128
  return parts.join(", ") + " issues";
236
129
  }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Centralized AI error classification and formatting for CLI commands.
3
+ *
4
+ * Provides consistent error handling across all 8 AI-powered commands.
5
+ * Classifies errors into distinct types (timeout, unreachable, auth, etc.)
6
+ * and formats user-friendly messages with actionable suggestions.
7
+ */
8
+ import { type SpinnerInstance } from "../ui/spinner.js";
9
+ /** Default timeout for LLM API requests (30 seconds). */
10
+ export declare const AI_TIMEOUT_MS = 30000;
11
+ /**
12
+ * Classification of AI/LLM errors.
13
+ *
14
+ * Each kind maps to a distinct user-facing message with tailored suggestions.
15
+ */
16
+ export type AiErrorKind = "timeout" | "unreachable" | "auth" | "invalid_response" | "rate_limit" | "server_error" | "unknown";
17
+ /**
18
+ * Classify an unknown error into a specific AI error kind.
19
+ *
20
+ * Inspects error properties (name, code, message, status) to determine
21
+ * the most specific classification. Falls back to `"unknown"` when no
22
+ * pattern matches.
23
+ *
24
+ * @param error - The caught error (any type)
25
+ * @returns The classified error kind
26
+ */
27
+ export declare function classifyAiError(error: unknown): AiErrorKind;
28
+ /**
29
+ * Format an AI error kind into a user-friendly message with suggestions.
30
+ *
31
+ * @param kind - The classified error kind
32
+ * @param command - The CLI command name (e.g., "explain", "triage")
33
+ * @returns Multi-line string with error description and actionable suggestions
34
+ */
35
+ export declare function formatAiError(kind: AiErrorKind, command: string): string;
36
+ /**
37
+ * Handle an AI command error: classify, format, display, and exit.
38
+ *
39
+ * This is the primary entry point for error handling in AI commands.
40
+ * It classifies the error, fails the spinner with a summary, writes
41
+ * the full formatted message to stderr, and exits with ExitCode.ERROR.
42
+ *
43
+ * @param error - The caught error
44
+ * @param command - The CLI command name (e.g., "explain", "triage")
45
+ * @param spinner - Active spinner instance to fail
46
+ */
47
+ export declare function handleAiCommandError(error: unknown, command: string, spinner: SpinnerInstance): never;
48
+ //# sourceMappingURL=ai-error.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ai-error.d.ts","sourceRoot":"","sources":["../../src/utils/ai-error.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EAAe,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAMrE,yDAAyD;AACzD,eAAO,MAAM,aAAa,QAAS,CAAC;AAMpC;;;;GAIG;AACH,MAAM,MAAM,WAAW,GACnB,SAAS,GACT,aAAa,GACb,MAAM,GACN,kBAAkB,GAClB,YAAY,GACZ,cAAc,GACd,SAAS,CAAC;AAMd;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,WAAW,CAoF3D;AAeD;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CA2DxE;AAMD;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,OAAO,EACd,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,eAAe,GACvB,KAAK,CAQP"}
@@ -0,0 +1,190 @@
1
+ /**
2
+ * Centralized AI error classification and formatting for CLI commands.
3
+ *
4
+ * Provides consistent error handling across all 8 AI-powered commands.
5
+ * Classifies errors into distinct types (timeout, unreachable, auth, etc.)
6
+ * and formats user-friendly messages with actionable suggestions.
7
+ */
8
+ import { ExitCode } from "./exit-codes.js";
9
+ import { failSpinner } from "../ui/spinner.js";
10
+ // ---------------------------------------------------------------------------
11
+ // Constants
12
+ // ---------------------------------------------------------------------------
13
+ /** Default timeout for LLM API requests (30 seconds). */
14
+ export const AI_TIMEOUT_MS = 30_000;
15
+ // ---------------------------------------------------------------------------
16
+ // Classification
17
+ // ---------------------------------------------------------------------------
18
+ /**
19
+ * Classify an unknown error into a specific AI error kind.
20
+ *
21
+ * Inspects error properties (name, code, message, status) to determine
22
+ * the most specific classification. Falls back to `"unknown"` when no
23
+ * pattern matches.
24
+ *
25
+ * @param error - The caught error (any type)
26
+ * @returns The classified error kind
27
+ */
28
+ export function classifyAiError(error) {
29
+ if (!error)
30
+ return "unknown";
31
+ const err = error;
32
+ const name = typeof err.name === "string" ? err.name : "";
33
+ const code = typeof err.code === "string" ? err.code : "";
34
+ const message = error instanceof Error
35
+ ? error.message.toLowerCase()
36
+ : typeof error === "string"
37
+ ? error.toLowerCase()
38
+ : "";
39
+ const status = typeof err.status === "number"
40
+ ? err.status
41
+ : typeof err.statusCode === "number"
42
+ ? err.statusCode
43
+ : extractHttpStatus(message);
44
+ // Timeout indicators
45
+ if (name === "AbortError" ||
46
+ code === "ETIMEDOUT" ||
47
+ code === "TIMEOUT" ||
48
+ code === "UND_ERR_CONNECT_TIMEOUT" ||
49
+ message.includes("timeout") ||
50
+ message.includes("timed out") ||
51
+ message.includes("aborted")) {
52
+ return "timeout";
53
+ }
54
+ // Unreachable / network errors
55
+ if (code === "ECONNREFUSED" ||
56
+ code === "ENOTFOUND" ||
57
+ code === "ECONNRESET" ||
58
+ code === "FETCH_ERROR" ||
59
+ code === "UND_ERR_SOCKET" ||
60
+ message.includes("fetch failed") ||
61
+ message.includes("network error") ||
62
+ message.includes("dns resolution") ||
63
+ name === "TypeError" && message.includes("fetch")) {
64
+ return "unreachable";
65
+ }
66
+ // HTTP status-based classification
67
+ if (status !== null) {
68
+ if (status === 401 || status === 403)
69
+ return "auth";
70
+ if (status === 429)
71
+ return "rate_limit";
72
+ if (status >= 500)
73
+ return "server_error";
74
+ }
75
+ // Auth keywords in message
76
+ if (message.includes("unauthorized") ||
77
+ message.includes("authentication required") ||
78
+ message.includes("invalid api key") ||
79
+ message.includes("forbidden")) {
80
+ return "auth";
81
+ }
82
+ // Rate limit keywords
83
+ if (message.includes("rate limit") ||
84
+ message.includes("too many requests")) {
85
+ return "rate_limit";
86
+ }
87
+ // Invalid response / schema mismatch
88
+ if (message.includes("unexpected token") ||
89
+ message.includes("invalid json") ||
90
+ message.includes("unexpected response") ||
91
+ message.includes("schema") ||
92
+ message.includes("validation failed")) {
93
+ return "invalid_response";
94
+ }
95
+ return "unknown";
96
+ }
97
+ /**
98
+ * Extract an HTTP status code from an error message like "HTTP 401: Unauthorized".
99
+ */
100
+ function extractHttpStatus(message) {
101
+ const match = message.match(/\bhttp\s+(\d{3})\b/i);
102
+ if (match)
103
+ return parseInt(match[1], 10);
104
+ return null;
105
+ }
106
+ // ---------------------------------------------------------------------------
107
+ // Formatting
108
+ // ---------------------------------------------------------------------------
109
+ /**
110
+ * Format an AI error kind into a user-friendly message with suggestions.
111
+ *
112
+ * @param kind - The classified error kind
113
+ * @param command - The CLI command name (e.g., "explain", "triage")
114
+ * @returns Multi-line string with error description and actionable suggestions
115
+ */
116
+ export function formatAiError(kind, command) {
117
+ switch (kind) {
118
+ case "timeout":
119
+ return [
120
+ "LLM request timed out after 30 seconds.",
121
+ "",
122
+ " Suggestions:",
123
+ " - Try again (transient server load)",
124
+ " - Use a simpler audit (fewer issues = faster response)",
125
+ " - Check service status at https://status.vertaaux.ai",
126
+ ].join("\n");
127
+ case "unreachable":
128
+ return [
129
+ "Could not reach the VertaaUX API.",
130
+ "",
131
+ " Suggestions:",
132
+ " - Check your internet connection",
133
+ " - Verify VERTAAUX_API_BASE is correct",
134
+ " - Try: vertaa doctor",
135
+ ].join("\n");
136
+ case "auth":
137
+ return [
138
+ "Authentication required for AI commands.",
139
+ "",
140
+ " Run: vertaa login",
141
+ " Or set: VERTAAUX_API_KEY=<key>",
142
+ ].join("\n");
143
+ case "invalid_response":
144
+ return [
145
+ "Received unexpected response from AI service.",
146
+ "",
147
+ " This is likely a temporary issue. Try again in a few moments.",
148
+ ].join("\n");
149
+ case "rate_limit":
150
+ return [
151
+ "Rate limit exceeded.",
152
+ "",
153
+ " Wait a few seconds and try again.",
154
+ ].join("\n");
155
+ case "server_error":
156
+ return [
157
+ "VertaaUX API returned a server error.",
158
+ "",
159
+ " This is usually temporary. Try again in a moment.",
160
+ ].join("\n");
161
+ case "unknown":
162
+ return [
163
+ `An unexpected error occurred during ${command}.`,
164
+ "",
165
+ " Try: vertaa doctor",
166
+ " If this persists, file an issue.",
167
+ ].join("\n");
168
+ }
169
+ }
170
+ // ---------------------------------------------------------------------------
171
+ // Handler
172
+ // ---------------------------------------------------------------------------
173
+ /**
174
+ * Handle an AI command error: classify, format, display, and exit.
175
+ *
176
+ * This is the primary entry point for error handling in AI commands.
177
+ * It classifies the error, fails the spinner with a summary, writes
178
+ * the full formatted message to stderr, and exits with ExitCode.ERROR.
179
+ *
180
+ * @param error - The caught error
181
+ * @param command - The CLI command name (e.g., "explain", "triage")
182
+ * @param spinner - Active spinner instance to fail
183
+ */
184
+ export function handleAiCommandError(error, command, spinner) {
185
+ const kind = classifyAiError(error);
186
+ const message = formatAiError(kind, command);
187
+ failSpinner(spinner, `${command} failed`);
188
+ console.error(message);
189
+ process.exit(ExitCode.ERROR);
190
+ }
@@ -29,13 +29,11 @@ export declare function isPiped(): boolean;
29
29
  /**
30
30
  * Determine if colors should be used in output.
31
31
  *
32
- * Priority:
33
- * 1. NO_COLOR env var (disable)
34
- * 2. FORCE_COLOR env var (enable)
35
- * 3. TTY detection
36
- * 4. CI environment (usually disable)
32
+ * Delegates to @vertaaux/tui's isColorEnabled() which is set
33
+ * by the CLI's preAction hook based on --color/--no-color flags
34
+ * and environment detection.
37
35
  */
38
- export declare function shouldUseColor(): boolean;
36
+ export { isColorEnabled as shouldUseColor } from "@vertaaux/tui";
39
37
  /**
40
38
  * Detect the appropriate output format based on environment and explicit override.
41
39
  *
@@ -53,7 +51,7 @@ export declare function shouldUseColor(): boolean;
53
51
  export declare function detectOutputFormat(explicit?: string): OutputFormat;
54
52
  /**
55
53
  * Get terminal width for formatting.
56
- * Falls back to 80 columns if not detectable.
54
+ * Delegates to @vertaaux/tui (reads from stderr where UI renders).
57
55
  */
58
- export declare function getTerminalWidth(): number;
56
+ export { getTerminalWidth } from "@vertaaux/tui";
59
57
  //# sourceMappingURL=detect-env.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"detect-env.d.ts","sourceRoot":"","sources":["../../src/utils/detect-env.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,OAAO,GAAG,OAAO,GAAG,MAAM,GAAG,OAAO,CAAC;AAEzE;;;GAGG;AACH,wBAAgB,IAAI,IAAI,OAAO,CAY9B;AAED;;GAEG;AACH,wBAAgB,eAAe,IAAI,OAAO,CAEzC;AAED;;GAEG;AACH,wBAAgB,UAAU,IAAI,OAAO,CAEpC;AAED;;GAEG;AACH,wBAAgB,KAAK,IAAI,OAAO,CAE/B;AAED;;GAEG;AACH,wBAAgB,OAAO,IAAI,OAAO,CAEjC;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,IAAI,OAAO,CAkBxC;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,YAAY,CA0BlE;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC"}
1
+ {"version":3,"file":"detect-env.d.ts","sourceRoot":"","sources":["../../src/utils/detect-env.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,OAAO,GAAG,OAAO,GAAG,MAAM,GAAG,OAAO,CAAC;AAEzE;;;GAGG;AACH,wBAAgB,IAAI,IAAI,OAAO,CAY9B;AAED;;GAEG;AACH,wBAAgB,eAAe,IAAI,OAAO,CAEzC;AAED;;GAEG;AACH,wBAAgB,UAAU,IAAI,OAAO,CAEpC;AAED;;GAEG;AACH,wBAAgB,KAAK,IAAI,OAAO,CAE/B;AAED;;GAEG;AACH,wBAAgB,OAAO,IAAI,OAAO,CAEjC;AAED;;;;;;GAMG;AACH,OAAO,EAAE,cAAc,IAAI,cAAc,EAAE,MAAM,eAAe,CAAC;AAEjE;;;;;;;;;;;;;GAaG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,YAAY,CA0BlE;AAED;;;GAGG;AACH,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC"}
@@ -46,28 +46,11 @@ export function isPiped() {
46
46
  /**
47
47
  * Determine if colors should be used in output.
48
48
  *
49
- * Priority:
50
- * 1. NO_COLOR env var (disable)
51
- * 2. FORCE_COLOR env var (enable)
52
- * 3. TTY detection
53
- * 4. CI environment (usually disable)
49
+ * Delegates to @vertaaux/tui's isColorEnabled() which is set
50
+ * by the CLI's preAction hook based on --color/--no-color flags
51
+ * and environment detection.
54
52
  */
55
- export function shouldUseColor() {
56
- // NO_COLOR takes highest priority (https://no-color.org/)
57
- if (process.env.NO_COLOR !== undefined) {
58
- return false;
59
- }
60
- // FORCE_COLOR overrides TTY detection
61
- if (process.env.FORCE_COLOR !== undefined) {
62
- return true;
63
- }
64
- // No color in CI by default
65
- if (isCI()) {
66
- return false;
67
- }
68
- // Use color if interactive TTY
69
- return isTTY();
70
- }
53
+ export { isColorEnabled as shouldUseColor } from "@vertaaux/tui";
71
54
  /**
72
55
  * Detect the appropriate output format based on environment and explicit override.
73
56
  *
@@ -108,8 +91,6 @@ export function detectOutputFormat(explicit) {
108
91
  }
109
92
  /**
110
93
  * Get terminal width for formatting.
111
- * Falls back to 80 columns if not detectable.
94
+ * Delegates to @vertaaux/tui (reads from stderr where UI renders).
112
95
  */
113
- export function getTerminalWidth() {
114
- return process.stdout.columns || 80;
115
- }
96
+ export { getTerminalWidth } from "@vertaaux/tui";
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Universal stdin reader for CLI commands.
3
+ *
4
+ * Detects piped input vs TTY, reads JSON or plaintext from stdin,
5
+ * and provides a consistent interface for all commands that accept
6
+ * piped data (explain, triage, fix-plan, patch-review, etc.).
7
+ */
8
+ /**
9
+ * Result of reading from stdin.
10
+ */
11
+ export interface StdinResult {
12
+ /** Raw string content from stdin */
13
+ raw: string;
14
+ /** Parsed JSON if input was valid JSON, otherwise null */
15
+ json: unknown | null;
16
+ /** Whether the input was valid JSON */
17
+ isJson: boolean;
18
+ }
19
+ /**
20
+ * Check if stdin has piped data available.
21
+ */
22
+ export declare function hasPipedInput(): boolean;
23
+ /**
24
+ * Read all available data from stdin.
25
+ *
26
+ * Only reads when stdin is piped (not a TTY). Returns null if
27
+ * stdin is a TTY (interactive terminal).
28
+ *
29
+ * @returns Parsed stdin result, or null if no piped input
30
+ */
31
+ export declare function readStdin(): Promise<StdinResult | null>;
32
+ /**
33
+ * Read JSON from stdin, a file path, or return null.
34
+ *
35
+ * Provides the common pattern used by AI commands:
36
+ * 1. Check --file flag → read file
37
+ * 2. Check stdin pipe → read piped JSON
38
+ * 3. Return null (caller should check --job or show error)
39
+ *
40
+ * @param filePath - Optional file path from --file flag
41
+ * @returns Parsed JSON object, or null if no input found
42
+ */
43
+ export declare function readJsonInput(filePath?: string): Promise<unknown | null>;
44
+ /**
45
+ * Read plaintext from stdin (for diffs, code, etc.).
46
+ *
47
+ * @returns Raw text content, or null if no piped input
48
+ */
49
+ export declare function readTextInput(): Promise<string | null>;
50
+ //# sourceMappingURL=stdin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stdin.d.ts","sourceRoot":"","sources":["../../src/utils/stdin.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,oCAAoC;IACpC,GAAG,EAAE,MAAM,CAAC;IACZ,0DAA0D;IAC1D,IAAI,EAAE,OAAO,GAAG,IAAI,CAAC;IACrB,uCAAuC;IACvC,MAAM,EAAE,OAAO,CAAC;CACjB;AAED;;GAEG;AACH,wBAAgB,aAAa,IAAI,OAAO,CAEvC;AAED;;;;;;;GAOG;AACH,wBAAsB,SAAS,IAAI,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CA2B7D;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,aAAa,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CA+B9E;AAED;;;;GAIG;AACH,wBAAsB,aAAa,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAG5D"}