@warlock.js/fs 4.1.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 (46) hide show
  1. package/README.md +76 -0
  2. package/cjs/index.cjs +424 -0
  3. package/cjs/index.cjs.map +1 -0
  4. package/esm/atomic.d.mts +22 -0
  5. package/esm/atomic.mjs +40 -0
  6. package/esm/atomic.mjs.map +1 -0
  7. package/esm/copy.d.mts +14 -0
  8. package/esm/copy.mjs +29 -0
  9. package/esm/copy.mjs.map +1 -0
  10. package/esm/dirs.d.mts +10 -0
  11. package/esm/dirs.mjs +18 -0
  12. package/esm/dirs.mjs.map +1 -0
  13. package/esm/exists.d.mts +44 -0
  14. package/esm/exists.mjs +86 -0
  15. package/esm/exists.mjs.map +1 -0
  16. package/esm/hash.d.mts +32 -0
  17. package/esm/hash.mjs +52 -0
  18. package/esm/hash.mjs.map +1 -0
  19. package/esm/index.d.mts +12 -0
  20. package/esm/index.mjs +13 -0
  21. package/esm/list.d.mts +20 -0
  22. package/esm/list.mjs +39 -0
  23. package/esm/list.mjs.map +1 -0
  24. package/esm/read.d.mts +16 -0
  25. package/esm/read.mjs +28 -0
  26. package/esm/read.mjs.map +1 -0
  27. package/esm/remove.d.mts +14 -0
  28. package/esm/remove.mjs +40 -0
  29. package/esm/remove.mjs.map +1 -0
  30. package/esm/rename.d.mts +9 -0
  31. package/esm/rename.mjs +17 -0
  32. package/esm/rename.mjs.map +1 -0
  33. package/esm/stats.d.mts +16 -0
  34. package/esm/stats.mjs +28 -0
  35. package/esm/stats.mjs.map +1 -0
  36. package/esm/write.d.mts +14 -0
  37. package/esm/write.mjs +29 -0
  38. package/esm/write.mjs.map +1 -0
  39. package/llms-full.txt +570 -0
  40. package/llms.txt +13 -0
  41. package/package.json +24 -0
  42. package/skills/hash-files/SKILL.md +122 -0
  43. package/skills/manage-directories/SKILL.md +140 -0
  44. package/skills/overview/SKILL.md +77 -0
  45. package/skills/read-and-write-files/SKILL.md +107 -0
  46. package/skills/write-atomically/SKILL.md +98 -0
