@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.
- package/README.md +22 -1
- package/browser-extension/artifacts/{gno-browser-clipper-v1.34.5.zip → gno-browser-clipper-v1.35.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.35.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +4 -1
- package/spec/cli.md +43 -0
- package/spec/output-schemas/mcp-job-status.schema.json +6 -2
- package/src/config/index.ts +4 -0
- package/src/config/types.ts +14 -0
- package/src/core/path-rules.ts +34 -0
- package/src/ingestion/index.ts +21 -0
- package/src/ingestion/record-container.ts +23 -1
- package/src/ingestion/source-availability/darwin-io.ts +295 -0
- package/src/ingestion/source-availability/darwin-path.ts +58 -0
- package/src/ingestion/source-availability/directory.ts +402 -0
- package/src/ingestion/source-availability/index.ts +74 -0
- package/src/ingestion/source-availability/readers.ts +360 -0
- package/src/ingestion/source-availability/resolve.ts +28 -0
- package/src/ingestion/source-availability/types.ts +170 -0
- package/src/ingestion/sync.ts +565 -108
- package/src/ingestion/types.ts +45 -3
- package/src/ingestion/walker.ts +263 -5
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/watch-reconciliation-fallback-disk.ts +359 -0
- package/src/serve/watch-reconciliation-fallback.ts +434 -0
- package/src/serve/watch-reconciliation-shared.ts +348 -0
- package/src/serve/watch-reconciliation.ts +129 -0
- package/src/serve/watch-service-events.ts +261 -0
- package/src/serve/watch-service-flush-generation.ts +140 -0
- package/src/serve/watch-service-flush-helpers.ts +147 -0
- package/src/serve/watch-service-flush.ts +443 -0
- package/src/serve/watch-service-hosts.ts +109 -0
- package/src/serve/watch-service-lifecycle.ts +221 -0
- package/src/serve/watch-service-run-flush.ts +236 -0
- package/src/serve/watch-service-snapshot.ts +125 -0
- package/src/serve/watch-service-state.ts +146 -0
- package/src/serve/watch-service.ts +266 -306
- package/src/serve/watch-snapshot-availability.ts +51 -0
- package/src/serve/watch-snapshot-handles.ts +365 -0
- package/src/serve/watch-snapshot-libc.ts +510 -0
- package/src/serve/watch-snapshot-ops.ts +541 -0
- package/src/serve/watch-snapshot-resolve.ts +246 -0
- package/src/serve/watch-snapshot-scan.ts +300 -0
- package/src/serve/watch-snapshot-types.ts +392 -0
- package/src/serve/watch-snapshot.ts +51 -0
- package/src/store/index.ts +1 -1
- package/src/store/sqlite/adapter.ts +191 -0
- package/src/store/types.ts +66 -0
- 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
|
+
}
|