Skip to main content

Mountain/Binary/Initialize/
CliParse.rs

1//! # CliParse
2//!
3//! Parses command-line arguments for workspace configuration.
4//!
5//! ## RESPONSIBILITIES
6//!
7//! ### Argument Parsing
8//! - Parse CLI arguments
9//! - Extract workspace file from arguments
10//! - Validate workspace file extension
11//!
12//! ## ARCHITECTURAL ROLE
13//!
14//! ### Position in Mountain
15//! - Early initialization component in Binary subsystem
16//! - Provides workspace configuration from CLI
17//!
18//! ### Dependencies
19//! - std::env: Environment argument access
20//!
21//! ### Dependents
22//! - Fn() main entry point: Uses parsed CLI args
23//!
24//! ## SECURITY
25//!
26//! ### Considerations
27//! - Validate workspace paths to prevent directory traversal
28//! - Ensure only .code-workspace files are processed
29//!
30//! ## PERFORMANCE
31//!
32//! ### Considerations
33//! - CLI parsing is fast, minimal overhead
34
35use std::path::{Path, PathBuf};
36
37/// Parse CLI arguments and extract workspace path.
38///
39/// Looks for a .code-workspace file argument in the command-line
40/// arguments and returns it if found.
41///
42/// # Returns
43///
44/// Returns the workspace file path if found, or None.
45pub fn Parse() -> Option<PathBuf> {
46	let CliArgs:Vec<String> = std::env::args().collect();
47
48	let WorkspacePathArgument = CliArgs.iter().find(|Arg| Arg.ends_with(".code-workspace"));
49
50	WorkspacePathArgument.map(|PathString| PathBuf::from(PathString))
51}
52
53/// Check if a workspace argument was provided.
54///
55/// Returns true if a workspace file path was found in CLI arguments.
56pub fn HasWorkspaceArgument() -> bool { Parse().is_some() }
57
58/// Parse workspace folder paths from CLI / env with the following precedence:
59///
60/// 1. Every `--folder <path>` pair on the command line (repeatable).
61/// 2. Any non-flag positional argument that resolves to an existing directory
62///    (convention used when the user drags a folder onto the app).
63/// 3. `Open` env var (colon-separated on POSIX, `;`-separated on Windows to
64///    match the platform's PATH delimiter).
65/// 4. The current working directory, if no other source is available AND `Walk`
66///    isn't set to `false`.
67///
68/// Returned paths are canonicalised; non-existent / non-directory entries
69/// are dropped with a warning.
70pub fn ParseWorkspaceFolders() -> Vec<PathBuf> {
71	let mut Collected:Vec<PathBuf> = Vec::new();
72
73	let CliArgs:Vec<String> = std::env::args().skip(1).collect();
74	let mut Index = 0;
75	while Index < CliArgs.len() {
76		let Argument = &CliArgs[Index];
77		if (Argument == "--folder" || Argument == "-F") && Index + 1 < CliArgs.len() {
78			Collected.push(PathBuf::from(&CliArgs[Index + 1]));
79			Index += 2;
80			continue;
81		}
82		// Positional existing-directory argument. Skip flags + workspace files.
83		if !Argument.starts_with('-') && !Argument.ends_with(".code-workspace") {
84			let Candidate = PathBuf::from(Argument);
85			if Candidate.is_dir() {
86				Collected.push(Candidate);
87			}
88		}
89		Index += 1;
90	}
91
92	if Collected.is_empty() {
93		if let Ok(EnvValue) = std::env::var("Open") {
94			let Separator = if cfg!(windows) { ';' } else { ':' };
95			for Piece in EnvValue.split(Separator) {
96				let Piece = Piece.trim();
97				if Piece.is_empty() {
98					continue;
99				}
100				Collected.push(PathBuf::from(Piece));
101			}
102		}
103	}
104
105	// Recently-opened fallback. The webview's initial URL is built from
106	// `~/.land/workspaces/RecentlyOpened.json`'s top entry (see
107	// `Binary/Build/WindowBuild.rs::BuildInitialUrl`), so when the user
108	// picks a folder from the recent-list / "Open Folder" UI, the URL
109	// loads with `?folder=<their-pick>` but Mountain's boot-time seeder
110	// previously fell straight through to CWD walk-up. Result: webview
111	// title says "Mountain" but Cocoon's init payload ships "Land",
112	// vscode.git scans the wrong root, SCM panel reports zeros while
113	// `git status` in the actual folder shows uncommitted changes.
114	//
115	// Probe the same source of truth as `BuildInitialUrl` so the seeded
116	// workspace and the loaded URL agree. Slot this between env/CLI
117	// (explicit user intent) and CWD walk-up (last resort).
118	if Collected.is_empty() {
119		if let Some(Path) = ResolveRecentlyOpenedTopFolder() {
120			Collected.push(Path);
121		}
122	}
123
124	if Collected.is_empty() {
125		// CWD-autoload: ON in every profile. The earlier
126		// debug-only default left release `.app` launches via Finder /
127		// `open` with no workspace folder (cwd=`/` after `open`,
128		// `RecentlyOpened.json` may be empty/stale → tree-view empty,
129		// `vscode.workspace.findFiles` returns nothing, SCM panel can't
130		// find a repo). Override with `Walk=0` to keep the stock
131		// VS Code "File → Open Folder" UX.
132		//
133		// Safety: when cwd is the filesystem root `/` (always the case
134		// when launched via `open` from Finder/Dock), the walk-up
135		// returns `/` itself which would scan the entire disk. Skip
136		// that and fall through to the HOME fallback below.
137		let AutoloadCwd = std::env::var("Walk")
138			.map(|Value| matches!(Value.as_str(), "1" | "true" | "yes" | "on"))
139			.unwrap_or(true);
140		if AutoloadCwd && let Ok(Cwd) = std::env::current_dir() {
141			let IsFilesystemRoot = Cwd.parent().is_none();
142			if !IsFilesystemRoot {
143				Collected.push(WalkUpToProjectRoot(&Cwd));
144			}
145		}
146	}
147
148	// Final fallback: HOME directory. Reached when the binary was
149	// launched via Finder / `open` (cwd=`/`), there's no
150	// `RecentlyOpened.json` entry, and no `Open=` env. A workspace
151	// rooted at `$HOME` lets the tree view list the user's actual
152	// directories instead of showing an empty "no folder open" panel.
153	// The user can still pick a more specific folder via "File → Open
154	// Folder"; this just ensures something visible is there on first
155	// launch.
156	if Collected.is_empty()
157		&& let Some(Home) = dirs::home_dir()
158		&& Home.is_dir()
159	{
160		Collected.push(Home);
161	}
162
163	Collected
164		.into_iter()
165		.filter_map(|Path| {
166			if !Path.is_dir() {
167				eprintln!("[LandFix:WsInit] Skipping non-directory workspace folder: {}", Path.display());
168				return None;
169			}
170			Path.canonicalize().ok().or(Some(Path))
171		})
172		.collect()
173}
174
175/// Read `~/.land/workspaces/RecentlyOpened.json`'s top workspace entry and
176/// resolve it to a directory path. Mirrors the probe used by
177/// `Binary/Build/WindowBuild.rs::BuildInitialUrl` so the boot-seeded
178/// workspace folder agrees with the URL the webview actually loads. Returns
179/// `None` when the file is missing/malformed, the entry has no resolvable
180/// path, the path doesn't exist on disk, or it isn't a directory.
181fn ResolveRecentlyOpenedTopFolder() -> Option<PathBuf> {
182	use crate::IPC::WindServiceHandlers::Utilities::RecentlyOpened::ReadRecentlyOpened;
183
184	let Recent = ReadRecentlyOpened().ok()?;
185	let Workspaces = Recent.get("workspaces").and_then(|V| V.as_array())?;
186
187	// Same priority order as BuildInitialUrl: own writer's `uri`,
188	// VS Code's `folderUri`/`folderUri.path`, then `workspace.configPath.path`.
189	let Probe = |Entry:&serde_json::Value| -> Option<String> {
190		if let Some(Uri) = Entry.get("uri").and_then(|V| V.as_str()) {
191			return Some(Uri.to_string());
192		}
193		if let Some(Uri) = Entry.get("folderUri").and_then(|V| V.as_str()) {
194			return Some(Uri.to_string());
195		}
196		if let Some(Path) = Entry.get("folderUri").and_then(|V| V.get("path")).and_then(|V| V.as_str()) {
197			return Some(Path.to_string());
198		}
199		if let Some(Path) = Entry
200			.get("workspace")
201			.and_then(|V| V.get("configPath"))
202			.and_then(|V| V.get("path"))
203			.and_then(|V| V.as_str())
204		{
205			return Some(Path.to_string());
206		}
207		None
208	};
209
210	let Raw = Workspaces.iter().find_map(Probe)?;
211	let Normalised = Raw.strip_prefix("file://").unwrap_or(Raw.as_str()).to_string();
212	let Candidate = PathBuf::from(&Normalised);
213	if Candidate.is_dir() { Some(Candidate) } else { None }
214}
215
216/// Walk up from `Start` looking for a project-root marker (`Cargo.toml`,
217/// `package.json`, `.git`, `pyproject.toml`, `go.mod`, `pnpm-workspace.yaml`).
218/// Returns the first ancestor that owns one. Falls back to `Start` itself
219/// when nothing matches before the filesystem root.
220///
221/// Why: when a developer launches the binary from a `Target/debug/` build
222/// directory, `current_dir()` is the build folder, which has no source
223/// files. `vscode.workspace.findFiles('**/*')` walks it and returns
224/// nothing; the SCM panel can't find a repo; tree-views render empty.
225/// Walking up to the nearest project root mirrors what every
226/// `git`/`cargo`/`npm` CLI does and gives extensions a workspace folder
227/// they can actually scan.
228fn WalkUpToProjectRoot(Start:&Path) -> PathBuf {
229	const Markers:&[&str] = &[
230		"Cargo.toml",
231		"package.json",
232		".git",
233		"pyproject.toml",
234		"go.mod",
235		"pnpm-workspace.yaml",
236		"deno.json",
237		"deno.jsonc",
238	];
239	let mut Cursor:&Path = Start;
240	loop {
241		for Marker in Markers {
242			if Cursor.join(Marker).exists() {
243				return Cursor.to_path_buf();
244			}
245		}
246		match Cursor.parent() {
247			Some(Parent) if Parent != Cursor => Cursor = Parent,
248			_ => break,
249		}
250	}
251	Start.to_path_buf()
252}