@procedures/fs 0.0.0-stage → 0.1.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 +5 -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 +16 -0
  10. package/dist/module.d.ts.map +1 -0
  11. package/dist/module.js +178 -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 +8 -0
  66. package/dist/register.d.ts.map +1 -0
  67. package/dist/register.js +14 -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 +74 -3
  74. package/src/index.ts +17 -0
  75. package/src/module.ts +238 -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 +17 -0
  90. package/src/types.ts +407 -0
package/src/module.ts ADDED
@@ -0,0 +1,238 @@
1
+ /**
2
+ * Module (no side effects): the procedures of this package as data, for `createRegistry()`.
3
+ *
4
+ * Procedure Registration for filesystem operations
5
+ *
6
+ * Registers fs.* procedures with the client system.
7
+ * This file is referenced by package.json's client.procedures field.
8
+ */
9
+
10
+ import { createProcedure, zodAdapter, defineModule, type ProcedureModule } from "@procedures/client";
11
+ import { read } from "./procedures/fs/read.js";
12
+ import { write } from "./procedures/fs/write.js";
13
+ import { exists } from "./procedures/fs/exists.js";
14
+ import { mkdir } from "./procedures/fs/mkdir.js";
15
+ import { rm } from "./procedures/fs/rm.js";
16
+ import { readdir } from "./procedures/fs/readdir.js";
17
+ import { stat } from "./procedures/fs/stat.js";
18
+ import { copy } from "./procedures/fs/copy.js";
19
+ import { move } from "./procedures/fs/move.js";
20
+ import { glob } from "./procedures/fs/glob.js";
21
+ import { readJson } from "./procedures/fs/json.js";
22
+ import {
23
+ ReadInputSchema,
24
+ WriteInputSchema,
25
+ ExistsInputSchema,
26
+ MkdirInputSchema,
27
+ RmInputSchema,
28
+ ReaddirInputSchema,
29
+ StatInputSchema,
30
+ CopyInputSchema,
31
+ MoveInputSchema,
32
+ GlobInputSchema,
33
+ ReadJsonInputSchema,
34
+ type ReadInput,
35
+ type ReadOutput,
36
+ type WriteInput,
37
+ type WriteOutput,
38
+ type ExistsInput,
39
+ type ExistsOutput,
40
+ type MkdirInput,
41
+ type MkdirOutput,
42
+ type RmInput,
43
+ type RmOutput,
44
+ type ReaddirInput,
45
+ type ReaddirOutput,
46
+ type StatInput,
47
+ type StatOutput,
48
+ type CopyInput,
49
+ type CopyOutput,
50
+ type MoveInput,
51
+ type MoveOutput,
52
+ type GlobInput,
53
+ type GlobOutput,
54
+ type ReadJsonInput,
55
+ type ReadJsonOutput,
56
+ ReadOutputSchema,
57
+ WriteOutputSchema,
58
+ ExistsOutputSchema,
59
+ MkdirOutputSchema,
60
+ RmOutputSchema,
61
+ ReaddirOutputSchema,
62
+ StatOutputSchema,
63
+ CopyOutputSchema,
64
+ MoveOutputSchema,
65
+ GlobOutputSchema,
66
+ ReadJsonOutputSchema,
67
+ } from "./types.js";
68
+
69
+ // =============================================================================
70
+ // Procedure Definitions
71
+ // =============================================================================
72
+
73
+ const fsReadProcedure = createProcedure()
74
+ .path(["fs", "read"])
75
+ .input(zodAdapter<ReadInput>(ReadInputSchema))
76
+ .output(zodAdapter<ReadOutput>(ReadOutputSchema))
77
+ .meta({
78
+ description: "Read file contents",
79
+ args: ["path"],
80
+ shorts: { encoding: "e" },
81
+ output: "json",
82
+ })
83
+ .handler(read)
84
+ .build();
85
+
86
+ const fsWriteProcedure = createProcedure()
87
+ .path(["fs", "write"])
88
+ .input(zodAdapter<WriteInput>(WriteInputSchema))
89
+ .output(zodAdapter<WriteOutput>(WriteOutputSchema))
90
+ .meta({
91
+ description: "Write content to file",
92
+ args: ["path", "content"],
93
+ shorts: { encoding: "e", mode: "m" },
94
+ output: "json",
95
+ })
96
+ .handler(write)
97
+ .build();
98
+
99
+ const fsExistsProcedure = createProcedure()
100
+ .path(["fs", "exists"])
101
+ .input(zodAdapter<ExistsInput>(ExistsInputSchema))
102
+ .output(zodAdapter<ExistsOutput>(ExistsOutputSchema))
103
+ .meta({
104
+ description: "Check if file or directory exists",
105
+ args: ["path"],
106
+ shorts: {},
107
+ output: "json",
108
+ })
109
+ .handler(exists)
110
+ .build();
111
+
112
+ const fsMkdirProcedure = createProcedure()
113
+ .path(["fs", "mkdir"])
114
+ .input(zodAdapter<MkdirInput>(MkdirInputSchema))
115
+ .output(zodAdapter<MkdirOutput>(MkdirOutputSchema))
116
+ .meta({
117
+ description: "Create directory",
118
+ args: ["path"],
119
+ shorts: { recursive: "r" },
120
+ output: "json",
121
+ })
122
+ .handler(mkdir)
123
+ .build();
124
+
125
+ const fsRmProcedure = createProcedure()
126
+ .path(["fs", "rm"])
127
+ .input(zodAdapter<RmInput>(RmInputSchema))
128
+ .output(zodAdapter<RmOutput>(RmOutputSchema))
129
+ .meta({
130
+ description: "Remove file or directory",
131
+ args: ["path"],
132
+ shorts: { recursive: "r", force: "f" },
133
+ output: "json",
134
+ })
135
+ .handler(rm)
136
+ .build();
137
+
138
+ const fsReaddirProcedure = createProcedure()
139
+ .path(["fs", "readdir"])
140
+ .input(zodAdapter<ReaddirInput>(ReaddirInputSchema))
141
+ .output(zodAdapter<ReaddirOutput>(ReaddirOutputSchema))
142
+ .meta({
143
+ description: "Read directory contents",
144
+ args: ["path"],
145
+ shorts: { recursive: "r", includeStats: "s" },
146
+ output: "json",
147
+ })
148
+ .handler(readdir)
149
+ .build();
150
+
151
+ const fsStatProcedure = createProcedure()
152
+ .path(["fs", "stat"])
153
+ .input(zodAdapter<StatInput>(StatInputSchema))
154
+ .output(zodAdapter<StatOutput>(StatOutputSchema))
155
+ .meta({
156
+ description: "Get file or directory stats",
157
+ args: ["path"],
158
+ shorts: {},
159
+ output: "json",
160
+ })
161
+ .handler(stat)
162
+ .build();
163
+
164
+ const fsCopyProcedure = createProcedure()
165
+ .path(["fs", "copy"])
166
+ .input(zodAdapter<CopyInput>(CopyInputSchema))
167
+ .output(zodAdapter<CopyOutput>(CopyOutputSchema))
168
+ .meta({
169
+ description: "Copy file or directory",
170
+ args: ["src", "dest"],
171
+ shorts: { recursive: "r", overwrite: "o" },
172
+ output: "json",
173
+ })
174
+ .handler(copy)
175
+ .build();
176
+
177
+ const fsMoveProcedure = createProcedure()
178
+ .path(["fs", "move"])
179
+ .input(zodAdapter<MoveInput>(MoveInputSchema))
180
+ .output(zodAdapter<MoveOutput>(MoveOutputSchema))
181
+ .meta({
182
+ description: "Move or rename file/directory",
183
+ args: ["src", "dest"],
184
+ shorts: { overwrite: "o" },
185
+ output: "json",
186
+ })
187
+ .handler(move)
188
+ .build();
189
+
190
+ const fsGlobProcedure = createProcedure()
191
+ .path(["fs", "glob"])
192
+ .input(zodAdapter<GlobInput>(GlobInputSchema))
193
+ .output(zodAdapter<GlobOutput>(GlobOutputSchema))
194
+ .meta({
195
+ description: "Find files matching glob pattern",
196
+ args: ["pattern"],
197
+ shorts: { cwd: "c", absolute: "a", dot: "d" },
198
+ output: "json",
199
+ })
200
+ .handler(glob)
201
+ .build();
202
+
203
+ const fsReadJsonProcedure = createProcedure()
204
+ .path(["fs", "read.json"])
205
+ .input(zodAdapter<ReadJsonInput>(ReadJsonInputSchema))
206
+ .output(zodAdapter<ReadJsonOutput>(ReadJsonOutputSchema))
207
+ .meta({
208
+ description: "Read and parse JSON file",
209
+ args: ["path"],
210
+ shorts: {},
211
+ output: "json",
212
+ })
213
+ .handler(readJson)
214
+ .build();
215
+
216
+ // =============================================================================
217
+ // Registration
218
+ // =============================================================================
219
+
220
+ /**
221
+ * The module of this package, for `createRegistry()`: its procedures as data. Importing this
222
+ * file registers nothing. (`register.ts` registers the same procedures in the global registry.)
223
+ */
224
+ export const fsModule: ProcedureModule = defineModule("@procedures/fs", [
225
+ fsReadProcedure,
226
+ fsWriteProcedure,
227
+ fsExistsProcedure,
228
+ fsMkdirProcedure,
229
+ fsRmProcedure,
230
+ fsReaddirProcedure,
231
+ fsStatProcedure,
232
+ fsCopyProcedure,
233
+ fsMoveProcedure,
234
+ fsGlobProcedure,
235
+ fsReadJsonProcedure,
236
+ ]);
237
+
238
+ export default fsModule;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * copy procedure
3
+ *
4
+ * Copy file or directory
5
+ */
6
+
7
+ import { copyFile, cp, constants } from "node:fs/promises";
8
+ import type { CopyInput, CopyOutput } from "../../types.js";
9
+
10
+ /**
11
+ * Copy file or directory
12
+ */
13
+ export async function copy(input: CopyInput): Promise<CopyOutput> {
14
+ const { src, dest, recursive, overwrite } = input;
15
+ const flags = overwrite ? 0 : constants.COPYFILE_EXCL;
16
+
17
+ if (recursive) {
18
+ await cp(src, dest, { recursive, force: overwrite });
19
+ } else {
20
+ await copyFile(src, dest, flags);
21
+ }
22
+
23
+ return { src, dest };
24
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * exists procedure
3
+ *
4
+ * Check if file or directory exists
5
+ */
6
+
7
+ import { type ExistsInput, type ExistsOutput } from "../../types.js";
8
+ import { stat } from "./stat.js";
9
+
10
+ /**
11
+ * Check if file or directory exists
12
+ */
13
+ export async function exists(input: ExistsInput): Promise<ExistsOutput> {
14
+ try {
15
+ const stats = await stat(input);
16
+ return { exists: true, stats };
17
+ } catch {
18
+ return { exists: false };
19
+ }
20
+ }
@@ -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,17 @@
1
+ /**
2
+ * Importing this file registers the procedures of @procedures/fs in the global registry
3
+ * (DECISIONS-2026-10, "Every package root registers its procedures"). The procedures as data
4
+ * are in `module.ts`, which registers nothing: a host with its own registry uses that one.
5
+ */
6
+
7
+ import { registerProcedures } from "@procedures/client";
8
+ import { fsModule } from "./module.js";
9
+
10
+ export * from "./module.js";
11
+
12
+ export function registerFsProcedures(): void {
13
+ registerProcedures(fsModule.procedures);
14
+ }
15
+
16
+ // Auto-register when this module is loaded
17
+ registerFsProcedures();