package/llms-full.txt ADDED
@@ -0,0 +1,570 @@
1
+ # Warlock FS — full skills
2
+
3
+ > Package: `@warlock.js/fs`
4
+
5
+ > Generated artifact. Concatenates every SKILL.md and reference file under `@warlock.js/fs/skills/`. Re-run `node scripts/generate-llms.mjs` after any change.
6
+
7
+ ## hash-files `@warlock.js/fs/hash-files/SKILL.md`
8
+
9
+ ---
10
+ name: hash-files
11
+ description: 'Compute hex digests — hashFile / hashFileAsync (streaming for large files), hashFileSmallAsync, hashString, hashBuffer. Defaults to SHA-256; supports sha1 / md5 / sha512. Triggers: `hashFile`, `hashFileAsync`, `hashFileSmallAsync`, `hashString`, `hashBuffer`, `HashAlgorithm`; "fingerprint a file for cache invalidation", "compute SHA-256 checksum", "compare two files for equality", "cache key from request input"; typical import `import { hashFileAsync, hashString } from "@warlock.js/fs"`. Skip: file IO — `@warlock.js/fs/read-and-write-files/SKILL.md`; competing libs `hasha`, `md5-file`, `crypto-hash`; native `node:crypto` `createHash`.'
12
+ ---
13
+
14
+ # Compute file and content hashes
15
+
16
+ Hex-digest helpers backed by `node:crypto`. Picks the right strategy for the input size — streaming for files (memory stays flat), one-shot for in-memory content.
17
+
18
+ ## Available algorithms
19
+
20
+ ```ts
21
+ type HashAlgorithm = "sha256" | "sha1" | "md5" | "sha512";
22
+ ```
23
+
24
+ Default is `"sha256"` — the right choice for cache-bust, content-addressable storage, and fingerprinting. Pick `"md5"` only when matching an external system that requires it; never for security.
25
+
26
+ ## Hash a file
27
+
28
+ ```ts
29
+ import { hashFile, hashFileAsync } from "@warlock.js/fs";
30
+
31
+ // Streaming — constant memory regardless of file size
32
+ const fingerprint = await hashFileAsync("./bundle.js");
33
+ // → "8a7d3e2f9b4c..."
34
+
35
+ // Sync, with custom algorithm
36
+ const md5 = hashFile("./small.txt", "md5");
37
+ ```
38
+
39
+ `hashFileAsync` uses a stream, so a 1 GB file doesn't blow the heap. `hashFile` (sync) reads the whole file at once — fine for small files.
40
+
41
+ ## Hash a small file in one shot
42
+
43
+ ```ts
44
+ import { hashFileSmallAsync } from "@warlock.js/fs";
45
+
46
+ const digest = await hashFileSmallAsync("./icon.svg");
47
+ ```
48
+
49
+ `hashFileSmallAsync` reads the file in a single `readFile` call before hashing. Slightly faster than streaming when the file is < ~1 MB; **don't** use it on large files (it'll load the whole thing into memory).
50
+
51
+ | Use | Reach for |
52
+ | --- | --- |
53
+ | Streaming async (default for files) | `hashFileAsync` |
54
+ | Small file, slightly faster async | `hashFileSmallAsync` |
55
+ | Sync (CLI / config loader only) | `hashFile` |
56
+ | In-memory string | `hashString` |
57
+ | In-memory Buffer / Uint8Array | `hashBuffer` |
58
+
59
+ ## Hash in-memory content
60
+
61
+ ```ts
62
+ import { hashString, hashBuffer } from "@warlock.js/fs";
63
+
64
+ const stringDigest = hashString("hello world");
65
+ const bufferDigest = hashBuffer(Buffer.from([0x01, 0x02, 0x03]));
66
+ ```
67
+
68
+ Both sync, both default to SHA-256, both accept the algorithm override.
69
+
70
+ ## Common shapes
71
+
72
+ ### Cache key from request input
73
+
74
+ ```ts
75
+ import { hashString } from "@warlock.js/fs";
76
+
77
+ const key = `report.${hashString(JSON.stringify(filters))}`;
78
+ await cache.set(key, report, "1h");
79
+ ```
80
+
81
+ Stable, short, collision-resistant. JSON stringification is the gotcha — key order matters; sort keys if the input might arrive in different orders.
82
+
83
+ ### Bust a CDN cache when a build artifact changes
84
+
85
+ ```ts
86
+ import { hashFileAsync } from "@warlock.js/fs";
87
+
88
+ const digest = await hashFileAsync("./dist/bundle.js");
89
+ await renameFileAsync("./dist/bundle.js", `./dist/bundle.${digest.slice(0, 8)}.js`);
90
+ ```
91
+
92
+ 8 hex chars (≈32 bits) is enough for a single-app deployment — collisions on a per-build basis are vanishingly small.
93
+
94
+ ### Skip work if content hasn't changed
95
+
96
+ ```ts
97
+ import { hashFileAsync, fileExistsAsync, getFileAsync } from "@warlock.js/fs";
98
+
99
+ const inputDigest = await hashFileAsync("./input.json");
100
+ const cachedDigest = (await fileExistsAsync("./.last-input-digest"))
101
+ ? await getFileAsync("./.last-input-digest")
102
+ : null;
103
+
104
+ if (inputDigest === cachedDigest) {
105
+ return; // input unchanged — skip the expensive pipeline
106
+ }
107
+
108
+ await runPipeline();
109
+ await putFileAsync("./.last-input-digest", inputDigest);
110
+ ```
111
+
112
+ ### Compare two files for equality
113
+
114
+ ```ts
115
+ const same = (await hashFileAsync(a)) === (await hashFileAsync(b));
116
+ ```
117
+
118
+ Cheaper than `cmp`-byte-comparing two large files when the result is "yes/no different" — and you cache the digests for later comparisons against other candidates.
119
+
120
+ ## See also
121
+
122
+ - [`@warlock.js/fs/read-and-write-files/SKILL.md`](@warlock.js/fs/read-and-write-files/SKILL.md) — reading files before hashing in-memory content
123
+ - [`@warlock.js/cache/use-cached-hof/SKILL.md`](@warlock.js/cache/use-cached-hof/SKILL.md) — building cache keys from hashes
124
+
125
+ ## Things NOT to do
126
+
127
+ - Don't use MD5 or SHA-1 for security purposes (password hashing, signature verification). Both are broken cryptographically — they're fine for cache keys and non-adversarial fingerprinting, nothing more.
128
+ - Don't truncate the digest below 32 bits (8 hex chars) for collision-sensitive uses. Two builds with the same prefix do happen; full digest is 64 hex chars for SHA-256.
129
+ - Don't load a large file via `getFileAsync` and then `hashString` it — that defeats the streaming optimization. Use `hashFileAsync` directly.
130
+ - Don't expect digests to compare lexically meaningfully — they're random hex. For ordering, hash and then `bigint`-convert if you really need sort.
131
+
132
+
133
+ ## manage-directories `@warlock.js/fs/manage-directories/SKILL.md`
134
+
135
+ ---
136
+ name: manage-directories
137
+ description: 'Manage directories + files — ensureDirectory (mkdir -p), list / listFiles / listDirectories, copyFile / copyDirectory, renameFile, unlink (ENOENT-safe), removeDirectory (recursive force). Triggers: `ensureDirectoryAsync`, `listAsync`, `listFilesAsync`, `listDirectoriesAsync`, `copyFileAsync`, `copyDirectoryAsync`, `renameFileAsync`, `unlinkAsync`, `removeDirectoryAsync`; "create directory recursively", "list files in folder", "delete directory recursively", "copy or rename folder", "walk a tree"; typical import `import { ensureDirectoryAsync, listFilesAsync, removeDirectoryAsync } from "@warlock.js/fs"`. Skip: file IO — `@warlock.js/fs/read-and-write-files/SKILL.md`; atomic writes — `@warlock.js/fs/write-atomically/SKILL.md`; competing libs `fs-extra`, `mkdirp`, `rimraf`, `recursive-readdir`, `glob`; native `node:fs/promises`.'
138
+ ---
139
+
140
+ # Manage directories and files on disk
141
+
142
+ Same two-suffix convention as the rest of `@warlock.js/fs` — `*Async` is async, the bare name is sync.
143
+
144
+ ## Ensure a directory exists
145
+
146
+ ```ts
147
+ import { ensureDirectoryAsync } from "@warlock.js/fs";
148
+
149
+ await ensureDirectoryAsync("./dist/cache/v2");
150
+ // recursively creates dist, dist/cache, dist/cache/v2 if missing
151
+ // no-op if everything already exists
152
+ ```
153
+
154
+ Idempotent. Pair it with anything that writes — though `putFileAsync` already does this internally, so you rarely need `ensureDirectory` for the immediate parent of a file you're about to write.
155
+
156
+ ## List children
157
+
158
+ Three variants:
159
+
160
+ ```ts
161
+ import { listAsync, listFilesAsync, listDirectoriesAsync } from "@warlock.js/fs";
162
+
163
+ await listAsync("./src"); // [files + subdirs] full paths
164
+ await listFilesAsync("./src"); // only regular files
165
+ await listDirectoriesAsync("./src"); // only directories
166
+ ```
167
+
168
+ Returns **full paths** (joined to the directory you passed in), not bare entry names. Pass them straight to other fs calls.
169
+
170
+ ```ts
171
+ const components = await listFilesAsync("./src/components");
172
+ for (const file of components) {
173
+ // file = "./src/components/Button.tsx"
174
+ await processComponent(file);
175
+ }
176
+ ```
177
+
178
+ Only the immediate children — non-recursive. Recurse yourself with the directory variant + a stack/queue if you need deep traversal.
179
+
180
+ ## Copy
181
+
182
+ ```ts
183
+ import { copyFileAsync, copyDirectoryAsync } from "@warlock.js/fs";
184
+
185
+ // File — creates the destination's parent directories
186
+ await copyFileAsync("./dist/bundle.js", "./snapshot/v2/bundle.js");
187
+
188
+ // Directory — fully recursive
189
+ await copyDirectoryAsync("./public", "./dist/public");
190
+ ```
191
+
192
+ `copyFile` creates the destination's parent directories. `copyDirectory` uses Node's recursive `cp` under the hood — preserves the tree, overwrites existing files.
193
+
194
+ ## Rename / move
195
+
196
+ ```ts
197
+ import { renameFileAsync } from "@warlock.js/fs";
198
+
199
+ await renameFileAsync("./tmp/foo.txt", "./final/foo.txt");
200
+ ```
201
+
202
+ Works on files and directories. Cross-device renames (e.g. `/tmp` → `/var`) may fail with `EXDEV` — the OS won't move across mounts. For cross-device, copy then delete.
203
+
204
+ ## Delete
205
+
206
+ ```ts
207
+ import { unlinkAsync, removeDirectoryAsync } from "@warlock.js/fs";
208
+
209
+ await unlinkAsync("./obsolete.txt"); // single file — ENOENT-safe
210
+ await removeDirectoryAsync("./dist"); // recursive + force — ENOENT-safe
211
+ ```
212
+
213
+ Both are **idempotent for missing targets** — calling on a path that doesn't exist is a no-op, not an error. Other errors (`EACCES`, `EBUSY`) still throw.
214
+
215
+ ## Picking a delete shape
216
+
217
+ | Task | Reach for |
218
+ | --- | --- |
219
+ | Drop one file | `unlinkAsync(path)` |
220
+ | Drop a whole tree | `removeDirectoryAsync(path)` |
221
+ | Drop everything in a folder but keep the folder | `for (const f of await listAsync(dir)) await removeDirectoryAsync(f)` (file? unlink; dir? recurse) |
222
+
223
+ ## Common shapes
224
+
225
+ ### Snapshot a build output
226
+
227
+ ```ts
228
+ import { ensureDirectoryAsync, copyDirectoryAsync, removeDirectoryAsync } from "@warlock.js/fs";
229
+
230
+ const target = `./snapshots/${Date.now()}`;
231
+ await ensureDirectoryAsync(target);
232
+ await copyDirectoryAsync("./dist", target);
233
+ ```
234
+
235
+ ### Reset a temp dir between runs
236
+
237
+ ```ts
238
+ await removeDirectoryAsync("./tmp");
239
+ await ensureDirectoryAsync("./tmp");
240
+ ```
241
+
242
+ ### Walk every TS file under src
243
+
244
+ ```ts
245
+ async function walk(dir: string): Promise<string[]> {
246
+ const entries = await listAsync(dir);
247
+ const results: string[] = [];
248
+
249
+ for (const entry of entries) {
250
+ if (await directoryExistsAsync(entry)) {
251
+ results.push(...(await walk(entry)));
252
+ } else if (entry.endsWith(".ts")) {
253
+ results.push(entry);
254
+ }
255
+ }
256
+
257
+ return results;
258
+ }
259
+
260
+ const tsFiles = await walk("./src");
261
+ ```
262
+
263
+ ## See also
264
+
265
+ - [`@warlock.js/fs/read-and-write-files/SKILL.md`](@warlock.js/fs/read-and-write-files/SKILL.md) — text / JSON file IO and existence checks
266
+ - [`@warlock.js/fs/write-atomically/SKILL.md`](@warlock.js/fs/write-atomically/SKILL.md) — atomic writes when concurrent readers might see the file mid-write
267
+ - [`@warlock.js/fs/hash-files/SKILL.md`](@warlock.js/fs/hash-files/SKILL.md) — fingerprinting
268
+
269
+ ## Things NOT to do
270
+
271
+ - Don't expect `listAsync` to recurse — it's intentionally one level. Write your own walker (see above) for deep traversal.
272
+ - Don't catch `ENOENT` on `unlink` / `removeDirectory` — both functions already swallow missing-target errors. If you're catching, you're handling a real error and should re-throw.
273
+ - Don't use `renameFile` across filesystems / mounts — `EXDEV` will surface. Copy + delete for cross-device.
274
+ - Don't list a directory you're concurrently modifying — readdir snapshots aren't atomic across concurrent writes.
275
+
276
+
277
+ ## overview `@warlock.js/fs/overview/SKILL.md`
278
+
279
+ ---
280
+ name: overview
281
+ description: 'Front-door orientation for `@warlock.js/fs` — filesystem primitives (read/write/JSON, dirs, copy/rename/delete, atomic writes, hashing, existence + stats). Two-suffix convention: `*Async` returns Promise, bare name is sync. Single canonical name per operation — no aliases. TRIGGER when: code imports anything from `@warlock.js/fs`; user asks "what does @warlock.js/fs do", "is fs the right package for X", "list all fs helpers", "fs sync vs async convention"; package.json adds `@warlock.js/fs`; user is choosing between fs vs `node:fs/promises`/`fs-extra`/`graceful-fs`. Skip: specific task already known — load the matching task skill directly (`@warlock.js/fs/read-and-write-files/SKILL.md`, `@warlock.js/fs/manage-directories/SKILL.md`, `@warlock.js/fs/write-atomically/SKILL.md`, `@warlock.js/fs/hash-files/SKILL.md`); the user is using plain `node:fs` and not touching fs imports.'
282
+ ---
283
+
284
+ # `@warlock.js/fs` — overview
285
+
286
+ Thin, opinionated wrapper over `node:fs` and `node:fs/promises`. Same operations you'd write by hand against the Node primitives, but with consistent naming, parent-directory auto-creation, ENOENT-safe deletes, atomic writes, and streaming hashes — the boring-but-load-bearing utilities every backend grows by month two anyway.
287
+
288
+ ## When to reach for it
289
+
290
+ - You're inside a `@warlock.js/*` project — every other framework package already depends on this one. Use it for consistency.
291
+ - You're outside Warlock but want one import that gives you sane defaults (auto-mkdir on writes, idempotent deletes, streaming hashes, atomic config writes).
292
+ - You're choosing between `node:fs` and a wrapper — and want a small, opinionated surface (~40 exports) rather than the kitchen sink of `fs-extra` or the patching-Node behavior of `graceful-fs`.
293
+
294
+ Skip if your code path is one-off and bare `node:fs/promises` is already imported elsewhere — there's no value in adding a dependency for a single `readFile`.
295
+
296
+ ## Convention — read these once and you know the shape
297
+
298
+ - **`*Async` is async** (returns `Promise<…>`). Use these in server / runtime code.
299
+ - **Bare name is synchronous.** Use these in CLI tools, code generators, and one-shot scripts where blocking the loop doesn't matter.
300
+ - **One canonical name per operation.** No aliases (`unlinkAsync`, not `deleteAsync` or `removeAsync` for the same thing). Reach for the obvious name; if you don't find it, it doesn't exist.
301
+ - **Writes auto-create parent directories.** `putFileAsync("./a/b/c.txt", …)` works without a separate `ensureDirectory("./a/b")` first.
302
+ - **Deletes are ENOENT-safe.** `unlinkAsync` and `removeDirectoryAsync` no-op on missing targets. Other errors (`EACCES`, `EBUSY`) still throw.
303
+
304
+ ## Skills index
305
+
306
+ Four task skills cover the surface. Load the one that matches what you're trying to do — don't load all four unless you're touring the package.
307
+
308
+ ### [`read-and-write-files/SKILL.md`](@warlock.js/fs/read-and-write-files/SKILL.md)
309
+
310
+ Read and write text or JSON files; check existence and metadata. Covers
311
+ `getFile` / `getFileAsync` / `getJsonFile` / `getJsonFileAsync`,
312
+ `putFile` / `putFileAsync` / `putJsonFile` / `putJsonFileAsync`,
313
+ `pathExists` / `fileExists` / `directoryExists`,
314
+ `lastModified` / `stats`.
315
+
316
+ Load when reading or writing text or JSON, or gating creation on existence.
317
+
318
+ ### [`manage-directories/SKILL.md`](@warlock.js/fs/manage-directories/SKILL.md)
319
+
320
+ Create, list, copy, move, and delete directories and files. Covers
321
+ `ensureDirectory(Async)`, `list(Async)` / `listFiles(Async)` / `listDirectories(Async)`,
322
+ `copyFile(Async)` / `copyDirectory(Async)`, `renameFile(Async)`,
323
+ `unlink(Async)`, `removeDirectory(Async)`.
324
+
325
+ Load when scaffolding, walking trees, snapshotting, cleaning, or moving files around.
326
+
327
+ ### [`write-atomically/SKILL.md`](@warlock.js/fs/write-atomically/SKILL.md)
328
+
329
+ Write files so concurrent readers never see a half-written state. Covers
330
+ `atomicWriteAsync(path, content)` and `atomicWriteJsonAsync(path, value)`.
331
+ Sibling temp file + atomic rename — last-writer-wins on contention, no locking.
332
+
333
+ Load when writing a file that other processes / file watchers / build steps consume in parallel (config, manifest, state, lockfile).
334
+
335
+ ### [`hash-files/SKILL.md`](@warlock.js/fs/hash-files/SKILL.md)
336
+
337
+ Compute hex digests for files (streaming) or in-memory content. Covers
338
+ `hashFile(Async)` (streaming — constant memory),
339
+ `hashFileSmallAsync` (one-shot for small files),
340
+ `hashString`, `hashBuffer`, plus the `HashAlgorithm` type.
341
+ Defaults to SHA-256; supports SHA-1 / MD5 / SHA-512.
342
+
343
+ Load when fingerprinting for cache invalidation, content-addressable storage, change detection, or file-equality comparison. Never for security (password hashing, signing).
344
+
345
+ ## What this package deliberately doesn't do
346
+
347
+ - **Globbing.** Use `tinyglobby` / `fast-glob`. Adding a glob engine here would double the surface for one use case.
348
+ - **Watching.** Use `chokidar` or `node:fs.watch` directly. Watchers have their own lifecycle that doesn't fit the one-shot utility shape.
349
+ - **Permissions / chmod / chown.** Out of scope. Reach for `node:fs/promises`'s `chmod` / `chown` directly when you need them.
350
+ - **Streaming pipelines beyond hashing.** This isn't a general streams library; it's a wrapper for the common one-shot file operations.
351
+
352
+ ## See also
353
+
354
+ - [`@warlock.js/core/warlock-conventions/SKILL.md`](@warlock.js/core/warlock-conventions/SKILL.md) — the parent framework's conventions; `fs` is one of its foundation packages.
355
+ - `mongez-agent-kit-authoring-skills` (load via agent-kit sync) — how this `overview/SKILL.md` becomes the front-door skill in `.claude/skills/warlock-js-fs-overview/`.
356
+
357
+
358
+ ## read-and-write-files `@warlock.js/fs/read-and-write-files/SKILL.md`
359
+
360
+ ---
361
+ name: read-and-write-files
362
+ description: 'Read and write files — getFile / getFileAsync / getJsonFile / putFile (auto-creates parent dirs), plus pathExists / fileExists / directoryExists / lastModified / stats. Triggers: `getFileAsync`, `getJsonFileAsync`, `putFileAsync`, `putJsonFileAsync`, `pathExists`, `fileExists`, `lastModifiedAsync`, `statsAsync`; "read a text file", "write a JSON file", "check if file exists"; typical import `import { getFileAsync, putJsonFileAsync, fileExists } from "@warlock.js/fs"`. Skip: atomic writes — `@warlock.js/fs/write-atomically/SKILL.md`; dirs + copy + delete — `@warlock.js/fs/manage-directories/SKILL.md`; hashing — `@warlock.js/fs/hash-files/SKILL.md`; competing libs `fs-extra`, `jsonfile`, `graceful-fs`; native `node:fs/promises`.'
363
+ ---
364
+
365
+ # Read and write files
366
+
367
+ Thin, opinionated wrapper around `node:fs` and `node:fs/promises`. Two-suffix convention: `*Async` returns a Promise, the bare name is synchronous. No `Sync` suffix on the sync calls — that would mean you have to remember the inverse.
368
+
369
+ ## Install
370
+
371
+ ```bash
372
+ yarn add @warlock.js/fs
373
+ ```
374
+
375
+ ## Read
376
+
377
+ ```ts
378
+ import { getFile, getFileAsync, getJsonFile, getJsonFileAsync } from "@warlock.js/fs";
379
+
380
+ // UTF-8 text
381
+ const config = await getFileAsync("./config.toml");
382
+ const sync = getFile("./config.toml");
383
+
384
+ // Parsed JSON, generic-typed
385
+ type Manifest = { version: string; files: string[] };
386
+ const manifest = await getJsonFileAsync<Manifest>("./manifest.json");
387
+ ```
388
+
389
+ Behavior:
390
+ - All read functions return UTF-8 strings (or parsed JSON in the `JsonFile` variants).
391
+ - Throws if the file doesn't exist (`ENOENT`) or JSON is invalid. Don't try/catch for "file might not be there" — use `pathExists` / `fileExists` below.
392
+
393
+ ## Write
394
+
395
+ ```ts
396
+ import { putFile, putFileAsync, putJsonFile, putJsonFileAsync } from "@warlock.js/fs";
397
+
398
+ await putFileAsync("./dist/output.txt", "hello world");
399
+ await putJsonFileAsync("./dist/manifest.json", { version: "1.0.0" });
400
+ ```
401
+
402
+ Behavior:
403
+ - Parent directories are created recursively — no need to `ensureDirectory` first.
404
+ - JSON variants pretty-print at 2-space indent. For minified output, stringify yourself and use the plain `putFile`.
405
+ - Overwrites existing files. For atomic write semantics (readers never see a half-written file), use [`write-atomically`](@warlock.js/fs/write-atomically/SKILL.md).
406
+
407
+ ## Existence checks
408
+
409
+ Three variants — pick the strictest one that fits the question you're asking. Each has an async (`*Async`) and a sync form:
410
+
411
+ ```ts
412
+ import { pathExistsAsync, fileExistsAsync, directoryExistsAsync } from "@warlock.js/fs";
413
+
414
+ await pathExistsAsync("./anything"); // true if file OR directory
415
+ await fileExistsAsync("./config.toml"); // true ONLY if a regular file
416
+ await directoryExistsAsync("./dist"); // true ONLY if a directory
417
+
418
+ // sync counterparts, same semantics
419
+ import { pathExists, fileExists, directoryExists } from "@warlock.js/fs";
420
+ ```
421
+
422
+ Match the check to the target: gate a directory operation (listing, walking) on `directoryExistsAsync`, not `fileExistsAsync` — the latter resolves `false` for a folder and would skip it entirely. Reach for `pathExistsAsync` only when the type genuinely doesn't matter.
423
+
424
+ `fileExists*` and `directoryExists*` follow symlinks (they're `stat`-based, not `lstat`). Use them to gate creation logic instead of try-catching a `read`:
425
+
426
+ ```ts
427
+ // ✅ Clearer intent
428
+ if (!(await fileExists("./config.toml"))) {
429
+ await putFileAsync("./config.toml", defaultConfig);
430
+ }
431
+
432
+ // ❌ Don't catch ENOENT as control flow
433
+ try {
434
+ await getFileAsync("./config.toml");
435
+ } catch {
436
+ await putFileAsync("./config.toml", defaultConfig);
437
+ }
438
+ ```
439
+
440
+ ## Metadata
441
+
442
+ ```ts
443
+ import { lastModified, stats } from "@warlock.js/fs";
444
+
445
+ const mtime = await lastModifiedAsync("./bundle.js"); // Date
446
+ const all = await statsAsync("./bundle.js"); // fs.Stats
447
+ ```
448
+
449
+ `lastModified` is sugar around `stat().mtime`. Reach for `stats` when you need size, mode bits, or other fields.
450
+
451
+ ## When to pick sync vs async
452
+
453
+ - **Async by default** — everything in a Warlock server / app runtime should be async. The event loop stays free.
454
+ - **Sync only in CLI tools and config-loaders that run once** — startup config, code generators, scripts. The blocking call is fine when there's nothing else to do.
455
+
456
+ ## See also
457
+
458
+ - [`@warlock.js/fs/manage-directories/SKILL.md`](@warlock.js/fs/manage-directories/SKILL.md) — directory listing, copying, removing, renaming
459
+ - [`@warlock.js/fs/write-atomically/SKILL.md`](@warlock.js/fs/write-atomically/SKILL.md) — safe writes for files that other readers depend on
460
+ - [`@warlock.js/fs/hash-files/SKILL.md`](@warlock.js/fs/hash-files/SKILL.md) — fingerprinting files
461
+
462
+ ## Things NOT to do
463
+
464
+ - Don't call `putFileAsync` on a file that other processes / readers consume in parallel — use `atomicWriteAsync` from [`write-atomically`](@warlock.js/fs/write-atomically/SKILL.md) instead.
465
+ - Don't rely on `try { getFileAsync(...) } catch` for existence checks — `fileExists` is faster and reads better.
466
+ - Don't pass binary content to `putFile` / `putFileAsync` as a `Buffer` directly — these are text-only (UTF-8). For binaries, use `node:fs/promises`'s `writeFile` directly, or use `atomicWriteAsync` (which accepts `string | Buffer`).
467
+
468
+
469
+ ## write-atomically `@warlock.js/fs/write-atomically/SKILL.md`
470
+
471
+ ---
472
+ name: write-atomically
473
+ description: 'Atomic file writes via atomicWriteAsync(path, content) — writes to a uniquely-named sibling temp + rename onto target so readers see old or complete new content, never half-written. Triggers: `atomicWriteAsync`, `atomicWriteJsonAsync`; "atomic file write", "write config file safely with concurrent readers", "manifest written by build step", "state file across runs", "avoid half-written files"; typical import `import { atomicWriteAsync, atomicWriteJsonAsync } from "@warlock.js/fs"`. Skip: plain writes — `@warlock.js/fs/read-and-write-files/SKILL.md`; read-modify-write locking — `@warlock.js/cache/use-cache-lock/SKILL.md`; competing libs `write-file-atomic`, `steno`, `fs-extra` `outputFile`.'
474
+ ---
475
+
476
+ # Atomic file writes
477
+
478
+ `atomicWriteAsync` is the safe replacement for `putFileAsync` when readers can see the file at any moment. Same parent-directory auto-creation; the difference is the write strategy.
479
+
480
+ ## Why use it
481
+
482
+ `putFileAsync` writes directly to the destination. If a reader picks the file up while you're partway through the write, they see truncated content. That's fine for ephemeral logs; not fine for:
483
+
484
+ - **Config files watched by a dev server / linter.** Half-written config makes the watcher emit a spurious error.
485
+ - **Manifests consumed by another process.** Two-process pipelines deserialize and crash on partial JSON.
486
+ - **State files between runs of the same script.** A crash mid-write leaves you with a corrupt file you can't read on the next run.
487
+
488
+ `atomicWriteAsync` writes to a uniquely-named sibling temp file first, then `rename`s it onto the target. On most filesystems the rename is atomic — readers see the old content, then the new content, never anything in between.
489
+
490
+ ## Shape
491
+
492
+ ```ts
493
+ import { atomicWriteAsync, atomicWriteJsonAsync } from "@warlock.js/fs";
494
+
495
+ await atomicWriteAsync("./config.toml", configString);
496
+ await atomicWriteAsync("./binary.bin", Buffer.from([0x01, 0x02])); // accepts string OR Buffer
497
+
498
+ // JSON sugar — pretty-prints at 2-space indent
499
+ await atomicWriteJsonAsync("./manifest.json", { version: "1.0.0", files: [...] });
500
+ ```
501
+
502
+ ## What happens internally
503
+
504
+ ```
505
+ 1. mkdir(dir, recursive)
506
+ 2. tempPath = `${dir}/.${name}.${randomHex(6)}.tmp` ← unique sibling temp
507
+ 3. writeFile(tempPath, content)
508
+ 4. rename(tempPath, filePath) ← atomic on POSIX, near-atomic on NTFS
509
+ on failure: unlink(tempPath)
510
+ ```
511
+
512
+ The random 6-byte suffix prevents two concurrent writers from racing on the same temp file. The temp file lives in the **same directory** as the target so the rename is intra-mount (cross-mount rename would fall back to copy + unlink, which isn't atomic).
513
+
514
+ ## Concurrent writers
515
+
516
+ Two `atomicWriteAsync` calls to the same target serialize at the rename. Whichever rename completes last wins. **No locking** — last-writer-wins is the contract.
517
+
518
+ If you need read-modify-write atomicity (each writer sees the previous writer's result), wrap the calls in a distributed lock — e.g. [`@warlock.js/cache/use-cache-lock/SKILL.md`](@warlock.js/cache/use-cache-lock/SKILL.md).
519
+
520
+ ## Common shapes
521
+
522
+ ### State file written across multiple runs
523
+
524
+ ```ts
525
+ // On every successful run
526
+ await atomicWriteJsonAsync("./.cache/last-run.json", {
527
+ finishedAt: new Date().toISOString(),
528
+ buildId: process.env.BUILD_ID,
529
+ });
530
+ ```
531
+
532
+ Crash partway through? Either the file has the previous run's content or the new run's content. Never garbage.
533
+
534
+ ### Manifest emitted by a build step
535
+
536
+ ```ts
537
+ const manifest = computeManifest(files);
538
+ await atomicWriteJsonAsync("./dist/manifest.json", manifest);
539
+ ```
540
+
541
+ A reader (CDN purge script, deployment tool) that picks up `dist/manifest.json` while the build is mid-write doesn't crash.
542
+
543
+ ### Config file watched by a dev server
544
+
545
+ ```ts
546
+ const config = transformConfig(input);
547
+ await atomicWriteAsync("./config.toml", config);
548
+ ```
549
+
550
+ The dev server's file watcher fires once after the rename, sees complete content. No double-event or partial-content noise.
551
+
552
+ ## What it doesn't protect against
553
+
554
+ - **Filesystem corruption.** Power-loss between `writeFile` and `rename` leaves the temp file behind — that's `fsync` territory, not handled here. For ironclad durability, you'd need a `writeFile + fsync + rename + fsync(parent)` sequence; this helper skips the fsyncs for write speed.
555
+ - **Cross-filesystem renames.** If `dir` is a different mount from the target's actual storage (unusual), `rename` may fall back to copy + delete, which isn't atomic. Keep the temp on the same mount — the helper does this automatically.
556
+ - **Race conditions in your callers.** `atomicWriteAsync` makes the file write atomic; it doesn't serialize callers. Two callers stomping each other is your problem, not the helper's.
557
+
558
+ ## See also
559
+
560
+ - [`@warlock.js/fs/read-and-write-files/SKILL.md`](@warlock.js/fs/read-and-write-files/SKILL.md) — `putFileAsync` for non-atomic writes
561
+ - [`@warlock.js/cache/use-cache-lock/SKILL.md`](@warlock.js/cache/use-cache-lock/SKILL.md) — distributed lock for read-modify-write protection
562
+
563
+ ## Things NOT to do
564
+
565
+ - Don't use `atomicWriteAsync` when you don't need atomicity — `putFileAsync` is slightly faster (no rename round-trip). For ephemeral files, plain write is fine.
566
+ - Don't store the temp file outside the target directory. The helper picks the same dir on purpose so the rename is intra-mount; if you reach inside the source and change that, you lose the atomicity guarantee on cross-mount setups.
567
+ - Don't pair atomic writes with locked reads expecting consistency. A reader between the rename and your next write sees the intermediate complete state — that's the point of the helper. If you want every read to see a particular write, serialize with a lock.
568
+ - Don't sync after atomic write expecting "definitely persisted to disk" — the helper doesn't fsync. For durability guarantees, fsync the parent directory after the rename yourself.
569
+
570
+
package/llms.txt ADDED
@@ -0,0 +1,13 @@
1
+ # Warlock FS
2
+
3
+ > Package: `@warlock.js/fs`
4
+
5
+ > Filesystem primitives for Warlock.js — drop-in replacement for @warlock.js/fs.
6
+
7
+ ## Skills
8
+
9
+ - [hash-files](@warlock.js/fs/hash-files/SKILL.md): Compute hex digests — hashFile / hashFileAsync (streaming for large files), hashFileSmallAsync, hashString, hashBuffer. Defaults to SHA-256; supports sha1 / md5 / sha512. Triggers: `hashFile`, `hashFileAsync`, `hashFileSmallAsync`, `hashString`, `hashBuffer`, `HashAlgorithm`; "fingerprint a file for cache invalidation", "compute SHA-256 checksum", "compare two files for equality", "cache key from request input"; typical import `import { hashFileAsync, hashString } from "@warlock.js/fs"`. Skip: file IO — `@warlock.js/fs/read-and-write-files/SKILL.md`; competing libs `hasha`, `md5-file`, `crypto-hash`; native `node:crypto` `createHash`.
10
+ - [manage-directories](@warlock.js/fs/manage-directories/SKILL.md): Manage directories + files — ensureDirectory (mkdir -p), list / listFiles / listDirectories, copyFile / copyDirectory, renameFile, unlink (ENOENT-safe), removeDirectory (recursive force). Triggers: `ensureDirectoryAsync`, `listAsync`, `listFilesAsync`, `listDirectoriesAsync`, `copyFileAsync`, `copyDirectoryAsync`, `renameFileAsync`, `unlinkAsync`, `removeDirectoryAsync`; "create directory recursively", "list files in folder", "delete directory recursively", "copy or rename folder", "walk a tree"; typical import `import { ensureDirectoryAsync, listFilesAsync, removeDirectoryAsync } from "@warlock.js/fs"`. Skip: file IO — `@warlock.js/fs/read-and-write-files/SKILL.md`; atomic writes — `@warlock.js/fs/write-atomically/SKILL.md`; competing libs `fs-extra`, `mkdirp`, `rimraf`, `recursive-readdir`, `glob`; native `node:fs/promises`.
11
+ - [overview](@warlock.js/fs/overview/SKILL.md): Front-door orientation for `@warlock.js/fs` — filesystem primitives (read/write/JSON, dirs, copy/rename/delete, atomic writes, hashing, existence + stats). Two-suffix convention: `*Async` returns Promise, bare name is sync. Single canonical name per operation — no aliases. TRIGGER when: code imports anything from `@warlock.js/fs`; user asks "what does @warlock.js/fs do", "is fs the right package for X", "list all fs helpers", "fs sync vs async convention"; package.json adds `@warlock.js/fs`; user is choosing between fs vs `node:fs/promises`/`fs-extra`/`graceful-fs`. Skip: specific task already known — load the matching task skill directly (`@warlock.js/fs/read-and-write-files/SKILL.md`, `@warlock.js/fs/manage-directories/SKILL.md`, `@warlock.js/fs/write-atomically/SKILL.md`, `@warlock.js/fs/hash-files/SKILL.md`); the user is using plain `node:fs` and not touching fs imports.
12
+ - [read-and-write-files](@warlock.js/fs/read-and-write-files/SKILL.md): Read and write files — getFile / getFileAsync / getJsonFile / putFile (auto-creates parent dirs), plus pathExists / fileExists / directoryExists / lastModified / stats. Triggers: `getFileAsync`, `getJsonFileAsync`, `putFileAsync`, `putJsonFileAsync`, `pathExists`, `fileExists`, `lastModifiedAsync`, `statsAsync`; "read a text file", "write a JSON file", "check if file exists"; typical import `import { getFileAsync, putJsonFileAsync, fileExists } from "@warlock.js/fs"`. Skip: atomic writes — `@warlock.js/fs/write-atomically/SKILL.md`; dirs + copy + delete — `@warlock.js/fs/manage-directories/SKILL.md`; hashing — `@warlock.js/fs/hash-files/SKILL.md`; competing libs `fs-extra`, `jsonfile`, `graceful-fs`; native `node:fs/promises`.
13
+ - [write-atomically](@warlock.js/fs/write-atomically/SKILL.md): Atomic file writes via atomicWriteAsync(path, content) — writes to a uniquely-named sibling temp + rename onto target so readers see old or complete new content, never half-written. Triggers: `atomicWriteAsync`, `atomicWriteJsonAsync`; "atomic file write", "write config file safely with concurrent readers", "manifest written by build step", "state file across runs", "avoid half-written files"; typical import `import { atomicWriteAsync, atomicWriteJsonAsync } from "@warlock.js/fs"`. Skip: plain writes — `@warlock.js/fs/read-and-write-files/SKILL.md`; read-modify-write locking — `@warlock.js/cache/use-cache-lock/SKILL.md`; competing libs `write-file-atomic`, `steno`, `fs-extra` `outputFile`.
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@warlock.js/fs",
3
+ "description": "Filesystem primitives for Warlock.js — drop-in replacement for @warlock.js/fs.",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "https://github.com/warlockjs/fs"
7
+ },
8
+ "version": "4.1.1",
9
+ "main": "./cjs/index.cjs",
10
+ "module": "./esm/index.mjs",
11
+ "types": "./esm/index.d.mts",
12
+ "exports": {
13
+ ".": {
14
+ "import": {
15
+ "types": "./esm/index.d.mts",
16
+ "default": "./esm/index.mjs"
17
+ },
18
+ "require": {
19
+ "types": "./esm/index.d.mts",
20
+ "default": "./cjs/index.cjs"
21
+ }
22
+ }
23
+ }
24
+ }