From 39c05f23a32e596995eccd4797431b8f8b2e0aa4 Mon Sep 17 00:00:00 2001 From: gustav-fff <286169375+gustav-fff@users.noreply.github.com> Date: Mon, 22 Jun 2026 16:59:31 -0700 Subject: [PATCH 1/2] feat: expose follow_symlinks for SDK/MCP/python paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #627 Bumps FFF_CREATE_OPTIONS_VERSION to 2 (append-only). Adds --follow-symlinks flag to fff-mcp, follow_symlinks kwarg to the pyo3 finder, and followSymlinks option to @ff-labs/fff-node. Default stays false everywhere; follow_symlinks is preserved across reindex in C and Python. Watcher cycle handling is unchanged — caller must ensure the indexed tree has no symlink loops. --- crates/fff-c/include/fff.h | 13 ++++++++----- crates/fff-c/src/ffi_types.rs | 11 +++++++++-- crates/fff-c/src/lib.rs | 9 +++++---- crates/fff-mcp/src/main.rs | 7 ++++++- crates/fff-python/src/finder.rs | 11 +++++++---- packages/fff-node/src/fff-api.ts | 6 ++++++ packages/fff-node/src/ffi.ts | 5 ++++- packages/fff-node/src/finder.ts | 1 + 8 files changed, 46 insertions(+), 17 deletions(-) diff --git a/crates/fff-c/include/fff.h b/crates/fff-c/include/fff.h index 6be43935..8bc42059 100644 --- a/crates/fff-c/include/fff.h +++ b/crates/fff-c/include/fff.h @@ -12,7 +12,7 @@ /** * Current used version of [`FffCreateOptions`]. */ -#define FFF_CREATE_OPTIONS_VERSION 1 +#define FFF_CREATE_OPTIONS_VERSION 2 /** * Result envelope returned by all `fff_*` functions. @@ -100,10 +100,7 @@ typedef struct FffCreateOptions { */ bool ai_mode; /** - * Path-shape hint for the per-session log file. Each call writes a fresh - * sibling file `++.` next to this path. - * The literal path is never written to, so concurrent processes get - * unique per-pid files. NULL/empty to skip log init. + * Tracing log file path. NULL/empty to skip log init. */ const char *log_file_path; /** @@ -133,6 +130,12 @@ typedef struct FffCreateOptions { * `enable_fs_root_scanning`. */ bool enable_home_dir_scanning; + /** + * Follow symlinks during scan and watcher walks. Off by default — + * enabling this without external loop protection can wedge the watcher + * on cyclic symlink graphs. Caller is responsible for the trade-off. + */ + bool follow_symlinks; } FffCreateOptions; /** diff --git a/crates/fff-c/src/ffi_types.rs b/crates/fff-c/src/ffi_types.rs index d2b0e972..6421de72 100644 --- a/crates/fff-c/src/ffi_types.rs +++ b/crates/fff-c/src/ffi_types.rs @@ -15,7 +15,7 @@ use fff::{ }; /// Current used version of [`FffCreateOptions`]. -pub const FFF_CREATE_OPTIONS_VERSION: u32 = 1; +pub const FFF_CREATE_OPTIONS_VERSION: u32 = 2; /// Options for `fff_create_instance_with`. /// @@ -57,7 +57,12 @@ pub struct FffCreateOptions { /// Allow indexing the user's home directory. Same trade-off as /// `enable_fs_root_scanning`. pub enable_home_dir_scanning: bool, - // ----- new version 2+ fields go here, ALWAYS appended ----- + // ----- v2 fields ----- + /// Follow symlinks during scan and watcher walks. Off by default — + /// enabling this without external loop protection can wedge the watcher + /// on cyclic symlink graphs. Caller is responsible for the trade-off. + pub follow_symlinks: bool, + // ----- new version 3+ fields go here, ALWAYS appended ----- } impl FffCreateOptions { @@ -79,6 +84,7 @@ impl FffCreateOptions { cache_budget_max_file_size: 0, enable_fs_root_scanning: false, enable_home_dir_scanning: false, + follow_symlinks: false, } } } @@ -820,5 +826,6 @@ mod options_layout_tests { assert_eq!(offset_of!(FffCreateOptions, cache_budget_max_file_size), 72); assert_eq!(offset_of!(FffCreateOptions, enable_fs_root_scanning), 80); assert_eq!(offset_of!(FffCreateOptions, enable_home_dir_scanning), 81); + assert_eq!(offset_of!(FffCreateOptions, follow_symlinks), 82); } } diff --git a/crates/fff-c/src/lib.rs b/crates/fff-c/src/lib.rs index 0e0d0c91..56283d7e 100644 --- a/crates/fff-c/src/lib.rs +++ b/crates/fff-c/src/lib.rs @@ -292,7 +292,7 @@ pub unsafe extern "C" fn fff_create_instance_with(opts: *const FffCreateOptions) watch: opts.watch, mode, cache_budget, - follow_symlinks: false, + follow_symlinks: opts.version >= 2 && opts.follow_symlinks, enable_fs_root_scanning: opts.enable_fs_root_scanning, enable_home_dir_scanning: opts.enable_home_dir_scanning, }, @@ -1018,7 +1018,7 @@ pub unsafe extern "C" fn fff_restart_index( Err(e) => return FffResult::err(&format!("Failed to acquire file picker lock: {}", e)), }; - let (warmup_caches, content_indexing, watch, mode, fs_root, home_dir) = + let (warmup_caches, content_indexing, watch, mode, fs_root, home_dir, follow_symlinks) = if let Some(ref picker) = *guard { ( picker.has_mmap_cache(), @@ -1027,9 +1027,10 @@ pub unsafe extern "C" fn fff_restart_index( picker.mode(), picker.fs_root_scanning_enabled(), picker.home_dir_scanning_enabled(), + picker.follows_symlinks(), ) } else { - (false, true, true, FFFMode::default(), false, false) + (false, true, true, FFFMode::default(), false, false, false) }; drop(guard); @@ -1044,7 +1045,7 @@ pub unsafe extern "C" fn fff_restart_index( watch, mode, cache_budget: None, - follow_symlinks: false, + follow_symlinks, enable_fs_root_scanning: fs_root, enable_home_dir_scanning: home_dir, }, diff --git a/crates/fff-mcp/src/main.rs b/crates/fff-mcp/src/main.rs index 497c5ee9..19a4705c 100644 --- a/crates/fff-mcp/src/main.rs +++ b/crates/fff-mcp/src/main.rs @@ -149,6 +149,11 @@ pub(crate) struct Args { #[arg(long = "max-cached-files", env = "FFF_MAX_CACHED_FILES")] max_cached_files: Option, + /// Follow symlinks during scan and watcher walks. Off by default — + /// enabling on cyclic symlink layouts can wedge the watcher. + #[arg(long = "follow-symlinks")] + follow_symlinks: bool, + /// Run a health check and print diagnostic information, then exit. #[arg(long = "healthcheck")] pub(crate) healthcheck: bool, @@ -262,7 +267,7 @@ async fn main() -> Result<(), Box> { cache_budget: args .max_cached_files .map(fff::ContentCacheBudget::new_for_repo), - follow_symlinks: false, + follow_symlinks: args.follow_symlinks, ..Default::default() }, ) diff --git a/crates/fff-python/src/finder.rs b/crates/fff-python/src/finder.rs index e61232fc..3384ae7e 100644 --- a/crates/fff-python/src/finder.rs +++ b/crates/fff-python/src/finder.rs @@ -176,6 +176,7 @@ impl FileFinder { cache_budget_max_file_size=0, enable_fs_root_scanning=false, enable_home_dir_scanning=false, + follow_symlinks=false, ))] #[allow(clippy::too_many_arguments)] fn new( @@ -194,6 +195,7 @@ impl FileFinder { cache_budget_max_file_size: u64, enable_fs_root_scanning: bool, enable_home_dir_scanning: bool, + follow_symlinks: bool, ) -> PyResult { let shared_picker = SharedFilePicker::default(); let shared_frecency = SharedFrecency::default(); @@ -243,7 +245,7 @@ impl FileFinder { cache_budget_max_bytes, cache_budget_max_file_size, ), - follow_symlinks: false, + follow_symlinks, enable_fs_root_scanning, enable_home_dir_scanning, }, @@ -742,7 +744,7 @@ impl FileFinder { } let canonical = fff::path_utils::canonicalize(&new_path).map_err(py_err)?; - let (warmup_caches, content_indexing, watch, mode, fs_root, home_dir) = { + let (warmup_caches, content_indexing, watch, mode, fs_root, home_dir, follow_symlinks) = { let guard = picker.read().map_err(py_err)?; if let Some(ref picker) = *guard { ( @@ -752,9 +754,10 @@ impl FileFinder { picker.mode(), picker.fs_root_scanning_enabled(), picker.home_dir_scanning_enabled(), + picker.follows_symlinks(), ) } else { - (false, true, true, FFFMode::default(), false, false) + (false, true, true, FFFMode::default(), false, false, false) } }; @@ -772,7 +775,7 @@ impl FileFinder { cache_budget_max_bytes, cache_budget_max_file_size, ), - follow_symlinks: false, + follow_symlinks, enable_fs_root_scanning: fs_root, enable_home_dir_scanning: home_dir, }, diff --git a/packages/fff-node/src/fff-api.ts b/packages/fff-node/src/fff-api.ts index abff537c..04d74d4d 100644 --- a/packages/fff-node/src/fff-api.ts +++ b/packages/fff-node/src/fff-api.ts @@ -108,6 +108,12 @@ export interface InitOptions { * `enableFsRootScanning`. */ enableHomeDirScanning?: boolean; + /** + * Follow symlinks during scan and watcher walks. Off by default — + * enabling on cyclic symlink layouts can wedge the watcher. Caller is + * responsible for ensuring the indexed tree has no symlink loops. + */ + followSymlinks?: boolean; } /** diff --git a/packages/fff-node/src/ffi.ts b/packages/fff-node/src/ffi.ts index e9ab6d4d..e1077f9c 100644 --- a/packages/fff-node/src/ffi.ts +++ b/packages/fff-node/src/ffi.ts @@ -78,10 +78,11 @@ const FFF_CREATE_OPTIONS_STRUCT = { cache_budget_max_file_size: DataType.U64, enable_fs_root_scanning: DataType.U8, enable_home_dir_scanning: DataType.U8, + follow_symlinks: DataType.U8, }; // ALWAYS KEEP IN SYNC WITH fff.h -const FFF_CREATE_OPTIONS_VERSION = 1; +const FFF_CREATE_OPTIONS_VERSION = 2; /** Grep mode constants matching the C API (u8). */ const GREP_MODE_PLAIN = 0; @@ -359,6 +360,7 @@ export function ffiCreate( cacheBudgetMaxFileSize: number, enableFsRootScanning: boolean, enableHomeDirScanning: boolean, + followSymlinks: boolean, ): Result { loadLibrary(); @@ -378,6 +380,7 @@ export function ffiCreate( cache_budget_max_file_size: cacheBudgetMaxFileSize, enable_fs_root_scanning: enableFsRootScanning ? 1 : 0, enable_home_dir_scanning: enableHomeDirScanning ? 1 : 0, + follow_symlinks: followSymlinks ? 1 : 0, }; const rawPtr = load({ diff --git a/packages/fff-node/src/finder.ts b/packages/fff-node/src/finder.ts index a7b91626..3d1b4584 100644 --- a/packages/fff-node/src/finder.ts +++ b/packages/fff-node/src/finder.ts @@ -127,6 +127,7 @@ export class FileFinder implements FileFinderApi { options.cacheBudgetMaxFileSize ?? 0, options.enableFsRootScanning ?? false, options.enableHomeDirScanning ?? false, + options.followSymlinks ?? false, ); if (!result.ok) { From 1fb274d23af5b071f747d6f3fd58a22b9374c266 Mon Sep 17 00:00:00 2001 From: Dmitriy Kovalenko Date: Fri, 26 Jun 2026 16:46:18 -0700 Subject: [PATCH 2/2] fix build --- packages/fff-bun/src/fff-api.ts | 16 +++++++-------- packages/fff-node/src/fff-api.ts | 20 ++++++------------- packages/pi-fff/src/index.ts | 5 +---- packages/shared/fff-api.ts | 34 +++++++++++++++----------------- 4 files changed, 30 insertions(+), 45 deletions(-) diff --git a/packages/fff-bun/src/fff-api.ts b/packages/fff-bun/src/fff-api.ts index abff537c..e5c99a2d 100644 --- a/packages/fff-bun/src/fff-api.ts +++ b/packages/fff-bun/src/fff-api.ts @@ -97,17 +97,15 @@ export interface InitOptions { /** Override for the per-file byte cap in the content cache. */ cacheBudgetMaxFileSize?: number; /** - * Allow indexing the filesystem root (`/`). Off by default — root is - * rarely the intended target and floods the watcher with churn-prone - * events. Setting this true is opt-in and the caller is responsible for - * the resulting fs-event volume. - */ + * Allow indexing the filesystem root (`/`). Off by default, having fff instance at the large folder + * will generally require file watcher + * */ + enableFsRootScanning?: boolean; - /** - * Allow indexing the user's home directory. Same trade-off as - * `enableFsRootScanning`. - */ + /** Allow indexing the user's home directory. Same trade-off as `enableFsRootScanning`. */ enableHomeDirScanning?: boolean; + /** Follow symlinks for directories */ + followSymlinks?: boolean; } /** diff --git a/packages/fff-node/src/fff-api.ts b/packages/fff-node/src/fff-api.ts index 04d74d4d..e5c99a2d 100644 --- a/packages/fff-node/src/fff-api.ts +++ b/packages/fff-node/src/fff-api.ts @@ -97,22 +97,14 @@ export interface InitOptions { /** Override for the per-file byte cap in the content cache. */ cacheBudgetMaxFileSize?: number; /** - * Allow indexing the filesystem root (`/`). Off by default — root is - * rarely the intended target and floods the watcher with churn-prone - * events. Setting this true is opt-in and the caller is responsible for - * the resulting fs-event volume. - */ + * Allow indexing the filesystem root (`/`). Off by default, having fff instance at the large folder + * will generally require file watcher + * */ + enableFsRootScanning?: boolean; - /** - * Allow indexing the user's home directory. Same trade-off as - * `enableFsRootScanning`. - */ + /** Allow indexing the user's home directory. Same trade-off as `enableFsRootScanning`. */ enableHomeDirScanning?: boolean; - /** - * Follow symlinks during scan and watcher walks. Off by default — - * enabling on cyclic symlink layouts can wedge the watcher. Caller is - * responsible for ensuring the indexed tree has no symlink loops. - */ + /** Follow symlinks for directories */ followSymlinks?: boolean; } diff --git a/packages/pi-fff/src/index.ts b/packages/pi-fff/src/index.ts index 7c8091f4..f8f65c0c 100644 --- a/packages/pi-fff/src/index.ts +++ b/packages/pi-fff/src/index.ts @@ -944,10 +944,7 @@ export default function fffExtension(pi: ExtensionAPI) { if (!arg) { const mode = getMode(); const flag = pi.getFlag("fff-mode") ?? "unset"; - ctx.ui.notify( - `Current mode: '${mode}' (flag: ${flag})`, - "info", - ); + ctx.ui.notify(`Current mode: '${mode}' (flag: ${flag})`, "info"); return; } diff --git a/packages/shared/fff-api.ts b/packages/shared/fff-api.ts index f4420148..75021c4a 100644 --- a/packages/shared/fff-api.ts +++ b/packages/shared/fff-api.ts @@ -43,23 +43,17 @@ export interface InitOptions { frecencyDbPath?: string; /** Path to query history database (optional, omit to skip query tracker initialization) */ historyDbPath?: string; - /** - * @deprecated No-op. The no-lock LMDB flags showed no measurable win under - * realistic contention and are now ignored. Kept for source-compat. - */ + /** @deprecated no-op */ useUnsafeNoLock?: boolean; /** * Disable mmap cache warmup after the initial scan. When mmap cache is * enabled (the default), the first grep search is as fast as subsequent * ones at the cost of a longer scan time and higher initial memory usage. - * (default: false) */ disableMmapCache?: boolean; /** * Disable the content index built after the initial scan. * Content indexing enables faster content-aware filtering during grep. - * When omitted, follows `disableMmapCache` for backward compatibility. - * (default: follows `disableMmapCache`) */ disableContentIndexing?: boolean; /** @@ -91,17 +85,15 @@ export interface InitOptions { /** Override for the per-file byte cap in the content cache. */ cacheBudgetMaxFileSize?: number; /** - * Allow indexing the filesystem root (`/`). Off by default — root is - * rarely the intended target and floods the watcher with churn-prone - * events. Setting this true is opt-in and the caller is responsible for - * the resulting fs-event volume. - */ + * Allow indexing the filesystem root (`/`). + * Off by default, having fff instance at the large folder will generally require + * file watcher and indexing which will consume a lot of resources if performed uncontrolled + **/ enableFsRootScanning?: boolean; - /** - * Allow indexing the user's home directory. Same trade-off as - * `enableFsRootScanning`. - */ + /** Allow indexing the user's home directory. Same trade-off as `enableFsRootScanning`. */ enableHomeDirScanning?: boolean; + /** Follow symlinks for directories */ + followSymlinks?: boolean; } /** @@ -547,10 +539,16 @@ export interface FileFinderApi { glob(pattern: string, options?: GlobOptions): Result; /** Fuzzy directory search. */ - directorySearch(query: string, options?: DirSearchOptions): Result; + directorySearch( + query: string, + options?: DirSearchOptions, + ): Result; /** Fuzzy search over files and directories interleaved by score. */ - mixedSearch(query: string, options?: SearchOptions): Result; + mixedSearch( + query: string, + options?: SearchOptions, + ): Result; /** Content search (live grep). */ grep(query: string, options?: GrepOptions): Result;