@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
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 }
|