@meistrari/tela-cli 0.6.0 → 0.7.4
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/README.md +56 -106
- package/dist/commands/auth.d.ts +2 -0
- package/dist/commands/context.d.ts +2 -0
- package/dist/commands/doctor-auth.d.ts +2 -0
- package/dist/commands/env.d.ts +2 -0
- package/dist/credentials.d.ts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +7027 -0
- package/dist/output.d.ts +13 -0
- package/dist/program.d.ts +2 -0
- package/package.json +17 -24
- package/scripts/install-skills.cjs +127 -0
- package/skills/tela-cli/SKILL.md +20 -0
- package/skills/tela-cli/references/agent.md +111 -0
- package/cli.ts +0 -25
- package/postinstall.sh +0 -19
- package/src/agent-api.ts +0 -300
- package/src/assets.d.ts +0 -4
- package/src/auth.ts +0 -432
- package/src/claude.ts +0 -101
- package/src/commands/doctor.ts +0 -35
- package/src/commands/feedback.ts +0 -701
- package/src/commands/metadata.ts +0 -242
- package/src/commands/push.ts +0 -208
- package/src/commands/run.ts +0 -1699
- package/src/commands/status.ts +0 -109
- package/src/commands/sync.ts +0 -245
- package/src/commit-message.ts +0 -69
- package/src/config.ts +0 -799
- package/src/diagnostics.ts +0 -813
- package/src/errors.ts +0 -122
- package/src/forgejo.ts +0 -233
- package/src/git.ts +0 -457
- package/src/ink/assets/160092__jorickhoofd__dog-bark-1.mp3 +0 -0
- package/src/ink/bark.tsx +0 -240
- package/src/ink/components.tsx +0 -177
- package/src/ink/runtime.tsx +0 -89
- package/src/ink/screens/feedback-screen.tsx +0 -394
- package/src/ink/screens/push-screen.tsx +0 -231
- package/src/ink/screens/run-screen.tsx +0 -291
- package/src/ink/screens/setup/breadcrumb.tsx +0 -23
- package/src/ink/screens/setup/setup-screen.tsx +0 -611
- package/src/ink/screens/setup/step-frame.tsx +0 -30
- package/src/ink/screens/setup/steps/done-step.tsx +0 -28
- package/src/ink/screens/setup/steps/environment-step.tsx +0 -22
- package/src/ink/screens/setup/steps/organization-step.tsx +0 -34
- package/src/ink/screens/setup/steps/review-step.tsx +0 -77
- package/src/ink/screens/setup/steps/running-step.tsx +0 -36
- package/src/ink/screens/setup/steps/ssh-key-step.tsx +0 -47
- package/src/ink/screens/setup/steps/text-field-step.tsx +0 -17
- package/src/ink/screens/setup/steps.ts +0 -156
- package/src/ink/screens/setup/types.ts +0 -65
- package/src/ink/screens/sync-screen.tsx +0 -282
- package/src/process.ts +0 -251
- package/src/program.ts +0 -2105
- package/src/runtime.ts +0 -147
- package/src/types.ts +0 -165
- package/src/ui.ts +0 -295
- package/src/version.ts +0 -3
|
@@ -1,282 +0,0 @@
|
|
|
1
|
-
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
|
2
|
-
import { Box, Text, useInput } from 'ink'
|
|
3
|
-
import type { BatchSyncFailure, SyncResult } from '../../types.ts'
|
|
4
|
-
import { AppFrame, Callout, KeyHints, Section, SelectList, SpinnerText, TextEntry } from '../components.tsx'
|
|
5
|
-
import type { CommandScreenController } from '../runtime.tsx'
|
|
6
|
-
|
|
7
|
-
export interface SyncScreenResult {
|
|
8
|
-
ok: boolean
|
|
9
|
-
mode: 'current' | 'repository' | 'all'
|
|
10
|
-
successes: SyncResult[]
|
|
11
|
-
failures: BatchSyncFailure[]
|
|
12
|
-
nextActions: string[]
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface SyncProgressItem {
|
|
16
|
-
id: string
|
|
17
|
-
label: string
|
|
18
|
-
status: 'pending' | 'running' | 'success' | 'failure'
|
|
19
|
-
detail?: string
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface SyncScreenProps {
|
|
23
|
-
controller: CommandScreenController<SyncScreenResult>
|
|
24
|
-
mode: 'current' | 'repository' | 'all'
|
|
25
|
-
dryRun?: boolean
|
|
26
|
-
targetLabel: string
|
|
27
|
-
cloneRoot?: string
|
|
28
|
-
initialRepository?: string
|
|
29
|
-
availableRepositories?: string[]
|
|
30
|
-
execute: (
|
|
31
|
-
repository: string | undefined,
|
|
32
|
-
onProgress: (update: SyncProgressItem) => void,
|
|
33
|
-
) => Promise<SyncScreenResult>
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
type SyncPhase = 'pick' | 'manual' | 'preview' | 'running' | 'done'
|
|
37
|
-
|
|
38
|
-
const MANUAL_ENTRY_VALUE = '__manual__'
|
|
39
|
-
|
|
40
|
-
export function SyncScreen(props: SyncScreenProps): React.ReactElement {
|
|
41
|
-
const [phase, setPhase] = useState<SyncPhase>(() => {
|
|
42
|
-
if (props.mode === 'repository' && !props.initialRepository) {
|
|
43
|
-
return props.availableRepositories && props.availableRepositories.length > 0 ? 'pick' : 'manual'
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return 'preview'
|
|
47
|
-
})
|
|
48
|
-
const [repository, setRepository] = useState(props.initialRepository ?? '')
|
|
49
|
-
const [manualRepository, setManualRepository] = useState('')
|
|
50
|
-
const [currentIndex, setCurrentIndex] = useState(0)
|
|
51
|
-
const [result, setResult] = useState<SyncScreenResult | null>(null)
|
|
52
|
-
const [progressItems, setProgressItems] = useState<SyncProgressItem[]>(() => {
|
|
53
|
-
if (props.mode === 'all' && props.availableRepositories) {
|
|
54
|
-
return props.availableRepositories.map(repo => ({
|
|
55
|
-
id: repo,
|
|
56
|
-
label: repo,
|
|
57
|
-
status: 'pending',
|
|
58
|
-
}))
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return []
|
|
62
|
-
})
|
|
63
|
-
const startedRef = useRef(false)
|
|
64
|
-
|
|
65
|
-
const selectableRepositories = useMemo(() => {
|
|
66
|
-
return [
|
|
67
|
-
...(props.availableRepositories ?? []).map(repo => ({
|
|
68
|
-
key: repo,
|
|
69
|
-
label: repo,
|
|
70
|
-
})),
|
|
71
|
-
{ key: MANUAL_ENTRY_VALUE, label: 'Enter a repository name manually' },
|
|
72
|
-
]
|
|
73
|
-
}, [props.availableRepositories])
|
|
74
|
-
|
|
75
|
-
useEffect(() => {
|
|
76
|
-
if (phase !== 'running' || startedRef.current) {
|
|
77
|
-
return
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
startedRef.current = true
|
|
81
|
-
void props.execute(
|
|
82
|
-
repository || undefined,
|
|
83
|
-
(update) => {
|
|
84
|
-
setProgressItems(current => {
|
|
85
|
-
const existingIndex = current.findIndex(item => item.id === update.id)
|
|
86
|
-
if (existingIndex === -1) {
|
|
87
|
-
return [...current, update]
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return current.map(item => item.id === update.id ? update : item)
|
|
91
|
-
})
|
|
92
|
-
},
|
|
93
|
-
)
|
|
94
|
-
.then((nextResult) => {
|
|
95
|
-
setResult(nextResult)
|
|
96
|
-
setPhase('done')
|
|
97
|
-
})
|
|
98
|
-
.catch((error) => {
|
|
99
|
-
props.controller.reject(error)
|
|
100
|
-
})
|
|
101
|
-
}, [phase, props, repository])
|
|
102
|
-
|
|
103
|
-
useEffect(() => {
|
|
104
|
-
if (phase === 'done' && result) {
|
|
105
|
-
props.controller.resolve(result, 40)
|
|
106
|
-
}
|
|
107
|
-
}, [phase, props, result])
|
|
108
|
-
|
|
109
|
-
useInput((input, key) => {
|
|
110
|
-
if (phase === 'pick') {
|
|
111
|
-
if (key.downArrow || input === 'j') {
|
|
112
|
-
setCurrentIndex(current => Math.min(current + 1, selectableRepositories.length - 1))
|
|
113
|
-
return
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if (key.upArrow || input === 'k') {
|
|
117
|
-
setCurrentIndex(current => Math.max(current - 1, 0))
|
|
118
|
-
return
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
if (key.escape) {
|
|
122
|
-
props.controller.reject(new Error('Sync cancelled.'))
|
|
123
|
-
return
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (key.return) {
|
|
127
|
-
const selected = selectableRepositories[currentIndex]?.key
|
|
128
|
-
if (selected === MANUAL_ENTRY_VALUE) {
|
|
129
|
-
setPhase('manual')
|
|
130
|
-
return
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
setRepository(String(selected ?? ''))
|
|
134
|
-
setPhase('preview')
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
return
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
if (phase === 'manual') {
|
|
141
|
-
if (key.escape) {
|
|
142
|
-
props.controller.reject(new Error('Sync cancelled.'))
|
|
143
|
-
return
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if (key.backspace || key.delete) {
|
|
147
|
-
setManualRepository(current => current.slice(0, -1))
|
|
148
|
-
return
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
if (key.return) {
|
|
152
|
-
if (!manualRepository.trim()) {
|
|
153
|
-
props.controller.reject(new Error('Repository name is required when not running inside a git repository.'))
|
|
154
|
-
return
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
setRepository(manualRepository.trim())
|
|
158
|
-
setPhase('preview')
|
|
159
|
-
return
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
if (input) {
|
|
163
|
-
setManualRepository(current => `${current}${input}`)
|
|
164
|
-
}
|
|
165
|
-
return
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
if (phase === 'preview') {
|
|
169
|
-
if (key.escape) {
|
|
170
|
-
props.controller.reject(new Error('Sync cancelled.'))
|
|
171
|
-
return
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
if (key.return) {
|
|
175
|
-
if (props.dryRun) {
|
|
176
|
-
setResult({
|
|
177
|
-
ok: true,
|
|
178
|
-
mode: props.mode,
|
|
179
|
-
successes: [],
|
|
180
|
-
failures: [],
|
|
181
|
-
nextActions: [
|
|
182
|
-
props.mode === 'current'
|
|
183
|
-
? 'Run `tela sync --current` without `--dry-run` to update this checkout.'
|
|
184
|
-
: props.mode === 'all'
|
|
185
|
-
? 'Run `tela sync --all` without `--dry-run` to refresh all repositories.'
|
|
186
|
-
: `Run \`tela sync ${repository}\` without \`--dry-run\` to clone or update the repository.`,
|
|
187
|
-
],
|
|
188
|
-
})
|
|
189
|
-
setPhase('done')
|
|
190
|
-
return
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
setPhase('running')
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
})
|
|
197
|
-
|
|
198
|
-
return (
|
|
199
|
-
<AppFrame
|
|
200
|
-
title="Sync"
|
|
201
|
-
subtitle="Refresh the current checkout, a named repository, or the whole clone root."
|
|
202
|
-
footer={
|
|
203
|
-
phase === 'pick'
|
|
204
|
-
? '↑/↓ move · Enter select · Esc cancel'
|
|
205
|
-
: phase === 'manual'
|
|
206
|
-
? 'Type repository name · Enter continue · Esc cancel'
|
|
207
|
-
: phase === 'preview'
|
|
208
|
-
? 'Enter run · Esc cancel'
|
|
209
|
-
: undefined
|
|
210
|
-
}
|
|
211
|
-
status={phase === 'running' ? 'Sync in progress' : undefined}
|
|
212
|
-
>
|
|
213
|
-
{phase === 'pick' ? (
|
|
214
|
-
<>
|
|
215
|
-
<Section title="Repository picker">
|
|
216
|
-
<SelectList items={selectableRepositories} currentIndex={currentIndex} />
|
|
217
|
-
</Section>
|
|
218
|
-
<KeyHints items={['Select a repository from the clone root or switch to manual entry']} />
|
|
219
|
-
</>
|
|
220
|
-
) : null}
|
|
221
|
-
|
|
222
|
-
{phase === 'manual' ? (
|
|
223
|
-
<Section title="Repository name">
|
|
224
|
-
<TextEntry label="Repository" value={manualRepository} placeholder="agent-api" />
|
|
225
|
-
</Section>
|
|
226
|
-
) : null}
|
|
227
|
-
|
|
228
|
-
{phase === 'preview' ? (
|
|
229
|
-
<>
|
|
230
|
-
<Section title={props.dryRun ? 'Dry run' : 'Plan'}>
|
|
231
|
-
<Text>Mode: {props.mode}</Text>
|
|
232
|
-
<Text>Target: {props.mode === 'repository' ? repository : props.targetLabel}</Text>
|
|
233
|
-
{props.cloneRoot ? <Text>Clone root: {props.cloneRoot}</Text> : null}
|
|
234
|
-
</Section>
|
|
235
|
-
{props.availableRepositories && props.mode === 'all' ? (
|
|
236
|
-
<Section title="Repositories">
|
|
237
|
-
{props.availableRepositories.map(repo => (
|
|
238
|
-
<Text key={repo}>{repo}</Text>
|
|
239
|
-
))}
|
|
240
|
-
</Section>
|
|
241
|
-
) : null}
|
|
242
|
-
</>
|
|
243
|
-
) : null}
|
|
244
|
-
|
|
245
|
-
{phase === 'running' ? (
|
|
246
|
-
<>
|
|
247
|
-
<SpinnerText label={`Syncing ${props.mode === 'repository' ? repository : props.targetLabel}`} />
|
|
248
|
-
{progressItems.length > 0 ? (
|
|
249
|
-
<Section title="Progress">
|
|
250
|
-
{progressItems.map(item => (
|
|
251
|
-
<Text key={item.id}>
|
|
252
|
-
{`${item.label}: ${item.status}${item.detail ? ` — ${item.detail}` : ''}`}
|
|
253
|
-
</Text>
|
|
254
|
-
))}
|
|
255
|
-
</Section>
|
|
256
|
-
) : null}
|
|
257
|
-
</>
|
|
258
|
-
) : null}
|
|
259
|
-
|
|
260
|
-
{phase === 'done' && result ? (
|
|
261
|
-
<>
|
|
262
|
-
<Section title="Result">
|
|
263
|
-
<Text>Mode: {result.mode}</Text>
|
|
264
|
-
<Text>Successes: {result.successes.length}</Text>
|
|
265
|
-
<Text>Failures: {result.failures.length}</Text>
|
|
266
|
-
</Section>
|
|
267
|
-
{result.failures.length > 0 ? (
|
|
268
|
-
<Section title="Failures">
|
|
269
|
-
{result.failures.map(failure => (
|
|
270
|
-
<Text key={failure.targetDir} color="yellow">
|
|
271
|
-
{`${failure.targetDir}: ${failure.error}`}
|
|
272
|
-
</Text>
|
|
273
|
-
))}
|
|
274
|
-
</Section>
|
|
275
|
-
) : (
|
|
276
|
-
<Callout tone="success">Sync finished successfully.</Callout>
|
|
277
|
-
)}
|
|
278
|
-
</>
|
|
279
|
-
) : null}
|
|
280
|
-
</AppFrame>
|
|
281
|
-
)
|
|
282
|
-
}
|
package/src/process.ts
DELETED
|
@@ -1,251 +0,0 @@
|
|
|
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
|
-
}
|