@ff-labs/pi-fff 0.10.7-nightly.f3647b7 → 0.11.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 +2 -0
- package/package.json +3 -3
- package/src/index.ts +207 -47
- package/src/paths.ts +6 -0
- package/src/sdk.ts +37 -2
package/README.md
CHANGED
|
@@ -162,6 +162,8 @@ All fields are optional:
|
|
|
162
162
|
| `warnOnHomeDirScan` | boolean | `true` |
|
|
163
163
|
| `followSymlinks` | boolean | `true` |
|
|
164
164
|
|
|
165
|
+
Starting a session in a directory the config opts out of indexing (`$HOME` with `enableHomeDirScanning: false`, `/` with `enableFsRootScanning: false`) disables FFF workspace search for that session: the extension reports it once as a warning and keeps the `ffgrep`/`fffind` names even in `override` mode, so pi's built-in `grep`/`find` stay reachable.
|
|
166
|
+
|
|
165
167
|
CLI flags take precedence over environment variables, which take precedence over this file. A missing file is ignored. Malformed JSON, unknown fields, and invalid values stop the extension from loading and report the file path and error. `/fff-mode` changes the current session; it does not edit this file.
|
|
166
168
|
|
|
167
169
|
The file is global only. Project-level config cannot safely control tool names because pi decides which tools an extension registers before project configuration can be trusted.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ff-labs/pi-fff",
|
|
3
3
|
"public": true,
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.11.0",
|
|
5
5
|
"description": "pi extension: FFF-powered fuzzy file and content search",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"license": "MIT",
|
|
@@ -41,8 +41,8 @@
|
|
|
41
41
|
"typecheck": "tsc --noEmit"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@ff-labs/fff-bun": "0.
|
|
45
|
-
"@ff-labs/fff-node": "0.
|
|
44
|
+
"@ff-labs/fff-bun": "0.11.0",
|
|
45
|
+
"@ff-labs/fff-node": "0.11.0"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
48
|
"@earendil-works/pi-coding-agent": "*",
|
package/src/index.ts
CHANGED
|
@@ -14,7 +14,10 @@ import type {
|
|
|
14
14
|
import {
|
|
15
15
|
type AutocompleteItem,
|
|
16
16
|
type AutocompleteProvider,
|
|
17
|
+
type Component,
|
|
18
|
+
sliceByColumn,
|
|
17
19
|
Text,
|
|
20
|
+
visibleWidth,
|
|
18
21
|
} from "@earendil-works/pi-tui";
|
|
19
22
|
import type {
|
|
20
23
|
FileFinderApi,
|
|
@@ -28,7 +31,7 @@ import { Type, type TSchema } from "@sinclair/typebox";
|
|
|
28
31
|
import { AuxFinderPool, routePathConstraint } from "./aux-finders";
|
|
29
32
|
import { type FffMode, loadConfig, VALID_MODES } from "./config";
|
|
30
33
|
import { FilePickerFactory } from "./file-picker";
|
|
31
|
-
import { isHomeDir, resolveDbPaths } from "./paths";
|
|
34
|
+
import { isFsRoot, isHomeDir, resolveDbPaths } from "./paths";
|
|
32
35
|
import { buildQuery } from "./query";
|
|
33
36
|
|
|
34
37
|
export { SCAN_TIMEOUT_MS } from "./sdk";
|
|
@@ -78,6 +81,10 @@ function resolveToolNames(mode: FffMode): ToolNames {
|
|
|
78
81
|
return mode === "override" ? OVERRIDE_TOOL_NAMES : FFF_TOOL_NAMES;
|
|
79
82
|
}
|
|
80
83
|
|
|
84
|
+
function toolNameList(names: ToolNames): string[] {
|
|
85
|
+
return [names.grep, names.find, names.multiGrep];
|
|
86
|
+
}
|
|
87
|
+
|
|
81
88
|
// ---------------------------------------------------------------------------
|
|
82
89
|
// Cursor store — simple bounded Map for pagination cursors
|
|
83
90
|
// ---------------------------------------------------------------------------
|
|
@@ -460,8 +467,22 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
460
467
|
});
|
|
461
468
|
}
|
|
462
469
|
|
|
470
|
+
// The native layer refuses a picker rooted at $HOME / the fs root unless the
|
|
471
|
+
// matching opt-in is set (crates/fff-core/src/file_picker.rs). Detect that here
|
|
472
|
+
// so the opt-out reads as "search off" instead of an init failure (issue #857).
|
|
473
|
+
function scanOptOutReason(cwd: string): string | null {
|
|
474
|
+
if (!enableHomeDirScanning && isHomeDir(cwd))
|
|
475
|
+
return `(fff): cwd is $HOME and "enableHomeDirScanning" is false, so FFF search is disabled for this session. Start pi from a project directory, or set "enableHomeDirScanning": true / --fff-enable-home-scan=true to index $HOME.`;
|
|
476
|
+
if (!enableFsRootScanning && isFsRoot(cwd))
|
|
477
|
+
return `(fff): cwd is the filesystem root and "enableFsRootScanning" is false, so FFF search is disabled for this session. Start pi from a project directory, or set "enableFsRootScanning": true / --fff-enable-root-scan=true to index it.`;
|
|
478
|
+
return null;
|
|
479
|
+
}
|
|
480
|
+
|
|
463
481
|
// in case cwd changes we need to figure this out
|
|
464
482
|
function ensureFinder(cwd: string): Promise<FileFinderApi> {
|
|
483
|
+
const optOut = scanOptOutReason(cwd);
|
|
484
|
+
if (optOut) return Promise.reject(new Error(optOut));
|
|
485
|
+
|
|
465
486
|
if (mainFinder && !mainFinder.isDestroyed && finderCwd === cwd)
|
|
466
487
|
return Promise.resolve(mainFinder);
|
|
467
488
|
|
|
@@ -637,30 +658,66 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
637
658
|
};
|
|
638
659
|
|
|
639
660
|
const pendingTools: (() => string)[] = [];
|
|
661
|
+
const registeredToolNames = new Set<string>();
|
|
662
|
+
// A renderer is attached to a concrete registered name. Keep that name outside
|
|
663
|
+
// row state so old fffgrep rows retain their title after mode changes to override.
|
|
664
|
+
const renderToolNames = new WeakMap<object, string>();
|
|
640
665
|
let toolsRegistered = false;
|
|
641
666
|
|
|
667
|
+
function getRenderToolName(context: object, fallback: string): string {
|
|
668
|
+
return renderToolNames.get(context) ?? fallback;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function registerTool<TParams extends TSchema, TDetails = unknown, TState = any>(
|
|
672
|
+
resolveName: () => string,
|
|
673
|
+
definition: PendingToolDefinition<TParams, TDetails, TState>,
|
|
674
|
+
): string {
|
|
675
|
+
const resolvedName = resolveName();
|
|
676
|
+
if (registeredToolNames.has(resolvedName)) return resolvedName;
|
|
677
|
+
|
|
678
|
+
const { promptGuidelines, renderCall, ...tool } = definition;
|
|
679
|
+
pi.registerTool({
|
|
680
|
+
...tool,
|
|
681
|
+
name: resolvedName,
|
|
682
|
+
label: resolvedName,
|
|
683
|
+
promptGuidelines: promptGuidelines?.(toolNames),
|
|
684
|
+
renderCall: renderCall
|
|
685
|
+
? (args, theme, context) => {
|
|
686
|
+
renderToolNames.set(context, resolvedName);
|
|
687
|
+
return renderCall(args, theme, context);
|
|
688
|
+
}
|
|
689
|
+
: undefined,
|
|
690
|
+
});
|
|
691
|
+
registeredToolNames.add(resolvedName);
|
|
692
|
+
return resolvedName;
|
|
693
|
+
}
|
|
694
|
+
|
|
642
695
|
function queueTool<TParams extends TSchema, TDetails = unknown, TState = any>(
|
|
643
696
|
resolveName: () => string,
|
|
644
697
|
definition: PendingToolDefinition<TParams, TDetails, TState>,
|
|
645
698
|
): void {
|
|
646
|
-
pendingTools.push(() =>
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
promptGuidelines: promptGuidelines?.(toolNames),
|
|
654
|
-
});
|
|
655
|
-
return resolvedName;
|
|
656
|
-
});
|
|
699
|
+
pendingTools.push(() => registerTool(resolveName, definition));
|
|
700
|
+
|
|
701
|
+
// Pi restores historical tool rows before session_start. Register the
|
|
702
|
+
// FFF-named tools now so their renderers resolve. Pi activates every
|
|
703
|
+
// newly registered tool, so registerPendingTools prunes the names the
|
|
704
|
+
// final mode did not select.
|
|
705
|
+
registerTool(resolveName, definition);
|
|
657
706
|
}
|
|
658
707
|
|
|
659
|
-
|
|
708
|
+
// Pi carries the active tool list across /reload, so names activated under a
|
|
709
|
+
// previously used mode stay active unless we drop them here (#855).
|
|
710
|
+
function registerPendingTools(staleNames: readonly string[]): void {
|
|
660
711
|
if (toolsRegistered) return;
|
|
661
712
|
|
|
662
|
-
const registeredNames = pendingTools.map((register) => register());
|
|
663
|
-
|
|
713
|
+
const registeredNames = new Set(pendingTools.map((register) => register()));
|
|
714
|
+
const stale = new Set(staleNames.filter((name) => !registeredNames.has(name)));
|
|
715
|
+
pi.setActiveTools([
|
|
716
|
+
...new Set([
|
|
717
|
+
...pi.getActiveTools().filter((name) => !stale.has(name)),
|
|
718
|
+
...registeredNames,
|
|
719
|
+
]),
|
|
720
|
+
]);
|
|
664
721
|
toolsRegistered = true;
|
|
665
722
|
}
|
|
666
723
|
|
|
@@ -720,34 +777,51 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
720
777
|
// Pi populates extension flag values after loading extensions.
|
|
721
778
|
resolveStartupConfig();
|
|
722
779
|
|
|
780
|
+
// FFF-named tools are ours alone, so they are always safe to drop. Override
|
|
781
|
+
// names collide with pi's builtins and are only stale once this session ran
|
|
782
|
+
// in override mode, which is the sole way we could have activated them.
|
|
783
|
+
const staleNames = toolNameList(FFF_TOOL_NAMES);
|
|
784
|
+
let usedOverride = currentMode === "override";
|
|
785
|
+
|
|
723
786
|
// Restore persisted mode before registering tools so a saved override
|
|
724
787
|
// can safely change their names after /reload or session resume.
|
|
725
|
-
const
|
|
726
|
-
if (
|
|
727
|
-
const
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
(e: { type: string; customType?: string }) =>
|
|
731
|
-
e.type === "custom" && e.customType === "fff-mode",
|
|
732
|
-
);
|
|
733
|
-
if (
|
|
734
|
-
modeEntry &&
|
|
735
|
-
typeof (modeEntry as any).data?.mode === "string" &&
|
|
736
|
-
VALID_MODES.includes((modeEntry as any).data.mode as FffMode)
|
|
737
|
-
) {
|
|
738
|
-
const restored = (modeEntry as any).data.mode as FffMode;
|
|
739
|
-
if (restored !== currentMode) setMode(restored);
|
|
740
|
-
}
|
|
788
|
+
const modes = sessionModes(ctx.sessionManager?.getEntries());
|
|
789
|
+
if (modes.length > 0) {
|
|
790
|
+
const restored = modes[modes.length - 1];
|
|
791
|
+
if (restored !== currentMode) setMode(restored);
|
|
792
|
+
usedOverride = usedOverride || modes.includes("override");
|
|
741
793
|
}
|
|
742
794
|
|
|
743
795
|
initializeFinderFactories();
|
|
744
|
-
|
|
796
|
+
|
|
797
|
+
// `override` replaces pi's built-in grep/find. With the cwd opted out of
|
|
798
|
+
// indexing that would leave the session without any working workspace
|
|
799
|
+
// search, so keep the FFF names and let the built-ins stand (issue #857).
|
|
800
|
+
const keepBuiltins =
|
|
801
|
+
currentMode === "override" && scanOptOutReason(activeCwd) !== null;
|
|
802
|
+
if (keepBuiltins) toolNames = FFF_TOOL_NAMES;
|
|
803
|
+
|
|
804
|
+
// Pruning the override names here would deactivate the built-ins #857 just
|
|
805
|
+
// chose to keep, so only prune once FFF really takes those names over.
|
|
806
|
+
if (usedOverride && !keepBuiltins)
|
|
807
|
+
staleNames.push(...toolNameList(OVERRIDE_TOOL_NAMES));
|
|
808
|
+
|
|
809
|
+
registerPendingTools(staleNames);
|
|
745
810
|
}
|
|
746
811
|
|
|
747
812
|
pi.on("session_start", async (_event, ctx) => {
|
|
748
813
|
try {
|
|
749
814
|
prepareSession(ctx);
|
|
750
815
|
registerAutocompleteProvider(ctx);
|
|
816
|
+
|
|
817
|
+
// The user opted out of indexing this cwd, so skip the picker entirely
|
|
818
|
+
// instead of letting the native refusal surface as an error (issue #857).
|
|
819
|
+
const optOut = scanOptOutReason(activeCwd);
|
|
820
|
+
if (optOut) {
|
|
821
|
+
ctx.ui.notify(optOut, "warning");
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
|
|
751
825
|
await ensureFinder(activeCwd);
|
|
752
826
|
|
|
753
827
|
// Warn when launched from $HOME with home scanning on: indexing a large
|
|
@@ -786,7 +860,72 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
786
860
|
|
|
787
861
|
// --- Shared render helpers ---
|
|
788
862
|
|
|
789
|
-
|
|
863
|
+
class CollapsedText implements Component {
|
|
864
|
+
constructor(
|
|
865
|
+
private readonly preview: string,
|
|
866
|
+
private readonly suffix: string,
|
|
867
|
+
private readonly marker: string,
|
|
868
|
+
) {}
|
|
869
|
+
|
|
870
|
+
render(width: number): string[] {
|
|
871
|
+
const availableWidth = Math.max(1, width);
|
|
872
|
+
if (!this.suffix) {
|
|
873
|
+
if (visibleWidth(this.preview) <= availableWidth) return [this.preview];
|
|
874
|
+
const markerWidth = visibleWidth(this.marker);
|
|
875
|
+
if (markerWidth >= availableWidth) {
|
|
876
|
+
return [sliceByColumn(this.marker, 0, availableWidth, true)];
|
|
877
|
+
}
|
|
878
|
+
return [
|
|
879
|
+
`${sliceByColumn(this.preview, 0, availableWidth - markerWidth, true)}${this.marker}`,
|
|
880
|
+
];
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
const suffixWidth = visibleWidth(this.suffix);
|
|
884
|
+
if (suffixWidth >= availableWidth) {
|
|
885
|
+
return [sliceByColumn(this.suffix, 0, availableWidth, true)];
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
const previewWidth = availableWidth - suffixWidth - 1;
|
|
889
|
+
if (visibleWidth(this.preview) <= previewWidth) {
|
|
890
|
+
return [`${this.preview} ${this.suffix}`];
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
const markerWidth = visibleWidth(this.marker);
|
|
894
|
+
return [
|
|
895
|
+
`${sliceByColumn(this.preview, 0, Math.max(0, previewWidth - markerWidth), true)}${this.marker} ${this.suffix}`,
|
|
896
|
+
];
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
invalidate(): void {}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// Pi wraps returned components in its own click-to-expand MouseRegion and passes
|
|
903
|
+
// the toggled state via options.expanded, so no custom mouse handling is needed.
|
|
904
|
+
const renderCompactTextResult = (
|
|
905
|
+
result: { content?: { type: string; text?: string }[] },
|
|
906
|
+
options: { expanded?: boolean },
|
|
907
|
+
theme: any,
|
|
908
|
+
context: any,
|
|
909
|
+
): Component => {
|
|
910
|
+
const output = result.content?.find((c) => c.type === "text")?.text?.trim() ?? "";
|
|
911
|
+
if (!output) return new Text(theme.fg("muted", "No output"), 0, 0);
|
|
912
|
+
|
|
913
|
+
const lines = output.split("\n");
|
|
914
|
+
const color = context.isError ? "error" : "toolOutput";
|
|
915
|
+
if (options.expanded) {
|
|
916
|
+
return new Text(lines.map((line) => theme.fg(color, line)).join("\n"), 0, 0);
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
const suffix =
|
|
920
|
+
lines.length > 1 ? theme.fg("muted", `... (${lines.length - 1} more lines)`) : "";
|
|
921
|
+
return new CollapsedText(
|
|
922
|
+
theme.fg(color, lines[0] ?? ""),
|
|
923
|
+
suffix,
|
|
924
|
+
theme.fg("muted", "..."),
|
|
925
|
+
);
|
|
926
|
+
};
|
|
927
|
+
|
|
928
|
+
const renderPreviewResult = (
|
|
790
929
|
result: { content?: { type: string; text?: string }[] },
|
|
791
930
|
options: { expanded?: boolean },
|
|
792
931
|
theme: any,
|
|
@@ -993,23 +1132,24 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
993
1132
|
},
|
|
994
1133
|
|
|
995
1134
|
renderCall(args, theme, context) {
|
|
996
|
-
const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
|
|
997
1135
|
const pattern = args?.pattern ?? "";
|
|
998
1136
|
const path = args?.path ?? ".";
|
|
999
1137
|
let content =
|
|
1000
|
-
theme.fg("toolTitle", theme.bold(toolNames.grep)) +
|
|
1138
|
+
theme.fg("toolTitle", theme.bold(getRenderToolName(context, toolNames.grep))) +
|
|
1001
1139
|
" " +
|
|
1002
1140
|
theme.fg("accent", `/${pattern}/`) +
|
|
1003
1141
|
theme.fg("toolOutput", ` in ${path}`);
|
|
1004
|
-
|
|
1005
|
-
|
|
1142
|
+
const options: string[] = [];
|
|
1143
|
+
if (args?.limit !== undefined) options.push(`limit ${args.limit}`);
|
|
1144
|
+
if (args?.context !== undefined) options.push(`context ${args.context}`);
|
|
1145
|
+
if (options.length > 0)
|
|
1146
|
+
content += theme.fg("toolOutput", ` (${options.join(", ")})`);
|
|
1006
1147
|
if (args?.cursor) content += theme.fg("muted", ` (page)`);
|
|
1007
|
-
|
|
1008
|
-
return text;
|
|
1148
|
+
return new CollapsedText(content, "", theme.fg("muted", "..."));
|
|
1009
1149
|
},
|
|
1010
1150
|
|
|
1011
1151
|
renderResult(result, options, theme, context) {
|
|
1012
|
-
return
|
|
1152
|
+
return renderCompactTextResult(result, options, theme, context);
|
|
1013
1153
|
},
|
|
1014
1154
|
});
|
|
1015
1155
|
|
|
@@ -1136,23 +1276,21 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
1136
1276
|
},
|
|
1137
1277
|
|
|
1138
1278
|
renderCall(args, theme, context) {
|
|
1139
|
-
const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
|
|
1140
1279
|
const pattern = args?.pattern ?? "";
|
|
1141
1280
|
const path = args?.path ?? ".";
|
|
1142
1281
|
let content =
|
|
1143
|
-
theme.fg("toolTitle", theme.bold(toolNames.find)) +
|
|
1282
|
+
theme.fg("toolTitle", theme.bold(getRenderToolName(context, toolNames.find))) +
|
|
1144
1283
|
" " +
|
|
1145
1284
|
theme.fg("accent", pattern) +
|
|
1146
1285
|
theme.fg("toolOutput", ` in ${path}`);
|
|
1147
1286
|
if (args?.limit !== undefined)
|
|
1148
1287
|
content += theme.fg("toolOutput", ` (limit ${args.limit})`);
|
|
1149
1288
|
if (args?.cursor) content += theme.fg("muted", ` (page)`);
|
|
1150
|
-
|
|
1151
|
-
return text;
|
|
1289
|
+
return new CollapsedText(content, "", theme.fg("muted", "..."));
|
|
1152
1290
|
},
|
|
1153
1291
|
|
|
1154
1292
|
renderResult(result, options, theme, context) {
|
|
1155
|
-
return
|
|
1293
|
+
return renderCompactTextResult(result, options, theme, context);
|
|
1156
1294
|
},
|
|
1157
1295
|
});
|
|
1158
1296
|
|
|
@@ -1244,7 +1382,10 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
1244
1382
|
const patterns = args?.patterns ?? [];
|
|
1245
1383
|
const constraints = args?.constraints;
|
|
1246
1384
|
let content =
|
|
1247
|
-
theme.fg(
|
|
1385
|
+
theme.fg(
|
|
1386
|
+
"toolTitle",
|
|
1387
|
+
theme.bold(getRenderToolName(context, toolNames.multiGrep)),
|
|
1388
|
+
) +
|
|
1248
1389
|
" " +
|
|
1249
1390
|
theme.fg("accent", patterns.map((p: string) => `"${p}"`).join(", "));
|
|
1250
1391
|
if (constraints) content += theme.fg("toolOutput", ` (${constraints})`);
|
|
@@ -1254,7 +1395,7 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
1254
1395
|
},
|
|
1255
1396
|
|
|
1256
1397
|
renderResult(result, options, theme, context) {
|
|
1257
|
-
return
|
|
1398
|
+
return renderPreviewResult(result, options, theme, context, 15);
|
|
1258
1399
|
},
|
|
1259
1400
|
});
|
|
1260
1401
|
} // end if (enableMultiGrep)
|
|
@@ -1358,3 +1499,22 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
1358
1499
|
},
|
|
1359
1500
|
});
|
|
1360
1501
|
}
|
|
1502
|
+
|
|
1503
|
+
// Every mode this session selected via /fff-mode, oldest first.
|
|
1504
|
+
function sessionModes(entries: unknown): FffMode[] {
|
|
1505
|
+
if (!Array.isArray(entries)) return [];
|
|
1506
|
+
|
|
1507
|
+
const modes: FffMode[] = [];
|
|
1508
|
+
for (const entry of entries as {
|
|
1509
|
+
type?: string;
|
|
1510
|
+
customType?: string;
|
|
1511
|
+
data?: unknown;
|
|
1512
|
+
}[]) {
|
|
1513
|
+
if (entry?.type !== "custom" || entry.customType !== "fff-mode") continue;
|
|
1514
|
+
const mode = (entry.data as { mode?: unknown } | undefined)?.mode;
|
|
1515
|
+
if (typeof mode === "string" && VALID_MODES.includes(mode as FffMode)) {
|
|
1516
|
+
modes.push(mode as FffMode);
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
return modes;
|
|
1520
|
+
}
|
package/src/paths.ts
CHANGED
|
@@ -18,6 +18,12 @@ export function isHomeDir(dir: string): boolean {
|
|
|
18
18
|
return path.resolve(dir) === HOME_DIR;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
// Mirrors the native `path.parent().is_none()` check: a path that is its own parent.
|
|
22
|
+
export function isFsRoot(dir: string): boolean {
|
|
23
|
+
const resolved = path.resolve(dir);
|
|
24
|
+
return path.dirname(resolved) === resolved;
|
|
25
|
+
}
|
|
26
|
+
|
|
21
27
|
// Resolution order: explicit override > existing fff.nvim db > pi-local data dir.
|
|
22
28
|
// Reusing the nvim db lets pi rank files by the frecency the user built in their editor.
|
|
23
29
|
export function resolveDbPaths(overrides: {
|
package/src/sdk.ts
CHANGED
|
@@ -9,6 +9,22 @@ export type FileFinderStatic = {
|
|
|
9
9
|
|
|
10
10
|
let sdkPromise: Promise<{ FileFinder: FileFinderStatic }> | null = null;
|
|
11
11
|
|
|
12
|
+
const SDK_ORDER: Record<"bun" | "node", readonly [string, string]> = {
|
|
13
|
+
// fff-bun is TS-source only and cannot be imported by Bun-compiled hosts
|
|
14
|
+
// (e.g. omp) whose module resolver rejects .ts under node_modules, so the
|
|
15
|
+
// JS-compiled fff-node is kept as a fallback for every runtime.
|
|
16
|
+
bun: ["@ff-labs/fff-bun", "@ff-labs/fff-node"],
|
|
17
|
+
node: ["@ff-labs/fff-node", "@ff-labs/fff-bun"],
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// Literal dynamic imports so hosts that statically scan extension graphs
|
|
21
|
+
// (omp's legacy-pi-compat loader) discover and hook both SDK packages;
|
|
22
|
+
// a variable `import(pkg)` would bypass that scan and fail at runtime.
|
|
23
|
+
const SDK_IMPORTS = {
|
|
24
|
+
"@ff-labs/fff-bun": () => import("@ff-labs/fff-bun"),
|
|
25
|
+
"@ff-labs/fff-node": () => import("@ff-labs/fff-node"),
|
|
26
|
+
} as const;
|
|
27
|
+
|
|
12
28
|
function detectRuntime(): "bun" | "node" {
|
|
13
29
|
if (typeof (globalThis as { Bun?: unknown }).Bun !== "undefined") return "bun";
|
|
14
30
|
if (
|
|
@@ -19,6 +35,26 @@ function detectRuntime(): "bun" | "node" {
|
|
|
19
35
|
return "node";
|
|
20
36
|
}
|
|
21
37
|
|
|
38
|
+
/** Preferred SDK order for the detected runtime. */
|
|
39
|
+
export function sdkCandidates(): readonly [string, string] {
|
|
40
|
+
return SDK_ORDER[detectRuntime()];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function loadFirst(
|
|
44
|
+
candidates: readonly [string, string],
|
|
45
|
+
loaders: Record<string, () => Promise<unknown>> = SDK_IMPORTS,
|
|
46
|
+
): Promise<{ FileFinder: FileFinderStatic }> {
|
|
47
|
+
let lastError: unknown;
|
|
48
|
+
for (const pkg of candidates) {
|
|
49
|
+
try {
|
|
50
|
+
return (await loaders[pkg]()) as { FileFinder: FileFinderStatic };
|
|
51
|
+
} catch (error) {
|
|
52
|
+
lastError = error;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
throw lastError;
|
|
56
|
+
}
|
|
57
|
+
|
|
22
58
|
export function loadSdk(): Promise<{ FileFinder: FileFinderStatic }> {
|
|
23
59
|
if (sdkPromise) return sdkPromise;
|
|
24
60
|
|
|
@@ -34,8 +70,7 @@ export function loadSdk(): Promise<{ FileFinder: FileFinderStatic }> {
|
|
|
34
70
|
}
|
|
35
71
|
|
|
36
72
|
// default to node as it seems like default option
|
|
37
|
-
const
|
|
38
|
-
const p = import(pkg) as Promise<{ FileFinder: FileFinderStatic }>;
|
|
73
|
+
const p = loadFirst(sdkCandidates());
|
|
39
74
|
sdkPromise = p;
|
|
40
75
|
(globalThis as Record<string, unknown>).__fffSdkPromiseGlobal = p;
|
|
41
76
|
return p;
|