@gmickel/gno 1.34.5 → 1.35.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 (49) hide show
  1. package/README.md +22 -1
  2. package/browser-extension/artifacts/{gno-browser-clipper-v1.34.5.zip → gno-browser-clipper-v1.35.0.zip} +0 -0
  3. package/browser-extension/artifacts/gno-browser-clipper-v1.35.0.zip.sha256 +1 -0
  4. package/browser-extension/dist/manifest.json +1 -1
  5. package/package.json +4 -1
  6. package/spec/cli.md +43 -0
  7. package/spec/output-schemas/mcp-job-status.schema.json +6 -2
  8. package/src/config/index.ts +4 -0
  9. package/src/config/types.ts +14 -0
  10. package/src/core/path-rules.ts +34 -0
  11. package/src/ingestion/index.ts +21 -0
  12. package/src/ingestion/record-container.ts +23 -1
  13. package/src/ingestion/source-availability/darwin-io.ts +295 -0
  14. package/src/ingestion/source-availability/darwin-path.ts +58 -0
  15. package/src/ingestion/source-availability/directory.ts +402 -0
  16. package/src/ingestion/source-availability/index.ts +74 -0
  17. package/src/ingestion/source-availability/readers.ts +360 -0
  18. package/src/ingestion/source-availability/resolve.ts +28 -0
  19. package/src/ingestion/source-availability/types.ts +170 -0
  20. package/src/ingestion/sync.ts +565 -108
  21. package/src/ingestion/types.ts +45 -3
  22. package/src/ingestion/walker.ts +263 -5
  23. package/src/serve/public/globals.built.css +1 -1
  24. package/src/serve/watch-reconciliation-fallback-disk.ts +359 -0
  25. package/src/serve/watch-reconciliation-fallback.ts +434 -0
  26. package/src/serve/watch-reconciliation-shared.ts +348 -0
  27. package/src/serve/watch-reconciliation.ts +129 -0
  28. package/src/serve/watch-service-events.ts +261 -0
  29. package/src/serve/watch-service-flush-generation.ts +140 -0
  30. package/src/serve/watch-service-flush-helpers.ts +147 -0
  31. package/src/serve/watch-service-flush.ts +443 -0
  32. package/src/serve/watch-service-hosts.ts +109 -0
  33. package/src/serve/watch-service-lifecycle.ts +221 -0
  34. package/src/serve/watch-service-run-flush.ts +236 -0
  35. package/src/serve/watch-service-snapshot.ts +125 -0
  36. package/src/serve/watch-service-state.ts +146 -0
  37. package/src/serve/watch-service.ts +266 -306
  38. package/src/serve/watch-snapshot-availability.ts +51 -0
  39. package/src/serve/watch-snapshot-handles.ts +365 -0
  40. package/src/serve/watch-snapshot-libc.ts +510 -0
  41. package/src/serve/watch-snapshot-ops.ts +541 -0
  42. package/src/serve/watch-snapshot-resolve.ts +246 -0
  43. package/src/serve/watch-snapshot-scan.ts +300 -0
  44. package/src/serve/watch-snapshot-types.ts +392 -0
  45. package/src/serve/watch-snapshot.ts +51 -0
  46. package/src/store/index.ts +1 -1
  47. package/src/store/sqlite/adapter.ts +191 -0
  48. package/src/store/types.ts +66 -0
  49. package/browser-extension/artifacts/gno-browser-clipper-v1.34.5.zip.sha256 +0 -1
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Source-availability boundaries shared by watcher snapshot operations.
3
+ *
4
+ * @module src/serve/watch-snapshot-availability
5
+ */
6
+
7
+ // node:path — Bun has no path join helper.
8
+ import { join } from "node:path";
9
+
10
+ import type { WatcherSnapshotFs } from "./watch-snapshot-types";
11
+
12
+ import {
13
+ isUnprovenDirectoryResult,
14
+ type DirectoryAvailabilityPort,
15
+ } from "../ingestion/source-availability";
16
+ import { readDirectChildren } from "./watch-snapshot-scan";
17
+
18
+ export async function directoryAllowsDescent(
19
+ rootAbs: string,
20
+ dirRel: string,
21
+ classifier: DirectoryAvailabilityPort | undefined
22
+ ): Promise<boolean> {
23
+ if (!classifier || classifier.mode === "any") {
24
+ return true;
25
+ }
26
+ const absPath = dirRel === "" ? rootAbs : join(rootAbs, dirRel);
27
+ const classified = await classifier.classify(absPath);
28
+ return !isUnprovenDirectoryResult(classified);
29
+ }
30
+
31
+ export async function readAvailableDirectory(
32
+ rootAbs: string,
33
+ dirRel: string,
34
+ fs: WatcherSnapshotFs,
35
+ maxEntries: number,
36
+ classifier: DirectoryAvailabilityPort | undefined
37
+ ): Promise<
38
+ Awaited<ReturnType<typeof readDirectChildren>> | { status: "unproven" }
39
+ > {
40
+ if (!classifier || classifier.mode === "any") {
41
+ return readDirectChildren(rootAbs, dirRel, fs, maxEntries);
42
+ }
43
+ if (!fs.readDirectChildrenSync) {
44
+ return { status: "unproven" };
45
+ }
46
+ const absPath = dirRel === "" ? rootAbs : join(rootAbs, dirRel);
47
+ const read = classifier.readDirectory(absPath, () =>
48
+ fs.readDirectChildrenSync!(rootAbs, dirRel, maxEntries)
49
+ );
50
+ return read.kind === "available" ? read.value : { status: "unproven" };
51
+ }
@@ -0,0 +1,365 @@
1
+ /**
2
+ * Production anchored directory handles (openat / fdopendir).
3
+ *
4
+ * Uses Bun FFI for fd-relative open/enumerate/no-follow metadata and node:fs
5
+ * structure ops for directory open + fstat confirmation. No new dependencies.
6
+ *
7
+ * Windows and runtimes without a safe anchored path report
8
+ * `supportsAnchoredHandles: false` so callers fall back rather than claiming
9
+ * strict no-follow.
10
+ *
11
+ * @module src/serve/watch-snapshot-handles
12
+ */
13
+
14
+ // node:fs — open/fstat/close with O_DIRECTORY|O_NOFOLLOW; no Bun equivalent
15
+ import { closeSync, fstatSync, openSync } from "node:fs";
16
+ // node:path — Bun has no path utilities
17
+ import { join } from "node:path";
18
+
19
+ import type { LoadedLibc } from "./watch-snapshot-libc";
20
+ import type {
21
+ SnapshotEntryFingerprint,
22
+ WatcherDirHandle,
23
+ WatcherSnapshotFs,
24
+ WatcherSnapshotStat,
25
+ } from "./watch-snapshot-types";
26
+
27
+ import {
28
+ loadLibc,
29
+ openatOrThrow,
30
+ readDirNames,
31
+ statatNoFollowOrThrow,
32
+ } from "./watch-snapshot-libc";
33
+ import { fingerprintFromStat, isMissingFsError } from "./watch-snapshot-types";
34
+
35
+ type NativeDir = {
36
+ fd: number;
37
+ };
38
+
39
+ const nativeHandles = new WeakMap<object, NativeDir>();
40
+
41
+ function asHandle(native: NativeDir): WatcherDirHandle {
42
+ const handle = {} as WatcherDirHandle;
43
+ nativeHandles.set(handle as object, native);
44
+ return handle;
45
+ }
46
+
47
+ function requireNative(handle: WatcherDirHandle): NativeDir {
48
+ const native = nativeHandles.get(handle as object);
49
+ if (!native) {
50
+ throw Object.assign(new Error("Invalid or closed directory handle"), {
51
+ code: "EBADF",
52
+ });
53
+ }
54
+ return native;
55
+ }
56
+
57
+ function openNativeDirByRel(
58
+ libc: LoadedLibc,
59
+ rootAbs: string,
60
+ dirRel: string
61
+ ): number {
62
+ let fd = openSync(rootAbs, libc.openDirFlags);
63
+ try {
64
+ for (const segment of dirRel.split("/").filter(Boolean)) {
65
+ const child = openatOrThrow(
66
+ libc,
67
+ fd,
68
+ segment,
69
+ libc.openChildFlags,
70
+ "openat"
71
+ );
72
+ closeSync(fd);
73
+ fd = child;
74
+ }
75
+ return fd;
76
+ } catch (cause) {
77
+ closeSync(fd);
78
+ throw cause;
79
+ }
80
+ }
81
+
82
+ function readDirectChildrenNative(
83
+ libc: LoadedLibc,
84
+ rootAbs: string,
85
+ dirRel: string,
86
+ maxEntries: number
87
+ ): ReturnType<NonNullable<WatcherSnapshotFs["readDirectChildrenSync"]>> {
88
+ if (!Number.isInteger(maxEntries) || maxEntries < 0) {
89
+ return {
90
+ status: "scan_failed",
91
+ cause: new Error("maxEntries must be a non-negative integer"),
92
+ };
93
+ }
94
+
95
+ let fd: number;
96
+ try {
97
+ fd = openNativeDirByRel(libc, rootAbs, dirRel);
98
+ } catch (cause) {
99
+ return isMissingFsError(cause)
100
+ ? { status: "missing" }
101
+ : { status: "scan_failed", cause };
102
+ }
103
+
104
+ try {
105
+ const listed = readDirNames(libc, fd, maxEntries);
106
+ if (listed.status === "overflow") {
107
+ return listed;
108
+ }
109
+ const entries = new Map<string, SnapshotEntryFingerprint>();
110
+ listed.names.sort((left, right) =>
111
+ left < right ? -1 : left > right ? 1 : 0
112
+ );
113
+ for (const name of listed.names) {
114
+ if (name.includes("/") || name.includes("\\") || name.includes("\0")) {
115
+ return {
116
+ status: "scan_failed",
117
+ cause: new Error(`Invalid directory entry name: ${name}`),
118
+ };
119
+ }
120
+ if (entries.size >= maxEntries) {
121
+ return { status: "overflow" };
122
+ }
123
+ try {
124
+ const fingerprinted = fingerprintFromStat(
125
+ statatNoFollowOrThrow(libc, fd, name)
126
+ );
127
+ if (!fingerprinted.ok) {
128
+ return { status: "unreliable_metadata" };
129
+ }
130
+ entries.set(name, fingerprinted.fingerprint);
131
+ } catch (cause) {
132
+ return { status: "scan_failed", cause };
133
+ }
134
+ }
135
+ return { status: "present", entries };
136
+ } catch (cause) {
137
+ return isMissingFsError(cause)
138
+ ? { status: "missing" }
139
+ : { status: "scan_failed", cause };
140
+ } finally {
141
+ closeSync(fd);
142
+ }
143
+ }
144
+
145
+ function lstatChildByRelNative(
146
+ libc: LoadedLibc,
147
+ rootAbs: string,
148
+ parentRel: string,
149
+ name: string
150
+ ): WatcherSnapshotStat {
151
+ const parentFd = openNativeDirByRel(libc, rootAbs, parentRel);
152
+ try {
153
+ return statatNoFollowOrThrow(libc, parentFd, name);
154
+ } finally {
155
+ closeSync(parentFd);
156
+ }
157
+ }
158
+
159
+ function createNativeAnchoredFs(libc: LoadedLibc): WatcherSnapshotFs {
160
+ return {
161
+ supportsAnchoredHandles: true,
162
+
163
+ readDirectChildrenSync: (rootAbs, dirRel, maxEntries) =>
164
+ readDirectChildrenNative(libc, rootAbs, dirRel, maxEntries),
165
+
166
+ lstatChildByRelSync: (rootAbs, parentRel, name) =>
167
+ lstatChildByRelNative(libc, rootAbs, parentRel, name),
168
+
169
+ async openDir(absPath: string): Promise<WatcherDirHandle> {
170
+ const fd = openSync(absPath, libc.openDirFlags);
171
+ return asHandle({ fd });
172
+ },
173
+
174
+ async readDir(handle: WatcherDirHandle, maxNames: number) {
175
+ const native = requireNative(handle);
176
+ return readDirNames(libc, native.fd, maxNames);
177
+ },
178
+
179
+ async lstatChild(
180
+ handle: WatcherDirHandle,
181
+ name: string
182
+ ): Promise<WatcherSnapshotStat> {
183
+ const native = requireNative(handle);
184
+ return statatNoFollowOrThrow(libc, native.fd, name);
185
+ },
186
+
187
+ async openChildDir(
188
+ handle: WatcherDirHandle,
189
+ name: string
190
+ ): Promise<WatcherDirHandle> {
191
+ const native = requireNative(handle);
192
+ const fd = openatOrThrow(
193
+ libc,
194
+ native.fd,
195
+ name,
196
+ libc.openChildFlags,
197
+ "openat"
198
+ );
199
+ // Confirm real directory (O_DIRECTORY should enforce; double-check kind).
200
+ try {
201
+ const stat = fstatSync(fd, { bigint: true });
202
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
203
+ closeSync(fd);
204
+ throw Object.assign(
205
+ new Error(`Expected real directory child: ${name}`),
206
+ { code: "ENOTDIR" }
207
+ );
208
+ }
209
+ } catch (cause) {
210
+ try {
211
+ closeSync(fd);
212
+ } catch {
213
+ // ignore close errors when fstat failed
214
+ }
215
+ throw cause;
216
+ }
217
+ return asHandle({ fd });
218
+ },
219
+
220
+ async closeDir(handle: WatcherDirHandle): Promise<void> {
221
+ const native = nativeHandles.get(handle as object);
222
+ if (!native) {
223
+ return;
224
+ }
225
+ nativeHandles.delete(handle as object);
226
+ try {
227
+ closeSync(native.fd);
228
+ } catch (cause) {
229
+ if (!isMissingFsError(cause)) {
230
+ // EBADF after double-close is fine; surface others.
231
+ const code =
232
+ cause && typeof cause === "object" && "code" in cause
233
+ ? String(cause.code)
234
+ : "";
235
+ if (code !== "EBADF") {
236
+ throw cause;
237
+ }
238
+ }
239
+ }
240
+ },
241
+ };
242
+ }
243
+
244
+ function createUnsupportedFs(reason: string): WatcherSnapshotFs {
245
+ const fail = async (): Promise<never> => {
246
+ throw Object.assign(new Error(reason), { code: "ENOTSUP" });
247
+ };
248
+ return {
249
+ supportsAnchoredHandles: false,
250
+ openDir: fail,
251
+ readDir: fail,
252
+ lstatChild: fail,
253
+ openChildDir: fail,
254
+ closeDir: async () => undefined,
255
+ };
256
+ }
257
+
258
+ /**
259
+ * Production filesystem adapter.
260
+ * Unix + working libc FFI → anchored handles; otherwise explicit unsupported.
261
+ */
262
+ export function createDefaultWatcherFs(): WatcherSnapshotFs {
263
+ if (process.platform === "win32") {
264
+ return createUnsupportedFs(
265
+ "Anchored no-follow directory handles are not available on Windows; watcher uses fallback reconciliation"
266
+ );
267
+ }
268
+ const libc = loadLibc();
269
+ if (!libc) {
270
+ return createUnsupportedFs(
271
+ "Anchored no-follow directory handles unavailable on this runtime"
272
+ );
273
+ }
274
+ return createNativeAnchoredFs(libc);
275
+ }
276
+
277
+ /**
278
+ * Path-backed handle adapter for deterministic unit tests.
279
+ * NOT production-safe against TOCTOU path swaps — tests that need race
280
+ * coverage must inject genuine pin-by-identity handles.
281
+ */
282
+ export function createPathBackedWatcherFs(ops: {
283
+ readdir(absPath: string): Promise<string[]>;
284
+ lstat(absPath: string): Promise<WatcherSnapshotStat>;
285
+ }): WatcherSnapshotFs {
286
+ type PathHandle = { absPath: string };
287
+ const table = new WeakMap<object, PathHandle>();
288
+
289
+ const wrap = (absPath: string): WatcherDirHandle => {
290
+ const handle = {} as WatcherDirHandle;
291
+ table.set(handle as object, { absPath });
292
+ return handle;
293
+ };
294
+
295
+ const unwrap = (handle: WatcherDirHandle): PathHandle => {
296
+ const value = table.get(handle as object);
297
+ if (!value) {
298
+ throw Object.assign(new Error("Invalid or closed directory handle"), {
299
+ code: "EBADF",
300
+ });
301
+ }
302
+ return value;
303
+ };
304
+
305
+ return {
306
+ supportsAnchoredHandles: true,
307
+
308
+ async openDir(absPath: string): Promise<WatcherDirHandle> {
309
+ const stat = await ops.lstat(absPath);
310
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
311
+ throw Object.assign(new Error(`Not a real directory: ${absPath}`), {
312
+ code: "ENOTDIR",
313
+ });
314
+ }
315
+ return wrap(absPath);
316
+ },
317
+
318
+ async readDir(handle: WatcherDirHandle, maxNames: number) {
319
+ if (!Number.isInteger(maxNames) || maxNames < 0) {
320
+ throw Object.assign(
321
+ new Error("maxNames must be a non-negative integer"),
322
+ { code: "EINVAL" }
323
+ );
324
+ }
325
+ const listed = await ops.readdir(unwrap(handle).absPath);
326
+ const names: string[] = [];
327
+ for (const name of listed) {
328
+ if (name === "" || name === "." || name === "..") {
329
+ continue;
330
+ }
331
+ // Cap storage at maxNames; the next name is overflow-only evidence.
332
+ if (names.length >= maxNames) {
333
+ return { status: "overflow" as const };
334
+ }
335
+ names.push(name);
336
+ }
337
+ return { status: "ok" as const, names };
338
+ },
339
+
340
+ async lstatChild(
341
+ handle: WatcherDirHandle,
342
+ name: string
343
+ ): Promise<WatcherSnapshotStat> {
344
+ return ops.lstat(join(unwrap(handle).absPath, name));
345
+ },
346
+
347
+ async openChildDir(
348
+ handle: WatcherDirHandle,
349
+ name: string
350
+ ): Promise<WatcherDirHandle> {
351
+ const absPath = join(unwrap(handle).absPath, name);
352
+ const stat = await ops.lstat(absPath);
353
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
354
+ throw Object.assign(new Error(`Not a real directory: ${absPath}`), {
355
+ code: "ENOTDIR",
356
+ });
357
+ }
358
+ return wrap(absPath);
359
+ },
360
+
361
+ async closeDir(handle: WatcherDirHandle): Promise<void> {
362
+ table.delete(handle as object);
363
+ },
364
+ };
365
+ }