@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,283 @@
1
+ import path from "path"
2
+ import os from "os"
3
+ import { randomUUID } from "crypto"
4
+ import { Context, Effect, Function, Layer, Option, Schedule, Schema } from "effect"
5
+ import type { FileSystem, Scope } from "effect"
6
+ import type { PlatformError } from "effect/PlatformError"
7
+ import { AppFileSystem } from "../filesystem"
8
+ import { Global } from "../global"
9
+ import { Hash } from "./hash"
10
+
11
+ export namespace EffectFlock {
12
+ // ---------------------------------------------------------------------------
13
+ // Errors
14
+ // ---------------------------------------------------------------------------
15
+
16
+ export class LockTimeoutError extends Schema.TaggedErrorClass<LockTimeoutError>()("LockTimeoutError", {
17
+ key: Schema.String,
18
+ }) {}
19
+
20
+ export class LockCompromisedError extends Schema.TaggedErrorClass<LockCompromisedError>()("LockCompromisedError", {
21
+ detail: Schema.String,
22
+ }) {}
23
+
24
+ class ReleaseError extends Schema.TaggedErrorClass<ReleaseError>()("ReleaseError", {
25
+ detail: Schema.String,
26
+ cause: Schema.optional(Schema.Defect),
27
+ }) {
28
+ override get message() {
29
+ return this.detail
30
+ }
31
+ }
32
+
33
+ /** Internal: signals "lock is held, retry later". Never leaks to callers. */
34
+ class NotAcquired extends Schema.TaggedErrorClass<NotAcquired>()("NotAcquired", {}) {}
35
+
36
+ export type LockError = LockTimeoutError | LockCompromisedError
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // Timing (baked in — no caller ever overrides these)
40
+ // ---------------------------------------------------------------------------
41
+
42
+ const STALE_MS = 60_000
43
+ const TIMEOUT_MS = 5 * 60_000
44
+ const BASE_DELAY_MS = 100
45
+ const MAX_DELAY_MS = 2_000
46
+ const HEARTBEAT_MS = Math.max(100, Math.floor(STALE_MS / 3))
47
+
48
+ const retrySchedule = Schedule.exponential(BASE_DELAY_MS, 1.7).pipe(
49
+ Schedule.either(Schedule.spaced(MAX_DELAY_MS)),
50
+ Schedule.jittered,
51
+ Schedule.while((meta) => meta.elapsed < TIMEOUT_MS),
52
+ )
53
+
54
+ // ---------------------------------------------------------------------------
55
+ // Lock metadata schema
56
+ // ---------------------------------------------------------------------------
57
+
58
+ const LockMetaJson = Schema.fromJsonString(
59
+ Schema.Struct({
60
+ token: Schema.String,
61
+ pid: Schema.Number,
62
+ hostname: Schema.String,
63
+ createdAt: Schema.String,
64
+ }),
65
+ )
66
+
67
+ const decodeMeta = Schema.decodeUnknownSync(LockMetaJson)
68
+ const encodeMeta = Schema.encodeSync(LockMetaJson)
69
+
70
+ // ---------------------------------------------------------------------------
71
+ // Service
72
+ // ---------------------------------------------------------------------------
73
+
74
+ export interface Interface {
75
+ readonly acquire: (key: string, dir?: string) => Effect.Effect<void, LockError, Scope.Scope>
76
+ readonly withLock: {
77
+ (key: string, dir?: string): <A, E, R>(body: Effect.Effect<A, E, R>) => Effect.Effect<A, E | LockError, R>
78
+ <A, E, R>(body: Effect.Effect<A, E, R>, key: string, dir?: string): Effect.Effect<A, E | LockError, R>
79
+ }
80
+ }
81
+
82
+ export class Service extends Context.Service<Service, Interface>()("EffectFlock") {}
83
+
84
+ // ---------------------------------------------------------------------------
85
+ // Layer
86
+ // ---------------------------------------------------------------------------
87
+
88
+ function wall() {
89
+ return performance.timeOrigin + performance.now()
90
+ }
91
+
92
+ const mtimeMs = (info: FileSystem.File.Info) => Option.getOrElse(info.mtime, () => new Date(0)).getTime()
93
+
94
+ const isPathGone = (e: PlatformError) => e.reason._tag === "NotFound" || e.reason._tag === "Unknown"
95
+
96
+ export const layer: Layer.Layer<Service, never, Global.Service | AppFileSystem.Service> = Layer.effect(
97
+ Service,
98
+ Effect.gen(function* () {
99
+ const global = yield* Global.Service
100
+ const fs = yield* AppFileSystem.Service
101
+ const lockRoot = path.join(global.state, "locks")
102
+ const hostname = os.hostname()
103
+ const ensuredDirs = new Set<string>()
104
+
105
+ // -- helpers (close over fs) --
106
+
107
+ const safeStat = (file: string) =>
108
+ fs.stat(file).pipe(
109
+ Effect.catchIf(isPathGone, () => Effect.void),
110
+ Effect.orDie,
111
+ )
112
+
113
+ const forceRemove = (target: string) => fs.remove(target, { recursive: true }).pipe(Effect.ignore)
114
+
115
+ /** Atomic mkdir — returns true if created, false if already exists, dies on other errors. */
116
+ const atomicMkdir = (dir: string) =>
117
+ fs.makeDirectory(dir, { mode: 0o700 }).pipe(
118
+ Effect.as(true),
119
+ Effect.catchIf(
120
+ (e) => e.reason._tag === "AlreadyExists",
121
+ () => Effect.succeed(false),
122
+ ),
123
+ Effect.orDie,
124
+ )
125
+
126
+ /** Write with exclusive create — compromised error if file already exists. */
127
+ const exclusiveWrite = (filePath: string, content: string, lockDir: string, detail: string) =>
128
+ fs.writeFileString(filePath, content, { flag: "wx" }).pipe(
129
+ Effect.catch(() =>
130
+ Effect.gen(function* () {
131
+ yield* forceRemove(lockDir)
132
+ return yield* new LockCompromisedError({ detail })
133
+ }),
134
+ ),
135
+ )
136
+
137
+ const cleanStaleBreaker = Effect.fnUntraced(function* (breakerPath: string) {
138
+ const bs = yield* safeStat(breakerPath)
139
+ if (bs && wall() - mtimeMs(bs) > STALE_MS) yield* forceRemove(breakerPath)
140
+ return false
141
+ })
142
+
143
+ const ensureDir = Effect.fnUntraced(function* (dir: string) {
144
+ if (ensuredDirs.has(dir)) return
145
+ yield* fs.makeDirectory(dir, { recursive: true }).pipe(Effect.orDie)
146
+ ensuredDirs.add(dir)
147
+ })
148
+
149
+ const isStale = Effect.fnUntraced(function* (lockDir: string, heartbeatPath: string, metaPath: string) {
150
+ const now = wall()
151
+
152
+ const hb = yield* safeStat(heartbeatPath)
153
+ if (hb) return now - mtimeMs(hb) > STALE_MS
154
+
155
+ const meta = yield* safeStat(metaPath)
156
+ if (meta) return now - mtimeMs(meta) > STALE_MS
157
+
158
+ const dir = yield* safeStat(lockDir)
159
+ if (!dir) return false
160
+
161
+ return now - mtimeMs(dir) > STALE_MS
162
+ })
163
+
164
+ // -- single lock attempt --
165
+
166
+ type Handle = { token: string; metaPath: string; heartbeatPath: string; lockDir: string }
167
+
168
+ const tryAcquireLockDir = (lockDir: string, key: string) =>
169
+ Effect.gen(function* () {
170
+ const token = randomUUID()
171
+ const metaPath = path.join(lockDir, "meta.json")
172
+ const heartbeatPath = path.join(lockDir, "heartbeat")
173
+
174
+ // Atomic mkdir — the POSIX lock primitive
175
+ const created = yield* atomicMkdir(lockDir)
176
+
177
+ if (!created) {
178
+ if (!(yield* isStale(lockDir, heartbeatPath, metaPath))) return yield* new NotAcquired()
179
+
180
+ // Stale — race for breaker ownership
181
+ const breakerPath = lockDir + ".breaker"
182
+
183
+ const claimed = yield* fs.makeDirectory(breakerPath, { mode: 0o700 }).pipe(
184
+ Effect.as(true),
185
+ Effect.catchIf(
186
+ (e) => e.reason._tag === "AlreadyExists",
187
+ () => cleanStaleBreaker(breakerPath),
188
+ ),
189
+ Effect.catchIf(isPathGone, () => Effect.succeed(false)),
190
+ Effect.orDie,
191
+ )
192
+
193
+ if (!claimed) return yield* new NotAcquired()
194
+
195
+ // We own the breaker — double-check staleness, nuke, recreate
196
+ const recreated = yield* Effect.gen(function* () {
197
+ if (!(yield* isStale(lockDir, heartbeatPath, metaPath))) return false
198
+ yield* forceRemove(lockDir)
199
+ return yield* atomicMkdir(lockDir)
200
+ }).pipe(Effect.ensuring(forceRemove(breakerPath)))
201
+
202
+ if (!recreated) return yield* new NotAcquired()
203
+ }
204
+
205
+ // We own the lock dir — write heartbeat + meta with exclusive create
206
+ yield* exclusiveWrite(heartbeatPath, "", lockDir, "heartbeat already existed")
207
+
208
+ const metaJson = encodeMeta({ token, pid: process.pid, hostname, createdAt: new Date().toISOString() })
209
+ yield* exclusiveWrite(metaPath, metaJson, lockDir, "meta.json already existed")
210
+
211
+ return { token, metaPath, heartbeatPath, lockDir } satisfies Handle
212
+ }).pipe(
213
+ Effect.withSpan("EffectFlock.tryAcquire", {
214
+ attributes: { key },
215
+ }),
216
+ )
217
+
218
+ // -- retry wrapper (preserves Handle type) --
219
+
220
+ const acquireHandle = (lockfile: string, key: string): Effect.Effect<Handle, LockError> =>
221
+ tryAcquireLockDir(lockfile, key).pipe(
222
+ Effect.retry({
223
+ while: (err) => err._tag === "NotAcquired",
224
+ schedule: retrySchedule,
225
+ }),
226
+ Effect.catchTag("NotAcquired", () => Effect.fail(new LockTimeoutError({ key }))),
227
+ )
228
+
229
+ // -- release --
230
+
231
+ const release = (handle: Handle) =>
232
+ Effect.gen(function* () {
233
+ const raw = yield* fs.readFileString(handle.metaPath).pipe(
234
+ Effect.catch((err) => {
235
+ if (isPathGone(err)) return Effect.die(new ReleaseError({ detail: "metadata missing" }))
236
+ return Effect.die(err)
237
+ }),
238
+ )
239
+
240
+ const parsed = yield* Effect.try({
241
+ try: () => decodeMeta(raw),
242
+ catch: (cause) => new ReleaseError({ detail: "metadata invalid", cause }),
243
+ }).pipe(Effect.orDie)
244
+
245
+ if (parsed.token !== handle.token) return yield* Effect.die(new ReleaseError({ detail: "token mismatch" }))
246
+
247
+ yield* forceRemove(handle.lockDir)
248
+ })
249
+
250
+ // -- build service --
251
+
252
+ const acquire = Effect.fn("EffectFlock.acquire")(function* (key: string, dir?: string) {
253
+ const lockDir = dir ?? lockRoot
254
+ yield* ensureDir(lockDir)
255
+
256
+ const lockfile = path.join(lockDir, Hash.fast(key) + ".lock")
257
+
258
+ // acquireRelease: acquire is uninterruptible, release is guaranteed
259
+ const handle = yield* Effect.acquireRelease(acquireHandle(lockfile, key), (handle) => release(handle))
260
+
261
+ // Heartbeat fiber — scoped, so it's interrupted before release runs
262
+ yield* fs
263
+ .utimes(handle.heartbeatPath, new Date(), new Date())
264
+ .pipe(Effect.ignore, Effect.repeat(Schedule.spaced(HEARTBEAT_MS)), Effect.forkScoped)
265
+ })
266
+
267
+ const withLock: Interface["withLock"] = Function.dual(
268
+ (args) => Effect.isEffect(args[0]),
269
+ <A, E, R>(body: Effect.Effect<A, E, R>, key: string, dir?: string): Effect.Effect<A, E | LockError, R> =>
270
+ Effect.scoped(
271
+ Effect.gen(function* () {
272
+ yield* acquire(key, dir)
273
+ return yield* body
274
+ }),
275
+ ),
276
+ )
277
+
278
+ return Service.of({ acquire, withLock })
279
+ }),
280
+ )
281
+
282
+ export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer), Layer.provide(Global.layer))
283
+ }
@@ -0,0 +1,52 @@
1
+ export function base64Encode(value: string) {
2
+ const normalized = value.replace(/\\/g, "/")
3
+ const bytes = new TextEncoder().encode(normalized)
4
+ const binary = Array.from(bytes, (b) => String.fromCharCode(b)).join("")
5
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "")
6
+ }
7
+
8
+ export function base64Decode(value: string) {
9
+ const binary = atob(value.replace(/-/g, "+").replace(/_/g, "/"))
10
+ const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0))
11
+ return new TextDecoder().decode(bytes)
12
+ }
13
+
14
+ export async function hash(content: string, algorithm = "SHA-256"): Promise<string> {
15
+ const encoder = new TextEncoder()
16
+ const data = encoder.encode(content)
17
+ const hashBuffer = await crypto.subtle.digest(algorithm, data)
18
+ const hashArray = Array.from(new Uint8Array(hashBuffer))
19
+ const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("")
20
+ return hashHex
21
+ }
22
+
23
+ export function checksum(content: string): string | undefined {
24
+ if (!content) return undefined
25
+ let hash = 0x811c9dc5
26
+ for (let i = 0; i < content.length; i++) {
27
+ hash ^= content.charCodeAt(i)
28
+ hash = Math.imul(hash, 0x01000193)
29
+ }
30
+ return (hash >>> 0).toString(36)
31
+ }
32
+
33
+ export function sampledChecksum(content: string, limit = 500_000): string | undefined {
34
+ if (!content) return undefined
35
+ if (content.length <= limit) return checksum(content)
36
+
37
+ const size = 4096
38
+ const points = [
39
+ 0,
40
+ Math.floor(content.length * 0.25),
41
+ Math.floor(content.length * 0.5),
42
+ Math.floor(content.length * 0.75),
43
+ content.length - size,
44
+ ]
45
+ const hashes = points
46
+ .map((point) => {
47
+ const start = Math.max(0, Math.min(content.length - size, point - Math.floor(size / 2)))
48
+ return checksum(content.slice(start, start + size)) ?? ""
49
+ })
50
+ .join(":")
51
+ return `${content.length}:${hashes}`
52
+ }
@@ -0,0 +1,60 @@
1
+ import z from "zod"
2
+
3
+ export abstract class NamedError extends Error {
4
+ abstract schema(): z.core.$ZodType
5
+ abstract toObject(): { name: string; data: any }
6
+
7
+ static hasName(error: unknown, name: string): boolean {
8
+ return (
9
+ typeof error === "object" && error !== null && "name" in error && (error as Record<string, unknown>).name === name
10
+ )
11
+ }
12
+
13
+ static create<Name extends string, Data extends z.core.$ZodType>(name: Name, data: Data) {
14
+ const schema = z
15
+ .object({
16
+ name: z.literal(name),
17
+ data,
18
+ })
19
+ .meta({
20
+ ref: name,
21
+ })
22
+ const result = class extends NamedError {
23
+ public static readonly Schema = schema
24
+
25
+ public override readonly name = name as Name
26
+
27
+ constructor(
28
+ public readonly data: z.input<Data>,
29
+ options?: ErrorOptions,
30
+ ) {
31
+ super(name, options)
32
+ this.name = name
33
+ }
34
+
35
+ static isInstance(input: any): input is InstanceType<typeof result> {
36
+ return typeof input === "object" && "name" in input && input.name === name
37
+ }
38
+
39
+ schema() {
40
+ return schema
41
+ }
42
+
43
+ toObject() {
44
+ return {
45
+ name: name,
46
+ data: this.data,
47
+ }
48
+ }
49
+ }
50
+ Object.defineProperty(result, "name", { value: name })
51
+ return result
52
+ }
53
+
54
+ public static readonly Unknown = NamedError.create(
55
+ "UnknownError",
56
+ z.object({
57
+ message: z.string(),
58
+ }),
59
+ )
60
+ }