-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathexecFileNoThrow.ts
More file actions
150 lines (141 loc) · 4.23 KB
/
execFileNoThrow.ts
File metadata and controls
150 lines (141 loc) · 4.23 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
// This file represents useful wrappers over node:child_process
// These wrappers ease error handling and cross-platform compatbility
// By using execa, Windows automatically gets shell escaping + BAT / CMD handling
import { type ExecaError, execa } from 'execa'
import { getCwd } from '../utils/cwd.js'
import { logError } from './log.js'
export { execSyncWithDefaults_DEPRECATED } from './execFileNoThrowPortable.js'
const MS_IN_SECOND = 1000
const SECONDS_IN_MINUTE = 60
type ExecFileOptions = {
abortSignal?: AbortSignal
timeout?: number
preserveOutputOnError?: boolean
// Setting useCwd=false avoids circular dependencies during initialization
// getCwd() -> PersistentShell -> logEvent() -> execFileNoThrow
useCwd?: boolean
env?: NodeJS.ProcessEnv
stdin?: 'ignore' | 'inherit' | 'pipe'
input?: string
}
export function execFileNoThrow(
file: string,
args: string[],
options: ExecFileOptions = {
timeout: 10 * SECONDS_IN_MINUTE * MS_IN_SECOND,
preserveOutputOnError: true,
useCwd: true,
},
): Promise<{ stdout: string; stderr: string; code: number; error?: string }> {
return execFileNoThrowWithCwd(file, args, {
abortSignal: options.abortSignal,
timeout: options.timeout,
preserveOutputOnError: options.preserveOutputOnError,
cwd: options.useCwd ? getCwd() : undefined,
env: options.env,
stdin: options.stdin,
input: options.input,
})
}
type ExecFileWithCwdOptions = {
abortSignal?: AbortSignal
timeout?: number
preserveOutputOnError?: boolean
maxBuffer?: number
cwd?: string
env?: NodeJS.ProcessEnv
shell?: boolean | string | undefined
stdin?: 'ignore' | 'inherit' | 'pipe'
input?: string
}
type ExecaResultWithError = {
shortMessage?: string
signal?: string
}
/**
* Extracts a human-readable error message from an execa result.
*
* Priority order:
* 1. shortMessage - execa's human-readable error (e.g., "Command failed with exit code 1: ...")
* This is preferred because it already includes signal info when a process is killed,
* making it more informative than just the signal name.
* 2. signal - the signal that killed the process (e.g., "SIGTERM")
* 3. errorCode - fallback to just the numeric exit code
*/
function getErrorMessage(
result: ExecaResultWithError,
errorCode: number,
): string {
if (result.shortMessage) {
return result.shortMessage
}
if (typeof result.signal === 'string') {
return result.signal
}
return String(errorCode)
}
/**
* execFile, but always resolves (never throws)
*/
export function execFileNoThrowWithCwd(
file: string,
args: string[],
{
abortSignal,
timeout: finalTimeout = 10 * SECONDS_IN_MINUTE * MS_IN_SECOND,
preserveOutputOnError: finalPreserveOutput = true,
cwd: finalCwd,
env: finalEnv,
maxBuffer,
shell,
stdin: finalStdin,
input: finalInput,
}: ExecFileWithCwdOptions = {
timeout: 10 * SECONDS_IN_MINUTE * MS_IN_SECOND,
preserveOutputOnError: true,
maxBuffer: 1_000_000,
},
): Promise<{ stdout: string; stderr: string; code: number; error?: string }> {
return new Promise(resolve => {
// Use execa for cross-platform .bat/.cmd compatibility on Windows
execa(file, args, {
maxBuffer,
cancelSignal: abortSignal,
timeout: finalTimeout,
cwd: finalCwd,
env: finalEnv,
shell,
stdin: finalStdin,
input: finalInput,
reject: false, // Don't throw on non-zero exit codes
})
.then(result => {
if (result.failed) {
if (finalPreserveOutput) {
const errorCode = result.exitCode ?? 1
void resolve({
stdout: result.stdout || '',
stderr: result.stderr || '',
code: errorCode,
error: getErrorMessage(
result as unknown as ExecaResultWithError,
errorCode,
),
})
} else {
void resolve({ stdout: '', stderr: '', code: result.exitCode ?? 1 })
}
} else {
void resolve({
stdout: result.stdout,
stderr: result.stderr,
code: 0,
})
}
})
.catch((error: ExecaError) => {
logError(error)
void resolve({ stdout: '', stderr: '', code: 1 })
})
})
}