nativ 0.3.0

Nativ CLI — compile .nativ DSL to real SwiftUI and Jetpack Compose
mod build;
mod check;
mod ci;
mod dev;
mod format;
mod init;
mod ota;
mod preview;
mod submit;
mod version;
mod watch;

use clap::{Parser, Subcommand};
use std::io::{self, IsTerminal, Write};

#[derive(Parser)]
#[command(
    name = "nativ",
    about = "Compile-time native UI compiler: .nativ -> SwiftUI + Jetpack Compose",
    version = env!("CARGO_PKG_VERSION"),
    after_help = "Learn more: https://nativ.dev"
)]
pub struct Cli {
    #[command(subcommand)]
    pub command: Option<Command>,

    /// Show verbose output
    #[arg(long, global = true)]
    pub verbose: bool,

    /// Quiet mode (errors only)
    #[arg(long, global = true)]
    pub quiet: bool,
}

#[derive(Subcommand)]
pub enum Command {
    /// Create a new Nativ project
    Init(init::InitArgs),

    /// Compile .nativ files -> .swift/.kt
    Build(build::BuildArgs),

    /// Validate .nativ files without generating code
    Check(check::CheckArgs),

    /// Generate CI/CD helper files
    Ci(ci::CiArgs),

    /// Watch for changes and rebuild automatically
    Watch(watch::WatchArgs),

    /// Dev server: watch for changes and classify edit type
    Dev(dev::DevArgs),

    /// Format .nativ files
    Format(format::FormatArgs),

    /// Open a live preview in the browser
    Preview(preview::PreviewArgs),

    /// Submit builds through user-owned Fastlane lanes
    Submit(submit::SubmitArgs),

    /// Sign and pack Nativ Live Updates bundles (asset/i18n/config OTA)
    Ota(ota::OtaArgs),

    /// Show or update version information
    Version(version::VersionArgs),
}

pub fn run(cli: Cli) -> Result<(), Box<dyn std::error::Error>> {
    let command = match cli.command {
        Some(command) => command,
        None if !cli.quiet && io::stdin().is_terminal() && io::stdout().is_terminal() => {
            prompt_root_command()?
        }
        None => return Err("No command selected. Run `nativ --help`.".into()),
    };

    match command {
        Command::Init(args) => init::run(args, cli.verbose),
        Command::Build(args) => build::run(args, cli.verbose, cli.quiet),
        Command::Check(args) => check::run(args, cli.verbose),
        Command::Ci(args) => ci::run(args),
        Command::Watch(args) => watch::run(args, cli.verbose),
        Command::Dev(args) => dev::run(args, cli.verbose, cli.quiet),
        Command::Format(args) => format::run(args, cli.verbose),
        Command::Preview(args) => preview::run(args),
        Command::Submit(args) => submit::run(args),
        Command::Ota(args) => ota::run(args),
        Command::Version(args) => version::run(args),
    }
}

fn prompt_root_command() -> Result<Command, Box<dyn std::error::Error>> {
    println!("Nativ command:");
    println!("  1) Init project");
    println!("  2) Build");
    println!("  3) Native app preview");
    println!("  4) Browser preview");
    println!("  5) Check");
    println!("  6) Format");
    println!("  7) Version");
    print!("Select: ");
    io::stdout().flush()?;

    let mut input = String::new();
    io::stdin().read_line(&mut input)?;
    parse_root_menu_choice(input.trim()).ok_or_else(|| "Invalid command selection".into())
}

fn parse_root_menu_choice(choice: &str) -> Option<Command> {
    Some(match choice {
        "1" => Command::Init(init::InitArgs { name: None }),
        "2" => Command::Build(build::BuildArgs {
            ios: false,
            android: false,
            web: false,
            dev: false,
            dir: ".".to_string(),
        }),
        "3" => Command::Dev(dev::DevArgs {
            dir: ".".to_string(),
            ios: false,
            android: false,
            dev_shell: false,
            dev_shell_port: 0,
            dev_shell_compile: false,
        }),
        "4" => Command::Preview(preview::PreviewArgs {
            path: ".".to_string(),
            out: None,
            open: true,
            watch: true,
            serve: true,
            stdin: false,
            json: false,
            port: 4173,
        }),
        "5" => Command::Check(check::CheckArgs {
            dir: ".".to_string(),
        }),
        "6" => Command::Format(format::FormatArgs {
            check: false,
            dir: ".".to_string(),
        }),
        "7" => Command::Version(version::VersionArgs { command: None }),
        _ => return None,
    })
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn root_menu_maps_build_and_native_preview() {
        assert!(matches!(
            parse_root_menu_choice("2"),
            Some(Command::Build(build::BuildArgs { .. }))
        ));
        assert!(matches!(
            parse_root_menu_choice("3"),
            Some(Command::Dev(dev::DevArgs { .. }))
        ));
    }

    #[test]
    fn root_menu_rejects_unknown_choice() {
        assert!(parse_root_menu_choice("").is_none());
        assert!(parse_root_menu_choice("x").is_none());
    }
}