feat(memory-cli): cross-platform IDE detection for memory init#1251
Open
bplatz wants to merge 3 commits into
Open
feat(memory-cli): cross-platform IDE detection for memory init#1251bplatz wants to merge 3 commits into
memory init#1251bplatz wants to merge 3 commits into
Conversation
- Factor filesystem/env probing behind a `DetectionEnv` trait so detection logic is unit-testable (`RealEnv` + `FakeEnv`). Project-root walk-up is derived from the same probes. - Detect each IDE via four signal classes (any one is enough): binary on `PATH`, macOS app bundle (`/Applications` *and* `~/Applications`), home-dir marker, and `dirs::config_dir()` marker (Linux `~/.config/<App>`, macOS `~/Library/Application Support/<App>`, Windows `%APPDATA%\<App>`). Adds `claude` as a first-class PATH probe. - Fix Claude Code project-root handling: `claude_code_already_configured` keys off `project_root` (with canonical + non-canonical + cwd fallbacks), `install_claude_code` spawns `claude mcp add` from `project_root`, and `CLAUDE.md` is written at `<project_root>/CLAUDE.md` instead of cwd. - Refuse to clobber unsafe configs in Cursor/VS Code/Windsurf/Zed installers. `load_config` now distinguishes missing/empty (default), parsed (merge), and unsafe (refuse). Unsafe covers JSONC, invalid UTF-8, permission errors, and any non-NotFound IO error. - Distinguish skipped from installed in `run_init`: new `InstallOutcome` enum so the refused-overwrite and missing-`claude`-binary paths no longer get counted as successful installs. Prints a separate "Skipped N tools" line when applicable. - Windows `PATHEXT` fallback: if unset/empty, try `.COM/.EXE/.BAT/.CMD` plus the bare name. - macOS app probe early-returns when off macOS so `~/Applications` is never probed on Linux/Windows. - 21 unit tests covering project-root walk-up, per-IDE presence via each signal class, Claude already-configured from a nested cwd, the `load_config` matrix (including invalid UTF-8 and JSONC), and the macOS-only app probe guard. - Docs updated: `docs/memory/cli/init.md`, `docs/memory/reference/ide-matrix.md`, `docs/cli/memory.md`, `docs/memory/cli/mcp-install.md` describe the new detection signals, the "running from a subdir" behavior, and the refuse-to-overwrite policy.
memory.rs had grown to 1774 lines mixing memory CRUD with IDE detection,
MCP config installs, filesystem/env abstraction, and ~700 lines of
detection tests. Split the IDE concern out into a sibling module and
collapse the per-IDE duplication along the way.
- Move AiTool, DetectionEnv/RealEnv, detection probes, install_*,
LoadedConfig, InstallOutcome, run_mcp_install, and all detection tests
into fluree-db-cli/src/commands/memory/ide.rs. memory.rs now declares
`mod ide;`, delegates Phase 2 of init via `ide::run_mcp_phase(yes)`,
and routes McpInstall to `ide::run_mcp_install`.
- Make IDE detection data-driven via `IdeDetectionSpec` (binary, macOS
app, home markers, config markers). One `is_ide_present(env, spec)`
replaces five near-identical `is_*_present` functions; `path_exists`
matches both files and dirs, collapsing Claude's `.claude` (dir) vs
`.claude.json` (file) into one marker list. Adding another IDE is now
a table edit. Already-configured probes are similarly table-driven via
`already_configured_probes(tool, env)`.
- Extract `install_json_mcp_config(JsonMcpInstall { config_path, top_key,
default, entry })` — the load/merge/refuse-on-unsafe/write shape used
by Cursor / VS Code / Windsurf / Zed. Per-IDE installers shrink to a
few lines each.
- Extract `install_rules_file(dir)` for the bundled fluree_rules.md.
Best-effort: prints a warning on failure instead of propagating, so a
rules-file IO error no longer masks a successful MCP config install in
the per-tool summary.
- Add `build_synced_store(dirs)` to memory.rs, replacing 7 copy-pasted
`build_store + ensure_synced` pairs in the CRUD subcommands. run_init
keeps a plain build_store since it creates an empty store before sync.
- Rename `InstallOutcome::Skipped` → `ManualRequired` and
`LoadedConfig::Default` → `MissingOrEmpty` for more precise intent.
The init summary now reports "N tools need manual setup" instead of
"Skipped N tools".
- Tighten load_config: a parsed JSON value that isn't an object now
returns Unsafe, since merge_mcp_entry can't merge into arrays /
scalars / null. Previously a top-level `[]` would have been written
back as-is with InstallOutcome::Installed reported to the user.
Tests: 28 unit tests in commands::memory::ide::tests (up from 21),
covering project-root walk-up, per-IDE presence via each signal class,
Claude already-configured under nested cwd, the load_config matrix
(missing, empty, JSONC, invalid UTF-8, non-object root, valid),
install_json_mcp_config (happy + refuse + non-object), and the
best-effort install_rules_file failure path. CLI test suite: 73/73.
memory.rs is now 498 lines focused on dispatch + memory CRUD; ide.rs is
1305 lines and self-contained.
The non-object root check in load_config caught files like `[]` or
`"foo"`, but a file shaped like `{"mcpServers": []}` still slipped
through: the root is an object (so load_config returned Parsed), and
merge_mcp_entry's second branch silently replaced the array with our
default object. The user's pre-existing entry under that key would be
lost.
install_json_mcp_config now inspects `config[top_key]` after load and
returns ManualRequired if the value exists and is not an object,
mirroring the don't-clobber-user-config rule we already apply at the
root level. Behavior is unchanged for the common cases — missing key
(create), existing object (merge while preserving siblings).
Tests:
- install_json_mcp_config_refuses_non_object_top_key — file
`{"mcpServers":[]}` returns ManualRequired and is left untouched.
- install_json_mcp_config_merges_into_existing_object_top_key —
pre-existing sibling entry under mcpServers is preserved while
fluree-memory is inserted.
28 → 30 IDE tests; CLI suite still 73/73; workspace clippy clean.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
fluree memory initused to detect installed IDEs primarily via macOS-specific/Applications/<App>.appprobes, with fallbacks limited to~/.<app>markers. That left Linux and Windows in a "must have launched the IDE once" hole, and made Claude Code's detection (no.appbundle) weaker than the others on every OS.This PR makes detection multi-signal and cross-platform, hardens the IDE config installers against clobbering, and corrects Claude Code's project-root handling so running
fluree memory initfrom a subdirectory behaves identically to running it from the repo root.What changed
Detection refactor (
fluree-db-cli/src/commands/memory.rs)DetectionEnvtrait. Production code usesRealEnv(delegates tostd::env,dirs,std::fs); tests useFakeEnv.PATH(claude,code,cursor,windsurf,zed).claudeis now a first-class signal — previously Claude Code only matched~/.claude/./Applicationsor~/Applications.~/.claude,~/.cursor,~/.vscode,~/.codeium/windsurf,~/.zed,~/.config/zed,~/.claude.json).dirs::config_dir()—~/.config/Codeon Linux,~/Library/Application Support/Cursoron macOS,%APPDATA%\Windsurfon Windows, etc. This is what closes the "launched once" gap on Linux/Windows.Claude Code project-root handling
claude_code_already_configuredused the canonicalized cwd as the lookup key — wrong wheninitis run from a subdirectory, since Claude stores itsprojectsmap keyed by the repo root. Now:canonical(project_root) → project_root → canonical(cwd)in order.install_claude_codespawnsclaude mcp addwith.current_dir(project_root).CLAUDE.mdappend target is<project_root>/CLAUDE.md, not the cwd-relativePath::new("CLAUDE.md").Refuse-to-clobber for IDE configs
read_or_default()silently replaced any unreadable/unparsable JSON with a fresh default — fine for missing files, dangerous for JSONC, hand-edits, invalid UTF-8, or permission errors. Replaced withload_config()returning:Default— file missing (ErrorKind::NotFound) or empty.Parsed— valid JSON.Unsafe { reason }— any other state. Installer prints a snippet to paste manually and returnsInstallOutcome::Skipped.This now applies to Cursor, VS Code, Windsurf, and Zed (Zed was already safe; the others now match).
Skipped vs installed accounting
Previously,
install_tool() -> Ok(())covered both "actually installed" and "refused to overwrite, printed snippet." So the user would see "Configured N tools" with N including refused targets. Fixed with a newInstallOutcome { Installed, Skipped }enum;run_initonly countsInstalledand now prints a separate"Skipped N tools (see manual snippet above)"line when needed. Claude Code's failedclaude mcp add(binary missing, non-zero exit) also reportsSkipped.Windows
PATHEXTfallbackThe PATH probe previously checked nothing if
PATHEXTwas unset or empty. Now falls back to.COM / .EXE / .BAT / .CMDand also tries the bare name (so an extensionless file still matches).macOS app-probe tidying
macos_app_installednow early-returns whensystem_applications_dirisNone, so~/Applicationsis no longer probed on Linux/Windows (matches the docs).canonicalizemoved behindDetectionEnvclaude_code_already_configuredno longer reaches intostd::fs::canonicalizedirectly — exposes the test seam fully.Tests
21 unit tests in
commands::memory::tests:project_root_walks_up_to_git_marker,project_root_falls_back_to_cwd_when_no_markerclaude_present_via_path,claude_present_via_home_marker,claude_absent_when_no_signal,vscode_present_via_config_dir,cursor_present_via_config_dir,windsurf_present_via_config_dirmacos_app_probe_checks_both_system_and_user_applications,macos_app_probe_is_noop_when_system_dir_unsetpath_extensions_unix_returns_bare_name_onlyclaude_already_configured_matches_project_root_from_nested_cwd,claude_already_configured_returns_false_when_other_projectdetect_returns_empty_on_empty_env,detect_finds_claude_via_path_only,detect_flags_already_configured_via_project_mcp_jsonload_configmatrix: missing → Default, empty → Default, JSONC → Unsafe, invalid UTF-8 → Unsafe, valid JSON → ParsedDocs
docs/memory/cli/init.md— added Detection signals section + manual-fallback pointer.docs/memory/reference/ide-matrix.md— replaced macOS-centric wording with the four signal classes; added "running from a subdir" note.docs/cli/memory.md— added Detection signals subsection underfluree memory init.docs/memory/cli/mcp-install.md— documented the refuse-to-overwrite policy for non-Zed targets.