@opencode-ai/util 0.0.0-bootstrap.0 → 0.0.0-next-15994

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 (50) hide show
  1. package/dist/cross-spawn-spawner.d.ts +3 -0
  2. package/dist/cross-spawn-spawner.js +403 -0
  3. package/dist/effect/app-node-platform.d.ts +6 -0
  4. package/dist/effect/app-node-platform.js +8 -0
  5. package/dist/effect/app-node.d.ts +50 -0
  6. package/dist/effect/app-node.js +8 -0
  7. package/dist/effect/layer-node.d.ts +79 -0
  8. package/dist/effect/layer-node.js +181 -0
  9. package/dist/effect/memo-map.d.ts +2 -0
  10. package/dist/effect/memo-map.js +2 -0
  11. package/dist/effect/runtime.d.ts +8 -0
  12. package/dist/effect/runtime.js +16 -0
  13. package/dist/effect/service-use.d.ts +7 -0
  14. package/dist/effect/service-use.js +27 -0
  15. package/dist/effect-flock.d.ts +31 -0
  16. package/dist/effect-flock.js +185 -0
  17. package/dist/flock.d.ts +30 -0
  18. package/dist/flock.js +273 -0
  19. package/dist/fs-util.d.ts +139 -0
  20. package/dist/fs-util.js +224 -0
  21. package/dist/glob.d.ts +12 -0
  22. package/dist/glob.js +26 -0
  23. package/dist/global.d.ts +30 -0
  24. package/dist/global.js +57 -0
  25. package/dist/hash.d.ts +4 -0
  26. package/dist/hash.js +12 -0
  27. package/dist/npm-config.d.ts +4 -0
  28. package/dist/npm-config.js +32 -0
  29. package/dist/npm.d.ts +35 -0
  30. package/dist/npm.js +207 -0
  31. package/dist/observability/logging.d.ts +6 -0
  32. package/dist/observability/logging.js +67 -0
  33. package/dist/observability/otlp.d.ts +18 -0
  34. package/dist/observability/otlp.js +73 -0
  35. package/dist/observability/shared.d.ts +1 -0
  36. package/dist/observability/shared.js +1 -0
  37. package/dist/observability.d.ts +13 -0
  38. package/dist/observability.js +31 -0
  39. package/dist/patch.d.ts +42 -0
  40. package/dist/patch.js +206 -0
  41. package/dist/process.d.ts +54 -0
  42. package/dist/process.js +162 -0
  43. package/dist/runtime/import.bun.d.ts +2 -0
  44. package/dist/runtime/import.bun.js +6 -0
  45. package/dist/runtime/import.node.d.ts +2 -0
  46. package/dist/runtime/import.node.js +36 -0
  47. package/dist/runtime-import.d.ts +1 -0
  48. package/dist/runtime-import.js +1 -0
  49. package/package.json +56 -7
  50. package/index.js +0 -1
