@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
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/package.json",
3
+ "version": "7.3.1",
4
+ "name": "@saeeol/core",
5
+ "type": "module",
6
+ "license": "Apache-2.0",
7
+ "publishConfig": {
8
+ "access": "public",
9
+ "registry": "https://registry.npmjs.org"
10
+ },
11
+ "scripts": {
12
+ "test": "bun test",
13
+ "test:ci": "mkdir -p .artifacts/unit && bun test --timeout 30000 --reporter=junit --reporter-outfile=.artifacts/unit/junit.xml",
14
+ "typecheck": "tsgo --noEmit"
15
+ },
16
+ "exports": {
17
+ "./*": "./src/*.ts"
18
+ },
19
+ "imports": {},
20
+ "devDependencies": {
21
+ "@tsconfig/bun": "catalog:",
22
+ "@types/bun": "catalog:",
23
+ "@types/cross-spawn": "catalog:",
24
+ "@types/npm-package-arg": "6.1.4",
25
+ "@types/npmcli__arborist": "6.3.3",
26
+ "@types/semver": "catalog:"
27
+ },
28
+ "dependencies": {
29
+ "@effect/opentelemetry": "catalog:",
30
+ "@effect/platform-node": "catalog:",
31
+ "@npmcli/arborist": "9.4.0",
32
+ "@npmcli/config": "10.8.1",
33
+ "@opentelemetry/api": "1.9.0",
34
+ "@opentelemetry/context-async-hooks": "2.6.1",
35
+ "@opentelemetry/exporter-trace-otlp-http": "0.214.0",
36
+ "@opentelemetry/sdk-trace-base": "2.6.1",
37
+ "effect": "catalog:",
38
+ "cross-spawn": "catalog:",
39
+ "glob": "13.0.5",
40
+ "mime-types": "3.0.2",
41
+ "minimatch": "10.2.5",
42
+ "npm-package-arg": "13.0.2",
43
+ "rotating-file-stream": "3.2.9",
44
+ "semver": "^7.6.3",
45
+ "xdg-basedir": "5.1.0",
46
+ "zod": "catalog:"
47
+ },
48
+ "overrides": {
49
+ "drizzle-orm": "catalog:"
50
+ },
51
+ "peerDependencies": {}
52
+ }
@@ -0,0 +1,273 @@
1
+ import { NodeSink, NodeStream } from "@effect/platform-node"
2
+ import * as Deferred from "effect/Deferred"
3
+ import * as Effect from "effect/Effect"
4
+ import * as Exit from "effect/Exit"
5
+ import * as PlatformError from "effect/PlatformError"
6
+ import * as Predicate from "effect/Predicate"
7
+ import * as Sink from "effect/Sink"
8
+ import * as Stream from "effect/Stream"
9
+ import * as ChildProcess from "effect/unstable/process/ChildProcess"
10
+ import type { ChildProcessHandle } from "effect/unstable/process/ChildProcessSpawner"
11
+ import {
12
+ makeHandle,
13
+ ProcessId,
14
+ ExitCode,
15
+ } from "effect/unstable/process/ChildProcessSpawner"
16
+ import * as NodeChildProcess from "node:child_process"
17
+ import { PassThrough } from "node:stream"
18
+ import launch from "cross-spawn"
19
+ import { toError, toPlatformError, type ExitSignal } from "./cross-spawn-utils"
20
+
21
+ export const env = (opts: ChildProcess.CommandOptions) =>
22
+ opts.extendEnv ? { ...globalThis.process.env, ...opts.env } : opts.env
23
+
24
+ const input = (x: ChildProcess.CommandInput | undefined): NodeChildProcess.IOType | undefined =>
25
+ Stream.isStream(x) ? "pipe" : x
26
+
27
+ const output = (x: ChildProcess.CommandOutput | undefined): NodeChildProcess.IOType | undefined =>
28
+ Sink.isSink(x) ? "pipe" : x
29
+
30
+ export const stdin = (opts: ChildProcess.CommandOptions): ChildProcess.StdinConfig => {
31
+ const cfg: ChildProcess.StdinConfig = { stream: "pipe", encoding: "utf-8", endOnDone: true }
32
+ if (Predicate.isUndefined(opts.stdin)) return cfg
33
+ if (typeof opts.stdin === "string") return { ...cfg, stream: opts.stdin }
34
+ if (Stream.isStream(opts.stdin)) return { ...cfg, stream: opts.stdin }
35
+ return {
36
+ stream: opts.stdin.stream,
37
+ encoding: opts.stdin.encoding ?? cfg.encoding,
38
+ endOnDone: opts.stdin.endOnDone ?? cfg.endOnDone,
39
+ }
40
+ }
41
+
42
+ export const stdio = (opts: ChildProcess.CommandOptions, key: "stdout" | "stderr"): ChildProcess.StdoutConfig => {
43
+ const cfg = opts[key]
44
+ if (Predicate.isUndefined(cfg)) return { stream: "pipe" }
45
+ if (typeof cfg === "string") return { stream: cfg }
46
+ if (Sink.isSink(cfg)) return { stream: cfg }
47
+ return { stream: cfg.stream }
48
+ }
49
+
50
+ export const fds = (opts: ChildProcess.CommandOptions) => {
51
+ if (Predicate.isUndefined(opts.additionalFds)) return []
52
+ return Object.entries(opts.additionalFds)
53
+ .flatMap(([name, config]) => {
54
+ const fd = ChildProcess.parseFdName(name)
55
+ return Predicate.isUndefined(fd) ? [] : [{ fd, config }]
56
+ })
57
+ .toSorted((a, b) => a.fd - b.fd)
58
+ }
59
+
60
+ export const stdios = (
61
+ sin: ChildProcess.StdinConfig,
62
+ sout: ChildProcess.StdoutConfig,
63
+ serr: ChildProcess.StderrConfig,
64
+ extra: ReadonlyArray<{ fd: number; config: ChildProcess.AdditionalFdConfig }>,
65
+ ): NodeChildProcess.StdioOptions => {
66
+ const pipe = (x: NodeChildProcess.IOType | undefined) =>
67
+ process.platform === "win32" && x === "pipe" ? "overlapped" : x
68
+ const arr: Array<NodeChildProcess.IOType | undefined> = [
69
+ pipe(input(sin.stream)),
70
+ pipe(output(sout.stream)),
71
+ pipe(output(serr.stream)),
72
+ ]
73
+ if (extra.length === 0) return arr as NodeChildProcess.StdioOptions
74
+ const max = extra.reduce((acc, x) => Math.max(acc, x.fd), 2)
75
+ for (let i = 3; i <= max; i++) arr[i] = "ignore"
76
+ for (const x of extra) arr[x.fd] = pipe("pipe")
77
+ return arr as NodeChildProcess.StdioOptions
78
+ }
79
+
80
+ export const setupFds = Effect.fnUntraced(function* (
81
+ command: ChildProcess.StandardCommand,
82
+ proc: NodeChildProcess.ChildProcess,
83
+ extra: ReadonlyArray<{ fd: number; config: ChildProcess.AdditionalFdConfig }>,
84
+ ) {
85
+ if (extra.length === 0) {
86
+ return {
87
+ getInputFd: () => Sink.drain,
88
+ getOutputFd: () => Stream.empty,
89
+ }
90
+ }
91
+
92
+ const ins = new Map<number, Sink.Sink<void, Uint8Array, never, PlatformError.PlatformError>>()
93
+ const outs = new Map<number, Stream.Stream<Uint8Array, PlatformError.PlatformError>>()
94
+
95
+ for (const x of extra) {
96
+ const node = proc.stdio[x.fd]
97
+ switch (x.config.type) {
98
+ case "input": {
99
+ let sink: Sink.Sink<void, Uint8Array, never, PlatformError.PlatformError> = Sink.drain
100
+ if (node && "write" in node) {
101
+ sink = NodeSink.fromWritable({
102
+ evaluate: () => node,
103
+ onError: (err) => toPlatformError(`fromWritable(fd${x.fd})`, toError(err), command),
104
+ endOnDone: true,
105
+ })
106
+ }
107
+ if (x.config.stream) yield* Effect.forkScoped(Stream.run(x.config.stream, sink))
108
+ ins.set(x.fd, sink)
109
+ break
110
+ }
111
+ case "output": {
112
+ let stream: Stream.Stream<Uint8Array, PlatformError.PlatformError> = Stream.empty
113
+ if (node && "read" in node) {
114
+ const tap = new PassThrough()
115
+ node.on("error", (err) => tap.destroy(toError(err)))
116
+ node.pipe(tap)
117
+ stream = NodeStream.fromReadable({
118
+ evaluate: () => tap,
119
+ onError: (err) => toPlatformError(`fromReadable(fd${x.fd})`, toError(err), command),
120
+ })
121
+ }
122
+ if (x.config.sink) stream = Stream.transduce(stream, x.config.sink)
123
+ outs.set(x.fd, stream)
124
+ break
125
+ }
126
+ }
127
+ }
128
+
129
+ return {
130
+ getInputFd: (fd: number) => ins.get(fd) ?? Sink.drain,
131
+ getOutputFd: (fd: number) => outs.get(fd) ?? Stream.empty,
132
+ }
133
+ })
134
+
135
+ export const setupStdin = (
136
+ command: ChildProcess.StandardCommand,
137
+ proc: NodeChildProcess.ChildProcess,
138
+ cfg: ChildProcess.StdinConfig,
139
+ ) =>
140
+ Effect.suspend(() => {
141
+ let sink: Sink.Sink<void, unknown, never, PlatformError.PlatformError> = Sink.drain
142
+ if (Predicate.isNotNull(proc.stdin)) {
143
+ sink = NodeSink.fromWritable({
144
+ evaluate: () => proc.stdin!,
145
+ onError: (err) => toPlatformError("fromWritable(stdin)", toError(err), command),
146
+ endOnDone: cfg.endOnDone,
147
+ encoding: cfg.encoding,
148
+ })
149
+ }
150
+ if (Stream.isStream(cfg.stream)) return Effect.as(Effect.forkScoped(Stream.run(cfg.stream, sink)), sink)
151
+ return Effect.succeed(sink)
152
+ })
153
+
154
+ export const setupOutput = (
155
+ command: ChildProcess.StandardCommand,
156
+ proc: NodeChildProcess.ChildProcess,
157
+ out: ChildProcess.StdoutConfig,
158
+ err: ChildProcess.StderrConfig,
159
+ ) => {
160
+ let stdout = proc.stdout
161
+ ? NodeStream.fromReadable({
162
+ evaluate: () => proc.stdout!,
163
+ onError: (cause) => toPlatformError("fromReadable(stdout)", toError(cause), command),
164
+ })
165
+ : Stream.empty
166
+ let stderr = proc.stderr
167
+ ? NodeStream.fromReadable({
168
+ evaluate: () => proc.stderr!,
169
+ onError: (cause) => toPlatformError("fromReadable(stderr)", toError(cause), command),
170
+ })
171
+ : Stream.empty
172
+
173
+ if (Sink.isSink(out.stream)) stdout = Stream.transduce(stdout, out.stream)
174
+ if (Sink.isSink(err.stream)) stderr = Stream.transduce(stderr, err.stream)
175
+
176
+ return { stdout, stderr, all: Stream.merge(stdout, stderr) }
177
+ }
178
+
179
+ export const spawn = (command: ChildProcess.StandardCommand, opts: NodeChildProcess.SpawnOptions) =>
180
+ Effect.callback<readonly [NodeChildProcess.ChildProcess, ExitSignal], PlatformError.PlatformError>((resume) => {
181
+ const signal = Deferred.makeUnsafe<readonly [code: number | null, signal: NodeJS.Signals | null]>()
182
+ const proc = launch(command.command, command.args, opts)
183
+ let end = false
184
+ let exit: readonly [code: number | null, signal: NodeJS.Signals | null] | undefined
185
+ proc.on("error", (err) => {
186
+ resume(Effect.fail(toPlatformError("spawn", err, command)))
187
+ })
188
+ proc.on("exit", (...args) => {
189
+ exit = args
190
+ })
191
+ proc.on("close", (...args) => {
192
+ if (end) return
193
+ end = true
194
+ Deferred.doneUnsafe(signal, Exit.succeed(exit ?? args))
195
+ })
196
+ proc.on("spawn", () => {
197
+ resume(Effect.succeed([proc, signal]))
198
+ })
199
+ return Effect.sync(() => {
200
+ proc.kill("SIGTERM")
201
+ })
202
+ })
203
+
204
+ export const killGroup = (
205
+ command: ChildProcess.StandardCommand,
206
+ proc: NodeChildProcess.ChildProcess,
207
+ signal: NodeJS.Signals,
208
+ ) => {
209
+ if (globalThis.process.platform === "win32") {
210
+ return Effect.callback<void, PlatformError.PlatformError>((resume) => {
211
+ NodeChildProcess.exec(`taskkill /pid ${proc.pid} /T /F`, { windowsHide: true }, (err) => {
212
+ if (err) return resume(Effect.fail(toPlatformError("kill", toError(err), command)))
213
+ resume(Effect.void)
214
+ })
215
+ })
216
+ }
217
+
218
+ return Effect.try({
219
+ try: () => {
220
+ globalThis.process.kill(-proc.pid!, signal)
221
+ },
222
+ catch: (err) => toPlatformError("kill", toError(err), command),
223
+ })
224
+ }
225
+
226
+ export const killOne = (
227
+ command: ChildProcess.StandardCommand,
228
+ proc: NodeChildProcess.ChildProcess,
229
+ signal: NodeJS.Signals,
230
+ ) =>
231
+ Effect.suspend(() => {
232
+ if (proc.kill(signal)) return Effect.void
233
+ return Effect.fail(toPlatformError("kill", new Error("Failed to kill child process"), command))
234
+ })
235
+
236
+ export const timeout =
237
+ (
238
+ proc: NodeChildProcess.ChildProcess,
239
+ command: ChildProcess.StandardCommand,
240
+ opts: ChildProcess.KillOptions | undefined,
241
+ ) =>
242
+ <A, E, R>(
243
+ f: (
244
+ command: ChildProcess.StandardCommand,
245
+ proc: NodeChildProcess.ChildProcess,
246
+ signal: NodeJS.Signals,
247
+ ) => Effect.Effect<A, E, R>,
248
+ ) => {
249
+ const signal = opts?.killSignal ?? "SIGTERM"
250
+ if (Predicate.isUndefined(opts?.forceKillAfter)) return f(command, proc, signal)
251
+ return Effect.timeoutOrElse(f(command, proc, signal), {
252
+ duration: opts.forceKillAfter,
253
+ orElse: () => f(command, proc, "SIGKILL"),
254
+ })
255
+ }
256
+
257
+ export const source = (handle: ChildProcessHandle, from: ChildProcess.PipeFromOption | undefined) => {
258
+ const opt = from ?? "stdout"
259
+ switch (opt) {
260
+ case "stdout":
261
+ return handle.stdout
262
+ case "stderr":
263
+ return handle.stderr
264
+ case "all":
265
+ return handle.all
266
+ default: {
267
+ const fd = ChildProcess.parseFdName(opt)
268
+ return Predicate.isNotUndefined(fd) ? handle.getOutputFd(fd) : handle.stdout
269
+ }
270
+ }
271
+ }
272
+
273
+ export { makeHandle, ProcessId, ExitCode }