@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.
- package/LICENSE +21 -0
- package/README.md +58 -2
- package/dist/auth/device-flow.d.ts.map +1 -1
- package/dist/auth/device-flow.js +46 -14
- package/dist/commands/audit.d.ts +2 -0
- package/dist/commands/audit.d.ts.map +1 -1
- package/dist/commands/audit.js +167 -8
- package/dist/commands/client.d.ts +14 -0
- package/dist/commands/client.d.ts.map +1 -0
- package/dist/commands/client.js +362 -0
- package/dist/commands/compare.d.ts +20 -0
- package/dist/commands/compare.d.ts.map +1 -0
- package/dist/commands/compare.js +335 -0
- package/dist/commands/doc.d.ts +18 -0
- package/dist/commands/doc.d.ts.map +1 -0
- package/dist/commands/doc.js +161 -0
- package/dist/commands/download.d.ts.map +1 -1
- package/dist/commands/download.js +9 -8
- package/dist/commands/drift.d.ts +15 -0
- package/dist/commands/drift.d.ts.map +1 -0
- package/dist/commands/drift.js +309 -0
- package/dist/commands/explain.d.ts +14 -33
- package/dist/commands/explain.d.ts.map +1 -1
- package/dist/commands/explain.js +277 -179
- package/dist/commands/fix-plan.d.ts +15 -0
- package/dist/commands/fix-plan.d.ts.map +1 -0
- package/dist/commands/fix-plan.js +182 -0
- package/dist/commands/patch-review.d.ts +14 -0
- package/dist/commands/patch-review.d.ts.map +1 -0
- package/dist/commands/patch-review.js +200 -0
- package/dist/commands/protect.d.ts +16 -0
- package/dist/commands/protect.d.ts.map +1 -0
- package/dist/commands/protect.js +323 -0
- package/dist/commands/release-notes.d.ts +17 -0
- package/dist/commands/release-notes.d.ts.map +1 -0
- package/dist/commands/release-notes.js +145 -0
- package/dist/commands/report.d.ts +15 -0
- package/dist/commands/report.d.ts.map +1 -0
- package/dist/commands/report.js +214 -0
- package/dist/commands/suggest.d.ts +18 -0
- package/dist/commands/suggest.d.ts.map +1 -0
- package/dist/commands/suggest.js +152 -0
- package/dist/commands/triage.d.ts +17 -0
- package/dist/commands/triage.d.ts.map +1 -0
- package/dist/commands/triage.js +205 -0
- package/dist/commands/upload.d.ts.map +1 -1
- package/dist/commands/upload.js +8 -7
- package/dist/index.js +62 -25
- package/dist/output/formats.d.ts.map +1 -1
- package/dist/output/formats.js +18 -2
- package/dist/output/human.d.ts +1 -10
- package/dist/output/human.d.ts.map +1 -1
- package/dist/output/human.js +26 -98
- package/dist/policy/sync.d.ts +67 -0
- package/dist/policy/sync.d.ts.map +1 -0
- package/dist/policy/sync.js +147 -0
- package/dist/prompts/command-catalog.d.ts +46 -0
- package/dist/prompts/command-catalog.d.ts.map +1 -0
- package/dist/prompts/command-catalog.js +187 -0
- package/dist/ui/spinner.d.ts +10 -35
- package/dist/ui/spinner.d.ts.map +1 -1
- package/dist/ui/spinner.js +11 -58
- package/dist/ui/table.d.ts +1 -18
- package/dist/ui/table.d.ts.map +1 -1
- package/dist/ui/table.js +56 -163
- package/dist/utils/ai-error.d.ts +48 -0
- package/dist/utils/ai-error.d.ts.map +1 -0
- package/dist/utils/ai-error.js +190 -0
- package/dist/utils/detect-env.d.ts +6 -8
- package/dist/utils/detect-env.d.ts.map +1 -1
- package/dist/utils/detect-env.js +6 -25
- package/dist/utils/stdin.d.ts +50 -0
- package/dist/utils/stdin.d.ts.map +1 -0
- package/dist/utils/stdin.js +93 -0
- 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
|
-
*
|
|
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
|
|
8
|
-
import
|
|
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 =
|
|
68
|
-
const orderB =
|
|
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
|
|
79
|
-
const group = groups.get(
|
|
25
|
+
const sev = issue.severity?.toLowerCase() || "info";
|
|
26
|
+
const group = groups.get(sev) || [];
|
|
80
27
|
group.push(issue);
|
|
81
|
-
groups.set(
|
|
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
|
|
92
|
-
const group = groups.get(
|
|
38
|
+
const cat = issue.category || "Uncategorized";
|
|
39
|
+
const group = groups.get(cat) || [];
|
|
93
40
|
group.push(issue);
|
|
94
|
-
groups.set(
|
|
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
|
|
111
|
-
?
|
|
50
|
+
return isColorEnabled()
|
|
51
|
+
? text.success("No issues found.")
|
|
112
52
|
: "No issues found.";
|
|
113
53
|
}
|
|
114
|
-
const
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
124
|
-
colWidths.push(15);
|
|
62
|
+
columns.push({ key: "category", label: "Category", width: 15 });
|
|
125
63
|
}
|
|
126
64
|
if (showSelector) {
|
|
127
|
-
|
|
128
|
-
colWidths.push(maxSelectorWidth);
|
|
65
|
+
columns.push({ key: "selector", label: "Selector", width: maxSelectorWidth });
|
|
129
66
|
}
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
|
170
|
-
?
|
|
82
|
+
return isColorEnabled()
|
|
83
|
+
? text.dim("No scores available.")
|
|
171
84
|
: "No scores available.";
|
|
172
85
|
}
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
:
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
|
212
|
-
counts[
|
|
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
|
-
|
|
219
|
-
|
|
111
|
+
const parts = [];
|
|
112
|
+
for (const sev of order) {
|
|
113
|
+
const count = counts[sev];
|
|
220
114
|
if (count) {
|
|
221
|
-
const label = count
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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(
|
|
121
|
+
parts.push(label);
|
|
229
122
|
}
|
|
230
123
|
}
|
|
231
124
|
}
|
|
232
125
|
if (!parts.length) {
|
|
233
|
-
return
|
|
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
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
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
|
|
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
|
-
*
|
|
54
|
+
* Delegates to @vertaaux/tui (reads from stderr where UI renders).
|
|
57
55
|
*/
|
|
58
|
-
export
|
|
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
|
|
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"}
|
package/dist/utils/detect-env.js
CHANGED
|
@@ -46,28 +46,11 @@ export function isPiped() {
|
|
|
46
46
|
/**
|
|
47
47
|
* Determine if colors should be used in output.
|
|
48
48
|
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
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
|
|
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
|
-
*
|
|
94
|
+
* Delegates to @vertaaux/tui (reads from stderr where UI renders).
|
|
112
95
|
*/
|
|
113
|
-
export
|
|
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"}
|