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
141			&& let Ok(Cwd) = std::env::current_dir()
142		{
143			let IsFilesystemRoot = Cwd.parent().is_none();
144			if !IsFilesystemRoot {
145				Collected.push(WalkUpToProjectRoot(&Cwd));
146			}
147		}
148	}
149
150	// Final fallback: HOME directory. Reached when the binary was
151	// launched via Finder / `open` (cwd=`/`), there's no
152	// `RecentlyOpened.json` entry, and no `Open=` env. A workspace
153	// rooted at `$HOME` lets the tree view list the user's actual
154	// directories instead of showing an empty "no folder open" panel.
155	// The user can still pick a more specific folder via "File → Open
156	// Folder"; this just ensures something visible is there on first
157	// launch.
158	if Collected.is_empty()
159		&& let Some(Home) = dirs::home_dir()
160		&& Home.is_dir()
161	{
162		Collected.push(Home);
163	}
164
165	Collected
166		.into_iter()
167		.filter_map(|Path| {
168			if !Path.is_dir() {
169				eprintln!("[LandFix:WsInit] Skipping non-directory workspace folder: {}", Path.display());
170				return None;
171			}
172			Path.canonicalize().ok().or(Some(Path))
173		})
174		.collect()
175}
176
177/// Read `~/.land/workspaces/RecentlyOpened.json`'s top workspace entry and
178/// resolve it to a directory path. Mirrors the probe used by
179/// `Binary/Build/WindowBuild.rs::BuildInitialUrl` so the boot-seeded
180/// workspace folder agrees with the URL the webview actually loads. Returns
181/// `None` when the file is missing/malformed, the entry has no resolvable
182/// path, the path doesn't exist on disk, or it isn't a directory.
183fn ResolveRecentlyOpenedTopFolder() -> Option<PathBuf> {
184	use crate::IPC::WindServiceHandlers::Utilities::RecentlyOpened::ReadRecentlyOpened;
185
186	let Recent = ReadRecentlyOpened().ok()?;
187	let Workspaces = Recent.get("workspaces").and_then(|V| V.as_array())?;
188
189	// Same priority order as BuildInitialUrl: own writer's `uri`,
190	// VS Code's `folderUri`/`folderUri.path`, then `workspace.configPath.path`.
191	let Probe = |Entry:&serde_json::Value| -> Option<String> {
192		if let Some(Uri) = Entry.get("uri").and_then(|V| V.as_str()) {
193			return Some(Uri.to_string());
194		}
195		if let Some(Uri) = Entry.get("folderUri").and_then(|V| V.as_str()) {
196			return Some(Uri.to_string());
197		}
198		if let Some(Path) = Entry.get("folderUri").and_then(|V| V.get("path")).and_then(|V| V.as_str()) {
199			return Some(Path.to_string());
200		}
201		if let Some(Path) = Entry
202			.get("workspace")
203			.and_then(|V| V.get("configPath"))
204			.and_then(|V| V.get("path"))
205			.and_then(|V| V.as_str())
206		{
207			return Some(Path.to_string());
208		}
209		None
210	};
211
212	let Raw = Workspaces.iter().find_map(Probe)?;
213	let Normalised = Raw.strip_prefix("file://").unwrap_or(Raw.as_str()).to_string();
214	let Candidate = PathBuf::from(&Normalised);
215	if Candidate.is_dir() { Some(Candidate) } else { None }
216}
217
218/// Walk up from `Start` looking for a project-root marker (`Cargo.toml`,
219/// `package.json`, `.git`, `pyproject.toml`, `go.mod`, `pnpm-workspace.yaml`).
220/// Returns the first ancestor that owns one. Falls back to `Start` itself
221/// when nothing matches before the filesystem root.
222///
223/// Why: when a developer launches the binary from a `Target/debug/` build
224/// directory, `current_dir()` is the build folder, which has no source
225/// files. `vscode.workspace.findFiles('**/*')` walks it and returns
226/// nothing; the SCM panel can't find a repo; tree-views render empty.
227/// Walking up to the nearest project root mirrors what every
228/// `git`/`cargo`/`npm` CLI does and gives extensions a workspace folder
229/// they can actually scan.
230fn WalkUpToProjectRoot(Start:&Path) -> PathBuf {
231	const Markers:&[&str] = &[
232		"Cargo.toml",
233		"package.json",
234		".git",
235		"pyproject.toml",
236		"go.mod",
237		"pnpm-workspace.yaml",
238		"deno.json",
239		"deno.jsonc",
240	];
241	let mut Cursor:&Path = Start;
242	loop {
243		for Marker in Markers {
244			if Cursor.join(Marker).exists() {
245				return Cursor.to_path_buf();
246			}
247		}
248		match Cursor.parent() {
249			Some(Parent) if Parent != Cursor => Cursor = Parent,
250			_ => break,
251		}
252	}
253	Start.to_path_buf()
254}