@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,359 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* No-follow disk enumeration for watcher fallback classification.
|
|
3
|
+
* Platforms without genuine anchored handles fail closed — no path-based TOCTOU
|
|
4
|
+
* fallback in production (createPathBackedWatcherFs is test-only).
|
|
5
|
+
*
|
|
6
|
+
* @module src/serve/watch-reconciliation-fallback-disk
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// node:path — Bun has no path join helper.
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
|
|
12
|
+
import type { Collection } from "../config/types";
|
|
13
|
+
import type { DirectoryAvailabilityPort } from "../ingestion/source-availability";
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
matchesCollectionExclusion,
|
|
17
|
+
matchesCollectionSubtreeExclusion,
|
|
18
|
+
} from "../core/path-rules";
|
|
19
|
+
import { collectionToWalkConfig, matchesWalkPath } from "../ingestion";
|
|
20
|
+
import { findUnprovenAvailabilityPrefix } from "../ingestion/source-availability";
|
|
21
|
+
import { defaultFs, openDirByRel } from "./watch-snapshot-scan";
|
|
22
|
+
import {
|
|
23
|
+
isMissingFsError,
|
|
24
|
+
joinWatcherRelPath,
|
|
25
|
+
parentWatcherDir,
|
|
26
|
+
type WatcherSnapshotFs,
|
|
27
|
+
} from "./watch-snapshot-types";
|
|
28
|
+
|
|
29
|
+
export interface FallbackBudget {
|
|
30
|
+
readonly limit: number;
|
|
31
|
+
visitedDirs: number;
|
|
32
|
+
candidates: number;
|
|
33
|
+
removals: number;
|
|
34
|
+
dirtyDirs: number;
|
|
35
|
+
storeRows: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function budgetExceeded(b: FallbackBudget): boolean {
|
|
39
|
+
return (
|
|
40
|
+
b.visitedDirs > b.limit ||
|
|
41
|
+
b.candidates > b.limit ||
|
|
42
|
+
b.removals > b.limit ||
|
|
43
|
+
b.dirtyDirs > b.limit ||
|
|
44
|
+
b.storeRows > b.limit
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Production fallback FS: anchored handles only. Unsupported platforms surface
|
|
50
|
+
* scan_failed/ENOTSUP via openDirByRel — never claim path-based safety.
|
|
51
|
+
*/
|
|
52
|
+
export function fallbackFs(): WatcherSnapshotFs {
|
|
53
|
+
return defaultFs;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type DiskListResult =
|
|
57
|
+
| {
|
|
58
|
+
status: "ok";
|
|
59
|
+
paths: string[];
|
|
60
|
+
rootDirNames: string[];
|
|
61
|
+
unprovenPrefixes: string[];
|
|
62
|
+
}
|
|
63
|
+
| { status: "overflow" }
|
|
64
|
+
| { status: "error"; cause: unknown };
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Bounded no-follow BFS under `dirRel`. Stops enumeration at remaining+1 names.
|
|
68
|
+
* Never descends through symlinks.
|
|
69
|
+
*/
|
|
70
|
+
export async function listEligibleDiskSources(
|
|
71
|
+
rootAbs: string,
|
|
72
|
+
dirRel: string,
|
|
73
|
+
collection: Collection,
|
|
74
|
+
fs: WatcherSnapshotFs,
|
|
75
|
+
budget: FallbackBudget,
|
|
76
|
+
directoryAvailability?: DirectoryAvailabilityPort
|
|
77
|
+
): Promise<DiskListResult> {
|
|
78
|
+
const walkConfig = collectionToWalkConfig(collection, 0);
|
|
79
|
+
const paths: string[] = [];
|
|
80
|
+
const rootDirNames: string[] = [];
|
|
81
|
+
const unprovenPrefixes: string[] = [];
|
|
82
|
+
const queue: string[] = [dirRel];
|
|
83
|
+
let head = 0;
|
|
84
|
+
|
|
85
|
+
while (head < queue.length) {
|
|
86
|
+
const current = queue[head] as string;
|
|
87
|
+
head += 1;
|
|
88
|
+
if (
|
|
89
|
+
current !== "" &&
|
|
90
|
+
matchesCollectionSubtreeExclusion(current, walkConfig.exclude)
|
|
91
|
+
) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
budget.visitedDirs += 1;
|
|
95
|
+
if (budgetExceeded(budget)) {
|
|
96
|
+
return { status: "overflow" };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const remaining = Math.max(0, budget.limit - budget.visitedDirs + 1);
|
|
100
|
+
if (directoryAvailability?.mode === "local") {
|
|
101
|
+
if (!fs.readDirectChildrenSync) {
|
|
102
|
+
unprovenPrefixes.push(current);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const absPath = current === "" ? rootAbs : join(rootAbs, current);
|
|
106
|
+
const guarded = directoryAvailability.readDirectory(absPath, () =>
|
|
107
|
+
fs.readDirectChildrenSync!(rootAbs, current, remaining)
|
|
108
|
+
);
|
|
109
|
+
if (guarded.kind !== "available") {
|
|
110
|
+
unprovenPrefixes.push(current);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const listed = guarded.value;
|
|
114
|
+
if (listed.status === "missing") {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (listed.status === "overflow") {
|
|
118
|
+
return { status: "overflow" };
|
|
119
|
+
}
|
|
120
|
+
if (listed.status !== "present") {
|
|
121
|
+
return {
|
|
122
|
+
status: "error",
|
|
123
|
+
cause:
|
|
124
|
+
listed.status === "scan_failed"
|
|
125
|
+
? listed.cause
|
|
126
|
+
: new Error(`Disk scan failed under ${current || "."}`),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
for (const [name, fingerprint] of listed.entries) {
|
|
130
|
+
const childRel = joinWatcherRelPath(current, name);
|
|
131
|
+
if (fingerprint.kind === "directory") {
|
|
132
|
+
if (matchesCollectionSubtreeExclusion(childRel, walkConfig.exclude)) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (current === "") {
|
|
136
|
+
rootDirNames.push(name);
|
|
137
|
+
}
|
|
138
|
+
queue.push(childRel);
|
|
139
|
+
} else if (matchesCollectionExclusion(childRel, walkConfig.exclude)) {
|
|
140
|
+
continue;
|
|
141
|
+
} else if (fingerprint.kind === "symlink") {
|
|
142
|
+
if (matchesWalkPath(childRel, walkConfig)) {
|
|
143
|
+
paths.push(childRel);
|
|
144
|
+
}
|
|
145
|
+
} else if (
|
|
146
|
+
fingerprint.kind === "file" &&
|
|
147
|
+
matchesWalkPath(childRel, walkConfig)
|
|
148
|
+
) {
|
|
149
|
+
paths.push(childRel);
|
|
150
|
+
}
|
|
151
|
+
if (paths.length > budget.limit) {
|
|
152
|
+
return { status: "overflow" };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const scanCurrent = async (): Promise<
|
|
159
|
+
| { status: "ok" }
|
|
160
|
+
| { status: "overflow" }
|
|
161
|
+
| { status: "error"; cause: unknown }
|
|
162
|
+
> => {
|
|
163
|
+
const opened = await openDirByRel(rootAbs, current, fs);
|
|
164
|
+
if (opened.status === "missing") {
|
|
165
|
+
return { status: "ok" };
|
|
166
|
+
}
|
|
167
|
+
if (opened.status !== "ok") {
|
|
168
|
+
return {
|
|
169
|
+
status: "error",
|
|
170
|
+
cause:
|
|
171
|
+
opened.status === "scan_failed"
|
|
172
|
+
? opened.cause
|
|
173
|
+
: new Error(`Disk scan failed under ${current || "."}`),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
const listed = await fs.readDir(opened.handle, remaining);
|
|
179
|
+
if (listed.status === "overflow") {
|
|
180
|
+
return { status: "overflow" };
|
|
181
|
+
}
|
|
182
|
+
const names = [...listed.names].sort((a, b) =>
|
|
183
|
+
a < b ? -1 : a > b ? 1 : 0
|
|
184
|
+
);
|
|
185
|
+
for (const name of names) {
|
|
186
|
+
if (name === "" || name === "." || name === "..") {
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (
|
|
190
|
+
name.includes("/") ||
|
|
191
|
+
name.includes("\\") ||
|
|
192
|
+
name.includes("\0")
|
|
193
|
+
) {
|
|
194
|
+
return {
|
|
195
|
+
status: "error",
|
|
196
|
+
cause: new Error(`Invalid directory entry name: ${name}`),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const stat = await fs.lstatChild(opened.handle, name);
|
|
201
|
+
const childRel = joinWatcherRelPath(current, name);
|
|
202
|
+
if (stat.isDirectory()) {
|
|
203
|
+
if (
|
|
204
|
+
matchesCollectionSubtreeExclusion(childRel, walkConfig.exclude)
|
|
205
|
+
) {
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (current === "") {
|
|
209
|
+
rootDirNames.push(name);
|
|
210
|
+
}
|
|
211
|
+
queue.push(childRel);
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (matchesCollectionExclusion(childRel, walkConfig.exclude)) {
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (stat.isSymbolicLink()) {
|
|
218
|
+
if (matchesWalkPath(childRel, walkConfig)) {
|
|
219
|
+
paths.push(childRel);
|
|
220
|
+
if (paths.length > budget.limit) {
|
|
221
|
+
return { status: "overflow" };
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (!stat.isFile() || !matchesWalkPath(childRel, walkConfig)) {
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
paths.push(childRel);
|
|
230
|
+
if (paths.length > budget.limit) {
|
|
231
|
+
return { status: "overflow" };
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return { status: "ok" };
|
|
235
|
+
} catch (cause) {
|
|
236
|
+
return { status: "error", cause };
|
|
237
|
+
} finally {
|
|
238
|
+
await fs.closeDir(opened.handle);
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const scanned = await scanCurrent();
|
|
243
|
+
if (scanned.status !== "ok") {
|
|
244
|
+
return scanned;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return { status: "ok", paths, rootDirNames, unprovenPrefixes };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export async function inspectNoFollowPresence(
|
|
252
|
+
rootAbs: string,
|
|
253
|
+
relPath: string,
|
|
254
|
+
fs: WatcherSnapshotFs,
|
|
255
|
+
directoryAvailability?: DirectoryAvailabilityPort
|
|
256
|
+
): Promise<
|
|
257
|
+
| { status: "present"; indexable: boolean }
|
|
258
|
+
| { status: "missing" }
|
|
259
|
+
| { status: "error"; cause: unknown }
|
|
260
|
+
> {
|
|
261
|
+
if (directoryAvailability?.mode === "local") {
|
|
262
|
+
const unproven = await findUnprovenAvailabilityPrefix(
|
|
263
|
+
rootAbs,
|
|
264
|
+
relPath,
|
|
265
|
+
directoryAvailability
|
|
266
|
+
);
|
|
267
|
+
if (unproven) {
|
|
268
|
+
return {
|
|
269
|
+
status: "error",
|
|
270
|
+
cause: new Error(
|
|
271
|
+
`Source absence is unproven under ${unproven.relPath || "."}: ${unproven.code}`
|
|
272
|
+
),
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
const parent = parentWatcherDir(relPath);
|
|
277
|
+
if (parent === null) {
|
|
278
|
+
return { status: "missing" };
|
|
279
|
+
}
|
|
280
|
+
const base = parent === "" ? relPath : relPath.slice(parent.length + 1);
|
|
281
|
+
if (base === "" || base.includes("/")) {
|
|
282
|
+
return { status: "error", cause: new Error(`Invalid path: ${relPath}`) };
|
|
283
|
+
}
|
|
284
|
+
const inspectParent = async (): Promise<
|
|
285
|
+
| { status: "present"; indexable: boolean }
|
|
286
|
+
| { status: "missing" }
|
|
287
|
+
| { status: "error"; cause: unknown }
|
|
288
|
+
> => {
|
|
289
|
+
const opened = await openDirByRel(rootAbs, parent, fs);
|
|
290
|
+
if (opened.status === "missing") {
|
|
291
|
+
return { status: "missing" };
|
|
292
|
+
}
|
|
293
|
+
if (opened.status !== "ok") {
|
|
294
|
+
return {
|
|
295
|
+
status: "error",
|
|
296
|
+
cause:
|
|
297
|
+
opened.status === "scan_failed"
|
|
298
|
+
? opened.cause
|
|
299
|
+
: new Error("Failed to open parent for presence check"),
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
try {
|
|
303
|
+
const stat = await fs.lstatChild(opened.handle, base);
|
|
304
|
+
// Only regular files / symlinks are indexable sources. Directory, FIFO,
|
|
305
|
+
// device, and other specials prove the prior file source is gone.
|
|
306
|
+
const indexable = stat.isFile() || stat.isSymbolicLink();
|
|
307
|
+
return { status: "present", indexable };
|
|
308
|
+
} catch (cause) {
|
|
309
|
+
if (isMissingFsError(cause)) {
|
|
310
|
+
return { status: "missing" };
|
|
311
|
+
}
|
|
312
|
+
return { status: "error", cause };
|
|
313
|
+
} finally {
|
|
314
|
+
await fs.closeDir(opened.handle);
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
if (directoryAvailability?.mode === "local") {
|
|
319
|
+
if (!fs.lstatChildByRelSync) {
|
|
320
|
+
return {
|
|
321
|
+
status: "error",
|
|
322
|
+
cause: new Error(
|
|
323
|
+
"Synchronous anchored child metadata is unavailable in local mode"
|
|
324
|
+
),
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
const parentAbs = parent === "" ? rootAbs : join(rootAbs, parent);
|
|
328
|
+
const guarded = directoryAvailability.readDirectory(parentAbs, () => {
|
|
329
|
+
try {
|
|
330
|
+
return {
|
|
331
|
+
status: "present" as const,
|
|
332
|
+
stat: fs.lstatChildByRelSync!(rootAbs, parent, base),
|
|
333
|
+
};
|
|
334
|
+
} catch (cause) {
|
|
335
|
+
if (isMissingFsError(cause)) {
|
|
336
|
+
return { status: "missing" as const };
|
|
337
|
+
}
|
|
338
|
+
throw cause;
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
if (guarded.kind !== "available") {
|
|
342
|
+
return {
|
|
343
|
+
status: "error",
|
|
344
|
+
cause: new Error(
|
|
345
|
+
`Source absence is unproven under ${parent || "."}: ${guarded.code}`
|
|
346
|
+
),
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
if (guarded.value.status === "missing") {
|
|
350
|
+
return { status: "missing" };
|
|
351
|
+
}
|
|
352
|
+
return {
|
|
353
|
+
status: "present",
|
|
354
|
+
indexable:
|
|
355
|
+
guarded.value.stat.isFile() || guarded.value.stat.isSymbolicLink(),
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
return inspectParent();
|
|
359
|
+
}
|