Skip to main content

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			if OverridePath.exists() {
67				dev_log!("extensions", "[Extensions] [ScanPaths] + {} (Ship)", OverridePath.display());
68				ScanPathsGuard.push(OverridePath);
69			} else {
70				dev_log!(
71					"extensions",
72					"warn: [Extensions] [ScanPaths] Ship={} does not exist; ignoring",
73					Override
74				);
75			}
76		}
77	}
78
79	// Resolve paths from executable directory
80	if !SkipBuiltins {
81		if let Ok(ExecutableDirectory) = std::env::current_exe() {
82			if let Some(Parent) = ExecutableDirectory.parent() {
83				// Standard Tauri bundle path: ../Resources/extensions.
84				// When launched from a `.app`, Parent is `Contents/MacOS/` and
85				// this resolves to `Contents/Resources/extensions`.
86				let ResourcesPath = Parent.join("../Resources/extensions");
87				dev_log!("extensions", "[Extensions] [ScanPaths] + {}", ResourcesPath.display());
88				ScanPathsGuard.push(ResourcesPath);
89
90				// VS Code-style bundle layout: `.app/Contents/Resources/app/extensions`.
91				// Some tooling copies built-ins here; probe both conventions so a
92				// single bundle works regardless of which copy step placed them.
93				let ResourcesAppPath = Parent.join("../Resources/app/extensions");
94				dev_log!("extensions", "[Extensions] [ScanPaths] + {}", ResourcesAppPath.display());
95				ScanPathsGuard.push(ResourcesAppPath);
96
97				// Debug/dev path: Target/debug/extensions
98				let LocalPath = Parent.join("extensions");
99				dev_log!("extensions", "[Extensions] [ScanPaths] + {}", LocalPath.display());
100				ScanPathsGuard.push(LocalPath);
101
102				// Monorepo-layout fallback paths: resolved relative to
103				// `Element/Mountain/Target/{debug,release}/`, so they only
104				// materialise when the binary runs from inside the repo.
105				// Shipped `.app`s launched from `/Applications/` hit the
106				// `.exists()` guard and silently skip - no need for a
107				// `cfg(debug_assertions)` gate. Keeping these live in release
108				// lets a raw `Target/release/<name>` launch find the same 98
109				// built-in extensions a debug build does.
110				//
111				// Sky Target path: where CopyVSCodeAssets copies built-in
112				// extensions during the Sky build.
113				let SkyTargetPath = Parent.join("../../../Sky/Target/Static/Application/extensions");
114				if SkyTargetPath.exists() {
115					dev_log!(
116						"extensions",
117						"[Extensions] [ScanPaths] + {} (Sky Target, repo-layout)",
118						SkyTargetPath.display()
119					);
120					ScanPathsGuard.push(SkyTargetPath);
121				}
122
123				// VS Code dependency path: built-in extensions from the VS
124				// Code source checkout - avoids requiring a copy step.
125				let DependencyPath = Parent.join("../../../../Dependency/Microsoft/Dependency/Editor/extensions");
126				if DependencyPath.exists() {
127					dev_log!(
128						"extensions",
129						"[Extensions] [ScanPaths] + {} (VS Code Dependency, repo-layout)",
130						DependencyPath.display()
131					);
132					ScanPathsGuard.push(DependencyPath);
133				}
134			}
135		}
136	} // end !SkipBuiltins
137
138	// User-scope paths: always scanned, independent of whether the binary
139	// was launched from the repo, a `.app`, or a symlink on the Desktop.
140	// Mirrors VS Code's `~/.vscode-oss/extensions` convention.
141	//
142	// Atom U1: `Lodge` overrides the default
143	// `~/.land/extensions`. Useful for per-workspace sandboxes, shared
144	// caches on CI, or running against a test extensions set without
145	// polluting the user's real profile.
146	if let Ok(UserOverride) = std::env::var("Lodge") {
147		let OverridePath = ExpandUserPath(&UserOverride);
148		dev_log!("extensions", "[Extensions] [ScanPaths] + {} (Lodge)", OverridePath.display());
149		ScanPathsGuard.push(OverridePath);
150	} else if let Some(HomeDirectory) = dirs::home_dir() {
151		let UserExtensionPath = HomeDirectory.join(".land/extensions");
152		dev_log!(
153			"extensions",
154			"[Extensions] [ScanPaths] + {} (User)",
155			UserExtensionPath.display()
156		);
157		ScanPathsGuard.push(UserExtensionPath);
158	}
159
160	// Atom U1: additional paths via `Extend`. Mirrors
161	// VS Code's `--extensions-dir=<a>:<b>:<c>` CLI. Platform-separator:
162	// semicolon on Windows (matches PATHEXT), colon elsewhere.
163	if let Ok(Extras) = std::env::var("Extend") {
164		let Separator = if cfg!(target_os = "windows") { ';' } else { ':' };
165		for Candidate in Extras.split(Separator) {
166			let Trimmed = Candidate.trim();
167			if Trimmed.is_empty() {
168				continue;
169			}
170			let ExtraPath = ExpandUserPath(Trimmed);
171			dev_log!("extensions", "[Extensions] [ScanPaths] + {} (Extend)", ExtraPath.display());
172			ScanPathsGuard.push(ExtraPath);
173		}
174	}
175
176	// Atom U1: development extensions path - the VS Code equivalent of
177	// `--extensionDevelopmentPath=<dir>`. Extensions here always load
178	// regardless of enablement state; kept separate from user-scope so a
179	// broken dev extension doesn't persist into the user's profile.
180	if let Ok(DevExtensions) = std::env::var("Probe") {
181		let DevPath = ExpandUserPath(&DevExtensions);
182		dev_log!("extensions", "[Extensions] [ScanPaths] + {} (Probe)", DevPath.display());
183		ScanPathsGuard.push(DevPath);
184	}
185
186	let ScanPaths = ScanPathsGuard.clone();
187
188	dev_log!("extensions", "[Extensions] [ScanPaths] Configured: {:?}", ScanPaths);
189
190	Ok(ScanPaths)
191}
192
193/// Expand a leading `~/` to `$HOME/` for user-provided paths. Env-var
194/// overrides frequently come from operators typing `~/.vscode/extensions`
195/// without shell expansion (e.g. in `.env` files, GUI launchers, sidecar
196/// manifests). Leaves absolute and relative paths untouched.
197fn ExpandUserPath(Raw:&str) -> PathBuf {
198	if let Some(Stripped) = Raw.strip_prefix("~/") {
199		if let Some(Home) = dirs::home_dir() {
200			return Home.join(Stripped);
201		}
202	}
203	PathBuf::from(Raw)
204}