@procedures/fs 0.0.0-stage → 0.2.0

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 (90) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/LICENSE +21 -0
  3. package/README.md +895 -2
  4. package/dist/.tsbuildinfo +1 -0
  5. package/dist/index.d.ts +17 -0
  6. package/dist/index.d.ts.map +1 -0
  7. package/dist/index.js +17 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/module.d.ts +22 -0
  10. package/dist/module.d.ts.map +1 -0
  11. package/dist/module.js +203 -0
  12. package/dist/module.js.map +1 -0
  13. package/dist/procedures/fs/copy.d.ts +11 -0
  14. package/dist/procedures/fs/copy.d.ts.map +1 -0
  15. package/dist/procedures/fs/copy.js +21 -0
  16. package/dist/procedures/fs/copy.js.map +1 -0
  17. package/dist/procedures/fs/exists.d.ts +11 -0
  18. package/dist/procedures/fs/exists.d.ts.map +1 -0
  19. package/dist/procedures/fs/exists.js +20 -0
  20. package/dist/procedures/fs/exists.js.map +1 -0
  21. package/dist/procedures/fs/glob.d.ts +17 -0
  22. package/dist/procedures/fs/glob.d.ts.map +1 -0
  23. package/dist/procedures/fs/glob.js +85 -0
  24. package/dist/procedures/fs/glob.js.map +1 -0
  25. package/dist/procedures/fs/index.d.ts +12 -0
  26. package/dist/procedures/fs/index.d.ts.map +1 -0
  27. package/dist/procedures/fs/index.js +12 -0
  28. package/dist/procedures/fs/index.js.map +1 -0
  29. package/dist/procedures/fs/json.d.ts +11 -0
  30. package/dist/procedures/fs/json.d.ts.map +1 -0
  31. package/dist/procedures/fs/json.js +16 -0
  32. package/dist/procedures/fs/json.js.map +1 -0
  33. package/dist/procedures/fs/mkdir.d.ts +11 -0
  34. package/dist/procedures/fs/mkdir.d.ts.map +1 -0
  35. package/dist/procedures/fs/mkdir.js +39 -0
  36. package/dist/procedures/fs/mkdir.js.map +1 -0
  37. package/dist/procedures/fs/move.d.ts +11 -0
  38. package/dist/procedures/fs/move.d.ts.map +1 -0
  39. package/dist/procedures/fs/move.js +46 -0
  40. package/dist/procedures/fs/move.js.map +1 -0
  41. package/dist/procedures/fs/read.d.ts +11 -0
  42. package/dist/procedures/fs/read.d.ts.map +1 -0
  43. package/dist/procedures/fs/read.js +21 -0
  44. package/dist/procedures/fs/read.js.map +1 -0
  45. package/dist/procedures/fs/readdir.d.ts +11 -0
  46. package/dist/procedures/fs/readdir.d.ts.map +1 -0
  47. package/dist/procedures/fs/readdir.js +38 -0
  48. package/dist/procedures/fs/readdir.js.map +1 -0
  49. package/dist/procedures/fs/rm.d.ts +11 -0
  50. package/dist/procedures/fs/rm.d.ts.map +1 -0
  51. package/dist/procedures/fs/rm.js +30 -0
  52. package/dist/procedures/fs/rm.js.map +1 -0
  53. package/dist/procedures/fs/stat.d.ts +11 -0
  54. package/dist/procedures/fs/stat.d.ts.map +1 -0
  55. package/dist/procedures/fs/stat.js +29 -0
  56. package/dist/procedures/fs/stat.js.map +1 -0
  57. package/dist/procedures/fs/write.d.ts +11 -0
  58. package/dist/procedures/fs/write.d.ts.map +1 -0
  59. package/dist/procedures/fs/write.js +24 -0
  60. package/dist/procedures/fs/write.js.map +1 -0
  61. package/dist/procedures/fs.d.ts +5 -0
  62. package/dist/procedures/fs.d.ts.map +1 -0
  63. package/dist/procedures/fs.js +5 -0
  64. package/dist/procedures/fs.js.map +1 -0
  65. package/dist/register.d.ts +7 -0
  66. package/dist/register.d.ts.map +1 -0
  67. package/dist/register.js +9 -0
  68. package/dist/register.js.map +1 -0
  69. package/dist/types.d.ts +187 -0
  70. package/dist/types.d.ts.map +1 -0
  71. package/dist/types.js +197 -0
  72. package/dist/types.js.map +1 -0
  73. package/package.json +77 -3
  74. package/src/index.ts +17 -0
  75. package/src/module.ts +265 -0
  76. package/src/procedures/fs/copy.ts +24 -0
  77. package/src/procedures/fs/exists.ts +20 -0
  78. package/src/procedures/fs/glob.ts +92 -0
  79. package/src/procedures/fs/index.ts +11 -0
  80. package/src/procedures/fs/json.ts +21 -0
  81. package/src/procedures/fs/mkdir.ts +43 -0
  82. package/src/procedures/fs/move.ts +47 -0
  83. package/src/procedures/fs/read.ts +23 -0
  84. package/src/procedures/fs/readdir.ts +44 -0
  85. package/src/procedures/fs/rm.ts +31 -0
  86. package/src/procedures/fs/stat.ts +30 -0
  87. package/src/procedures/fs/write.ts +27 -0
  88. package/src/procedures/fs.ts +5 -0
  89. package/src/register.ts +11 -0
  90. package/src/types.ts +407 -0
