Skip to main content

DevelopmentNodeEnvironment_MicrosoftVSCodeDependency_22NodeVersion_Bundle_Clean_Debug_ElectronProfile_EsbuildCompiler_Mountain/Binary/Extension/
ScanPathConfigure.rs

1//! # Extension Scan Path Configure Module
2//!
3//! Configures extension scan paths from the executable directory.
4
5use std::path::PathBuf;
6
7use crate::{
8	ApplicationState::State::ApplicationState::{ApplicationState, MapLockError},
9	dev_log,
10};
11
12/// Configures extension scan paths by resolving paths from the executable
13/// directory.
14///
15/// # Arguments
16///
17/// * `AppState` - The application state containing ExtensionScanPaths
18///
19/// # Returns
20///
21/// A `Result` indicating success or failure.
22///
23/// # Scan Path Configuration
24///
25/// This function adds the following default scan paths:
26/// - `../Resources/extensions` - Bundled extensions in app resources directory
27/// - `extensions` - Local extensions directory relative to executable
28///
29/// # Errors
30///
31/// Returns an error if ExtensionScanPaths mutex lock fails.
32pub fn ScanPathConfigure(AppState:&std::sync::Arc<ApplicationState>) -> Result<Vec<PathBuf>, String> {
33	dev_log!("extensions", "[Extensions] [ScanPaths] Locking ExtensionScanPaths...");
34
35	let mut ScanPathsGuard = AppState
36		.Extension
37		.Registry
38		.ExtensionScanPaths
39		.lock()
40		.map_err(MapLockError)
41		.map_err(|e| format!("Failed to lock ExtensionScanPaths: {}", e))?;
42
43	// Skip all built-in extensions when either the legacy
44	// `Skip` or the `.env.Land.Extensions` flag
45	// `Skip` is set. Both accepted so kernel /
46	// minimal profiles and the skill-file env stay in sync. User scan path
47	// still runs so VSIX-installed extensions remain visible.
48	let SkipBuiltins = matches!(std::env::var("Skip").as_deref(), Ok("1") | Ok("true"))
49		|| matches!(std::env::var("Skip").as_deref(), Ok("1") | Ok("true"));
50
51	if SkipBuiltins {
52		dev_log!(
53			"extensions",
54			"[Extensions] [ScanPaths] Skip=true - skipping all built-in paths, keeping user path"
55		);
56	} else {
57		dev_log!("extensions", "[Extensions] [ScanPaths] Adding default scan paths...");
58	}
59
60	// `Ship` takes precedence over the executable-
61	// relative probing chain. Useful for CI builds where the bundle layout
62	// differs from both the `.app` convention and the repo layout.
63	if !SkipBuiltins {
64		if let Ok(Override) = std::env::var("Ship") {
65			let OverridePath = ExpandUserPath(&Override);
66
67			if OverridePath.exists() {
68				dev_log!("extensions", "[Extensions] [ScanPaths] + {} (Ship)", OverridePath.display());
69
70				ScanPathsGuard.push(OverridePath);
71			} else {
72				dev_log!(
73					"extensions",
74					"warn: [Extensions] [ScanPaths] Ship={} does not exist; ignoring",
75					Override
76				);
77			}
78		}
79	}
80
81	// Resolve paths from executable directory
82	if !SkipBuiltins {
83		if let Ok(ExecutableDirectory) = std::env::current_exe() {
84			if let Some(Parent) = ExecutableDirectory.parent() {
85				// Resolve `..` segments lexically (no filesystem access needed)
86				// so log output is always clean even for non-existent paths.
87				// Falls back to `std::fs::canonicalize` for existing paths to
88				// also resolve symlinks.
89				let Normalize = |P:std::path::PathBuf| -> std::path::PathBuf {
90					if P.exists() {
91						return P.canonicalize().unwrap_or(P);
92					}
93
94					let mut Out:Vec<std::path::Component> = Vec::new();
95
96					for C in P.components() {
97						match C {
98							std::path::Component::ParentDir => {
99								Out.pop();
100							},
101
102							_ => Out.push(C),
103						}
104					}
105
106					Out.iter().collect()
107				};
108
109				// New canonical path: ../Resources/Static/Application/extensions.
110				// Extensions land here when tauri.conf.json bundle.resources maps
111				// them to "Static/Application/extensions" (single source layout).
112				let StaticAppExtPath = Parent.join("../Resources/Static/Application/extensions");
113
114				if StaticAppExtPath.exists() {
115					let StaticAppExtPath = Normalize(StaticAppExtPath);
116
117					dev_log!(
118						"extensions",
119						"[Extensions] [ScanPaths] + {} (Static/Application canonical)",
120						StaticAppExtPath.display()
121					);
122
123					ScanPathsGuard.push(StaticAppExtPath);
124				}
125
126				// Legacy flat path: ../Resources/extensions (kept for backward
127				// compat while the tauri.conf.json resources remap takes effect).
128				let ResourcesPath = Normalize(Parent.join("../Resources/extensions"));
129
130				dev_log!("extensions", "[Extensions] [ScanPaths] + {}", ResourcesPath.display());
131
132				ScanPathsGuard.push(ResourcesPath);
133
134				// VS Code-style bundle layout: `.app/Contents/Resources/app/extensions`.
135				let ResourcesAppPath = Normalize(Parent.join("../Resources/app/extensions"));
136
137				dev_log!("extensions", "[Extensions] [ScanPaths] + {}", ResourcesAppPath.display());
138
139				ScanPathsGuard.push(ResourcesAppPath);
140
141				// Debug/dev path: Target/debug/extensions
142				let LocalPath = Parent.join("extensions");
143
144				dev_log!("extensions", "[Extensions] [ScanPaths] + {}", LocalPath.display());
145
146				ScanPathsGuard.push(LocalPath);
147
148				// Monorepo-layout fallback paths: resolved relative to
149				// `Element/Mountain/Target/{debug,release}/`, so they only
150				// materialise when the binary runs from inside the repo.
151				// Shipped `.app`s launched from `/Applications/` hit the
152				// `.exists()` guard and silently skip - no need for a
153				// `cfg(debug_assertions)` gate. Keeping these live in release
154				// lets a raw `Target/release/<name>` launch find the same 98
155				// built-in extensions a debug build does.
156				//
157				// Sky Target path: where CopyVSCodeAssets copies built-in
158				// extensions during the Sky build.
159				let SkyTargetPath = Parent.join("../../../Sky/Target/Static/Application/extensions");
160
161				if SkyTargetPath.exists() {
162					let SkyTargetPath = Normalize(SkyTargetPath);
163
164					dev_log!(
165						"extensions",
166						"[Extensions] [ScanPaths] + {} (Sky Target, repo-layout)",
167						SkyTargetPath.display()
168					);
169
170					ScanPathsGuard.push(SkyTargetPath);
171				}
172
173				// VS Code dependency path: built-in extensions from the VS
174				// Code source checkout - avoids requiring a copy step.
175				let DependencyPath = Parent.join("../../../../Dependency/Microsoft/Dependency/Editor/extensions");
176
177				if DependencyPath.exists() {
178					dev_log!(
179						"extensions",
180						"[Extensions] [ScanPaths] + {} (VS Code Dependency, repo-layout)",
181						DependencyPath.display()
182					);
183
184					ScanPathsGuard.push(DependencyPath);
185				}
186			}
187		}
188	} // end !SkipBuiltins
189
190	// User-scope paths: always scanned, independent of whether the binary
191	// was launched from the repo, a `.app`, or a symlink on the Desktop.
192	// Mirrors VS Code's `~/.vscode-oss/extensions` convention.
193	//
194	// Atom U1: `Lodge` overrides the default
195	// `~/.fiddee/extensions`. Useful for per-workspace sandboxes, shared
196	// caches on CI, or running against a test extensions set without
197	// polluting the user's real profile. The default root is resolved
198	// through the `Utilities::FiddeeRoot` atom so the dotfile name lives
199	// in one place.
200	if let Ok(UserOverride) = std::env::var("Lodge") {
201		let OverridePath = ExpandUserPath(&UserOverride);
202
203		dev_log!("extensions", "[Extensions] [ScanPaths] + {} (Lodge)", OverridePath.display());
204
205		ScanPathsGuard.push(OverridePath);
206	} else {
207		let UserExtensionPath = crate::IPC::WindServiceHandlers::Utilities::FiddeeRoot::Fn().join("extensions");
208
209		dev_log!(
210			"extensions",
211			"[Extensions] [ScanPaths] + {} (User)",
212			UserExtensionPath.display()
213		);
214
215		ScanPathsGuard.push(UserExtensionPath);
216
217		// Legacy `.land` extensions directory. Pre-FIDDEE installs landed
218		// here, and many users still have extensions installed at
219		// `~/.land/extensions` (Roo Code, GitLens, rust-analyzer, etc.).
220		// Scan it too when it exists, so the renderer's
221		// `extensions:scanUserExtensions` IPC doesn't return 0 entries
222		// while assets at those paths get served by the asset cache. The
223		// additive scan matches the task-plan T3 recommendation in
224		// `NEXT-SESSION-2026-05-26.md`. When `~/.land` is empty or
225		// missing, the push is harmless - the scanner skips paths whose
226		// directory enumeration yields no manifests.
227		if let Some(Home) = dirs::home_dir() {
228			let LandLegacy = Home.join(".land").join("extensions");
229
230			if LandLegacy.is_dir() {
231				dev_log!(
232					"extensions",
233					"[Extensions] [ScanPaths] + {} (User legacy ~/.land)",
234					LandLegacy.display()
235				);
236
237				ScanPathsGuard.push(LandLegacy);
238			}
239		}
240	}
241
242	// Atom U1: additional paths via `Extend`. Mirrors
243	// VS Code's `--extensions-dir=<a>:<b>:<c>` CLI. Platform-separator:
244	// semicolon on Windows (matches PATHEXT), colon elsewhere.
245	if let Ok(Extras) = std::env::var("Extend") {
246		let Separator = if cfg!(target_os = "windows") { ';' } else { ':' };
247
248		for Candidate in Extras.split(Separator) {
249			let Trimmed = Candidate.trim();
250
251			if Trimmed.is_empty() {
252				continue;
253			}
254
255			let ExtraPath = ExpandUserPath(Trimmed);
256
257			dev_log!("extensions", "[Extensions] [ScanPaths] + {} (Extend)", ExtraPath.display());
258
259			ScanPathsGuard.push(ExtraPath);
260		}
261	}
262
263	// Atom U1: development extensions path - the VS Code equivalent of
264	// `--extensionDevelopmentPath=<dir>`. Extensions here always load
265	// regardless of enablement state; kept separate from user-scope so a
266	// broken dev extension doesn't persist into the user's profile.
267	if let Ok(DevExtensions) = std::env::var("Probe") {
268		let DevPath = ExpandUserPath(&DevExtensions);
269
270		dev_log!("extensions", "[Extensions] [ScanPaths] + {} (Probe)", DevPath.display());
271
272		ScanPathsGuard.push(DevPath);
273	}
274
275	let ScanPaths = ScanPathsGuard.clone();
276
277	dev_log!("extensions", "[Extensions] [ScanPaths] Configured: {:?}", ScanPaths);
278
279	Ok(ScanPaths)
280}
281
282/// Expand a leading `~/` to `$HOME/` for user-provided paths. Env-var
283/// overrides frequently come from operators typing `~/.vscode/extensions`
284/// without shell expansion (e.g. in `.env` files, GUI launchers, sidecar
285/// manifests). Leaves absolute and relative paths untouched.
286fn ExpandUserPath(Raw:&str) -> PathBuf {
287	if let Some(Stripped) = Raw.strip_prefix("~/") {
288		if let Some(Home) = dirs::home_dir() {
289			return Home.join(Stripped);
290		}
291	}
292
293	PathBuf::from(Raw)
294}