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}