crepuscularity-cli 0.8.1

crepus CLI — scaffolding and builds for Crepuscularity (UNSTABLE; in active development).
use std::path::Path;
use std::process::{exit, Child, Command};
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;

use console::style;

pub fn run(args: &[String]) {
    if args.is_empty() || args.first().is_some_and(|a| a == "--help" || a == "-h") {
        print_usage();
        return;
    }

    let cwd = std::env::current_dir().unwrap_or_else(|_| ".".into());
    let app_name = aurora_app_name(&cwd);
    let child_slot: Arc<Mutex<Option<Child>>> = Arc::new(Mutex::new(None));
    let child_for_signal = Arc::clone(&child_slot);
    let app_name_for_signal = app_name.clone();

    if let Err(e) = ctrlc::set_handler(move || {
        stop_aurora_child(&child_for_signal);
        stop_aurora_app(app_name_for_signal.as_deref());
    }) {
        eprintln!(
            "{} could not install Ctrl-C handler: {}",
            style("crepus aurora").yellow(),
            e
        );
    }

    let child = Command::new("aurorality")
        .args(args)
        .spawn()
        .unwrap_or_else(|e| {
            eprintln!(
                "{} failed to run aurorality: {}. {}",
                style("crepus aurora").red().bold(),
                e,
                style("Is aurorality installed? Run: cargo install aurorality-cli").dim()
            );
            exit(1);
        });

    {
        let mut slot = child_slot.lock().unwrap_or_else(|e| e.into_inner());
        *slot = Some(child);
    }

    let status = loop {
        let mut finished = None;
        {
            let mut slot = child_slot.lock().unwrap_or_else(|e| e.into_inner());
            if let Some(child) = slot.as_mut() {
                match child.try_wait() {
                    Ok(Some(status)) => {
                        finished = Some(status);
                    }
                    Ok(None) => {}
                    Err(e) => {
                        eprintln!(
                            "{} aurorality wait failed: {}",
                            style("crepus aurora").red(),
                            e
                        );
                        exit(1);
                    }
                }
            } else {
                stop_aurora_app(app_name.as_deref());
                exit(130);
            }
        }
        if let Some(status) = finished {
            let mut slot = child_slot.lock().unwrap_or_else(|e| e.into_inner());
            *slot = None;
            break status;
        }
        thread::sleep(Duration::from_millis(100));
    };

    if !status.success() {
        if args.first().is_some_and(|arg| arg == "dev") {
            stop_aurora_app(app_name.as_deref());
        }
        exit(status.code().unwrap_or(1));
    }
}

fn stop_aurora_child(child_slot: &Arc<Mutex<Option<Child>>>) {
    let mut slot = child_slot.lock().unwrap_or_else(|e| e.into_inner());
    if let Some(mut child) = slot.take() {
        let _ = child.kill();
        let _ = child.wait();
    }
}

fn stop_aurora_app(app_name: Option<&str>) {
    if let Some(app_name) = app_name {
        let _ = Command::new("pkill").args(["-f", app_name]).status();
    }
}

fn aurora_app_name(cwd: &Path) -> Option<String> {
    let content = std::fs::read_to_string(cwd.join("crepus.toml")).ok()?;
    let value = content.parse::<toml::Value>().ok()?;
    value
        .get("package")
        .and_then(|package| package.get("name"))
        .and_then(|name| name.as_str())
        .map(ToOwned::to_owned)
}

fn print_usage() {
    eprintln!(
        "{}  {}",
        style("crepus aurora").cyan().bold(),
        style("SwiftUI + Rust shell for .crepus templates").dim()
    );
    eprintln!();
    eprintln!("{}", style("SUBCOMMANDS").dim());
    eprintln!(
        "  {}  {}",
        style("dev [--watch DIR]").green(),
        style("hot-reload dev server + live preview window").dim()
    );
    eprintln!(
        "  {}  {}",
        style("run [PROJECT] [--ios]").green(),
        style("build & launch SwiftUI app (macOS or iOS)").dim()
    );
    eprintln!(
        "  {}  {}",
        style("build --watch DIR --out DIR").green(),
        style("compile .crepus templates to JSON IR").dim()
    );
    eprintln!(
        "  {}  {}",
        style("new <name>").green(),
        style("scaffold a new aurorality project").dim()
    );
    eprintln!(
        "  {}  {}",
        style("swiftgen --view FILE --out DIR --view-name NAME").green(),
        style("generate SwiftUI source from .crepus").dim()
    );
    eprintln!(
        "  {}  {}",
        style("bundle ...").green(),
        style("bundle JS + compile templates").dim()
    );
    eprintln!(
        "  {}  {}",
        style("bindgen --input DIR --output DIR").green(),
        style("generate Swift wrappers from JS exports").dim()
    );
    eprintln!();
    eprintln!("{}", style("EXAMPLES").dim());
    eprintln!(
        "  {}  {}",
        style("crepus aurora dev").green(),
        style("live preview of views/ (opens preview window)").dim()
    );
    eprintln!(
        "  {}  {}",
        style("crepus aurora run . --macos").green(),
        style("build & run current project").dim()
    );
    eprintln!(
        "  {}  {}",
        style("crepus aurora run examples/counter --macos").green(),
        style("build & run the counter example").dim()
    );
    eprintln!();
    eprintln!(
        "{}  {}",
        style("aurorality --help").dim(),
        style("for full options").dim()
    );
}