@saeeol/core 7.3.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.
- package/package.json +52 -0
- package/src/cross-spawn-process.ts +273 -0
- package/src/cross-spawn-spawner.ts +505 -0
- package/src/cross-spawn-utils.ts +74 -0
- package/src/effect/logger.ts +73 -0
- package/src/effect/memo-map.ts +3 -0
- package/src/effect/observability.ts +107 -0
- package/src/effect/runtime.ts +21 -0
- package/src/filesystem.ts +262 -0
- package/src/flag/flag.ts +107 -0
- package/src/global.ts +91 -0
- package/src/installation/version.ts +11 -0
- package/src/npm-config.ts +40 -0
- package/src/npm.ts +271 -0
- package/src/saeeol/global.ts +23 -0
- package/src/saeeol/kilocode/global.ts +23 -0
- package/src/saeeol/kilocode/spotlight.ts +23 -0
- package/src/saeeol/spotlight.ts +23 -0
- package/src/util/array.ts +10 -0
- package/src/util/binary.ts +41 -0
- package/src/util/effect-flock.ts +283 -0
- package/src/util/encode.ts +52 -0
- package/src/util/error.ts +60 -0
- package/src/util/flock.ts +358 -0
- package/src/util/glob.ts +34 -0
- package/src/util/hash.ts +7 -0
- package/src/util/identifier.ts +48 -0
- package/src/util/iife.ts +3 -0
- package/src/util/lazy.ts +11 -0
- package/src/util/log.ts +208 -0
- package/src/util/module.ts +10 -0
- package/src/util/path.ts +37 -0
- package/src/util/retry.ts +42 -0
- package/src/util/saeeol-process.ts +24 -0
- package/src/util/slug.ts +74 -0
- package/sst-env.d.ts +10 -0
- package/test/effect/cross-spawn-spawner.test.ts +423 -0
- package/test/effect/observability.test.ts +46 -0
- package/test/filesystem/filesystem.test.ts +338 -0
- package/test/fixture/effect-flock-worker.ts +60 -0
- package/test/fixture/flock-worker.ts +72 -0
- package/test/fixture/tmpdir.ts +13 -0
- package/test/global.test.ts +16 -0
- package/test/lib/effect.ts +53 -0
- package/test/npm-config.test.ts +51 -0
- package/test/npm.test.ts +91 -0
- package/test/saeeol/filesystem-containment.test.ts +13 -0
- package/test/saeeol/kilocode/filesystem-containment.test.ts +13 -0
- package/test/util/effect-flock.test.ts +386 -0
- package/test/util/flock.test.ts +426 -0
- package/tsconfig.json +8 -0
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
import type * as Arr from "effect/Array"
|
|
2
|
+
import { NodeFileSystem, NodeSink, NodeStream } from "@effect/platform-node"
|
|
3
|
+
import * as NodePath from "@effect/platform-node/NodePath"
|
|
4
|
+
import * as Deferred from "effect/Deferred"
|
|
5
|
+
import * as Effect from "effect/Effect"
|
|
6
|
+
import * as Exit from "effect/Exit"
|
|
7
|
+
import * as FileSystem from "effect/FileSystem"
|
|
8
|
+
import * as Layer from "effect/Layer"
|
|
9
|
+
import * as Path from "effect/Path"
|
|
10
|
+
import * as PlatformError from "effect/PlatformError"
|
|
11
|
+
import * as Predicate from "effect/Predicate"
|
|
12
|
+
import type * as Scope from "effect/Scope"
|
|
13
|
+
import * as Sink from "effect/Sink"
|
|
14
|
+
import * as Stream from "effect/Stream"
|
|
15
|
+
import * as ChildProcess from "effect/unstable/process/ChildProcess"
|
|
16
|
+
import type { ChildProcessHandle } from "effect/unstable/process/ChildProcessSpawner"
|
|
17
|
+
import {
|
|
18
|
+
ChildProcessSpawner,
|
|
19
|
+
ExitCode,
|
|
20
|
+
make as makeSpawner,
|
|
21
|
+
makeHandle,
|
|
22
|
+
ProcessId,
|
|
23
|
+
} from "effect/unstable/process/ChildProcessSpawner"
|
|
24
|
+
import * as NodeChildProcess from "node:child_process"
|
|
25
|
+
import { PassThrough } from "node:stream"
|
|
26
|
+
import launch from "cross-spawn"
|
|
27
|
+
|
|
28
|
+
const toError = (err: unknown): Error => (err instanceof globalThis.Error ? err : new globalThis.Error(String(err)))
|
|
29
|
+
|
|
30
|
+
const toTag = (err: NodeJS.ErrnoException): PlatformError.SystemErrorTag => {
|
|
31
|
+
switch (err.code) {
|
|
32
|
+
case "ENOENT":
|
|
33
|
+
return "NotFound"
|
|
34
|
+
case "EACCES":
|
|
35
|
+
return "PermissionDenied"
|
|
36
|
+
case "EEXIST":
|
|
37
|
+
return "AlreadyExists"
|
|
38
|
+
case "EISDIR":
|
|
39
|
+
return "BadResource"
|
|
40
|
+
case "ENOTDIR":
|
|
41
|
+
return "BadResource"
|
|
42
|
+
case "EBUSY":
|
|
43
|
+
return "Busy"
|
|
44
|
+
case "ELOOP":
|
|
45
|
+
return "BadResource"
|
|
46
|
+
default:
|
|
47
|
+
return "Unknown"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const flatten = (command: ChildProcess.Command) => {
|
|
52
|
+
const commands: Array<ChildProcess.StandardCommand> = []
|
|
53
|
+
const opts: Array<ChildProcess.PipeOptions> = []
|
|
54
|
+
|
|
55
|
+
const walk = (cmd: ChildProcess.Command): void => {
|
|
56
|
+
switch (cmd._tag) {
|
|
57
|
+
case "StandardCommand":
|
|
58
|
+
commands.push(cmd)
|
|
59
|
+
return
|
|
60
|
+
case "PipedCommand":
|
|
61
|
+
walk(cmd.left)
|
|
62
|
+
opts.push(cmd.options)
|
|
63
|
+
walk(cmd.right)
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
walk(command)
|
|
69
|
+
if (commands.length === 0) throw new Error("flatten produced empty commands array")
|
|
70
|
+
const [head, ...tail] = commands
|
|
71
|
+
return {
|
|
72
|
+
commands: [head, ...tail] as Arr.NonEmptyReadonlyArray<ChildProcess.StandardCommand>,
|
|
73
|
+
opts,
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const toPlatformError = (
|
|
78
|
+
method: string,
|
|
79
|
+
err: NodeJS.ErrnoException,
|
|
80
|
+
command: ChildProcess.Command,
|
|
81
|
+
): PlatformError.PlatformError => {
|
|
82
|
+
const cmd = flatten(command)
|
|
83
|
+
.commands.map((x) => `${x.command} ${x.args.join(" ")}`)
|
|
84
|
+
.join(" | ")
|
|
85
|
+
return PlatformError.systemError({
|
|
86
|
+
_tag: toTag(err),
|
|
87
|
+
module: "ChildProcess",
|
|
88
|
+
method,
|
|
89
|
+
pathOrDescriptor: cmd,
|
|
90
|
+
syscall: err.syscall,
|
|
91
|
+
cause: err,
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
type ExitSignal = Deferred.Deferred<readonly [code: number | null, signal: NodeJS.Signals | null]>
|
|
96
|
+
|
|
97
|
+
export const make = Effect.gen(function* () {
|
|
98
|
+
const fs = yield* FileSystem.FileSystem
|
|
99
|
+
const path = yield* Path.Path
|
|
100
|
+
|
|
101
|
+
const cwd = Effect.fnUntraced(function* (opts: ChildProcess.CommandOptions) {
|
|
102
|
+
if (Predicate.isUndefined(opts.cwd)) return undefined
|
|
103
|
+
yield* fs.access(opts.cwd)
|
|
104
|
+
return path.resolve(opts.cwd)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
const env = (opts: ChildProcess.CommandOptions) =>
|
|
108
|
+
opts.extendEnv ? { ...globalThis.process.env, ...opts.env } : opts.env
|
|
109
|
+
|
|
110
|
+
const input = (x: ChildProcess.CommandInput | undefined): NodeChildProcess.IOType | undefined =>
|
|
111
|
+
Stream.isStream(x) ? "pipe" : x
|
|
112
|
+
|
|
113
|
+
const output = (x: ChildProcess.CommandOutput | undefined): NodeChildProcess.IOType | undefined =>
|
|
114
|
+
Sink.isSink(x) ? "pipe" : x
|
|
115
|
+
|
|
116
|
+
const stdin = (opts: ChildProcess.CommandOptions): ChildProcess.StdinConfig => {
|
|
117
|
+
const cfg: ChildProcess.StdinConfig = { stream: "pipe", encoding: "utf-8", endOnDone: true }
|
|
118
|
+
if (Predicate.isUndefined(opts.stdin)) return cfg
|
|
119
|
+
if (typeof opts.stdin === "string") return { ...cfg, stream: opts.stdin }
|
|
120
|
+
if (Stream.isStream(opts.stdin)) return { ...cfg, stream: opts.stdin }
|
|
121
|
+
return {
|
|
122
|
+
stream: opts.stdin.stream,
|
|
123
|
+
encoding: opts.stdin.encoding ?? cfg.encoding,
|
|
124
|
+
endOnDone: opts.stdin.endOnDone ?? cfg.endOnDone,
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const stdio = (opts: ChildProcess.CommandOptions, key: "stdout" | "stderr"): ChildProcess.StdoutConfig => {
|
|
129
|
+
const cfg = opts[key]
|
|
130
|
+
if (Predicate.isUndefined(cfg)) return { stream: "pipe" }
|
|
131
|
+
if (typeof cfg === "string") return { stream: cfg }
|
|
132
|
+
if (Sink.isSink(cfg)) return { stream: cfg }
|
|
133
|
+
return { stream: cfg.stream }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const fds = (opts: ChildProcess.CommandOptions) => {
|
|
137
|
+
if (Predicate.isUndefined(opts.additionalFds)) return []
|
|
138
|
+
return Object.entries(opts.additionalFds)
|
|
139
|
+
.flatMap(([name, config]) => {
|
|
140
|
+
const fd = ChildProcess.parseFdName(name)
|
|
141
|
+
return Predicate.isUndefined(fd) ? [] : [{ fd, config }]
|
|
142
|
+
})
|
|
143
|
+
.toSorted((a, b) => a.fd - b.fd)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const stdios = (
|
|
147
|
+
sin: ChildProcess.StdinConfig,
|
|
148
|
+
sout: ChildProcess.StdoutConfig,
|
|
149
|
+
serr: ChildProcess.StderrConfig,
|
|
150
|
+
extra: ReadonlyArray<{ fd: number; config: ChildProcess.AdditionalFdConfig }>,
|
|
151
|
+
): NodeChildProcess.StdioOptions => {
|
|
152
|
+
const pipe = (x: NodeChildProcess.IOType | undefined) =>
|
|
153
|
+
process.platform === "win32" && x === "pipe" ? "overlapped" : x
|
|
154
|
+
const arr: Array<NodeChildProcess.IOType | undefined> = [
|
|
155
|
+
pipe(input(sin.stream)),
|
|
156
|
+
pipe(output(sout.stream)),
|
|
157
|
+
pipe(output(serr.stream)),
|
|
158
|
+
]
|
|
159
|
+
if (extra.length === 0) return arr as NodeChildProcess.StdioOptions
|
|
160
|
+
const max = extra.reduce((acc, x) => Math.max(acc, x.fd), 2)
|
|
161
|
+
for (let i = 3; i <= max; i++) arr[i] = "ignore"
|
|
162
|
+
for (const x of extra) arr[x.fd] = pipe("pipe")
|
|
163
|
+
return arr as NodeChildProcess.StdioOptions
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const setupFds = Effect.fnUntraced(function* (
|
|
167
|
+
command: ChildProcess.StandardCommand,
|
|
168
|
+
proc: NodeChildProcess.ChildProcess,
|
|
169
|
+
extra: ReadonlyArray<{ fd: number; config: ChildProcess.AdditionalFdConfig }>,
|
|
170
|
+
) {
|
|
171
|
+
if (extra.length === 0) {
|
|
172
|
+
return {
|
|
173
|
+
getInputFd: () => Sink.drain,
|
|
174
|
+
getOutputFd: () => Stream.empty,
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const ins = new Map<number, Sink.Sink<void, Uint8Array, never, PlatformError.PlatformError>>()
|
|
179
|
+
const outs = new Map<number, Stream.Stream<Uint8Array, PlatformError.PlatformError>>()
|
|
180
|
+
|
|
181
|
+
for (const x of extra) {
|
|
182
|
+
const node = proc.stdio[x.fd]
|
|
183
|
+
switch (x.config.type) {
|
|
184
|
+
case "input": {
|
|
185
|
+
let sink: Sink.Sink<void, Uint8Array, never, PlatformError.PlatformError> = Sink.drain
|
|
186
|
+
if (node && "write" in node) {
|
|
187
|
+
sink = NodeSink.fromWritable({
|
|
188
|
+
evaluate: () => node,
|
|
189
|
+
onError: (err) => toPlatformError(`fromWritable(fd${x.fd})`, toError(err), command),
|
|
190
|
+
endOnDone: true,
|
|
191
|
+
})
|
|
192
|
+
}
|
|
193
|
+
if (x.config.stream) yield* Effect.forkScoped(Stream.run(x.config.stream, sink))
|
|
194
|
+
ins.set(x.fd, sink)
|
|
195
|
+
break
|
|
196
|
+
}
|
|
197
|
+
case "output": {
|
|
198
|
+
let stream: Stream.Stream<Uint8Array, PlatformError.PlatformError> = Stream.empty
|
|
199
|
+
if (node && "read" in node) {
|
|
200
|
+
const tap = new PassThrough()
|
|
201
|
+
node.on("error", (err) => tap.destroy(toError(err)))
|
|
202
|
+
node.pipe(tap)
|
|
203
|
+
stream = NodeStream.fromReadable({
|
|
204
|
+
evaluate: () => tap,
|
|
205
|
+
onError: (err) => toPlatformError(`fromReadable(fd${x.fd})`, toError(err), command),
|
|
206
|
+
})
|
|
207
|
+
}
|
|
208
|
+
if (x.config.sink) stream = Stream.transduce(stream, x.config.sink)
|
|
209
|
+
outs.set(x.fd, stream)
|
|
210
|
+
break
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
getInputFd: (fd: number) => ins.get(fd) ?? Sink.drain,
|
|
217
|
+
getOutputFd: (fd: number) => outs.get(fd) ?? Stream.empty,
|
|
218
|
+
}
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
const setupStdin = (
|
|
222
|
+
command: ChildProcess.StandardCommand,
|
|
223
|
+
proc: NodeChildProcess.ChildProcess,
|
|
224
|
+
cfg: ChildProcess.StdinConfig,
|
|
225
|
+
) =>
|
|
226
|
+
Effect.suspend(() => {
|
|
227
|
+
let sink: Sink.Sink<void, unknown, never, PlatformError.PlatformError> = Sink.drain
|
|
228
|
+
if (Predicate.isNotNull(proc.stdin)) {
|
|
229
|
+
sink = NodeSink.fromWritable({
|
|
230
|
+
evaluate: () => proc.stdin!,
|
|
231
|
+
onError: (err) => toPlatformError("fromWritable(stdin)", toError(err), command),
|
|
232
|
+
endOnDone: cfg.endOnDone,
|
|
233
|
+
encoding: cfg.encoding,
|
|
234
|
+
})
|
|
235
|
+
}
|
|
236
|
+
if (Stream.isStream(cfg.stream)) return Effect.as(Effect.forkScoped(Stream.run(cfg.stream, sink)), sink)
|
|
237
|
+
return Effect.succeed(sink)
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
const setupOutput = (
|
|
241
|
+
command: ChildProcess.StandardCommand,
|
|
242
|
+
proc: NodeChildProcess.ChildProcess,
|
|
243
|
+
out: ChildProcess.StdoutConfig,
|
|
244
|
+
err: ChildProcess.StderrConfig,
|
|
245
|
+
) => {
|
|
246
|
+
let stdout = proc.stdout
|
|
247
|
+
? NodeStream.fromReadable({
|
|
248
|
+
evaluate: () => proc.stdout!,
|
|
249
|
+
onError: (cause) => toPlatformError("fromReadable(stdout)", toError(cause), command),
|
|
250
|
+
})
|
|
251
|
+
: Stream.empty
|
|
252
|
+
let stderr = proc.stderr
|
|
253
|
+
? NodeStream.fromReadable({
|
|
254
|
+
evaluate: () => proc.stderr!,
|
|
255
|
+
onError: (cause) => toPlatformError("fromReadable(stderr)", toError(cause), command),
|
|
256
|
+
})
|
|
257
|
+
: Stream.empty
|
|
258
|
+
|
|
259
|
+
if (Sink.isSink(out.stream)) stdout = Stream.transduce(stdout, out.stream)
|
|
260
|
+
if (Sink.isSink(err.stream)) stderr = Stream.transduce(stderr, err.stream)
|
|
261
|
+
|
|
262
|
+
return { stdout, stderr, all: Stream.merge(stdout, stderr) }
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const spawn = (command: ChildProcess.StandardCommand, opts: NodeChildProcess.SpawnOptions) =>
|
|
266
|
+
Effect.callback<readonly [NodeChildProcess.ChildProcess, ExitSignal], PlatformError.PlatformError>((resume) => {
|
|
267
|
+
const signal = Deferred.makeUnsafe<readonly [code: number | null, signal: NodeJS.Signals | null]>()
|
|
268
|
+
const proc = launch(command.command, command.args, opts)
|
|
269
|
+
let end = false
|
|
270
|
+
let exit: readonly [code: number | null, signal: NodeJS.Signals | null] | undefined
|
|
271
|
+
proc.on("error", (err) => {
|
|
272
|
+
resume(Effect.fail(toPlatformError("spawn", err, command)))
|
|
273
|
+
})
|
|
274
|
+
proc.on("exit", (...args) => {
|
|
275
|
+
exit = args
|
|
276
|
+
})
|
|
277
|
+
proc.on("close", (...args) => {
|
|
278
|
+
if (end) return
|
|
279
|
+
end = true
|
|
280
|
+
Deferred.doneUnsafe(signal, Exit.succeed(exit ?? args))
|
|
281
|
+
})
|
|
282
|
+
proc.on("spawn", () => {
|
|
283
|
+
resume(Effect.succeed([proc, signal]))
|
|
284
|
+
})
|
|
285
|
+
return Effect.sync(() => {
|
|
286
|
+
proc.kill("SIGTERM")
|
|
287
|
+
})
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
const killGroup = (
|
|
291
|
+
command: ChildProcess.StandardCommand,
|
|
292
|
+
proc: NodeChildProcess.ChildProcess,
|
|
293
|
+
signal: NodeJS.Signals,
|
|
294
|
+
) => {
|
|
295
|
+
if (globalThis.process.platform === "win32") {
|
|
296
|
+
return Effect.callback<void, PlatformError.PlatformError>((resume) => {
|
|
297
|
+
NodeChildProcess.exec(`taskkill /pid ${proc.pid} /T /F`, { windowsHide: true }, (err) => {
|
|
298
|
+
if (err) return resume(Effect.fail(toPlatformError("kill", toError(err), command)))
|
|
299
|
+
resume(Effect.void)
|
|
300
|
+
})
|
|
301
|
+
})
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return Effect.try({
|
|
305
|
+
try: () => {
|
|
306
|
+
globalThis.process.kill(-proc.pid!, signal)
|
|
307
|
+
},
|
|
308
|
+
catch: (err) => toPlatformError("kill", toError(err), command),
|
|
309
|
+
})
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const killOne = (
|
|
313
|
+
command: ChildProcess.StandardCommand,
|
|
314
|
+
proc: NodeChildProcess.ChildProcess,
|
|
315
|
+
signal: NodeJS.Signals,
|
|
316
|
+
) =>
|
|
317
|
+
Effect.suspend(() => {
|
|
318
|
+
if (proc.kill(signal)) return Effect.void
|
|
319
|
+
return Effect.fail(toPlatformError("kill", new Error("Failed to kill child process"), command))
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
const timeout =
|
|
323
|
+
(
|
|
324
|
+
proc: NodeChildProcess.ChildProcess,
|
|
325
|
+
command: ChildProcess.StandardCommand,
|
|
326
|
+
opts: ChildProcess.KillOptions | undefined,
|
|
327
|
+
) =>
|
|
328
|
+
<A, E, R>(
|
|
329
|
+
f: (
|
|
330
|
+
command: ChildProcess.StandardCommand,
|
|
331
|
+
proc: NodeChildProcess.ChildProcess,
|
|
332
|
+
signal: NodeJS.Signals,
|
|
333
|
+
) => Effect.Effect<A, E, R>,
|
|
334
|
+
) => {
|
|
335
|
+
const signal = opts?.killSignal ?? "SIGTERM"
|
|
336
|
+
if (Predicate.isUndefined(opts?.forceKillAfter)) return f(command, proc, signal)
|
|
337
|
+
return Effect.timeoutOrElse(f(command, proc, signal), {
|
|
338
|
+
duration: opts.forceKillAfter,
|
|
339
|
+
orElse: () => f(command, proc, "SIGKILL"),
|
|
340
|
+
})
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const source = (handle: ChildProcessHandle, from: ChildProcess.PipeFromOption | undefined) => {
|
|
344
|
+
const opt = from ?? "stdout"
|
|
345
|
+
switch (opt) {
|
|
346
|
+
case "stdout":
|
|
347
|
+
return handle.stdout
|
|
348
|
+
case "stderr":
|
|
349
|
+
return handle.stderr
|
|
350
|
+
case "all":
|
|
351
|
+
return handle.all
|
|
352
|
+
default: {
|
|
353
|
+
const fd = ChildProcess.parseFdName(opt)
|
|
354
|
+
return Predicate.isNotUndefined(fd) ? handle.getOutputFd(fd) : handle.stdout
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const spawnCommand: (
|
|
360
|
+
command: ChildProcess.Command,
|
|
361
|
+
) => Effect.Effect<ChildProcessHandle, PlatformError.PlatformError, Scope.Scope> = Effect.fnUntraced(
|
|
362
|
+
function* (command) {
|
|
363
|
+
switch (command._tag) {
|
|
364
|
+
case "StandardCommand": {
|
|
365
|
+
const sin = stdin(command.options)
|
|
366
|
+
const sout = stdio(command.options, "stdout")
|
|
367
|
+
const serr = stdio(command.options, "stderr")
|
|
368
|
+
const extra = fds(command.options)
|
|
369
|
+
const dir = yield* cwd(command.options)
|
|
370
|
+
|
|
371
|
+
const [proc, signal] = yield* Effect.acquireRelease(
|
|
372
|
+
spawn(command, {
|
|
373
|
+
cwd: dir,
|
|
374
|
+
env: env(command.options),
|
|
375
|
+
stdio: stdios(sin, sout, serr, extra),
|
|
376
|
+
detached: command.options.detached ?? process.platform !== "win32",
|
|
377
|
+
shell: command.options.shell,
|
|
378
|
+
windowsHide: process.platform === "win32",
|
|
379
|
+
}),
|
|
380
|
+
Effect.fnUntraced(function* ([proc, signal]) {
|
|
381
|
+
const done = yield* Deferred.isDone(signal)
|
|
382
|
+
const kill = timeout(proc, command, command.options)
|
|
383
|
+
if (done) {
|
|
384
|
+
const [code] = yield* Deferred.await(signal)
|
|
385
|
+
if (process.platform === "win32") return yield* Effect.void
|
|
386
|
+
if (code !== 0 && Predicate.isNotNull(code)) return yield* Effect.ignore(kill(killGroup))
|
|
387
|
+
return yield* Effect.void
|
|
388
|
+
}
|
|
389
|
+
const send = (s: NodeJS.Signals) =>
|
|
390
|
+
Effect.catch(killGroup(command, proc, s), () => killOne(command, proc, s))
|
|
391
|
+
const sig = command.options.killSignal ?? "SIGTERM"
|
|
392
|
+
const attempt = send(sig).pipe(Effect.andThen(Deferred.await(signal)), Effect.asVoid)
|
|
393
|
+
const escalated = command.options.forceKillAfter
|
|
394
|
+
? Effect.timeoutOrElse(attempt, {
|
|
395
|
+
duration: command.options.forceKillAfter,
|
|
396
|
+
orElse: () => send("SIGKILL").pipe(Effect.andThen(Deferred.await(signal)), Effect.asVoid),
|
|
397
|
+
})
|
|
398
|
+
: attempt
|
|
399
|
+
return yield* Effect.ignore(escalated)
|
|
400
|
+
}),
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
const fd = yield* setupFds(command, proc, extra)
|
|
404
|
+
const out = setupOutput(command, proc, sout, serr)
|
|
405
|
+
let ref = true
|
|
406
|
+
return makeHandle({
|
|
407
|
+
pid: ProcessId(proc.pid!),
|
|
408
|
+
stdin: yield* setupStdin(command, proc, sin),
|
|
409
|
+
stdout: out.stdout,
|
|
410
|
+
stderr: out.stderr,
|
|
411
|
+
all: out.all,
|
|
412
|
+
getInputFd: fd.getInputFd,
|
|
413
|
+
getOutputFd: fd.getOutputFd,
|
|
414
|
+
isRunning: Effect.map(Deferred.isDone(signal), (done) => !done),
|
|
415
|
+
exitCode: Effect.flatMap(Deferred.await(signal), ([code, signal]) => {
|
|
416
|
+
if (Predicate.isNotNull(code)) return Effect.succeed(ExitCode(code))
|
|
417
|
+
return Effect.fail(
|
|
418
|
+
toPlatformError(
|
|
419
|
+
"exitCode",
|
|
420
|
+
new Error(`Process interrupted due to receipt of signal: '${signal}'`),
|
|
421
|
+
command,
|
|
422
|
+
),
|
|
423
|
+
)
|
|
424
|
+
}),
|
|
425
|
+
kill: (opts?: ChildProcess.KillOptions) => {
|
|
426
|
+
const sig = opts?.killSignal ?? "SIGTERM"
|
|
427
|
+
const send = (s: NodeJS.Signals) =>
|
|
428
|
+
Effect.catch(killGroup(command, proc, s), () => killOne(command, proc, s))
|
|
429
|
+
const attempt = send(sig).pipe(Effect.andThen(Deferred.await(signal)), Effect.asVoid)
|
|
430
|
+
if (!opts?.forceKillAfter) return attempt
|
|
431
|
+
return Effect.timeoutOrElse(attempt, {
|
|
432
|
+
duration: opts.forceKillAfter,
|
|
433
|
+
orElse: () => send("SIGKILL").pipe(Effect.andThen(Deferred.await(signal)), Effect.asVoid),
|
|
434
|
+
})
|
|
435
|
+
},
|
|
436
|
+
unref: Effect.sync(() => {
|
|
437
|
+
if (ref) {
|
|
438
|
+
proc.unref()
|
|
439
|
+
ref = false
|
|
440
|
+
}
|
|
441
|
+
return Effect.sync(() => {
|
|
442
|
+
if (!ref) {
|
|
443
|
+
proc.ref()
|
|
444
|
+
ref = true
|
|
445
|
+
}
|
|
446
|
+
})
|
|
447
|
+
}),
|
|
448
|
+
})
|
|
449
|
+
}
|
|
450
|
+
case "PipedCommand": {
|
|
451
|
+
const flat = flatten(command)
|
|
452
|
+
const [head, ...tail] = flat.commands
|
|
453
|
+
let handle = spawnCommand(head)
|
|
454
|
+
for (let i = 0; i < tail.length; i++) {
|
|
455
|
+
const next = tail[i]
|
|
456
|
+
const opts = flat.opts[i] ?? {}
|
|
457
|
+
const sin = stdin(next.options)
|
|
458
|
+
const stream = Stream.unwrap(Effect.map(handle, (x) => source(x, opts.from)))
|
|
459
|
+
const to = opts.to ?? "stdin"
|
|
460
|
+
if (to === "stdin") {
|
|
461
|
+
handle = spawnCommand(
|
|
462
|
+
ChildProcess.make(next.command, next.args, {
|
|
463
|
+
...next.options,
|
|
464
|
+
stdin: { ...sin, stream },
|
|
465
|
+
}),
|
|
466
|
+
)
|
|
467
|
+
continue
|
|
468
|
+
}
|
|
469
|
+
const fd = ChildProcess.parseFdName(to)
|
|
470
|
+
if (Predicate.isUndefined(fd)) {
|
|
471
|
+
handle = spawnCommand(
|
|
472
|
+
ChildProcess.make(next.command, next.args, {
|
|
473
|
+
...next.options,
|
|
474
|
+
stdin: { ...sin, stream },
|
|
475
|
+
}),
|
|
476
|
+
)
|
|
477
|
+
continue
|
|
478
|
+
}
|
|
479
|
+
handle = spawnCommand(
|
|
480
|
+
ChildProcess.make(next.command, next.args, {
|
|
481
|
+
...next.options,
|
|
482
|
+
additionalFds: {
|
|
483
|
+
...next.options.additionalFds,
|
|
484
|
+
[ChildProcess.fdName(fd) as `fd${number}`]: { type: "input", stream },
|
|
485
|
+
},
|
|
486
|
+
}),
|
|
487
|
+
)
|
|
488
|
+
}
|
|
489
|
+
return yield* handle
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
},
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
return makeSpawner(spawnCommand)
|
|
496
|
+
})
|
|
497
|
+
|
|
498
|
+
export const layer: Layer.Layer<ChildProcessSpawner, never, FileSystem.FileSystem | Path.Path> = Layer.effect(
|
|
499
|
+
ChildProcessSpawner,
|
|
500
|
+
make,
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
export const defaultLayer = layer.pipe(Layer.provide(NodeFileSystem.layer), Layer.provide(NodePath.layer))
|
|
504
|
+
|
|
505
|
+
export * as CrossSpawnSpawner from "./cross-spawn-spawner"
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type * as Arr from "effect/Array"
|
|
2
|
+
import * as PlatformError from "effect/PlatformError"
|
|
3
|
+
import * as ChildProcess from "effect/unstable/process/ChildProcess"
|
|
4
|
+
import type { Deferred } from "effect/Deferred"
|
|
5
|
+
|
|
6
|
+
export const toError = (err: unknown): Error =>
|
|
7
|
+
err instanceof globalThis.Error ? err : new globalThis.Error(String(err))
|
|
8
|
+
|
|
9
|
+
const toTag = (err: NodeJS.ErrnoException): PlatformError.SystemErrorTag => {
|
|
10
|
+
switch (err.code) {
|
|
11
|
+
case "ENOENT":
|
|
12
|
+
return "NotFound"
|
|
13
|
+
case "EACCES":
|
|
14
|
+
return "PermissionDenied"
|
|
15
|
+
case "EEXIST":
|
|
16
|
+
return "AlreadyExists"
|
|
17
|
+
case "EISDIR":
|
|
18
|
+
return "BadResource"
|
|
19
|
+
case "ENOTDIR":
|
|
20
|
+
return "BadResource"
|
|
21
|
+
case "EBUSY":
|
|
22
|
+
return "Busy"
|
|
23
|
+
case "ELOOP":
|
|
24
|
+
return "BadResource"
|
|
25
|
+
default:
|
|
26
|
+
return "Unknown"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const flatten = (command: ChildProcess.Command) => {
|
|
31
|
+
const commands: Array<ChildProcess.StandardCommand> = []
|
|
32
|
+
const opts: Array<ChildProcess.PipeOptions> = []
|
|
33
|
+
|
|
34
|
+
const walk = (cmd: ChildProcess.Command): void => {
|
|
35
|
+
switch (cmd._tag) {
|
|
36
|
+
case "StandardCommand":
|
|
37
|
+
commands.push(cmd)
|
|
38
|
+
return
|
|
39
|
+
case "PipedCommand":
|
|
40
|
+
walk(cmd.left)
|
|
41
|
+
opts.push(cmd.options)
|
|
42
|
+
walk(cmd.right)
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
walk(command)
|
|
48
|
+
if (commands.length === 0) throw new Error("flatten produced empty commands array")
|
|
49
|
+
const [head, ...tail] = commands
|
|
50
|
+
return {
|
|
51
|
+
commands: [head, ...tail] as Arr.NonEmptyReadonlyArray<ChildProcess.StandardCommand>,
|
|
52
|
+
opts,
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const toPlatformError = (
|
|
57
|
+
method: string,
|
|
58
|
+
err: NodeJS.ErrnoException,
|
|
59
|
+
command: ChildProcess.Command,
|
|
60
|
+
): PlatformError.PlatformError => {
|
|
61
|
+
const cmd = flatten(command)
|
|
62
|
+
.commands.map((x) => `${x.command} ${x.args.join(" ")}`)
|
|
63
|
+
.join(" | ")
|
|
64
|
+
return PlatformError.systemError({
|
|
65
|
+
_tag: toTag(err),
|
|
66
|
+
module: "ChildProcess",
|
|
67
|
+
method,
|
|
68
|
+
pathOrDescriptor: cmd,
|
|
69
|
+
syscall: err.syscall,
|
|
70
|
+
cause: err,
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export type ExitSignal = Deferred<readonly [code: number | null, signal: NodeJS.Signals | null]>
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Cause, Effect, Logger, References } from "effect"
|
|
2
|
+
import * as Log from "../util/log"
|
|
3
|
+
|
|
4
|
+
type Fields = Record<string, unknown>
|
|
5
|
+
|
|
6
|
+
const normalizeKey = (key: string) => (key === "sessionID" ? "session.id" : key)
|
|
7
|
+
|
|
8
|
+
export interface Handle {
|
|
9
|
+
readonly debug: (msg?: unknown, extra?: Fields) => Effect.Effect<void>
|
|
10
|
+
readonly info: (msg?: unknown, extra?: Fields) => Effect.Effect<void>
|
|
11
|
+
readonly warn: (msg?: unknown, extra?: Fields) => Effect.Effect<void>
|
|
12
|
+
readonly error: (msg?: unknown, extra?: Fields) => Effect.Effect<void>
|
|
13
|
+
readonly with: (extra: Fields) => Handle
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const clean = (input?: Fields): Fields =>
|
|
17
|
+
Object.fromEntries(
|
|
18
|
+
Object.entries(input ?? {})
|
|
19
|
+
.filter((entry) => entry[1] !== undefined && entry[1] !== null)
|
|
20
|
+
.map(([key, value]) => [normalizeKey(key), value]),
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
const text = (input: unknown): string => {
|
|
24
|
+
// oxlint-disable-next-line no-base-to-string
|
|
25
|
+
if (Array.isArray(input)) return input.map((item) => String(item)).join(" ")
|
|
26
|
+
// oxlint-disable-next-line no-base-to-string
|
|
27
|
+
return input === undefined ? "" : String(input)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const call = (run: (msg?: unknown) => Effect.Effect<void>, base: Fields, msg?: unknown, extra?: Fields) => {
|
|
31
|
+
const ann = clean({ ...base, ...extra })
|
|
32
|
+
const fx = run(msg)
|
|
33
|
+
return Object.keys(ann).length ? Effect.annotateLogs(fx, ann) : fx
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const logger = Logger.make((opts) => {
|
|
37
|
+
const extra = clean(opts.fiber.getRef(References.CurrentLogAnnotations))
|
|
38
|
+
const now = opts.date.getTime()
|
|
39
|
+
for (const [key, start] of opts.fiber.getRef(References.CurrentLogSpans)) {
|
|
40
|
+
extra[`logSpan.${key}`] = `${now - start}ms`
|
|
41
|
+
}
|
|
42
|
+
if (opts.cause.reasons.length > 0) {
|
|
43
|
+
extra.cause = Cause.pretty(opts.cause)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const svc = typeof extra.service === "string" ? extra.service : undefined
|
|
47
|
+
if (svc) delete extra.service
|
|
48
|
+
const log = svc ? Log.create({ service: svc }) : Log.Default
|
|
49
|
+
const msg = text(opts.message)
|
|
50
|
+
|
|
51
|
+
switch (opts.logLevel) {
|
|
52
|
+
case "Trace":
|
|
53
|
+
case "Debug":
|
|
54
|
+
return log.debug(msg, extra)
|
|
55
|
+
case "Warn":
|
|
56
|
+
return log.warn(msg, extra)
|
|
57
|
+
case "Error":
|
|
58
|
+
case "Fatal":
|
|
59
|
+
return log.error(msg, extra)
|
|
60
|
+
default:
|
|
61
|
+
return log.info(msg, extra)
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
export const layer = Logger.layer([logger], { mergeWithExisting: false })
|
|
66
|
+
|
|
67
|
+
export const create = (base: Fields = {}): Handle => ({
|
|
68
|
+
debug: (msg, extra) => call((item) => Effect.logDebug(item), base, msg, extra),
|
|
69
|
+
info: (msg, extra) => call((item) => Effect.logInfo(item), base, msg, extra),
|
|
70
|
+
warn: (msg, extra) => call((item) => Effect.logWarning(item), base, msg, extra),
|
|
71
|
+
error: (msg, extra) => call((item) => Effect.logError(item), base, msg, extra),
|
|
72
|
+
with: (extra) => create({ ...base, ...extra }),
|
|
73
|
+
})
|