Skip to main content

lean_ctx/core/editor_registry/
paths.rs

1use std::path::{Path, PathBuf};
2
3pub fn zed_settings_path(home: &std::path::Path) -> PathBuf {
4    if cfg!(target_os = "macos") {
5        home.join("Library/Application Support/Zed/settings.json")
6    } else {
7        home.join(".config/zed/settings.json")
8    }
9}
10
11pub fn zed_config_dir(home: &std::path::Path) -> PathBuf {
12    if cfg!(target_os = "macos") {
13        home.join("Library/Application Support/Zed")
14    } else {
15        home.join(".config/zed")
16    }
17}
18
19pub fn vscode_mcp_path() -> PathBuf {
20    if let Some(home) = dirs::home_dir() {
21        #[cfg(target_os = "macos")]
22        {
23            return home.join("Library/Application Support/Code/User/mcp.json");
24        }
25        #[cfg(target_os = "linux")]
26        {
27            return resolve_vscode_global_storage(&home, "User/mcp.json");
28        }
29        #[cfg(target_os = "windows")]
30        {
31            if let Ok(appdata) = std::env::var("APPDATA") {
32                return PathBuf::from(appdata).join("Code/User/mcp.json");
33            }
34        }
35        #[allow(unreachable_code)]
36        home.join(".config/Code/User/mcp.json")
37    } else {
38        PathBuf::from("/nonexistent")
39    }
40}
41
42/// VS Code **Insiders** user-scope MCP config. Insiders keeps a fully
43/// separate profile dir (`Code - Insiders`), so a server registered in
44/// stable's `Code/User/mcp.json` simply does not exist there — an Insiders
45/// user then sees an empty `MCP: Open User Configuration` even after
46/// `lean-ctx setup` succeeded (GH #694). On Linux `vscode_mcp_path()` can
47/// already resolve to Insiders via the shared fallback chain when it is the
48/// only install; this path is the *dedicated* Insiders location for the
49/// distinct-target case.
50pub fn vscode_insiders_mcp_path() -> PathBuf {
51    let Some(home) = dirs::home_dir() else {
52        return PathBuf::from("/nonexistent");
53    };
54    #[cfg(target_os = "macos")]
55    {
56        home.join("Library/Application Support/Code - Insiders/User/mcp.json")
57    }
58    #[cfg(target_os = "windows")]
59    {
60        if let Ok(appdata) = std::env::var("APPDATA") {
61            return PathBuf::from(appdata).join("Code - Insiders/User/mcp.json");
62        }
63        home.join("AppData/Roaming/Code - Insiders/User/mcp.json")
64    }
65    #[cfg(not(any(target_os = "macos", target_os = "windows")))]
66    {
67        home.join(".config/Code - Insiders/User/mcp.json")
68    }
69}
70
71pub fn qoder_mcp_path(home: &Path) -> PathBuf {
72    #[cfg(target_os = "windows")]
73    {
74        if let Ok(appdata) = std::env::var("APPDATA") {
75            return PathBuf::from(appdata)
76                .join("Qoder")
77                .join("SharedClientCache")
78                .join("mcp.json");
79        }
80    }
81    home.join(".qoder").join("mcp.json")
82}
83
84#[cfg(target_os = "macos")]
85pub fn qoder_mcp_paths(home: &Path) -> Vec<PathBuf> {
86    let mut paths = vec![qoder_mcp_path(home)];
87    paths.push(home.join("Library/Application Support/Qoder/User/mcp.json"));
88    paths.push(home.join("Library/Application Support/Qoder/SharedClientCache/mcp.json"));
89    paths
90}
91
92#[cfg(not(target_os = "macos"))]
93pub fn qoder_mcp_paths(home: &Path) -> Vec<PathBuf> {
94    vec![qoder_mcp_path(home)]
95}
96
97#[allow(unreachable_code)]
98pub fn cline_mcp_path() -> PathBuf {
99    #[cfg(target_os = "windows")]
100    {
101        if let Ok(appdata) = std::env::var("APPDATA") {
102            return PathBuf::from(appdata).join(
103                "Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json",
104            );
105        }
106        return PathBuf::from("/nonexistent");
107    }
108
109    let Some(home) = dirs::home_dir() else {
110        return PathBuf::from("/nonexistent");
111    };
112    #[cfg(target_os = "macos")]
113    {
114        return home.join("Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json");
115    }
116    #[cfg(target_os = "linux")]
117    {
118        let suffix = "User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json";
119        return resolve_vscode_global_storage(&home, suffix);
120    }
121    PathBuf::from("/nonexistent")
122}
123
124#[allow(unreachable_code)]
125pub fn roo_mcp_path() -> PathBuf {
126    #[cfg(target_os = "windows")]
127    {
128        if let Ok(appdata) = std::env::var("APPDATA") {
129            return PathBuf::from(appdata)
130                .join("Code/User/globalStorage/rooveterinaryinc.roo-cline/settings/cline_mcp_settings.json");
131        }
132        return PathBuf::from("/nonexistent");
133    }
134
135    let Some(home) = dirs::home_dir() else {
136        return PathBuf::from("/nonexistent");
137    };
138    #[cfg(target_os = "macos")]
139    {
140        return home.join("Library/Application Support/Code/User/globalStorage/rooveterinaryinc.roo-cline/settings/cline_mcp_settings.json");
141    }
142    #[cfg(target_os = "linux")]
143    {
144        let suffix =
145            "User/globalStorage/rooveterinaryinc.roo-cline/settings/cline_mcp_settings.json";
146        return resolve_vscode_global_storage(&home, suffix);
147    }
148    PathBuf::from("/nonexistent")
149}
150
151/// Resolves the correct VS Code-family base directory on Linux.
152/// Checks VSCodium, Code - OSS, Code, Code - Insiders, then the dev-container
153/// server dir (in that order) — returns the first existing path, falling back
154/// to the standard `Code` path for fresh installs. Most-specific first: a
155/// VSCodium user with a leftover `~/.config/Code` (one accidental Code launch
156/// is enough) must keep resolving to VSCodium. Dev containers use
157/// `.vscode-server/data` instead of `.config`, so we check both.
158#[cfg(target_os = "linux")]
159fn resolve_vscode_global_storage(home: &Path, suffix: &str) -> PathBuf {
160    const CANDIDATES: &[&str] = &[
161        ".config/VSCodium",
162        ".config/Code - OSS",
163        ".config/Code",
164        ".config/Code - Insiders",
165        ".vscode-server/data",
166    ];
167    for base in CANDIDATES {
168        let path = home.join(base);
169        if path.exists() {
170            return path.join(suffix);
171        }
172    }
173    home.join(".config/Code").join(suffix)
174}
175
176pub fn qoder_settings_path(home: &Path) -> PathBuf {
177    #[cfg(target_os = "windows")]
178    {
179        if let Ok(appdata) = std::env::var("APPDATA") {
180            return PathBuf::from(appdata)
181                .join("Qoder")
182                .join("SharedClientCache")
183                .join("mcp.json");
184        }
185    }
186    home.join(".qoder/mcp.json")
187}
188
189pub fn qoder_all_mcp_paths(home: &Path) -> Vec<PathBuf> {
190    let paths = vec![qoder_settings_path(home)];
191    #[cfg(target_os = "macos")]
192    let paths = {
193        let mut paths = paths;
194        paths.push(home.join("Library/Application Support/Qoder/User/mcp.json"));
195        paths.push(home.join("Library/Application Support/Qoder/SharedClientCache/mcp.json"));
196        paths
197    };
198    paths
199}
200
201pub fn qoderwork_mcp_path(home: &Path) -> PathBuf {
202    home.join(".qoderwork/mcp.json")
203}
204
205/// Qoder CLI stores user-scoped MCP servers in the shared Qoder settings file.
206/// This is intentionally separate from Qoder IDE's `mcp.json` locations: the
207/// two applications use the same `.qoder` state directory but do not consume
208/// the same MCP configuration file.
209pub fn qodercli_settings_path(home: &Path) -> PathBuf {
210    home.join(".qoder/settings.json")
211}
212
213pub fn claude_mcp_json_path(home: &Path) -> PathBuf {
214    if let Ok(dir) = std::env::var("CLAUDE_CONFIG_DIR") {
215        let dir = dir.trim();
216        if !dir.is_empty() {
217            return PathBuf::from(dir).join(".claude.json");
218        }
219    }
220    home.join(".claude.json")
221}
222
223pub fn claude_state_dir(home: &Path) -> PathBuf {
224    if let Ok(dir) = std::env::var("CLAUDE_CONFIG_DIR") {
225        let dir = dir.trim();
226        if !dir.is_empty() {
227            return PathBuf::from(dir);
228        }
229    }
230    home.join(".claude")
231}
232
233pub fn claude_rules_dir(home: &Path) -> PathBuf {
234    claude_state_dir(home).join("rules")
235}
236
237pub fn codebuddy_mcp_json_path(home: &Path) -> PathBuf {
238    codebuddy_state_dir(home).join("mcp.json")
239}
240
241pub fn codebuddy_state_dir(home: &Path) -> PathBuf {
242    if let Ok(dir) = std::env::var("CODEBUDDY_CONFIG_DIR") {
243        let dir = dir.trim();
244        if !dir.is_empty() {
245            return PathBuf::from(dir);
246        }
247    }
248    home.join(".codebuddy")
249}
250
251pub fn codebuddy_rules_dir(home: &Path) -> PathBuf {
252    codebuddy_state_dir(home).join("rules")
253}
254
255pub fn augment_cli_settings_path(home: &Path) -> PathBuf {
256    home.join(".augment/settings.json")
257}
258
259/// MCP server list for the Augment VS Code extension.
260///
261/// The extension persists registered MCP servers as a top-level JSON array in
262/// its globalStorage directory. Confirmed empirically against
263/// `augment.vscode-augment` build shipped on 2026-05-21 (see PR description).
264///
265/// On Windows the User dir lives under `%APPDATA%/Code` rather than the
266/// user's home, so we honour that when the env var is set; we fall back to
267/// the home-relative path for tests and unusual setups.
268pub fn augment_vscode_mcp_path(home: &Path) -> PathBuf {
269    const TAIL: &str = "globalStorage/augment.vscode-augment/augment-global-state/mcpServers.json";
270
271    #[cfg(target_os = "macos")]
272    {
273        return home
274            .join("Library/Application Support/Code/User")
275            .join(TAIL);
276    }
277    #[cfg(target_os = "linux")]
278    {
279        for path in [
280            ".config/Code/User",
281            ".config/Code - Insiders/User",
282            ".vscode-server/data/User",
283        ] {
284            let full_path = home.join(path).join(TAIL);
285            if full_path.exists() {
286                return full_path;
287            }
288        }
289        // Fall back to primary path if none exist
290        return home.join(".config/Code/User").join(TAIL);
291    }
292    #[cfg(target_os = "windows")]
293    {
294        if let Ok(appdata) = std::env::var("APPDATA") {
295            return PathBuf::from(appdata).join("Code/User").join(TAIL);
296        }
297    }
298    #[allow(unreachable_code)]
299    home.join(".config/Code/User").join(TAIL)
300}
301
302pub fn vibe_config_path(home: &Path) -> PathBuf {
303    home.join(".vibe/config.toml")
304}
305
306pub fn detect_vibe_path(home: &Path) -> PathBuf {
307    let vibe_dir = home.join(".vibe");
308    if vibe_dir.exists() {
309        return vibe_dir;
310    }
311    PathBuf::from("/nonexistent")
312}
313
314#[cfg(test)]
315mod augment_tests {
316    use super::*;
317
318    #[test]
319    fn augment_cli_settings_path_is_under_dot_augment() {
320        let home = Path::new("/home/tester");
321        assert_eq!(
322            augment_cli_settings_path(home),
323            home.join(".augment").join("settings.json")
324        );
325    }
326
327    #[test]
328    #[cfg(target_os = "linux")]
329    fn augment_vscode_mcp_path_uses_linux_globalstorage() {
330        let home = Path::new("/home/tester");
331        assert_eq!(
332            augment_vscode_mcp_path(home),
333            home.join(".config/Code/User/globalStorage/augment.vscode-augment/augment-global-state/mcpServers.json")
334        );
335    }
336
337    #[test]
338    #[cfg(target_os = "macos")]
339    fn augment_vscode_mcp_path_uses_macos_application_support() {
340        let home = Path::new("/Users/tester");
341        assert_eq!(
342            augment_vscode_mcp_path(home),
343            home.join("Library/Application Support/Code/User/globalStorage/augment.vscode-augment/augment-global-state/mcpServers.json")
344        );
345    }
346
347    /// On Windows we honour `%APPDATA%` when set, falling back to a
348    /// home-relative path only when it is missing. We can't reliably mutate
349    /// process-wide env vars in a parallel test runner, so this test only
350    /// asserts the invariant tail (which is platform-agnostic) and that the
351    /// final segment is the expected file name. Both branches share that tail.
352    #[test]
353    #[cfg(target_os = "windows")]
354    fn augment_vscode_mcp_path_ends_with_globalstorage_tail() {
355        let home = Path::new("C:/Users/tester");
356        let path = augment_vscode_mcp_path(home);
357        let s = path.to_string_lossy().replace('\\', "/");
358        assert!(
359            s.ends_with(
360                "Code/User/globalStorage/augment.vscode-augment/augment-global-state/mcpServers.json"
361            ),
362            "unexpected windows path: {s}"
363        );
364    }
365}
366
367#[cfg(test)]
368mod qodercli_tests {
369    use super::*;
370
371    #[test]
372    fn qodercli_settings_path_uses_shared_qoder_settings_file() {
373        let home = Path::new("/home/tester");
374        assert_eq!(
375            qodercli_settings_path(home),
376            home.join(".qoder/settings.json")
377        );
378    }
379
380    #[test]
381    fn qodercli_settings_path_is_distinct_from_qoder_ide_mcp_path() {
382        let home = Path::new("/home/tester");
383        assert_ne!(qodercli_settings_path(home), qoder_mcp_path(home));
384    }
385
386    #[test]
387    fn qodercli_settings_path_is_distinct_from_qoderwork_path() {
388        let home = Path::new("/home/tester");
389        assert_ne!(qodercli_settings_path(home), qoderwork_mcp_path(home));
390    }
391}
392
393#[cfg(all(test, target_os = "macos"))]
394mod tests {
395    use super::*;
396
397    #[test]
398    #[cfg(target_os = "macos")]
399    fn qoder_mcp_paths_include_macos_user_and_shared_cache_locations() {
400        let home = Path::new("/Users/tester");
401        let paths = qoder_mcp_paths(home);
402
403        assert_eq!(
404            paths,
405            vec![
406                home.join(".qoder/mcp.json"),
407                home.join("Library/Application Support/Qoder/User/mcp.json"),
408                home.join("Library/Application Support/Qoder/SharedClientCache/mcp.json"),
409            ]
410        );
411    }
412}