@@ -0,0 +1,92 @@
1
+ /**
2
+ * glob procedure
3
+ *
4
+ * Find files matching glob pattern
5
+ *
6
+ * The matches are relative to `cwd` (or absolute with `absolute: true`), with "/" separators.
7
+ * `ignore` removes the matches of its patterns and does not enter the folders that they match.
8
+ *
9
+ * Before, `ignore` was stripped by the input schema, and `absolute` and `dot` had no effect:
10
+ * `node:fs` glob has no such options (deep dive DATA-7).
11
+ */
12
+
13
+ import { glob as nodeGlob, readdir } from "node:fs/promises";
14
+ import { posix, resolve } from "node:path";
15
+ import type { GlobInput, GlobOutput } from "../../types.js";
16
+
17
+ // A character that a file name cannot hold. It takes the place of a leading "." of a name, so
18
+ // that "*" and "**" match dot entries when `dot` is true.
19
+ const DOT = "\u0001";
20
+
21
+ /** "a\\b" (Windows) becomes "a/b". */
22
+ function toPosix(path: string): string {
23
+ return path.split("\\").join("/");
24
+ }
25
+
26
+ /** Each name of the path that starts with "." starts with DOT instead. */
27
+ function hideDots(path: string): string {
28
+ return path
29
+ .split("/")
30
+ .map((name) => (name.startsWith(".") && name !== "." && name !== ".." ? DOT + name.slice(1) : name))
31
+ .join("/");
32
+ }
33
+
34
+ function ignored(path: string, ignore: readonly string[]): boolean {
35
+ return ignore.some((pattern) => posix.matchesGlob(path, pattern));
36
+ }
37
+
38
+ /** A folder is not entered when each path inside it is ignored. */
39
+ function folderIgnored(path: string, ignore: readonly string[]): boolean {
40
+ return ignored(`${path}/${DOT}`, ignore) || ignored(path, ignore);
41
+ }
42
+
43
+ /** All the matches of `pattern` under `root`, dot entries included. */
44
+ async function walkWithDots(root: string, pattern: string, ignore: readonly string[]): Promise<string[]> {
45
+ const hiddenPattern = hideDots(pattern);
46
+ const hiddenIgnore = ignore.map(hideDots);
47
+ const matches: string[] = [];
48
+
49
+ async function visit(relative: string): Promise<void> {
50
+ const entries = await readdir(relative ? resolve(root, relative) : root, { withFileTypes: true });
51
+ for (const entry of entries) {
52
+ const path = relative ? `${relative}/${entry.name}` : entry.name;
53
+ const hidden = hideDots(path);
54
+ if (entry.isDirectory()) {
55
+ if (folderIgnored(hidden, hiddenIgnore)) continue;
56
+ if (posix.matchesGlob(hidden, hiddenPattern)) matches.push(path);
57
+ await visit(path);
58
+ } else if (posix.matchesGlob(hidden, hiddenPattern) && !ignored(hidden, hiddenIgnore)) {
59
+ matches.push(path);
60
+ }
61
+ }
62
+ }
63
+
64
+ await visit("");
65
+ return matches;
66
+ }
67
+
68
+ /**
69
+ * Find files matching glob pattern
70
+ */
71
+ export async function glob(input: GlobInput): Promise<GlobOutput> {
72
+ const { pattern, absolute, dot } = input;
73
+ const ignore = input.ignore ?? [];
74
+ const root = resolve(input.cwd ?? process.cwd());
75
+
76
+ let relative: string[];
77
+ if (dot) {
78
+ relative = await walkWithDots(root, toPosix(pattern), ignore);
79
+ } else {
80
+ relative = [];
81
+ for await (const match of nodeGlob(pattern, { cwd: root, exclude: [...ignore] })) {
82
+ const path = toPosix(match);
83
+ // The exclude list of node:fs prunes folders. This check also covers the Node versions
84
+ // that ignore it.
85
+ if (!ignored(path, ignore)) relative.push(path);
86
+ }
87
+ }
88
+
89
+ relative.sort();
90
+ const matches = absolute ? relative.map((path) => resolve(root, path)) : relative;
91
+ return { pattern, matches };
92
+ }
@@ -0,0 +1,11 @@
1
+ export { read } from "./read.js";
2
+ export { write } from "./write.js";
3
+ export { exists } from "./exists.js";
4
+ export { mkdir } from "./mkdir.js";
5
+ export { rm } from "./rm.js";
6
+ export { readdir } from "./readdir.js";
7
+ export { stat } from "./stat.js";
8
+ export { copy } from "./copy.js";
9
+ export { move } from "./move.js";
10
+ export { glob } from "./glob.js";
11
+ export { readJson } from "./json.js";
@@ -0,0 +1,21 @@
1
+ /**
2
+ * read.json procedure
3
+ *
4
+ * Read and parse JSON file
5
+ */
6
+
7
+ import type { ReadJsonInput, ReadJsonOutput } from "../../types.js";
8
+ import { read } from "./read.js";
9
+
10
+ /**
11
+ * Read and parse JSON file
12
+ */
13
+ export async function readJson(input: ReadJsonInput): Promise<ReadJsonOutput> {
14
+ const { path } = input;
15
+ const {
16
+ content,
17
+ stats
18
+ } = await read({path, encoding : 'utf8'})
19
+ const data = JSON.parse(content);
20
+ return { path, data, stats };
21
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * mkdir procedure
3
+ *
4
+ * Create directory
5
+ */
6
+
7
+ import { mkdir as fsMkdir } from "node:fs/promises";
8
+ import { FileType, type MkdirInput, type MkdirOutput, type StatOutput } from "../../types.js";
9
+ import { stat } from "./stat.js";
10
+
11
+ /**
12
+ * Create directory
13
+ */
14
+ export async function mkdir(input: MkdirInput): Promise<MkdirOutput> {
15
+ const { path, recursive } = input;
16
+
17
+ // Probe the existing path. Only a genuine "not found" (ENOENT) means we
18
+ // should proceed to create it; any other stat error (e.g. EACCES) must
19
+ // propagate rather than being masked by an mkdir attempt.
20
+ let existing: StatOutput | undefined;
21
+ try {
22
+ existing = await stat(input);
23
+ } catch (err) {
24
+ if ((err as NodeJS.ErrnoException).code !== "ENOENT") throw err;
25
+ }
26
+
27
+ if (existing) {
28
+ // The path already exists. It's only a no-op success if it's a directory;
29
+ // an existing file (or other non-directory) is a real conflict, not a
30
+ // silently-"already created" directory.
31
+ if (existing.type === FileType.Directory) {
32
+ return { path, created: false };
33
+ }
34
+ const conflict = new Error(
35
+ `Cannot create directory: path already exists and is not a directory: ${path}`,
36
+ ) as NodeJS.ErrnoException;
37
+ conflict.code = "EEXIST";
38
+ throw conflict;
39
+ }
40
+
41
+ await fsMkdir(path, { recursive });
42
+ return { path, created: true };
43
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * move procedure
3
+ *
4
+ * Move or rename file/directory
5
+ */
6
+
7
+ import { cp, rename } from "node:fs/promises";
8
+ import type { MoveInput, MoveOutput } from "../../types.js";
9
+ import { stat } from "./stat.js";
10
+ import { rm } from "./rm.js";
11
+
12
+ /**
13
+ * Move or rename file/directory
14
+ */
15
+ export async function move(input: MoveInput): Promise<MoveOutput> {
16
+ const { src, dest, overwrite } = input;
17
+
18
+ if (!overwrite) {
19
+ try {
20
+ await stat({path : dest});
21
+ throw new Error(`Destination already exists: ${dest}`);
22
+ } catch (err) {
23
+ if ((err as NodeJS.ErrnoException).code !== "ENOENT") throw err;
24
+ }
25
+ }
26
+
27
+ try {
28
+ await rename(src, dest);
29
+ } catch (err) {
30
+ if ((err as NodeJS.ErrnoException).code === "EXDEV") {
31
+ // Cross-device move: copy then delete the source. When not overwriting,
32
+ // use errorOnExist so an already-present destination throws instead of
33
+ // being silently skipped by `cp` — otherwise the subsequent removal of
34
+ // `src` would lose the files that were never copied over.
35
+ await cp(src, dest, {
36
+ recursive: true,
37
+ force: overwrite,
38
+ errorOnExist: !overwrite,
39
+ });
40
+ await rm({ path: src, recursive: true, force: false });
41
+ } else {
42
+ throw err;
43
+ }
44
+ }
45
+
46
+ return { src, dest };
47
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * read procedure
3
+ *
4
+ * Read file contents
5
+ */
6
+
7
+ import { readFile } from "node:fs/promises";
8
+ import type { ReadInput, ReadOutput } from "../../types.js";
9
+ import { stat } from "./stat.js";
10
+
11
+ /**
12
+ * Read file contents
13
+ */
14
+ export async function read(input: ReadInput): Promise<ReadOutput> {
15
+ const { path, encoding } = input;
16
+ const content = await readFile(path, { encoding: encoding as BufferEncoding });
17
+ const stats = await stat({path});
18
+ return {
19
+ content,
20
+ path,
21
+ stats
22
+ };
23
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * readdir procedure
3
+ *
4
+ * Read directory contents
5
+ */
6
+
7
+ import { readdir as fsReaddir } from "node:fs/promises";
8
+ import { join } from "node:path";
9
+ import { type ReaddirInput, type ReaddirOutput, type ReaddirEntry, FileType } from "../../types.js";
10
+ import { stat } from "./stat.js";
11
+
12
+ /**
13
+ * Read directory contents
14
+ */
15
+ export async function readdir(input: ReaddirInput): Promise<ReaddirOutput> {
16
+ const { path, recursive, includeStats } = input;
17
+ const entries: ReaddirEntry[] = [];
18
+
19
+ async function readDir(dir: string): Promise<void> {
20
+ const items = await fsReaddir(dir, { withFileTypes: true });
21
+ for (const item of items) {
22
+ const fullPath = join(dir, item.name);
23
+
24
+ let type: FileType = FileType.Other;
25
+ if (item.isFile()) type = FileType.File;
26
+ else if (item.isDirectory()) type = FileType.Directory;
27
+
28
+ const entry: ReaddirEntry = { name: item.name, path: fullPath, type };
29
+
30
+ if (includeStats) {
31
+ entry.stats = await stat({path : fullPath});
32
+ }
33
+
34
+ entries.push(entry);
35
+
36
+ if (recursive && item.isDirectory()) {
37
+ await readDir(fullPath);
38
+ }
39
+ }
40
+ }
41
+
42
+ await readDir(path);
43
+ return { path, entries };
44
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * rm procedure
3
+ *
4
+ * Remove file or directory
5
+ */
6
+
7
+ import { rm as fsRm } from "node:fs/promises";
8
+ import type { RmInput, RmOutput } from "../../types.js";
9
+
10
+ /**
11
+ * Remove file or directory
12
+ */
13
+ export async function rm(input: RmInput): Promise<RmOutput> {
14
+ const { path, recursive, force } = input;
15
+ // Always call with force:false so Node reports a missing path via ENOENT
16
+ // (letting us distinguish "removed" from "didn't exist"). Node's `force`
17
+ // option only suppresses ENOENT anyway, so replicating it here keeps the
18
+ // exact same behavior while still surfacing EPERM/ENOTEMPTY/EBUSY/etc.
19
+ try {
20
+ await fsRm(path, { recursive, force: false });
21
+ return { path, removed: true };
22
+ } catch (err) {
23
+ // With force:true a non-existent path is not an error — report it as
24
+ // "nothing removed". Every other error (permissions, non-empty, busy) must
25
+ // propagate rather than being silently swallowed.
26
+ if (force && (err as NodeJS.ErrnoException).code === "ENOENT") {
27
+ return { path, removed: false };
28
+ }
29
+ throw err;
30
+ }
31
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * stat procedure
3
+ *
4
+ * Get file or directory stats
5
+ */
6
+
7
+ import { stat as fsStat } from "node:fs/promises";
8
+ import { FileType, type StatInput, type StatOutput } from "../../types.js";
9
+
10
+ /**
11
+ * Get file or directory stats
12
+ */
13
+ export async function stat(input: StatInput): Promise<StatOutput> {
14
+ const { path } = input;
15
+ const stats = await fsStat(path);
16
+
17
+ let type: FileType = FileType.Other;
18
+ if (stats.isFile()) type = FileType.File;
19
+ else if (stats.isDirectory()) type = FileType.Directory;
20
+
21
+ return {
22
+ path,
23
+ type,
24
+ size: stats.size,
25
+ mtime: stats.mtime.toISOString(),
26
+ ctime: stats.ctime.toISOString(),
27
+ atime: stats.atime.toISOString(),
28
+ mode: stats.mode,
29
+ };
30
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * write procedure
3
+ *
4
+ * Write content to file
5
+ */
6
+
7
+ import { writeFile, appendFile } from "node:fs/promises";
8
+ import type { WriteInput, WriteOutput } from "../../types.js";
9
+
10
+ /**
11
+ * Write content to file
12
+ */
13
+ export async function write(input: WriteInput): Promise<WriteOutput> {
14
+ const { path, content, encoding, mode } = input;
15
+ const writeOptions = { encoding: encoding as BufferEncoding };
16
+
17
+ if (mode === "append") {
18
+ await appendFile(path, content, writeOptions);
19
+ } else {
20
+ await writeFile(path, content, writeOptions);
21
+ }
22
+
23
+ return {
24
+ path,
25
+ bytesWritten: Buffer.byteLength(content, encoding as BufferEncoding),
26
+ };
27
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Filesystem procedures barrel export
3
+ */
4
+
5
+ export * from "./fs/index.js";
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Importing this file registers the procedures of @procedures/fs, and of the modules that it
3
+ * requires, in the global registry: `import "@procedures/fs/register"`. The package root and the
4
+ * `./module` entry register nothing (ARCHITECTURE-PROPOSALS-2026-10, P9).
5
+ */
6
+
7
+ import { registerFsProcedures } from "./module.js";
8
+
9
+ export * from "./module.js";
10
+
11
+ registerFsProcedures();