@effect/platform-node-shared 0.57.0 → 4.0.0-beta.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.
Files changed (218) hide show
  1. package/README.md +3 -3
  2. package/dist/NodeChildProcessSpawner.d.ts +37 -0
  3. package/dist/NodeChildProcessSpawner.d.ts.map +1 -0
  4. package/dist/NodeChildProcessSpawner.js +567 -0
  5. package/dist/NodeChildProcessSpawner.js.map +1 -0
  6. package/dist/{dts/NodeClusterSocket.d.ts → NodeClusterSocket.d.ts} +4 -7
  7. package/dist/NodeClusterSocket.d.ts.map +1 -0
  8. package/dist/{esm/NodeClusterSocket.js → NodeClusterSocket.js} +9 -10
  9. package/dist/NodeClusterSocket.js.map +1 -0
  10. package/dist/NodeFileSystem.d.ts +8 -0
  11. package/dist/NodeFileSystem.d.ts.map +1 -0
  12. package/dist/{esm/internal/fileSystem.js → NodeFileSystem.js} +125 -96
  13. package/dist/NodeFileSystem.js.map +1 -0
  14. package/dist/NodePath.d.ts +18 -0
  15. package/dist/NodePath.d.ts.map +1 -0
  16. package/dist/NodePath.js +56 -0
  17. package/dist/NodePath.js.map +1 -0
  18. package/dist/NodeRuntime.d.ts +28 -0
  19. package/dist/NodeRuntime.d.ts.map +1 -0
  20. package/dist/{esm/internal/runtime.js → NodeRuntime.js} +8 -8
  21. package/dist/NodeRuntime.js.map +1 -0
  22. package/dist/NodeSink.d.ts +40 -0
  23. package/dist/NodeSink.d.ts.map +1 -0
  24. package/dist/NodeSink.js +50 -0
  25. package/dist/NodeSink.js.map +1 -0
  26. package/dist/{dts/NodeSocket.d.ts → NodeSocket.d.ts} +10 -10
  27. package/dist/NodeSocket.d.ts.map +1 -0
  28. package/dist/{esm/NodeSocket.js → NodeSocket.js} +51 -39
  29. package/dist/NodeSocket.js.map +1 -0
  30. package/dist/{dts/NodeSocketServer.d.ts → NodeSocketServer.d.ts} +8 -10
  31. package/dist/NodeSocketServer.d.ts.map +1 -0
  32. package/dist/NodeSocketServer.js +192 -0
  33. package/dist/NodeSocketServer.js.map +1 -0
  34. package/dist/NodeStdio.d.ts +11 -0
  35. package/dist/NodeStdio.d.ts.map +1 -0
  36. package/dist/NodeStdio.js +43 -0
  37. package/dist/NodeStdio.js.map +1 -0
  38. package/dist/NodeStream.d.ts +127 -0
  39. package/dist/NodeStream.d.ts.map +1 -0
  40. package/dist/NodeStream.js +249 -0
  41. package/dist/NodeStream.js.map +1 -0
  42. package/dist/NodeTerminal.d.ts +15 -0
  43. package/dist/NodeTerminal.d.ts.map +1 -0
  44. package/dist/{esm/internal/terminal.js → NodeTerminal.js} +28 -21
  45. package/dist/NodeTerminal.js.map +1 -0
  46. package/dist/internal/utils.d.ts +2 -0
  47. package/dist/internal/utils.d.ts.map +1 -0
  48. package/dist/{esm/internal/error.js → internal/utils.js} +4 -5
  49. package/dist/internal/utils.js.map +1 -0
  50. package/package.json +53 -124
  51. package/src/NodeChildProcessSpawner.ts +713 -0
  52. package/src/NodeClusterSocket.ts +12 -13
  53. package/src/NodeFileSystem.ts +632 -5
  54. package/src/NodePath.ts +48 -9
  55. package/src/NodeRuntime.ts +53 -4
  56. package/src/NodeSink.ts +65 -62
  57. package/src/NodeSocket.ts +65 -49
  58. package/src/NodeSocketServer.ts +108 -88
  59. package/src/NodeStdio.ts +49 -0
  60. package/src/NodeStream.ts +324 -83
  61. package/src/NodeTerminal.ts +100 -9
  62. package/src/internal/{error.ts → utils.ts} +6 -7
  63. package/NodeClusterSocket/package.json +0 -6
  64. package/NodeCommandExecutor/package.json +0 -6
  65. package/NodeFileSystem/ParcelWatcher/package.json +0 -6
  66. package/NodeFileSystem/package.json +0 -6
  67. package/NodeKeyValueStore/package.json +0 -6
  68. package/NodeMultipart/package.json +0 -6
  69. package/NodePath/package.json +0 -6
  70. package/NodeRuntime/package.json +0 -6
  71. package/NodeSink/package.json +0 -6
  72. package/NodeSocket/package.json +0 -6
  73. package/NodeSocketServer/package.json +0 -6
  74. package/NodeStream/package.json +0 -6
  75. package/NodeTerminal/package.json +0 -6
  76. package/dist/cjs/NodeClusterSocket.js +0 -50
  77. package/dist/cjs/NodeClusterSocket.js.map +0 -1
  78. package/dist/cjs/NodeCommandExecutor.js +0 -14
  79. package/dist/cjs/NodeCommandExecutor.js.map +0 -1
  80. package/dist/cjs/NodeFileSystem/ParcelWatcher.js +0 -20
  81. package/dist/cjs/NodeFileSystem/ParcelWatcher.js.map +0 -1
  82. package/dist/cjs/NodeFileSystem.js +0 -18
  83. package/dist/cjs/NodeFileSystem.js.map +0 -1
  84. package/dist/cjs/NodeKeyValueStore.js +0 -18
  85. package/dist/cjs/NodeKeyValueStore.js.map +0 -1
  86. package/dist/cjs/NodeMultipart.js +0 -24
  87. package/dist/cjs/NodeMultipart.js.map +0 -1
  88. package/dist/cjs/NodePath.js +0 -28
  89. package/dist/cjs/NodePath.js.map +0 -1
  90. package/dist/cjs/NodeRuntime.js +0 -14
  91. package/dist/cjs/NodeRuntime.js.map +0 -1
  92. package/dist/cjs/NodeSink.js +0 -50
  93. package/dist/cjs/NodeSink.js.map +0 -1
  94. package/dist/cjs/NodeSocket.js +0 -153
  95. package/dist/cjs/NodeSocket.js.map +0 -1
  96. package/dist/cjs/NodeSocketServer.js +0 -178
  97. package/dist/cjs/NodeSocketServer.js.map +0 -1
  98. package/dist/cjs/NodeStream.js +0 -76
  99. package/dist/cjs/NodeStream.js.map +0 -1
  100. package/dist/cjs/NodeTerminal.js +0 -19
  101. package/dist/cjs/NodeTerminal.js.map +0 -1
  102. package/dist/cjs/internal/commandExecutor.js +0 -153
  103. package/dist/cjs/internal/commandExecutor.js.map +0 -1
  104. package/dist/cjs/internal/error.js +0 -45
  105. package/dist/cjs/internal/error.js.map +0 -1
  106. package/dist/cjs/internal/fileSystem/parcelWatcher.js +0 -68
  107. package/dist/cjs/internal/fileSystem/parcelWatcher.js.map +0 -1
  108. package/dist/cjs/internal/fileSystem.js +0 -400
  109. package/dist/cjs/internal/fileSystem.js.map +0 -1
  110. package/dist/cjs/internal/multipart.js +0 -147
  111. package/dist/cjs/internal/multipart.js.map +0 -1
  112. package/dist/cjs/internal/path.js +0 -53
  113. package/dist/cjs/internal/path.js.map +0 -1
  114. package/dist/cjs/internal/runtime.js +0 -37
  115. package/dist/cjs/internal/runtime.js.map +0 -1
  116. package/dist/cjs/internal/sink.js +0 -28
  117. package/dist/cjs/internal/sink.js.map +0 -1
  118. package/dist/cjs/internal/stream.js +0 -233
  119. package/dist/cjs/internal/stream.js.map +0 -1
  120. package/dist/cjs/internal/terminal.js +0 -86
  121. package/dist/cjs/internal/terminal.js.map +0 -1
  122. package/dist/dts/NodeClusterSocket.d.ts.map +0 -1
  123. package/dist/dts/NodeCommandExecutor.d.ts +0 -12
  124. package/dist/dts/NodeCommandExecutor.d.ts.map +0 -1
  125. package/dist/dts/NodeFileSystem/ParcelWatcher.d.ts +0 -13
  126. package/dist/dts/NodeFileSystem/ParcelWatcher.d.ts.map +0 -1
  127. package/dist/dts/NodeFileSystem.d.ts +0 -11
  128. package/dist/dts/NodeFileSystem.d.ts.map +0 -1
  129. package/dist/dts/NodeKeyValueStore.d.ts +0 -12
  130. package/dist/dts/NodeKeyValueStore.d.ts.map +0 -1
  131. package/dist/dts/NodeMultipart.d.ts +0 -27
  132. package/dist/dts/NodeMultipart.d.ts.map +0 -1
  133. package/dist/dts/NodePath.d.ts +0 -21
  134. package/dist/dts/NodePath.d.ts.map +0 -1
  135. package/dist/dts/NodeRuntime.d.ts +0 -10
  136. package/dist/dts/NodeRuntime.d.ts.map +0 -1
  137. package/dist/dts/NodeSink.d.ts +0 -36
  138. package/dist/dts/NodeSink.d.ts.map +0 -1
  139. package/dist/dts/NodeSocket.d.ts.map +0 -1
  140. package/dist/dts/NodeSocketServer.d.ts.map +0 -1
  141. package/dist/dts/NodeStream.d.ts +0 -119
  142. package/dist/dts/NodeStream.d.ts.map +0 -1
  143. package/dist/dts/NodeTerminal.d.ts +0 -18
  144. package/dist/dts/NodeTerminal.d.ts.map +0 -1
  145. package/dist/dts/internal/commandExecutor.d.ts +0 -2
  146. package/dist/dts/internal/commandExecutor.d.ts.map +0 -1
  147. package/dist/dts/internal/error.d.ts +0 -2
  148. package/dist/dts/internal/error.d.ts.map +0 -1
  149. package/dist/dts/internal/fileSystem/parcelWatcher.d.ts +0 -4
  150. package/dist/dts/internal/fileSystem/parcelWatcher.d.ts.map +0 -1
  151. package/dist/dts/internal/fileSystem.d.ts +0 -2
  152. package/dist/dts/internal/fileSystem.d.ts.map +0 -1
  153. package/dist/dts/internal/multipart.d.ts +0 -2
  154. package/dist/dts/internal/multipart.d.ts.map +0 -1
  155. package/dist/dts/internal/path.d.ts +0 -2
  156. package/dist/dts/internal/path.d.ts.map +0 -1
  157. package/dist/dts/internal/runtime.d.ts +0 -2
  158. package/dist/dts/internal/runtime.d.ts.map +0 -1
  159. package/dist/dts/internal/sink.d.ts +0 -2
  160. package/dist/dts/internal/sink.d.ts.map +0 -1
  161. package/dist/dts/internal/stream.d.ts +0 -2
  162. package/dist/dts/internal/stream.d.ts.map +0 -1
  163. package/dist/dts/internal/terminal.d.ts +0 -2
  164. package/dist/dts/internal/terminal.d.ts.map +0 -1
  165. package/dist/esm/NodeClusterSocket.js.map +0 -1
  166. package/dist/esm/NodeCommandExecutor.js +0 -7
  167. package/dist/esm/NodeCommandExecutor.js.map +0 -1
  168. package/dist/esm/NodeFileSystem/ParcelWatcher.js +0 -12
  169. package/dist/esm/NodeFileSystem/ParcelWatcher.js.map +0 -1
  170. package/dist/esm/NodeFileSystem.js +0 -10
  171. package/dist/esm/NodeFileSystem.js.map +0 -1
  172. package/dist/esm/NodeKeyValueStore.js +0 -10
  173. package/dist/esm/NodeKeyValueStore.js.map +0 -1
  174. package/dist/esm/NodeMultipart.js +0 -17
  175. package/dist/esm/NodeMultipart.js.map +0 -1
  176. package/dist/esm/NodePath.js +0 -20
  177. package/dist/esm/NodePath.js.map +0 -1
  178. package/dist/esm/NodeRuntime.js +0 -7
  179. package/dist/esm/NodeRuntime.js.map +0 -1
  180. package/dist/esm/NodeSink.js +0 -43
  181. package/dist/esm/NodeSink.js.map +0 -1
  182. package/dist/esm/NodeSocket.js.map +0 -1
  183. package/dist/esm/NodeSocketServer.js +0 -167
  184. package/dist/esm/NodeSocketServer.js.map +0 -1
  185. package/dist/esm/NodeStream.js +0 -69
  186. package/dist/esm/NodeStream.js.map +0 -1
  187. package/dist/esm/NodeTerminal.js +0 -12
  188. package/dist/esm/NodeTerminal.js.map +0 -1
  189. package/dist/esm/internal/commandExecutor.js +0 -146
  190. package/dist/esm/internal/commandExecutor.js.map +0 -1
  191. package/dist/esm/internal/error.js.map +0 -1
  192. package/dist/esm/internal/fileSystem/parcelWatcher.js +0 -61
  193. package/dist/esm/internal/fileSystem/parcelWatcher.js.map +0 -1
  194. package/dist/esm/internal/fileSystem.js.map +0 -1
  195. package/dist/esm/internal/multipart.js +0 -137
  196. package/dist/esm/internal/multipart.js.map +0 -1
  197. package/dist/esm/internal/path.js +0 -46
  198. package/dist/esm/internal/path.js.map +0 -1
  199. package/dist/esm/internal/runtime.js.map +0 -1
  200. package/dist/esm/internal/sink.js +0 -19
  201. package/dist/esm/internal/sink.js.map +0 -1
  202. package/dist/esm/internal/stream.js +0 -217
  203. package/dist/esm/internal/stream.js.map +0 -1
  204. package/dist/esm/internal/terminal.js.map +0 -1
  205. package/dist/esm/package.json +0 -4
  206. package/src/NodeCommandExecutor.ts +0 -13
  207. package/src/NodeFileSystem/ParcelWatcher.ts +0 -15
  208. package/src/NodeKeyValueStore.ts +0 -20
  209. package/src/NodeMultipart.ts +0 -40
  210. package/src/internal/commandExecutor.ts +0 -251
  211. package/src/internal/fileSystem/parcelWatcher.ts +0 -64
  212. package/src/internal/fileSystem.ts +0 -648
  213. package/src/internal/multipart.ts +0 -141
  214. package/src/internal/path.ts +0 -63
  215. package/src/internal/runtime.ts +0 -34
  216. package/src/internal/sink.ts +0 -57
  217. package/src/internal/stream.ts +0 -375
  218. package/src/internal/terminal.ts +0 -100
