@livestore/utils-dev 0.4.0-dev.9 → 0.5.0-dev.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/dist/.tsbuildinfo +1 -0
- package/dist/node/DockerCompose/DockerCompose.d.ts +52 -0
- package/dist/node/DockerCompose/DockerCompose.d.ts.map +1 -0
- package/dist/node/DockerCompose/DockerCompose.js +158 -0
- package/dist/node/DockerCompose/DockerCompose.js.map +1 -0
- package/dist/node/DockerCompose/DockerCompose.test.d.ts +2 -0
- package/dist/node/DockerCompose/DockerCompose.test.d.ts.map +1 -0
- package/dist/node/{DockerComposeService/DockerComposeService.test.js → DockerCompose/DockerCompose.test.js} +18 -18
- package/dist/node/DockerCompose/DockerCompose.test.js.map +1 -0
- package/dist/node/DockerCompose/mod.d.ts +2 -0
- package/dist/node/DockerCompose/mod.d.ts.map +1 -0
- package/dist/node/DockerCompose/mod.js +2 -0
- package/dist/node/DockerCompose/mod.js.map +1 -0
- package/dist/node/FileLogger.d.ts +15 -8
- package/dist/node/FileLogger.d.ts.map +1 -1
- package/dist/node/FileLogger.js +64 -80
- package/dist/node/FileLogger.js.map +1 -1
- package/dist/node/cmd-log.d.ts +21 -0
- package/dist/node/cmd-log.d.ts.map +1 -0
- package/dist/node/cmd-log.js +58 -0
- package/dist/node/cmd-log.js.map +1 -0
- package/dist/node/cmd.d.ts +20 -14
- package/dist/node/cmd.d.ts.map +1 -1
- package/dist/node/cmd.js +248 -33
- package/dist/node/cmd.js.map +1 -1
- package/dist/node/cmd.test.d.ts +2 -0
- package/dist/node/cmd.test.d.ts.map +1 -0
- package/dist/node/cmd.test.js +103 -0
- package/dist/node/cmd.test.js.map +1 -0
- package/dist/node/mod.d.ts +5 -5
- package/dist/node/mod.d.ts.map +1 -1
- package/dist/node/mod.js +114 -66
- package/dist/node/mod.js.map +1 -1
- package/dist/node/workspace.d.ts +22 -0
- package/dist/node/workspace.d.ts.map +1 -0
- package/dist/node/workspace.js +26 -0
- package/dist/node/workspace.js.map +1 -0
- package/dist/node-vitest/Vitest.d.ts +17 -8
- package/dist/node-vitest/Vitest.d.ts.map +1 -1
- package/dist/node-vitest/Vitest.js +16 -16
- package/dist/node-vitest/Vitest.js.map +1 -1
- package/dist/node-vitest/Vitest.test.d.ts +6 -0
- package/dist/node-vitest/Vitest.test.d.ts.map +1 -1
- package/dist/node-vitest/Vitest.test.js +25 -10
- package/dist/node-vitest/Vitest.test.js.map +1 -1
- package/dist/wrangler/WranglerDevServer.d.ts +32 -20
- package/dist/wrangler/WranglerDevServer.d.ts.map +1 -1
- package/dist/wrangler/WranglerDevServer.js +79 -60
- package/dist/wrangler/WranglerDevServer.js.map +1 -1
- package/dist/wrangler/WranglerDevServer.test.js +31 -31
- package/dist/wrangler/WranglerDevServer.test.js.map +1 -1
- package/dist/wrangler/mod.d.ts +1 -1
- package/dist/wrangler/mod.d.ts.map +1 -1
- package/dist/wrangler/mod.js +1 -1
- package/dist/wrangler/mod.js.map +1 -1
- package/package.json +56 -21
- package/src/node/{DockerComposeService/DockerComposeService.test.ts → DockerCompose/DockerCompose.test.ts} +23 -20
- package/src/node/DockerCompose/DockerCompose.ts +328 -0
- package/src/node/DockerCompose/mod.ts +1 -0
- package/src/node/DockerCompose/test-fixtures/docker-compose.yml +4 -0
- package/src/node/FileLogger.ts +91 -115
- package/src/node/cmd-log.ts +95 -0
- package/src/node/cmd.test.ts +136 -0
- package/src/node/cmd.ts +431 -82
- package/src/node/mod.ts +152 -96
- package/src/node/workspace.ts +45 -0
- package/src/node-vitest/Vitest.test.ts +34 -10
- package/src/node-vitest/Vitest.ts +60 -66
- package/src/wrangler/WranglerDevServer.test.ts +38 -40
- package/src/wrangler/WranglerDevServer.ts +165 -108
- package/src/wrangler/fixtures/wrangler.toml +1 -1
- package/src/wrangler/mod.ts +1 -6
- package/dist/.tsbuildinfo.json +0 -1
- package/dist/node/DockerComposeService/DockerComposeService.d.ts +0 -58
- package/dist/node/DockerComposeService/DockerComposeService.d.ts.map +0 -1
- package/dist/node/DockerComposeService/DockerComposeService.js +0 -144
- package/dist/node/DockerComposeService/DockerComposeService.js.map +0 -1
- package/dist/node/DockerComposeService/DockerComposeService.test.d.ts +0 -2
- package/dist/node/DockerComposeService/DockerComposeService.test.d.ts.map +0 -1
- package/dist/node/DockerComposeService/DockerComposeService.test.js.map +0 -1
- package/src/node/DockerComposeService/DockerComposeService.ts +0 -328
- package/src/node/DockerComposeService/test-fixtures/docker-compose.yml +0 -4
package/src/node/cmd.ts
CHANGED
|
@@ -1,99 +1,448 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
Cause,
|
|
3
|
+
ChildProcess,
|
|
4
|
+
ChildProcessSpawner,
|
|
5
|
+
Effect,
|
|
6
|
+
Fiber,
|
|
7
|
+
Logger,
|
|
8
|
+
type PlatformError,
|
|
9
|
+
Predicate,
|
|
10
|
+
References,
|
|
11
|
+
Schema,
|
|
12
|
+
Stream,
|
|
13
|
+
} from '@livestore/utils/effect'
|
|
14
|
+
import { NodeFileSystem } from '@livestore/utils/node'
|
|
15
|
+
|
|
16
|
+
import { applyLoggingToCommand } from './cmd-log.ts'
|
|
17
|
+
import * as FileLogger from './FileLogger.ts'
|
|
18
|
+
import { CurrentWorkingDirectory } from './workspace.ts'
|
|
19
|
+
|
|
20
|
+
// Branded zero value so we can compare exit codes without touching internals.
|
|
21
|
+
const SUCCESS_EXIT_CODE: ChildProcessSpawner.ExitCode = ChildProcessSpawner.ExitCode(0)
|
|
3
22
|
|
|
4
23
|
export const cmd: (
|
|
5
24
|
commandInput: string | (string | undefined)[],
|
|
6
|
-
options?:
|
|
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
|
-
|
|
25
|
+
options?: {
|
|
26
|
+
stderr?: 'inherit' | 'pipe'
|
|
27
|
+
stdout?: 'inherit' | 'pipe'
|
|
28
|
+
shell?: boolean
|
|
29
|
+
env?: Record<string, string | undefined>
|
|
30
|
+
/**
|
|
31
|
+
* When provided, streams command output to terminal AND to a canonical log file (`${logDir}/dev.log`) in this directory.
|
|
32
|
+
* Also archives the previous run to `${logDir}/archive/dev-<ISO>.log` and keeps only the latest 50 archives.
|
|
33
|
+
*/
|
|
34
|
+
logDir?: string
|
|
35
|
+
/** Optional basename for the canonical log file; defaults to 'dev.log' */
|
|
36
|
+
logFileName?: string
|
|
37
|
+
/** Optional number of archived logs to retain; defaults to 50 */
|
|
38
|
+
logRetention?: number
|
|
39
|
+
},
|
|
40
|
+
) => Effect.Effect<
|
|
41
|
+
ChildProcessSpawner.ExitCode,
|
|
42
|
+
PlatformError.PlatformError | CmdError,
|
|
43
|
+
ChildProcessSpawner.ChildProcessSpawner | CurrentWorkingDirectory
|
|
44
|
+
> = Effect.fn('cmd')(function* (commandInput, options) {
|
|
45
|
+
const cwd = yield* CurrentWorkingDirectory
|
|
46
|
+
|
|
47
|
+
const asArray = Array.isArray(commandInput)
|
|
48
|
+
const parts = asArray === true ? commandInput.filter(Predicate.isNotUndefined) : undefined
|
|
49
|
+
const [command, ...args] = asArray === true ? (parts as string[]) : commandInput.split(' ')
|
|
50
|
+
|
|
51
|
+
const debugEnvStr = Object.entries(options?.env ?? {})
|
|
52
|
+
.map(([key, value]) => `${key}='${String(value)}' `)
|
|
53
|
+
.join('')
|
|
54
|
+
|
|
55
|
+
const loggingOpts = {
|
|
56
|
+
...(options?.logDir !== undefined ? { logDir: options.logDir } : {}),
|
|
57
|
+
...(options?.logFileName !== undefined ? { logFileName: options.logFileName } : {}),
|
|
58
|
+
...(options?.logRetention !== undefined ? { logRetention: options.logRetention } : {}),
|
|
59
|
+
} as const
|
|
60
|
+
const { input: finalInput, subshell: needsShell, logPath } = yield* applyLoggingToCommand(commandInput, loggingOpts)
|
|
61
|
+
|
|
62
|
+
const stdoutMode = options?.stdout ?? 'inherit'
|
|
63
|
+
const stderrMode = options?.stderr ?? 'inherit'
|
|
64
|
+
const useShell = (options?.shell === true ? true : false) || needsShell
|
|
65
|
+
|
|
66
|
+
const commandDebugStr = debugEnvStr + (Array.isArray(finalInput) === true ? finalInput.join(' ') : finalInput)
|
|
67
|
+
const subshellStr = useShell === true ? ' (in subshell)' : ''
|
|
68
|
+
|
|
69
|
+
yield* Effect.logDebug(`Running '${commandDebugStr}' in '${cwd}'${subshellStr}`)
|
|
70
|
+
yield* Effect.annotateCurrentSpan({
|
|
71
|
+
'span.label': commandDebugStr,
|
|
72
|
+
cwd,
|
|
73
|
+
command,
|
|
74
|
+
args,
|
|
75
|
+
logDir: options?.logDir,
|
|
56
76
|
})
|
|
57
77
|
|
|
78
|
+
const baseArgs = {
|
|
79
|
+
commandInput: finalInput,
|
|
80
|
+
cwd,
|
|
81
|
+
env: options?.env ?? {},
|
|
82
|
+
stdoutMode,
|
|
83
|
+
stderrMode,
|
|
84
|
+
useShell,
|
|
85
|
+
} as const
|
|
86
|
+
|
|
87
|
+
const exitCode = yield* Predicate.isNotUndefined(logPath) === true
|
|
88
|
+
? Effect.gen(function* () {
|
|
89
|
+
yield* Effect.sync(() => console.log(`Logging output to ${logPath}`))
|
|
90
|
+
return yield* runWithLogging({ ...baseArgs, logPath, threadName: commandDebugStr })
|
|
91
|
+
})
|
|
92
|
+
: runWithoutLogging(baseArgs)
|
|
93
|
+
|
|
94
|
+
if (exitCode !== SUCCESS_EXIT_CODE) {
|
|
95
|
+
return yield* CmdError.make({
|
|
96
|
+
command: command!,
|
|
97
|
+
args,
|
|
98
|
+
cwd,
|
|
99
|
+
env: options?.env ?? {},
|
|
100
|
+
stderr: stderrMode,
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return exitCode
|
|
105
|
+
})
|
|
106
|
+
|
|
58
107
|
export const cmdText: (
|
|
59
108
|
commandInput: string | (string | undefined)[],
|
|
60
109
|
options?: {
|
|
61
|
-
cwd?: string
|
|
62
110
|
stderr?: 'inherit' | 'pipe'
|
|
63
111
|
runInShell?: boolean
|
|
64
112
|
env?: Record<string, string | undefined>
|
|
65
113
|
},
|
|
66
|
-
) => Effect.Effect<
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
return yield* Command.make(command!, ...args).pipe(
|
|
83
|
-
// inherit = Stream stderr to process.stderr, pipe = Stream stderr to process.stdout
|
|
84
|
-
Command.stderr(options?.stderr ?? 'inherit'),
|
|
85
|
-
Command.workingDirectory(cwd),
|
|
86
|
-
options?.runInShell ? Command.runInShell(true) : identity,
|
|
87
|
-
Command.env(options?.env ?? {}),
|
|
88
|
-
Command.string,
|
|
89
|
-
)
|
|
90
|
-
},
|
|
91
|
-
)
|
|
114
|
+
) => Effect.Effect<
|
|
115
|
+
string,
|
|
116
|
+
PlatformError.PlatformError,
|
|
117
|
+
ChildProcessSpawner.ChildProcessSpawner | CurrentWorkingDirectory
|
|
118
|
+
> = Effect.fn('cmdText')(function* (commandInput, options) {
|
|
119
|
+
const cwd = yield* CurrentWorkingDirectory
|
|
120
|
+
const commandParts =
|
|
121
|
+
Array.isArray(commandInput) === true ? commandInput.filter(Predicate.isNotUndefined) : commandInput.split(' ')
|
|
122
|
+
const [command, ...args] = commandParts
|
|
123
|
+
const debugEnvStr = Object.entries(options?.env ?? {})
|
|
124
|
+
.map(([key, value]) => `${key}='${String(value)}' `)
|
|
125
|
+
.join('')
|
|
126
|
+
|
|
127
|
+
const commandDebugStr = debugEnvStr + [command, ...args].join(' ')
|
|
128
|
+
const subshellStr = options?.runInShell === true ? ' (in subshell)' : ''
|
|
92
129
|
|
|
93
|
-
|
|
130
|
+
yield* Effect.logDebug(`Running '${commandDebugStr}' in '${cwd}'${subshellStr}`)
|
|
131
|
+
yield* Effect.annotateCurrentSpan({ 'span.label': commandDebugStr, command, cwd })
|
|
132
|
+
|
|
133
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
|
|
134
|
+
return yield* spawner.string(
|
|
135
|
+
buildCommand(commandParts, {
|
|
136
|
+
cwd,
|
|
137
|
+
env: options?.env ?? {},
|
|
138
|
+
stderr: options?.stderr ?? 'inherit',
|
|
139
|
+
shell: options?.runInShell === true,
|
|
140
|
+
}),
|
|
141
|
+
)
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
export class CmdError extends Schema.TaggedError<CmdError>('~@livestore/utils-dev/CmdError')('CmdError', {
|
|
94
145
|
command: Schema.String,
|
|
95
146
|
args: Schema.Array(Schema.String),
|
|
96
147
|
cwd: Schema.String,
|
|
97
|
-
env: Schema.Record(
|
|
98
|
-
stderr: Schema.
|
|
148
|
+
env: Schema.Record(Schema.String, Schema.String.pipe(Schema.UndefinedOr)),
|
|
149
|
+
stderr: Schema.Literals(['inherit', 'pipe']),
|
|
99
150
|
}) {}
|
|
151
|
+
|
|
152
|
+
type TRunBaseArgs = {
|
|
153
|
+
readonly commandInput: string | string[]
|
|
154
|
+
readonly cwd: string
|
|
155
|
+
readonly env: Record<string, string | undefined>
|
|
156
|
+
readonly stdoutMode: 'inherit' | 'pipe'
|
|
157
|
+
readonly stderrMode: 'inherit' | 'pipe'
|
|
158
|
+
readonly useShell: boolean
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const runWithoutLogging = ({ commandInput, cwd, env, stdoutMode, stderrMode, useShell }: TRunBaseArgs) =>
|
|
162
|
+
Effect.gen(function* () {
|
|
163
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
|
|
164
|
+
return yield* spawner.exitCode(
|
|
165
|
+
buildCommand(commandInput, {
|
|
166
|
+
cwd,
|
|
167
|
+
env,
|
|
168
|
+
stdin: 'inherit',
|
|
169
|
+
stdout: stdoutMode,
|
|
170
|
+
stderr: stderrMode,
|
|
171
|
+
shell: useShell,
|
|
172
|
+
}),
|
|
173
|
+
)
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
type TRunWithLoggingArgs = TRunBaseArgs & {
|
|
177
|
+
readonly logPath: string
|
|
178
|
+
readonly threadName: string
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const runWithLogging = ({
|
|
182
|
+
commandInput,
|
|
183
|
+
cwd,
|
|
184
|
+
env,
|
|
185
|
+
stdoutMode,
|
|
186
|
+
stderrMode,
|
|
187
|
+
useShell,
|
|
188
|
+
logPath,
|
|
189
|
+
threadName,
|
|
190
|
+
}: TRunWithLoggingArgs) =>
|
|
191
|
+
// When logging is enabled we have to replace the `2>&1 | tee` pipeline the
|
|
192
|
+
// shell used to give us. We now pipe both streams through Effect so we can
|
|
193
|
+
// mirror to the terminal (only when requested) and append formatted entries
|
|
194
|
+
// into the canonical log ourselves.
|
|
195
|
+
Effect.scoped(
|
|
196
|
+
Effect.gen(function* () {
|
|
197
|
+
const envWithColor = env.FORCE_COLOR === undefined ? { ...env, FORCE_COLOR: '1' } : env
|
|
198
|
+
|
|
199
|
+
const prettyLoggerOptions = {
|
|
200
|
+
colors: true,
|
|
201
|
+
stderr: false,
|
|
202
|
+
formatDate: FileLogger.formatLogTime,
|
|
203
|
+
} as const
|
|
204
|
+
|
|
205
|
+
const logStringToFile = yield* Logger.make<unknown, string>(({ message }) => {
|
|
206
|
+
if (typeof message === 'string') return message
|
|
207
|
+
return String(message)
|
|
208
|
+
}).pipe(Logger.toFile(logPath, { flag: 'a' }), Effect.provide(NodeFileSystem.layer))
|
|
209
|
+
|
|
210
|
+
const FileLoggerLive = Logger.layer([logStringToFile])
|
|
211
|
+
|
|
212
|
+
const appendLog = ({ channel, content }: { channel: 'stdout' | 'stderr'; content: string }) =>
|
|
213
|
+
Effect.gen(function* () {
|
|
214
|
+
const date = new Date()
|
|
215
|
+
const formatted = FileLogger.formatPrettyLog({
|
|
216
|
+
logLevel: channel === 'stdout' ? 'Info' : 'Warn',
|
|
217
|
+
message: [`[${channel}]${content.length > 0 ? ` ${content}` : ''}`],
|
|
218
|
+
cause: Cause.empty,
|
|
219
|
+
date,
|
|
220
|
+
fiberId: 0,
|
|
221
|
+
spans: [[threadName, date.getTime()]],
|
|
222
|
+
annotations: { thread: threadName },
|
|
223
|
+
options: prettyLoggerOptions,
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
yield* Effect.log(formatted.endsWith('\n') === true ? formatted.slice(0, -1) : formatted).pipe(
|
|
227
|
+
Effect.provideService(References.MinimumLogLevel, 'All'),
|
|
228
|
+
Effect.provide(FileLoggerLive),
|
|
229
|
+
)
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
const command = buildCommand(commandInput, {
|
|
233
|
+
cwd,
|
|
234
|
+
env: envWithColor,
|
|
235
|
+
stdin: 'inherit',
|
|
236
|
+
stdout: 'pipe',
|
|
237
|
+
stderr: 'pipe',
|
|
238
|
+
shell: useShell,
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
// Acquire/start the command and make sure we kill it on interruption.
|
|
242
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
|
|
243
|
+
const runningProcess = yield* Effect.acquireRelease(spawner.spawn(command), (proc) =>
|
|
244
|
+
proc.isRunning.pipe(
|
|
245
|
+
Effect.flatMap((running) =>
|
|
246
|
+
running === true ? proc.kill().pipe(Effect.catch(() => Effect.void)) : Effect.void,
|
|
247
|
+
),
|
|
248
|
+
Effect.ignore,
|
|
249
|
+
),
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
const stdoutHandler = makeStreamHandler({
|
|
253
|
+
channel: 'stdout',
|
|
254
|
+
...(stdoutMode === 'inherit' ? { mirrorTarget: process.stdout } : {}),
|
|
255
|
+
appendLog,
|
|
256
|
+
})
|
|
257
|
+
const stderrHandler = makeStreamHandler({
|
|
258
|
+
channel: 'stderr',
|
|
259
|
+
...(stderrMode === 'inherit' ? { mirrorTarget: process.stderr } : {}),
|
|
260
|
+
appendLog,
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
const stdoutFiber = yield* runningProcess.stdout.pipe(
|
|
264
|
+
Stream.decodeText({ encoding: 'utf8' }),
|
|
265
|
+
Stream.runForEach((chunk) => stdoutHandler.onChunk(chunk)),
|
|
266
|
+
Effect.forkScoped,
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
const stderrFiber = yield* runningProcess.stderr.pipe(
|
|
270
|
+
Stream.decodeText({ encoding: 'utf8' }),
|
|
271
|
+
Stream.runForEach((chunk) => stderrHandler.onChunk(chunk)),
|
|
272
|
+
Effect.forkScoped,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
// Dump any buffered data and finish both stream fibers before we return.
|
|
276
|
+
const flushOutputs = Effect.gen(function* () {
|
|
277
|
+
const stillRunning = yield* runningProcess.isRunning.pipe(Effect.orElseSucceed(() => false))
|
|
278
|
+
if (stillRunning === true) {
|
|
279
|
+
yield* Effect.ignore(runningProcess.kill())
|
|
280
|
+
}
|
|
281
|
+
yield* Effect.ignore(Fiber.join(stdoutFiber))
|
|
282
|
+
yield* Effect.ignore(Fiber.join(stderrFiber))
|
|
283
|
+
yield* stdoutHandler.flush()
|
|
284
|
+
yield* stderrHandler.flush()
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
const exitCode = yield* runningProcess.exitCode.pipe(Effect.ensuring(flushOutputs))
|
|
288
|
+
|
|
289
|
+
return exitCode
|
|
290
|
+
}),
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
type TBuildCommandOptions = {
|
|
294
|
+
readonly cwd: string
|
|
295
|
+
readonly env: Record<string, string | undefined>
|
|
296
|
+
readonly stdin?: ChildProcess.CommandInput
|
|
297
|
+
readonly stdout?: ChildProcess.CommandOutput
|
|
298
|
+
readonly stderr?: ChildProcess.CommandOutput
|
|
299
|
+
readonly shell: boolean
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const buildCommand = (input: string | string[], options: TBuildCommandOptions): ChildProcess.Command => {
|
|
303
|
+
const commandOptions = {
|
|
304
|
+
cwd: options.cwd,
|
|
305
|
+
env: options.env,
|
|
306
|
+
extendEnv: true,
|
|
307
|
+
shell: options.shell,
|
|
308
|
+
...(options.stdin !== undefined ? { stdin: options.stdin } : {}),
|
|
309
|
+
...(options.stdout !== undefined ? { stdout: options.stdout } : {}),
|
|
310
|
+
...(options.stderr !== undefined ? { stderr: options.stderr } : {}),
|
|
311
|
+
} as const
|
|
312
|
+
|
|
313
|
+
if (Array.isArray(input) === true) {
|
|
314
|
+
const [c, ...a] = input
|
|
315
|
+
return ChildProcess.make(c ?? '', a, commandOptions)
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (options.shell === true) {
|
|
319
|
+
return ChildProcess.make(input, commandOptions)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const [c, ...a] = input.split(' ')
|
|
323
|
+
return ChildProcess.make(c ?? '', a, commandOptions)
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
type TLineTerminator = 'newline' | 'carriage-return' | 'none'
|
|
327
|
+
|
|
328
|
+
type TStreamHandler = {
|
|
329
|
+
readonly onChunk: (chunk: string) => Effect.Effect<void>
|
|
330
|
+
readonly flush: () => Effect.Effect<void>
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const makeStreamHandler = ({
|
|
334
|
+
channel,
|
|
335
|
+
mirrorTarget,
|
|
336
|
+
appendLog,
|
|
337
|
+
}: {
|
|
338
|
+
readonly channel: 'stdout' | 'stderr'
|
|
339
|
+
readonly mirrorTarget?: NodeJS.WriteStream
|
|
340
|
+
readonly appendLog: (args: { channel: 'stdout' | 'stderr'; content: string }) => Effect.Effect<void>
|
|
341
|
+
}): TStreamHandler => {
|
|
342
|
+
let buffer = ''
|
|
343
|
+
|
|
344
|
+
// Effect's FileLogger expects line-oriented messages, but the subprocess
|
|
345
|
+
// gives us arbitrary UTF-8 chunks. We keep a tiny line splitter here so the
|
|
346
|
+
// log and console stay readable (and consistent with the previous `tee`
|
|
347
|
+
// behaviour).
|
|
348
|
+
const emit = (content: string, terminator: TLineTerminator) =>
|
|
349
|
+
emitSegment({
|
|
350
|
+
channel,
|
|
351
|
+
content,
|
|
352
|
+
terminator,
|
|
353
|
+
...(mirrorTarget !== undefined ? { mirrorTarget } : {}),
|
|
354
|
+
appendLog,
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
const consumeBuffer = (): Effect.Effect<void> => {
|
|
358
|
+
if (buffer.length === 0) return Effect.void
|
|
359
|
+
|
|
360
|
+
const lastChar = buffer[buffer.length - 1]
|
|
361
|
+
if (lastChar === '\r') {
|
|
362
|
+
const line = buffer.slice(0, -1)
|
|
363
|
+
buffer = ''
|
|
364
|
+
return emit(line, 'carriage-return')
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const line = buffer
|
|
368
|
+
buffer = ''
|
|
369
|
+
return line.length === 0 ? Effect.void : emit(line, 'none')
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return {
|
|
373
|
+
onChunk: (chunk) =>
|
|
374
|
+
Effect.gen(function* () {
|
|
375
|
+
buffer += chunk
|
|
376
|
+
while (buffer.length > 0) {
|
|
377
|
+
const newlineIndex = buffer.indexOf('\n')
|
|
378
|
+
const carriageIndex = buffer.indexOf('\r')
|
|
379
|
+
|
|
380
|
+
if (newlineIndex === -1 && carriageIndex === -1) {
|
|
381
|
+
break
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
let index: number
|
|
385
|
+
let terminator: TLineTerminator
|
|
386
|
+
let skip = 1
|
|
387
|
+
|
|
388
|
+
if (carriageIndex !== -1 && (newlineIndex === -1 || carriageIndex < newlineIndex)) {
|
|
389
|
+
index = carriageIndex
|
|
390
|
+
if (carriageIndex + 1 < buffer.length && buffer[carriageIndex + 1] === '\n') {
|
|
391
|
+
skip = 2
|
|
392
|
+
terminator = 'newline'
|
|
393
|
+
} else {
|
|
394
|
+
terminator = 'carriage-return'
|
|
395
|
+
}
|
|
396
|
+
} else {
|
|
397
|
+
index = newlineIndex!
|
|
398
|
+
terminator = 'newline'
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const line = buffer.slice(0, index)
|
|
402
|
+
buffer = buffer.slice(index + skip)
|
|
403
|
+
yield* emit(line, terminator)
|
|
404
|
+
}
|
|
405
|
+
}),
|
|
406
|
+
flush: () => consumeBuffer(),
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const emitSegment = ({
|
|
411
|
+
channel,
|
|
412
|
+
content,
|
|
413
|
+
terminator,
|
|
414
|
+
mirrorTarget,
|
|
415
|
+
appendLog,
|
|
416
|
+
}: {
|
|
417
|
+
readonly channel: 'stdout' | 'stderr'
|
|
418
|
+
readonly content: string
|
|
419
|
+
readonly terminator: TLineTerminator
|
|
420
|
+
readonly mirrorTarget?: NodeJS.WriteStream
|
|
421
|
+
readonly appendLog: (args: { channel: 'stdout' | 'stderr'; content: string }) => Effect.Effect<void>
|
|
422
|
+
}) =>
|
|
423
|
+
Effect.gen(function* () {
|
|
424
|
+
if (mirrorTarget !== undefined) {
|
|
425
|
+
yield* Effect.sync(() => mirrorSegment(mirrorTarget, content, terminator))
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const contentForLog = terminator === 'carriage-return' ? `${content}\r` : content
|
|
429
|
+
|
|
430
|
+
yield* appendLog({ channel, content: contentForLog })
|
|
431
|
+
})
|
|
432
|
+
|
|
433
|
+
const mirrorSegment = (target: NodeJS.WriteStream, content: string, terminator: TLineTerminator) => {
|
|
434
|
+
switch (terminator) {
|
|
435
|
+
case 'newline': {
|
|
436
|
+
target.write(`${content}\n`)
|
|
437
|
+
break
|
|
438
|
+
}
|
|
439
|
+
case 'carriage-return': {
|
|
440
|
+
target.write(`${content}\r`)
|
|
441
|
+
break
|
|
442
|
+
}
|
|
443
|
+
case 'none': {
|
|
444
|
+
target.write(content)
|
|
445
|
+
break
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|