@vertaaux/cli 0.2.3 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +58 -2
- package/dist/auth/device-flow.js +6 -8
- package/dist/commands/audit.d.ts +2 -0
- package/dist/commands/audit.d.ts.map +1 -1
- package/dist/commands/audit.js +165 -6
- 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/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/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/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 +14 -0
- 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/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/node_modules/@vertaaux/tui/dist/index.cjs +1157 -0
- package/node_modules/@vertaaux/tui/dist/index.cjs.map +1 -0
- package/node_modules/@vertaaux/tui/dist/index.d.cts +609 -0
- package/node_modules/@vertaaux/tui/dist/index.d.ts +609 -0
- package/node_modules/@vertaaux/tui/dist/index.js +1100 -0
- package/node_modules/@vertaaux/tui/dist/index.js.map +1 -0
- package/node_modules/@vertaaux/tui/package.json +64 -0
- package/package.json +12 -5
|
@@ -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"}
|
|
@@ -0,0 +1,93 @@
|
|
|
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
|
+
* Check if stdin has piped data available.
|
|
10
|
+
*/
|
|
11
|
+
export function hasPipedInput() {
|
|
12
|
+
return !process.stdin.isTTY;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Read all available data from stdin.
|
|
16
|
+
*
|
|
17
|
+
* Only reads when stdin is piped (not a TTY). Returns null if
|
|
18
|
+
* stdin is a TTY (interactive terminal).
|
|
19
|
+
*
|
|
20
|
+
* @returns Parsed stdin result, or null if no piped input
|
|
21
|
+
*/
|
|
22
|
+
export async function readStdin() {
|
|
23
|
+
if (process.stdin.isTTY) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
const chunks = [];
|
|
27
|
+
for await (const chunk of process.stdin) {
|
|
28
|
+
chunks.push(chunk);
|
|
29
|
+
}
|
|
30
|
+
const raw = Buffer.concat(chunks).toString("utf-8").trim();
|
|
31
|
+
if (!raw) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
let json = null;
|
|
35
|
+
let isJson = false;
|
|
36
|
+
try {
|
|
37
|
+
json = JSON.parse(raw);
|
|
38
|
+
isJson = true;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// Not JSON — that's fine, could be plaintext (e.g., a diff)
|
|
42
|
+
}
|
|
43
|
+
return { raw, json, isJson };
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Read JSON from stdin, a file path, or return null.
|
|
47
|
+
*
|
|
48
|
+
* Provides the common pattern used by AI commands:
|
|
49
|
+
* 1. Check --file flag → read file
|
|
50
|
+
* 2. Check stdin pipe → read piped JSON
|
|
51
|
+
* 3. Return null (caller should check --job or show error)
|
|
52
|
+
*
|
|
53
|
+
* @param filePath - Optional file path from --file flag
|
|
54
|
+
* @returns Parsed JSON object, or null if no input found
|
|
55
|
+
*/
|
|
56
|
+
export async function readJsonInput(filePath) {
|
|
57
|
+
// Priority 1: explicit file path
|
|
58
|
+
if (filePath) {
|
|
59
|
+
const fs = await import("fs");
|
|
60
|
+
const path = await import("path");
|
|
61
|
+
const resolved = path.resolve(process.cwd(), filePath);
|
|
62
|
+
if (!fs.existsSync(resolved)) {
|
|
63
|
+
throw new Error(`Input file not found: ${filePath}`);
|
|
64
|
+
}
|
|
65
|
+
const content = fs.readFileSync(resolved, "utf-8");
|
|
66
|
+
try {
|
|
67
|
+
return JSON.parse(content);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
throw new Error(`Invalid JSON in file: ${filePath}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// Priority 2: piped stdin
|
|
74
|
+
const stdin = await readStdin();
|
|
75
|
+
if (stdin) {
|
|
76
|
+
if (!stdin.isJson) {
|
|
77
|
+
throw new Error("Could not parse input as JSON. If piping from vertaa, use --json flag:\n" +
|
|
78
|
+
" vertaa audit https://example.com --json | vertaa explain");
|
|
79
|
+
}
|
|
80
|
+
return stdin.json;
|
|
81
|
+
}
|
|
82
|
+
// No input found
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Read plaintext from stdin (for diffs, code, etc.).
|
|
87
|
+
*
|
|
88
|
+
* @returns Raw text content, or null if no piped input
|
|
89
|
+
*/
|
|
90
|
+
export async function readTextInput() {
|
|
91
|
+
const stdin = await readStdin();
|
|
92
|
+
return stdin?.raw ?? null;
|
|
93
|
+
}
|