cargo_e/
e_installer.rs

1use crate::e_prompts::yesno;
2use anyhow::{bail, Context, Result};
3use std::error::Error;
4use std::path::{Path, PathBuf};
5use std::process::{Command, Stdio};
6use which::which;
7
8/// Ensure `npm` is on PATH.  
9/// Ensures Node.js is installed first.  
10/// Returns the full path to the `npm` executable, or an error.
11pub fn ensure_npm() -> Result<PathBuf> {
12    // Ensure Node.js is installed
13    ensure_node()?;
14    which("npm").context("`npm` not found in PATH. Please install Node.js and npm.")
15}
16
17/// Ensure the `napi` CLI is on PATH (provided by `@napi-rs/cli`).  
18/// If missing, prompts the user and installs it globally via `npm install -g @napi-rs/cli`.  
19/// Returns the full path to the `napi` executable.
20pub fn ensure_napi_cli() -> Result<PathBuf, Box<dyn Error>> {
21    // 1) Already installed?
22    if let Ok(path) = which("napi") {
23        return Ok(path);
24    }
25
26    // 2) Prompt the user to install it via npm
27    println!("`napi` CLI not found. Install it globally now?");
28    match yesno(
29        "Do you want to install `@napi-rs/cli` globally via npm?",
30        Some(true),
31    ) {
32        Ok(Some(true)) => {
33            let npm = ensure_npm()?;
34            println!("Installing `@napi-rs/cli` via `npm install -g @napi-rs/cli`…");
35            let mut child = Command::new(npm)
36                .args(&["install", "-g", "@napi-rs/cli"])
37                .stdin(Stdio::null())
38                .stdout(Stdio::inherit())
39                .stderr(Stdio::inherit())
40                .spawn()
41                .map_err(|e| format!("Failed to spawn install command: {}", e))?;
42
43            child
44                .wait()
45                .map_err(|e| format!("Error while waiting for installation: {}", e))?;
46        }
47        Ok(Some(false)) => return Err("User skipped installing `@napi-rs/cli`".into()),
48        Ok(None) => return Err("Installation of `@napi-rs/cli` cancelled (timeout)".into()),
49        Err(e) => return Err(format!("Error during prompt: {}", e).into()),
50    }
51
52    // 3) Retry locating `napi`
53    which("napi").map_err(|_| "`napi` still not found after installation".into())
54}
55
56/// Ensure `cross-env` is on PATH.  
57/// If it’s missing, prompts the user and installs it globally via `npm install -g cross-env`.  
58/// Returns the full path to the `cross-env` executable.
59pub fn ensure_cross_env() -> Result<PathBuf, Box<dyn Error>> {
60    // 1) Already installed?
61    if let Ok(path) = which("cross-env") {
62        return Ok(path);
63    }
64
65    // 2) Prompt the user to install it via npm
66    println!("`cross-env` is not installed. Install it globally now?");
67    match yesno(
68        "Do you want to install `cross-env` globally via npm?",
69        Some(true),
70    ) {
71        Ok(Some(true)) => {
72            // Make sure npm is available
73            let npm = ensure_npm()?;
74            println!("Installing `cross-env` via `npm install -g cross-env`…");
75            let mut child = Command::new(npm)
76                .args(&["install", "-g", "cross-env"])
77                .stdin(Stdio::null())
78                .stdout(Stdio::inherit())
79                .stderr(Stdio::inherit())
80                .spawn()
81                .map_err(|e| format!("Failed to spawn install command: {}", e))?;
82
83            // Wait for the installation to finish
84            child
85                .wait()
86                .map_err(|e| format!("Error while waiting for installation: {}", e))?;
87        }
88        Ok(Some(false)) => return Err("User skipped installing `cross-env`".into()),
89        Ok(None) => return Err("Installation of `cross-env` cancelled (timeout)".into()),
90        Err(e) => return Err(format!("Error during prompt: {}", e).into()),
91    }
92
93    // 3) Retry locating `cross-env`
94    which("cross-env").map_err(|_| "`cross-env` still not found after installation".into())
95}
96/// Ensure `pnpm` is on PATH.  
97/// Ensures Node.js is installed first.  
98/// If it’s missing, will use `npm` (via `ensure_npm`) to install `pnpm` globally.  
99/// Returns the full path to the `pnpm` executable.
100pub fn ensure_pnpm() -> Result<PathBuf> {
101    // Ensure Node.js is installed
102    ensure_node()?;
103
104    // Check if `pnpm` is already installed
105    if let Ok(path) = which("pnpm") {
106        return Ok(path);
107    }
108
109    // Otherwise, prompt the user to install it via npm
110    println!("`pnpm` is not installed. Install it now?");
111    match yesno(
112        "Do you want to install `pnpm` globally via npm?",
113        Some(true),
114    ) {
115        Ok(Some(true)) => {
116            // Make sure we have npm
117            let npm_path = ensure_npm()?;
118            println!("Installing `pnpm` via `npm install -g pnpm`…");
119
120            let mut child = Command::new(npm_path)
121                .args(&["install", "-g", "pnpm"])
122                .stdin(Stdio::null())
123                .stdout(Stdio::inherit())
124                .stderr(Stdio::inherit())
125                .spawn()
126                .context("failed to spawn `npm install -g pnpm`")?;
127
128            child
129                .wait()
130                .context("error while waiting for `npm install -g pnpm` to finish")?;
131        }
132        Ok(Some(false)) => bail!("user skipped installing `pnpm`"),
133        Ok(None) => bail!("installation of `pnpm` cancelled (timeout)"),
134        Err(e) => bail!("error during prompt: {}", e),
135    }
136
137    // Retry locating `pnpm`
138    which("pnpm").context("`pnpm` still not found in PATH after installation")
139}
140
141/// Ensure the `dx` CLI (the Dioxus helper) is on PATH.
142/// If missing, prompts the user to install the Dioxus CLI via `cargo install dioxus-cli`.
143/// Returns the full path to the `dx` executable.
144pub fn ensure_dx() -> Result<PathBuf> {
145    // 1) Check if `dx` is already on PATH
146    if let Ok(path) = which("dx") {
147        return Ok(path);
148    }
149
150    // 2) Prompt the user to install it
151    println!("`dx` CLI not found. Install the Dioxus CLI now?");
152    match yesno(
153        "Do you want to install the Dioxus CLI via `cargo install dioxus-cli`?",
154        Some(true),
155    ) {
156        Ok(Some(true)) => {
157            println!("Installing `dioxus-cli` via `cargo install dioxus-cli`…");
158            let mut child = Command::new("cargo")
159                .args(&["install", "dioxus-cli"])
160                .stdin(Stdio::null())
161                .stdout(Stdio::inherit())
162                .stderr(Stdio::inherit())
163                .spawn()
164                .context("failed to spawn `cargo install dioxus-cli`")?;
165
166            child
167                .wait()
168                .context("error while waiting for `cargo install dioxus-cli` to finish")?;
169        }
170        Ok(Some(false)) => bail!("user skipped installing the Dioxus CLI"),
171        Ok(None) => bail!("installation of the Dioxus CLI cancelled (timeout)"),
172        Err(e) => bail!("error during prompt: {}", e),
173    }
174
175    // 3) Retry locating `dx`
176    which("dx").context("`dx` still not found in PATH after installation")
177}
178
179/// Ensure `trunk` is on PATH.  
180/// Returns the full path to the `trunk` executable, or an error.
181pub fn ensure_trunk() -> Result<PathBuf> {
182    // 1) First try to locate `trunk`
183    if let Ok(path) = which("trunk") {
184        return Ok(path);
185    }
186
187    // 2) Prompt the user to install it
188    println!("`trunk` is not installed. Install it now?");
189    match yesno("Do you want to install `trunk`?", Some(true)) {
190        Ok(Some(true)) => {
191            println!("Installing `trunk` via `cargo install trunk`…");
192            let mut child = Command::new("cargo")
193                .args(&["install", "trunk"])
194                .stdin(Stdio::null())
195                .stdout(Stdio::inherit())
196                .stderr(Stdio::inherit())
197                .spawn()
198                .context("failed to spawn `cargo install trunk`")?;
199
200            child
201                .wait()
202                .context("failed while waiting for `cargo install trunk` to finish")?;
203        }
204        Ok(Some(false)) => {
205            anyhow::bail!("user skipped installing `trunk`");
206        }
207        Ok(None) => {
208            anyhow::bail!("installation of `trunk` cancelled (timeout)");
209        }
210        Err(e) => {
211            anyhow::bail!("error during prompt: {}", e);
212        }
213    }
214
215    // 3) Re‐try locating `trunk`
216    which("trunk").context("`trunk` still not found in PATH after installation")
217}
218
219/// Ensure `rust-script` is on PATH.  
220/// Returns the full path to the `rust-script` executable, or an error.
221pub fn ensure_rust_script() -> Result<PathBuf> {
222    // 1) First try to locate `trunk`
223    if let Ok(path) = which("rust-script") {
224        return Ok(path);
225    }
226
227    // 2) Prompt the user to install it
228    println!("`rust-script` is not installed. Install it now?");
229    match yesno("Do you want to install `rust-script`?", Some(true)) {
230        Ok(Some(true)) => {
231            println!("Installing `rust-script` via `cargo install rust-script`…");
232            let mut child = Command::new("cargo")
233                .args(&["install", "rust-script"])
234                .stdin(Stdio::null())
235                .stdout(Stdio::inherit())
236                .stderr(Stdio::inherit())
237                .spawn()
238                .context("failed to spawn `cargo install rust-script`")?;
239
240            child
241                .wait()
242                .context("failed while waiting for `cargo install rust-script` to finish")?;
243        }
244        Ok(Some(false)) => {
245            anyhow::bail!("user skipped installing `rust-script`");
246        }
247        Ok(None) => {
248            anyhow::bail!("installation of `rust-script` cancelled (timeout)");
249        }
250        Err(e) => {
251            anyhow::bail!("error during prompt: {}", e);
252        }
253    }
254    which("rust-script").context("`rust-script` still not found in PATH after installation")
255}
256// Helper function to check for package.json and run npm install if needed
257pub fn check_npm_and_install(workspace_parent: &Path) -> Result<(), Box<dyn Error>> {
258    if workspace_parent.join("pnpm-workspace.yaml").exists() {
259        // If this is a pnpm workspace, skip npm checks
260        println!("Skipping npm checks for pnpm workspace.");
261        return Ok(());
262    }
263    // Check if package.json exists at the workspace parent level
264    println!(
265        "Checking for package.json in: {}",
266        workspace_parent.display()
267    );
268    if workspace_parent.join("package.json").exists() {
269        println!("package.json found in: {}", workspace_parent.display());
270        // Get the path to npm using `which`.
271        match which("npm") {
272            Ok(npm_path) => {
273                println!("Found npm at: {}", npm_path.display());
274
275                // Run `npm ls --depth=1` in the specified directory
276                let output = Command::new(npm_path.clone())
277                    .arg("ls")
278                    .arg("--depth=1")
279                    .current_dir(workspace_parent)
280                    .output()
281                    .map_err(|e| eprintln!("Failed to execute npm ls: {}", e))
282                    .ok();
283
284                if let Some(output) = output {
285                    println!("npm ls output: {}", String::from_utf8_lossy(&output.stdout));
286                    if !output.status.success() {
287                        // Print the npm error output for debugging.
288                        eprintln!(
289                            "npm ls failed for directory: {}",
290                            workspace_parent.display()
291                        );
292                        eprintln!("{}", String::from_utf8_lossy(&output.stderr));
293
294                        // Run `npm install` to fix the missing dependencies
295                        println!(
296                            "Running npm install in directory: {}",
297                            workspace_parent.display()
298                        );
299                        let install_output = Command::new(npm_path)
300                            .arg("install")
301                            .current_dir(workspace_parent)
302                            .output()
303                            .map_err(|e| eprintln!("Failed to execute npm install: {}", e))
304                            .ok();
305
306                        if let Some(install_output) = install_output {
307                            println!(
308                                "npm install output: {}",
309                                String::from_utf8_lossy(&install_output.stdout)
310                            );
311                            if install_output.status.success() {
312                                println!(
313                                    "npm install completed successfully in: {}",
314                                    workspace_parent.display()
315                                );
316                            } else {
317                                eprintln!(
318                                    "npm install failed in directory: {}",
319                                    workspace_parent.display()
320                                );
321                                eprintln!("{}", String::from_utf8_lossy(&install_output.stderr));
322                            }
323                        }
324                    }
325                }
326            }
327            Err(_) => {
328                eprintln!("npm is not installed or not in the system PATH.");
329                return Err("npm not found".into());
330            }
331        }
332    }
333    Ok(())
334}
335
336/// Check for a pnpm workspace and, if found, run `pnpm install`.  
337/// Returns the full path to the `pnpm` executable.
338pub fn check_pnpm_and_install(workspace_parent: &Path) -> Result<PathBuf> {
339    // if this is a pnpm workspace, install deps
340    let workspace_yaml = workspace_parent.join("pnpm-workspace.yaml");
341    if workspace_yaml.exists() {
342        // ensure pnpm is available (and install it if necessary)
343        let pnpm = ensure_pnpm()?;
344        println!(
345            "Found pnpm-workspace.yaml in: {}",
346            workspace_parent.display()
347        );
348        println!("Running `pnpm install`…");
349
350        let status = Command::new(&pnpm)
351            .arg("install")
352            .current_dir(workspace_parent)
353            .stdin(Stdio::null())
354            .stdout(Stdio::inherit())
355            .stderr(Stdio::inherit())
356            .status()
357            .context("failed to execute `pnpm install`")?;
358
359        if !status.success() {
360            bail!("`pnpm install` failed with exit code {}", status);
361        }
362        //         if cfg!( target_os = "windows" ) {
363        //             #[cfg(windows)]
364        // use std::os::windows::process::CommandExt;
365        //             // WinAPI flag for “create a new console window”
366        // #[cfg(windows)]
367        // const CREATE_NEW_CONSOLE: u32 = 0x0000_0010;
368        //             println!("Running `pnpm run build:debug windows");
369        //                 // Build the command
370        //     let mut cmd = Command::new("cmd");
371        //     cmd.args(&["/C", "pnpm run build:debug"])
372        //        .current_dir(workspace_parent);
373        //     //    .stdin(Stdio::null())
374        //     //    .stdout(Stdio::inherit())
375        //     //    .stderr(Stdio::inherit());
376
377        //     // On Windows, ask for a new console window
378        //     #[cfg(windows)]
379        //     {
380        //         cmd.creation_flags(CREATE_NEW_CONSOLE);
381        //     }
382        //                 let status =
383        //     cmd.status()?;
384        // if !status.success() {
385        //     anyhow::bail!("`pnpm run build:debug` failed with {}", status);
386        // }
387        //         } else {
388        // ensure_napi_cli().ok();
389        // ensure_cross_env().ok();
390        Command::new(&pnpm)
391            .args(&["run", "build:debug"])
392            .current_dir(workspace_parent)
393            .env("CARGO", "cargo")
394            .status()?;
395        // };
396
397        println!("✅ pnpm install succeeded");
398        return Ok(pnpm);
399    } else {
400        println!(
401            "No pnpm-workspace.yaml found in {}, skipping `pnpm install`.",
402            workspace_parent.display()
403        );
404    }
405    Ok(PathBuf::new())
406}
407
408/// Ensure `node` is on PATH.  
409/// If missing, attempts to install Node.js using `nvm` (automated for Windows, manual prompt otherwise).  
410/// Returns the full path to the `node` executable.
411pub fn ensure_node() -> Result<PathBuf> {
412    // Check if `node` is already installed
413    if let Ok(path) = which("node") {
414        return Ok(path);
415    }
416
417    #[cfg(target_os = "windows")]
418    {
419        // On Windows, use Chocolatey to install NVM and set Node.js to LTS
420        println!("`node` is not installed.");
421        match yesno(
422            "Do you want to install Node.js using NVM (via Chocolatey)?",
423            Some(true),
424        ) {
425            Ok(Some(true)) => {
426                println!("Installing NVM via Chocolatey...");
427                let choco = ensure_choco()?;
428                let mut child = Command::new(choco)
429                    .args(&["install", "nvm"]) //, "-y"])
430                    .stdin(Stdio::null())
431                    .stdout(Stdio::inherit())
432                    .stderr(Stdio::inherit())
433                    .spawn()
434                    .context("Failed to spawn `choco install nvm`")?;
435
436                child
437                    .wait()
438                    .context("Error while waiting for `choco install nvm` to finish")?;
439
440                // Use NVM to install and use the latest LTS version of Node.js
441                let nvm = which("nvm").context("`nvm` not found in PATH after installation.")?;
442                let mut child = Command::new(&nvm)
443                    .args(&["install", "lts"])
444                    .stdin(Stdio::null())
445                    .stdout(Stdio::inherit())
446                    .stderr(Stdio::inherit())
447                    .spawn()
448                    .context("Failed to spawn `nvm install lts`")?;
449
450                child
451                    .wait()
452                    .context("Error while waiting for `nvm install lts` to finish")?;
453
454                let mut child = Command::new(&nvm)
455                    .args(&["use", "lts"])
456                    .stdin(Stdio::null())
457                    .stdout(Stdio::inherit())
458                    .stderr(Stdio::inherit())
459                    .spawn()
460                    .context("Failed to spawn `nvm use lts`")?;
461
462                child
463                    .wait()
464                    .context("Error while waiting for `nvm use lts` to finish")?;
465            }
466            Ok(Some(false)) => {
467                anyhow::bail!("User declined to install Node.js.");
468            }
469            Ok(None) => {
470                anyhow::bail!("Installation of Node.js cancelled (timeout).");
471            }
472            Err(e) => {
473                anyhow::bail!("Error during prompt: {}", e);
474            }
475        }
476    }
477
478    #[cfg(not(target_os = "windows"))]
479    {
480        // On non-Windows systems, prompt the user to install Node.js manually
481        println!("`node` is not installed. Please install Node.js manually.");
482        anyhow::bail!("Node.js installation is not automated for this platform.");
483    }
484
485    // Retry locating `node`
486    which("node").context("`node` still not found after installation")
487}
488
489/// Ensure `choco` (Chocolatey) is on PATH.  
490/// If missing, prompts the user to install Chocolatey manually.  
491/// Returns the full path to the `choco` executable.
492pub fn ensure_choco() -> Result<PathBuf> {
493    // Check if `choco` is already installed
494    if let Ok(path) = which("choco") {
495        return Ok(path);
496    }
497
498    #[cfg(target_os = "windows")]
499    {
500        // On Windows, prompt the user to install Chocolatey manually
501        println!("`choco` (Chocolatey) is not installed.");
502        println!("It is required to proceed. Do you want to install it manually?");
503        match yesno(
504            "Do you want to install Chocolatey manually by following the instructions?",
505            Some(true),
506        ) {
507            Ok(Some(true)) => {
508                println!("Please run the following command in PowerShell to install Chocolatey:");
509                println!("Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))");
510                anyhow::bail!(
511                    "Chocolatey installation is not automated. Please install it manually."
512                );
513            }
514            Ok(Some(false)) => {
515                anyhow::bail!("User declined to install Chocolatey.");
516            }
517            Ok(None) => {
518                anyhow::bail!("Installation of Chocolatey cancelled (timeout).");
519            }
520            Err(e) => {
521                anyhow::bail!("Error during prompt: {}", e);
522            }
523        }
524    }
525
526    #[cfg(not(target_os = "windows"))]
527    {
528        anyhow::bail!("Chocolatey is only supported on Windows.");
529    }
530}