@livestore/utils-dev 0.4.0 → 0.5.0-dev.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.
Files changed (78) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/node/DockerCompose/DockerCompose.d.ts +52 -0
  3. package/dist/node/DockerCompose/DockerCompose.d.ts.map +1 -0
  4. package/dist/node/DockerCompose/DockerCompose.js +158 -0
  5. package/dist/node/DockerCompose/DockerCompose.js.map +1 -0
  6. package/dist/node/DockerCompose/DockerCompose.test.d.ts +2 -0
  7. package/dist/node/DockerCompose/DockerCompose.test.d.ts.map +1 -0
  8. package/dist/node/{DockerComposeService/DockerComposeService.test.js → DockerCompose/DockerCompose.test.js} +16 -16
  9. package/dist/node/DockerCompose/DockerCompose.test.js.map +1 -0
  10. package/dist/node/DockerCompose/mod.d.ts +2 -0
  11. package/dist/node/DockerCompose/mod.d.ts.map +1 -0
  12. package/dist/node/DockerCompose/mod.js +2 -0
  13. package/dist/node/DockerCompose/mod.js.map +1 -0
  14. package/dist/node/FileLogger.d.ts +15 -8
  15. package/dist/node/FileLogger.d.ts.map +1 -1
  16. package/dist/node/FileLogger.js +63 -79
  17. package/dist/node/FileLogger.js.map +1 -1
  18. package/dist/node/cmd-log.d.ts.map +1 -1
  19. package/dist/node/cmd-log.js +19 -8
  20. package/dist/node/cmd-log.js.map +1 -1
  21. package/dist/node/cmd.d.ts +10 -12
  22. package/dist/node/cmd.d.ts.map +1 -1
  23. package/dist/node/cmd.js +76 -39
  24. package/dist/node/cmd.js.map +1 -1
  25. package/dist/node/cmd.test.js +19 -19
  26. package/dist/node/cmd.test.js.map +1 -1
  27. package/dist/node/mod.d.ts +4 -5
  28. package/dist/node/mod.d.ts.map +1 -1
  29. package/dist/node/mod.js +63 -60
  30. package/dist/node/mod.js.map +1 -1
  31. package/dist/node/workspace.d.ts +2 -2
  32. package/dist/node/workspace.d.ts.map +1 -1
  33. package/dist/node/workspace.js +7 -7
  34. package/dist/node/workspace.js.map +1 -1
  35. package/dist/node-vitest/Vitest.d.ts +8 -40
  36. package/dist/node-vitest/Vitest.d.ts.map +1 -1
  37. package/dist/node-vitest/Vitest.js +7 -78
  38. package/dist/node-vitest/Vitest.js.map +1 -1
  39. package/dist/node-vitest/mod.js +2 -2
  40. package/dist/wrangler/WranglerDevServer.d.ts +15 -20
  41. package/dist/wrangler/WranglerDevServer.d.ts.map +1 -1
  42. package/dist/wrangler/WranglerDevServer.js +77 -71
  43. package/dist/wrangler/WranglerDevServer.js.map +1 -1
  44. package/dist/wrangler/WranglerDevServer.test.js +28 -28
  45. package/dist/wrangler/WranglerDevServer.test.js.map +1 -1
  46. package/dist/wrangler/fixtures/cf-worker.d.ts.map +1 -1
  47. package/dist/wrangler/mod.d.ts +1 -1
  48. package/dist/wrangler/mod.d.ts.map +1 -1
  49. package/dist/wrangler/mod.js +1 -1
  50. package/dist/wrangler/mod.js.map +1 -1
  51. package/package.json +40 -58
  52. package/src/node/{DockerComposeService/DockerComposeService.test.ts → DockerCompose/DockerCompose.test.ts} +18 -18
  53. package/src/node/DockerCompose/DockerCompose.ts +328 -0
  54. package/src/node/DockerCompose/mod.ts +1 -0
  55. package/src/node/FileLogger.ts +89 -114
  56. package/src/node/cmd-log.ts +21 -10
  57. package/src/node/cmd.test.ts +19 -17
  58. package/src/node/cmd.ts +117 -88
  59. package/src/node/mod.ts +112 -104
  60. package/src/node/workspace.ts +9 -10
  61. package/src/node-vitest/Vitest.ts +21 -172
  62. package/src/wrangler/WranglerDevServer.test.ts +33 -37
  63. package/src/wrangler/WranglerDevServer.ts +145 -129
  64. package/src/wrangler/mod.ts +1 -6
  65. package/dist/node/DockerComposeService/DockerComposeService.d.ts +0 -63
  66. package/dist/node/DockerComposeService/DockerComposeService.d.ts.map +0 -1
  67. package/dist/node/DockerComposeService/DockerComposeService.js +0 -149
  68. package/dist/node/DockerComposeService/DockerComposeService.js.map +0 -1
  69. package/dist/node/DockerComposeService/DockerComposeService.test.d.ts +0 -2
  70. package/dist/node/DockerComposeService/DockerComposeService.test.d.ts.map +0 -1
  71. package/dist/node/DockerComposeService/DockerComposeService.test.js.map +0 -1
  72. package/dist/node-vitest/Vitest.test.d.ts +0 -10
  73. package/dist/node-vitest/Vitest.test.d.ts.map +0 -1
  74. package/dist/node-vitest/Vitest.test.js +0 -74
  75. package/dist/node-vitest/Vitest.test.js.map +0 -1
  76. package/src/node/DockerComposeService/DockerComposeService.ts +0 -343
  77. package/src/node-vitest/Vitest.test.ts +0 -117
  78. /package/src/node/{DockerComposeService → DockerCompose}/test-fixtures/docker-compose.yml +0 -0
