@meistrari/tela-cli 0.1.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/src/process.ts ADDED
@@ -0,0 +1,251 @@
1
+ export interface CommandResult {
2
+ command: string[]
3
+ exitCode: number
4
+ stdout: string
5
+ stderr: string
6
+ }
7
+
8
+ export interface RunCommandOptions {
9
+ cwd?: string
10
+ env?: Record<string, string | undefined>
11
+ stdin?: 'pipe' | 'inherit' | 'ignore'
12
+ input?: string
13
+ allowFailure?: boolean
14
+ }
15
+
16
+ export interface RunStreamingCommandOptions extends RunCommandOptions {
17
+ onStdoutLine?: (line: string) => void | Promise<void>
18
+ onStderrLine?: (line: string) => void | Promise<void>
19
+ }
20
+
21
+ export interface StreamingCommandHandle {
22
+ result: Promise<CommandResult>
23
+ interrupt(): void
24
+ kill(): void
25
+ }
26
+
27
+ export class CommandError extends Error {
28
+ constructor(
29
+ message: string,
30
+ public readonly result: CommandResult,
31
+ ) {
32
+ super(message)
33
+ }
34
+ }
35
+
36
+ async function readStream(stream: ReadableStream<Uint8Array> | null | undefined): Promise<string> {
37
+ if (!stream) {
38
+ return ''
39
+ }
40
+
41
+ return await new Response(stream).text()
42
+ }
43
+
44
+ async function readLineStream(
45
+ stream: ReadableStream<Uint8Array> | null | undefined,
46
+ onLine?: (line: string) => void | Promise<void>,
47
+ ): Promise<string> {
48
+ if (!stream) {
49
+ return ''
50
+ }
51
+
52
+ const reader = stream.getReader()
53
+ const decoder = new TextDecoder()
54
+ let buffer = ''
55
+ let output = ''
56
+
57
+ while (true) {
58
+ const { done, value } = await reader.read()
59
+ if (done) {
60
+ break
61
+ }
62
+
63
+ const chunk = decoder.decode(value, { stream: true })
64
+ output += chunk
65
+ buffer += chunk
66
+
67
+ let newlineIndex = buffer.indexOf('\n')
68
+ while (newlineIndex >= 0) {
69
+ const line = buffer.slice(0, newlineIndex).replace(/\r$/, '')
70
+ if (onLine) {
71
+ await onLine(line)
72
+ }
73
+ buffer = buffer.slice(newlineIndex + 1)
74
+ newlineIndex = buffer.indexOf('\n')
75
+ }
76
+ }
77
+
78
+ const remainder = decoder.decode()
79
+ if (remainder) {
80
+ output += remainder
81
+ buffer += remainder
82
+ }
83
+
84
+ if (buffer.length > 0 && onLine) {
85
+ await onLine(buffer.replace(/\r$/, ''))
86
+ }
87
+
88
+ return output
89
+ }
90
+
91
+ export async function runCommand(
92
+ command: string[],
93
+ options: RunCommandOptions = {},
94
+ ): Promise<CommandResult> {
95
+ const stdin = options.input !== undefined
96
+ ? new Blob([options.input])
97
+ : (options.stdin ?? 'pipe')
98
+
99
+ const proc = Bun.spawn(command, {
100
+ cwd: options.cwd,
101
+ env: options.env ? { ...process.env, ...options.env } : process.env,
102
+ stdin,
103
+ stdout: 'pipe',
104
+ stderr: 'pipe',
105
+ })
106
+
107
+ const [stdout, stderr, exitCode] = await Promise.all([
108
+ readStream(proc.stdout),
109
+ readStream(proc.stderr),
110
+ proc.exited,
111
+ ])
112
+
113
+ const result: CommandResult = {
114
+ command,
115
+ exitCode: exitCode ?? 1,
116
+ stdout,
117
+ stderr,
118
+ }
119
+
120
+ if (result.exitCode !== 0 && !options.allowFailure) {
121
+ throw new CommandError(
122
+ `Command failed (${result.exitCode}): ${command.join(' ')}`,
123
+ result,
124
+ )
125
+ }
126
+
127
+ return result
128
+ }
129
+
130
+ export async function runShell(
131
+ script: string,
132
+ options: Omit<RunCommandOptions, 'input'> = {},
133
+ ): Promise<CommandResult> {
134
+ return await runCommand(
135
+ ['/bin/bash', '-lc', script],
136
+ options,
137
+ )
138
+ }
139
+
140
+ export async function commandExists(command: string): Promise<boolean> {
141
+ const result = await runCommand(['which', command], { allowFailure: true })
142
+ return result.exitCode === 0
143
+ }
144
+
145
+ export async function runInteractiveCommand(
146
+ command: string[],
147
+ options: Omit<RunCommandOptions, 'stdin' | 'input'> = {},
148
+ ): Promise<CommandResult> {
149
+ const proc = Bun.spawn(command, {
150
+ cwd: options.cwd,
151
+ env: options.env ? { ...process.env, ...options.env } : process.env,
152
+ stdin: 'inherit',
153
+ stdout: 'inherit',
154
+ stderr: 'inherit',
155
+ })
156
+
157
+ const exitCode = await proc.exited
158
+ const result: CommandResult = {
159
+ command,
160
+ exitCode: exitCode ?? 1,
161
+ stdout: '',
162
+ stderr: '',
163
+ }
164
+
165
+ if (result.exitCode !== 0 && !options.allowFailure) {
166
+ throw new CommandError(
167
+ `Command failed (${result.exitCode}): ${command.join(' ')}`,
168
+ result,
169
+ )
170
+ }
171
+
172
+ return result
173
+ }
174
+
175
+ export async function runStreamingCommand(
176
+ command: string[],
177
+ options: RunStreamingCommandOptions = {},
178
+ ): Promise<CommandResult> {
179
+ return await spawnStreamingCommand(command, options).result
180
+ }
181
+
182
+ export function spawnStreamingCommand(
183
+ command: string[],
184
+ options: RunStreamingCommandOptions = {},
185
+ ): StreamingCommandHandle {
186
+ const stdin = options.input !== undefined
187
+ ? new Blob([options.input])
188
+ : (options.stdin ?? 'pipe')
189
+
190
+ const proc = Bun.spawn(command, {
191
+ cwd: options.cwd,
192
+ env: options.env ? { ...process.env, ...options.env } : process.env,
193
+ stdin,
194
+ stdout: 'pipe',
195
+ stderr: 'pipe',
196
+ })
197
+
198
+ const sendSignal = (signal?: string) => {
199
+ try {
200
+ ;(proc as { kill: (signal?: string) => void }).kill(signal)
201
+ }
202
+ catch {
203
+ try {
204
+ proc.kill()
205
+ }
206
+ catch {
207
+ // Best-effort shutdown only.
208
+ }
209
+ }
210
+ }
211
+
212
+ const result = (async () => {
213
+ try {
214
+ const [stdout, stderr, exitCode] = await Promise.all([
215
+ readLineStream(proc.stdout, options.onStdoutLine),
216
+ readLineStream(proc.stderr, options.onStderrLine),
217
+ proc.exited,
218
+ ])
219
+
220
+ const commandResult: CommandResult = {
221
+ command,
222
+ exitCode: exitCode ?? 1,
223
+ stdout,
224
+ stderr,
225
+ }
226
+
227
+ if (commandResult.exitCode !== 0 && !options.allowFailure) {
228
+ throw new CommandError(
229
+ `Command failed (${commandResult.exitCode}): ${command.join(' ')}`,
230
+ commandResult,
231
+ )
232
+ }
233
+
234
+ return commandResult
235
+ }
236
+ catch (error) {
237
+ sendSignal()
238
+ throw error
239
+ }
240
+ })()
241
+
242
+ return {
243
+ result,
244
+ interrupt() {
245
+ sendSignal('SIGINT')
246
+ },
247
+ kill() {
248
+ sendSignal('SIGKILL')
249
+ },
250
+ }
251
+ }