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
75 let mut Index = 0;
76
77 while Index < CliArgs.len() {
78 let Argument = &CliArgs[Index];
79
80 if (Argument == "--folder" || Argument == "-F") && Index + 1 < CliArgs.len() {
81 Collected.push(PathBuf::from(&CliArgs[Index + 1]));
82
83 Index += 2;
84
85 continue;
86 }
87
88 // Positional existing-directory argument. Skip flags + workspace files.
89 if !Argument.starts_with('-') && !Argument.ends_with(".code-workspace") {
90 let Candidate = PathBuf::from(Argument);
91
92 if Candidate.is_dir() {
93 Collected.push(Candidate);
94 }
95 }
96
97 Index += 1;
98 }
99
100 if Collected.is_empty() {
101 if let Ok(EnvValue) = std::env::var("Open") {
102 let Separator = if cfg!(windows) { ';' } else { ':' };
103
104 for Piece in EnvValue.split(Separator) {
105 let Piece = Piece.trim();
106
107 if Piece.is_empty() {
108 continue;
109 }
110
111 Collected.push(PathBuf::from(Piece));
112 }
113 }
114 }
115
116 // Recently-opened fallback. The webview's initial URL is built from
117 // `~/.land/workspaces/RecentlyOpened.json`'s top entry (see
118 // `Binary/Build/WindowBuild.rs::BuildInitialUrl`), so when the user
119 // picks a folder from the recent-list / "Open Folder" UI, the URL
120 // loads with `?folder=<their-pick>` but Mountain's boot-time seeder
121 // previously fell straight through to CWD walk-up. Result: webview
122 // title says "Mountain" but Cocoon's init payload ships "Land",
123 // vscode.git scans the wrong root, SCM panel reports zeros while
124 // `git status` in the actual folder shows uncommitted changes.
125 //
126 // Probe the same source of truth as `BuildInitialUrl` so the seeded
127 // workspace and the loaded URL agree. Slot this between env/CLI
128 // (explicit user intent) and CWD walk-up (last resort).
129 if Collected.is_empty() {
130 if let Some(Path) = ResolveRecentlyOpenedTopFolder() {
131 Collected.push(Path);
132 }
133 }
134
135 if Collected.is_empty() {
136 // CWD-autoload: ON in every profile. The earlier
137 // debug-only default left release `.app` launches via Finder /
138 // `open` with no workspace folder (cwd=`/` after `open`,
139 // `RecentlyOpened.json` may be empty/stale → tree-view empty,
140 // `vscode.workspace.findFiles` returns nothing, SCM panel can't
141 // find a repo). Override with `Walk=0` to keep the stock
142 // VS Code "File → Open Folder" UX.
143 //
144 // Safety: when cwd is the filesystem root `/` (always the case
145 // when launched via `open` from Finder/Dock), the walk-up
146 // returns `/` itself which would scan the entire disk. Skip
147 // that and fall through to the HOME fallback below.
148 let AutoloadCwd = std::env::var("Walk")
149 .map(|Value| matches!(Value.as_str(), "1" | "true" | "yes" | "on"))
150 .unwrap_or(true);
151
152 if AutoloadCwd && let Ok(Cwd) = std::env::current_dir() {
153 let IsFilesystemRoot = Cwd.parent().is_none();
154
155 if !IsFilesystemRoot {
156 Collected.push(WalkUpToProjectRoot(&Cwd));
157 }
158 }
159 }
160
161 // Final fallback: HOME directory. Reached when the binary was
162 // launched via Finder / `open` (cwd=`/`), there's no
163 // `RecentlyOpened.json` entry, and no `Open=` env. A workspace
164 // rooted at `$HOME` lets the tree view list the user's actual
165 // directories instead of showing an empty "no folder open" panel.
166 // The user can still pick a more specific folder via "File → Open
167 // Folder"; this just ensures something visible is there on first
168 // launch.
169 if Collected.is_empty()
170 && let Some(Home) = dirs::home_dir()
171 && Home.is_dir()
172 {
173 Collected.push(Home);
174 }
175
176 Collected
177 .into_iter()
178 .filter_map(|Path| {
179 if !Path.is_dir() {
180 eprintln!("[LandFix:WsInit] Skipping non-directory workspace folder: {}", Path.display());
181 return None;
182 }
183 Path.canonicalize().ok().or(Some(Path))
184 })
185 .collect()
186}
187
188/// Read `~/.land/workspaces/RecentlyOpened.json`'s top workspace entry and
189/// resolve it to a directory path. Mirrors the probe used by
190/// `Binary/Build/WindowBuild.rs::BuildInitialUrl` so the boot-seeded
191/// workspace folder agrees with the URL the webview actually loads. Returns
192/// `None` when the file is missing/malformed, the entry has no resolvable
193/// path, the path doesn't exist on disk, or it isn't a directory.
194fn ResolveRecentlyOpenedTopFolder() -> Option<PathBuf> {
195 use crate::IPC::WindServiceHandlers::Utilities::RecentlyOpened::ReadRecentlyOpened;
196
197 let Recent = ReadRecentlyOpened().ok()?;
198
199 let Workspaces = Recent.get("workspaces").and_then(|V| V.as_array())?;
200
201 // Same priority order as BuildInitialUrl: own writer's `uri`,
202 // VS Code's `folderUri`/`folderUri.path`, then `workspace.configPath.path`.
203 let Probe = |Entry:&serde_json::Value| -> Option<String> {
204 if let Some(Uri) = Entry.get("uri").and_then(|V| V.as_str()) {
205 return Some(Uri.to_string());
206 }
207
208 if let Some(Uri) = Entry.get("folderUri").and_then(|V| V.as_str()) {
209 return Some(Uri.to_string());
210 }
211
212 if let Some(Path) = Entry.get("folderUri").and_then(|V| V.get("path")).and_then(|V| V.as_str()) {
213 return Some(Path.to_string());
214 }
215
216 if let Some(Path) = Entry
217 .get("workspace")
218 .and_then(|V| V.get("configPath"))
219 .and_then(|V| V.get("path"))
220 .and_then(|V| V.as_str())
221 {
222 return Some(Path.to_string());
223 }
224
225 None
226 };
227
228 let Raw = Workspaces.iter().find_map(Probe)?;
229
230 let Normalised = Raw.strip_prefix("file://").unwrap_or(Raw.as_str()).to_string();
231
232 let Candidate = PathBuf::from(&Normalised);
233
234 if Candidate.is_dir() { Some(Candidate) } else { None }
235}
236
237/// Walk up from `Start` looking for a project-root marker (`Cargo.toml`,
238/// `package.json`, `.git`, `pyproject.toml`, `go.mod`, `pnpm-workspace.yaml`).
239/// Returns the first ancestor that owns one. Falls back to `Start` itself
240/// when nothing matches before the filesystem root.
241///
242/// Why: when a developer launches the binary from a `Target/debug/` build
243/// directory, `current_dir()` is the build folder, which has no source
244/// files. `vscode.workspace.findFiles('**/*')` walks it and returns
245/// nothing; the SCM panel can't find a repo; tree-views render empty.
246/// Walking up to the nearest project root mirrors what every
247/// `git`/`cargo`/`npm` CLI does and gives extensions a workspace folder
248/// they can actually scan.
249fn WalkUpToProjectRoot(Start:&Path) -> PathBuf {
250 const Markers:&[&str] = &[
251 "Cargo.toml",
252 "package.json",
253 ".git",
254 "pyproject.toml",
255 "go.mod",
256 "pnpm-workspace.yaml",
257 "deno.json",
258 "deno.jsonc",
259 ];
260
261 let mut Cursor:&Path = Start;
262
263 loop {
264 for Marker in Markers {
265 if Cursor.join(Marker).exists() {
266 return Cursor.to_path_buf();
267 }
268 }
269
270 match Cursor.parent() {
271 Some(Parent) if Parent != Cursor => Cursor = Parent,
272
273 _ => break,
274 }
275 }
276
277 Start.to_path_buf()
278}