@@ -1,37 +1,39 @@
1
1
  import fs from 'node:fs'
2
2
  import path from 'node:path'
3
- import { CommandExecutor, Duration, Effect, Layer } from '@livestore/utils/effect'
4
- import { PlatformNode } from '@livestore/utils/node'
5
- import { Vitest } from '@livestore/utils-dev/node-vitest'
3
+
6
4
  import { expect } from 'vitest'
7
5
 
6
+ import { Vitest } from '@livestore/utils-dev/node-vitest'
7
+ import { ChildProcessSpawner, Duration, Effect, Layer } from '@livestore/utils/effect'
8
+ import { PlatformNode } from '@livestore/utils/node'
9
+
8
10
  import { cmd } from './cmd.ts'
9
11
  import { CurrentWorkingDirectory } from './workspace.ts'
10
12
 
11
13
  const withNode = Vitest.makeWithTestCtx({
12
- makeLayer: () => Layer.mergeAll(PlatformNode.NodeContext.layer, CurrentWorkingDirectory.live),
14
+ makeLayer: () => Layer.mergeAll(PlatformNode.NodeServices.layer, CurrentWorkingDirectory.live),
13
15
  timeout: 20_000,
14
16
  })
15
17
 
16
18
  Vitest.describe('cmd helper', () => {
17
19
  const ansiRegex = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, 'g')
18
20
 
19
- Vitest.scopedLive('runs tokenized string without shell', (test) =>
21
+ Vitest.live('runs tokenized string without shell', (test) =>
20
22
  Effect.gen(function* () {
21
23
  const exit = yield* cmd('printf ok')
22
- expect(exit).toBe(CommandExecutor.ExitCode(0))
24
+ expect(exit).toBe(ChildProcessSpawner.ExitCode(0))
23
25
  }).pipe(withNode(test)),
24
26
  )
25
27
 
26
- Vitest.scopedLive('runs array input', (test) =>
28
+ Vitest.live('runs array input', (test) =>
27
29
  Effect.gen(function* () {
28
30
  const exit = yield* cmd(['printf', 'ok'])
29
- expect(exit).toBe(CommandExecutor.ExitCode(0))
31
+ expect(exit).toBe(ChildProcessSpawner.ExitCode(0))
30
32
  }).pipe(withNode(test)),
31
33
  )
32
34
 
33
35
  /** TODO(#1020): Investigate CI timeout flakiness for cmd tests that spawn many child processes */
34
- Vitest.scopedLive(
36
+ Vitest.live(
35
37
  'supports logging with archive + retention',
36
38
  (test) =>
37
39
  Effect.gen(function* () {
@@ -40,7 +42,7 @@ Vitest.describe('cmd helper', () => {
40
42
 
41
43
  // first run
42
44
  const exit1 = yield* cmd('printf first', { logDir: logsDir })
43
- expect(exit1).toBe(CommandExecutor.ExitCode(0))
45
+ expect(exit1).toBe(ChildProcessSpawner.ExitCode(0))
44
46
  const current = path.join(logsDir, 'dev.log')
45
47
  expect(fs.existsSync(current)).toBe(true)
46
48
  const firstLog = fs.readFileSync(current, 'utf8')
@@ -48,13 +50,13 @@ Vitest.describe('cmd helper', () => {
48
50
  expect(firstStdoutLines.length).toBeGreaterThan(0)
49
51
  for (const line of firstStdoutLines) {
50
52
  expect(line).toContain('[stdout] first')
51
- expect(line).toContain('INFO')
53
+ expect(line).toMatch(/Info/i)
52
54
  expect(line).toContain('printf first')
53
55
  }
54
56
 
55
57
  // second run — archives previous
56
58
  const exit2 = yield* cmd('printf second', { logDir: logsDir })
57
- expect(exit2).toBe(CommandExecutor.ExitCode(0))
59
+ expect(exit2).toBe(ChildProcessSpawner.ExitCode(0))
58
60
  const archiveDir = path.join(logsDir, 'archive')
59
61
  const archives = fs.readdirSync(archiveDir).filter((f) => f.endsWith('.log'))
60
62
  expect(archives.length).toBe(1)
@@ -71,7 +73,7 @@ Vitest.describe('cmd helper', () => {
71
73
  expect(secondStdoutLines.length).toBeGreaterThan(0)
72
74
  for (const line of secondStdoutLines) {
73
75
  expect(line).toContain('[stdout] second')
74
- expect(line).toContain('INFO')
76
+ expect(line).toMatch(/Info/i)
75
77
  }
76
78
 
77
79
  // generate many archives to exercise retention (keep 50)
@@ -85,7 +87,7 @@ Vitest.describe('cmd helper', () => {
85
87
  { timeout: 30_000, retry: 3 },
86
88
  )
87
89
 
88
- Vitest.scopedLive('streams stdout and stderr with logger formatting', (test) =>
90
+ Vitest.live('streams stdout and stderr with logger formatting', (test) =>
89
91
  Effect.gen(function* () {
90
92
  const workspace = process.env.WORKSPACE_ROOT!
91
93
  const logsDir = path.join(workspace, 'tmp', 'cmd-tests', `format-${Date.now()}`)
@@ -93,7 +95,7 @@ Vitest.describe('cmd helper', () => {
93
95
  const exit = yield* cmd(['node', '-e', "console.log('out'); console.error('err')"], {
94
96
  logDir: logsDir,
95
97
  })
96
- expect(exit).toBe(CommandExecutor.ExitCode(0))
98
+ expect(exit).toBe(ChildProcessSpawner.ExitCode(0))
97
99
 
98
100
  const current = path.join(logsDir, 'dev.log')
99
101
  const logContent = fs.readFileSync(current, 'utf8')
@@ -110,13 +112,13 @@ Vitest.describe('cmd helper', () => {
110
112
  for (const line of relevantLines) {
111
113
  const stripped = line.replace(ansiRegex, '')
112
114
  expect(stripped.startsWith('[')).toBe(true)
113
- expect(stripped).toMatch(/(INFO|WARN)/)
115
+ expect(stripped).toMatch(/(Info|Warn)/i)
114
116
  expect(stripped).toMatch(/\[(stdout|stderr)]/)
115
117
  }
116
118
  }).pipe(withNode(test)),
117
119
  )
118
120
 
119
- Vitest.scopedLive('cleans up logged child process when interrupted', (test) =>
121
+ Vitest.live('cleans up logged child process when interrupted', (test) =>
120
122
  Effect.gen(function* () {
121
123
  const workspace = process.env.WORKSPACE_ROOT!
122
124
  const logsDir = path.join(workspace, 'tmp', 'cmd-tests', `timeout-${Date.now()}`)
package/src/node/cmd.ts CHANGED
@@ -1,29 +1,24 @@
1
- import fs from 'node:fs'
2
-
3
- import { isNotUndefined } from '@livestore/utils'
4
1
  import {
5
2
  Cause,
6
- Command,
7
- type CommandExecutor,
3
+ ChildProcess,
4
+ ChildProcessSpawner,
8
5
  Effect,
9
6
  Fiber,
10
- FiberId,
11
- FiberRefs,
12
- HashMap,
13
- identity,
14
- List,
15
- LogLevel,
7
+ Logger,
16
8
  type PlatformError,
9
+ Predicate,
10
+ References,
17
11
  Schema,
18
12
  Stream,
19
13
  } from '@livestore/utils/effect'
14
+ import { NodeFileSystem } from '@livestore/utils/node'
20
15
 
21
16
  import { applyLoggingToCommand } from './cmd-log.ts'
22
17
  import * as FileLogger from './FileLogger.ts'
23
18
  import { CurrentWorkingDirectory } from './workspace.ts'
24
19
 
25
20
  // Branded zero value so we can compare exit codes without touching internals.
26
- const SUCCESS_EXIT_CODE: CommandExecutor.ExitCode = 0 as CommandExecutor.ExitCode
21
+ const SUCCESS_EXIT_CODE: ChildProcessSpawner.ExitCode = ChildProcessSpawner.ExitCode(0)
27
22
 
28
23
  export const cmd: (
29
24
  commandInput: string | (string | undefined)[],
@@ -43,14 +38,14 @@ export const cmd: (
43
38
  logRetention?: number
44
39
  },
45
40
  ) => Effect.Effect<
46
- CommandExecutor.ExitCode,
41
+ ChildProcessSpawner.ExitCode,
47
42
  PlatformError.PlatformError | CmdError,
48
- CommandExecutor.CommandExecutor | CurrentWorkingDirectory
43
+ ChildProcessSpawner.ChildProcessSpawner | CurrentWorkingDirectory
49
44
  > = Effect.fn('cmd')(function* (commandInput, options) {
50
45
  const cwd = yield* CurrentWorkingDirectory
51
46
 
52
47
  const asArray = Array.isArray(commandInput)
53
- const parts = asArray === true ? commandInput.filter(isNotUndefined) : undefined
48
+ const parts = asArray === true ? commandInput.filter(Predicate.isNotUndefined) : undefined
54
49
  const [command, ...args] = asArray === true ? (parts as string[]) : commandInput.split(' ')
55
50
 
56
51
  const debugEnvStr = Object.entries(options?.env ?? {})
@@ -89,7 +84,7 @@ export const cmd: (
89
84
  useShell,
90
85
  } as const
91
86
 
92
- const exitCode = yield* isNotUndefined(logPath) === true
87
+ const exitCode = yield* Predicate.isNotUndefined(logPath) === true
93
88
  ? Effect.gen(function* () {
94
89
  yield* Effect.sync(() => console.log(`Logging output to ${logPath}`))
95
90
  return yield* runWithLogging({ ...baseArgs, logPath, threadName: commandDebugStr })
@@ -116,37 +111,42 @@ export const cmdText: (
116
111
  runInShell?: boolean
117
112
  env?: Record<string, string | undefined>
118
113
  },
119
- ) => Effect.Effect<string, PlatformError.PlatformError, CommandExecutor.CommandExecutor | CurrentWorkingDirectory> =
120
- Effect.fn('cmdText')(function* (commandInput, options) {
121
- const cwd = yield* CurrentWorkingDirectory
122
- const [command, ...args] =
123
- Array.isArray(commandInput) === true ? commandInput.filter(isNotUndefined) : commandInput.split(' ')
124
- const debugEnvStr = Object.entries(options?.env ?? {})
125
- .map(([key, value]) => `${key}='${String(value)}' `)
126
- .join('')
127
-
128
- const commandDebugStr = debugEnvStr + [command, ...args].join(' ')
129
- const subshellStr = options?.runInShell === true ? ' (in subshell)' : ''
130
-
131
- yield* Effect.logDebug(`Running '${commandDebugStr}' in '${cwd}'${subshellStr}`)
132
- yield* Effect.annotateCurrentSpan({ 'span.label': commandDebugStr, command, cwd })
133
-
134
- return yield* Command.make(command!, ...args).pipe(
135
- // inherit = Stream stderr to process.stderr, pipe = Stream stderr to process.stdout
136
- Command.stderr(options?.stderr ?? 'inherit'),
137
- Command.workingDirectory(cwd),
138
- options?.runInShell === true ? Command.runInShell(true) : identity,
139
- Command.env(options?.env ?? {}),
140
- Command.string,
141
- )
142
- })
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)' : ''
129
+
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
143
 
144
144
  export class CmdError extends Schema.TaggedError<CmdError>('~@livestore/utils-dev/CmdError')('CmdError', {
145
145
  command: Schema.String,
146
146
  args: Schema.Array(Schema.String),
147
147
  cwd: Schema.String,
148
- env: Schema.Record({ key: Schema.String, value: Schema.String.pipe(Schema.UndefinedOr) }),
149
- stderr: Schema.Literal('inherit', 'pipe'),
148
+ env: Schema.Record(Schema.String, Schema.String.pipe(Schema.UndefinedOr)),
149
+ stderr: Schema.Literals(['inherit', 'pipe']),
150
150
  }) {}
151
151
 
152
152
  type TRunBaseArgs = {
@@ -159,15 +159,19 @@ type TRunBaseArgs = {
159
159
  }
160
160
 
161
161
  const runWithoutLogging = ({ commandInput, cwd, env, stdoutMode, stderrMode, useShell }: TRunBaseArgs) =>
162
- buildCommand(commandInput, useShell).pipe(
163
- Command.stdin('inherit'),
164
- Command.stdout(stdoutMode),
165
- Command.stderr(stderrMode),
166
- Command.workingDirectory(cwd),
167
- useShell === true ? Command.runInShell(true) : identity,
168
- Command.env(env),
169
- Command.exitCode,
170
- )
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
+ })
171
175
 
172
176
  type TRunWithLoggingArgs = TRunBaseArgs & {
173
177
  readonly logPath: string
@@ -192,47 +196,53 @@ const runWithLogging = ({
192
196
  Effect.gen(function* () {
193
197
  const envWithColor = env.FORCE_COLOR === undefined ? { ...env, FORCE_COLOR: '1' } : env
194
198
 
195
- const logFile = yield* Effect.acquireRelease(
196
- Effect.sync(() => fs.openSync(logPath, 'a', 0o666)),
197
- (fd) => Effect.sync(() => fs.closeSync(fd)),
198
- )
199
-
200
- const prettyLogger = FileLogger.prettyLoggerTty({
199
+ const prettyLoggerOptions = {
201
200
  colors: true,
202
201
  stderr: false,
203
- formatDate: (date) => `${FileLogger.defaultDateFormat(date)} ${threadName}`,
204
- })
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])
205
211
 
206
212
  const appendLog = ({ channel, content }: { channel: 'stdout' | 'stderr'; content: string }) =>
207
- Effect.sync(() => {
208
- const formatted = prettyLogger.log({
209
- fiberId: FiberId.none,
210
- logLevel: channel === 'stdout' ? LogLevel.Info : LogLevel.Warning,
213
+ Effect.gen(function* () {
214
+ const date = new Date()
215
+ const formatted = FileLogger.formatPrettyLog({
216
+ logLevel: channel === 'stdout' ? 'Info' : 'Warn',
211
217
  message: [`[${channel}]${content.length > 0 ? ` ${content}` : ''}`],
212
218
  cause: Cause.empty,
213
- context: FiberRefs.empty(),
214
- spans: List.empty(),
215
- annotations: HashMap.empty(),
216
- date: new Date(),
219
+ date,
220
+ fiberId: 0,
221
+ spans: [[threadName, date.getTime()]],
222
+ annotations: { thread: threadName },
223
+ options: prettyLoggerOptions,
217
224
  })
218
- fs.writeSync(logFile, formatted)
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
+ )
219
230
  })
220
231
 
221
- const command = buildCommand(commandInput, useShell).pipe(
222
- Command.stdin('inherit'),
223
- Command.stdout('pipe'),
224
- Command.stderr('pipe'),
225
- Command.workingDirectory(cwd),
226
- useShell === true ? Command.runInShell(true) : identity,
227
- Command.env(envWithColor),
228
- )
232
+ const command = buildCommand(commandInput, {
233
+ cwd,
234
+ env: envWithColor,
235
+ stdin: 'inherit',
236
+ stdout: 'pipe',
237
+ stderr: 'pipe',
238
+ shell: useShell,
239
+ })
229
240
 
230
241
  // Acquire/start the command and make sure we kill it on interruption.
231
- const runningProcess = yield* Effect.acquireRelease(command.pipe(Command.start), (proc) =>
242
+ const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
243
+ const runningProcess = yield* Effect.acquireRelease(spawner.spawn(command), (proc) =>
232
244
  proc.isRunning.pipe(
233
- Effect.flatMap((running) =>
234
- running === true ? proc.kill().pipe(Effect.catchAll(() => Effect.void)) : Effect.void,
235
- ),
245
+ Effect.flatMap((running) => (running === true ? proc.kill().pipe(Effect.ignore) : Effect.void)),
236
246
  Effect.ignore,
237
247
  ),
238
248
  )
@@ -249,20 +259,20 @@ const runWithLogging = ({
249
259
  })
250
260
 
251
261
  const stdoutFiber = yield* runningProcess.stdout.pipe(
252
- Stream.decodeText('utf8'),
262
+ Stream.decodeText({ encoding: 'utf8' }),
253
263
  Stream.runForEach((chunk) => stdoutHandler.onChunk(chunk)),
254
264
  Effect.forkScoped,
255
265
  )
256
266
 
257
267
  const stderrFiber = yield* runningProcess.stderr.pipe(
258
- Stream.decodeText('utf8'),
268
+ Stream.decodeText({ encoding: 'utf8' }),
259
269
  Stream.runForEach((chunk) => stderrHandler.onChunk(chunk)),
260
270
  Effect.forkScoped,
261
271
  )
262
272
 
263
273
  // Dump any buffered data and finish both stream fibers before we return.
264
274
  const flushOutputs = Effect.gen(function* () {
265
- const stillRunning = yield* runningProcess.isRunning.pipe(Effect.catchAll(() => Effect.succeed(false)))
275
+ const stillRunning = yield* runningProcess.isRunning.pipe(Effect.orElseSucceed(() => false))
266
276
  if (stillRunning === true) {
267
277
  yield* Effect.ignore(runningProcess.kill())
268
278
  }
@@ -278,18 +288,37 @@ const runWithLogging = ({
278
288
  }),
279
289
  )
280
290
 
281
- const buildCommand = (input: string | string[], useShell: boolean) => {
291
+ type TBuildCommandOptions = {
292
+ readonly cwd: string
293
+ readonly env: Record<string, string | undefined>
294
+ readonly stdin?: ChildProcess.CommandInput
295
+ readonly stdout?: ChildProcess.CommandOutput
296
+ readonly stderr?: ChildProcess.CommandOutput
297
+ readonly shell: boolean
298
+ }
299
+
300
+ const buildCommand = (input: string | string[], options: TBuildCommandOptions): ChildProcess.Command => {
301
+ const commandOptions = {
302
+ cwd: options.cwd,
303
+ env: options.env,
304
+ extendEnv: true,
305
+ shell: options.shell,
306
+ ...(options.stdin !== undefined ? { stdin: options.stdin } : {}),
307
+ ...(options.stdout !== undefined ? { stdout: options.stdout } : {}),
308
+ ...(options.stderr !== undefined ? { stderr: options.stderr } : {}),
309
+ } as const
310
+
282
311
  if (Array.isArray(input) === true) {
283
312
  const [c, ...a] = input
284
- return Command.make(c!, ...a)
313
+ return ChildProcess.make(c ?? '', a, commandOptions)
285
314
  }
286
315
 
287
- if (useShell === true) {
288
- return Command.make(input)
316
+ if (options.shell === true) {
317
+ return ChildProcess.make(input, commandOptions)
289
318
  }
290
319
 
291
320
  const [c, ...a] = input.split(' ')
292
- return Command.make(c!, ...a)
321
+ return ChildProcess.make(c ?? '', a, commandOptions)
293
322
  }
294
323
 
295
324
  type TLineTerminator = 'newline' | 'carriage-return' | 'none'