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