@@ -0,0 +1,713 @@
1
+ /**
2
+ * Node.js implementation of `ChildProcessSpawner`.
3
+ *
4
+ * @since 4.0.0
5
+ */
6
+ import type * as Arr from "effect/Array"
7
+ import * as Deferred from "effect/Deferred"
8
+ import * as Effect from "effect/Effect"
9
+ import * as Exit from "effect/Exit"
10
+ import * as FileSystem from "effect/FileSystem"
11
+ import * as Layer from "effect/Layer"
12
+ import * as Path from "effect/Path"
13
+ import type * as PlatformError from "effect/PlatformError"
14
+ import * as Predicate from "effect/Predicate"
15
+ import type * as Scope from "effect/Scope"
16
+ import * as Sink from "effect/Sink"
17
+ import * as Stream from "effect/Stream"
18
+ import * as ChildProcess from "effect/unstable/process/ChildProcess"
19
+ import type { ChildProcessHandle } from "effect/unstable/process/ChildProcessSpawner"
20
+ import { ChildProcessSpawner, ExitCode, makeHandle, ProcessId } from "effect/unstable/process/ChildProcessSpawner"
21
+ import * as NodeChildProcess from "node:child_process"
22
+ import * as NodeJSStream from "node:stream"
23
+ import { handleErrnoException } from "./internal/utils.ts"
24
+ import * as NodeSink from "./NodeSink.ts"
25
+ import * as NodeStream from "./NodeStream.ts"
26
+
27
+ const toError = (error: unknown): Error =>
28
+ error instanceof globalThis.Error
29
+ ? error
30
+ : new globalThis.Error(String(error))
31
+
32
+ const toPlatformError = (
33
+ method: string,
34
+ error: NodeJS.ErrnoException,
35
+ command: ChildProcess.Command
36
+ ): PlatformError.PlatformError => {
37
+ const { commands } = flattenCommand(command)
38
+ const commandStr = commands.reduce((acc, curr) => {
39
+ const cmd = `${curr.command} ${curr.args.join(" ")}`
40
+ return acc.length === 0 ? cmd : `${acc} | ${cmd}`
41
+ }, "")
42
+ return handleErrnoException("ChildProcess", method)(error, [commandStr])
43
+ }
44
+
45
+ type ExitCodeWithSignal = readonly [code: number | null, signal: NodeJS.Signals | null]
46
+ type ExitSignal = Deferred.Deferred<ExitCodeWithSignal>
47
+
48
+ const make = Effect.gen(function*() {
49
+ const fs = yield* FileSystem.FileSystem
50
+ const path = yield* Path.Path
51
+
52
+ const resolveWorkingDirectory = Effect.fnUntraced(
53
+ function*(options: ChildProcess.CommandOptions) {
54
+ if (Predicate.isUndefined(options.cwd)) return undefined
55
+ // Validate that the specified directory is accessible
56
+ yield* fs.access(options.cwd)
57
+ return path.resolve(options.cwd)
58
+ }
59
+ )
60
+
61
+ const resolveEnvironment = (options: ChildProcess.CommandOptions) => {
62
+ return options.extendEnv
63
+ ? { ...globalThis.process.env, ...options.env }
64
+ : options.env
65
+ }
66
+
67
+ const inputToStdioOption = (input: ChildProcess.CommandInput | undefined): NodeChildProcess.IOType | undefined =>
68
+ Stream.isStream(input) ? "pipe" : input
69
+
70
+ const outputToStdioOption = (input: ChildProcess.CommandOutput | undefined): NodeChildProcess.IOType | undefined =>
71
+ Sink.isSink(input) ? "pipe" : input
72
+
73
+ const resolveStdinOption = (options: ChildProcess.CommandOptions): ChildProcess.StdinConfig => {
74
+ const defaultConfig: ChildProcess.StdinConfig = { stream: "pipe", encoding: "utf-8", endOnDone: true }
75
+ if (Predicate.isUndefined(options.stdin)) {
76
+ return defaultConfig
77
+ }
78
+ if (typeof options.stdin === "string") {
79
+ return { ...defaultConfig, stream: options.stdin }
80
+ }
81
+ if (Stream.isStream(options.stdin)) {
82
+ return { ...defaultConfig, stream: options.stdin }
83
+ }
84
+ return {
85
+ stream: options.stdin.stream,
86
+ encoding: options.stdin.encoding ?? defaultConfig.encoding,
87
+ endOnDone: options.stdin.endOnDone ?? defaultConfig.endOnDone
88
+ }
89
+ }
90
+
91
+ const resolveOutputOption = (
92
+ options: ChildProcess.CommandOptions,
93
+ streamName: "stdout" | "stderr"
94
+ ): ChildProcess.StdoutConfig => {
95
+ const option = options[streamName]
96
+ if (Predicate.isUndefined(option)) {
97
+ return { stream: "pipe" }
98
+ }
99
+ if (typeof option === "string") {
100
+ return { stream: option }
101
+ }
102
+ if (Sink.isSink(option)) {
103
+ return { stream: option }
104
+ }
105
+ return { stream: option.stream }
106
+ }
107
+
108
+ interface ResolvedAdditionalFd {
109
+ readonly fd: number
110
+ readonly config: ChildProcess.AdditionalFdConfig
111
+ }
112
+
113
+ const resolveAdditionalFds = (
114
+ options: ChildProcess.CommandOptions
115
+ ): ReadonlyArray<ResolvedAdditionalFd> => {
116
+ if (Predicate.isUndefined(options.additionalFds)) {
117
+ return []
118
+ }
119
+ const result: Array<ResolvedAdditionalFd> = []
120
+ for (const [name, config] of Object.entries(options.additionalFds)) {
121
+ const fd = ChildProcess.parseFdName(name)
122
+ if (Predicate.isNotUndefined(fd)) {
123
+ result.push({ fd, config })
124
+ }
125
+ }
126
+ // Sort by fd number to ensure correct ordering
127
+ return result.sort((a, b) => a.fd - b.fd)
128
+ }
129
+
130
+ const buildStdioArray = (
131
+ stdinConfig: ChildProcess.StdinConfig,
132
+ stdoutConfig: ChildProcess.StdoutConfig,
133
+ stderrConfig: ChildProcess.StderrConfig,
134
+ additionalFds: ReadonlyArray<ResolvedAdditionalFd>
135
+ ): NodeChildProcess.StdioOptions => {
136
+ const stdio: Array<NodeChildProcess.IOType | undefined> = [
137
+ inputToStdioOption(stdinConfig.stream),
138
+ outputToStdioOption(stdoutConfig.stream),
139
+ outputToStdioOption(stderrConfig.stream)
140
+ ]
141
+
142
+ if (additionalFds.length === 0) {
143
+ return stdio as NodeChildProcess.StdioOptions
144
+ }
145
+
146
+ // Find the maximum fd number to size the array correctly
147
+ const maxFd = additionalFds.reduce((max, { fd }) => Math.max(max, fd), 2)
148
+
149
+ // Fill gaps with "ignore"
150
+ for (let i = 3; i <= maxFd; i++) {
151
+ stdio[i] = "ignore"
152
+ }
153
+
154
+ // Set up additional fds as "pipe"
155
+ for (const { fd } of additionalFds) {
156
+ stdio[fd] = "pipe"
157
+ }
158
+
159
+ return stdio as NodeChildProcess.StdioOptions
160
+ }
161
+
162
+ const setupAdditionalFds = Effect.fnUntraced(function*(
163
+ command: ChildProcess.StandardCommand,
164
+ childProcess: NodeChildProcess.ChildProcess,
165
+ additionalFds: ReadonlyArray<ResolvedAdditionalFd>
166
+ ) {
167
+ if (additionalFds.length === 0) {
168
+ return {
169
+ getInputFd: () => Sink.drain,
170
+ getOutputFd: () => Stream.empty
171
+ }
172
+ }
173
+
174
+ const inputSinks = new Map<number, Sink.Sink<void, Uint8Array, never, PlatformError.PlatformError>>()
175
+ const outputStreams = new Map<number, Stream.Stream<Uint8Array, PlatformError.PlatformError>>()
176
+
177
+ for (const { config, fd } of additionalFds) {
178
+ const nodeStream = childProcess.stdio[fd]
179
+
180
+ switch (config.type) {
181
+ case "input": {
182
+ // Create a sink to write to for input file descriptors
183
+ let sink: Sink.Sink<void, Uint8Array, never, PlatformError.PlatformError> = Sink.drain
184
+ if (Predicate.isNotNullish(nodeStream) && "write" in nodeStream) {
185
+ sink = NodeSink.fromWritable({
186
+ evaluate: () => nodeStream as NodeJS.WritableStream,
187
+ onError: (error) => toPlatformError(`fromWritable(fd${fd})`, toError(error), command)
188
+ })
189
+ }
190
+
191
+ // If user provided a stream, pipe it into the sink
192
+ if (Predicate.isNotUndefined(config.stream)) {
193
+ yield* Effect.forkScoped(Stream.run(config.stream, sink))
194
+ }
195
+
196
+ inputSinks.set(fd, sink)
197
+
198
+ break
199
+ }
200
+ case "output": {
201
+ // Create a stream to read from for output file descriptors
202
+ let stream: Stream.Stream<Uint8Array, PlatformError.PlatformError> = Stream.empty
203
+ if (Predicate.isNotNull(nodeStream) && Predicate.isNotUndefined(nodeStream) && "read" in nodeStream) {
204
+ stream = NodeStream.fromReadable({
205
+ evaluate: () => nodeStream as NodeJS.ReadableStream,
206
+ onError: (error) => toPlatformError(`fromReadable(fd${fd})`, toError(error), command)
207
+ })
208
+ }
209
+
210
+ // If user provided a sink, transduce the stream through it
211
+ if (Predicate.isNotUndefined(config.sink)) {
212
+ stream = Stream.transduce(stream, config.sink)
213
+ }
214
+
215
+ outputStreams.set(fd, stream)
216
+
217
+ break
218
+ }
219
+ }
220
+ }
221
+
222
+ return {
223
+ getInputFd: (fd: number) => inputSinks.get(fd) ?? Sink.drain,
224
+ getOutputFd: (fd: number) => outputStreams.get(fd) ?? Stream.empty
225
+ }
226
+ })
227
+
228
+ const setupChildStdin = (
229
+ command: ChildProcess.StandardCommand,
230
+ childProcess: NodeChildProcess.ChildProcess,
231
+ config: ChildProcess.StdinConfig
232
+ ) =>
233
+ Effect.suspend(() => {
234
+ // If the child process has a standard input stream, connect it to the
235
+ // sink that will attached to the process handle
236
+ let sink: Sink.Sink<void, unknown, never, PlatformError.PlatformError> = Sink.drain
237
+ if (Predicate.isNotNull(childProcess.stdin)) {
238
+ sink = NodeSink.fromWritable({
239
+ evaluate: () => childProcess.stdin!,
240
+ onError: (error) => toPlatformError("fromWritable(stdin)", toError(error), command),
241
+ endOnDone: config.endOnDone,
242
+ encoding: config.encoding
243
+ })
244
+ }
245
+
246
+ // If the user provided a `Stream`, run it into the stdin sink
247
+ if (Stream.isStream(config.stream)) {
248
+ return Effect.as(Effect.forkScoped(Stream.run(config.stream, sink)), sink)
249
+ }
250
+
251
+ return Effect.succeed(sink)
252
+ })
253
+
254
+ /**
255
+ * Given that `NodeStream.fromReadable` uses `.read` to read data from the
256
+ * provided `Readable` stream, consumers would race to read data from the
257
+ * `handle.stdout` and `handle.stderr` streams if they were also simultaneously
258
+ * reading from the `handle.all` stream.
259
+ *
260
+ * To solve this, we leverage the fact that NodeJS `Readable` streams can be
261
+ * piped to multiple destinations simultaneously. The logic for the solution
262
+ * is as follows:
263
+ *
264
+ * 1. Pipe each original stream to two `PassThrough` streams:
265
+ * - One dedicated PassThrough for individual access (.stdout / .stderr)
266
+ * - One shared PassThrough for combined access (.all)
267
+ * 2. Create Effect streams from the PassThrough streams (not the originals)
268
+ *
269
+ * **Diagram**
270
+ *
271
+ * ┌─────────────┐
272
+ * ┌────►│ passthrough │────► Effect stdout Stream
273
+ * │ └─────────────┘
274
+ * childProcess.stdout ────┤
275
+ * │ ┌─────────────┐
276
+ * └────►│ passthrough │────► Effect all Stream
277
+ * ┌────►│ │
278
+ * childProcess.stderr ────┤ └─────────────┘
279
+ * │ ┌─────────────┐
280
+ * └────►│ passthrough │────► Effect stderr Stream
281
+ * └─────────────┘
282
+ */
283
+ const setupChildOutputStreams = (
284
+ command: ChildProcess.StandardCommand,
285
+ childProcess: NodeChildProcess.ChildProcess,
286
+ stdoutConfig: ChildProcess.StdoutConfig,
287
+ stderrConfig: ChildProcess.StderrConfig
288
+ ): {
289
+ stdout: Stream.Stream<Uint8Array, PlatformError.PlatformError>
290
+ stderr: Stream.Stream<Uint8Array, PlatformError.PlatformError>
291
+ all: Stream.Stream<Uint8Array, PlatformError.PlatformError>
292
+ } => {
293
+ const nodeStdout = childProcess.stdout
294
+ const nodeStderr = childProcess.stderr
295
+
296
+ // Create PassThrough streams for individual access to stdout and stderr.
297
+ // We pipe the original Node.js streams to these PassThroughs so that
298
+ // the data can be consumed by both the individual streams AND the
299
+ // combined stream (.all) simultaneously.
300
+ const stdoutPassThrough = Predicate.isNotNull(nodeStdout)
301
+ ? new NodeJSStream.PassThrough()
302
+ : null
303
+ const stderrPassThrough = Predicate.isNotNull(nodeStderr)
304
+ ? new NodeJSStream.PassThrough()
305
+ : null
306
+
307
+ // Create PassThrough for combined output (.all)
308
+ const combinedPassThrough = new NodeJSStream.PassThrough()
309
+
310
+ // Track stream endings for the combined stream
311
+ const totalStreams = (Predicate.isNotNull(nodeStdout) ? 1 : 0) +
312
+ (Predicate.isNotNull(nodeStderr) ? 1 : 0)
313
+
314
+ let endedCount = 0
315
+ const onStreamEnd = () => {
316
+ endedCount++
317
+ if (endedCount >= totalStreams) {
318
+ combinedPassThrough.end()
319
+ }
320
+ }
321
+
322
+ // Pipe stdout to both its own PassThrough and the combined PassThrough
323
+ if (Predicate.isNotNull(nodeStdout) && Predicate.isNotNull(stdoutPassThrough)) {
324
+ nodeStdout.pipe(stdoutPassThrough)
325
+ nodeStdout.pipe(combinedPassThrough, { end: false })
326
+ nodeStdout.once("end", onStreamEnd)
327
+ }
328
+
329
+ // Pipe stderr to both its own PassThrough and the combined PassThrough
330
+ if (Predicate.isNotNull(nodeStderr) && Predicate.isNotNull(stderrPassThrough)) {
331
+ nodeStderr.pipe(stderrPassThrough)
332
+ nodeStderr.pipe(combinedPassThrough, { end: false })
333
+ nodeStderr.once("end", onStreamEnd)
334
+ }
335
+
336
+ // Handle edge case: no streams available
337
+ if (totalStreams === 0) {
338
+ combinedPassThrough.end()
339
+ }
340
+
341
+ // Create Effect stream for stdout from its PassThrough
342
+ let stdout: Stream.Stream<Uint8Array, PlatformError.PlatformError> = Stream.empty
343
+ if (Predicate.isNotNull(stdoutPassThrough)) {
344
+ stdout = NodeStream.fromReadable({
345
+ evaluate: () => stdoutPassThrough,
346
+ onError: (error) => toPlatformError("fromReadable(stdout)", toError(error), command)
347
+ })
348
+ // Apply user-provided Sink if configured
349
+ if (Sink.isSink(stdoutConfig.stream)) {
350
+ stdout = Stream.transduce(stdout, stdoutConfig.stream)
351
+ }
352
+ }
353
+
354
+ // Create Effect stream for stderr from its PassThrough
355
+ let stderr: Stream.Stream<Uint8Array, PlatformError.PlatformError> = Stream.empty
356
+ if (Predicate.isNotNull(stderrPassThrough)) {
357
+ stderr = NodeStream.fromReadable({
358
+ evaluate: () => stderrPassThrough,
359
+ onError: (error) => toPlatformError("fromReadable(stderr)", toError(error), command)
360
+ })
361
+ // Apply user-provided Sink if configured
362
+ if (Sink.isSink(stderrConfig.stream)) {
363
+ stderr = Stream.transduce(stderr, stderrConfig.stream)
364
+ }
365
+ }
366
+
367
+ // Create Effect stream for combined output from the combined PassThrough
368
+ const all: Stream.Stream<Uint8Array, PlatformError.PlatformError> = NodeStream.fromReadable({
369
+ evaluate: () => combinedPassThrough,
370
+ onError: (error) => toPlatformError("fromReadable(all)", toError(error), command)
371
+ })
372
+
373
+ return { stdout, stderr, all }
374
+ }
375
+
376
+ const spawn = (
377
+ command: ChildProcess.StandardCommand,
378
+ spawnOptions: NodeChildProcess.SpawnOptions
379
+ ) =>
380
+ Effect.callback<
381
+ readonly [NodeChildProcess.ChildProcess, ExitSignal],
382
+ PlatformError.PlatformError
383
+ >((resume) => {
384
+ const deferred = Deferred.makeUnsafe<ExitCodeWithSignal>()
385
+ const handle = NodeChildProcess.spawn(
386
+ command.command,
387
+ command.args,
388
+ spawnOptions
389
+ )
390
+ handle.on("error", (error) => {
391
+ resume(Effect.fail(toPlatformError("spawn", error, command)))
392
+ })
393
+ handle.on("exit", (...args) => {
394
+ Deferred.doneUnsafe(deferred, Exit.succeed(args))
395
+ })
396
+ handle.on("spawn", () => {
397
+ resume(Effect.succeed([handle, deferred]))
398
+ })
399
+ return Effect.sync(() => {
400
+ handle.kill("SIGTERM")
401
+ })
402
+ })
403
+
404
+ const killProcessGroup = (
405
+ command: ChildProcess.StandardCommand,
406
+ childProcess: NodeChildProcess.ChildProcess,
407
+ signal: NodeJS.Signals
408
+ ) => {
409
+ if (globalThis.process.platform === "win32") {
410
+ return Effect.callback<void, PlatformError.PlatformError>((resume) => {
411
+ NodeChildProcess.exec(`taskkill /pid ${childProcess.pid} /T /F`, (error) => {
412
+ if (error) {
413
+ resume(Effect.fail(toPlatformError("kill", toError(error), command)))
414
+ } else {
415
+ resume(Effect.void)
416
+ }
417
+ })
418
+ })
419
+ }
420
+ return Effect.try({
421
+ try: () => {
422
+ globalThis.process.kill(-childProcess.pid!, signal)
423
+ },
424
+ catch: (error) => toPlatformError("kill", toError(error), command)
425
+ })
426
+ }
427
+
428
+ const killProcess = (
429
+ command: ChildProcess.StandardCommand,
430
+ childProcess: NodeChildProcess.ChildProcess,
431
+ signal: NodeJS.Signals
432
+ ) =>
433
+ Effect.suspend(() => {
434
+ const killed = childProcess.kill(signal)
435
+ if (!killed) {
436
+ const error = new globalThis.Error("Failed to kill child process")
437
+ return Effect.fail(toPlatformError("kill", error, command))
438
+ }
439
+ return Effect.void
440
+ })
441
+
442
+ const withTimeout = (
443
+ childProcess: NodeChildProcess.ChildProcess,
444
+ command: ChildProcess.StandardCommand,
445
+ options: ChildProcess.KillOptions | undefined
446
+ ) =>
447
+ <A, E, R>(
448
+ kill: (
449
+ command: ChildProcess.StandardCommand,
450
+ childProcess: NodeChildProcess.ChildProcess,
451
+ signal: NodeJS.Signals
452
+ ) => Effect.Effect<A, E, R>
453
+ ) => {
454
+ const killSignal = options?.killSignal ?? "SIGTERM"
455
+ return Predicate.isUndefined(options?.forceKillAfter)
456
+ ? kill(command, childProcess, killSignal)
457
+ : Effect.timeoutOrElse(kill(command, childProcess, killSignal), {
458
+ duration: options.forceKillAfter,
459
+ onTimeout: () => kill(command, childProcess, "SIGKILL")
460
+ })
461
+ }
462
+
463
+ /**
464
+ * Get the appropriate source stream from a process handle based on the
465
+ * `from` pipe option.
466
+ */
467
+ const getSourceStream = (
468
+ handle: ChildProcessHandle,
469
+ from: ChildProcess.PipeFromOption | undefined
470
+ ): Stream.Stream<Uint8Array, PlatformError.PlatformError> => {
471
+ const fromOption = from ?? "stdout"
472
+ switch (fromOption) {
473
+ case "stdout":
474
+ return handle.stdout
475
+ case "stderr":
476
+ return handle.stderr
477
+ case "all":
478
+ return handle.all
479
+ default: {
480
+ // Handle fd3, fd4, etc.
481
+ const fd = ChildProcess.parseFdName(fromOption)
482
+ if (Predicate.isNotUndefined(fd)) {
483
+ return handle.getOutputFd(fd)
484
+ }
485
+ // Fallback to stdout for invalid fd names
486
+ return handle.stdout
487
+ }
488
+ }
489
+ }
490
+
491
+ const spawnCommand: (
492
+ command: ChildProcess.Command
493
+ ) => Effect.Effect<
494
+ ChildProcessHandle,
495
+ PlatformError.PlatformError,
496
+ Scope.Scope
497
+ > = Effect.fnUntraced(function*(cmd) {
498
+ switch (cmd._tag) {
499
+ case "StandardCommand": {
500
+ const stdinConfig = resolveStdinOption(cmd.options)
501
+ const stdoutConfig = resolveOutputOption(cmd.options, "stdout")
502
+ const stderrConfig = resolveOutputOption(cmd.options, "stderr")
503
+ const resolvedAdditionalFds = resolveAdditionalFds(cmd.options)
504
+
505
+ const cwd = yield* resolveWorkingDirectory(cmd.options)
506
+ const env = resolveEnvironment(cmd.options)
507
+ const stdio = buildStdioArray(stdinConfig, stdoutConfig, stderrConfig, resolvedAdditionalFds)
508
+
509
+ const [childProcess, exitSignal] = yield* Effect.acquireRelease(
510
+ spawn(cmd, {
511
+ cwd,
512
+ env,
513
+ stdio,
514
+ detached: cmd.options.detached ?? process.platform !== "win32",
515
+ shell: cmd.options.shell
516
+ }),
517
+ Effect.fnUntraced(function*([childProcess, exitSignal]) {
518
+ const exited = yield* Deferred.isDone(exitSignal)
519
+ const killWithTimeout = withTimeout(childProcess, cmd, cmd.options)
520
+ if (exited) {
521
+ // Process already exited, check if children need cleanup
522
+ const [code] = yield* Deferred.await(exitSignal)
523
+ if (code !== 0 && Predicate.isNotNull(code)) {
524
+ // Non-zero exit code ,attempt to clean up process group
525
+ return yield* Effect.ignore(killWithTimeout(killProcessGroup))
526
+ }
527
+ return yield* Effect.void
528
+ }
529
+ // Process is still running, kill it
530
+ return yield* killWithTimeout((command, childProcess, signal) =>
531
+ Effect.catch(
532
+ killProcessGroup(command, childProcess, signal),
533
+ () => killProcess(command, childProcess, signal)
534
+ )
535
+ ).pipe(
536
+ Effect.andThen(Deferred.await(exitSignal)),
537
+ Effect.ignore
538
+ )
539
+ })
540
+ )
541
+
542
+ const pid = ProcessId(childProcess.pid!)
543
+ const stdin = yield* setupChildStdin(cmd, childProcess, stdinConfig)
544
+ const { all, stderr, stdout } = setupChildOutputStreams(cmd, childProcess, stdoutConfig, stderrConfig)
545
+ const { getInputFd, getOutputFd } = yield* setupAdditionalFds(cmd, childProcess, resolvedAdditionalFds)
546
+ const isRunning = Effect.map(Deferred.isDone(exitSignal), (done) => !done)
547
+ const exitCode = Effect.flatMap(Deferred.await(exitSignal), ([code, signal]) => {
548
+ if (Predicate.isNotNull(code)) {
549
+ return Effect.succeed(ExitCode(code))
550
+ }
551
+ // If code is `null`, then `signal` must be defined. See the NodeJS
552
+ // documentation for the `"exit"` event on a `child_process`.
553
+ // https://nodejs.org/api/child_process.html#child_process_event_exit
554
+ const error = new globalThis.Error(`Process interrupted due to receipt of signal: '${signal}'`)
555
+ return Effect.fail(toPlatformError("exitCode", error, cmd))
556
+ })
557
+ const kill = (options?: ChildProcess.KillOptions | undefined) => {
558
+ const killWithTimeout = withTimeout(childProcess, cmd, options)
559
+ return killWithTimeout((command, childProcess, signal) =>
560
+ Effect.catch(
561
+ killProcessGroup(command, childProcess, signal),
562
+ () => killProcess(command, childProcess, signal)
563
+ )
564
+ ).pipe(
565
+ Effect.andThen(Deferred.await(exitSignal)),
566
+ Effect.asVoid
567
+ )
568
+ }
569
+
570
+ return makeHandle({
571
+ pid,
572
+ exitCode,
573
+ isRunning,
574
+ kill,
575
+ stdin,
576
+ stdout,
577
+ stderr,
578
+ all,
579
+ getInputFd,
580
+ getOutputFd
581
+ })
582
+ }
583
+ case "PipedCommand": {
584
+ const { commands, pipeOptions } = flattenCommand(cmd)
585
+ const [root, ...pipeline] = commands
586
+
587
+ let handle = spawnCommand(root)
588
+
589
+ for (let i = 0; i < pipeline.length; i++) {
590
+ const command = pipeline[i]
591
+ const options = pipeOptions[i] ?? {}
592
+ const stdinConfig = resolveStdinOption(command.options)
593
+
594
+ // Get the appropriate stream from the source based on `from` option
595
+ const sourceStream = Stream.unwrap(
596
+ Effect.map(handle, (h) => getSourceStream(h, options.from))
597
+ )
598
+
599
+ // Determine where to pipe: stdin or custom fd
600
+ const toOption = options.to ?? "stdin"
601
+
602
+ if (toOption === "stdin") {
603
+ // Pipe to stdin (default behavior)
604
+ handle = spawnCommand(ChildProcess.make(command.command, command.args, {
605
+ ...command.options,
606
+ stdin: { ...stdinConfig, stream: sourceStream }
607
+ }))
608
+ } else {
609
+ // Pipe to custom fd (fd3, fd4, etc.)
610
+ const fd = ChildProcess.parseFdName(toOption)
611
+ if (Predicate.isNotUndefined(fd)) {
612
+ const fdName = ChildProcess.fdName(fd) as `fd${number}`
613
+ const existingFds = command.options.additionalFds ?? {}
614
+ handle = spawnCommand(ChildProcess.make(command.command, command.args, {
615
+ ...command.options,
616
+ additionalFds: {
617
+ ...existingFds,
618
+ [fdName]: { type: "input" as const, stream: sourceStream }
619
+ }
620
+ }))
621
+ } else {
622
+ // Invalid fd name, fall back to stdin
623
+ handle = spawnCommand(ChildProcess.make(command.command, command.args, {
624
+ ...command.options,
625
+ stdin: { ...stdinConfig, stream: sourceStream }
626
+ }))
627
+ }
628
+ }
629
+ }
630
+
631
+ return yield* handle
632
+ }
633
+ }
634
+ })
635
+
636
+ return ChildProcessSpawner.of({
637
+ spawn: spawnCommand
638
+ })
639
+ })
640
+
641
+ /**
642
+ * Layer providing the `NodeChildProcessSpawner` implementation.
643
+ *
644
+ * @since 4.0.0
645
+ * @category Layers
646
+ */
647
+ export const layer: Layer.Layer<
648
+ ChildProcessSpawner,
649
+ never,
650
+ FileSystem.FileSystem | Path.Path
651
+ > = Layer.effect(ChildProcessSpawner, make)
652
+
653
+ // =============================================================================
654
+ // Internal Helpers
655
+ // =============================================================================
656
+
657
+ /**
658
+ * Result of flattening a pipeline of commands.
659
+ *
660
+ * @since 4.0.0
661
+ * @category Models
662
+ */
663
+ export interface FlattenedPipeline {
664
+ readonly commands: Arr.NonEmptyReadonlyArray<ChildProcess.StandardCommand>
665
+ readonly pipeOptions: ReadonlyArray<ChildProcess.PipeOptions>
666
+ }
667
+
668
+ /**
669
+ * Flattens a `Command` into an array of `StandardCommand`s along with pipe
670
+ * options for each connection.
671
+ *
672
+ * @since 4.0.0
673
+ * @category Utilities
674
+ */
675
+ export const flattenCommand = (
676
+ command: ChildProcess.Command
677
+ ): FlattenedPipeline => {
678
+ const commands: Array<ChildProcess.StandardCommand> = []
679
+ const pipeOptions: Array<ChildProcess.PipeOptions> = []
680
+
681
+ const flatten = (cmd: ChildProcess.Command): void => {
682
+ switch (cmd._tag) {
683
+ case "StandardCommand": {
684
+ commands.push(cmd)
685
+ break
686
+ }
687
+ case "PipedCommand": {
688
+ // Recursively flatten left side first
689
+ flatten(cmd.left)
690
+ // Store the pipe options for this connection
691
+ pipeOptions.push(cmd.options)
692
+ // Then flatten right side
693
+ flatten(cmd.right)
694
+ break
695
+ }
696
+ }
697
+ }
698
+
699
+ flatten(command)
700
+
701
+ // The commands array is guaranteed to be non-empty since we always have at
702
+ // least one command in the input. We validate this at runtime and return a
703
+ // properly typed tuple.
704
+ if (commands.length === 0) {
705
+ // This should never happen given a valid Command input
706
+ throw new Error("flattenCommand produced empty commands array")
707
+ }
708
+
709
+ const [first, ...rest] = commands
710
+ const nonEmptyCommands: Arr.NonEmptyReadonlyArray<ChildProcess.StandardCommand> = [first, ...rest]
711
+
712
+ return { commands: nonEmptyCommands, pipeOptions }
713
+ }