From b692d0aea4ff98a7b6b371cba1addc6be784479f Mon Sep 17 00:00:00 2001 From: EthanHuo Date: Mon, 3 Aug 2026 14:53:48 +0800 Subject: [PATCH 1/6] fix: unify Git ignore policy across scan and watch --- Cargo.lock | 3 +- Cargo.toml | 2 +- crates/fff-core/src/file_picker.rs | 8 ++ crates/fff-core/src/ignore.rs | 118 ++++++++++++++- crates/fff-core/src/scan.rs | 7 + crates/fff-core/src/walk/mod.rs | 66 +++++++++ crates/fff-core/src/walk/ripgrep.rs | 14 +- crates/fff-core/src/walk/zlob.rs | 9 ++ .../src/watcher/background_watcher.rs | 136 +++++++++++++++--- packages/fff-node/test/ignore-policy.mjs | 94 ++++++++++++ .../fff-node/test/non-git-ignore-policy.mjs | 111 ++++++++++++++ 11 files changed, 541 insertions(+), 27 deletions(-) create mode 100644 packages/fff-node/test/ignore-policy.mjs create mode 100644 packages/fff-node/test/non-git-ignore-policy.mjs diff --git a/Cargo.lock b/Cargo.lock index bfe636b1..94d6bb18 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3236,8 +3236,7 @@ dependencies = [ [[package]] name = "zlob" version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e41cb327ac1b7e7e0d4514658500cb5734cd655edbe4e5ffeda69955da9028ee" +source = "git+https://github.com/celados/zlob?rev=a64a9bc87b1d4820ce1e4e2406692935be3b5078#a64a9bc87b1d4820ce1e4e2406692935be3b5078" dependencies = [ "bindgen", "bitflags 2.11.0", diff --git a/Cargo.toml b/Cargo.toml index 4aa7459c..1b043b44 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,7 +36,7 @@ ignore = "0.4.22" memmap2 = "0.9" mimalloc = "0.1.47" signal-hook-registry = "1.4" -zlob = { version = "=1.6.1" } +zlob = { git = "https://github.com/celados/zlob", rev = "a64a9bc87b1d4820ce1e4e2406692935be3b5078" } mlua = { version = "0.11.1", features = ["module", "luajit"] } neo_frizbee = { version = "0.11.0", features = ["match_end_col"] } diff --git a/crates/fff-core/src/file_picker.rs b/crates/fff-core/src/file_picker.rs index 6f9055fc..ab284eb9 100644 --- a/crates/fff-core/src/file_picker.rs +++ b/crates/fff-core/src/file_picker.rs @@ -124,6 +124,7 @@ pub(crate) struct FileSync { /// Ignore rules the walker assembled (zlob backend only). Shared with the /// background watcher so filesystem events can be filtered without libgit2. pub(crate) ignore_rules: Option>, + pub(crate) policy_sources: Arc>, } impl FileSync { @@ -142,6 +143,7 @@ impl FileSync { bigram_overlay: None, chunked_paths: None, ignore_rules: None, + policy_sources: Arc::new(Vec::new()), } } @@ -636,6 +638,10 @@ impl FilePicker { self.sync_data.ignore_rules.clone() } + pub(crate) fn policy_sources(&self) -> Arc> { + Arc::clone(&self.sync_data.policy_sources) + } + pub fn has_mmap_cache(&self) -> bool { self.enable_mmap_cache } @@ -2028,6 +2034,7 @@ impl FileSync { synced_files_count, )?; let ignore_rules = walk_output.ignore_rules.take().map(Arc::new); + let policy_sources = Arc::new(walk_output.policy_sources); let mut pairs = walk_output.pairs; // Sort by (dir_part, filename). This groups files by their directory @@ -2131,6 +2138,7 @@ impl FileSync { bigram_overlay: None, chunked_paths: Some(Arc::new(chunked_paths)), ignore_rules, + policy_sources, }) } } diff --git a/crates/fff-core/src/ignore.rs b/crates/fff-core/src/ignore.rs index ac29f523..372f79d6 100644 --- a/crates/fff-core/src/ignore.rs +++ b/crates/fff-core/src/ignore.rs @@ -1,4 +1,84 @@ -use std::path::Path; +use git2::{Config, Repository}; +use std::path::{Path, PathBuf}; + +#[derive(Debug, Default)] +pub(crate) struct GitIgnorePolicy { + pub(crate) base_document: String, + pub(crate) ignore_files: Vec, + pub(crate) sources: Vec, +} + +impl GitIgnorePolicy { + pub(crate) fn discover(base_path: &Path) -> Self { + let repo = Repository::discover(base_path).ok(); + // User excludes are process-wide Git policy, not repository metadata. + // `open_default` keeps non-Git vaults on the same contract as Git roots. + let global = repo + .as_ref() + .map(Repository::config) + .unwrap_or_else(Config::open_default) + .and_then(|mut config| config.snapshot()) + .and_then(|config| config.get_path("core.excludesFile")) + .ok() + .or_else(default_global_excludes_path); + + let mut policy = Self::default(); + if let Some(path) = global { + policy.add_source(path); + } + if let Some(repo) = &repo { + // Linked worktrees share this file with the primary worktree. + policy.add_source(repo.commondir().join("info/exclude")); + } + policy.sources.extend(config_sources(repo.as_ref())); + policy.sources.sort_unstable(); + policy.sources.dedup(); + policy + } + + fn add_source(&mut self, path: PathBuf) { + if let Ok(content) = std::fs::read_to_string(&path) { + self.base_document.push_str(&content); + if !content.ends_with('\n') { + self.base_document.push('\n'); + } + self.ignore_files.push(path.clone()); + } + self.sources.push(path); + } + + #[cfg(feature = "zlob")] + pub(crate) fn patterns(&self) -> impl Iterator { + self.base_document.lines() + } +} + +fn default_global_excludes_path() -> Option { + std::env::var_os("XDG_CONFIG_HOME") + .filter(|value| !value.is_empty()) + .map(PathBuf::from) + .or_else(|| dirs::home_dir().map(|home| home.join(".config"))) + .map(|config| config.join("git/ignore")) +} + +fn config_sources(repo: Option<&Repository>) -> Vec { + let mut paths = [ + Config::find_system().ok(), + Config::find_global().ok(), + Config::find_xdg().ok(), + ] + .into_iter() + .flatten() + .collect::>(); + if let Some(repo) = repo { + paths.push(repo.commondir().join("config")); + // extensions.worktreeConfig stores per-worktree overrides here. + paths.push(repo.path().join("config.worktree")); + } + paths.sort_unstable(); + paths.dedup(); + paths +} /// Directories excluded when walking a non-git root. Entries are `cfg`-gated /// so a single iteration covers standard + platform-specific overrides. @@ -66,3 +146,39 @@ pub(crate) fn is_non_code_directory(path: &Path) -> bool { path_str.contains(dir) }) } + +#[cfg(test)] +mod tests { + use super::GitIgnorePolicy; + use std::fs; + + #[test] + fn policy_document_orders_global_before_info() { + let dir = tempfile::tempdir().unwrap(); + let global = dir.path().join("global-ignore"); + let info = dir.path().join("info-exclude"); + fs::write(&global, "*.tmp").unwrap(); + fs::write(&info, "!keep.tmp\n").unwrap(); + + let mut policy = GitIgnorePolicy::default(); + policy.add_source(global.clone()); + policy.add_source(info.clone()); + + assert_eq!(policy.base_document, "*.tmp\n!keep.tmp\n"); + assert_eq!(policy.ignore_files, vec![global.clone(), info.clone()]); + assert_eq!(policy.sources, vec![global, info]); + } + + #[test] + fn missing_policy_source_is_watched_but_not_loaded() { + let dir = tempfile::tempdir().unwrap(); + let missing = dir.path().join("future-ignore"); + + let mut policy = GitIgnorePolicy::default(); + policy.add_source(missing.clone()); + + assert!(policy.base_document.is_empty()); + assert!(policy.ignore_files.is_empty()); + assert_eq!(policy.sources, vec![missing]); + } +} diff --git a/crates/fff-core/src/scan.rs b/crates/fff-core/src/scan.rs index 29f1bb32..8f4a43f3 100644 --- a/crates/fff-core/src/scan.rs +++ b/crates/fff-core/src/scan.rs @@ -395,4 +395,11 @@ fn rescubscribe_watcher_post_scan(shared_picker: &SharedFilePicker) { watcher.request_watch_dir(dir.to_path_buf()); std::ops::ControlFlow::Continue(()) }); + for dir in picker + .policy_sources() + .iter() + .filter_map(|source| source.parent()) + { + watcher.request_watch_policy_source_dir(dir.to_path_buf()); + } } diff --git a/crates/fff-core/src/walk/mod.rs b/crates/fff-core/src/walk/mod.rs index a533f0bb..6ada48ec 100644 --- a/crates/fff-core/src/walk/mod.rs +++ b/crates/fff-core/src/walk/mod.rs @@ -21,6 +21,7 @@ pub(crate) use ripgrep::walk_collect_files; pub(crate) struct WalkOutput { pub(crate) pairs: Vec<(FileItem, String)>, pub(crate) ignore_rules: Option, + pub(crate) policy_sources: Vec, } pub(crate) struct WalkIgnoreRules { @@ -143,4 +144,69 @@ mod tests { assert!(rules.is_ignored(Path::new("debug.log"))); assert!(!rules.is_ignored(Path::new("Cargo.toml"))); } + + #[test] + fn nested_negation_prevents_directory_pruning() { + let dir = tempfile::tempdir().unwrap(); + let root = dir.path(); + fs::create_dir(root.join(".git")).unwrap(); + fs::write(root.join(".gitignore"), "*\n!*.*\n!/**/\n").unwrap(); + fs::create_dir_all(root.join("sub1/sub2")).unwrap(); + fs::write(root.join("top.rs"), "").unwrap(); + fs::write(root.join("sub1/mid.rs"), "").unwrap(); + fs::write(root.join("sub1/sub2/deep.rs"), "").unwrap(); + + let counter = Arc::new(AtomicUsize::new(0)); + let out = walk_collect_files(root, true, false, 1, &counter).unwrap(); + let names: Vec<_> = out.pairs.into_iter().map(|(_, rel)| rel).collect(); + + assert!(names.contains(&"top.rs".to_string()), "got {names:?}"); + assert!(names.contains(&"sub1/mid.rs".to_string()), "got {names:?}"); + assert!( + names.contains(&"sub1/sub2/deep.rs".to_string()), + "got {names:?}" + ); + } + + #[cfg(feature = "zlob")] + #[test] + fn git_exclude_layers_follow_git_precedence() { + let dir = tempfile::tempdir().unwrap(); + let root = dir.path(); + let repo = git2::Repository::init(root).unwrap(); + let global = root.join("global-ignore"); + fs::write(&global, "*.tmp\n*.info\n").unwrap(); + fs::create_dir_all(repo.commondir().join("info")).unwrap(); + fs::write(repo.commondir().join("info/exclude"), "!info-keep.tmp\n").unwrap(); + fs::write(root.join(".gitignore"), "!root-keep.info\n").unwrap(); + repo.config() + .unwrap() + .set_str("core.excludesFile", global.to_str().unwrap()) + .unwrap(); + for path in [ + "drop.tmp", + "info-keep.tmp", + "drop.info", + "root-keep.info", + "visible.md", + ] { + fs::write(root.join(path), "").unwrap(); + } + + let counter = Arc::new(AtomicUsize::new(0)); + let out = walk_collect_files(root, true, false, 1, &counter).unwrap(); + let names: Vec<_> = out.pairs.into_iter().map(|(_, rel)| rel).collect(); + + assert!(!names.contains(&"drop.tmp".to_string()), "got {names:?}"); + assert!( + names.contains(&"info-keep.tmp".to_string()), + "got {names:?}" + ); + assert!(!names.contains(&"drop.info".to_string()), "got {names:?}"); + assert!( + names.contains(&"root-keep.info".to_string()), + "got {names:?}" + ); + assert!(names.contains(&"visible.md".to_string()), "got {names:?}"); + } } diff --git a/crates/fff-core/src/walk/ripgrep.rs b/crates/fff-core/src/walk/ripgrep.rs index def4599b..a9ce928e 100644 --- a/crates/fff-core/src/walk/ripgrep.rs +++ b/crates/fff-core/src/walk/ripgrep.rs @@ -17,17 +17,26 @@ pub(crate) fn walk_collect_files( threads: usize, synced_files_count: &Arc, ) -> crate::Result { + let policy = crate::ignore::GitIgnorePolicy::discover(base_path); let mut walk_builder = WalkBuilder::new(base_path); walk_builder // this is a very important guard for the user opening ~/ or other root non-git dir .hidden(!is_git_repo) .git_ignore(true) - .git_exclude(true) - .git_global(true) + // User and repository-wide excludes enter through GitIgnorePolicy so + // scan and watcher rebuild from one precedence-ordered source list. + .git_exclude(false) + .git_global(false) .ignore(true) .follow_links(follow_symlinks) .threads(threads); + for path in &policy.ignore_files { + if let Some(error) = walk_builder.add_ignore(path) { + tracing::warn!(?error, path = %path.display(), "Failed to load Git ignore policy source"); + } + } + if !is_git_repo && let Some(overrides) = non_git_repo_overrides(base_path) { walk_builder.overrides(overrides); } @@ -68,5 +77,6 @@ pub(crate) fn walk_collect_files( Ok(WalkOutput { pairs: pairs.into_inner(), ignore_rules: None, + policy_sources: policy.sources, }) } diff --git a/crates/fff-core/src/walk/zlob.rs b/crates/fff-core/src/walk/zlob.rs index 0978ad1b..bff2ff4f 100644 --- a/crates/fff-core/src/walk/zlob.rs +++ b/crates/fff-core/src/walk/zlob.rs @@ -20,6 +20,7 @@ pub(crate) fn walk_collect_files( threads: usize, synced_files_count: &Arc, ) -> crate::Result { + let policy = crate::ignore::GitIgnorePolicy::discover(base_path); // gitignore on; skip hidden on non-git roots (so `~/` doesn't recurse into // ~/.cache, ~/.config, etc.); optionally follow symlinks. let mut flags = WalkFlags::GITIGNORE; @@ -38,6 +39,13 @@ pub(crate) fn walk_collect_files( // Bulk-fetch the only metadata FileItem needs; zlob never stats more. .metadata(WalkMetadata::SIZE | WalkMetadata::MTIME); + let base_patterns = policy.patterns().collect::>(); + if !base_patterns.is_empty() { + builder + .base_ignore(&base_patterns) + .map_err(|e| crate::Error::WalkFailed(format!("base ignore: {e:?}")))?; + } + if !is_git_repo && !IGNORED_DIRS.is_empty() && let Err(e) = builder.extra_ignore(IGNORED_DIRS) @@ -107,5 +115,6 @@ pub(crate) fn walk_collect_files( Ok(WalkOutput { pairs, ignore_rules, + policy_sources: policy.sources, }) } diff --git a/crates/fff-core/src/watcher/background_watcher.rs b/crates/fff-core/src/watcher/background_watcher.rs index 36a9e124..09b51267 100644 --- a/crates/fff-core/src/watcher/background_watcher.rs +++ b/crates/fff-core/src/watcher/background_watcher.rs @@ -22,10 +22,15 @@ type Debouncer = notify_debouncer_full::Debouncer>>, - watch_tx: Option>, + watch_tx: Option>, owner_thread: Option>, } +enum WatchRequest { + Directory(PathBuf), + PolicySourceDirectory(PathBuf), +} + const DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(50); /// Minimum seconds between frecency tracks of the same file in AI mode. /// Prevents score inflation from rapid burst edits by AI agents. @@ -77,7 +82,7 @@ impl BackgroundWatcher { // spare watcher (configurable by the user, usually 100k - 1m) let use_recursive = cfg!(any(target_os = "macos", target_os = "windows")); - let (watch_tx, watch_rx) = mpsc::channel::(); + let (watch_tx, watch_rx) = mpsc::channel::(); let watch_tx_for_debouncer = watch_tx.clone(); let owner_weak_picker = shared_picker.weaken(); @@ -98,9 +103,6 @@ impl BackgroundWatcher { info!("Background file watcher initialized successfully"); let debouncer = Arc::new(Mutex::new(Some(debouncer))); - // Only the Linux per-dir-watch branch needs this clone; on other - // platforms the owner thread never touches the debouncer. - #[cfg(target_os = "linux")] let owner_debouncer = Arc::clone(&debouncer); let owner_span = trace_span.clone(); @@ -108,7 +110,7 @@ impl BackgroundWatcher { .name("fff-watcher-own".into()) .spawn(move || { let _g = owner_span.enter(); - while let Ok(dir) = watch_rx.recv() { + while let Ok(request) = watch_rx.recv() { // if the picker is dropped we do need to exit the loop let Some(strong_picker) = owner_weak_picker.upgrade() else { break; @@ -121,8 +123,14 @@ impl BackgroundWatcher { // from the base path (see `create_debouncer`), and // registering a second overlapping stream there produces // duplicate/out-of-order events. - #[cfg(target_os = "linux")] - { + let watch_path = match &request { + #[cfg(target_os = "linux")] + WatchRequest::Directory(path) => Some(path), + #[cfg(not(target_os = "linux"))] + WatchRequest::Directory(_) => None, + WatchRequest::PolicySourceDirectory(path) => Some(path), + }; + if let Some(dir) = watch_path { // Register the new directory with the debouncer, then // drop the mutex BEFORE doing picker-side work — see // the comment on `BackgroundWatcher::stop` for the @@ -132,7 +140,7 @@ impl BackgroundWatcher { break; }; - if let Err(e) = debouncer.watch(&dir, RecursiveMode::NonRecursive) { + if let Err(e) = debouncer.watch(dir, RecursiveMode::NonRecursive) { warn!( ?e, dir = %dir.display(), @@ -141,12 +149,14 @@ impl BackgroundWatcher { } } - track_files_from_new_directories( - &dir, - &strong_picker, - &owner_git_workdir, - &owner_git_worker, - ); + if let WatchRequest::Directory(dir) = request { + track_files_from_new_directories( + &dir, + &strong_picker, + &owner_git_workdir, + &owner_git_worker, + ); + } // Transient strong ref drops here, back // to weak-only before the next `recv()`. @@ -171,7 +181,7 @@ impl BackgroundWatcher { shared_frecency: SharedFrecency, mode: FFFMode, use_recursive: bool, - watch_tx: mpsc::Sender, + watch_tx: mpsc::Sender, git_status_worker: Arc, ) -> Result { let config = Config::default() @@ -206,7 +216,7 @@ impl BackgroundWatcher { // every new directory created has to be reflected in the picker state for dir in new_dirs { - if let Err(e) = watch_tx.send(dir) { + if let Err(e) = watch_tx.send(WatchRequest::Directory(dir)) { error!(?e, "Failed to send directory update error"); } } @@ -284,6 +294,7 @@ impl BackgroundWatcher { // to observe changes that affect git status (staging, unstaging, // committing, branch switches, merges, etc) watch_git_status_paths(&mut debouncer, git_workdir.as_ref()); + watch_policy_source_dirs(&mut debouncer, &shared_picker_for_watching); Ok(debouncer) } @@ -304,7 +315,14 @@ impl BackgroundWatcher { pub(crate) fn request_watch_dir(&self, dir: PathBuf) -> bool { match self.watch_tx.as_ref() { - Some(tx) => tx.send(dir).is_ok(), + Some(tx) => tx.send(WatchRequest::Directory(dir)).is_ok(), + None => false, + } + } + + pub(crate) fn request_watch_policy_source_dir(&self, dir: PathBuf) -> bool { + match self.watch_tx.as_ref() { + Some(tx) => tx.send(WatchRequest::PolicySourceDirectory(dir)).is_ok(), None => false, } } @@ -330,10 +348,14 @@ fn handle_debounced_events( let repo = git_workdir.as_ref().and_then(|p| Repository::open(p).ok()); // Prefer the walker's own ignore rules (zlob); grab a cheap Arc clone once // per batch so we don't hold the picker lock during filtering. - let walker_rules = shared_picker + let (walker_rules, policy_sources) = shared_picker .read() .ok() - .and_then(|g| g.as_ref().and_then(|p| p.ignore_rules())); + .and_then(|g| { + g.as_ref() + .map(|picker| (picker.ignore_rules(), picker.policy_sources())) + }) + .unwrap_or_default(); let filter = IgnoreFilter::new(base_path, walker_rules, repo.as_ref()); let mut need_full_rescan = false; let mut need_full_git_rescan = false; @@ -386,7 +408,8 @@ fn handle_debounced_events( if matches!( path.file_name().and_then(|f| f.to_str()), Some(".ignore") | Some(".gitignore") - ) { + ) || policy_sources.iter().any(|source| source == path) + { info!( "Detected change in ignore definition file: {}", path.display() @@ -873,6 +896,21 @@ fn watch_git_status_paths(debouncer: &mut Debouncer, git_workdir: Option<&PathBu } } +fn watch_policy_source_dirs(debouncer: &mut Debouncer, picker: &SharedFilePicker) { + let Some(sources) = picker + .read() + .ok() + .and_then(|guard| guard.as_ref().map(|picker| picker.policy_sources())) + else { + return; + }; + for dir in sources.iter().filter_map(|source| source.parent()) { + if let Err(error) = debouncer.watch(dir, RecursiveMode::NonRecursive) { + warn!(?error, path = %dir.display(), "Failed to watch ignore policy source directory"); + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -947,6 +985,62 @@ mod tests { assert_eq!(received[0].kind, WatchEventKind::Modified); } + #[test] + fn policy_source_change_broadcasts_rescan() { + let tmp = tempfile::tempdir().unwrap(); + let base = crate::path_utils::canonicalize(tmp.path()).unwrap(); + let repo = git2::Repository::init(&base).unwrap(); + let global = base.join("global-ignore"); + std::fs::write(&global, "*.tmp\n").unwrap(); + repo.config() + .unwrap() + .set_str("core.excludesFile", global.to_str().unwrap()) + .unwrap(); + + let shared_picker = SharedFilePicker::default(); + let shared_frecency = SharedFrecency::noop(); + let mut picker = FilePicker::new(FilePickerOptions { + base_path: base.to_string_lossy().into_owned(), + watch: false, + ..Default::default() + }) + .unwrap(); + picker.collect_files().unwrap(); + shared_picker.rebase_watches(&base); + *shared_picker.write().unwrap() = Some(picker); + + let (sender, receiver) = mpsc::channel::>(); + shared_picker + .watch_registry() + .subscribe( + &base, + "**", + WatchOptions::default(), + Box::new(move |_, events| sender.send(events.to_vec()).unwrap()), + ) + .unwrap(); + + std::fs::write(&global, "*.log\n").unwrap(); + let events = vec![DebouncedEvent::new( + Event::new(EventKind::Modify(ModifyKind::Data(DataChange::Content))).add_path(global), + Instant::now(), + )]; + handle_debounced_events( + FFFMode::Neovim, + events, + &base, + &Some(base.clone()), + &shared_picker, + &shared_frecency, + &GitStatusWorker::new(), + ); + + let delivered = receiver.recv_timeout(Duration::from_secs(1)).unwrap(); + assert_eq!(delivered.len(), 1); + assert_eq!(delivered[0].kind, WatchEventKind::Rescan); + assert_eq!(delivered[0].path, base); + } + #[test] fn dotgit_status_filter_matches_worktree_state_changes() { let tmp = tempfile::tempdir().unwrap(); diff --git a/packages/fff-node/test/ignore-policy.mjs b/packages/fff-node/test/ignore-policy.mjs new file mode 100644 index 00000000..90eaf3ea --- /dev/null +++ b/packages/fff-node/test/ignore-policy.mjs @@ -0,0 +1,94 @@ +import { strict as assert } from "node:assert"; +import { execFileSync } from "node:child_process"; +import { + mkdirSync, + mkdtempSync, + rmSync, + writeFileSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { after, before, describe, it } from "node:test"; +import { FileFinder } from "../dist/src/index.js"; + +const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + +async function waitFor(predicate, timeoutMs = 10_000) { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + const value = predicate(); + if (value) return value; + await sleep(50); + } + return predicate(); +} + +let repoDir = ""; +let globalIgnore = ""; +let finder = null; + +function indexedPaths() { + const result = finder.fileSearch("", { pageSize: 100 }); + assert.ok(result.ok, `search failed: ${!result.ok ? result.error : ""}`); + return new Set(result.value.items.map((item) => item.relativePath)); +} + +describe("fff-node Git ignore policy", { concurrency: 1 }, () => { + before(async () => { + repoDir = mkdtempSync(join(tmpdir(), "fff-ignore-policy-")); + globalIgnore = join(tmpdir(), `fff-global-ignore-${process.pid}`); + execFileSync("git", ["init", "--quiet", repoDir]); + execFileSync("git", ["-C", repoDir, "config", "core.excludesFile", globalIgnore]); + + mkdirSync(join(repoDir, "nested")); + mkdirSync(join(repoDir, ".git", "info"), { recursive: true }); + writeFileSync(globalIgnore, "*.tmp\n"); + writeFileSync(join(repoDir, ".git", "info", "exclude"), "info-only.txt\n"); + writeFileSync(join(repoDir, ".gitignore"), "!kept.tmp\nnested/*.log\n"); + writeFileSync(join(repoDir, "global.tmp"), "ignored by the global policy\n"); + writeFileSync(join(repoDir, "kept.tmp"), "root negation wins\n"); + writeFileSync(join(repoDir, "info-only.txt"), "ignored by info/exclude\n"); + writeFileSync(join(repoDir, "visible.md"), "visible\n"); + writeFileSync(join(repoDir, "nested", "ignored.log"), "ignored by root\n"); + + const result = FileFinder.create({ basePath: repoDir }); + assert.ok(result.ok, `create failed: ${!result.ok ? result.error : ""}`); + finder = result.value; + const scanned = await finder.waitForScan(10_000); + assert.ok(scanned.ok && scanned.value, "initial scan should finish"); + const watcherReady = await waitFor(() => { + const progress = finder.getScanProgress(); + return progress.ok && progress.value.isWatcherReady; + }); + assert.ok(watcherReady, "watcher should become ready"); + }); + + after(() => { + if (finder && !finder.isDestroyed) finder.destroy(); + if (repoDir) rmSync(repoDir, { recursive: true, force: true }); + if (globalIgnore) rmSync(globalIgnore, { force: true }); + }); + + it("applies global, info, root, and nested precedence in the Node binding", () => { + const paths = indexedPaths(); + assert.ok(paths.has("kept.tmp")); + assert.ok(paths.has("visible.md")); + assert.ok(!paths.has("global.tmp")); + assert.ok(!paths.has("info-only.txt")); + assert.ok(!paths.has("nested/ignored.log")); + }); + + it("rescans when an external policy source changes", async () => { + const events = []; + const subscription = finder.watch(repoDir, (batch) => events.push(...batch)); + assert.ok(subscription.ok, `watch failed: ${!subscription.ok ? subscription.error : ""}`); + + writeFileSync(globalIgnore, "*.bak\n"); + const rescan = await waitFor(() => events.some((event) => event.kind === "rescan")); + assert.ok(rescan, `expected rescan event, got ${JSON.stringify(events)}`); + + const policyApplied = await waitFor(() => indexedPaths().has("global.tmp")); + assert.ok(policyApplied, "the rebuilt index should use the changed global policy"); + subscription.value(); + }); +}); diff --git a/packages/fff-node/test/non-git-ignore-policy.mjs b/packages/fff-node/test/non-git-ignore-policy.mjs new file mode 100644 index 00000000..e080f776 --- /dev/null +++ b/packages/fff-node/test/non-git-ignore-policy.mjs @@ -0,0 +1,111 @@ +import { strict as assert } from "node:assert"; +import { spawnSync } from "node:child_process"; +import { + mkdirSync, + mkdtempSync, + rmSync, + writeFileSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { describe, it } from "node:test"; + +const scenario = process.env.FFF_NON_GIT_IGNORE_SCENARIO; +const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + +async function waitFor(predicate, timeoutMs = 10_000) { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + const value = predicate(); + if (value) return value; + await sleep(50); + } + return predicate(); +} + +async function runScenario(kind) { + const fixture = mkdtempSync(join(tmpdir(), `fff-non-git-${kind}-`)); + const vault = join(fixture, "vault"); + const configHome = join(fixture, "config"); + const home = join(fixture, "home"); + const gitConfigDir = join(configHome, "git"); + const defaultIgnore = join(gitConfigDir, "ignore"); + const configuredIgnore = join(fixture, "configured-ignore"); + const policySource = kind === "configured" ? configuredIgnore : defaultIgnore; + + mkdirSync(vault); + mkdirSync(home); + mkdirSync(gitConfigDir, { recursive: true }); + if (kind === "configured") { + writeFileSync( + join(gitConfigDir, "config"), + `[core]\n\texcludesFile = ${configuredIgnore}\n`, + ); + } + writeFileSync(policySource, "*.tmp\n"); + writeFileSync(join(vault, "global.tmp"), "ignored\n"); + writeFileSync(join(vault, "visible.md"), "visible\n"); + + process.env.XDG_CONFIG_HOME = configHome; + process.env.HOME = home; + + const { FileFinder } = await import("../dist/src/index.js"); + let finder = null; + try { + const created = FileFinder.create({ basePath: vault }); + assert.ok(created.ok, `create failed: ${!created.ok ? created.error : ""}`); + finder = created.value; + const scanned = await finder.waitForScan(10_000); + assert.ok(scanned.ok && scanned.value, "initial scan should finish"); + const watcherReady = await waitFor(() => { + const progress = finder.getScanProgress(); + return progress.ok && progress.value.isWatcherReady; + }); + assert.ok(watcherReady, "watcher should become ready"); + + const initial = finder.fileSearch("", { pageSize: 100 }); + assert.ok(initial.ok); + const initialPaths = new Set(initial.value.items.map((item) => item.relativePath)); + assert.ok(!initialPaths.has("global.tmp"), `${kind} policy was not applied`); + assert.ok(initialPaths.has("visible.md")); + + const events = []; + const subscription = finder.watch(vault, (batch) => events.push(...batch)); + assert.ok(subscription.ok, `watch failed: ${!subscription.ok ? subscription.error : ""}`); + writeFileSync(policySource, "*.bak\n"); + + const rescanned = await waitFor(() => events.some((event) => event.kind === "rescan")); + assert.ok(rescanned, `${kind} policy change did not emit rescan`); + const applied = await waitFor(() => { + const result = finder.fileSearch("", { pageSize: 100 }); + return result.ok && result.value.items.some((item) => item.relativePath === "global.tmp"); + }); + assert.ok(applied, `${kind} policy change did not rebuild the index`); + subscription.value(); + } finally { + if (finder && !finder.isDestroyed) finder.destroy(); + rmSync(fixture, { recursive: true, force: true }); + } +} + +if (scenario) { + await runScenario(scenario); +} else { + describe("fff-node non-Git vault ignore policy", { concurrency: 1 }, () => { + for (const kind of ["configured", "default"]) { + it(`applies and watches ${kind} user excludes`, () => { + const result = spawnSync(process.execPath, [fileURLToPath(import.meta.url)], { + env: { ...process.env, FFF_NON_GIT_IGNORE_SCENARIO: kind }, + encoding: "utf8", + timeout: 20_000, + }); + assert.equal( + result.status, + 0, + `child failed (${result.signal ?? "no signal"}):\n${result.stdout}\n${result.stderr}`, + ); + }); + } + }); +} From 4b096d357548fb6765384efee91ade5db05a0ded Mon Sep 17 00:00:00 2001 From: EthanHuo Date: Mon, 3 Aug 2026 14:56:06 +0800 Subject: [PATCH 2/6] build: publish fork-owned Node binaries --- .github/workflows/release.yaml | 200 +++++++----------- Makefile | 2 +- bun.lock | 42 ++-- package-lock.json | 114 +++------- packages/fff-bin-android-arm64/package.json | 4 +- packages/fff-bin-darwin-arm64/package.json | 4 +- packages/fff-bin-darwin-x64/package.json | 4 +- packages/fff-bin-linux-arm64-gnu/package.json | 4 +- .../fff-bin-linux-arm64-musl/package.json | 4 +- packages/fff-bin-linux-x64-gnu/package.json | 4 +- packages/fff-bin-linux-x64-musl/package.json | 4 +- packages/fff-bin-win32-arm64/package.json | 4 +- packages/fff-bin-win32-x64/package.json | 4 +- packages/fff-bun/README.md | 30 +-- packages/fff-bun/package.json | 18 +- packages/fff-bun/src/download.ts | 6 +- packages/fff-bun/src/embedded.ts | 6 +- packages/fff-bun/src/fff-api.ts | 4 +- packages/fff-bun/src/ffi.ts | 2 +- packages/fff-bun/src/platform.ts | 20 +- packages/fff-node/README.md | 38 ++-- packages/fff-node/package.json | 28 +-- packages/fff-node/scripts/cli.ts | 122 ----------- packages/fff-node/scripts/postinstall.ts | 50 ----- packages/fff-node/src/binary.ts | 8 +- packages/fff-node/src/fff-api.ts | 4 +- packages/fff-node/src/ffi.ts | 2 +- packages/fff-node/src/finder.ts | 2 +- packages/fff-node/src/index.ts | 6 +- packages/fff-node/src/platform.ts | 24 +-- packages/fff-node/test/demo-grep.mjs | 2 +- packages/pi-fff/package.json | 2 +- packages/pi-fff/src/aux-finders.ts | 2 +- packages/pi-fff/src/index.ts | 2 +- packages/pi-fff/src/sdk.ts | 4 +- packages/pi-fff/test/aux-pool.test.ts | 2 +- packages/pi-fff/test/extension.test.ts | 2 +- packages/shared/fff-api.ts | 4 +- 38 files changed, 236 insertions(+), 548 deletions(-) delete mode 100644 packages/fff-node/scripts/cli.ts delete mode 100644 packages/fff-node/scripts/postinstall.ts diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 5c6df145..a0d664e6 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -7,12 +7,6 @@ on: - "v*" pull_request: workflow_dispatch: - inputs: - publish_pypi: - description: "Manually build and publish Python wheels to PyPI" - required: false - default: false - type: boolean env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true @@ -187,20 +181,6 @@ jobs: lib_filename: libfff_c.so ext: so - ## macOS builds - - os: macos-latest - target: x86_64-apple-darwin - artifact_name: target/x86_64-apple-darwin/ci/libfff_c.dylib - npm_package: fff-bin-darwin-x64 - lib_filename: libfff_c.dylib - ext: dylib - - os: macos-latest - target: aarch64-apple-darwin - artifact_name: target/aarch64-apple-darwin/ci/libfff_c.dylib - npm_package: fff-bin-darwin-arm64 - lib_filename: libfff_c.dylib - ext: dylib - ## Windows builds - os: windows-latest target: x86_64-pc-windows-msvc @@ -216,7 +196,7 @@ jobs: ext: dll steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 with: persist-credentials: false @@ -224,7 +204,7 @@ jobs: run: rustup target add ${{ matrix.target }} - name: Rust cache - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 + uses: Swatinem/rust-cache@v2 with: key: c-${{ matrix.target }} @@ -256,16 +236,6 @@ jobs: cargo build --profile ci --target ${{ matrix.target }} -p fff-c --no-default-features --features zlob mv "${{ matrix.artifact_name }}" "c-lib-${{ matrix.target }}.${{ matrix.ext }}" - - name: Build for macOS - if: contains(matrix.os, 'macos') - run: | - MACOSX_DEPLOYMENT_TARGET="13" cargo build --profile ci --target ${{ matrix.target }} -p fff-c --no-default-features --features zlob - mv "${{ matrix.artifact_name }}" "c-lib-${{ matrix.target }}.${{ matrix.ext }}" - - - name: Ad-hoc sign macOS binary - if: contains(matrix.os, 'macos') - run: codesign --force --sign - "c-lib-${{ matrix.target }}.${{ matrix.ext }}" - - name: Build for Windows if: contains(matrix.os, 'windows') shell: bash @@ -280,13 +250,68 @@ jobs: cp "c-lib-${{ matrix.target }}.${{ matrix.ext }}" "packages/${{ matrix.npm_package }}/${{ matrix.lib_filename }}" - name: Upload C library artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: c-lib-${{ matrix.target }} path: c-lib-${{ matrix.target }}.* - name: Upload npm package artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 + with: + name: npm-${{ matrix.npm_package }} + path: packages/${{ matrix.npm_package }}/ + + build-c-macos: + name: Build C FFI ${{ matrix.target }} + runs-on: [self-hosted, macOS, ARM64] + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + permissions: + contents: read + strategy: + matrix: + include: + - target: x86_64-apple-darwin + artifact_name: target/x86_64-apple-darwin/ci/libfff_c.dylib + npm_package: fff-bin-darwin-x64 + - target: aarch64-apple-darwin + artifact_name: target/aarch64-apple-darwin/ci/libfff_c.dylib + npm_package: fff-bin-darwin-arm64 + steps: + - name: Prepare persistent workspace + run: | + echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" + [ -d .git ] && git reset --hard --quiet || true + size=$(du -sg target 2>/dev/null | cut -f1 || echo 0) + [ "${size:-0}" -gt 40 ] && rm -rf target || true + + - uses: actions/checkout@v7 + with: + persist-credentials: false + clean: false + + - name: Install Rust target + run: rustup target add ${{ matrix.target }} + + - name: Install Zig + uses: mlugg/setup-zig@v2 + with: + version: 0.16.0 + + - name: Build and sign + run: | + MACOSX_DEPLOYMENT_TARGET="13" cargo build --profile ci --target ${{ matrix.target }} -p fff-c --no-default-features --features zlob + mv "${{ matrix.artifact_name }}" "c-lib-${{ matrix.target }}.dylib" + codesign --force --sign - "c-lib-${{ matrix.target }}.dylib" + cp "c-lib-${{ matrix.target }}.dylib" "packages/${{ matrix.npm_package }}/libfff_c.dylib" + + - name: Upload C library artifact + uses: actions/upload-artifact@v7 + with: + name: c-lib-${{ matrix.target }} + path: c-lib-${{ matrix.target }}.dylib + + - name: Upload npm package artifact + uses: actions/upload-artifact@v7 with: name: npm-${{ matrix.npm_package }} path: packages/${{ matrix.npm_package }}/ @@ -476,22 +501,22 @@ jobs: release: name: Release - needs: [build-nvim, build-c, build-mcp, build-python, build-python-sdist] + needs: [build-nvim, build-c, build-c-macos, build-mcp, build-python, build-python-sdist] runs-on: ubuntu-latest if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/fix/use-trusted-publishing' || startsWith(github.ref, 'refs/tags/v')) permissions: contents: write steps: # we have to make sure that pushing a commit on this workflow triggers the CI for nightly neovim - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 with: token: ${{ secrets.GUSTAV_PAT || github.token }} - name: Install Lua - uses: leafo/gh-actions-lua@v12 + uses: leafo/gh-actions-lua@v13 - name: Download artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: path: ./binaries @@ -595,7 +620,7 @@ jobs: body: | ${{ steps.version.outputs.is_release == 'true' && format('Release {0}', steps.version.outputs.version) || format('Nightly release from commit: {0}', github.sha) }} - npm packages, rust crates and python wheels are available under this version ${{ steps.version.outputs.version }} + Native assets and `@celados/fff-node` packages are available under this version ${{ steps.version.outputs.version }}. ## Neovim Plugin - `{target}.so` / `.dylib` / `.dll` - Lua module for Neovim @@ -608,12 +633,7 @@ jobs: ## Python Package - `python/*.whl` / `python/*.tar.gz` - Python wheels and sdist - - Install from PyPI: `pip install fff-search` (when published) - - Update mcp via: - ```sh - curl -fsSL https://raw.githubusercontent.com/dmtrKovalenko/fff.nvim/main/install-mcp.sh | bash - ``` + - Fork builds are GitHub Release assets only; this workflow does not publish PyPI. - name: Bump Homebrew formula (uses local checksums) if: steps.version.outputs.is_release == 'true' && github.repository_owner == 'dmtrKovalenko' @@ -634,78 +654,22 @@ jobs: commit_user_name: github-actions[bot] commit_user_email: 41898282+github-actions[bot]@users.noreply.github.com - pypi-publish: - name: Publish Python wheels to PyPI - needs: [build-python, build-python-sdist] - runs-on: ubuntu-latest - if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/fix/use-trusted-publishing' || startsWith(github.ref, 'refs/tags/v')) - environment: - name: pypi - url: https://pypi.org/p/fff-search - permissions: - contents: read - id-token: write - steps: - - name: Download Python wheels and sdist - uses: actions/download-artifact@v4 - with: - pattern: python-* - path: dist - merge-multiple: true - - - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 - with: - packages-dir: dist/ - skip-existing: true - - crates-publish: - name: Publish Rust crates - needs: [build-nvim, build-c, build-mcp] - runs-on: ubuntu-latest - if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/fix/use-trusted-publishing' || startsWith(github.ref, 'refs/tags/v')) - permissions: - contents: read - id-token: write - steps: - - uses: actions/checkout@v5 - - uses: rust-lang/crates-io-auth-action@v1 - id: auth - - - name: Install Lua - uses: leafo/gh-actions-lua@v12 - - - name: Install Rust - uses: dtolnay/rust-toolchain@stable - - - name: Install cargo-edit - run: cargo install cargo-edit --force --locked - - - name: Determine version - id: version - run: lua scripts/determine-version.lua - - - name: Publish crates - env: - CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }} - run: make publish-crates V="${{ steps.version.outputs.version }}" - npm-publish: name: Publish npm packages - needs: [build-c] + needs: [build-c, build-c-macos] runs-on: ubuntu-latest if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/fix/use-trusted-publishing' || startsWith(github.ref, 'refs/tags/v')) permissions: contents: read id-token: write steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v7 - name: Install Lua - uses: leafo/gh-actions-lua@v12 + uses: leafo/gh-actions-lua@v13 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v7 with: node-version: "25" registry-url: "https://registry.npmjs.org" @@ -715,7 +679,7 @@ jobs: run: lua scripts/determine-version.lua - name: Download npm package artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: pattern: npm-* path: ./npm-packages @@ -738,37 +702,15 @@ jobs: fi done - - name: Publish bun package - run: | - VERSION="${{ steps.version.outputs.version }}" - TAG="${{ steps.version.outputs.npm_tag }}" - - echo "Publishing @ff-labs/fff-bun@${VERSION} with tag ${TAG}..." - make set-npm-version PKG=packages/fff-bun VERSION="$VERSION" - - cd packages/fff-bun - npm publish --tag "$TAG" --access public --provenance - - name: Publish Node.js package run: | VERSION="${{ steps.version.outputs.version }}" TAG="${{ steps.version.outputs.npm_tag }}" - echo "Publishing @ff-labs/fff-node@${VERSION} with tag ${TAG}..." + echo "Publishing @celados/fff-node@${VERSION} with tag ${TAG}..." make set-npm-version PKG=packages/fff-node VERSION="$VERSION" cd packages/fff-node npm install npm run build npm publish --tag "$TAG" --access public --provenance - - - name: Publish pi-fff package - run: | - VERSION="${{ steps.version.outputs.version }}" - TAG="${{ steps.version.outputs.npm_tag }}" - - echo "Publishing @ff-labs/pi-fff@${VERSION} with tag ${TAG}..." - make set-npm-version PKG=packages/pi-fff VERSION="$VERSION" - - cd packages/pi-fff - npm publish --tag "$TAG" --access public --provenance diff --git a/Makefile b/Makefile index 92e45407..84366d80 100644 --- a/Makefile +++ b/Makefile @@ -259,7 +259,7 @@ set-npm-version: pkg.optionalDependencies[dep] = '$(VERSION)'; \ } \ } \ - for (const dep of ['@ff-labs/fff-bun', '@ff-labs/fff-node']) { \ + for (const dep of ['@ff-labs/fff-bun', '@celados/fff-node']) { \ if (pkg.dependencies?.[dep]) pkg.dependencies[dep] = '$(VERSION)'; \ } \ fs.writeFileSync('$(PKG)/package.json', JSON.stringify(pkg, null, 2) + '\n'); \ diff --git a/bun.lock b/bun.lock index 93bc79fd..f9c60b5a 100644 --- a/bun.lock +++ b/bun.lock @@ -15,18 +15,19 @@ "typescript": "^5.0.0", }, "optionalDependencies": { - "@ff-labs/fff-bin-darwin-arm64": "0.0.0", - "@ff-labs/fff-bin-darwin-x64": "0.0.0", - "@ff-labs/fff-bin-linux-arm64-gnu": "0.0.0", - "@ff-labs/fff-bin-linux-arm64-musl": "0.0.0", - "@ff-labs/fff-bin-linux-x64-gnu": "0.0.0", - "@ff-labs/fff-bin-linux-x64-musl": "0.0.0", - "@ff-labs/fff-bin-win32-arm64": "0.0.0", - "@ff-labs/fff-bin-win32-x64": "0.0.0", + "@celados/fff-bin-android-arm64": "0.0.0", + "@celados/fff-bin-darwin-arm64": "0.0.0", + "@celados/fff-bin-darwin-x64": "0.0.0", + "@celados/fff-bin-linux-arm64-gnu": "0.0.0", + "@celados/fff-bin-linux-arm64-musl": "0.0.0", + "@celados/fff-bin-linux-x64-gnu": "0.0.0", + "@celados/fff-bin-linux-x64-musl": "0.0.0", + "@celados/fff-bin-win32-arm64": "0.0.0", + "@celados/fff-bin-win32-x64": "0.0.0", }, }, "packages/fff-node": { - "name": "@ff-labs/fff-node", + "name": "@celados/fff-node", "version": "0.1.37", "dependencies": { "ffi-rs": "^1.0.0", @@ -36,22 +37,23 @@ "typescript": "^5.0.0", }, "optionalDependencies": { - "@ff-labs/fff-bin-darwin-arm64": "0.0.0", - "@ff-labs/fff-bin-darwin-x64": "0.0.0", - "@ff-labs/fff-bin-linux-arm64-gnu": "0.0.0", - "@ff-labs/fff-bin-linux-arm64-musl": "0.0.0", - "@ff-labs/fff-bin-linux-x64-gnu": "0.0.0", - "@ff-labs/fff-bin-linux-x64-musl": "0.0.0", - "@ff-labs/fff-bin-win32-arm64": "0.0.0", - "@ff-labs/fff-bin-win32-x64": "0.0.0", + "@celados/fff-bin-android-arm64": "0.0.0", + "@celados/fff-bin-darwin-arm64": "0.0.0", + "@celados/fff-bin-darwin-x64": "0.0.0", + "@celados/fff-bin-linux-arm64-gnu": "0.0.0", + "@celados/fff-bin-linux-arm64-musl": "0.0.0", + "@celados/fff-bin-linux-x64-gnu": "0.0.0", + "@celados/fff-bin-linux-x64-musl": "0.0.0", + "@celados/fff-bin-win32-arm64": "0.0.0", + "@celados/fff-bin-win32-x64": "0.0.0", }, }, "packages/pi-fff": { "name": "@ff-labs/pi-fff", "version": "0.6.0", "dependencies": { + "@celados/fff-node": "*", "@ff-labs/fff-bun": "*", - "@ff-labs/fff-node": "*", }, "devDependencies": { "@types/node": "^22.0.0", @@ -137,6 +139,8 @@ "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.4", "", { "os": "win32", "cpu": "x64" }, "sha512-gnOHKVPFAAPrpoPt2t+Q6FZ7RPry/FDV3GcpU53P3PtLNnQjBmKyN2Vh/JtqXet+H4pme8CC76rScwdjDcT1/A=="], + "@celados/fff-node": ["@celados/fff-node@workspace:packages/fff-node"], + "@earendil-works/pi-agent-core": ["@earendil-works/pi-agent-core@0.78.1", "", { "dependencies": { "@earendil-works/pi-ai": "^0.78.1", "ignore": "7.0.5", "typebox": "1.1.38", "yaml": "2.9.0" } }, "sha512-oPwVRkkAvyKPWyM7E4k+EaTNmynbYn7ZLG/LBh9BUnMNb2gvpMp+VQ420R6JCJ20uogSqrHnWTyosSa/rU8lVw=="], "@earendil-works/pi-ai": ["@earendil-works/pi-ai@0.78.1", "", { "dependencies": { "@anthropic-ai/sdk": "0.91.1", "@aws-sdk/client-bedrock-runtime": "3.1048.0", "@google/genai": "1.52.0", "@mistralai/mistralai": "2.2.1", "@smithy/node-http-handler": "4.7.3", "http-proxy-agent": "7.0.2", "https-proxy-agent": "7.0.6", "openai": "6.26.0", "partial-json": "0.1.7", "typebox": "1.1.38" }, "bin": { "pi-ai": "dist/cli.js" } }, "sha512-CM2pkTs1iupG/maw381lC9Q/Y/aQaMGK7GILc28ttImD0ci3LDwKroDsGkWbly5JIy3iqxdRxB9JlG7vvzCzTg=="], @@ -147,8 +151,6 @@ "@ff-labs/fff-bun": ["@ff-labs/fff-bun@workspace:packages/fff-bun"], - "@ff-labs/fff-node": ["@ff-labs/fff-node@workspace:packages/fff-node"], - "@ff-labs/pi-fff": ["@ff-labs/pi-fff@workspace:packages/pi-fff"], "@google/genai": ["@google/genai@1.52.0", "", { "dependencies": { "google-auth-library": "^10.3.0", "p-retry": "^4.6.2", "protobufjs": "^7.5.4", "ws": "^8.18.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.25.2" }, "optionalPeers": ["@modelcontextprotocol/sdk"] }, "sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q=="], diff --git a/package-lock.json b/package-lock.json index 429b55de..9158746c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -931,6 +931,10 @@ "url": "https://github.com/sponsors/Borewit" } }, + "node_modules/@celados/fff-node": { + "resolved": "packages/fff-node", + "link": true + }, "node_modules/@earendil-works/pi-agent-core": { "version": "0.74.0", "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.74.0.tgz", @@ -1069,10 +1073,6 @@ "resolved": "packages/fff-bun", "link": true }, - "node_modules/@ff-labs/fff-node": { - "resolved": "packages/fff-node", - "link": true - }, "node_modules/@ff-labs/pi-fff": { "resolved": "packages/pi-fff", "link": true @@ -4113,51 +4113,19 @@ "bun": ">=1.0.0" }, "optionalDependencies": { - "@ff-labs/fff-bin-android-arm64": "0.0.0", - "@ff-labs/fff-bin-darwin-arm64": "0.0.0", - "@ff-labs/fff-bin-darwin-x64": "0.0.0", - "@ff-labs/fff-bin-linux-arm64-gnu": "0.0.0", - "@ff-labs/fff-bin-linux-arm64-musl": "0.0.0", - "@ff-labs/fff-bin-linux-x64-gnu": "0.0.0", - "@ff-labs/fff-bin-linux-x64-musl": "0.0.0", - "@ff-labs/fff-bin-win32-arm64": "0.0.0", - "@ff-labs/fff-bin-win32-x64": "0.0.0" + "@celados/fff-bin-android-arm64": "0.0.0", + "@celados/fff-bin-darwin-arm64": "0.0.0", + "@celados/fff-bin-darwin-x64": "0.0.0", + "@celados/fff-bin-linux-arm64-gnu": "0.0.0", + "@celados/fff-bin-linux-arm64-musl": "0.0.0", + "@celados/fff-bin-linux-x64-gnu": "0.0.0", + "@celados/fff-bin-linux-x64-musl": "0.0.0", + "@celados/fff-bin-win32-arm64": "0.0.0", + "@celados/fff-bin-win32-x64": "0.0.0" } }, - "packages/fff-bun/node_modules/@ff-labs/fff-bin-darwin-arm64": { - "version": "0.0.0", - "optional": true - }, - "packages/fff-bun/node_modules/@ff-labs/fff-bin-darwin-x64": { - "version": "0.0.0", - "optional": true - }, - "packages/fff-bun/node_modules/@ff-labs/fff-bin-linux-arm64-gnu": { - "version": "0.0.0", - "optional": true - }, - "packages/fff-bun/node_modules/@ff-labs/fff-bin-linux-arm64-musl": { - "version": "0.0.0", - "optional": true - }, - "packages/fff-bun/node_modules/@ff-labs/fff-bin-linux-x64-gnu": { - "version": "0.0.0", - "optional": true - }, - "packages/fff-bun/node_modules/@ff-labs/fff-bin-linux-x64-musl": { - "version": "0.0.0", - "optional": true - }, - "packages/fff-bun/node_modules/@ff-labs/fff-bin-win32-arm64": { - "version": "0.0.0", - "optional": true - }, - "packages/fff-bun/node_modules/@ff-labs/fff-bin-win32-x64": { - "version": "0.0.0", - "optional": true - }, "packages/fff-node": { - "name": "@ff-labs/fff-node", + "name": "@celados/fff-node", "version": "0.1.37", "cpu": [ "x64", @@ -4181,56 +4149,24 @@ "node": ">=18.0.0" }, "optionalDependencies": { - "@ff-labs/fff-bin-android-arm64": "0.0.0", - "@ff-labs/fff-bin-darwin-arm64": "0.0.0", - "@ff-labs/fff-bin-darwin-x64": "0.0.0", - "@ff-labs/fff-bin-linux-arm64-gnu": "0.0.0", - "@ff-labs/fff-bin-linux-arm64-musl": "0.0.0", - "@ff-labs/fff-bin-linux-x64-gnu": "0.0.0", - "@ff-labs/fff-bin-linux-x64-musl": "0.0.0", - "@ff-labs/fff-bin-win32-arm64": "0.0.0", - "@ff-labs/fff-bin-win32-x64": "0.0.0" + "@celados/fff-bin-android-arm64": "0.0.0", + "@celados/fff-bin-darwin-arm64": "0.0.0", + "@celados/fff-bin-darwin-x64": "0.0.0", + "@celados/fff-bin-linux-arm64-gnu": "0.0.0", + "@celados/fff-bin-linux-arm64-musl": "0.0.0", + "@celados/fff-bin-linux-x64-gnu": "0.0.0", + "@celados/fff-bin-linux-x64-musl": "0.0.0", + "@celados/fff-bin-win32-arm64": "0.0.0", + "@celados/fff-bin-win32-x64": "0.0.0" } }, - "packages/fff-node/node_modules/@ff-labs/fff-bin-darwin-arm64": { - "version": "0.0.0", - "optional": true - }, - "packages/fff-node/node_modules/@ff-labs/fff-bin-darwin-x64": { - "version": "0.0.0", - "optional": true - }, - "packages/fff-node/node_modules/@ff-labs/fff-bin-linux-arm64-gnu": { - "version": "0.0.0", - "optional": true - }, - "packages/fff-node/node_modules/@ff-labs/fff-bin-linux-arm64-musl": { - "version": "0.0.0", - "optional": true - }, - "packages/fff-node/node_modules/@ff-labs/fff-bin-linux-x64-gnu": { - "version": "0.0.0", - "optional": true - }, - "packages/fff-node/node_modules/@ff-labs/fff-bin-linux-x64-musl": { - "version": "0.0.0", - "optional": true - }, - "packages/fff-node/node_modules/@ff-labs/fff-bin-win32-arm64": { - "version": "0.0.0", - "optional": true - }, - "packages/fff-node/node_modules/@ff-labs/fff-bin-win32-x64": { - "version": "0.0.0", - "optional": true - }, "packages/pi-fff": { "name": "@ff-labs/pi-fff", "version": "0.6.0", "license": "MIT", "dependencies": { - "@ff-labs/fff-bun": "*", - "@ff-labs/fff-node": "*" + "@celados/fff-node": "*", + "@ff-labs/fff-bun": "*" }, "devDependencies": { "@types/node": "^22.0.0", diff --git a/packages/fff-bin-android-arm64/package.json b/packages/fff-bin-android-arm64/package.json index 71f8405c..7edbf0f4 100644 --- a/packages/fff-bin-android-arm64/package.json +++ b/packages/fff-bin-android-arm64/package.json @@ -1,5 +1,5 @@ { - "name": "@ff-labs/fff-bin-android-arm64", + "name": "@celados/fff-bin-android-arm64", "version": "0.0.0", "description": "fff native binary for Android ARM64 (Termux)", "os": ["android"], @@ -12,7 +12,7 @@ "license": "MIT", "repository": { "type": "git", - "url": "git+https://github.com/dmtrKovalenko/fff.git", + "url": "git+https://github.com/celados/fff.git", "directory": "packages/fff-bin-android-arm64" } } diff --git a/packages/fff-bin-darwin-arm64/package.json b/packages/fff-bin-darwin-arm64/package.json index 8cef16b7..d12ad5f8 100644 --- a/packages/fff-bin-darwin-arm64/package.json +++ b/packages/fff-bin-darwin-arm64/package.json @@ -1,5 +1,5 @@ { - "name": "@ff-labs/fff-bin-darwin-arm64", + "name": "@celados/fff-bin-darwin-arm64", "version": "0.0.0", "description": "fff native binary for macOS ARM64 (Apple Silicon)", "os": ["darwin"], @@ -12,7 +12,7 @@ "license": "MIT", "repository": { "type": "git", - "url": "git+https://github.com/dmtrKovalenko/fff.git", + "url": "git+https://github.com/celados/fff.git", "directory": "packages/fff-bin-darwin-arm64" } } diff --git a/packages/fff-bin-darwin-x64/package.json b/packages/fff-bin-darwin-x64/package.json index b43b9916..c47f8343 100644 --- a/packages/fff-bin-darwin-x64/package.json +++ b/packages/fff-bin-darwin-x64/package.json @@ -1,5 +1,5 @@ { - "name": "@ff-labs/fff-bin-darwin-x64", + "name": "@celados/fff-bin-darwin-x64", "version": "0.0.0", "description": "fff native binary for macOS x64 (Intel)", "os": ["darwin"], @@ -12,7 +12,7 @@ "license": "MIT", "repository": { "type": "git", - "url": "git+https://github.com/dmtrKovalenko/fff.git", + "url": "git+https://github.com/celados/fff.git", "directory": "packages/fff-bin-darwin-x64" } } diff --git a/packages/fff-bin-linux-arm64-gnu/package.json b/packages/fff-bin-linux-arm64-gnu/package.json index 01106ace..368a8ffd 100644 --- a/packages/fff-bin-linux-arm64-gnu/package.json +++ b/packages/fff-bin-linux-arm64-gnu/package.json @@ -1,5 +1,5 @@ { - "name": "@ff-labs/fff-bin-linux-arm64-gnu", + "name": "@celados/fff-bin-linux-arm64-gnu", "version": "0.0.0", "description": "fff native binary for Linux ARM64 (glibc)", "os": ["linux"], @@ -12,7 +12,7 @@ "license": "MIT", "repository": { "type": "git", - "url": "git+https://github.com/dmtrKovalenko/fff.git", + "url": "git+https://github.com/celados/fff.git", "directory": "packages/fff-bin-linux-arm64-gnu" }, "libc": ["glibc"] diff --git a/packages/fff-bin-linux-arm64-musl/package.json b/packages/fff-bin-linux-arm64-musl/package.json index 06e031cb..de309bdf 100644 --- a/packages/fff-bin-linux-arm64-musl/package.json +++ b/packages/fff-bin-linux-arm64-musl/package.json @@ -1,5 +1,5 @@ { - "name": "@ff-labs/fff-bin-linux-arm64-musl", + "name": "@celados/fff-bin-linux-arm64-musl", "version": "0.0.0", "description": "fff native binary for Linux ARM64 (musl)", "os": ["linux"], @@ -12,7 +12,7 @@ "license": "MIT", "repository": { "type": "git", - "url": "git+https://github.com/dmtrKovalenko/fff.git", + "url": "git+https://github.com/celados/fff.git", "directory": "packages/fff-bin-linux-arm64-musl" }, "libc": ["musl"] diff --git a/packages/fff-bin-linux-x64-gnu/package.json b/packages/fff-bin-linux-x64-gnu/package.json index 4995c980..3f30cdb3 100644 --- a/packages/fff-bin-linux-x64-gnu/package.json +++ b/packages/fff-bin-linux-x64-gnu/package.json @@ -1,5 +1,5 @@ { - "name": "@ff-labs/fff-bin-linux-x64-gnu", + "name": "@celados/fff-bin-linux-x64-gnu", "version": "0.0.0", "description": "fff native binary for Linux x64 (glibc)", "os": ["linux"], @@ -12,7 +12,7 @@ "license": "MIT", "repository": { "type": "git", - "url": "git+https://github.com/dmtrKovalenko/fff.git", + "url": "git+https://github.com/celados/fff.git", "directory": "packages/fff-bin-linux-x64-gnu" }, "libc": ["glibc"] diff --git a/packages/fff-bin-linux-x64-musl/package.json b/packages/fff-bin-linux-x64-musl/package.json index 8dcee424..d49ce3ab 100644 --- a/packages/fff-bin-linux-x64-musl/package.json +++ b/packages/fff-bin-linux-x64-musl/package.json @@ -1,5 +1,5 @@ { - "name": "@ff-labs/fff-bin-linux-x64-musl", + "name": "@celados/fff-bin-linux-x64-musl", "version": "0.0.0", "description": "fff native binary for Linux x64 (musl)", "os": ["linux"], @@ -12,7 +12,7 @@ "license": "MIT", "repository": { "type": "git", - "url": "git+https://github.com/dmtrKovalenko/fff.git", + "url": "git+https://github.com/celados/fff.git", "directory": "packages/fff-bin-linux-x64-musl" }, "libc": ["musl"] diff --git a/packages/fff-bin-win32-arm64/package.json b/packages/fff-bin-win32-arm64/package.json index 27ad95de..7d373d30 100644 --- a/packages/fff-bin-win32-arm64/package.json +++ b/packages/fff-bin-win32-arm64/package.json @@ -1,5 +1,5 @@ { - "name": "@ff-labs/fff-bin-win32-arm64", + "name": "@celados/fff-bin-win32-arm64", "version": "0.0.0", "description": "fff native binary for Windows ARM64", "os": ["win32"], @@ -12,7 +12,7 @@ "license": "MIT", "repository": { "type": "git", - "url": "git+https://github.com/dmtrKovalenko/fff.git", + "url": "git+https://github.com/celados/fff.git", "directory": "packages/fff-bin-win32-arm64" } } diff --git a/packages/fff-bin-win32-x64/package.json b/packages/fff-bin-win32-x64/package.json index 7f3a90bc..1d859c1f 100644 --- a/packages/fff-bin-win32-x64/package.json +++ b/packages/fff-bin-win32-x64/package.json @@ -1,5 +1,5 @@ { - "name": "@ff-labs/fff-bin-win32-x64", + "name": "@celados/fff-bin-win32-x64", "version": "0.0.0", "description": "fff native binary for Windows x64", "os": ["win32"], @@ -12,7 +12,7 @@ "license": "MIT", "repository": { "type": "git", - "url": "git+https://github.com/dmtrKovalenko/fff.git", + "url": "git+https://github.com/celados/fff.git", "directory": "packages/fff-bin-win32-x64" } } diff --git a/packages/fff-bun/README.md b/packages/fff-bun/README.md index 6dded878..bdde5e9f 100644 --- a/packages/fff-bun/README.md +++ b/packages/fff-bun/README.md @@ -12,22 +12,22 @@ Faster than ripgrep & fzf on any workflow that runs more than once per process. bun add @ff-labs/fff-bun ``` -The correct native binary for your platform is installed automatically via platform-specific packages (e.g. `@ff-labs/fff-bin-darwin-arm64`, `@ff-labs/fff-bin-linux-x64-gnu`) +The correct native binary for your platform is installed automatically via fork-owned platform packages (e.g. `@celados/fff-bin-darwin-arm64`, `@celados/fff-bin-linux-x64-gnu`). ### Supported Platforms | Platform | Architecture | Package | | -------- | --------------------- | ----------------------------------- | -| macOS | ARM64 (Apple Silicon) | `@ff-labs/fff-bin-darwin-arm64` | -| macOS | x64 (Intel) | `@ff-labs/fff-bin-darwin-x64` | -| Linux | x64 (glibc) | `@ff-labs/fff-bin-linux-x64-gnu` | -| Linux | ARM64 (glibc) | `@ff-labs/fff-bin-linux-arm64-gnu` | -| Linux | x64 (musl) | `@ff-labs/fff-bin-linux-x64-musl` | -| Linux | ARM64 (musl) | `@ff-labs/fff-bin-linux-arm64-musl` | -| Windows | x64 | `@ff-labs/fff-bin-win32-x64` | -| Windows | ARM64 | `@ff-labs/fff-bin-win32-arm64` | +| macOS | ARM64 (Apple Silicon) | `@celados/fff-bin-darwin-arm64` | +| macOS | x64 (Intel) | `@celados/fff-bin-darwin-x64` | +| Linux | x64 (glibc) | `@celados/fff-bin-linux-x64-gnu` | +| Linux | ARM64 (glibc) | `@celados/fff-bin-linux-arm64-gnu` | +| Linux | x64 (musl) | `@celados/fff-bin-linux-x64-musl` | +| Linux | ARM64 (musl) | `@celados/fff-bin-linux-arm64-musl` | +| Windows | x64 | `@celados/fff-bin-win32-x64` | +| Windows | ARM64 | `@celados/fff-bin-win32-arm64` | -If the platform package isn't available, the postinstall script will attempt to download from GitHub releases as a fallback. +The package does not download executable code at install or runtime. Unsupported platforms fail during native-library resolution. ### Standalone executables (`bun build --compile`) @@ -183,16 +183,6 @@ cargo build --release -p fff-c # The binary will be at target/release/libfff_c.{so,dylib,dll} ``` -## CLI examples - -```bash -# Download binary manually (fallback if npm package unavailable) -bunx fff download [tag] - -# Show platform info and binary location -bunx fff info -``` - ## License MIT diff --git a/packages/fff-bun/package.json b/packages/fff-bun/package.json index 97ae109e..30334dd2 100644 --- a/packages/fff-bun/package.json +++ b/packages/fff-bun/package.json @@ -59,15 +59,15 @@ }, "homepage": "https://github.com/dmtrKovalenko/fff#readme", "optionalDependencies": { - "@ff-labs/fff-bin-darwin-arm64": "0.0.0", - "@ff-labs/fff-bin-darwin-x64": "0.0.0", - "@ff-labs/fff-bin-linux-x64-gnu": "0.0.0", - "@ff-labs/fff-bin-linux-arm64-gnu": "0.0.0", - "@ff-labs/fff-bin-linux-x64-musl": "0.0.0", - "@ff-labs/fff-bin-linux-arm64-musl": "0.0.0", - "@ff-labs/fff-bin-win32-x64": "0.0.0", - "@ff-labs/fff-bin-win32-arm64": "0.0.0", - "@ff-labs/fff-bin-android-arm64": "0.0.0" + "@celados/fff-bin-darwin-arm64": "0.0.0", + "@celados/fff-bin-darwin-x64": "0.0.0", + "@celados/fff-bin-linux-x64-gnu": "0.0.0", + "@celados/fff-bin-linux-arm64-gnu": "0.0.0", + "@celados/fff-bin-linux-x64-musl": "0.0.0", + "@celados/fff-bin-linux-arm64-musl": "0.0.0", + "@celados/fff-bin-win32-x64": "0.0.0", + "@celados/fff-bin-win32-arm64": "0.0.0", + "@celados/fff-bin-android-arm64": "0.0.0" }, "devDependencies": { "@types/bun": "^1.3.8", diff --git a/packages/fff-bun/src/download.ts b/packages/fff-bun/src/download.ts index 74918f3d..41d168fa 100644 --- a/packages/fff-bun/src/download.ts +++ b/packages/fff-bun/src/download.ts @@ -2,7 +2,7 @@ * Binary resolution utilities for fff * * Resolves the native library from: - * 1. Platform-specific npm package (e.g. @ff-labs/fff-bin-darwin-arm64) + * 1. Platform-specific npm package (e.g. @celados/fff-bin-darwin-arm64) * 2. Local dev build (target/release or target/debug) */ @@ -48,8 +48,8 @@ export function binaryExists(): boolean { /** * Try to resolve the binary from the platform-specific npm package. * - * When users install @ff-labs/fff-bun, npm/bun automatically installs the matching - * optionalDependency (e.g. @ff-labs/fff-bin-darwin-arm64). We resolve the binary + * When users install the Bun frontend, npm/bun automatically installs the matching + * optionalDependency (e.g. @celados/fff-bin-darwin-arm64). We resolve the binary * path by requiring that package's package.json and looking for the binary * in the same directory. */ diff --git a/packages/fff-bun/src/embedded.ts b/packages/fff-bun/src/embedded.ts index 569688fc..7854b258 100644 --- a/packages/fff-bun/src/embedded.ts +++ b/packages/fff-bun/src/embedded.ts @@ -23,7 +23,7 @@ async function importFile(promise: Promise<{ default: string }>): Promise { if (process.platform === "darwin") { return importFile( - import(`@ff-labs/fff-bin-darwin-${process.arch}/libfff_c.dylib`, { + import(`@celados/fff-bin-darwin-${process.arch}/libfff_c.dylib`, { with: { type: "file" }, }), ); @@ -31,7 +31,7 @@ async function resolveEmbeddedLibPath(): Promise { if (process.platform === "win32") { return importFile( - import(`@ff-labs/fff-bin-win32-${process.arch}/fff_c.dll`, { + import(`@celados/fff-bin-win32-${process.arch}/fff_c.dll`, { with: { type: "file" }, }), ); @@ -40,7 +40,7 @@ async function resolveEmbeddedLibPath(): Promise { if (process.platform === "linux") { return importFile( import( - `@ff-labs/fff-bin-linux-${process.arch}-${typeof FFF_LIBC === "string" ? FFF_LIBC : "gnu"}/libfff_c.so`, + `@celados/fff-bin-linux-${process.arch}-${typeof FFF_LIBC === "string" ? FFF_LIBC : "gnu"}/libfff_c.so`, { with: { type: "file" } } ), ); diff --git a/packages/fff-bun/src/fff-api.ts b/packages/fff-bun/src/fff-api.ts index d22c6de5..1aeaaf71 100644 --- a/packages/fff-bun/src/fff-api.ts +++ b/packages/fff-bun/src/fff-api.ts @@ -6,7 +6,7 @@ /** * The shared public API surface for the fff file finder, implemented identically - * by `@ff-labs/fff-node` and `@ff-labs/fff-bun`. + * by the Node.js and Bun frontends. * * This file is the single source of truth for every type, helper, and the * `FileFinderApi` interface that crosses the package boundary. It is copied @@ -553,7 +553,7 @@ export interface MultiGrepOptions { /** * The shared instance surface implemented by `FileFinder` in both - * `@ff-labs/fff-node` and `@ff-labs/fff-bun`. + * the Node.js and Bun frontends. * * Both packages must implement this identically. Only instance members belong * here. Static helpers (`create`, `isAvailable`, `ensureLoaded`, diff --git a/packages/fff-bun/src/ffi.ts b/packages/fff-bun/src/ffi.ts index 0a9f3b07..88ad3476 100644 --- a/packages/fff-bun/src/ffi.ts +++ b/packages/fff-bun/src/ffi.ts @@ -361,7 +361,7 @@ function libNotFoundMessage(): string { ].join("\n"); } - return "fff native library was not embedded into this executable. Rebuild with `bun build --compile` and ensure the @ff-labs/fff-bin-* package for this platform is installed."; + return "fff native library was not embedded into this executable. Rebuild with `bun build --compile` and ensure the @celados/fff-bin-* package for this platform is installed."; } return "fff native library not found. Build from source with `cargo build --release -p fff-c` or install the platform package."; diff --git a/packages/fff-bun/src/platform.ts b/packages/fff-bun/src/platform.ts index 11070e20..6928a24c 100644 --- a/packages/fff-bun/src/platform.ts +++ b/packages/fff-bun/src/platform.ts @@ -101,21 +101,21 @@ export function getLibFilename(): string { * Map from Rust target triple to npm platform package name */ const TRIPLE_TO_NPM_PACKAGE: Record = { - "aarch64-apple-darwin": "@ff-labs/fff-bin-darwin-arm64", - "x86_64-apple-darwin": "@ff-labs/fff-bin-darwin-x64", - "x86_64-unknown-linux-gnu": "@ff-labs/fff-bin-linux-x64-gnu", - "aarch64-unknown-linux-gnu": "@ff-labs/fff-bin-linux-arm64-gnu", - "x86_64-unknown-linux-musl": "@ff-labs/fff-bin-linux-x64-musl", - "aarch64-unknown-linux-musl": "@ff-labs/fff-bin-linux-arm64-musl", - "x86_64-pc-windows-msvc": "@ff-labs/fff-bin-win32-x64", - "aarch64-pc-windows-msvc": "@ff-labs/fff-bin-win32-arm64", - "aarch64-linux-android": "@ff-labs/fff-bin-android-arm64", + "aarch64-apple-darwin": "@celados/fff-bin-darwin-arm64", + "x86_64-apple-darwin": "@celados/fff-bin-darwin-x64", + "x86_64-unknown-linux-gnu": "@celados/fff-bin-linux-x64-gnu", + "aarch64-unknown-linux-gnu": "@celados/fff-bin-linux-arm64-gnu", + "x86_64-unknown-linux-musl": "@celados/fff-bin-linux-x64-musl", + "aarch64-unknown-linux-musl": "@celados/fff-bin-linux-arm64-musl", + "x86_64-pc-windows-msvc": "@celados/fff-bin-win32-x64", + "aarch64-pc-windows-msvc": "@celados/fff-bin-win32-arm64", + "aarch64-linux-android": "@celados/fff-bin-android-arm64", }; /** * Get the npm package name for the current platform's native binary. * - * @returns Package name like "@ff-labs/fff-bin-darwin-arm64" + * @returns Package name like "@celados/fff-bin-darwin-arm64" * @throws If the current platform is not supported */ export function getNpmPackageName(): string { diff --git a/packages/fff-node/README.md b/packages/fff-node/README.md index b0b7e85a..2e52211e 100644 --- a/packages/fff-node/README.md +++ b/packages/fff-node/README.md @@ -9,25 +9,25 @@ Faster than ripgrep & fzf on any workflow that runs more than once per process. ## Installation ```bash -npm install @ff-labs/fff-node +npm install @celados/fff-node ``` -The correct native binary for your platform is installed automatically via platform-specific packages (e.g. `@ff-labs/fff-bin-darwin-arm64`, `@ff-labs/fff-bin-linux-x64-gnu`) +The correct native binary for your platform is installed automatically via platform-specific `@celados/fff-bin-*` packages. ### Supported Platforms | Platform | Architecture | Package | | -------- | --------------------- | ----------------------------------- | -| macOS | ARM64 (Apple Silicon) | `@ff-labs/fff-bin-darwin-arm64` | -| macOS | x64 (Intel) | `@ff-labs/fff-bin-darwin-x64` | -| Linux | x64 (glibc) | `@ff-labs/fff-bin-linux-x64-gnu` | -| Linux | ARM64 (glibc) | `@ff-labs/fff-bin-linux-arm64-gnu` | -| Linux | x64 (musl) | `@ff-labs/fff-bin-linux-x64-musl` | -| Linux | ARM64 (musl) | `@ff-labs/fff-bin-linux-arm64-musl` | -| Windows | x64 | `@ff-labs/fff-bin-win32-x64` | -| Windows | ARM64 | `@ff-labs/fff-bin-win32-arm64` | +| macOS | ARM64 (Apple Silicon) | `@celados/fff-bin-darwin-arm64` | +| macOS | x64 (Intel) | `@celados/fff-bin-darwin-x64` | +| Linux | x64 (glibc) | `@celados/fff-bin-linux-x64-gnu` | +| Linux | ARM64 (glibc) | `@celados/fff-bin-linux-arm64-gnu` | +| Linux | x64 (musl) | `@celados/fff-bin-linux-x64-musl` | +| Linux | ARM64 (musl) | `@celados/fff-bin-linux-arm64-musl` | +| Windows | x64 | `@celados/fff-bin-win32-x64` | +| Windows | ARM64 | `@celados/fff-bin-win32-arm64` | -If the platform package isn't available, the postinstall script will attempt to download from GitHub releases as a fallback. +The package never downloads executable code at install or runtime. Unsupported platforms fail at native-library resolution. ## Quick Start @@ -35,7 +35,7 @@ Each `FileFinder` instance owns an independent native index. Create one, wait for the initial scan, then run as many searches as you like. ```typescript -import { FileFinder } from "@ff-labs/fff-node"; +import { FileFinder } from "@celados/fff-node"; // Create an instance bound to a directory const created = FileFinder.create({ basePath: "/path/to/project" }); @@ -152,8 +152,8 @@ If prebuilt binaries aren't available for your platform: ```bash # Clone the repository -git clone https://github.com/dmtrKovalenko/fff.nvim -cd fff.nvim +git clone https://github.com/celados/fff +cd fff # Build the C library cargo build --release -p fff-c @@ -161,16 +161,6 @@ cargo build --release -p fff-c # The binary will be at target/release/libfff_c.{so,dylib,dll} ``` -## CLI examples - -```bash -# Download binary manually (fallback if npm package unavailable) -npx @ff-labs/fff-node download [tag] - -# Show platform info and binary location -npx @ff-labs/fff-node info -``` - ## License MIT diff --git a/packages/fff-node/package.json b/packages/fff-node/package.json index 5e501c75..4d6c77d2 100644 --- a/packages/fff-node/package.json +++ b/packages/fff-node/package.json @@ -1,5 +1,5 @@ { - "name": "@ff-labs/fff-node", + "name": "@celados/fff-node", "version": "0.1.37", "private": false, "description": "High-performance fuzzy file finder for Node.js - perfect for LLM agent tools", @@ -17,7 +17,7 @@ ], "scripts": { "build": "tsc", - "test": "node test/e2e.mjs && node test/watch.mjs", + "test": "node test/e2e.mjs && node test/watch.mjs && node test/ignore-policy.mjs && node test/non-git-ignore-policy.mjs", "typecheck": "tsc --noEmit" }, "engines": { @@ -35,7 +35,7 @@ ], "repository": { "type": "git", - "url": "git+https://github.com/dmtrKovalenko/fff.git", + "url": "git+https://github.com/celados/fff.git", "directory": "packages/fff-node" }, "keywords": [ @@ -54,22 +54,22 @@ "access": "public" }, "bugs": { - "url": "https://github.com/dmtrKovalenko/fff/issues" + "url": "https://github.com/celados/fff/issues" }, - "homepage": "https://github.com/dmtrKovalenko/fff#readme", + "homepage": "https://github.com/celados/fff#readme", "dependencies": { "ffi-rs": "^1.0.0" }, "optionalDependencies": { - "@ff-labs/fff-bin-darwin-arm64": "0.0.0", - "@ff-labs/fff-bin-darwin-x64": "0.0.0", - "@ff-labs/fff-bin-linux-x64-gnu": "0.0.0", - "@ff-labs/fff-bin-linux-arm64-gnu": "0.0.0", - "@ff-labs/fff-bin-linux-x64-musl": "0.0.0", - "@ff-labs/fff-bin-linux-arm64-musl": "0.0.0", - "@ff-labs/fff-bin-win32-x64": "0.0.0", - "@ff-labs/fff-bin-win32-arm64": "0.0.0", - "@ff-labs/fff-bin-android-arm64": "0.0.0" + "@celados/fff-bin-darwin-arm64": "0.0.0", + "@celados/fff-bin-darwin-x64": "0.0.0", + "@celados/fff-bin-linux-x64-gnu": "0.0.0", + "@celados/fff-bin-linux-arm64-gnu": "0.0.0", + "@celados/fff-bin-linux-x64-musl": "0.0.0", + "@celados/fff-bin-linux-arm64-musl": "0.0.0", + "@celados/fff-bin-win32-x64": "0.0.0", + "@celados/fff-bin-win32-arm64": "0.0.0", + "@celados/fff-bin-android-arm64": "0.0.0" }, "devDependencies": { "typescript": "^5.0.0", diff --git a/packages/fff-node/scripts/cli.ts b/packages/fff-node/scripts/cli.ts deleted file mode 100644 index 9e3ddeb0..00000000 --- a/packages/fff-node/scripts/cli.ts +++ /dev/null @@ -1,122 +0,0 @@ -/** - * CLI tool for fff-node package management - * - * Usage: - * npx @ff-labs/fff-node download [tag] - Download native binary from GitHub - * npx @ff-labs/fff-node info - Show platform and binary info - */ - -import { readFileSync } from "node:fs"; -import { dirname, join } from "node:path"; -import { fileURLToPath } from "node:url"; -import { downloadBinary, findBinary, getBinaryPath } from "../src/binary.js"; -import { - getLibExtension, - getLibFilename, - getNpmPackageName, - getTriple, -} from "../src/platform.js"; - -const args = process.argv.slice(2); -const command = args[0]; - -interface PackageJson { - version: string; -} - -function getPackageInfo(): PackageJson { - const currentDir = dirname(fileURLToPath(import.meta.url)); - const packageJsonPath = join(currentDir, "..", "package.json"); - - try { - return JSON.parse(readFileSync(packageJsonPath, "utf-8")); - } catch { - return { version: "unknown" }; - } -} - -async function main() { - switch (command) { - case "download": { - const tag = args[1]; - console.log("fff: Downloading native library from GitHub..."); - try { - const resolvedTag = await downloadBinary(tag); - console.log(`fff: Download complete! (${resolvedTag})`); - } catch (error) { - console.error("fff: Download failed:", error); - process.exit(1); - } - break; - } - - case "info": { - const pkg = getPackageInfo(); - let npmPackage: string; - try { - npmPackage = getNpmPackageName(); - } catch { - npmPackage = "unsupported"; - } - - console.log("fff - Fast File Finder (Node.js)"); - console.log(`Package version: ${pkg.version}`); - console.log(""); - console.log("Platform Information:"); - console.log(` Triple: ${getTriple()}`); - console.log(` Extension: ${getLibExtension()}`); - console.log(` Library name: ${getLibFilename()}`); - console.log(` npm package: ${npmPackage}`); - console.log(""); - console.log("Binary Status:"); - const existing = findBinary(); - if (existing) { - console.log(` Found: ${existing}`); - } else { - console.log(" Not found"); - console.log(` Expected path: ${getBinaryPath()}`); - console.log(` Try: npm add ${npmPackage}`); - } - break; - } - - case "version": - case "--version": - case "-v": { - const pkg = getPackageInfo(); - console.log(pkg.version); - break; - } - - default: { - const pkg = getPackageInfo(); - console.log(`fff - Fast File Finder CLI (Node.js) v${pkg.version}`); - console.log(""); - console.log("Usage:"); - console.log( - " npx @ff-labs/fff-node download [tag] Download native binary from GitHub (fallback)", - ); - console.log( - " npx @ff-labs/fff-node info Show platform and binary info", - ); - console.log(" npx @ff-labs/fff-node version Show version"); - console.log(" npx @ff-labs/fff-node help Show this help message"); - console.log(""); - console.log("Examples:"); - console.log( - " npx @ff-labs/fff-node download Download latest binary from GitHub", - ); - console.log( - " npx @ff-labs/fff-node download abc1234 Download specific release tag", - ); - console.log(""); - console.log( - "Note: Binaries are normally provided via platform-specific npm packages.", - ); - console.log("The download command is a fallback for when those aren't available."); - break; - } - } -} - -main(); diff --git a/packages/fff-node/scripts/postinstall.ts b/packages/fff-node/scripts/postinstall.ts deleted file mode 100644 index ddbd2f45..00000000 --- a/packages/fff-node/scripts/postinstall.ts +++ /dev/null @@ -1,50 +0,0 @@ -/** - * Postinstall script - ensures the native binary is available - * - * Resolution order: - * 1. Platform-specific npm package (installed via optionalDependencies) - * 2. Local dev build (target/release or target/debug) - * 3. Fallback: download from GitHub releases - */ - -import { downloadBinary, findBinary } from "../src/binary.js"; -import { getNpmPackageName } from "../src/platform.js"; - -async function main() { - // Check if binary is already available (npm package or dev build) - const existing = findBinary(); - if (existing) { - console.log(`fff: Native library found at ${existing}`); - return; - } - - // Binary not found via npm package - try downloading from GitHub as fallback - let packageName: string; - try { - packageName = getNpmPackageName(); - } catch { - packageName = "unknown"; - } - - console.log( - `fff: Platform package ${packageName} not found, falling back to GitHub download...`, - ); - - try { - const tag = await downloadBinary(); - console.log(`fff: Native library installed successfully! (${tag})`); - } catch (error) { - console.error("fff: Failed to download native library:", error); - console.error(""); - console.error("fff: You can build from source instead:"); - console.error(" cargo build --release -p fff-c"); - console.error(""); - console.error( - "fff: Or run `npx @ff-labs/fff-node download` after fixing network issues.", - ); - // Don't exit with error - allow install to complete - // The error will surface when the user tries to use the library - } -} - -main(); diff --git a/packages/fff-node/src/binary.ts b/packages/fff-node/src/binary.ts index 7e2c7aac..7fe2a5a8 100644 --- a/packages/fff-node/src/binary.ts +++ b/packages/fff-node/src/binary.ts @@ -2,7 +2,7 @@ * Binary resolution utilities for fff-node * * Resolves the native library from: - * 1. Platform-specific npm package (e.g. @ff-labs/fff-bin-darwin-arm64) + * 1. Platform-specific npm package (e.g. @celados/fff-bin-darwin-arm64) * 2. Local dev build (target/release or target/debug) */ @@ -37,7 +37,7 @@ function getPackageDir(): string { if (existsSync(join(dir, "package.json"))) { try { const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf-8")); - if (pkg.name === "@ff-labs/fff-node") { + if (pkg.name === "@celados/fff-node") { return dir; } } catch { @@ -60,8 +60,8 @@ export function binaryExists(): boolean { /** * Try to resolve the binary from the platform-specific npm package. * - * When users install @ff-labs/fff-node, npm automatically installs the matching - * optionalDependency (e.g. @ff-labs/fff-bin-darwin-arm64). We resolve the binary + * When users install @celados/fff-node, npm automatically installs the matching + * optionalDependency (e.g. @celados/fff-bin-darwin-arm64). We resolve the binary * path by requiring that package's package.json and looking for the binary * in the same directory. */ diff --git a/packages/fff-node/src/fff-api.ts b/packages/fff-node/src/fff-api.ts index d22c6de5..1aeaaf71 100644 --- a/packages/fff-node/src/fff-api.ts +++ b/packages/fff-node/src/fff-api.ts @@ -6,7 +6,7 @@ /** * The shared public API surface for the fff file finder, implemented identically - * by `@ff-labs/fff-node` and `@ff-labs/fff-bun`. + * by the Node.js and Bun frontends. * * This file is the single source of truth for every type, helper, and the * `FileFinderApi` interface that crosses the package boundary. It is copied @@ -553,7 +553,7 @@ export interface MultiGrepOptions { /** * The shared instance surface implemented by `FileFinder` in both - * `@ff-labs/fff-node` and `@ff-labs/fff-bun`. + * the Node.js and Bun frontends. * * Both packages must implement this identically. Only instance members belong * here. Static helpers (`create`, `isAvailable`, `ensureLoaded`, diff --git a/packages/fff-node/src/ffi.ts b/packages/fff-node/src/ffi.ts index e2315600..7a9242b4 100644 --- a/packages/fff-node/src/ffi.ts +++ b/packages/fff-node/src/ffi.ts @@ -142,7 +142,7 @@ function loadLibrary(): void { const binaryPath = findBinary(); if (!binaryPath) { throw new Error( - "fff native library not found. Run `npx @ff-labs/fff-node download` or build from source with `cargo build --release -p fff-c`", + "fff native library not found. Install the matching @celados/fff-bin-* package or build from source with `cargo build --release -p fff-c`", ); } diff --git a/packages/fff-node/src/finder.ts b/packages/fff-node/src/finder.ts index e80b4ef6..1a975038 100644 --- a/packages/fff-node/src/finder.ts +++ b/packages/fff-node/src/finder.ts @@ -66,7 +66,7 @@ import { err } from "./fff-api.js"; * @example * * ```ts - * import { FileFinder } from "@ff-labs/fff-node"; + * import { FileFinder } from "@celados/fff-node"; * * // Create an instance * const finder = FileFinder.create({ basePath: "/path/to/project" }); diff --git a/packages/fff-node/src/index.ts b/packages/fff-node/src/index.ts index c1ac706f..b13200e8 100644 --- a/packages/fff-node/src/index.ts +++ b/packages/fff-node/src/index.ts @@ -7,11 +7,11 @@ * Each `FileFinder` instance is backed by an independent native file picker. * Create as many as you need and destroy them when done. * - * Uses ffi-rs to load the same native libfff_c binary used by @ff-labs/fff-bun. + * Uses ffi-rs to load the native libfff_c binary from @celados/fff-bin-*. * * @example * ```typescript - * import { FileFinder } from "@ff-labs/fff-node"; + * import { FileFinder } from "@celados/fff-node"; * * // Create a file finder instance * const result = FileFinder.create({ basePath: "/path/to/project" }); @@ -43,7 +43,6 @@ export { binaryExists, findBinary, } from "./binary.js"; -export { closeLibrary } from "./ffi.js"; export type { DbHealth, DirItem, @@ -75,6 +74,7 @@ export type { } from "./fff-api.js"; // Result helpers export { err, ok } from "./fff-api.js"; +export { closeLibrary } from "./ffi.js"; export { FileFinder } from "./finder.js"; export { getLibExtension, diff --git a/packages/fff-node/src/platform.ts b/packages/fff-node/src/platform.ts index e4750310..457130e3 100644 --- a/packages/fff-node/src/platform.ts +++ b/packages/fff-node/src/platform.ts @@ -1,5 +1,5 @@ /** - * Platform detection utilities for downloading the correct binary + * Platform detection utilities for resolving the correct native package */ import { execSync } from "node:child_process"; @@ -99,25 +99,25 @@ export function getLibFilename(): string { /** * Map from Rust target triple to npm platform package name. - * The @ff-labs/fff-bin-* packages contain the pre-built libfff_c + * The @celados/fff-bin-* packages contain the pre-built libfff_c * shared library and are runtime-agnostic (used by both Bun and Node). */ const TRIPLE_TO_NPM_PACKAGE: Record = { - "aarch64-apple-darwin": "@ff-labs/fff-bin-darwin-arm64", - "x86_64-apple-darwin": "@ff-labs/fff-bin-darwin-x64", - "x86_64-unknown-linux-gnu": "@ff-labs/fff-bin-linux-x64-gnu", - "aarch64-unknown-linux-gnu": "@ff-labs/fff-bin-linux-arm64-gnu", - "x86_64-unknown-linux-musl": "@ff-labs/fff-bin-linux-x64-musl", - "aarch64-unknown-linux-musl": "@ff-labs/fff-bin-linux-arm64-musl", - "x86_64-pc-windows-msvc": "@ff-labs/fff-bin-win32-x64", - "aarch64-pc-windows-msvc": "@ff-labs/fff-bin-win32-arm64", - "aarch64-linux-android": "@ff-labs/fff-bin-android-arm64", + "aarch64-apple-darwin": "@celados/fff-bin-darwin-arm64", + "x86_64-apple-darwin": "@celados/fff-bin-darwin-x64", + "x86_64-unknown-linux-gnu": "@celados/fff-bin-linux-x64-gnu", + "aarch64-unknown-linux-gnu": "@celados/fff-bin-linux-arm64-gnu", + "x86_64-unknown-linux-musl": "@celados/fff-bin-linux-x64-musl", + "aarch64-unknown-linux-musl": "@celados/fff-bin-linux-arm64-musl", + "x86_64-pc-windows-msvc": "@celados/fff-bin-win32-x64", + "aarch64-pc-windows-msvc": "@celados/fff-bin-win32-arm64", + "aarch64-linux-android": "@celados/fff-bin-android-arm64", }; /** * Get the npm package name for the current platform's native binary. * - * @returns Package name like "@ff-labs/fff-bin-darwin-arm64" + * @returns Package name like "@celados/fff-bin-darwin-arm64" * @throws If the current platform is not supported */ export function getNpmPackageName(): string { diff --git a/packages/fff-node/test/demo-grep.mjs b/packages/fff-node/test/demo-grep.mjs index c717fbfb..bcfdaa03 100644 --- a/packages/fff-node/test/demo-grep.mjs +++ b/packages/fff-node/test/demo-grep.mjs @@ -1,4 +1,4 @@ -import { FileFinder } from "@ff-labs/fff-node"; +import { FileFinder } from "@celados/fff-node"; const finder = FileFinder.create({ basePath: "~/dev/linux-root" }); if (!finder.ok) { diff --git a/packages/pi-fff/package.json b/packages/pi-fff/package.json index e25fc306..0e93a284 100644 --- a/packages/pi-fff/package.json +++ b/packages/pi-fff/package.json @@ -41,7 +41,7 @@ }, "dependencies": { "@ff-labs/fff-bun": "*", - "@ff-labs/fff-node": "*" + "@celados/fff-node": "*" }, "peerDependencies": { "@earendil-works/pi-coding-agent": "*", diff --git a/packages/pi-fff/src/aux-finders.ts b/packages/pi-fff/src/aux-finders.ts index 37bfe23c..4697f352 100644 --- a/packages/pi-fff/src/aux-finders.ts +++ b/packages/pi-fff/src/aux-finders.ts @@ -1,7 +1,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import type { FileFinderApi } from "@ff-labs/fff-node"; +import type { FileFinderApi } from "@celados/fff-node"; import { loadSdk, SCAN_TIMEOUT_MS } from "./sdk"; export const MAX_AUX = 3; diff --git a/packages/pi-fff/src/index.ts b/packages/pi-fff/src/index.ts index b40c34de..494b7541 100644 --- a/packages/pi-fff/src/index.ts +++ b/packages/pi-fff/src/index.ts @@ -19,7 +19,7 @@ import type { GrepResult, MixedItem, SearchResult, -} from "@ff-labs/fff-node"; +} from "@celados/fff-node"; import { Type } from "@sinclair/typebox"; import { AuxFinderPool, routePathConstraint } from "./aux-finders"; import { buildQuery } from "./query"; diff --git a/packages/pi-fff/src/sdk.ts b/packages/pi-fff/src/sdk.ts index 8299ea01..be4d9cf4 100644 --- a/packages/pi-fff/src/sdk.ts +++ b/packages/pi-fff/src/sdk.ts @@ -1,4 +1,4 @@ -import type { FileFinderApi, InitOptions, Result } from "@ff-labs/fff-node"; +import type { FileFinderApi, InitOptions, Result } from "@celados/fff-node"; export const SCAN_TIMEOUT_MS = 15_000; @@ -23,7 +23,7 @@ export function loadSdk(): Promise<{ FileFinder: FileFinderStatic }> { if (sdkPromise) return sdkPromise; // default to node as it seems like default option - const pkg = detectRuntime() === "bun" ? "@ff-labs/fff-bun" : "@ff-labs/fff-node"; + const pkg = detectRuntime() === "bun" ? "@ff-labs/fff-bun" : "@celados/fff-node"; sdkPromise = import(pkg) as Promise<{ FileFinder: FileFinderStatic }>; return sdkPromise; } diff --git a/packages/pi-fff/test/aux-pool.test.ts b/packages/pi-fff/test/aux-pool.test.ts index 13110f59..ee71d708 100644 --- a/packages/pi-fff/test/aux-pool.test.ts +++ b/packages/pi-fff/test/aux-pool.test.ts @@ -35,7 +35,7 @@ const finderModule = { }, }; -mock.module("@ff-labs/fff-node", () => finderModule); +mock.module("@celados/fff-node", () => finderModule); mock.module("@ff-labs/fff-bun", () => finderModule); const { AuxFinderPool } = await import("../src/aux-finders"); diff --git a/packages/pi-fff/test/extension.test.ts b/packages/pi-fff/test/extension.test.ts index c0d351c6..267a6cd3 100644 --- a/packages/pi-fff/test/extension.test.ts +++ b/packages/pi-fff/test/extension.test.ts @@ -45,7 +45,7 @@ const finderModule = { }, }; -mock.module("@ff-labs/fff-node", () => finderModule); +mock.module("@celados/fff-node", () => finderModule); mock.module("@ff-labs/fff-bun", () => finderModule); mock.module("@earendil-works/pi-tui", () => ({ diff --git a/packages/shared/fff-api.ts b/packages/shared/fff-api.ts index 01b2201d..785fa8f9 100644 --- a/packages/shared/fff-api.ts +++ b/packages/shared/fff-api.ts @@ -1,6 +1,6 @@ /** * The shared public API surface for the fff file finder, implemented identically - * by `@ff-labs/fff-node` and `@ff-labs/fff-bun`. + * by the Node and Bun frontends. * * This file is the single source of truth for every type, helper, and the * `FileFinderApi` interface that crosses the package boundary. It is copied @@ -547,7 +547,7 @@ export interface MultiGrepOptions { /** * The shared instance surface implemented by `FileFinder` in both - * `@ff-labs/fff-node` and `@ff-labs/fff-bun`. + * the Node and Bun frontends. * * Both packages must implement this identically. Only instance members belong * here. Static helpers (`create`, `isAvailable`, `ensureLoaded`, From 735af3095ac252baa851b13eb1f94b5ce59e01ac Mon Sep 17 00:00:00 2001 From: EthanHuo Date: Mon, 3 Aug 2026 15:49:17 +0800 Subject: [PATCH 3/6] fix: deduplicate ignore policy watches --- .../src/watcher/background_watcher.rs | 209 ++++++++++++++++-- 1 file changed, 192 insertions(+), 17 deletions(-) diff --git a/crates/fff-core/src/watcher/background_watcher.rs b/crates/fff-core/src/watcher/background_watcher.rs index 09b51267..cf817f58 100644 --- a/crates/fff-core/src/watcher/background_watcher.rs +++ b/crates/fff-core/src/watcher/background_watcher.rs @@ -10,6 +10,7 @@ use notify::event::{AccessKind, AccessMode}; use notify::{Config, EventKind, EventKindMask, RecursiveMode}; use notify_debouncer_full::{DebounceEventResult, DebouncedEvent, NoCache, new_debouncer_opt}; use parking_lot::Mutex; +use std::collections::HashSet; use std::path::{Path, PathBuf}; use std::sync::Arc; use std::sync::mpsc; @@ -89,7 +90,8 @@ impl BackgroundWatcher { let owner_git_workdir = git_workdir.clone(); let owner_git_worker = Arc::clone(&git_status_worker); - let debouncer = Self::create_debouncer( + let policy_watch_dirs = policy_watch_directories(&shared_picker); + let (debouncer, mut successful_policy_watch_dirs) = Self::create_debouncer( base_path, git_workdir, shared_picker, @@ -98,6 +100,7 @@ impl BackgroundWatcher { use_recursive, watch_tx_for_debouncer, git_status_worker, + &policy_watch_dirs, )?; info!("Background file watcher initialized successfully"); @@ -123,14 +126,8 @@ impl BackgroundWatcher { // from the base path (see `create_debouncer`), and // registering a second overlapping stream there produces // duplicate/out-of-order events. - let watch_path = match &request { - #[cfg(target_os = "linux")] - WatchRequest::Directory(path) => Some(path), - #[cfg(not(target_os = "linux"))] - WatchRequest::Directory(_) => None, - WatchRequest::PolicySourceDirectory(path) => Some(path), - }; - if let Some(dir) = watch_path { + #[cfg(target_os = "linux")] + if let WatchRequest::Directory(dir) = &request { // Register the new directory with the debouncer, then // drop the mutex BEFORE doing picker-side work — see // the comment on `BackgroundWatcher::stop` for the @@ -149,6 +146,25 @@ impl BackgroundWatcher { } } + if let WatchRequest::PolicySourceDirectory(dir) = &request { + let mut guard = owner_debouncer.lock(); + let Some(debouncer) = guard.as_mut() else { + break; + }; + + if let Err(error) = try_register_policy_watch( + &mut successful_policy_watch_dirs, + dir, + |path| debouncer.watch(path, RecursiveMode::NonRecursive), + ) { + warn!( + ?error, + path = %dir.display(), + "Failed to watch ignore policy source directory" + ); + } + } + if let WatchRequest::Directory(dir) = request { track_files_from_new_directories( &dir, @@ -183,7 +199,8 @@ impl BackgroundWatcher { use_recursive: bool, watch_tx: mpsc::Sender, git_status_worker: Arc, - ) -> Result { + policy_watch_dirs: &[PathBuf], + ) -> Result<(Debouncer, HashSet), Error> { let config = Config::default() .with_follow_symlinks(false) // only the actual modification events, ignore the open syscals that we can generate by @@ -294,9 +311,10 @@ impl BackgroundWatcher { // to observe changes that affect git status (staging, unstaging, // committing, branch switches, merges, etc) watch_git_status_paths(&mut debouncer, git_workdir.as_ref()); - watch_policy_source_dirs(&mut debouncer, &shared_picker_for_watching); + let successful_policy_watch_dirs = + watch_policy_source_dirs(&mut debouncer, policy_watch_dirs); - Ok(debouncer) + Ok((debouncer, successful_policy_watch_dirs)) } /// Signal the watcher to shut down without blocking on its worker @@ -321,6 +339,9 @@ impl BackgroundWatcher { } pub(crate) fn request_watch_policy_source_dir(&self, dir: PathBuf) -> bool { + let Some(dir) = closest_existing_directory(&dir) else { + return false; + }; match self.watch_tx.as_ref() { Some(tx) => tx.send(WatchRequest::PolicySourceDirectory(dir)).is_ok(), None => false, @@ -405,10 +426,16 @@ fn handle_debounced_events( tracing::debug!(event = ?debounced_event.event, "Processing FS event"); for path in &debounced_event.event.paths { + // A missing policy source is watched through its nearest existing + // ancestor, so creating the next path component must rebuild the + // policy and move the watch closer to the eventual file. + let affects_policy = policy_sources + .iter() + .any(|source| source == path || source.starts_with(path)); if matches!( path.file_name().and_then(|f| f.to_str()), Some(".ignore") | Some(".gitignore") - ) || policy_sources.iter().any(|source| source == path) + ) || affects_policy { info!( "Detected change in ignore definition file: {}", @@ -896,19 +923,67 @@ fn watch_git_status_paths(debouncer: &mut Debouncer, git_workdir: Option<&PathBu } } -fn watch_policy_source_dirs(debouncer: &mut Debouncer, picker: &SharedFilePicker) { +fn policy_watch_directories(picker: &SharedFilePicker) -> Vec { let Some(sources) = picker .read() .ok() .and_then(|guard| guard.as_ref().map(|picker| picker.policy_sources())) else { - return; + return Vec::new(); }; - for dir in sources.iter().filter_map(|source| source.parent()) { - if let Err(error) = debouncer.watch(dir, RecursiveMode::NonRecursive) { + + policy_watch_directories_from_sources(&sources) +} + +fn policy_watch_directories_from_sources(sources: &[PathBuf]) -> Vec { + let mut directories = sources + .iter() + .filter_map(|source| source.parent()) + .filter_map(closest_existing_directory) + .collect::>(); + directories.sort_unstable(); + directories.dedup(); + directories +} + +fn closest_existing_directory(path: &Path) -> Option { + // notify rejects missing directories; watching the nearest ancestor keeps + // future source creation observable without retrying a slow failed watch + // ahead of every new-directory injection on the owner FIFO. + path.ancestors() + .find(|candidate| candidate.is_dir()) + .map(Path::to_path_buf) +} + +fn try_register_policy_watch( + successful: &mut HashSet, + dir: &Path, + watch: impl FnOnce(&Path) -> Result<(), E>, +) -> Result { + if successful.contains(dir) { + return Ok(false); + } + + // A failed native watch is not coverage. Keep it retryable so a transient + // permission/resource failure cannot silently freeze ignore policy state. + watch(dir)?; + successful.insert(dir.to_path_buf()); + Ok(true) +} + +fn watch_policy_source_dirs( + debouncer: &mut Debouncer, + directories: &[PathBuf], +) -> HashSet { + let mut successful = HashSet::new(); + for dir in directories { + if let Err(error) = try_register_policy_watch(&mut successful, dir, |path| { + debouncer.watch(path, RecursiveMode::NonRecursive) + }) { warn!(?error, path = %dir.display(), "Failed to watch ignore policy source directory"); } } + successful } #[cfg(test)] @@ -1041,6 +1116,106 @@ mod tests { assert_eq!(delivered[0].path, base); } + #[test] + fn missing_policy_parents_share_the_nearest_existing_watch() { + let tmp = tempfile::tempdir().unwrap(); + let config = tmp.path().join("config"); + std::fs::create_dir(&config).unwrap(); + let sources = vec![ + config.join("git/ignore"), + config.join("git/config"), + config.join("other/missing"), + ]; + + assert_eq!( + policy_watch_directories_from_sources(&sources), + vec![config] + ); + } + + #[test] + fn failed_policy_watch_remains_retryable_until_success() { + let dir = PathBuf::from("/policy"); + let mut successful = HashSet::new(); + let mut attempts = 0; + + let failed = try_register_policy_watch(&mut successful, &dir, |_| { + attempts += 1; + Err("transient failure") + }); + assert_eq!(failed, Err("transient failure")); + assert!(successful.is_empty()); + + let retried = try_register_policy_watch(&mut successful, &dir, |_| { + attempts += 1; + Ok::<_, &str>(()) + }); + assert_eq!(retried, Ok(true)); + assert_eq!(attempts, 2); + + let deduplicated = try_register_policy_watch(&mut successful, &dir, |_| { + attempts += 1; + Ok::<_, &str>(()) + }); + assert_eq!(deduplicated, Ok(false)); + assert_eq!(attempts, 2); + } + + #[test] + fn policy_source_ancestor_creation_broadcasts_rescan() { + let tmp = tempfile::tempdir().unwrap(); + let base = crate::path_utils::canonicalize(tmp.path()).unwrap(); + let repo = git2::Repository::init(&base).unwrap(); + let global = base.join("future/config/git/ignore"); + repo.config() + .unwrap() + .set_str("core.excludesFile", global.to_str().unwrap()) + .unwrap(); + + let shared_picker = SharedFilePicker::default(); + let shared_frecency = SharedFrecency::noop(); + let mut picker = FilePicker::new(FilePickerOptions { + base_path: base.to_string_lossy().into_owned(), + watch: false, + ..Default::default() + }) + .unwrap(); + picker.collect_files().unwrap(); + shared_picker.rebase_watches(&base); + *shared_picker.write().unwrap() = Some(picker); + + let (sender, receiver) = mpsc::channel::>(); + shared_picker + .watch_registry() + .subscribe( + &base, + "**", + WatchOptions::default(), + Box::new(move |_, events| sender.send(events.to_vec()).unwrap()), + ) + .unwrap(); + + let created_ancestor = base.join("future"); + let events = vec![DebouncedEvent::new( + Event::new(EventKind::Create(CreateKind::Folder)).add_path(created_ancestor), + Instant::now(), + )]; + handle_debounced_events( + FFFMode::Neovim, + events, + &base, + &Some(base.clone()), + &shared_picker, + &shared_frecency, + &GitStatusWorker::new(), + ); + + let delivered = receiver.recv_timeout(Duration::from_secs(1)).unwrap(); + assert_eq!(delivered.len(), 1); + assert_eq!(delivered[0].kind, WatchEventKind::Rescan); + assert_eq!(delivered[0].path, base); + } + #[test] fn dotgit_status_filter_matches_worktree_state_changes() { let tmp = tempfile::tempdir().unwrap(); From 6e31d6c7bf9cce2258c88f813cfcf26fd000b5c5 Mon Sep 17 00:00:00 2001 From: EthanHuo Date: Mon, 3 Aug 2026 16:18:21 +0800 Subject: [PATCH 4/6] fix: load packaged native binaries --- Makefile | 9 ++-- packages/fff-node/package.json | 2 +- packages/fff-node/src/binary.ts | 19 +++++-- packages/fff-node/test/binary-path.mjs | 68 ++++++++++++++++++++++++++ 4 files changed, 90 insertions(+), 8 deletions(-) create mode 100644 packages/fff-node/test/binary-path.mjs diff --git a/Makefile b/Makefile index 84366d80..aacd07c0 100644 --- a/Makefile +++ b/Makefile @@ -174,10 +174,11 @@ prepare-bun-packaged: prepare-bun esac; \ src=target/release/$$lib; \ [ -f "$$src" ] || { echo "missing built library: $$src" >&2; exit 1; }; \ - dest=packages/fff-bun/node_modules/@ff-labs/$$pkg; \ + dest=packages/fff-bun/node_modules/@celados/$$pkg; \ rm -rf "$$dest"; mkdir -p "$$dest"; \ cp "$$src" "$$dest/$$lib"; \ - printf '{ "name": "@ff-labs/%s", "version": "0.0.0", "main": "%s" }\n' "$$pkg" "$$lib" > "$$dest/package.json" + printf '{ "name": "@celados/%s", "version": "0.0.0", "main": "%s" }\n' "$$pkg" "$$lib" > "$$dest/package.json"; \ + node -e "const fs=require('node:fs'); const p=JSON.parse(fs.readFileSync(process.argv[1])); if(p.name!==process.argv[2]) throw new Error('staged package scope drift')" "$$dest/package.json" "@celados/$$pkg" # Compile a bun example to a standalone executable and run it. Verifies the # native libfff_c is embedded + loaded from a `bun build --compile` binary. @@ -191,13 +192,13 @@ test-bun-compile: prepare-bun-packaged else DEFINE=""; fi; \ bun build --compile $$DEFINE ./examples/glob-bench.ts --outfile ./glob-bench-bin && \ EXE=./glob-bench-bin; [ -f "$$EXE.exe" ] && EXE="$$EXE.exe"; \ - rm -rf bin node_modules/@ff-labs; \ + rm -rf bin node_modules/@celados; \ "$$EXE" . '**/*.ts' 1 | tee /tmp/fff-compile-e2e.log && \ grep -q 'fff.glob' /tmp/fff-compile-e2e.log rm -f packages/fff-bun/glob-bench-bin packages/fff-bun/glob-bench-bin.exe test-node: prepare-node - cd packages/fff-node && npm run build && node test/e2e.mjs && node test/watch.mjs + cd packages/fff-node && npm run build && npm test test-js: test-bun test-node diff --git a/packages/fff-node/package.json b/packages/fff-node/package.json index 4d6c77d2..cadb99ea 100644 --- a/packages/fff-node/package.json +++ b/packages/fff-node/package.json @@ -17,7 +17,7 @@ ], "scripts": { "build": "tsc", - "test": "node test/e2e.mjs && node test/watch.mjs && node test/ignore-policy.mjs && node test/non-git-ignore-policy.mjs", + "test": "node test/binary-path.mjs && node test/e2e.mjs && node test/watch.mjs && node test/ignore-policy.mjs && node test/non-git-ignore-policy.mjs", "typecheck": "tsc --noEmit" }, "engines": { diff --git a/packages/fff-node/src/binary.ts b/packages/fff-node/src/binary.ts index 7fe2a5a8..ae647c11 100644 --- a/packages/fff-node/src/binary.ts +++ b/packages/fff-node/src/binary.ts @@ -50,6 +50,21 @@ function getPackageDir(): string { return dirname(currentDir); } +/** + * Resolve a native path that the OS loader can open directly. + */ +export function resolveLoadableBinaryPath(binaryPath: string): string | null { + const asarSegment = /([\\/])app\.asar([\\/])/; + if (asarSegment.test(binaryPath)) { + const unpackedPath = binaryPath.replace(asarSegment, "$1app.asar.unpacked$2"); + // Electron's patched fs can see inside ASAR, but ffi-rs delegates to the + // OS loader, which requires the real file produced by asarUnpack. + return existsSync(unpackedPath) ? unpackedPath : null; + } + + return existsSync(binaryPath) ? binaryPath : null; +} + /** * Check if the binary exists in any known location */ @@ -75,9 +90,7 @@ function resolveFromNpmPackage(): string | null { const packageDir = dirname(packageJsonPath); const binaryPath = join(packageDir, getLibFilename()); - if (existsSync(binaryPath)) { - return binaryPath; - } + return resolveLoadableBinaryPath(binaryPath); } catch { // Package not installed - this is expected on unsupported platforms // or when installed without optional dependencies diff --git a/packages/fff-node/test/binary-path.mjs b/packages/fff-node/test/binary-path.mjs new file mode 100644 index 00000000..382e12a4 --- /dev/null +++ b/packages/fff-node/test/binary-path.mjs @@ -0,0 +1,68 @@ +import { strict as assert } from "node:assert"; +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import { after, describe, it } from "node:test"; +import { resolveLoadableBinaryPath } from "../dist/src/binary.js"; + +const root = mkdtempSync(join(tmpdir(), "fff-node-binary-path-")); + +after(() => rmSync(root, { recursive: true, force: true })); + +function createFile(path) { + mkdirSync(dirname(path), { recursive: true }); + writeFileSync(path, "native binary fixture"); +} + +describe("native binary path resolution", () => { + it("keeps a normal filesystem path", () => { + const binary = join(root, "node_modules", "@celados", "fff-bin", "libfff_c.dylib"); + createFile(binary); + + assert.equal(resolveLoadableBinaryPath(binary), binary); + }); + + it("uses the real unpacked file for an Electron ASAR path", () => { + const packed = join( + root, + "Markd.app", + "Contents", + "Resources", + "app.asar", + "node_modules", + "@celados", + "fff-bin", + "libfff_c.dylib", + ); + const unpacked = join( + root, + "Markd.app", + "Contents", + "Resources", + "app.asar.unpacked", + "node_modules", + "@celados", + "fff-bin", + "libfff_c.dylib", + ); + createFile(unpacked); + + assert.equal(resolveLoadableBinaryPath(packed), unpacked); + }); + + it("fails closed when Electron did not unpack the binary", () => { + const packed = join( + root, + "Missing.app", + "Contents", + "Resources", + "app.asar", + "node_modules", + "@celados", + "fff-bin", + "libfff_c.dylib", + ); + + assert.equal(resolveLoadableBinaryPath(packed), null); + }); +}); From 658e2ce4e1d13ed507915677a011cb0f29341897 Mon Sep 17 00:00:00 2001 From: EthanHuo Date: Mon, 3 Aug 2026 16:24:09 +0800 Subject: [PATCH 5/6] fix: align fork package scopes in CI --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- .github/ISSUE_TEMPLATE/feature_request.yml | 2 +- .github/workflows/external-tests.yml | 9 +++++---- README.md | 12 ++++++------ 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 3813ba85..4f108824 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -10,7 +10,7 @@ body: options: - Neovim plugin (fff.nvim) - MCP server (fff-mcp) - - Node SDK (@ff-labs/fff-node) + - Node SDK (@celados/fff-node) - Bun SDK - C SDK (libfff) - Other / multiple diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index caa70e05..ed8ac8ac 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -11,7 +11,7 @@ body: options: - Neovim plugin (fff.nvim) - MCP server (fff-mcp) - - Node SDK (@ff-labs/fff-node) + - Node SDK (@celados/fff-node) - Bun SDK (@ff-labs/fff-bun) - C lib (libfff) - Core or Rust crate diff --git a/.github/workflows/external-tests.yml b/.github/workflows/external-tests.yml index 8cf071f4..dc04a3aa 100644 --- a/.github/workflows/external-tests.yml +++ b/.github/workflows/external-tests.yml @@ -142,7 +142,7 @@ jobs: run: make test-node # Regression for https://github.com/dmtrKovalenko/fff/issues/480: build & - # run @ff-labs/fff-node end-to-end on real Alpine Linux (musl). Forces + # run @celados/fff-node end-to-end on real Alpine Linux (musl). Forces # findBinary() through the npm-package resolver so detectLinuxLibc() # actually runs. alpine-musl: @@ -197,16 +197,17 @@ jobs: "@yuuang/ffi-rs-linux-x64-musl@${FFI_RS_VERSION}" # Stage the freshly built libfff_c.so as the platform npm package - # so findBinary() resolves through the @ff-labs/fff-bin-* path — + # so findBinary() resolves through the fork-owned @celados/fff-bin-* path — # this is what exercises detectLinuxLibc(). - name: Stage musl bin package run: | - PKG_DIR=node_modules/@ff-labs/fff-bin-linux-x64-musl + PKG_DIR=node_modules/@celados/fff-bin-linux-x64-musl mkdir -p "$PKG_DIR" cp target/release/libfff_c.so "$PKG_DIR/libfff_c.so" cat >"$PKG_DIR/package.json" <<'JSON' - { "name": "@ff-labs/fff-bin-linux-x64-musl", "version": "0.0.0" } + { "name": "@celados/fff-bin-linux-x64-musl", "version": "0.0.0" } JSON + node -e "const p=require('./node_modules/@celados/fff-bin-linux-x64-musl/package.json'); if(p.name!=='@celados/fff-bin-linux-x64-musl') throw new Error('staged package scope drift')" - name: Build fff-node working-directory: packages/fff-node diff --git a/README.md b/README.md index 4e409dd4..61b752aa 100644 --- a/README.md +++ b/README.md @@ -539,13 +539,13 @@ The best file search picker for neovim. Period. Faster and more intuitive querie ```bash -npm install @ff-labs/fff-node +npm install @celados/fff-node # or -bun add @ff-labs/fff-node +bun add @celados/fff-node ``` ```ts -import { FileFinder } from "@ff-labs/fff-node"; +import { FileFinder } from "@celados/fff-node"; const finder = FileFinder.create({ basePath: process.cwd(), aiMode: true }); if (!finder.ok) throw new Error(finder.error); @@ -614,7 +614,7 @@ cargo build --release -p fff-c --features zlob The output is a `cdylib` (`libfff_c.so` / `libfff_c.dylib` / `fff_c.dll`). The header lives at [`crates/fff-c/include/fff.h`](./crates/fff-c/include/fff.h). -Prebuilt binaries for every version, including every commit on main, are on the [releases page](https://github.com/dmtrKovalenko/fff.nvim/releases). The same binaries also ship inside the `@ff-labs/fff-bin-*` npm packages. +Prebuilt binaries for every version, including every commit on main, are on the [releases page](https://github.com/celados/fff/releases). The same binaries also ship inside the `@celados/fff-bin-*` npm packages. ### Install @@ -789,7 +789,7 @@ Native Python bindings built with PyO3. Use them for notebooks, agent scripts, o FFF is a file search library, not a CLI. Ripgrep and fzf are great tools, but they are command-line programs: every call forks a new process, re-reads `.gitignore`, re-stats directories, and rebuilds whatever state it needs in memory before it can answer. That is fine when you grep once from a shell. It is bad when an editor or an AI agent wants to run hundreds of searches per session. -FFF keeps the index and the file cache resident in one long-lived process and exposes the same Rust core through four thin layers: a native crate (`fff-search`), a C library (`libfff_c`), a Node/Bun SDK (`@ff-labs/fff-node`), and an MCP server. You call `FileFinder.create()` once, then every subsequent search hits warm memory. On a 500k-file Chromium checkout, that is the difference between 3-9 **SECONDS** per ripgrep spawn and sub-10 ms per FFF query. +FFF keeps the index and the file cache resident in one long-lived process and exposes the same Rust core through four thin layers: a native crate (`fff-search`), a C library (`libfff_c`), a Node/Bun SDK (`@celados/fff-node`), and an MCP server. You call `FileFinder.create()` once, then every subsequent search hits warm memory. On a 500k-file Chromium checkout, that is the difference between 3-9 **SECONDS** per ripgrep spawn and sub-10 ms per FFF query. Algorithm for fuzzy matching is much more comprehensive than fzf's algorithm. It is **typo-resistant** and we provide a query language with additional constraint parsing for prefiltering e.g. "\*.rs !test/ shcema" is a perfectly valid query for fff, but fzf wouldn't find anything even for a single typo in "shcema". @@ -848,7 +848,7 @@ If you are running one grep from a terminal, `rg` is still the right tool. If yo - `crates/fff-c` - C FFI used by every language binding. - `crates/fff-nvim` - Lua/mlua bindings for the Neovim plugin. - `crates/fff-mcp` - MCP server binary. -- `packages/fff-node` - Node.js SDK (`@ff-labs/fff-node`). +- `packages/fff-node` - Node.js SDK (`@celados/fff-node`). - `packages/fff-bun` - Bun SDK (`@ff-labs/fff-bun`). - `packages/pi-fff` - pi extension (`@ff-labs/pi-fff`). - `lua/` - Neovim-side plugin code. From 62797b0d5975372a20cc4d3ac2bb1b5b22b5e442 Mon Sep 17 00:00:00 2001 From: EthanHuo Date: Mon, 3 Aug 2026 17:23:49 +0800 Subject: [PATCH 6/6] ci: narrow fork publishing to Markd macOS packages --- .github/workflows/external-tests.yml | 218 ----- .github/workflows/lua.yml | 56 -- .github/workflows/nix.yml | 37 - .github/workflows/panvimdoc.yaml | 85 -- .github/workflows/python.yml | 48 -- .github/workflows/release.yaml | 813 ++++-------------- .github/workflows/rust.yml | 162 ---- .github/workflows/spelling.yaml | 25 - .github/workflows/stylua.yaml | 37 - .gitignore | 1 + .npmrc.tpl | 5 + AGENTS.md | 11 +- README.md | 20 +- package-lock.json | 74 +- packages/fff-bin-android-arm64/package.json | 18 - packages/fff-bin-darwin-arm64/package.json | 1 + packages/fff-bin-darwin-x64/package.json | 1 + packages/fff-bin-linux-arm64-gnu/package.json | 19 - .../fff-bin-linux-arm64-musl/package.json | 19 - packages/fff-bin-linux-x64-gnu/package.json | 19 - packages/fff-bin-linux-x64-musl/package.json | 19 - packages/fff-bin-win32-arm64/package.json | 18 - packages/fff-bin-win32-x64/package.json | 18 - packages/fff-bun/src/fff-api.ts | 4 +- packages/fff-node/README.md | 23 +- packages/fff-node/package.json | 15 +- packages/fff-node/src/fff-api.ts | 4 +- packages/fff-node/src/platform.ts | 69 +- 28 files changed, 266 insertions(+), 1573 deletions(-) delete mode 100644 .github/workflows/external-tests.yml delete mode 100644 .github/workflows/lua.yml delete mode 100644 .github/workflows/nix.yml delete mode 100644 .github/workflows/panvimdoc.yaml delete mode 100644 .github/workflows/python.yml delete mode 100644 .github/workflows/rust.yml delete mode 100644 .github/workflows/spelling.yaml delete mode 100644 .github/workflows/stylua.yaml create mode 100644 .npmrc.tpl delete mode 100644 packages/fff-bin-android-arm64/package.json delete mode 100644 packages/fff-bin-linux-arm64-gnu/package.json delete mode 100644 packages/fff-bin-linux-arm64-musl/package.json delete mode 100644 packages/fff-bin-linux-x64-gnu/package.json delete mode 100644 packages/fff-bin-linux-x64-musl/package.json delete mode 100644 packages/fff-bin-win32-arm64/package.json delete mode 100644 packages/fff-bin-win32-x64/package.json diff --git a/.github/workflows/external-tests.yml b/.github/workflows/external-tests.yml deleted file mode 100644 index dc04a3aa..00000000 --- a/.github/workflows/external-tests.yml +++ /dev/null @@ -1,218 +0,0 @@ -name: e2e Tests - -on: - push: - branches: [main] - paths-ignore: - - '**.md' - - 'doc/**' - pull_request: - branches: [main] - paths-ignore: - - '**.md' - - 'doc/**' - -env: - CARGO_TERM_COLOR: always - MACOSX_DEPLOYMENT_TARGET: "13" - # Force Node 24 for all JS-based actions to avoid the libuv - # process_title assertion crash on Windows (known Node 20 bug). - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - -jobs: - lua-tests: - name: e2e (${{ matrix.os }}) - runs-on: ${{ matrix.os }} - # e2e tests could be flaky on CI so we do not block release creation if they failed - continue-on-error: ${{ github.ref == 'refs/heads/main' && github.event_name == 'push' }} - strategy: - fail-fast: false - matrix: - include: - - os: ubuntu-latest - - os: macos-latest - - os: windows-latest - target: x86_64-pc-windows-msvc - steps: - - uses: actions/checkout@v5 - - uses: oven-sh/setup-bun@v2 - - uses: actions/setup-node@v6 - - name: Install Zig - uses: mlugg/setup-zig@v2 - with: - version: 0.16.0 - - - name: Install Rust - uses: actions-rust-lang/setup-rust-toolchain@v1.15.4 - with: - cache: true - cache-on-failure: false - cache-key: "v2-lua-e2e" - rustflags: "" - target: ${{ matrix.target || '' }} - - - name: Build Rust binary (Windows) - if: matrix.target - run: cargo build --release --target ${{ matrix.target }} -p fff-nvim --no-default-features --features zlob - - - name: Copy binary to target/release (Windows) - if: matrix.target - shell: bash - run: | - cp target/${{ matrix.target }}/release/fff_nvim.dll target/release/fff_nvim.dll - - - name: Verify Windows DLL has no unexpected dependencies - if: matrix.target - shell: pwsh - run: | - # Find dumpbin via vswhere (always available on GitHub Actions Windows runners) - $vsPath = & "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" -latest -property installationPath - $dumpbin = Get-ChildItem "$vsPath" -Recurse -Filter "dumpbin.exe" | Select-Object -First 1 - if (-not $dumpbin) { Write-Error "dumpbin.exe not found"; exit 1 } - - $deps = & $dumpbin.FullName /DEPENDENTS target\release\fff_nvim.dll | Out-String - Write-Host $deps - # zlob must be statically linked - fail if zlob.dll appears as a dependency - if ($deps -match 'zlob\.dll') { - Write-Error "fff_nvim.dll has unexpected dynamic dependency on zlob.dll - zlob should be statically linked" - exit 1 - } - - - name: Build Rust binary - if: ${{ !matrix.target }} - run: make build - - - name: Install Neovim - uses: rhysd/action-setup-vim@v1 - with: - neovim: true - version: v0.10.4 - - - name: Clone plenary.nvim - shell: bash - run: git clone --depth 1 https://github.com/nvim-lua/plenary.nvim ../plenary.nvim - - - name: Run Lua tests - shell: bash - run: make test-lua - - - name: Dump fff trace log on failure - if: failure() - shell: bash - run: | - # init_tracing writes session files named fff-test++.log - found=0 - for f in fff-test*.log; do - [ -f "$f" ] || continue - found=1 - echo "=== $f ===" - cat "$f" - done - if [ "$found" = 0 ]; then - echo "(no log file produced)" - fi - - - name: Run version resolution tests - shell: bash - run: make test-version - - - name: Run non windows tests - shell: bash - if: ${{ matrix.os != 'windows-latest' }} - run: | - make test-bun - make test-c-api - - - name: Verify bun --compile - shell: bash - run: make test-bun-compile - - - name: Install Node.js - if: ${{ matrix.os != 'ubuntu-latest' }} - uses: actions/setup-node@v6 - with: - node-version: "25" - - - name: Install node dependencies - shell: bash - run: cd packages/fff-node && npm install - - - name: Run node tests - shell: bash - run: make test-node - - # Regression for https://github.com/dmtrKovalenko/fff/issues/480: build & - # run @celados/fff-node end-to-end on real Alpine Linux (musl). Forces - # findBinary() through the npm-package resolver so detectLinuxLibc() - # actually runs. - alpine-musl: - name: e2e (alpine-musl) - runs-on: ubuntu-latest - container: node:22-alpine - continue-on-error: ${{ github.ref == 'refs/heads/main' && github.event_name == 'push' }} - defaults: - run: - shell: sh - steps: - - name: Install build deps - run: apk add --no-cache git rust cargo musl-dev - - - uses: actions/checkout@v5 - - # libgit2 refuses repos owned by a different user; checkout in a - # container can land at a uid mismatch, so opt every dir in. - - name: Mark workspace safe for git - run: git config --global --add safe.directory '*' - - - name: Sanity check libc is musl - run: | - if ! ldd --version 2>&1 | grep -qi musl; then - echo "FAIL: container is not running musl libc" - exit 1 - fi - - - uses: actions/cache@v4 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - target - key: alpine-musl-cargo-${{ hashFiles('**/Cargo.lock') }} - restore-keys: | - alpine-musl-cargo- - - - name: Build libfff_c (musl) - run: cargo build --release -p fff-c - - - name: Install workspace npm deps - run: npm install --no-package-lock - - # Upstream @yuuang/ffi-rs-linux-x64-musl ships with libc:"glibc" in - # its package.json (a publishing bug in ffi-rs), so npm filters it - # out. Force-install it so the FFI runtime is present on Alpine. - - name: Install ffi-rs musl runtime - run: | - FFI_RS_VERSION=$(node -p "require('ffi-rs/package.json').version") - npm install --no-package-lock --no-save --force \ - "@yuuang/ffi-rs-linux-x64-musl@${FFI_RS_VERSION}" - - # Stage the freshly built libfff_c.so as the platform npm package - # so findBinary() resolves through the fork-owned @celados/fff-bin-* path — - # this is what exercises detectLinuxLibc(). - - name: Stage musl bin package - run: | - PKG_DIR=node_modules/@celados/fff-bin-linux-x64-musl - mkdir -p "$PKG_DIR" - cp target/release/libfff_c.so "$PKG_DIR/libfff_c.so" - cat >"$PKG_DIR/package.json" <<'JSON' - { "name": "@celados/fff-bin-linux-x64-musl", "version": "0.0.0" } - JSON - node -e "const p=require('./node_modules/@celados/fff-bin-linux-x64-musl/package.json'); if(p.name!=='@celados/fff-bin-linux-x64-musl') throw new Error('staged package scope drift')" - - - name: Build fff-node - working-directory: packages/fff-node - run: npm run build - - - name: Run fff-node e2e suite - working-directory: packages/fff-node - run: node test/e2e.mjs diff --git a/.github/workflows/lua.yml b/.github/workflows/lua.yml deleted file mode 100644 index 5455d13d..00000000 --- a/.github/workflows/lua.yml +++ /dev/null @@ -1,56 +0,0 @@ -name: Lua CI - -on: - push: - branches: [main] - paths-ignore: - - '**.md' - - 'doc/**' - pull_request: - branches: [main] - paths-ignore: - - '**.md' - - 'doc/**' - -jobs: - lua-ls: - name: lua-language-server type check - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v5 - - - name: Install Neovim - run: | - curl -L https://github.com/neovim/neovim/releases/download/v0.11.5/nvim-linux-x86_64.tar.gz -o /opt/nvim.tar.gz - mkdir /opt/nvim - tar xzf /opt/nvim.tar.gz -C /opt/nvim - mv /opt/nvim/nvim-linux-x86_64/* /opt/nvim - echo "/opt/nvim/bin" >> $GITHUB_PATH - - - name: Install lua-language-server - run: | - curl -L "https://github.com/LuaLS/lua-language-server/releases/download/3.17.1/lua-language-server-3.17.1-linux-x64.tar.gz" -o /opt/lls.tar.gz - mkdir /opt/lls - tar -xzf /opt/lls.tar.gz -C /opt/lls - echo "/opt/lls/bin" >> $GITHUB_PATH - - - name: Clone snacks.nvim - run: git clone --depth=1 https://github.com/folke/snacks.nvim /opt/snacks.nvim - - - name: Run lua-language-server - run: lua-language-server --configpath .luarc.ci.json --check=. - - luacheck: - name: luacheck lint - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v5 - - - name: Install luacheck - run: | - sudo apt-get update -qq - sudo apt-get install -y luarocks - sudo luarocks install luacheck - - - name: Run luacheck - run: luacheck lua/ diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml deleted file mode 100644 index 155eeca7..00000000 --- a/.github/workflows/nix.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: Nix CI - -on: - push: - branches: [main] - paths-ignore: - - '**.md' - - 'doc/**' - pull_request: - branches: [main] - paths-ignore: - - '**.md' - - 'doc/**' - -jobs: - check: - runs-on: ubuntu-22.04 - permissions: - id-token: "write" - contents: "read" - steps: - - uses: actions/checkout@v5 - - uses: DeterminateSystems/nix-installer-action@main - - uses: DeterminateSystems/magic-nix-cache-action@main - - uses: DeterminateSystems/flake-checker-action@main - - - name: Run `nix flake check` - run: nix flake check - - - name: Run `nix build` - run: nix build - - - name: Run `nix build .#fff-nvim` - run: nix build .#fff-nvim - - - name: Run `nix run .#release` - run: nix run .#release diff --git a/.github/workflows/panvimdoc.yaml b/.github/workflows/panvimdoc.yaml deleted file mode 100644 index d8d63a99..00000000 --- a/.github/workflows/panvimdoc.yaml +++ /dev/null @@ -1,85 +0,0 @@ -on: - schedule: - - cron: "0 4 * * *" - workflow_dispatch: - -name: docs - -jobs: - docs: - runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: write - steps: - - uses: actions/checkout@v5 - with: - ref: main - fetch-depth: 2 - - - name: Extract Neovim section from README.md - run: | - awk ' - /^
/ { capture=1; next } - capture && /^<\/details>/ { capture=0; exit } - capture && /^$/ { next } - capture && /^<\/summary>$/ { next } - capture && /

.*<\/h2>/ { - gsub(/<\/?h2>/, "") - sub(/^[[:space:]]+/, "") - print "# " $0 - print "" - print "The best file search picker for Neovim. Frecency-ranked, typo-resistant, git-award, very fast." - print "" - next - } - capture { print } - ' README.md > .panvimdoc-input.md - test -s .panvimdoc-input.md - - - name: panvimdoc - uses: kdheepak/panvimdoc@main - with: - vimdoc: fff.nvim - pandoc: .panvimdoc-input.md - version: "Neovim >= 0.10.0" - demojify: true - treesitter: true - - - name: Cleanup intermediate file - run: rm -f .panvimdoc-input.md - - # panvimdoc stamps "Last change: " every run, so a daily cron always - # produces a one-line diff. Skip the PR unless a non-date line changed. - - name: Detect real doc changes - id: docdiff - run: | - if git diff --quiet -I 'Last change:' -- doc/fff.nvim.txt; then - echo "changed=false" >> "$GITHUB_OUTPUT" - else - echo "changed=true" >> "$GITHUB_OUTPUT" - fi - - - name: Create pull request - id: cpr - if: steps.docdiff.outputs.changed == 'true' - uses: peter-evans/create-pull-request@v7 - with: - branch: bot/regenerate-vimdoc - token: ${{ secrets.GUSTAV_PAT }} - delete-branch: true - title: "chore: regenerate Neovim vimdoc" - commit-message: | - chore: regenerate Neovim vimdoc - - Co-authored-by: Dmitriy Kovalenko - author: "gustav-fff <66k7bxj9m6@privaterelay.appleid.com>" - committer: "gustav-fff <66k7bxj9m6@privaterelay.appleid.com>" - body: Automated vimdoc regeneration from README.md, scribed by Gustav. - add-paths: doc/fff.nvim.txt - - - name: Enable auto-merge - if: steps.cpr.outputs.pull-request-number - env: - GH_TOKEN: ${{ secrets.GUSTAV_PAT }} - run: gh pr merge --auto --squash "${{ steps.cpr.outputs.pull-request-number }}" diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml deleted file mode 100644 index 43426ef9..00000000 --- a/.github/workflows/python.yml +++ /dev/null @@ -1,48 +0,0 @@ -name: Python CI - -on: - push: - branches: [main] - paths-ignore: - - '**.md' - - 'doc/**' - pull_request: - branches: [main] - paths-ignore: - - '**.md' - - 'doc/**' - -env: - CARGO_TERM_COLOR: always - MACOSX_DEPLOYMENT_TARGET: "13.0" - -jobs: - test: - name: Python bindings (${{ matrix.os }}) - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - - steps: - - uses: actions/checkout@v5 - with: - persist-credentials: false - - - name: Install Rust - uses: dtolnay/rust-toolchain@stable - - - name: Setup uv - uses: astral-sh/setup-uv@v5 - with: - version: "0.11.14" - enable-cache: true - - - name: Build and test Python bindings - working-directory: packages/fff-python - shell: bash - run: | - uv sync --all-extras - uv run maturin develop --release - uv run pytest -v diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index a0d664e6..219b67a3 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -1,716 +1,195 @@ -name: Build & Publish +name: Markd Node CI & Publish on: push: - branches: [main, fix/use-trusted-publishing] - tags: - - "v*" - pull_request: + branches: [main] + tags: ["v*"] workflow_dispatch: -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - -jobs: - build-nvim: - name: Build Neovim ${{ matrix.target }} - runs-on: ${{ matrix.os }} - permissions: - contents: read - id-token: write - strategy: - matrix: - include: - ## Linux builds (using cargo-zigbuild) - # Glibc 2.31 (Ubuntu 20.04, Debian 11, RHEL 9). - # Rust 1.91+ requires glibc >= 2.31 for std::sys::random::getrandom, - # copy_file_range, and statx; earlier targets (2.17) no longer link. - - os: ubuntu-latest - target: x86_64-unknown-linux-gnu - zigbuild_target: x86_64-unknown-linux-gnu.2.31 - artifact_name: target/x86_64-unknown-linux-gnu/ci/libfff_nvim.so - ext: so - - os: ubuntu-latest - target: aarch64-unknown-linux-gnu - zigbuild_target: aarch64-unknown-linux-gnu.2.31 - artifact_name: target/aarch64-unknown-linux-gnu/ci/libfff_nvim.so - ext: so - # Musl (statically linked) - - os: ubuntu-latest - target: x86_64-unknown-linux-musl - artifact_name: target/x86_64-unknown-linux-musl/ci/libfff_nvim.so - ext: so - - os: ubuntu-latest - target: aarch64-unknown-linux-musl - artifact_name: target/aarch64-unknown-linux-musl/ci/libfff_nvim.so - ext: so - - ## Android (Termux) - - os: ubuntu-latest - target: aarch64-linux-android - artifact_name: target/aarch64-linux-android/ci/libfff_nvim.so - ext: so - - ## macOS builds - - os: macos-latest - target: x86_64-apple-darwin - artifact_name: target/x86_64-apple-darwin/ci/libfff_nvim.dylib - ext: dylib - - os: macos-latest - target: aarch64-apple-darwin - artifact_name: target/aarch64-apple-darwin/ci/libfff_nvim.dylib - ext: dylib - - - os: windows-latest - target: x86_64-pc-windows-msvc - artifact_name: target/x86_64-pc-windows-msvc/ci/fff_nvim.dll - ext: dll - - os: windows-latest - target: aarch64-pc-windows-msvc - artifact_name: target/aarch64-pc-windows-msvc/ci/fff_nvim.dll - ext: dll - - steps: - - uses: actions/checkout@v5 - with: - persist-credentials: false - - - name: Install Rust - run: rustup target add ${{ matrix.target }} - - # Cache the per-target build dir and cargo-zigbuild binary. Keyed by - # target so matrix legs don't collide. See issue on slow release CI. - - name: Rust cache - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 - with: - key: nvim-${{ matrix.target }} - - - name: Install Zig - uses: mlugg/setup-zig@v2 - with: - version: 0.16.0 - - - name: Install cargo-zigbuild - if: contains(matrix.os, 'ubuntu') - run: cargo install cargo-zigbuild - - - name: Build for Linux - if: contains(matrix.os, 'ubuntu') && !contains(matrix.target, 'android') - run: | - cargo zigbuild --profile ci --target ${{ matrix.zigbuild_target || matrix.target }} -p fff-nvim --no-default-features --features zlob - mv "${{ matrix.artifact_name }}" "${{ matrix.target }}.${{ matrix.ext }}" - - - name: Build for Android (Termux) - if: contains(matrix.target, 'android') - run: | - NDK_BIN="$ANDROID_NDK/toolchains/llvm/prebuilt/linux-x86_64/bin" - - # NDK clang for C deps (libgit2, lmdb, blake3) that need Bionic sysroot headers - export CC_aarch64_linux_android="$NDK_BIN/aarch64-linux-android24-clang" - export CXX_aarch64_linux_android="$NDK_BIN/aarch64-linux-android24-clang++" - export AR_aarch64_linux_android="$NDK_BIN/llvm-ar" - export CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER="$NDK_BIN/aarch64-linux-android24-clang" - - cargo build --profile ci --target ${{ matrix.target }} -p fff-nvim --no-default-features --features zlob - mv "${{ matrix.artifact_name }}" "${{ matrix.target }}.${{ matrix.ext }}" - - - name: Build for macOS - if: contains(matrix.os, 'macos') - run: | - MACOSX_DEPLOYMENT_TARGET="13" cargo build --profile ci --target ${{ matrix.target }} -p fff-nvim --no-default-features --features zlob - mv "${{ matrix.artifact_name }}" "${{ matrix.target }}.${{ matrix.ext }}" - - - name: Ad-hoc sign macOS binary - if: contains(matrix.os, 'macos') - run: codesign --force --sign - "${{ matrix.target }}.${{ matrix.ext }}" - - - name: Build for Windows - if: contains(matrix.os, 'windows') - shell: bash - run: | - cargo build --profile ci --target ${{ matrix.target }} -p fff-nvim --no-default-features --features zlob - mv "${{ matrix.artifact_name }}" "${{ matrix.target }}.${{ matrix.ext }}" - - - name: Upload artifacts - uses: actions/upload-artifact@v4 - with: - name: nvim-${{ matrix.target }} - path: ${{ matrix.target }}.* - - build-c: - name: Build C FFI ${{ matrix.target }} - runs-on: ${{ matrix.os }} - permissions: - contents: read - strategy: - matrix: - include: - ## Linux builds - - os: ubuntu-latest - target: x86_64-unknown-linux-gnu - zigbuild_target: x86_64-unknown-linux-gnu.2.31 - artifact_name: target/x86_64-unknown-linux-gnu/ci/libfff_c.so - npm_package: fff-bin-linux-x64-gnu - lib_filename: libfff_c.so - ext: so - - os: ubuntu-latest - target: aarch64-unknown-linux-gnu - zigbuild_target: aarch64-unknown-linux-gnu.2.31 - artifact_name: target/aarch64-unknown-linux-gnu/ci/libfff_c.so - npm_package: fff-bin-linux-arm64-gnu - lib_filename: libfff_c.so - ext: so - - os: ubuntu-latest - target: x86_64-unknown-linux-musl - artifact_name: target/x86_64-unknown-linux-musl/ci/libfff_c.so - npm_package: fff-bin-linux-x64-musl - lib_filename: libfff_c.so - ext: so - - os: ubuntu-latest - target: aarch64-unknown-linux-musl - artifact_name: target/aarch64-unknown-linux-musl/ci/libfff_c.so - npm_package: fff-bin-linux-arm64-musl - lib_filename: libfff_c.so - ext: so - - ## Android (Termux) - - os: ubuntu-latest - target: aarch64-linux-android - artifact_name: target/aarch64-linux-android/ci/libfff_c.so - npm_package: fff-bin-android-arm64 - lib_filename: libfff_c.so - ext: so - - ## Windows builds - - os: windows-latest - target: x86_64-pc-windows-msvc - artifact_name: target/x86_64-pc-windows-msvc/ci/fff_c.dll - npm_package: fff-bin-win32-x64 - lib_filename: fff_c.dll - ext: dll - - os: windows-latest - target: aarch64-pc-windows-msvc - artifact_name: target/aarch64-pc-windows-msvc/ci/fff_c.dll - npm_package: fff-bin-win32-arm64 - lib_filename: fff_c.dll - ext: dll - - steps: - - uses: actions/checkout@v7 - with: - persist-credentials: false - - - name: Install Rust - run: rustup target add ${{ matrix.target }} - - - name: Rust cache - uses: Swatinem/rust-cache@v2 - with: - key: c-${{ matrix.target }} - - - name: Install Zig - uses: mlugg/setup-zig@v2 - with: - version: 0.16.0 - - - name: Install cargo-zigbuild - if: contains(matrix.os, 'ubuntu') - run: cargo install cargo-zigbuild - - - name: Build for Linux - if: contains(matrix.os, 'ubuntu') && !contains(matrix.target, 'android') - run: | - cargo zigbuild --profile ci --target ${{ matrix.zigbuild_target || matrix.target }} -p fff-c --no-default-features --features zlob - mv "${{ matrix.artifact_name }}" "c-lib-${{ matrix.target }}.${{ matrix.ext }}" - - - name: Build for Android (Termux) - if: contains(matrix.target, 'android') - run: | - NDK_BIN="$ANDROID_NDK/toolchains/llvm/prebuilt/linux-x86_64/bin" - - export CC_aarch64_linux_android="$NDK_BIN/aarch64-linux-android24-clang" - export CXX_aarch64_linux_android="$NDK_BIN/aarch64-linux-android24-clang++" - export AR_aarch64_linux_android="$NDK_BIN/llvm-ar" - export CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER="$NDK_BIN/aarch64-linux-android24-clang" - - cargo build --profile ci --target ${{ matrix.target }} -p fff-c --no-default-features --features zlob - mv "${{ matrix.artifact_name }}" "c-lib-${{ matrix.target }}.${{ matrix.ext }}" +permissions: + contents: read - - name: Build for Windows - if: contains(matrix.os, 'windows') - shell: bash - run: | - cargo build --profile ci --target ${{ matrix.target }} -p fff-c --no-default-features --features zlob - mv "${{ matrix.artifact_name }}" "c-lib-${{ matrix.target }}.${{ matrix.ext }}" - - - name: Prepare npm package - shell: bash - run: | - # Copy the built binary into the platform npm package directory - cp "c-lib-${{ matrix.target }}.${{ matrix.ext }}" "packages/${{ matrix.npm_package }}/${{ matrix.lib_filename }}" +concurrency: + group: markd-node-${{ github.ref }} + cancel-in-progress: false - - name: Upload C library artifact - uses: actions/upload-artifact@v7 - with: - name: c-lib-${{ matrix.target }} - path: c-lib-${{ matrix.target }}.* - - - name: Upload npm package artifact - uses: actions/upload-artifact@v7 - with: - name: npm-${{ matrix.npm_package }} - path: packages/${{ matrix.npm_package }}/ +env: + CARGO_BUILD_JOBS: "4" + MACOSX_DEPLOYMENT_TARGET: "13" - build-c-macos: - name: Build C FFI ${{ matrix.target }} +jobs: + build-test-publish: + # The persistent runner must never execute fork or arbitrary branch code. + if: >- + github.event_name != 'workflow_dispatch' || + github.ref == 'refs/heads/main' || + startsWith(github.ref, 'refs/tags/v') runs-on: [self-hosted, macOS, ARM64] - if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository - permissions: - contents: read - strategy: - matrix: - include: - - target: x86_64-apple-darwin - artifact_name: target/x86_64-apple-darwin/ci/libfff_c.dylib - npm_package: fff-bin-darwin-x64 - - target: aarch64-apple-darwin - artifact_name: target/aarch64-apple-darwin/ci/libfff_c.dylib - npm_package: fff-bin-darwin-arm64 + timeout-minutes: 60 steps: - name: Prepare persistent workspace run: | + echo "/opt/homebrew/bin" >> "$GITHUB_PATH" echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" - [ -d .git ] && git reset --hard --quiet || true - size=$(du -sg target 2>/dev/null | cut -f1 || echo 0) - [ "${size:-0}" -gt 40 ] && rm -rf target || true + # A killed publish may leave tracked manifests versioned; reset before checkout. + [ -d "$GITHUB_WORKSPACE/.git" ] && git -C "$GITHUB_WORKSPACE" reset --hard --quiet || true + size=$(du -sg "$GITHUB_WORKSPACE/target" 2>/dev/null | cut -f1 || echo 0) + [ "${size:-0}" -gt 20 ] && rm -rf "$GITHUB_WORKSPACE/target" || true - uses: actions/checkout@v7 with: - persist-credentials: false clean: false - - - name: Install Rust target - run: rustup target add ${{ matrix.target }} - - - name: Install Zig - uses: mlugg/setup-zig@v2 - with: - version: 0.16.0 - - - name: Build and sign - run: | - MACOSX_DEPLOYMENT_TARGET="13" cargo build --profile ci --target ${{ matrix.target }} -p fff-c --no-default-features --features zlob - mv "${{ matrix.artifact_name }}" "c-lib-${{ matrix.target }}.dylib" - codesign --force --sign - "c-lib-${{ matrix.target }}.dylib" - cp "c-lib-${{ matrix.target }}.dylib" "packages/${{ matrix.npm_package }}/libfff_c.dylib" - - - name: Upload C library artifact - uses: actions/upload-artifact@v7 - with: - name: c-lib-${{ matrix.target }} - path: c-lib-${{ matrix.target }}.dylib - - - name: Upload npm package artifact - uses: actions/upload-artifact@v7 - with: - name: npm-${{ matrix.npm_package }} - path: packages/${{ matrix.npm_package }}/ - - build-mcp: - name: Build MCP ${{ matrix.target }} - runs-on: ${{ matrix.os }} - permissions: - contents: read - strategy: - matrix: - include: - ## Linux builds (using cargo-zigbuild) - - os: ubuntu-latest - target: x86_64-unknown-linux-gnu - zigbuild_target: x86_64-unknown-linux-gnu.2.31 - artifact_name: target/x86_64-unknown-linux-gnu/ci/fff-mcp - - os: ubuntu-latest - target: aarch64-unknown-linux-gnu - zigbuild_target: aarch64-unknown-linux-gnu.2.31 - artifact_name: target/aarch64-unknown-linux-gnu/ci/fff-mcp - - os: ubuntu-latest - target: x86_64-unknown-linux-musl - artifact_name: target/x86_64-unknown-linux-musl/ci/fff-mcp - - os: ubuntu-latest - target: aarch64-unknown-linux-musl - artifact_name: target/aarch64-unknown-linux-musl/ci/fff-mcp - - ## macOS builds - - os: macos-latest - target: x86_64-apple-darwin - artifact_name: target/x86_64-apple-darwin/ci/fff-mcp - - os: macos-latest - target: aarch64-apple-darwin - artifact_name: target/aarch64-apple-darwin/ci/fff-mcp - - ## Windows builds - - os: windows-latest - target: x86_64-pc-windows-msvc - artifact_name: target/x86_64-pc-windows-msvc/ci/fff-mcp.exe - - os: windows-latest - target: aarch64-pc-windows-msvc - artifact_name: target/aarch64-pc-windows-msvc/ci/fff-mcp.exe - - steps: - - uses: actions/checkout@v5 - with: + fetch-depth: 0 persist-credentials: false - - name: Install Rust - run: rustup target add ${{ matrix.target }} - - - name: Rust cache - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 + - uses: actions/setup-node@v7 with: - key: mcp-${{ matrix.target }} - - - name: Install Zig - uses: mlugg/setup-zig@v2 - with: - version: 0.16.0 + node-version: "24" - - name: Install cargo-zigbuild - if: contains(matrix.os, 'ubuntu') - run: cargo install cargo-zigbuild - - - name: Build for Linux - if: contains(matrix.os, 'ubuntu') - run: | - cargo zigbuild --profile ci --target ${{ matrix.zigbuild_target || matrix.target }} -p fff-mcp --no-default-features --features zlob - cp "${{ matrix.artifact_name }}" "fff-mcp-${{ matrix.target }}" - - - name: Build for macOS - if: contains(matrix.os, 'macos') - run: | - MACOSX_DEPLOYMENT_TARGET="13" cargo build --profile ci --target ${{ matrix.target }} -p fff-mcp --no-default-features --features zlob - cp "${{ matrix.artifact_name }}" "fff-mcp-${{ matrix.target }}" - - - name: Ad-hoc sign macOS binary - if: contains(matrix.os, 'macos') - run: codesign --force --sign - "fff-mcp-${{ matrix.target }}" - - - name: Build for Windows - if: contains(matrix.os, 'windows') - shell: bash - run: | - cargo build --profile ci --target ${{ matrix.target }} -p fff-mcp --no-default-features --features zlob - cp "${{ matrix.artifact_name }}" "fff-mcp-${{ matrix.target }}.exe" - - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: mcp-${{ matrix.target }} - path: fff-mcp-${{ matrix.target }}* - - build-python: - name: Build Python wheels ${{ matrix.target }} (${{ matrix.os }}) - # Wheels are release artifacts; PR validation uses the develop build in - # python.yml, so skip the cross-compile matrix on pull requests. - if: github.event_name != 'pull_request' - runs-on: ${{ matrix.os }} - permissions: - contents: read - strategy: - fail-fast: false - matrix: - include: - - os: ubuntu-latest - target: x86_64 - container: "off" - - os: ubuntu-latest - target: aarch64 - container: "off" - - os: macos-latest - target: x86_64 - - os: macos-latest - target: aarch64 - - os: windows-latest - target: x86_64 - - steps: - - uses: actions/checkout@v5 + - name: Install Lua + uses: leafo/gh-actions-lua@v13 with: - persist-credentials: false - - - name: Install Rust - uses: dtolnay/rust-toolchain@stable + luaVersion: "5.4" - name: Install Zig uses: mlugg/setup-zig@v2 with: version: 0.16.0 - - name: Install cargo-zigbuild - if: contains(matrix.os, 'ubuntu') - run: cargo install cargo-zigbuild - - - name: Install aarch64 cross compiler - if: matrix.target == 'aarch64' && contains(matrix.os, 'ubuntu') + - name: Configure private registry run: | - sudo apt-get update -qq - sudo apt-get install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu + printf '%s\n' \ + '@celados:registry=https://npm.celados.com' \ + '//npm.celados.com/:_authToken=${NODE_AUTH_TOKEN}' > .npmrc - - name: Build wheels - uses: PyO3/maturin-action@v1 + - name: Install Node dependencies env: - CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc - CC_aarch64_unknown_linux_gnu: aarch64-linux-gnu-gcc - CXX_aarch64_unknown_linux_gnu: aarch64-linux-gnu-g++ - AR_aarch64_unknown_linux_gnu: aarch64-linux-gnu-ar - with: - target: ${{ matrix.target }} - args: --release --out dist --no-default-features --features zlob - sccache: "true" - working-directory: packages/fff-python - container: ${{ matrix.container || '' }} - - - name: Upload wheels - uses: actions/upload-artifact@v4 - with: - name: python-wheels-${{ matrix.os }}-${{ matrix.target }} - path: packages/fff-python/dist/ - - build-python-sdist: - name: Build Python sdist - if: github.event_name != 'pull_request' - runs-on: ubuntu-latest - permissions: - contents: read - steps: - - uses: actions/checkout@v5 - with: - persist-credentials: false - - - name: Build sdist - uses: PyO3/maturin-action@v1 - with: - command: sdist - args: --out dist - working-directory: packages/fff-python - - - name: Upload sdist - uses: actions/upload-artifact@v4 - with: - name: python-sdist - path: packages/fff-python/dist/ - - release: - name: Release - needs: [build-nvim, build-c, build-c-macos, build-mcp, build-python, build-python-sdist] - runs-on: ubuntu-latest - if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/fix/use-trusted-publishing' || startsWith(github.ref, 'refs/tags/v')) - permissions: - contents: write - steps: - # we have to make sure that pushing a commit on this workflow triggers the CI for nightly neovim - - uses: actions/checkout@v7 - with: - token: ${{ secrets.GUSTAV_PAT || github.token }} - - - name: Install Lua - uses: leafo/gh-actions-lua@v13 + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: >- + npm ci --ignore-scripts + --workspace @celados/fff-node --include-workspace-root=false - - name: Download artifacts - uses: actions/download-artifact@v8 - with: - path: ./binaries + - name: Install macOS Rust targets + run: rustup target add aarch64-apple-darwin x86_64-apple-darwin - - name: Flatten and rename Neovim artifacts - working-directory: ./binaries + - name: Test zlob core and Node binding run: | - # Move nvim artifacts to root level with original naming - for dir in nvim-*/; do - target="${dir#nvim-}" - target="${target%/}" - for file in "$dir"*; do - if [ -f "$file" ]; then - filename=$(basename "$file") - mv "$file" "./$filename" - fi - done - rmdir "$dir" 2>/dev/null || true - done + cargo test -p fff-search --no-default-features --features zlob --lib + make test-node - - name: Flatten C library artifacts - working-directory: ./binaries + - name: Build macOS C libraries run: | - # Move c-lib artifacts to root level - for dir in c-lib-*/; do - for file in "$dir"*; do - if [ -f "$file" ]; then - filename=$(basename "$file") - mv "$file" "./$filename" - fi - done - rmdir "$dir" 2>/dev/null || true + for target in aarch64-apple-darwin x86_64-apple-darwin; do + cargo build --profile ci --target "$target" -p fff-c --no-default-features --features zlob done - - - name: Flatten MCP artifacts - working-directory: ./binaries - run: | - for dir in mcp-*/; do - for file in "$dir"*; do - if [ -f "$file" ]; then - filename=$(basename "$file") - mv "$file" "./$filename" - fi - done - rmdir "$dir" 2>/dev/null || true - done - - - name: Move Python wheels to release directory - working-directory: ./binaries - run: | - mkdir -p python - for dir in python-wheels-*/ python-sdist/; do - [ -d "$dir" ] || continue - for file in "$dir"*; do - if [ -f "$file" ]; then - mv "$file" "python/$(basename "$file")" - fi - done - rmdir "$dir" 2>/dev/null || true - done - - - name: Remove npm package artifacts from release binaries - working-directory: ./binaries - run: | - rm -rf npm-* - - - name: Generate checksums - working-directory: ./binaries - run: | - ls -la - for file in * python/*; do - if [ -f "$file" ] && [[ ! "$file" == *.sha256 ]]; then - sha256sum "$file" > "${file}.sha256" - fi + cp target/aarch64-apple-darwin/ci/libfff_c.dylib packages/fff-bin-darwin-arm64/libfff_c.dylib + cp target/x86_64-apple-darwin/ci/libfff_c.dylib packages/fff-bin-darwin-x64/libfff_c.dylib + [ "$(lipo -archs packages/fff-bin-darwin-arm64/libfff_c.dylib)" = "arm64" ] + [ "$(lipo -archs packages/fff-bin-darwin-x64/libfff_c.dylib)" = "x86_64" ] + file packages/fff-bin-darwin-arm64/libfff_c.dylib | grep -q 'arm64' + file packages/fff-bin-darwin-x64/libfff_c.dylib | grep -q 'x86_64' + for dylib in packages/fff-bin-darwin-*/libfff_c.dylib; do + codesign --force --sign - "$dylib" + codesign --verify --strict "$dylib" done - name: Determine version id: version run: lua scripts/determine-version.lua - # Nightlies publish to a permanent per-sha tag (release_tag == version) so - # pinned/stale installs always fetch the binary built for their own commit. - # The rolling `nightly` tag is also moved to HEAD for "give me latest" tooling. - - name: Move rolling nightly tag to current commit - if: steps.version.outputs.is_release != 'true' - run: | - git tag -f nightly "${{ github.sha }}" - git push -f origin refs/tags/nightly - - - name: Upload Release Assets - uses: softprops/action-gh-release@v2 - with: - name: "${{ steps.version.outputs.version }}" - tag_name: "${{ steps.version.outputs.release_tag }}" - token: ${{ github.token }} - files: | - ./binaries/* - ./binaries/python/* - draft: false - prerelease: ${{ steps.version.outputs.is_release != 'true' }} - generate_release_notes: ${{ steps.version.outputs.is_release == 'true' }} - body: | - ${{ steps.version.outputs.is_release == 'true' && format('Release {0}', steps.version.outputs.version) || format('Nightly release from commit: {0}', github.sha) }} - - Native assets and `@celados/fff-node` packages are available under this version ${{ steps.version.outputs.version }}. - - ## Neovim Plugin - - `{target}.so` / `.dylib` / `.dll` - Lua module for Neovim - - ## C FFI Library (for Bun/Node/Python) - - `c-lib-{target}.so` / `.dylib` / `.dll` - C FFI library - - ## MCP Server - - `fff-mcp-{target}` - MCP server binary - - ## Python Package - - `python/*.whl` / `python/*.tar.gz` - Python wheels and sdist - - Fork builds are GitHub Release assets only; this workflow does not publish PyPI. - - - name: Bump Homebrew formula (uses local checksums) - if: steps.version.outputs.is_release == 'true' && github.repository_owner == 'dmtrKovalenko' - run: make bump-homebrew-formula VERSION="${{ steps.version.outputs.version }}" BINARIES_DIR=./binaries - - - name: Pin SHAs in install-mcp.sh (uses local checksums) - if: steps.version.outputs.is_release == 'true' && github.repository_owner == 'dmtrKovalenko' - run: make bump-install-mcp-sh VERSION="${{ steps.version.outputs.version }}" BINARIES_DIR=./binaries - - - name: Commit formula + installer bump to main - if: steps.version.outputs.is_release == 'true' && github.repository_owner == 'dmtrKovalenko' - uses: stefanzweifel/git-auto-commit-action@v5 - with: - # Workflow runs on detached HEAD at the v* tag — explicit target needed. - branch: main - commit_message: "chore: bump fff-mcp release artifacts to v${{ steps.version.outputs.version }}" - file_pattern: "Formula/fff-mcp.rb install-mcp.sh" - commit_user_name: github-actions[bot] - commit_user_email: 41898282+github-actions[bot]@users.noreply.github.com - - npm-publish: - name: Publish npm packages - needs: [build-c, build-c-macos] - runs-on: ubuntu-latest - if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/fix/use-trusted-publishing' || startsWith(github.ref, 'refs/tags/v')) - permissions: - contents: read - id-token: write - steps: - - uses: actions/checkout@v7 - - - name: Install Lua - uses: leafo/gh-actions-lua@v13 - - - name: Setup Node.js - uses: actions/setup-node@v7 - with: - node-version: "25" - registry-url: "https://registry.npmjs.org" - - - name: Determine version - id: version - run: lua scripts/determine-version.lua - - - name: Download npm package artifacts - uses: actions/download-artifact@v8 - with: - pattern: npm-* - path: ./npm-packages - - - name: Publish platform packages + - name: Prepare publish manifests + if: github.event_name == 'push' + env: + VERSION: ${{ steps.version.outputs.version }} run: | - VERSION="${{ steps.version.outputs.version }}" - TAG="${{ steps.version.outputs.npm_tag }}" - - for pkg_dir in ./npm-packages/npm-*/; do - if [ -d "$pkg_dir" ]; then - pkg_name=$(node -p "require('${pkg_dir}package.json').name") - echo "Publishing ${pkg_name}@${VERSION} with tag ${TAG}..." - - make set-npm-version PKG="$pkg_dir" VERSION="$VERSION" - - cd "$pkg_dir" - npm publish --tag "$TAG" --access public --provenance - cd - + make set-npm-version PKG=packages/fff-bin-darwin-arm64 VERSION="$VERSION" + make set-npm-version PKG=packages/fff-bin-darwin-x64 VERSION="$VERSION" + make set-npm-version PKG=packages/fff-node VERSION="$VERSION" + node <<'NODE' + const fs = require("node:fs"); + const expected = [ + "@celados/fff-bin-darwin-arm64", + "@celados/fff-bin-darwin-x64", + "@celados/fff-node", + ]; + for (const name of expected) { + const dir = name.replace("@celados/", "packages/"); + const pkg = JSON.parse(fs.readFileSync(`${dir}/package.json`, "utf8")); + if (pkg.name !== name || pkg.version !== process.env.VERSION) { + throw new Error(`publish identity drift: ${pkg.name}@${pkg.version}`); + } + if (pkg.publishConfig?.registry !== "https://npm.celados.com") { + throw new Error(`registry drift: ${name}`); + } + } + const nodePkg = JSON.parse(fs.readFileSync("packages/fff-node/package.json", "utf8")); + const optionalNames = Object.keys(nodePkg.optionalDependencies ?? {}).sort(); + if (optionalNames.join("\n") !== expected.slice(0, 2).sort().join("\n")) { + throw new Error(`platform package drift: ${optionalNames.join(", ")}`); + } + NODE + + - name: Prove unpublished package artifacts + if: github.event_name == 'push' + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + VERSION: ${{ steps.version.outputs.version }} + run: | + for dir in packages/fff-bin-darwin-arm64 packages/fff-bin-darwin-x64 packages/fff-node; do + name=$(node -p "require('./$dir/package.json').name") + pack_json="$RUNNER_TEMP/$(basename "$dir")-pack.json" + (cd "$dir" && npm pack --dry-run --json) > "$pack_json" + if [[ "$dir" == packages/fff-bin-* ]]; then + node -e 'const p=require(process.argv[1]); const files=p[0].files.map(x=>x.path).sort(); if(files.join("\n")!=="libfff_c.dylib\npackage.json") throw new Error(`platform tarball drift: ${files}`)' "$pack_json" + else + node -e 'const p=require(process.argv[1])[0]; if(p.files.some(x=>/\.(dylib|so|dll)$/.test(x.path))) throw new Error("Node tarball duplicated a native binary")' "$pack_json" + fi + if npm view "$name@$VERSION" version --registry=https://npm.celados.com >/dev/null 2>"$RUNNER_TEMP/npm-view.err"; then + echo "$name@$VERSION is already published" >&2 + exit 1 + fi + if ! grep -Eq 'E404|404 Not Found' "$RUNNER_TEMP/npm-view.err"; then + cat "$RUNNER_TEMP/npm-view.err" >&2 + exit 1 fi done - - name: Publish Node.js package + - name: Publish exact version + if: github.event_name == 'push' + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + TAG: ${{ steps.version.outputs.npm_tag }} run: | - VERSION="${{ steps.version.outputs.version }}" - TAG="${{ steps.version.outputs.npm_tag }}" + (cd packages/fff-bin-darwin-arm64 && npm publish --tag "$TAG" --access public) + (cd packages/fff-bin-darwin-x64 && npm publish --tag "$TAG" --access public) + (cd packages/fff-node && npm publish --tag "$TAG" --access public) - echo "Publishing @celados/fff-node@${VERSION} with tag ${TAG}..." - make set-npm-version PKG=packages/fff-node VERSION="$VERSION" - - cd packages/fff-node - npm install - npm run build - npm publish --tag "$TAG" --access public --provenance + - name: Verify clean consumer + if: github.event_name == 'push' + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + VERSION: ${{ steps.version.outputs.version }} + run: | + consumer=$(mktemp -d "$RUNNER_TEMP/fff-consumer.XXXXXX") + cp .npmrc "$consumer/.npmrc" + printf '{"private":true,"type":"module"}\n' > "$consumer/package.json" + printf 'markd verification\n' > "$consumer/needle-markd.txt" + npm install --prefix "$consumer" --save-exact "@celados/fff-node@$VERSION" + cd "$consumer" + node --input-type=module <<'NODE' + import { FileFinder } from "@celados/fff-node"; + const created = FileFinder.create({ basePath: process.cwd() }); + if (!created.ok) throw new Error(created.error); + const finder = created.value; + try { + const scanned = await finder.waitForScan(5_000); + if (!scanned.ok || !scanned.value) throw new Error("scan did not finish"); + const result = finder.fileSearch("needle-markd", { pageSize: 5 }); + if (!result.ok || !result.value.items.some((item) => item.relativePath === "needle-markd.txt")) { + throw new Error("exact-version package failed a real FileFinder query"); + } + } finally { + finder.destroy(); + } + console.log(`verified @celados/fff-node@${process.env.VERSION} from npm.celados.com`); + NODE diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml deleted file mode 100644 index 1d60b0f5..00000000 --- a/.github/workflows/rust.yml +++ /dev/null @@ -1,162 +0,0 @@ -name: Rust CI - -on: - push: - branches: [main] - paths-ignore: - - '**.md' - - 'doc/**' - pull_request: - branches: [main] - paths-ignore: - - '**.md' - - 'doc/**' - -env: - CARGO_TERM_COLOR: always - # Ensure consistent macOS deployment target across all compiled objects - # (Rust, cc-compiled C code, and Zig-compiled zlob) to avoid linker warnings - MACOSX_DEPLOYMENT_TARGET: "13" - -jobs: - test: - name: Test - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - # Guard against deadlocks in the shared-picker / watcher teardown - # path: a stuck test would otherwise consume a full 6h CI slot. - timeout-minutes: 15 - steps: - - uses: actions/checkout@v5 - - # Zig is required to compile zlob - - name: Install Zig - uses: goto-bus-stop/setup-zig@v2 - with: - version: 0.16.0 - - - name: Install Rust - uses: actions-rust-lang/setup-rust-toolchain@v1.15.4 - with: - cache: true - cache-on-failure: true - cache-key: "v1-rust" - components: rustfmt, clippy - - - name: Run tests - run: cargo test --no-default-features --features zlob --workspace --exclude fff-nvim - - stress-test: - name: Fuzz Tests - runs-on: ${{ matrix.os }} - strategy: - # Keep going after one OS fails so we can see whether a bug - # reproduces everywhere or is platform-specific. - fail-fast: false - matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - # Long-running; don't let a stuck watcher thread burn a full CI - # timeout. Two scenarios should finish well under this limit. - timeout-minutes: 20 - env: - FFF_STRESS_CASES: "5" - FFF_STRESS_MIN_OPS: "30" - FFF_STRESS_MAX_OPS: "60" - steps: - - uses: actions/checkout@v5 - - - name: Install Zig - uses: goto-bus-stop/setup-zig@v2 - with: - version: 0.16.0 - - - name: Install Rust - uses: actions-rust-lang/setup-rust-toolchain@v1.15.4 - with: - cache: true - cache-on-failure: true - cache-key: "v1-rust-stress-${{ matrix.os }}" - components: rustfmt, clippy - - - name: Stress test seeded - shell: bash - run: make test-stress-seeded - - - name: Stress test random - shell: bash - run: make test-stress-random - - - name: Stress test regressions - shell: bash - run: make test-stress-regressions - - - name: Upload proptest regressions on failure - if: failure() - uses: actions/upload-artifact@v4 - with: - name: proptest-regressions-${{ matrix.os }} - path: crates/fff-core/tests/fuzz_git_watcher_stress.proptest-regressions - if-no-files-found: ignore - - build-i686: - name: Build i686-unknown-linux-gnu - runs-on: ubuntu-latest - # Verifies that fff-search compiles on 32-bit x86, where std::arch::x86_64 - # is unavailable. SIMD paths are disabled on this target; only the scalar - # fallback should build. See issue #656. - timeout-minutes: 15 - steps: - - uses: actions/checkout@v5 - - - name: Install cross toolchain - run: | - sudo apt-get update - sudo apt-get install -y gcc-multilib g++-multilib - - - name: Install Rust (i686 target) - uses: actions-rust-lang/setup-rust-toolchain@v1.15.4 - with: - target: i686-unknown-linux-gnu - cache: true - cache-on-failure: true - cache-key: "v1-rust-i686" - - - name: Build fff-search for i686 - run: cargo build -p fff-search --target i686-unknown-linux-gnu - - fmt: - name: cargo fmt - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v5 - - name: Install Rust - uses: dtolnay/rust-toolchain@master - with: - toolchain: stable - components: rustfmt - - name: Check formatting - run: cargo fmt -- --check - - clippy: - name: cargo clippy - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v5 - - # Zig is required to compile zlob - - name: Install Zig - uses: goto-bus-stop/setup-zig@v2 - with: - version: 0.16.0 - - - name: Install Rust - uses: dtolnay/rust-toolchain@master - with: - toolchain: stable - components: clippy - - - name: Run clippy - run: cargo clippy --no-default-features --features zlob -- -D warnings diff --git a/.github/workflows/spelling.yaml b/.github/workflows/spelling.yaml deleted file mode 100644 index b3b8d41c..00000000 --- a/.github/workflows/spelling.yaml +++ /dev/null @@ -1,25 +0,0 @@ -name: Spelling - -permissions: - contents: read - -on: - push: - branches: - - main - pull_request: - -env: - CLICOLOR: 1 - -jobs: - spelling: - name: Spell Check with Typos - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v5 - with: - persist-credentials: false - - - name: Spell Check Repo - uses: crate-ci/typos@685eb3d55be2f85191e8c84acb9f44d7756f84ab # v1.29.4 diff --git a/.github/workflows/stylua.yaml b/.github/workflows/stylua.yaml deleted file mode 100644 index fa7edb11..00000000 --- a/.github/workflows/stylua.yaml +++ /dev/null @@ -1,37 +0,0 @@ -name: Stylua - -permissions: - contents: read - -on: - push: - branches: - - main - paths: - - "**/*.lua" - - .stylua.toml - - .github/workflows/stylua.yaml - pull_request: - paths: - - "**/*.lua" - - .stylua.toml - - .github/workflows/stylua.yaml - -env: - CLICOLOR: 1 - -jobs: - stylua: - name: Check lua files using Stylua - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v5 - with: - persist-credentials: false - - - name: Stylua Check Repo - uses: JohnnyMorganz/stylua-action@v4 - with: - token: ${{ secrets.GITHUB_TOKEN }} - version: latest - args: --color=always --check . diff --git a/.gitignore b/.gitignore index 4d026b32..86db4bfb 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ result # all the perf like utility files *.data node_modules/ +.npmrc crates/fff-notify-debouncer-full/ packages/fff-bun/glob-bench-bin diff --git a/.npmrc.tpl b/.npmrc.tpl new file mode 100644 index 00000000..d24371be --- /dev/null +++ b/.npmrc.tpl @@ -0,0 +1,5 @@ +# Copy to .npmrc and provide NPM_TOKEN through the workspace secret runner. +# The rendered .npmrc is gitignored; never write the token into this file. + +@celados:registry=https://npm.celados.com +//npm.celados.com/:_authToken=${NPM_TOKEN} diff --git a/AGENTS.md b/AGENTS.md index 87bfb91b..d5cad992 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -83,12 +83,11 @@ Located at the score.rs file - `Cargo.toml` - Rust dependencies and build configuration (package name: `fff_nvim`) - `rust-toolchain.toml` - Specifies Rust nightly toolchain with required components - `Cross.toml` - Cross-compilation settings using Zig for Linux targets -- **CI/CD Workflows**: - - `.github/workflows/rust.yml` - Rust testing, formatting, and clippy checks - - `.github/workflows/release.yaml` - Automated multi-platform builds - - `.github/workflows/stylua.yaml` - Lua code formatting validation - - `.github/workflows/nix.yml` - Nix build validation -- **Cross-compilation Support**: Uses `cross` tool with Zig backend for efficient cross-compilation +- **Fork CI/CD**: `.github/workflows/release.yaml` validates the zlob-backed Node SDK, + builds macOS arm64/x64 C libraries on the Celados runner, and publishes only + `@celados/fff-node` plus those two native packages to `npm.celados.com`. +- **Cross-compilation Support**: Uses Zig for the native zlob dependency and Rust's + macOS targets for the two supported architectures. ## Development Notes diff --git a/README.md b/README.md index 61b752aa..3cf9df75 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,9 @@ Typo-resistant path and content search, frequency-ranked file access, a backgrou Powers file search in [opencode](http://github.com/anomalyco/opencode/), [nushell](https://github.com/nushell/nushell), and many more amazing projects! Originally started as [Neovim plugin](#neovim-plugin) people loved, but it turned out that plenty of AI harnesses and code editors need the same thing: accurate, fast file search as a library. That is what fff is. + +> [!IMPORTANT] +> The `celados/fff` fork ships only the private-registry Node binding used by Markd on macOS arm64 and x64. Upstream Neovim, MCP, Bun, Python, Linux, Windows, Android, Nix, spelling, and docs workflows are not part of this fork's release contract.

dmtrKovalenko%2Ffff | Trendshift

@@ -538,12 +541,21 @@ The best file search picker for neovim. Period. Faster and more intuitive querie

Node & Bun SDK

-```bash +Configure the Celados private registry with a team token, then install the package: + +```ini +# .npmrc +@celados:registry=https://npm.celados.com +//npm.celados.com/:_authToken=${NODE_AUTH_TOKEN} +``` + +```sh +export NODE_AUTH_TOKEN="" npm install @celados/fff-node -# or -bun add @celados/fff-node ``` +This fork supports macOS arm64 and x64 only. The matching `@celados/fff-bin-darwin-*` package is installed automatically. + ```ts import { FileFinder } from "@celados/fff-node"; @@ -614,7 +626,7 @@ cargo build --release -p fff-c --features zlob The output is a `cdylib` (`libfff_c.so` / `libfff_c.dylib` / `fff_c.dll`). The header lives at [`crates/fff-c/include/fff.h`](./crates/fff-c/include/fff.h). -Prebuilt binaries for every version, including every commit on main, are on the [releases page](https://github.com/celados/fff/releases). The same binaries also ship inside the `@celados/fff-bin-*` npm packages. +The fork publishes macOS arm64 and x64 dylibs inside the private `@celados/fff-bin-darwin-*` npm packages. It does not publish the upstream multi-platform GitHub Release asset matrix. ### Install diff --git a/package-lock.json b/package-lock.json index 9158746c..418438d4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "fff.nvim", + "name": "fff-issue-16", "lockfileVersion": 3, "requires": true, "packages": { @@ -931,6 +931,64 @@ "url": "https://github.com/sponsors/Borewit" } }, + "node_modules/@celados/fff-bin-android-arm64": { + "version": "0.0.0", + "cpu": ["arm64"], + "optional": true, + "os": ["android"] + }, + "node_modules/@celados/fff-bin-darwin-arm64": { + "version": "0.0.0", + "cpu": ["arm64"], + "optional": true, + "os": ["darwin"] + }, + "node_modules/@celados/fff-bin-darwin-x64": { + "version": "0.0.0", + "cpu": ["x64"], + "optional": true, + "os": ["darwin"] + }, + "node_modules/@celados/fff-bin-linux-arm64-gnu": { + "version": "0.0.0", + "cpu": ["arm64"], + "libc": ["glibc"], + "optional": true, + "os": ["linux"] + }, + "node_modules/@celados/fff-bin-linux-arm64-musl": { + "version": "0.0.0", + "cpu": ["arm64"], + "libc": ["musl"], + "optional": true, + "os": ["linux"] + }, + "node_modules/@celados/fff-bin-linux-x64-gnu": { + "version": "0.0.0", + "cpu": ["x64"], + "libc": ["glibc"], + "optional": true, + "os": ["linux"] + }, + "node_modules/@celados/fff-bin-linux-x64-musl": { + "version": "0.0.0", + "cpu": ["x64"], + "libc": ["musl"], + "optional": true, + "os": ["linux"] + }, + "node_modules/@celados/fff-bin-win32-arm64": { + "version": "0.0.0", + "cpu": ["arm64"], + "optional": true, + "os": ["win32"] + }, + "node_modules/@celados/fff-bin-win32-x64": { + "version": "0.0.0", + "cpu": ["x64"], + "optional": true, + "os": ["win32"] + }, "node_modules/@celados/fff-node": { "resolved": "packages/fff-node", "link": true @@ -4133,10 +4191,7 @@ ], "license": "MIT", "os": [ - "darwin", - "linux", - "win32", - "android" + "darwin" ], "dependencies": { "ffi-rs": "^1.0.0" @@ -4149,15 +4204,8 @@ "node": ">=18.0.0" }, "optionalDependencies": { - "@celados/fff-bin-android-arm64": "0.0.0", "@celados/fff-bin-darwin-arm64": "0.0.0", - "@celados/fff-bin-darwin-x64": "0.0.0", - "@celados/fff-bin-linux-arm64-gnu": "0.0.0", - "@celados/fff-bin-linux-arm64-musl": "0.0.0", - "@celados/fff-bin-linux-x64-gnu": "0.0.0", - "@celados/fff-bin-linux-x64-musl": "0.0.0", - "@celados/fff-bin-win32-arm64": "0.0.0", - "@celados/fff-bin-win32-x64": "0.0.0" + "@celados/fff-bin-darwin-x64": "0.0.0" } }, "packages/pi-fff": { diff --git a/packages/fff-bin-android-arm64/package.json b/packages/fff-bin-android-arm64/package.json deleted file mode 100644 index 7edbf0f4..00000000 --- a/packages/fff-bin-android-arm64/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "@celados/fff-bin-android-arm64", - "version": "0.0.0", - "description": "fff native binary for Android ARM64 (Termux)", - "os": ["android"], - "cpu": ["arm64"], - "main": "libfff_c.so", - "files": ["libfff_c.so"], - "publishConfig": { - "access": "public" - }, - "license": "MIT", - "repository": { - "type": "git", - "url": "git+https://github.com/celados/fff.git", - "directory": "packages/fff-bin-android-arm64" - } -} diff --git a/packages/fff-bin-darwin-arm64/package.json b/packages/fff-bin-darwin-arm64/package.json index d12ad5f8..48306c1b 100644 --- a/packages/fff-bin-darwin-arm64/package.json +++ b/packages/fff-bin-darwin-arm64/package.json @@ -7,6 +7,7 @@ "main": "libfff_c.dylib", "files": ["libfff_c.dylib"], "publishConfig": { + "registry": "https://npm.celados.com", "access": "public" }, "license": "MIT", diff --git a/packages/fff-bin-darwin-x64/package.json b/packages/fff-bin-darwin-x64/package.json index c47f8343..79c7f707 100644 --- a/packages/fff-bin-darwin-x64/package.json +++ b/packages/fff-bin-darwin-x64/package.json @@ -7,6 +7,7 @@ "main": "libfff_c.dylib", "files": ["libfff_c.dylib"], "publishConfig": { + "registry": "https://npm.celados.com", "access": "public" }, "license": "MIT", diff --git a/packages/fff-bin-linux-arm64-gnu/package.json b/packages/fff-bin-linux-arm64-gnu/package.json deleted file mode 100644 index 368a8ffd..00000000 --- a/packages/fff-bin-linux-arm64-gnu/package.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "@celados/fff-bin-linux-arm64-gnu", - "version": "0.0.0", - "description": "fff native binary for Linux ARM64 (glibc)", - "os": ["linux"], - "cpu": ["arm64"], - "main": "libfff_c.so", - "files": ["libfff_c.so"], - "publishConfig": { - "access": "public" - }, - "license": "MIT", - "repository": { - "type": "git", - "url": "git+https://github.com/celados/fff.git", - "directory": "packages/fff-bin-linux-arm64-gnu" - }, - "libc": ["glibc"] -} diff --git a/packages/fff-bin-linux-arm64-musl/package.json b/packages/fff-bin-linux-arm64-musl/package.json deleted file mode 100644 index de309bdf..00000000 --- a/packages/fff-bin-linux-arm64-musl/package.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "@celados/fff-bin-linux-arm64-musl", - "version": "0.0.0", - "description": "fff native binary for Linux ARM64 (musl)", - "os": ["linux"], - "cpu": ["arm64"], - "main": "libfff_c.so", - "files": ["libfff_c.so"], - "publishConfig": { - "access": "public" - }, - "license": "MIT", - "repository": { - "type": "git", - "url": "git+https://github.com/celados/fff.git", - "directory": "packages/fff-bin-linux-arm64-musl" - }, - "libc": ["musl"] -} diff --git a/packages/fff-bin-linux-x64-gnu/package.json b/packages/fff-bin-linux-x64-gnu/package.json deleted file mode 100644 index 3f30cdb3..00000000 --- a/packages/fff-bin-linux-x64-gnu/package.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "@celados/fff-bin-linux-x64-gnu", - "version": "0.0.0", - "description": "fff native binary for Linux x64 (glibc)", - "os": ["linux"], - "cpu": ["x64"], - "main": "libfff_c.so", - "files": ["libfff_c.so"], - "publishConfig": { - "access": "public" - }, - "license": "MIT", - "repository": { - "type": "git", - "url": "git+https://github.com/celados/fff.git", - "directory": "packages/fff-bin-linux-x64-gnu" - }, - "libc": ["glibc"] -} diff --git a/packages/fff-bin-linux-x64-musl/package.json b/packages/fff-bin-linux-x64-musl/package.json deleted file mode 100644 index d49ce3ab..00000000 --- a/packages/fff-bin-linux-x64-musl/package.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "@celados/fff-bin-linux-x64-musl", - "version": "0.0.0", - "description": "fff native binary for Linux x64 (musl)", - "os": ["linux"], - "cpu": ["x64"], - "main": "libfff_c.so", - "files": ["libfff_c.so"], - "publishConfig": { - "access": "public" - }, - "license": "MIT", - "repository": { - "type": "git", - "url": "git+https://github.com/celados/fff.git", - "directory": "packages/fff-bin-linux-x64-musl" - }, - "libc": ["musl"] -} diff --git a/packages/fff-bin-win32-arm64/package.json b/packages/fff-bin-win32-arm64/package.json deleted file mode 100644 index 7d373d30..00000000 --- a/packages/fff-bin-win32-arm64/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "@celados/fff-bin-win32-arm64", - "version": "0.0.0", - "description": "fff native binary for Windows ARM64", - "os": ["win32"], - "cpu": ["arm64"], - "main": "fff_c.dll", - "files": ["fff_c.dll"], - "publishConfig": { - "access": "public" - }, - "license": "MIT", - "repository": { - "type": "git", - "url": "git+https://github.com/celados/fff.git", - "directory": "packages/fff-bin-win32-arm64" - } -} diff --git a/packages/fff-bin-win32-x64/package.json b/packages/fff-bin-win32-x64/package.json deleted file mode 100644 index 1d859c1f..00000000 --- a/packages/fff-bin-win32-x64/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "@celados/fff-bin-win32-x64", - "version": "0.0.0", - "description": "fff native binary for Windows x64", - "os": ["win32"], - "cpu": ["x64"], - "main": "fff_c.dll", - "files": ["fff_c.dll"], - "publishConfig": { - "access": "public" - }, - "license": "MIT", - "repository": { - "type": "git", - "url": "git+https://github.com/celados/fff.git", - "directory": "packages/fff-bin-win32-x64" - } -} diff --git a/packages/fff-bun/src/fff-api.ts b/packages/fff-bun/src/fff-api.ts index 1aeaaf71..7fca8437 100644 --- a/packages/fff-bun/src/fff-api.ts +++ b/packages/fff-bun/src/fff-api.ts @@ -6,7 +6,7 @@ /** * The shared public API surface for the fff file finder, implemented identically - * by the Node.js and Bun frontends. + * by the Node and Bun frontends. * * This file is the single source of truth for every type, helper, and the * `FileFinderApi` interface that crosses the package boundary. It is copied @@ -553,7 +553,7 @@ export interface MultiGrepOptions { /** * The shared instance surface implemented by `FileFinder` in both - * the Node.js and Bun frontends. + * the Node and Bun frontends. * * Both packages must implement this identically. Only instance members belong * here. Static helpers (`create`, `isAvailable`, `ensureLoaded`, diff --git a/packages/fff-node/README.md b/packages/fff-node/README.md index 2e52211e..003038d0 100644 --- a/packages/fff-node/README.md +++ b/packages/fff-node/README.md @@ -8,11 +8,20 @@ Faster than ripgrep & fzf on any workflow that runs more than once per process. ## Installation -```bash +Configure the Celados private registry with a team token: + +```ini +# .npmrc +@celados:registry=https://npm.celados.com +//npm.celados.com/:_authToken=${NODE_AUTH_TOKEN} +``` + +```sh +export NODE_AUTH_TOKEN="" npm install @celados/fff-node ``` -The correct native binary for your platform is installed automatically via platform-specific `@celados/fff-bin-*` packages. +The matching macOS native binary is installed automatically via a platform package. ### Supported Platforms @@ -20,14 +29,8 @@ The correct native binary for your platform is installed automatically via platf | -------- | --------------------- | ----------------------------------- | | macOS | ARM64 (Apple Silicon) | `@celados/fff-bin-darwin-arm64` | | macOS | x64 (Intel) | `@celados/fff-bin-darwin-x64` | -| Linux | x64 (glibc) | `@celados/fff-bin-linux-x64-gnu` | -| Linux | ARM64 (glibc) | `@celados/fff-bin-linux-arm64-gnu` | -| Linux | x64 (musl) | `@celados/fff-bin-linux-x64-musl` | -| Linux | ARM64 (musl) | `@celados/fff-bin-linux-arm64-musl` | -| Windows | x64 | `@celados/fff-bin-win32-x64` | -| Windows | ARM64 | `@celados/fff-bin-win32-arm64` | - -The package never downloads executable code at install or runtime. Unsupported platforms fail at native-library resolution. + +The package never downloads executable code at install or runtime. This fork fails closed on non-macOS platforms. ## Quick Start diff --git a/packages/fff-node/package.json b/packages/fff-node/package.json index cadb99ea..5505104b 100644 --- a/packages/fff-node/package.json +++ b/packages/fff-node/package.json @@ -24,10 +24,7 @@ "node": ">=18.0.0" }, "os": [ - "darwin", - "linux", - "win32", - "android" + "darwin" ], "cpu": [ "x64", @@ -51,6 +48,7 @@ "author": "Dmitry Kovalenko", "license": "MIT", "publishConfig": { + "registry": "https://npm.celados.com", "access": "public" }, "bugs": { @@ -62,14 +60,7 @@ }, "optionalDependencies": { "@celados/fff-bin-darwin-arm64": "0.0.0", - "@celados/fff-bin-darwin-x64": "0.0.0", - "@celados/fff-bin-linux-x64-gnu": "0.0.0", - "@celados/fff-bin-linux-arm64-gnu": "0.0.0", - "@celados/fff-bin-linux-x64-musl": "0.0.0", - "@celados/fff-bin-linux-arm64-musl": "0.0.0", - "@celados/fff-bin-win32-x64": "0.0.0", - "@celados/fff-bin-win32-arm64": "0.0.0", - "@celados/fff-bin-android-arm64": "0.0.0" + "@celados/fff-bin-darwin-x64": "0.0.0" }, "devDependencies": { "typescript": "^5.0.0", diff --git a/packages/fff-node/src/fff-api.ts b/packages/fff-node/src/fff-api.ts index 1aeaaf71..7fca8437 100644 --- a/packages/fff-node/src/fff-api.ts +++ b/packages/fff-node/src/fff-api.ts @@ -6,7 +6,7 @@ /** * The shared public API surface for the fff file finder, implemented identically - * by the Node.js and Bun frontends. + * by the Node and Bun frontends. * * This file is the single source of truth for every type, helper, and the * `FileFinderApi` interface that crosses the package boundary. It is copied @@ -553,7 +553,7 @@ export interface MultiGrepOptions { /** * The shared instance surface implemented by `FileFinder` in both - * the Node.js and Bun frontends. + * the Node and Bun frontends. * * Both packages must implement this identically. Only instance members belong * here. Static helpers (`create`, `isAvailable`, `ensureLoaded`, diff --git a/packages/fff-node/src/platform.ts b/packages/fff-node/src/platform.ts index 457130e3..765b2f77 100644 --- a/packages/fff-node/src/platform.ts +++ b/packages/fff-node/src/platform.ts @@ -2,52 +2,17 @@ * Platform detection utilities for resolving the correct native package */ -import { execSync } from "node:child_process"; - /** - * Get the platform triple (e.g., "x86_64-unknown-linux-gnu") + * Get the supported macOS platform triple (e.g., "aarch64-apple-darwin") */ export function getTriple(): string { const platform = process.platform; const arch = process.arch; - let osName: string; - if (platform === "darwin") { - osName = "apple-darwin"; - } else if (platform === "android") { - osName = "linux-android"; - } else if (platform === "linux") { - osName = detectLinuxLibc(); - } else if (platform === "win32") { - osName = "pc-windows-msvc"; - } else { - throw new Error(`Unsupported platform: ${platform}`); - } + if (platform !== "darwin") throw new Error(`Unsupported platform: ${platform}`); const archName = normalizeArch(arch); - return `${archName}-${osName}`; -} - -/** - * Detect whether we're on musl or glibc Linux - */ -function detectLinuxLibc(): string { - let output = ""; - try { - output = execSync("ldd --version 2>&1", { - encoding: "utf-8", - timeout: 5000, - }); - } catch (e: unknown) { - const err = e as { stdout?: string | Buffer; stderr?: string | Buffer }; - output = String(err?.stdout ?? "") + String(err?.stderr ?? ""); - } - - // ldd on musl can produce stdout with musl either with exit code 1 or 0 - if (output.toLowerCase().includes("musl")) { - return "unknown-linux-musl"; - } - return "unknown-linux-gnu"; + return `${archName}-apple-darwin`; } /** @@ -60,8 +25,6 @@ function normalizeArch(arch: string): string { return "x86_64"; case "arm64": return "aarch64"; - case "arm": - return "arm"; default: throw new Error(`Unsupported architecture: ${arch}`); } @@ -70,22 +33,17 @@ function normalizeArch(arch: string): string { /** * Get the library file extension for the current platform */ -export function getLibExtension(): "dylib" | "so" | "dll" { - switch (process.platform) { - case "darwin": - return "dylib"; - case "win32": - return "dll"; - default: - return "so"; - } +export function getLibExtension(): "dylib" { + if (process.platform !== "darwin") + throw new Error(`Unsupported platform: ${process.platform}`); + return "dylib"; } /** - * Get the library filename prefix (empty on Windows) + * Get the macOS library filename prefix */ export function getLibPrefix(): string { - return process.platform === "win32" ? "" : "lib"; + return "lib"; } /** @@ -100,18 +58,11 @@ export function getLibFilename(): string { /** * Map from Rust target triple to npm platform package name. * The @celados/fff-bin-* packages contain the pre-built libfff_c - * shared library and are runtime-agnostic (used by both Bun and Node). + * shared library consumed by the Markd Node binding. */ const TRIPLE_TO_NPM_PACKAGE: Record = { "aarch64-apple-darwin": "@celados/fff-bin-darwin-arm64", "x86_64-apple-darwin": "@celados/fff-bin-darwin-x64", - "x86_64-unknown-linux-gnu": "@celados/fff-bin-linux-x64-gnu", - "aarch64-unknown-linux-gnu": "@celados/fff-bin-linux-arm64-gnu", - "x86_64-unknown-linux-musl": "@celados/fff-bin-linux-x64-musl", - "aarch64-unknown-linux-musl": "@celados/fff-bin-linux-arm64-musl", - "x86_64-pc-windows-msvc": "@celados/fff-bin-win32-x64", - "aarch64-pc-windows-msvc": "@celados/fff-bin-win32-arm64", - "aarch64-linux-android": "@celados/fff-bin-android-arm64", }; /**