package/dist/flock.js ADDED
@@ -0,0 +1,273 @@
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.js";
6
+ import { Effect } from "effect";
7
+ export var Flock;
8
+ (function (Flock) {
9
+ let global;
10
+ function setGlobal(g) {
11
+ global = g;
12
+ }
13
+ Flock.setGlobal = setGlobal;
14
+ const root = () => {
15
+ if (!global)
16
+ throw new Error("Flock global not set");
17
+ return path.join(global.state, "locks");
18
+ };
19
+ // Defaults for callers that do not provide timing options.
20
+ const defaultOpts = {
21
+ staleMs: 60_000,
22
+ timeoutMs: 5 * 60_000,
23
+ baseDelayMs: 100,
24
+ maxDelayMs: 2_000,
25
+ };
26
+ function code(err) {
27
+ if (typeof err !== "object" || err === null || !("code" in err))
28
+ return;
29
+ const value = err.code;
30
+ if (typeof value !== "string")
31
+ return;
32
+ return value;
33
+ }
34
+ function sleep(ms, signal) {
35
+ return new Promise((resolve, reject) => {
36
+ if (signal?.aborted) {
37
+ reject(signal.reason ?? new Error("Aborted"));
38
+ return;
39
+ }
40
+ let timer;
41
+ const done = () => {
42
+ signal?.removeEventListener("abort", abort);
43
+ resolve();
44
+ };
45
+ const abort = () => {
46
+ if (timer) {
47
+ clearTimeout(timer);
48
+ }
49
+ signal?.removeEventListener("abort", abort);
50
+ reject(signal?.reason ?? new Error("Aborted"));
51
+ };
52
+ signal?.addEventListener("abort", abort, { once: true });
53
+ timer = setTimeout(done, ms);
54
+ });
55
+ }
56
+ function jitter(ms) {
57
+ const j = Math.floor(ms * 0.3);
58
+ const d = Math.floor(Math.random() * (2 * j + 1)) - j;
59
+ return Math.max(0, ms + d);
60
+ }
61
+ function mono() {
62
+ return performance.now();
63
+ }
64
+ function wall() {
65
+ return performance.timeOrigin + mono();
66
+ }
67
+ async function stats(file) {
68
+ try {
69
+ return await stat(file);
70
+ }
71
+ catch (err) {
72
+ const errCode = code(err);
73
+ if (errCode === "ENOENT" || errCode === "ENOTDIR")
74
+ return;
75
+ throw err;
76
+ }
77
+ }
78
+ async function stale(lockDir, heartbeatPath, metaPath, staleMs) {
79
+ // Stale detection allows automatic recovery after crashed owners.
80
+ const now = wall();
81
+ const heartbeat = await stats(heartbeatPath);
82
+ if (heartbeat) {
83
+ return now - heartbeat.mtimeMs > staleMs;
84
+ }
85
+ const meta = await stats(metaPath);
86
+ if (meta) {
87
+ return now - meta.mtimeMs > staleMs;
88
+ }
89
+ const dir = await stats(lockDir);
90
+ if (!dir) {
91
+ return false;
92
+ }
93
+ return now - dir.mtimeMs > staleMs;
94
+ }
95
+ async function tryAcquireLockDir(lockDir, opts) {
96
+ const token = randomUUID?.() ?? randomBytes(16).toString("hex");
97
+ const metaPath = path.join(lockDir, "meta.json");
98
+ const heartbeatPath = path.join(lockDir, "heartbeat");
99
+ try {
100
+ await mkdir(lockDir, { mode: 0o700 });
101
+ }
102
+ catch (err) {
103
+ if (code(err) !== "EEXIST") {
104
+ throw err;
105
+ }
106
+ if (!(await stale(lockDir, heartbeatPath, metaPath, opts.staleMs))) {
107
+ return { acquired: false };
108
+ }
109
+ const breakerPath = lockDir + ".breaker";
110
+ try {
111
+ await mkdir(breakerPath, { mode: 0o700 });
112
+ }
113
+ catch (claimErr) {
114
+ const errCode = code(claimErr);
115
+ if (errCode === "EEXIST") {
116
+ const breaker = await stats(breakerPath);
117
+ if (breaker && wall() - breaker.mtimeMs > opts.staleMs) {
118
+ await rm(breakerPath, { recursive: true, force: true }).catch(() => undefined);
119
+ }
120
+ return { acquired: false };
121
+ }
122
+ if (errCode === "ENOENT" || errCode === "ENOTDIR") {
123
+ return { acquired: false };
124
+ }
125
+ throw claimErr;
126
+ }
127
+ try {
128
+ // Breaker ownership ensures only one contender performs stale cleanup.
129
+ if (!(await stale(lockDir, heartbeatPath, metaPath, opts.staleMs))) {
130
+ return { acquired: false };
131
+ }
132
+ await rm(lockDir, { recursive: true, force: true });
133
+ try {
134
+ await mkdir(lockDir, { mode: 0o700 });
135
+ }
136
+ catch (retryErr) {
137
+ const errCode = code(retryErr);
138
+ if (errCode === "EEXIST" || errCode === "ENOTEMPTY") {
139
+ return { acquired: false };
140
+ }
141
+ throw retryErr;
142
+ }
143
+ }
144
+ finally {
145
+ await rm(breakerPath, { recursive: true, force: true }).catch(() => undefined);
146
+ }
147
+ }
148
+ const meta = {
149
+ token,
150
+ pid: process.pid,
151
+ hostname: os.hostname(),
152
+ createdAt: new Date().toISOString(),
153
+ };
154
+ await writeFile(heartbeatPath, "", { flag: "wx" }).catch(async () => {
155
+ await rm(lockDir, { recursive: true, force: true });
156
+ throw new Error("Lock acquired but heartbeat already existed (possible compromise).");
157
+ });
158
+ await writeFile(metaPath, JSON.stringify(meta, null, 2), { flag: "wx" }).catch(async () => {
159
+ await rm(lockDir, { recursive: true, force: true });
160
+ throw new Error("Lock acquired but meta.json already existed (possible compromise).");
161
+ });
162
+ let timer;
163
+ const startHeartbeat = (intervalMs = Math.max(100, Math.floor(opts.staleMs / 3))) => {
164
+ if (timer)
165
+ return;
166
+ // Heartbeat prevents long critical sections from being evicted as stale.
167
+ timer = setInterval(() => {
168
+ const t = new Date();
169
+ void utimes(heartbeatPath, t, t).catch(() => undefined);
170
+ }, intervalMs);
171
+ timer.unref?.();
172
+ };
173
+ const release = async () => {
174
+ if (timer) {
175
+ clearInterval(timer);
176
+ timer = undefined;
177
+ }
178
+ const current = await readFile(metaPath, "utf8")
179
+ .then((raw) => {
180
+ const parsed = JSON.parse(raw);
181
+ if (!parsed || typeof parsed !== "object")
182
+ return {};
183
+ return {
184
+ token: "token" in parsed && typeof parsed.token === "string" ? parsed.token : undefined,
185
+ };
186
+ })
187
+ .catch((err) => {
188
+ const errCode = code(err);
189
+ if (errCode === "ENOENT" || errCode === "ENOTDIR") {
190
+ throw new Error("Refusing to release: lock is compromised (metadata missing).");
191
+ }
192
+ if (err instanceof SyntaxError) {
193
+ throw new Error("Refusing to release: lock is compromised (metadata invalid).");
194
+ }
195
+ throw err;
196
+ });
197
+ // Token check prevents deleting a lock that was re-acquired by another process.
198
+ if (current.token !== token) {
199
+ throw new Error("Refusing to release: lock token mismatch (not the owner).");
200
+ }
201
+ await rm(lockDir, { recursive: true, force: true });
202
+ };
203
+ return {
204
+ acquired: true,
205
+ startHeartbeat,
206
+ release,
207
+ };
208
+ }
209
+ async function acquireLockDir(lockDir, input, opts) {
210
+ const stop = mono() + opts.timeoutMs;
211
+ let attempt = 0;
212
+ let waited = 0;
213
+ let delay = opts.baseDelayMs;
214
+ while (true) {
215
+ input.signal?.throwIfAborted();
216
+ const res = await tryAcquireLockDir(lockDir, opts);
217
+ if (res.acquired) {
218
+ return res;
219
+ }
220
+ if (mono() > stop) {
221
+ throw new Error(`Timed out waiting for lock: ${input.key}`);
222
+ }
223
+ attempt += 1;
224
+ const ms = jitter(delay);
225
+ await input.onWait?.({
226
+ key: input.key,
227
+ attempt,
228
+ delay: ms,
229
+ waited,
230
+ });
231
+ await sleep(ms, input.signal);
232
+ waited += ms;
233
+ delay = Math.min(opts.maxDelayMs, Math.floor(delay * 1.7));
234
+ }
235
+ }
236
+ async function acquire(key, input = {}) {
237
+ input.signal?.throwIfAborted();
238
+ const cfg = {
239
+ staleMs: input.staleMs ?? defaultOpts.staleMs,
240
+ timeoutMs: input.timeoutMs ?? defaultOpts.timeoutMs,
241
+ baseDelayMs: input.baseDelayMs ?? defaultOpts.baseDelayMs,
242
+ maxDelayMs: input.maxDelayMs ?? defaultOpts.maxDelayMs,
243
+ };
244
+ const dir = input.dir ?? root();
245
+ await mkdir(dir, { recursive: true });
246
+ const lockfile = path.join(dir, Hash.fast(key) + ".lock");
247
+ const lock = await acquireLockDir(lockfile, {
248
+ key,
249
+ onWait: input.onWait,
250
+ signal: input.signal,
251
+ }, cfg);
252
+ lock.startHeartbeat();
253
+ const release = () => lock.release();
254
+ return {
255
+ release,
256
+ [Symbol.asyncDispose]() {
257
+ return release();
258
+ },
259
+ };
260
+ }
261
+ Flock.acquire = acquire;
262
+ async function withLock(key, fn, input = {}) {
263
+ await using _ = await acquire(key, input);
264
+ input.signal?.throwIfAborted();
265
+ return await fn();
266
+ }
267
+ Flock.withLock = withLock;
268
+ Flock.effect = Effect.fn("Flock.effect")(function* (key, input = {}) {
269
+ return yield* Effect.acquireRelease(Effect.promise((signal) => Flock.acquire(key, { ...input, signal })).pipe(Effect.withSpan("Flock.acquire", {
270
+ attributes: { key },
271
+ })), (lock) => Effect.promise(() => lock.release()).pipe(Effect.withSpan("Flock.release"))).pipe(Effect.asVoid);
272
+ });
273
+ })(Flock || (Flock = {}));
@@ -0,0 +1,139 @@
1
+ import { Context, Effect, FileSystem, Layer, Schema } from "effect";
2
+ import type { PlatformError } from "effect/PlatformError";
3
+ import { Glob } from "./glob.js";
4
+ export declare namespace FSUtil {
5
+ const FileSystemError_base: Schema.Class<FileSystemError, Schema.TaggedStruct<"FileSystemError", {
6
+ readonly method: Schema.String;
7
+ readonly cause: Schema.optional<Schema.Defect>;
8
+ }>, import("effect/Cause").YieldableError>;
9
+ export class FileSystemError extends FileSystemError_base {
10
+ get message(): string;
11
+ }
12
+ export type Error = PlatformError | FileSystemError;
13
+ export interface DirEntry {
14
+ readonly name: string;
15
+ readonly type: "file" | "directory" | "symlink" | "other";
16
+ }
17
+ export interface Interface extends FileSystem.FileSystem {
18
+ readonly isDir: (path: string) => Effect.Effect<boolean>;
19
+ readonly isFile: (path: string) => Effect.Effect<boolean>;
20
+ readonly existsSafe: (path: string) => Effect.Effect<boolean>;
21
+ readonly readFileStringSafe: (path: string) => Effect.Effect<string | undefined, Error>;
22
+ readonly readJson: (path: string) => Effect.Effect<unknown, Error>;
23
+ readonly writeJson: (path: string, data: unknown, mode?: number) => Effect.Effect<void, Error>;
24
+ readonly ensureDir: (path: string) => Effect.Effect<void, Error>;
25
+ readonly writeWithDirs: (path: string, content: string | Uint8Array, mode?: number) => Effect.Effect<void, Error>;
26
+ readonly readDirectoryEntries: (path: string) => Effect.Effect<DirEntry[], Error>;
27
+ readonly resolve: (path: string) => Effect.Effect<string>;
28
+ readonly findUp: (target: string, start: string, stop?: string) => Effect.Effect<string[], Error>;
29
+ readonly up: (options: {
30
+ targets: string[];
31
+ start: string;
32
+ stop?: string;
33
+ }) => Effect.Effect<string[], Error>;
34
+ readonly globUp: (pattern: string, start: string, stop?: string) => Effect.Effect<string[], Error>;
35
+ readonly scan: (pattern: string, options?: Glob.Options) => Effect.Effect<string[], Error>;
36
+ readonly globMatch: (pattern: string, filepath: string) => boolean;
37
+ }
38
+ const Service_base: Context.ServiceClass<Service, "@opencode/FileSystem", Interface>;
39
+ export class Service extends Service_base {
40
+ }
41
+ export const use: {
42
+ readonly isDir: (path: string) => Effect.Effect<boolean, never, Service>;
43
+ readonly isFile: (path: string) => Effect.Effect<boolean, never, Service>;
44
+ readonly existsSafe: (path: string) => Effect.Effect<boolean, never, Service>;
45
+ readonly readFileStringSafe: (path: string) => Effect.Effect<string | undefined, Error, Service>;
46
+ readonly readJson: (path: string) => Effect.Effect<unknown, Error, Service>;
47
+ readonly writeJson: (path: string, data: unknown, mode?: number | undefined) => Effect.Effect<void, Error, Service>;
48
+ readonly ensureDir: (path: string) => Effect.Effect<void, Error, Service>;
49
+ readonly writeWithDirs: (path: string, content: string | Uint8Array<ArrayBufferLike>, mode?: number | undefined) => Effect.Effect<void, Error, Service>;
50
+ readonly readDirectoryEntries: (path: string) => Effect.Effect<DirEntry[], Error, Service>;
51
+ readonly resolve: (path: string) => Effect.Effect<string, never, Service>;
52
+ readonly findUp: (target: string, start: string, stop?: string | undefined) => Effect.Effect<string[], Error, Service>;
53
+ readonly up: (options: {
54
+ targets: string[];
55
+ start: string;
56
+ stop?: string;
57
+ }) => Effect.Effect<string[], Error, Service>;
58
+ readonly globUp: (pattern: string, start: string, stop?: string | undefined) => Effect.Effect<string[], Error, Service>;
59
+ readonly scan: (pattern: string, options?: Glob.Options | undefined) => Effect.Effect<string[], Error, Service>;
60
+ readonly access: (path: string, options?: {
61
+ readonly ok?: boolean | undefined;
62
+ readonly readable?: boolean | undefined;
63
+ readonly writable?: boolean | undefined;
64
+ } | undefined) => Effect.Effect<void, PlatformError, Service>;
65
+ readonly copy: (fromPath: string, toPath: string, options?: {
66
+ readonly overwrite?: boolean | undefined;
67
+ readonly preserveTimestamps?: boolean | undefined;
68
+ } | undefined) => Effect.Effect<void, PlatformError, Service>;
69
+ readonly copyFile: (fromPath: string, toPath: string) => Effect.Effect<void, PlatformError, Service>;
70
+ readonly chmod: (path: string, mode: number) => Effect.Effect<void, PlatformError, Service>;
71
+ readonly chown: (path: string, uid: number, gid: number) => Effect.Effect<void, PlatformError, Service>;
72
+ readonly glob: (pattern: string, options?: {
73
+ readonly root?: string | undefined;
74
+ readonly exclude?: ReadonlyArray<string> | undefined;
75
+ } | undefined) => Effect.Effect<string[], PlatformError, Service>;
76
+ readonly exists: (path: string) => Effect.Effect<boolean, PlatformError, Service>;
77
+ readonly link: (fromPath: string, toPath: string) => Effect.Effect<void, PlatformError, Service>;
78
+ readonly makeDirectory: (path: string, options?: {
79
+ readonly recursive?: boolean | undefined;
80
+ readonly mode?: number | undefined;
81
+ } | undefined) => Effect.Effect<void, PlatformError, Service>;
82
+ readonly makeTempDirectory: (options?: {
83
+ readonly directory?: string | undefined;
84
+ readonly prefix?: string | undefined;
85
+ } | undefined) => Effect.Effect<string, PlatformError, Service>;
86
+ readonly makeTempDirectoryScoped: (options?: {
87
+ readonly directory?: string | undefined;
88
+ readonly prefix?: string | undefined;
89
+ } | undefined) => Effect.Effect<string, PlatformError, import("effect/Scope").Scope | Service>;
90
+ readonly makeTempFile: (options?: {
91
+ readonly directory?: string | undefined;
92
+ readonly prefix?: string | undefined;
93
+ readonly suffix?: string | undefined;
94
+ } | undefined) => Effect.Effect<string, PlatformError, Service>;
95
+ readonly makeTempFileScoped: (options?: {
96
+ readonly directory?: string | undefined;
97
+ readonly prefix?: string | undefined;
98
+ readonly suffix?: string | undefined;
99
+ } | undefined) => Effect.Effect<string, PlatformError, import("effect/Scope").Scope | Service>;
100
+ readonly open: (path: string, options?: {
101
+ readonly flag?: FileSystem.OpenFlag | undefined;
102
+ readonly mode?: number | undefined;
103
+ } | undefined) => Effect.Effect<FileSystem.File, PlatformError, import("effect/Scope").Scope | Service>;
104
+ readonly readDirectory: (path: string, options?: {
105
+ readonly recursive?: boolean | undefined;
106
+ } | undefined) => Effect.Effect<string[], PlatformError, Service>;
107
+ readonly readFile: (path: string) => Effect.Effect<Uint8Array<ArrayBufferLike>, PlatformError, Service>;
108
+ readonly readFileString: (path: string, encoding?: string | undefined) => Effect.Effect<string, PlatformError, Service>;
109
+ readonly readLink: (path: string) => Effect.Effect<string, PlatformError, Service>;
110
+ readonly realPath: (path: string) => Effect.Effect<string, PlatformError, Service>;
111
+ readonly remove: (path: string, options?: {
112
+ readonly recursive?: boolean | undefined;
113
+ readonly force?: boolean | undefined;
114
+ } | undefined) => Effect.Effect<void, PlatformError, Service>;
115
+ readonly rename: (oldPath: string, newPath: string) => Effect.Effect<void, PlatformError, Service>;
116
+ readonly stat: (path: string) => Effect.Effect<FileSystem.File.Info, PlatformError, Service>;
117
+ readonly symlink: (fromPath: string, toPath: string) => Effect.Effect<void, PlatformError, Service>;
118
+ readonly truncate: (path: string, length?: FileSystem.SizeInput | undefined) => Effect.Effect<void, PlatformError, Service>;
119
+ readonly utimes: (path: string, atime: number | Date, mtime: number | Date) => Effect.Effect<void, PlatformError, Service>;
120
+ readonly writeFile: (path: string, data: Uint8Array<ArrayBufferLike>, options?: {
121
+ readonly flag?: FileSystem.OpenFlag | undefined;
122
+ readonly mode?: number | undefined;
123
+ } | undefined) => Effect.Effect<void, PlatformError, Service>;
124
+ readonly writeFileString: (path: string, data: string, options?: {
125
+ readonly flag?: FileSystem.OpenFlag | undefined;
126
+ readonly mode?: number | undefined;
127
+ } | undefined) => Effect.Effect<void, PlatformError, Service>;
128
+ };
129
+ export const layer: Layer.Layer<Service, never, FileSystem.FileSystem>;
130
+ export const node: import("./effect/layer-node.js").Node<Service, never, import("./effect/layer-node.js").Tag<"global">>;
131
+ export function mimeType(p: string): string;
132
+ export function normalizePath(p: string): string;
133
+ export function normalizePathPattern(p: string): string;
134
+ export function resolve(p: string): string;
135
+ export function windowsPath(p: string): string;
136
+ export function overlaps(a: string, b: string): boolean;
137
+ export function contains(parent: string, child: string): boolean;
138
+ export {};
139
+ }
@@ -0,0 +1,224 @@
1
+ import { NodeFileSystem } from "@effect/platform-node";
2
+ import path, { dirname, isAbsolute, join, relative, sep } from "path";
3
+ import { realpathSync } from "fs";
4
+ import { readdir } from "fs/promises";
5
+ import { lookup } from "mime-types";
6
+ import { Context, Effect, FileSystem, Layer, Schema } from "effect";
7
+ import { Glob } from "./glob.js";
8
+ import { serviceUse } from "./effect/service-use.js";
9
+ import { makeGlobalNode } from "./effect/app-node.js";
10
+ import { filesystem } from "./effect/app-node-platform.js";
11
+ export var FSUtil;
12
+ (function (FSUtil) {
13
+ class FileSystemError extends Schema.TaggedErrorClass()("FileSystemError", {
14
+ method: Schema.String,
15
+ cause: Schema.optional(Schema.Defect()),
16
+ }) {
17
+ get message() {
18
+ const detail = this.cause instanceof Error ? this.cause.message : this.cause && String(this.cause);
19
+ return `Filesystem operation failed: ${this.method}${detail ? `: ${detail}` : ""}`;
20
+ }
21
+ }
22
+ FSUtil.FileSystemError = FileSystemError;
23
+ class Service extends Context.Service()("@opencode/FileSystem") {
24
+ }
25
+ FSUtil.Service = Service;
26
+ FSUtil.use = serviceUse(Service);
27
+ // Exported so simulation can wrap this layer and override the methods that
28
+ // bypass the injected FileSystem (readDirectoryEntries, scan, globUp).
29
+ FSUtil.layer = Layer.effect(Service, Effect.gen(function* () {
30
+ const fs = yield* FileSystem.FileSystem;
31
+ const existsSafe = Effect.fn("FileSystem.existsSafe")(function* (path) {
32
+ return yield* fs.exists(path).pipe(Effect.orElseSucceed(() => false));
33
+ });
34
+ const readFileStringSafe = Effect.fn("FileSystem.readFileStringSafe")(function* (path) {
35
+ return yield* fs.readFileString(path).pipe(Effect.catchReason("PlatformError", "NotFound", () => Effect.succeed(undefined)), Effect.catchReason("PlatformError", "PermissionDenied", () => Effect.succeed(undefined)));
36
+ });
37
+ const isDir = Effect.fn("FileSystem.isDir")(function* (path) {
38
+ const info = yield* fs.stat(path).pipe(Effect.catch(() => Effect.void));
39
+ return info?.type === "Directory";
40
+ });
41
+ const isFile = Effect.fn("FileSystem.isFile")(function* (path) {
42
+ const info = yield* fs.stat(path).pipe(Effect.catch(() => Effect.void));
43
+ return info?.type === "File";
44
+ });
45
+ const readDirectoryEntries = Effect.fn("FileSystem.readDirectoryEntries")(function* (dirPath) {
46
+ return yield* Effect.tryPromise({
47
+ try: async () => {
48
+ const entries = await readdir(dirPath, { withFileTypes: true });
49
+ return entries.map((e) => ({
50
+ name: e.name,
51
+ type: e.isDirectory() ? "directory" : e.isSymbolicLink() ? "symlink" : e.isFile() ? "file" : "other",
52
+ }));
53
+ },
54
+ catch: (cause) => new FileSystemError({ method: "readDirectoryEntries", cause }),
55
+ });
56
+ });
57
+ const resolve = Effect.fn("FileSystem.resolve")(function* (input) {
58
+ const resolved = path.resolve(windowsPath(input));
59
+ return yield* fs.realPath(resolved).pipe(Effect.catchReason("PlatformError", "NotFound", () => Effect.succeed(resolved)), Effect.orDie);
60
+ });
61
+ const readJson = Effect.fn("FileSystem.readJson")(function* (path) {
62
+ const text = yield* fs.readFileString(path);
63
+ return yield* Effect.try({
64
+ try: () => JSON.parse(text),
65
+ catch: (cause) => new FileSystemError({ method: "readJson", cause }),
66
+ });
67
+ });
68
+ const writeJson = Effect.fn("FileSystem.writeJson")(function* (path, data, mode) {
69
+ const content = JSON.stringify(data, null, 2);
70
+ yield* fs.writeFileString(path, content);
71
+ if (mode)
72
+ yield* fs.chmod(path, mode);
73
+ });
74
+ const ensureDir = Effect.fn("FileSystem.ensureDir")(function* (path) {
75
+ yield* fs.makeDirectory(path, { recursive: true }).pipe(
76
+ // Bun on Windows can throw EEXIST here despite recursive mode.
77
+ // https://github.com/oven-sh/bun/issues/21901
78
+ Effect.catchIf((error) => error.reason._tag === "AlreadyExists", (error) => isDir(path).pipe(Effect.flatMap((exists) => (exists ? Effect.void : Effect.fail(error))))));
79
+ });
80
+ const writeWithDirs = Effect.fn("FileSystem.writeWithDirs")(function* (path, content, mode) {
81
+ const write = typeof content === "string" ? fs.writeFileString(path, content) : fs.writeFile(path, content);
82
+ yield* write.pipe(Effect.catchIf((e) => e.reason._tag === "NotFound", () => Effect.gen(function* () {
83
+ yield* fs.makeDirectory(dirname(path), { recursive: true });
84
+ yield* write;
85
+ })));
86
+ if (mode)
87
+ yield* fs.chmod(path, mode);
88
+ });
89
+ const scan = Effect.fn("FileSystem.scan")(function* (pattern, options) {
90
+ return yield* Effect.tryPromise({
91
+ try: () => Glob.scan(pattern, options),
92
+ catch: (cause) => new FileSystemError({ method: "glob", cause }),
93
+ });
94
+ });
95
+ const findUp = Effect.fn("FileSystem.findUp")(function* (target, start, stop) {
96
+ const result = [];
97
+ let current = start;
98
+ while (true) {
99
+ const search = join(current, target);
100
+ if (yield* fs.exists(search))
101
+ result.push(search);
102
+ if (stop === current)
103
+ break;
104
+ const parent = dirname(current);
105
+ if (parent === current)
106
+ break;
107
+ current = parent;
108
+ }
109
+ return result;
110
+ });
111
+ const up = Effect.fn("FileSystem.up")(function* (options) {
112
+ const result = [];
113
+ let current = options.start;
114
+ while (true) {
115
+ for (const target of options.targets) {
116
+ const search = join(current, target);
117
+ if (yield* fs.exists(search))
118
+ result.push(search);
119
+ }
120
+ if (options.stop === current)
121
+ break;
122
+ const parent = dirname(current);
123
+ if (parent === current)
124
+ break;
125
+ current = parent;
126
+ }
127
+ return result;
128
+ });
129
+ const globUp = Effect.fn("FileSystem.globUp")(function* (pattern, start, stop) {
130
+ const result = [];
131
+ let current = start;
132
+ while (true) {
133
+ const matches = yield* scan(pattern, { cwd: current, absolute: true, include: "file", dot: true }).pipe(Effect.catch(() => Effect.succeed([])));
134
+ result.push(...matches);
135
+ if (stop === current)
136
+ break;
137
+ const parent = dirname(current);
138
+ if (parent === current)
139
+ break;
140
+ current = parent;
141
+ }
142
+ return result;
143
+ });
144
+ return Service.of({
145
+ ...fs,
146
+ existsSafe,
147
+ readFileStringSafe,
148
+ isDir,
149
+ isFile,
150
+ readDirectoryEntries,
151
+ resolve,
152
+ readJson,
153
+ writeJson,
154
+ ensureDir,
155
+ writeWithDirs,
156
+ findUp,
157
+ up,
158
+ globUp,
159
+ scan,
160
+ globMatch: Glob.match,
161
+ });
162
+ }));
163
+ FSUtil.node = makeGlobalNode({ service: Service, layer: FSUtil.layer, deps: [filesystem] });
164
+ // Pure helpers that don't need Effect (path manipulation, sync operations)
165
+ function mimeType(p) {
166
+ return lookup(p) || "application/octet-stream";
167
+ }
168
+ FSUtil.mimeType = mimeType;
169
+ function normalizePath(p) {
170
+ if (process.platform !== "win32")
171
+ return p;
172
+ const resolved = path.resolve(windowsPath(p));
173
+ try {
174
+ return realpathSync.native(resolved);
175
+ }
176
+ catch {
177
+ return resolved;
178
+ }
179
+ }
180
+ FSUtil.normalizePath = normalizePath;
181
+ function normalizePathPattern(p) {
182
+ if (process.platform !== "win32")
183
+ return p;
184
+ if (p === "*")
185
+ return p;
186
+ const match = p.match(/^(.*)[\\/]\*$/);
187
+ if (!match)
188
+ return normalizePath(p);
189
+ const dir = /^[A-Za-z]:$/.test(match[1]) ? match[1] + "\\" : match[1];
190
+ return join(normalizePath(dir), "*");
191
+ }
192
+ FSUtil.normalizePathPattern = normalizePathPattern;
193
+ function resolve(p) {
194
+ const resolved = path.resolve(windowsPath(p));
195
+ try {
196
+ return normalizePath(realpathSync(resolved));
197
+ }
198
+ catch (e) {
199
+ if (e?.code === "ENOENT")
200
+ return normalizePath(resolved);
201
+ throw e;
202
+ }
203
+ }
204
+ FSUtil.resolve = resolve;
205
+ function windowsPath(p) {
206
+ if (process.platform !== "win32")
207
+ return p;
208
+ return p
209
+ .replace(/^\/([a-zA-Z]):(?:[\\/]|$)/, (_, drive) => `${drive.toUpperCase()}:/`)
210
+ .replace(/^\/([a-zA-Z])(?:\/|$)/, (_, drive) => `${drive.toUpperCase()}:/`)
211
+ .replace(/^\/cygdrive\/([a-zA-Z])(?:\/|$)/, (_, drive) => `${drive.toUpperCase()}:/`)
212
+ .replace(/^\/mnt\/([a-zA-Z])(?:\/|$)/, (_, drive) => `${drive.toUpperCase()}:/`);
213
+ }
214
+ FSUtil.windowsPath = windowsPath;
215
+ function overlaps(a, b) {
216
+ return contains(a, b) || contains(b, a);
217
+ }
218
+ FSUtil.overlaps = overlaps;
219
+ function contains(parent, child) {
220
+ const result = relative(parent, child);
221
+ return result === "" || (!isAbsolute(result) && result !== ".." && !result.startsWith(`..${sep}`));
222
+ }
223
+ FSUtil.contains = contains;
224
+ })(FSUtil || (FSUtil = {}));
package/dist/glob.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ export declare namespace Glob {
2
+ interface Options {
3
+ cwd?: string;
4
+ absolute?: boolean;
5
+ include?: "file" | "all";
6
+ dot?: boolean;
7
+ symlink?: boolean;
8
+ }
9
+ function scan(pattern: string, options?: Options): Promise<string[]>;
10
+ function scanSync(pattern: string, options?: Options): string[];
11
+ function match(pattern: string, filepath: string): boolean;
12
+ }
package/dist/glob.js ADDED
@@ -0,0 +1,26 @@
1
+ import { glob, globSync } from "glob";
2
+ import { minimatch } from "minimatch";
3
+ export var Glob;
4
+ (function (Glob) {
5
+ function toGlobOptions(options) {
6
+ return {
7
+ cwd: options.cwd,
8
+ absolute: options.absolute,
9
+ dot: options.dot,
10
+ follow: options.symlink ?? false,
11
+ nodir: options.include !== "all",
12
+ };
13
+ }
14
+ async function scan(pattern, options = {}) {
15
+ return glob(pattern, toGlobOptions(options));
16
+ }
17
+ Glob.scan = scan;
18
+ function scanSync(pattern, options = {}) {
19
+ return globSync(pattern, toGlobOptions(options));
20
+ }
21
+ Glob.scanSync = scanSync;
22
+ function match(pattern, filepath) {
23
+ return minimatch(filepath, pattern, { dot: true });
24
+ }
25
+ Glob.match = match;
26
+ })(Glob || (Glob = {}));