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}