@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.
Files changed (51) hide show
  1. package/package.json +52 -0
  2. package/src/cross-spawn-process.ts +273 -0
  3. package/src/cross-spawn-spawner.ts +505 -0
  4. package/src/cross-spawn-utils.ts +74 -0
  5. package/src/effect/logger.ts +73 -0
  6. package/src/effect/memo-map.ts +3 -0
  7. package/src/effect/observability.ts +107 -0
  8. package/src/effect/runtime.ts +21 -0
  9. package/src/filesystem.ts +262 -0
  10. package/src/flag/flag.ts +107 -0
  11. package/src/global.ts +91 -0
  12. package/src/installation/version.ts +11 -0
  13. package/src/npm-config.ts +40 -0
  14. package/src/npm.ts +271 -0
  15. package/src/saeeol/global.ts +23 -0
  16. package/src/saeeol/kilocode/global.ts +23 -0
  17. package/src/saeeol/kilocode/spotlight.ts +23 -0
  18. package/src/saeeol/spotlight.ts +23 -0
  19. package/src/util/array.ts +10 -0
  20. package/src/util/binary.ts +41 -0
  21. package/src/util/effect-flock.ts +283 -0
  22. package/src/util/encode.ts +52 -0
  23. package/src/util/error.ts +60 -0
  24. package/src/util/flock.ts +358 -0
  25. package/src/util/glob.ts +34 -0
  26. package/src/util/hash.ts +7 -0
  27. package/src/util/identifier.ts +48 -0
  28. package/src/util/iife.ts +3 -0
  29. package/src/util/lazy.ts +11 -0
  30. package/src/util/log.ts +208 -0
  31. package/src/util/module.ts +10 -0
  32. package/src/util/path.ts +37 -0
  33. package/src/util/retry.ts +42 -0
  34. package/src/util/saeeol-process.ts +24 -0
  35. package/src/util/slug.ts +74 -0
  36. package/sst-env.d.ts +10 -0
  37. package/test/effect/cross-spawn-spawner.test.ts +423 -0
  38. package/test/effect/observability.test.ts +46 -0
  39. package/test/filesystem/filesystem.test.ts +338 -0
  40. package/test/fixture/effect-flock-worker.ts +60 -0
  41. package/test/fixture/flock-worker.ts +72 -0
  42. package/test/fixture/tmpdir.ts +13 -0
  43. package/test/global.test.ts +16 -0
  44. package/test/lib/effect.ts +53 -0
  45. package/test/npm-config.test.ts +51 -0
  46. package/test/npm.test.ts +91 -0
  47. package/test/saeeol/filesystem-containment.test.ts +13 -0
  48. package/test/saeeol/kilocode/filesystem-containment.test.ts +13 -0
  49. package/test/util/effect-flock.test.ts +386 -0
  50. package/test/util/flock.test.ts +426 -0
  51. 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
+ })
@@ -0,0 +1,3 @@
1
+ import { Layer } from "effect"
2
+
3
+ export const memoMap = Layer.makeMemoMapUnsafe()