@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,358 @@
1
+ import path from "path"
2
+ import os from "os"
3
+ import { randomBytes, randomUUID } from "crypto"
4
+ import { mkdir, readFile, rm, stat, utimes, writeFile } from "fs/promises"
5
+ import { Hash } from "./hash"
6
+ import { Effect } from "effect"
7
+
8
+ export type FlockGlobal = {
9
+ state: string
10
+ }
11
+
12
+ export namespace Flock {
13
+ let global: FlockGlobal | undefined
14
+
15
+ export function setGlobal(g: FlockGlobal) {
16
+ global = g
17
+ }
18
+
19
+ const root = () => {
20
+ if (!global) throw new Error("Flock global not set")
21
+ return path.join(global.state, "locks")
22
+ }
23
+
24
+ // Defaults for callers that do not provide timing options.
25
+ const defaultOpts = {
26
+ staleMs: 60_000,
27
+ timeoutMs: 5 * 60_000,
28
+ baseDelayMs: 100,
29
+ maxDelayMs: 2_000,
30
+ }
31
+
32
+ export interface WaitEvent {
33
+ key: string
34
+ attempt: number
35
+ delay: number
36
+ waited: number
37
+ }
38
+
39
+ export type Wait = (input: WaitEvent) => void | Promise<void>
40
+
41
+ export interface Options {
42
+ dir?: string
43
+ signal?: AbortSignal
44
+ staleMs?: number
45
+ timeoutMs?: number
46
+ baseDelayMs?: number
47
+ maxDelayMs?: number
48
+ onWait?: Wait
49
+ }
50
+
51
+ type Opts = {
52
+ staleMs: number
53
+ timeoutMs: number
54
+ baseDelayMs: number
55
+ maxDelayMs: number
56
+ }
57
+
58
+ type Owned = {
59
+ acquired: true
60
+ startHeartbeat: (intervalMs?: number) => void
61
+ release: () => Promise<void>
62
+ }
63
+
64
+ export interface Lease {
65
+ release: () => Promise<void>
66
+ [Symbol.asyncDispose]: () => Promise<void>
67
+ }
68
+
69
+ function code(err: unknown) {
70
+ if (typeof err !== "object" || err === null || !("code" in err)) return
71
+ const value = err.code
72
+ if (typeof value !== "string") return
73
+ return value
74
+ }
75
+
76
+ function sleep(ms: number, signal?: AbortSignal) {
77
+ return new Promise<void>((resolve, reject) => {
78
+ if (signal?.aborted) {
79
+ reject(signal.reason ?? new Error("Aborted"))
80
+ return
81
+ }
82
+
83
+ let timer: NodeJS.Timeout | undefined
84
+
85
+ const done = () => {
86
+ signal?.removeEventListener("abort", abort)
87
+ resolve()
88
+ }
89
+
90
+ const abort = () => {
91
+ if (timer) {
92
+ clearTimeout(timer)
93
+ }
94
+ signal?.removeEventListener("abort", abort)
95
+ reject(signal?.reason ?? new Error("Aborted"))
96
+ }
97
+
98
+ signal?.addEventListener("abort", abort, { once: true })
99
+ timer = setTimeout(done, ms)
100
+ })
101
+ }
102
+
103
+ function jitter(ms: number) {
104
+ const j = Math.floor(ms * 0.3)
105
+ const d = Math.floor(Math.random() * (2 * j + 1)) - j
106
+ return Math.max(0, ms + d)
107
+ }
108
+
109
+ function mono() {
110
+ return performance.now()
111
+ }
112
+
113
+ function wall() {
114
+ return performance.timeOrigin + mono()
115
+ }
116
+
117
+ async function stats(file: string) {
118
+ try {
119
+ return await stat(file)
120
+ } catch (err) {
121
+ const errCode = code(err)
122
+ if (errCode === "ENOENT" || errCode === "ENOTDIR") return
123
+ throw err
124
+ }
125
+ }
126
+
127
+ async function stale(lockDir: string, heartbeatPath: string, metaPath: string, staleMs: number) {
128
+ // Stale detection allows automatic recovery after crashed owners.
129
+ const now = wall()
130
+ const heartbeat = await stats(heartbeatPath)
131
+ if (heartbeat) {
132
+ return now - heartbeat.mtimeMs > staleMs
133
+ }
134
+
135
+ const meta = await stats(metaPath)
136
+ if (meta) {
137
+ return now - meta.mtimeMs > staleMs
138
+ }
139
+
140
+ const dir = await stats(lockDir)
141
+ if (!dir) {
142
+ return false
143
+ }
144
+
145
+ return now - dir.mtimeMs > staleMs
146
+ }
147
+
148
+ async function tryAcquireLockDir(lockDir: string, opts: Opts): Promise<Owned | { acquired: false }> {
149
+ const token = randomUUID?.() ?? randomBytes(16).toString("hex")
150
+ const metaPath = path.join(lockDir, "meta.json")
151
+ const heartbeatPath = path.join(lockDir, "heartbeat")
152
+
153
+ try {
154
+ await mkdir(lockDir, { mode: 0o700 })
155
+ } catch (err) {
156
+ if (code(err) !== "EEXIST") {
157
+ throw err
158
+ }
159
+
160
+ if (!(await stale(lockDir, heartbeatPath, metaPath, opts.staleMs))) {
161
+ return { acquired: false }
162
+ }
163
+
164
+ const breakerPath = lockDir + ".breaker"
165
+ try {
166
+ await mkdir(breakerPath, { mode: 0o700 })
167
+ } catch (claimErr) {
168
+ const errCode = code(claimErr)
169
+ if (errCode === "EEXIST") {
170
+ const breaker = await stats(breakerPath)
171
+ if (breaker && wall() - breaker.mtimeMs > opts.staleMs) {
172
+ await rm(breakerPath, { recursive: true, force: true }).catch(() => undefined)
173
+ }
174
+ return { acquired: false }
175
+ }
176
+
177
+ if (errCode === "ENOENT" || errCode === "ENOTDIR") {
178
+ return { acquired: false }
179
+ }
180
+
181
+ throw claimErr
182
+ }
183
+
184
+ try {
185
+ // Breaker ownership ensures only one contender performs stale cleanup.
186
+ if (!(await stale(lockDir, heartbeatPath, metaPath, opts.staleMs))) {
187
+ return { acquired: false }
188
+ }
189
+
190
+ await rm(lockDir, { recursive: true, force: true })
191
+
192
+ try {
193
+ await mkdir(lockDir, { mode: 0o700 })
194
+ } catch (retryErr) {
195
+ const errCode = code(retryErr)
196
+ if (errCode === "EEXIST" || errCode === "ENOTEMPTY") {
197
+ return { acquired: false }
198
+ }
199
+ throw retryErr
200
+ }
201
+ } finally {
202
+ await rm(breakerPath, { recursive: true, force: true }).catch(() => undefined)
203
+ }
204
+ }
205
+
206
+ const meta = {
207
+ token,
208
+ pid: process.pid,
209
+ hostname: os.hostname(),
210
+ createdAt: new Date().toISOString(),
211
+ }
212
+
213
+ await writeFile(heartbeatPath, "", { flag: "wx" }).catch(async () => {
214
+ await rm(lockDir, { recursive: true, force: true })
215
+ throw new Error("Lock acquired but heartbeat already existed (possible compromise).")
216
+ })
217
+
218
+ await writeFile(metaPath, JSON.stringify(meta, null, 2), { flag: "wx" }).catch(async () => {
219
+ await rm(lockDir, { recursive: true, force: true })
220
+ throw new Error("Lock acquired but meta.json already existed (possible compromise).")
221
+ })
222
+
223
+ let timer: NodeJS.Timeout | undefined
224
+
225
+ const startHeartbeat = (intervalMs = Math.max(100, Math.floor(opts.staleMs / 3))) => {
226
+ if (timer) return
227
+ // Heartbeat prevents long critical sections from being evicted as stale.
228
+ timer = setInterval(() => {
229
+ const t = new Date()
230
+ void utimes(heartbeatPath, t, t).catch(() => undefined)
231
+ }, intervalMs)
232
+ timer.unref?.()
233
+ }
234
+
235
+ const release = async () => {
236
+ if (timer) {
237
+ clearInterval(timer)
238
+ timer = undefined
239
+ }
240
+
241
+ const current = await readFile(metaPath, "utf8")
242
+ .then((raw) => {
243
+ const parsed = JSON.parse(raw)
244
+ if (!parsed || typeof parsed !== "object") return {}
245
+ return {
246
+ token: "token" in parsed && typeof parsed.token === "string" ? parsed.token : undefined,
247
+ }
248
+ })
249
+ .catch((err) => {
250
+ const errCode = code(err)
251
+ if (errCode === "ENOENT" || errCode === "ENOTDIR") {
252
+ throw new Error("Refusing to release: lock is compromised (metadata missing).")
253
+ }
254
+ if (err instanceof SyntaxError) {
255
+ throw new Error("Refusing to release: lock is compromised (metadata invalid).")
256
+ }
257
+ throw err
258
+ })
259
+ // Token check prevents deleting a lock that was re-acquired by another process.
260
+ if (current.token !== token) {
261
+ throw new Error("Refusing to release: lock token mismatch (not the owner).")
262
+ }
263
+
264
+ await rm(lockDir, { recursive: true, force: true })
265
+ }
266
+
267
+ return {
268
+ acquired: true,
269
+ startHeartbeat,
270
+ release,
271
+ }
272
+ }
273
+
274
+ async function acquireLockDir(
275
+ lockDir: string,
276
+ input: { key: string; onWait?: Wait; signal?: AbortSignal },
277
+ opts: Opts,
278
+ ) {
279
+ const stop = mono() + opts.timeoutMs
280
+ let attempt = 0
281
+ let waited = 0
282
+ let delay = opts.baseDelayMs
283
+
284
+ while (true) {
285
+ input.signal?.throwIfAborted()
286
+
287
+ const res = await tryAcquireLockDir(lockDir, opts)
288
+ if (res.acquired) {
289
+ return res
290
+ }
291
+
292
+ if (mono() > stop) {
293
+ throw new Error(`Timed out waiting for lock: ${input.key}`)
294
+ }
295
+
296
+ attempt += 1
297
+ const ms = jitter(delay)
298
+ await input.onWait?.({
299
+ key: input.key,
300
+ attempt,
301
+ delay: ms,
302
+ waited,
303
+ })
304
+ await sleep(ms, input.signal)
305
+ waited += ms
306
+ delay = Math.min(opts.maxDelayMs, Math.floor(delay * 1.7))
307
+ }
308
+ }
309
+
310
+ export async function acquire(key: string, input: Options = {}): Promise<Lease> {
311
+ input.signal?.throwIfAborted()
312
+ const cfg: Opts = {
313
+ staleMs: input.staleMs ?? defaultOpts.staleMs,
314
+ timeoutMs: input.timeoutMs ?? defaultOpts.timeoutMs,
315
+ baseDelayMs: input.baseDelayMs ?? defaultOpts.baseDelayMs,
316
+ maxDelayMs: input.maxDelayMs ?? defaultOpts.maxDelayMs,
317
+ }
318
+ const dir = input.dir ?? root()
319
+
320
+ await mkdir(dir, { recursive: true })
321
+ const lockfile = path.join(dir, Hash.fast(key) + ".lock")
322
+ const lock = await acquireLockDir(
323
+ lockfile,
324
+ {
325
+ key,
326
+ onWait: input.onWait,
327
+ signal: input.signal,
328
+ },
329
+ cfg,
330
+ )
331
+ lock.startHeartbeat()
332
+
333
+ const release = () => lock.release()
334
+ return {
335
+ release,
336
+ [Symbol.asyncDispose]() {
337
+ return release()
338
+ },
339
+ }
340
+ }
341
+
342
+ export async function withLock<T>(key: string, fn: () => Promise<T>, input: Options = {}) {
343
+ await using _ = await acquire(key, input)
344
+ input.signal?.throwIfAborted()
345
+ return await fn()
346
+ }
347
+
348
+ export const effect = Effect.fn("Flock.effect")(function* (key: string, input: Options = {}) {
349
+ return yield* Effect.acquireRelease(
350
+ Effect.promise((signal) => Flock.acquire(key, { ...input, signal })).pipe(
351
+ Effect.withSpan("Flock.acquire", {
352
+ attributes: { key },
353
+ }),
354
+ ),
355
+ (lock) => Effect.promise(() => lock.release()).pipe(Effect.withSpan("Flock.release")),
356
+ ).pipe(Effect.asVoid)
357
+ })
358
+ }
@@ -0,0 +1,34 @@
1
+ import { glob, globSync, type GlobOptions } from "glob"
2
+ import { minimatch } from "minimatch"
3
+
4
+ export namespace Glob {
5
+ export interface Options {
6
+ cwd?: string
7
+ absolute?: boolean
8
+ include?: "file" | "all"
9
+ dot?: boolean
10
+ symlink?: boolean
11
+ }
12
+
13
+ function toGlobOptions(options: Options): GlobOptions {
14
+ return {
15
+ cwd: options.cwd,
16
+ absolute: options.absolute,
17
+ dot: options.dot,
18
+ follow: options.symlink ?? false,
19
+ nodir: options.include !== "all",
20
+ }
21
+ }
22
+
23
+ export async function scan(pattern: string, options: Options = {}): Promise<string[]> {
24
+ return glob(pattern, toGlobOptions(options)) as Promise<string[]>
25
+ }
26
+
27
+ export function scanSync(pattern: string, options: Options = {}): string[] {
28
+ return globSync(pattern, toGlobOptions(options)) as string[]
29
+ }
30
+
31
+ export function match(pattern: string, filepath: string): boolean {
32
+ return minimatch(filepath, pattern, { dot: true })
33
+ }
34
+ }
@@ -0,0 +1,7 @@
1
+ import { createHash } from "crypto"
2
+
3
+ export namespace Hash {
4
+ export function fast(input: string | Buffer): string {
5
+ return createHash("sha1").update(input).digest("hex")
6
+ }
7
+ }
@@ -0,0 +1,48 @@
1
+ import { randomBytes } from "crypto"
2
+
3
+ export namespace Identifier {
4
+ const LENGTH = 26
5
+
6
+ // State for monotonic ID generation
7
+ let lastTimestamp = 0
8
+ let counter = 0
9
+
10
+ export function ascending() {
11
+ return create(false)
12
+ }
13
+
14
+ export function descending() {
15
+ return create(true)
16
+ }
17
+
18
+ function randomBase62(length: number): string {
19
+ const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
20
+ let result = ""
21
+ const bytes = randomBytes(length)
22
+ for (let i = 0; i < length; i++) {
23
+ result += chars[bytes[i] % 62]
24
+ }
25
+ return result
26
+ }
27
+
28
+ export function create(descending: boolean, timestamp?: number): string {
29
+ const currentTimestamp = timestamp ?? Date.now()
30
+
31
+ if (currentTimestamp !== lastTimestamp) {
32
+ lastTimestamp = currentTimestamp
33
+ counter = 0
34
+ }
35
+ counter++
36
+
37
+ let now = BigInt(currentTimestamp) * BigInt(0x1000) + BigInt(counter)
38
+
39
+ now = descending ? ~now : now
40
+
41
+ const timeBytes = Buffer.alloc(6)
42
+ for (let i = 0; i < 6; i++) {
43
+ timeBytes[i] = Number((now >> BigInt(40 - 8 * i)) & BigInt(0xff))
44
+ }
45
+
46
+ return timeBytes.toString("hex") + randomBase62(LENGTH - 12)
47
+ }
48
+ }
@@ -0,0 +1,3 @@
1
+ export function iife<T>(fn: () => T) {
2
+ return fn()
3
+ }
@@ -0,0 +1,11 @@
1
+ export function lazy<T>(fn: () => T) {
2
+ let value: T | undefined
3
+ let loaded = false
4
+
5
+ return (): T => {
6
+ if (loaded) return value as T
7
+ loaded = true
8
+ value = fn()
9
+ return value as T
10
+ }
11
+ }
@@ -0,0 +1,208 @@
1
+ import path from "path"
2
+ import { existsSync, writeFileSync } from "fs"
3
+ import fs from "fs/promises"
4
+ import * as Global from "../global"
5
+ import z from "zod"
6
+ import { Glob } from "./glob"
7
+ import { createStream } from "rotating-file-stream"
8
+
9
+ export const Level = z.enum(["DEBUG", "INFO", "WARN", "ERROR"]).meta({ ref: "LogLevel", description: "Log level" })
10
+ export type Level = z.infer<typeof Level>
11
+
12
+ const levelPriority: Record<Level, number> = {
13
+ DEBUG: 0,
14
+ INFO: 1,
15
+ WARN: 2,
16
+ ERROR: 3,
17
+ }
18
+ const keep = 10
19
+
20
+ let level: Level = "INFO"
21
+
22
+ function shouldLog(input: Level): boolean {
23
+ return levelPriority[input] >= levelPriority[level]
24
+ }
25
+
26
+ export type Logger = {
27
+ debug(message?: any, extra?: Record<string, any>): void
28
+ info(message?: any, extra?: Record<string, any>): void
29
+ error(message?: any, extra?: Record<string, any>): void
30
+ warn(message?: any, extra?: Record<string, any>): void
31
+ tag(key: string, value: string): Logger
32
+ clone(): Logger
33
+ time(
34
+ message: string,
35
+ extra?: Record<string, any>,
36
+ ): {
37
+ stop(): void
38
+ [Symbol.dispose](): void
39
+ }
40
+ }
41
+
42
+ const loggers = new Map<string, Logger>()
43
+
44
+ export const Default = create({ service: "default" })
45
+
46
+ export interface Options {
47
+ print: boolean
48
+ dev?: boolean
49
+ level?: Level
50
+ }
51
+
52
+ let logpath = ""
53
+ export function file() {
54
+ return logpath
55
+ }
56
+ let write = (msg: any) => {
57
+ process.stderr.write(msg)
58
+ return msg.length
59
+ }
60
+
61
+ export async function init(options: Options) {
62
+ if (options.level) level = options.level
63
+ void cleanup(Global.Path.log)
64
+ if (options.print) return
65
+ logpath = path.join(
66
+ Global.Path.log,
67
+ options.dev ? "dev.log" : new Date().toISOString().split(".")[0].replace(/:/g, "") + ".log",
68
+ )
69
+ await fs.truncate(logpath).catch(() => {})
70
+ const dir = path.dirname(logpath)
71
+ const stream = createStream(path.basename(logpath), {
72
+ size: "50M",
73
+ maxFiles: 10,
74
+ history: ".log-history",
75
+ path: dir,
76
+ })
77
+ stream.on("rotation", () => {
78
+ if (!existsSync(dir)) return
79
+
80
+ try {
81
+ // RATIONALE: If current log path was deleted while stream still holds the fd,
82
+ // rotating-file-stream will try to rename a missing path and emit ENOENT.
83
+ writeFileSync(logpath, "", { flag: "wx" })
84
+ } catch (err) {
85
+ if (typeof err === "object" && err && "code" in err && err.code === "EEXIST") return
86
+
87
+ const msg = err instanceof Error ? err.message : String(err)
88
+ process.stderr.write("log stream warning: " + msg + "\n")
89
+ }
90
+ })
91
+ stream.on("error", (err: Error) => {
92
+ process.stderr.write("log stream error: " + err.message + "\n")
93
+ })
94
+ stream.on("warning", (err: Error) => {
95
+ process.stderr.write("log stream warning: " + err.message + "\n")
96
+ })
97
+ write = (msg: any) => {
98
+ stream.write(msg)
99
+ return msg.length
100
+ }
101
+ }
102
+
103
+ async function cleanup(dir: string) {
104
+ const files = (
105
+ await Glob.scan("????-??-??T??????.log", {
106
+ cwd: dir,
107
+ absolute: false,
108
+ include: "file",
109
+ }).catch(() => [])
110
+ )
111
+ .filter((file) => path.basename(file) === file)
112
+ .sort()
113
+ if (files.length <= keep) return
114
+
115
+ const doomed = files.slice(0, -keep)
116
+ await Promise.all(doomed.map((file) => fs.unlink(path.join(dir, file)).catch(() => {})))
117
+ }
118
+
119
+ function formatError(error: Error, depth = 0): string {
120
+ const result = error.message
121
+ return error.cause instanceof Error && depth < 10
122
+ ? result + " Caused by: " + formatError(error.cause, depth + 1)
123
+ : result
124
+ }
125
+
126
+ let last = Date.now()
127
+ export function create(tags?: Record<string, any>) {
128
+ tags = tags || {}
129
+
130
+ const service = tags["service"]
131
+ if (service && typeof service === "string") {
132
+ const cached = loggers.get(service)
133
+ if (cached) {
134
+ return cached
135
+ }
136
+ }
137
+
138
+ function build(message: any, extra?: Record<string, any>) {
139
+ const prefix = Object.entries({
140
+ ...tags,
141
+ ...extra,
142
+ })
143
+ .filter(([_, value]) => value !== undefined && value !== null)
144
+ .map(([key, value]) => {
145
+ const prefix = `${key}=`
146
+ if (value instanceof Error) return prefix + formatError(value)
147
+ if (typeof value === "object") return prefix + JSON.stringify(value)
148
+ return prefix + value
149
+ })
150
+ .join(" ")
151
+ const next = new Date()
152
+ const diff = next.getTime() - last
153
+ last = next.getTime()
154
+ return [next.toISOString().split(".")[0], "+" + diff + "ms", prefix, message].filter(Boolean).join(" ") + "\n"
155
+ }
156
+ const result: Logger = {
157
+ debug(message?: any, extra?: Record<string, any>) {
158
+ if (shouldLog("DEBUG")) {
159
+ write("DEBUG " + build(message, extra))
160
+ }
161
+ },
162
+ info(message?: any, extra?: Record<string, any>) {
163
+ if (shouldLog("INFO")) {
164
+ write("INFO " + build(message, extra))
165
+ }
166
+ },
167
+ error(message?: any, extra?: Record<string, any>) {
168
+ if (shouldLog("ERROR")) {
169
+ write("ERROR " + build(message, extra))
170
+ }
171
+ },
172
+ warn(message?: any, extra?: Record<string, any>) {
173
+ if (shouldLog("WARN")) {
174
+ write("WARN " + build(message, extra))
175
+ }
176
+ },
177
+ tag(key: string, value: string) {
178
+ if (tags) tags[key] = value
179
+ return result
180
+ },
181
+ clone() {
182
+ return create({ ...tags })
183
+ },
184
+ time(message: string, extra?: Record<string, any>) {
185
+ const now = Date.now()
186
+ result.info(message, { status: "started", ...extra })
187
+ function stop() {
188
+ result.info(message, {
189
+ status: "completed",
190
+ duration: Date.now() - now,
191
+ ...extra,
192
+ })
193
+ }
194
+ return {
195
+ stop,
196
+ [Symbol.dispose]() {
197
+ stop()
198
+ },
199
+ }
200
+ },
201
+ }
202
+
203
+ if (service && typeof service === "string") {
204
+ loggers.set(service, result)
205
+ }
206
+
207
+ return result
208
+ }
@@ -0,0 +1,10 @@
1
+ import { createRequire } from "node:module"
2
+ import path from "node:path"
3
+
4
+ export namespace Module {
5
+ export function resolve(id: string, dir: string) {
6
+ try {
7
+ return createRequire(path.join(dir, "package.json")).resolve(id)
8
+ } catch {}
9
+ }
10
+ }