dsh-rules 0.1.1 → 0.1.2

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.
package/lib/fs.js CHANGED
@@ -1,195 +1,195 @@
1
- /**
2
- * Filesystem helpers for the dsh-rules plugin.
3
- *
4
- * Reads prefer the harness `fs` service (which respects containment and
5
- * produces stable `version` identities) and fall back to Node's own
6
- * filesystem when no `fs` service is mounted. All discovery is
7
- * cancellation-aware through `signal`.
8
- *
9
- * @module dsh-rules/fs
10
- */
11
- import { readdir, readFile, stat } from "node:fs/promises";
12
- import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
13
-
14
- /** Normalize a path to POSIX separators for glob matching. */
15
- function toPosix(path) {
16
- return path.split(sep).join("/");
17
- }
18
-
19
- /**
20
- * Walk up from `cwd` to the first directory containing any project-root
21
- * marker (e.g. `.git`); fall back to `cwd` itself.
22
- * @param cwd - absolute session working directory.
23
- * @param markers - marker file/directory names that identify a project root.
24
- * @param fileSystem - optional harness `fs` service.
25
- * @param signal - cancellation for provider probes.
26
- * @returns the absolute project root.
27
- */
28
- export async function findProjectRoot(cwd, markers, fileSystem, signal) {
29
- let current = resolve(cwd);
30
- while (true) {
31
- for (const marker of markers) {
32
- if (await pathExists(join(current, marker), fileSystem, signal)) return current;
33
- }
34
- const parent = dirname(current);
35
- if (parent === current) return resolve(cwd);
36
- current = parent;
37
- }
38
- }
39
-
40
- /**
41
- * Return the project-root-relative POSIX path of `path`, or `undefined` when
42
- * the path lies outside the project root (such files never activate rules).
43
- * @param projectRoot - absolute project root.
44
- * @param path - absolute path to relativize.
45
- * @returns relative POSIX path, or `undefined` when outside the root.
46
- */
47
- export function posixRelative(projectRoot, path) {
48
- const relativePath = relative(resolve(projectRoot), resolve(path));
49
- if (relativePath.length === 0) return ".";
50
- if (relativePath === ".." || relativePath.startsWith(`..${sep}`) || isAbsolute(relativePath)) return void 0;
51
- return toPosix(relativePath);
52
- }
53
-
54
- /**
55
- * Probe one rule-source path (a file) for current metadata.
56
- * @param path - absolute path to probe.
57
- * @param fileSystem - optional harness `fs` service.
58
- * @param signal - cancellation for provider probes.
59
- * @returns present metadata (version identity plus optional size), confirmed
60
- * absence, or temporary unavailability.
61
- */
62
- export async function statRuleFile(path, fileSystem, signal) {
63
- signal?.throwIfAborted();
64
- if (fileSystem !== void 0) {
65
- try {
66
- const target = await fileSystem.resolve(path, signalOptions(signal));
67
- signal?.throwIfAborted();
68
- const info = await fileSystem.stat(target, signal);
69
- signal?.throwIfAborted();
70
- if (info === void 0 || info.type !== "file") return { kind: "absent" };
71
- return {
72
- kind: "present",
73
- version: info.version,
74
- ...info.size === void 0 ? {} : { size: info.size }
75
- };
76
- } catch (error) {
77
- signal?.throwIfAborted();
78
- return isAbsentError(error) ? { kind: "absent" } : { kind: "unavailable" };
79
- }
80
- }
81
- try {
82
- const info = await stat(path, { signal });
83
- signal?.throwIfAborted();
84
- if (!info.isFile()) return { kind: "absent" };
85
- return {
86
- kind: "present",
87
- version: nodeVersionSignature(info),
88
- size: info.size
89
- };
90
- } catch (error) {
91
- signal?.throwIfAborted();
92
- return isAbsentError(error) ? { kind: "absent" } : { kind: "unavailable" };
93
- }
94
- }
95
-
96
- /**
97
- * Read one rule-source file's full text under a source byte cap.
98
- * @param path - absolute path to read.
99
- * @param fileSystem - optional harness `fs` service.
100
- * @param signal - cancellation for provider reads.
101
- * @param maxSourceBytes - maximum accepted UTF-8 bytes; larger files are skipped.
102
- * @returns the file text, or `undefined` when absent, unreadable, or oversized.
103
- */
104
- export async function readRuleText(path, fileSystem, signal, maxSourceBytes) {
105
- signal?.throwIfAborted();
106
- if (fileSystem !== void 0) {
107
- try {
108
- const target = await fileSystem.resolve(path, signalOptions(signal));
109
- signal?.throwIfAborted();
110
- return await fileSystem.readText(target, signal);
111
- } catch (error) {
112
- signal?.throwIfAborted();
113
- return void 0;
114
- }
115
- }
116
- try {
117
- const info = await stat(path, { signal });
118
- signal?.throwIfAborted();
119
- if (!info.isFile()) return void 0;
120
- if (info.size > maxSourceBytes) return void 0;
121
- return await readFile(path, { encoding: "utf8", signal });
122
- } catch (error) {
123
- signal?.throwIfAborted();
124
- return void 0;
125
- }
126
- }
127
-
128
- /**
129
- * List one rule-source directory's entries.
130
- * @param dir - absolute directory path.
131
- * @param fileSystem - optional harness `fs` service.
132
- * @param signal - cancellation for provider probes.
133
- * @returns entry descriptors, or `undefined` when the directory is absent.
134
- */
135
- export async function listRuleDirEntries(dir, fileSystem, signal) {
136
- signal?.throwIfAborted();
137
- if (fileSystem !== void 0) {
138
- try {
139
- const target = await fileSystem.resolve(dir, signalOptions(signal));
140
- signal?.throwIfAborted();
141
- return (await fileSystem.listDir(target, signal)).map((entry) => ({
142
- name: entry.name,
143
- type: entry.type
144
- }));
145
- } catch (error) {
146
- signal?.throwIfAborted();
147
- return isAbsentError(error) ? void 0 : null;
148
- }
149
- }
150
- try {
151
- const entries = await readdir(dir, { withFileTypes: true, encoding: "utf8" });
152
- return entries.map((entry) => ({
153
- name: entry.name,
154
- type: entry.isDirectory() ? "directory" : entry.isFile() ? "file" : "other"
155
- }));
156
- } catch (error) {
157
- signal?.throwIfAborted();
158
- return isAbsentError(error) ? void 0 : null;
159
- }
160
- }
161
-
162
- function signalOptions(signal) {
163
- return signal === void 0 ? void 0 : { signal };
164
- }
165
-
166
- /** Test one path for existence through the provider or the host filesystem. */
167
- async function pathExists(path, fileSystem, signal) {
168
- if (fileSystem !== void 0) {
169
- try {
170
- const target = await fileSystem.resolve(path, signalOptions(signal));
171
- signal?.throwIfAborted();
172
- const info = await fileSystem.stat(target, signal);
173
- signal?.throwIfAborted();
174
- return info !== void 0;
175
- } catch (error) {
176
- signal?.throwIfAborted();
177
- return false;
178
- }
179
- }
180
- try {
181
- await stat(path, signalOptions(signal));
182
- return true;
183
- } catch (error) {
184
- signal?.throwIfAborted();
185
- return false;
186
- }
187
- }
188
-
189
- function nodeVersionSignature(info) {
190
- return `${info.mtimeMs}:${info.size}`;
191
- }
192
-
193
- function isAbsentError(error) {
194
- return typeof error === "object" && error !== null && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR" || error.code === "FS_NOT_FOUND" || error.code === "FS_NOT_DIRECTORY");
195
- }
1
+ /**
2
+ * Filesystem helpers for the dsh-rules plugin.
3
+ *
4
+ * Reads prefer the harness `fs` service (which respects containment and
5
+ * produces stable `version` identities) and fall back to Node's own
6
+ * filesystem when no `fs` service is mounted. All discovery is
7
+ * cancellation-aware through `signal`.
8
+ *
9
+ * @module dsh-rules/fs
10
+ */
11
+ import { readdir, readFile, stat } from "node:fs/promises";
12
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
13
+
14
+ /** Normalize a path to POSIX separators for glob matching. */
15
+ function toPosix(path) {
16
+ return path.split(sep).join("/");
17
+ }
18
+
19
+ /**
20
+ * Walk up from `cwd` to the first directory containing any project-root
21
+ * marker (e.g. `.git`); fall back to `cwd` itself.
22
+ * @param cwd - absolute session working directory.
23
+ * @param markers - marker file/directory names that identify a project root.
24
+ * @param fileSystem - optional harness `fs` service.
25
+ * @param signal - cancellation for provider probes.
26
+ * @returns the absolute project root.
27
+ */
28
+ export async function findProjectRoot(cwd, markers, fileSystem, signal) {
29
+ let current = resolve(cwd);
30
+ while (true) {
31
+ for (const marker of markers) {
32
+ if (await pathExists(join(current, marker), fileSystem, signal)) return current;
33
+ }
34
+ const parent = dirname(current);
35
+ if (parent === current) return resolve(cwd);
36
+ current = parent;
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Return the project-root-relative POSIX path of `path`, or `undefined` when
42
+ * the path lies outside the project root (such files never activate rules).
43
+ * @param projectRoot - absolute project root.
44
+ * @param path - absolute path to relativize.
45
+ * @returns relative POSIX path, or `undefined` when outside the root.
46
+ */
47
+ export function posixRelative(projectRoot, path) {
48
+ const relativePath = relative(resolve(projectRoot), resolve(path));
49
+ if (relativePath.length === 0) return ".";
50
+ if (relativePath === ".." || relativePath.startsWith(`..${sep}`) || isAbsolute(relativePath)) return void 0;
51
+ return toPosix(relativePath);
52
+ }
53
+
54
+ /**
55
+ * Probe one rule-source path (a file) for current metadata.
56
+ * @param path - absolute path to probe.
57
+ * @param fileSystem - optional harness `fs` service.
58
+ * @param signal - cancellation for provider probes.
59
+ * @returns present metadata (version identity plus optional size), confirmed
60
+ * absence, or temporary unavailability.
61
+ */
62
+ export async function statRuleFile(path, fileSystem, signal) {
63
+ signal?.throwIfAborted();
64
+ if (fileSystem !== void 0) {
65
+ try {
66
+ const target = await fileSystem.resolve(path, signalOptions(signal));
67
+ signal?.throwIfAborted();
68
+ const info = await fileSystem.stat(target, signal);
69
+ signal?.throwIfAborted();
70
+ if (info === void 0 || info.type !== "file") return { kind: "absent" };
71
+ return {
72
+ kind: "present",
73
+ version: info.version,
74
+ ...info.size === void 0 ? {} : { size: info.size }
75
+ };
76
+ } catch (error) {
77
+ signal?.throwIfAborted();
78
+ return isAbsentError(error) ? { kind: "absent" } : { kind: "unavailable" };
79
+ }
80
+ }
81
+ try {
82
+ const info = await stat(path, { signal });
83
+ signal?.throwIfAborted();
84
+ if (!info.isFile()) return { kind: "absent" };
85
+ return {
86
+ kind: "present",
87
+ version: nodeVersionSignature(info),
88
+ size: info.size
89
+ };
90
+ } catch (error) {
91
+ signal?.throwIfAborted();
92
+ return isAbsentError(error) ? { kind: "absent" } : { kind: "unavailable" };
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Read one rule-source file's full text under a source byte cap.
98
+ * @param path - absolute path to read.
99
+ * @param fileSystem - optional harness `fs` service.
100
+ * @param signal - cancellation for provider reads.
101
+ * @param maxSourceBytes - maximum accepted UTF-8 bytes; larger files are skipped.
102
+ * @returns the file text, or `undefined` when absent, unreadable, or oversized.
103
+ */
104
+ export async function readRuleText(path, fileSystem, signal, maxSourceBytes) {
105
+ signal?.throwIfAborted();
106
+ if (fileSystem !== void 0) {
107
+ try {
108
+ const target = await fileSystem.resolve(path, signalOptions(signal));
109
+ signal?.throwIfAborted();
110
+ return await fileSystem.readText(target, signal);
111
+ } catch (error) {
112
+ signal?.throwIfAborted();
113
+ return void 0;
114
+ }
115
+ }
116
+ try {
117
+ const info = await stat(path, { signal });
118
+ signal?.throwIfAborted();
119
+ if (!info.isFile()) return void 0;
120
+ if (info.size > maxSourceBytes) return void 0;
121
+ return await readFile(path, { encoding: "utf8", signal });
122
+ } catch (error) {
123
+ signal?.throwIfAborted();
124
+ return void 0;
125
+ }
126
+ }
127
+
128
+ /**
129
+ * List one rule-source directory's entries.
130
+ * @param dir - absolute directory path.
131
+ * @param fileSystem - optional harness `fs` service.
132
+ * @param signal - cancellation for provider probes.
133
+ * @returns entry descriptors, or `undefined` when the directory is absent.
134
+ */
135
+ export async function listRuleDirEntries(dir, fileSystem, signal) {
136
+ signal?.throwIfAborted();
137
+ if (fileSystem !== void 0) {
138
+ try {
139
+ const target = await fileSystem.resolve(dir, signalOptions(signal));
140
+ signal?.throwIfAborted();
141
+ return (await fileSystem.listDir(target, signal)).map((entry) => ({
142
+ name: entry.name,
143
+ type: entry.type
144
+ }));
145
+ } catch (error) {
146
+ signal?.throwIfAborted();
147
+ return isAbsentError(error) ? void 0 : null;
148
+ }
149
+ }
150
+ try {
151
+ const entries = await readdir(dir, { withFileTypes: true, encoding: "utf8" });
152
+ return entries.map((entry) => ({
153
+ name: entry.name,
154
+ type: entry.isDirectory() ? "directory" : entry.isFile() ? "file" : "other"
155
+ }));
156
+ } catch (error) {
157
+ signal?.throwIfAborted();
158
+ return isAbsentError(error) ? void 0 : null;
159
+ }
160
+ }
161
+
162
+ function signalOptions(signal) {
163
+ return signal === void 0 ? void 0 : { signal };
164
+ }
165
+
166
+ /** Test one path for existence through the provider or the host filesystem. */
167
+ async function pathExists(path, fileSystem, signal) {
168
+ if (fileSystem !== void 0) {
169
+ try {
170
+ const target = await fileSystem.resolve(path, signalOptions(signal));
171
+ signal?.throwIfAborted();
172
+ const info = await fileSystem.stat(target, signal);
173
+ signal?.throwIfAborted();
174
+ return info !== void 0;
175
+ } catch (error) {
176
+ signal?.throwIfAborted();
177
+ return false;
178
+ }
179
+ }
180
+ try {
181
+ await stat(path, signalOptions(signal));
182
+ return true;
183
+ } catch (error) {
184
+ signal?.throwIfAborted();
185
+ return false;
186
+ }
187
+ }
188
+
189
+ function nodeVersionSignature(info) {
190
+ return `${info.mtimeMs}:${info.size}`;
191
+ }
192
+
193
+ function isAbsentError(error) {
194
+ return typeof error === "object" && error !== null && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR" || error.code === "FS_NOT_FOUND" || error.code === "FS_NOT_DIRECTORY");
195
+ }