@nklisch/pi-fff-compat 0.1.1 → 0.1.3
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.
|
@@ -26,6 +26,7 @@ import { statSync } from "node:fs";
|
|
|
26
26
|
import path from "node:path";
|
|
27
27
|
import { Type } from "@earendil-works/pi-ai";
|
|
28
28
|
import { type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
29
|
+
import { createGenerationGuardedFinderLifecycle } from "./finder-lifecycle.js";
|
|
29
30
|
|
|
30
31
|
const EXTENSION_NAME = "pi-fff-compat";
|
|
31
32
|
const PACKAGE_HINT = "bundled dependency of @nklisch/pi-fff-compat";
|
|
@@ -154,9 +155,6 @@ type Truncation = {
|
|
|
154
155
|
};
|
|
155
156
|
|
|
156
157
|
let fffModulePromise: Promise<FffModule> | null = null;
|
|
157
|
-
let finder: FffFinder | null = null;
|
|
158
|
-
let finderCwd: string | null = null;
|
|
159
|
-
let finderPromise: { cwd: string; promise: Promise<FffFinder> } | null = null;
|
|
160
158
|
let activeCwd = process.cwd();
|
|
161
159
|
|
|
162
160
|
function envFlagEnabled(value: string | undefined): boolean {
|
|
@@ -195,7 +193,7 @@ function loadFffModule(): Promise<FffModule> {
|
|
|
195
193
|
if (!fffModulePromise) {
|
|
196
194
|
fffModulePromise = (async () => {
|
|
197
195
|
try {
|
|
198
|
-
return (await import("@ff-labs/fff-node")) as FffModule;
|
|
196
|
+
return (await import("@ff-labs/fff-node")) as unknown as FffModule;
|
|
199
197
|
} catch (error) {
|
|
200
198
|
throw new Error(
|
|
201
199
|
`Failed to load @ff-labs/fff-node (${PACKAGE_HINT}): ${error instanceof Error ? error.message : String(error)}`,
|
|
@@ -206,44 +204,38 @@ function loadFffModule(): Promise<FffModule> {
|
|
|
206
204
|
return fffModulePromise;
|
|
207
205
|
}
|
|
208
206
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
historyDbPath: process.env.FFF_HISTORY_DB,
|
|
222
|
-
aiMode: true,
|
|
223
|
-
enableHomeDirScanning: envFlagEnabled(process.env[HOME_SCAN_ENV]),
|
|
224
|
-
enableFsRootScanning: envFlagEnabled(process.env.FFF_ENABLE_ROOT_SCAN),
|
|
225
|
-
disableWatch: envFlagEnabled(process.env[DISABLE_WATCH_ENV]),
|
|
226
|
-
});
|
|
227
|
-
if (!created.ok) throw new Error(`Failed to create FFF finder: ${created.error}`);
|
|
228
|
-
|
|
229
|
-
finder = created.value;
|
|
230
|
-
finderCwd = cwd;
|
|
231
|
-
|
|
232
|
-
const scan = await finder.waitForScan(INITIAL_SCAN_WAIT_MS);
|
|
233
|
-
if (!scan.ok) throw new Error(`FFF scan failed: ${scan.error}`);
|
|
234
|
-
return finder;
|
|
235
|
-
})().finally(() => {
|
|
236
|
-
finderPromise = null;
|
|
237
|
-
}),
|
|
238
|
-
};
|
|
207
|
+
const finderLifecycle = createGenerationGuardedFinderLifecycle<FffFinder>(async (cwd) => {
|
|
208
|
+
const { FileFinder } = await loadFffModule();
|
|
209
|
+
const created = FileFinder.create({
|
|
210
|
+
basePath: cwd,
|
|
211
|
+
frecencyDbPath: process.env.FFF_FRECENCY_DB,
|
|
212
|
+
historyDbPath: process.env.FFF_HISTORY_DB,
|
|
213
|
+
aiMode: true,
|
|
214
|
+
enableHomeDirScanning: envFlagEnabled(process.env[HOME_SCAN_ENV]),
|
|
215
|
+
enableFsRootScanning: envFlagEnabled(process.env.FFF_ENABLE_ROOT_SCAN),
|
|
216
|
+
disableWatch: envFlagEnabled(process.env[DISABLE_WATCH_ENV]),
|
|
217
|
+
});
|
|
218
|
+
if (!created.ok) throw new Error(`Failed to create FFF finder: ${created.error}`);
|
|
239
219
|
|
|
240
|
-
|
|
241
|
-
|
|
220
|
+
const candidate = created.value;
|
|
221
|
+
try {
|
|
222
|
+
const scan = await candidate.waitForScan(INITIAL_SCAN_WAIT_MS);
|
|
223
|
+
if (!scan.ok) throw new Error(`FFF scan failed: ${scan.error}`);
|
|
224
|
+
return candidate;
|
|
225
|
+
} catch (error) {
|
|
226
|
+
if (!candidate.isDestroyed) {
|
|
227
|
+
try {
|
|
228
|
+
candidate.destroy();
|
|
229
|
+
} catch {
|
|
230
|
+
// Preserve the scan/create failure; lifecycle cleanup is best effort.
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
throw error;
|
|
234
|
+
}
|
|
235
|
+
});
|
|
242
236
|
|
|
243
|
-
function
|
|
244
|
-
|
|
245
|
-
finder = null;
|
|
246
|
-
finderCwd = null;
|
|
237
|
+
function ensureFinder(cwd: string): Promise<FffFinder> {
|
|
238
|
+
return finderLifecycle.ensure(cwd);
|
|
247
239
|
}
|
|
248
240
|
|
|
249
241
|
function truncateLine(line: string): { text: string; wasTruncated: boolean } {
|
|
@@ -408,6 +400,21 @@ const findSchema = Type.Object({
|
|
|
408
400
|
limit: Type.Optional(Type.Number({ description: `Maximum number of results (default ${DEFAULT_FIND_LIMIT})` })),
|
|
409
401
|
});
|
|
410
402
|
|
|
403
|
+
function reportInitializationFailure(ctx: ExtensionContext, error: unknown): void {
|
|
404
|
+
let detail = "unknown failure";
|
|
405
|
+
try { detail = error instanceof Error ? error.message : String(error); }
|
|
406
|
+
catch { detail = "unreadable failure"; }
|
|
407
|
+
const message = `${EXTENSION_NAME} init failed: ${detail}`;
|
|
408
|
+
try {
|
|
409
|
+
ctx.ui.notify(message, "error");
|
|
410
|
+
} catch {
|
|
411
|
+
// Initialization can settle after session replacement. The old UI is then
|
|
412
|
+
// intentionally stale, so fall back to a process diagnostic without
|
|
413
|
+
// allowing either reporting sink to escape the awaited host boundary.
|
|
414
|
+
try { console.error(message); } catch { /* no safe reporting sink remains */ }
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
411
418
|
const grepSchema = Type.Object({
|
|
412
419
|
pattern: Type.String({ description: "Search pattern" }),
|
|
413
420
|
path: Type.Optional(Type.String({ description: "Directory or file to search in (default: current directory)" })),
|
|
@@ -449,12 +456,12 @@ export default function fffCompatSearch(pi: ExtensionAPI) {
|
|
|
449
456
|
try {
|
|
450
457
|
await ensureFinder(activeCwd);
|
|
451
458
|
} catch (error) {
|
|
452
|
-
ctx
|
|
459
|
+
reportInitializationFailure(ctx, error);
|
|
453
460
|
}
|
|
454
461
|
});
|
|
455
462
|
|
|
456
463
|
pi.on("session_shutdown", async () => {
|
|
457
|
-
|
|
464
|
+
finderLifecycle.revoke();
|
|
458
465
|
});
|
|
459
466
|
|
|
460
467
|
pi.registerTool({
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
export interface FinderLike {
|
|
2
|
+
readonly isDestroyed: boolean;
|
|
3
|
+
destroy(): void;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
interface CurrentFinder<F extends FinderLike> {
|
|
7
|
+
cwd: string;
|
|
8
|
+
generation: number;
|
|
9
|
+
finder: F;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface PendingFinder<F extends FinderLike> {
|
|
13
|
+
cwd: string;
|
|
14
|
+
generation: number;
|
|
15
|
+
promise: Promise<F>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Own the asynchronous finder lifecycle separately from the Pi event seam.
|
|
20
|
+
* A scan can outlive session_shutdown; the generation check prevents that late
|
|
21
|
+
* completion from becoming the finder for a replacement session.
|
|
22
|
+
*/
|
|
23
|
+
export function createGenerationGuardedFinderLifecycle<F extends FinderLike>(
|
|
24
|
+
createFinder: (cwd: string) => Promise<F>,
|
|
25
|
+
): {
|
|
26
|
+
ensure(cwd: string): Promise<F>;
|
|
27
|
+
revoke(): void;
|
|
28
|
+
} {
|
|
29
|
+
let generation = 0;
|
|
30
|
+
let current: CurrentFinder<F> | null = null;
|
|
31
|
+
let pending: PendingFinder<F> | null = null;
|
|
32
|
+
|
|
33
|
+
function destroyQuietly(candidate: F | null): void {
|
|
34
|
+
if (candidate === null || candidate.isDestroyed) return;
|
|
35
|
+
// A stale finder has no live session context in which to report a cleanup
|
|
36
|
+
// failure. Do not let best-effort shutdown cleanup reject the host event.
|
|
37
|
+
try {
|
|
38
|
+
candidate.destroy();
|
|
39
|
+
} catch {
|
|
40
|
+
// The lifecycle is already revoked; there is no safe session state to
|
|
41
|
+
// repopulate, which is the guarantee this cleanup protects.
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function revoke(): void {
|
|
46
|
+
generation++;
|
|
47
|
+
const old = current;
|
|
48
|
+
current = null;
|
|
49
|
+
destroyQuietly(old?.finder ?? null);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function ensure(cwd: string): Promise<F> {
|
|
53
|
+
if (current?.finder.isDestroyed) current = null;
|
|
54
|
+
if (current?.cwd === cwd) return Promise.resolve(current.finder);
|
|
55
|
+
if (pending?.cwd === cwd && pending.generation === generation) return pending.promise;
|
|
56
|
+
|
|
57
|
+
// A different workspace, or a new session after revocation, invalidates
|
|
58
|
+
// every earlier initialization. Its late result will be destroyed below.
|
|
59
|
+
generation++;
|
|
60
|
+
const requestedGeneration = generation;
|
|
61
|
+
const old = current;
|
|
62
|
+
current = null;
|
|
63
|
+
destroyQuietly(old?.finder ?? null);
|
|
64
|
+
|
|
65
|
+
const initialization = (async (): Promise<F> => {
|
|
66
|
+
let candidate: F | null = null;
|
|
67
|
+
try {
|
|
68
|
+
candidate = await createFinder(cwd);
|
|
69
|
+
if (requestedGeneration !== generation) {
|
|
70
|
+
throw new Error("FFF finder initialization was revoked");
|
|
71
|
+
}
|
|
72
|
+
current = { cwd, generation: requestedGeneration, finder: candidate };
|
|
73
|
+
return candidate;
|
|
74
|
+
} catch (error) {
|
|
75
|
+
// If creation itself fails after allocating a finder, the factory owns
|
|
76
|
+
// cleanup. A completed-but-invalidated candidate is closed exactly once
|
|
77
|
+
// here, before its rejected promise reaches the old session callback.
|
|
78
|
+
if (candidate !== null && current?.finder !== candidate) destroyQuietly(candidate);
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
})();
|
|
82
|
+
|
|
83
|
+
let guardedPromise!: Promise<F>;
|
|
84
|
+
guardedPromise = initialization.finally(() => {
|
|
85
|
+
if (pending?.promise === guardedPromise) pending = null;
|
|
86
|
+
});
|
|
87
|
+
pending = { cwd, generation: requestedGeneration, promise: guardedPromise };
|
|
88
|
+
return guardedPromise;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return { ensure, revoke };
|
|
92
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nklisch/pi-fff-compat",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Fast FFF-backed file search through Pi-native find/grep semantics — glob-only file lookup and exact regex/literal grep with no fuzzy fallback.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "nklisch"
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
],
|
|
31
31
|
"pi": {
|
|
32
32
|
"extensions": [
|
|
33
|
-
"./extensions"
|
|
33
|
+
"./extensions/fff-compat-search.ts"
|
|
34
34
|
]
|
|
35
35
|
},
|
|
36
36
|
"private": false,
|