oseda_cli/cmd/
run.rs

1use std::{process::Command, sync::mpsc};
2
3/// More in depth errors that could cause a project not to run
4#[derive(Debug)]
5pub enum OsedaRunError {
6    BuildError(String),
7    ServeError(String),
8}
9
10impl std::error::Error for OsedaRunError {}
11impl std::fmt::Display for OsedaRunError {
12    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
13        match self {
14            Self::BuildError(msg) => write!(f, "Oseda Build Error: {}", msg),
15            Self::ServeError(msg) => write!(f, "Oseda Serve Error: {}", msg),
16        }
17    }
18}
19
20/// Runs an Oseda project in the working directory
21///
22/// This will:
23/// - Run `npx vite build`
24/// - Start a static file server (`serve dist`)
25/// - Gracefully listen for Ctrl+C to shut down the server
26///     - This gracefull-ness here is important, this runs on a separate thread, do not attempt to orphan this process
27/// # Returns
28/// * `Ok(())` if both the build and serve steps succeed
29/// * `Err(OsedaRunError)` if any step fails (missing vite isn't installed, or `serve` fails to start)
30pub fn run() -> Result<(), OsedaRunError> {
31    // command run failure and command status are considered different, handled accordingly
32    match Command::new("npx").arg("vite").arg("build").status() {
33        Ok(status) => {
34            if !status.success() {
35                println!("Error: `npx vite build` exited with a failure.");
36                println!("Please ensure that npx and vite are installed properly.");
37                return Err(OsedaRunError::BuildError(
38                    "could not 'npx vite build'".to_string(),
39                ));
40            }
41        }
42        Err(e) => {
43            println!("Error: failed to execute `npx vite build`: {e}");
44            println!("Please ensure that `npx` and `vite` are installed and in your PATH.");
45            return Err(OsedaRunError::BuildError(
46                "could not 'npx vite build'".to_string(),
47            ));
48        }
49    }
50
51    let mut child = Command::new("npx")
52        .arg("serve")
53        .arg("dist")
54        .spawn()
55        .map_err(|e| {
56            println!("Error starting `serve dist`: {e}");
57            OsedaRunError::ServeError("failed to start serve".into())
58        })?;
59    // spawn will leave child running the background. Need to listen for ctrl+c, snatch it. Then kill subprocess
60
61    // https://github.com/Detegr/rust-ctrlc
62    let (tx, rx) = mpsc::channel();
63    ctrlc::set_handler(move || {
64        println!("\nSIGINT received. Attempting graceful shutdown...");
65        let _ = tx.send(());
66    })
67    .expect("Error setting Ctrl+C handler");
68
69    // block until ctrl+c
70    rx.recv().unwrap();
71
72    // attempt to kill the child process
73    if let Err(e) = child.kill() {
74        println!("Failed to kill `serve`: {e}");
75    } else {
76        println!("`serve` process terminated.");
77    }
78
79    let _ = child.wait();
80
81    Ok(())
82}