arqen 0.8.0

Backend infrastructure for agent-ready applications
Documentation
pub mod build;
pub mod check;
pub mod doc;
pub mod doctor;
pub mod exit;
pub mod format;
pub mod generate;
pub mod lint;
pub mod output;
pub mod serve;
pub mod test;
pub mod thingd_schema;

use clap::{Parser, Subcommand, ValueEnum};

use output::Output;

#[derive(Parser)]
#[command(
    name = "arqen",
    version,
    about = "Arqen CLI for generating and running applications"
)]
pub struct Cli {
    #[command(subcommand)]
    command: Commands,

    #[arg(long, global = true)]
    json: bool,
    #[arg(long, global = true)]
    verbose: bool,
    #[arg(long, global = true)]
    quiet: bool,
    #[arg(long, global = true, default_value = "auto")]
    color: ColorChoice,
}

#[derive(Clone, Copy, Debug, ValueEnum)]
enum ColorChoice {
    Auto,
    Always,
    Never,
}

#[derive(Subcommand)]
pub enum Commands {
    /// Generate a new Arqen application
    New {
        /// Project name
        name: String,
    },
    /// Generate code scaffolding
    Generate {
        #[command(subcommand)]
        kind: GenerateKind,
    },
    /// Run the application in development mode
    Dev {
        /// Host to bind to
        #[arg(long)]
        host: Option<String>,
        /// Port to bind to
        #[arg(short, long)]
        port: Option<u16>,
        /// Log level
        #[arg(short, long)]
        log: Option<String>,
        /// Storage mode
        #[arg(short, long)]
        storage: Option<String>,
        /// Log output format
        #[arg(long, value_enum)]
        log_format: Option<crate::config::LogFormat>,
        /// Config file path
        #[arg(long = "file")]
        config_file: Option<String>,
    },
    /// Run the application (aliases: run, serve)
    #[command(visible_alias = "run", alias = "serve")]
    Start {
        /// Host to bind to
        #[arg(long)]
        host: Option<String>,
        /// Port to bind to
        #[arg(short, long)]
        port: Option<u16>,
        /// Log level
        #[arg(short, long)]
        log: Option<String>,
        /// Storage mode
        #[arg(short, long)]
        storage: Option<String>,
        /// Log output format (production defaults to JSON)
        #[arg(long, value_enum)]
        log_format: Option<crate::config::LogFormat>,
        /// Config file path
        #[arg(long = "file")]
        config_file: Option<String>,
    },
    /// Run and supervise local dev services defined in arqen.toml
    Up {
        /// Names of services to start (defaults to all)
        #[arg(value_name = "SERVICE")]
        services: Vec<String>,
        /// Config file to read dev services from
        #[arg(long, default_value = "arqen.toml")]
        file: std::path::PathBuf,
        /// Print the services that would be started without running them
        #[arg(long)]
        dry_run: bool,
    },
    /// Run checks on the current environment
    Check,
    /// Diagnose Rust, thingd, Docker, and environment setup
    Doctor,
    /// Run lint checks (fmt + clippy)
    Lint,
    /// Auto-format code
    Format,
    /// Run tests
    Test {
        /// Build and run in release mode
        #[arg(long)]
        release: bool,
    },
    /// Build the project
    Build {
        /// Build in release mode
        #[arg(long)]
        release: bool,
    },
    /// Generate documentation
    Doc,
    /// Validate Thingd schemas and inspect a remote Thingd instance.
    Thingd {
        #[command(subcommand)]
        command: ThingdCommand,
    },
}

#[derive(Subcommand)]
pub enum ThingdCommand {
    /// Validate a local `.thingd` schema file.
    SchemaValidate {
        path: std::path::PathBuf,
        /// Optional Thingd URL for authoritative syntax validation.
        #[arg(long)]
        url: Option<String>,
        #[arg(long)]
        token: Option<String>,
    },
    /// Read the current remote schema and migration history.
    SchemaRemote {
        url: String,
        #[arg(long)]
        token: Option<String>,
    },
}

#[derive(Subcommand)]
pub enum GenerateKind {
    /// Generate a new module
    Module {
        /// Module name
        name: String,
    },
    /// Generate a new tool
    Tool {
        /// Tool name
        name: String,
    },
    /// Generate a new job handler
    Job {
        /// Job name
        name: String,
    },
}

fn dispatch(cli: &Cli, output: &Output) -> anyhow::Result<()> {
    match &cli.command {
        Commands::New { name } => {
            generate::generate_project(name, output)?;
        }
        Commands::Generate { kind } => match kind {
            GenerateKind::Module { name } => generate::generate_module(name, output)?,
            GenerateKind::Tool { name } => generate::generate_tool(name, output)?,
            GenerateKind::Job { name } => generate::generate_job(name, output)?,
        },
        Commands::Dev {
            host,
            port,
            log,
            storage,
            config_file,
            log_format,
        } => {
            let code = serve::serve_dev(
                config_file.as_deref(),
                host.as_deref(),
                *port,
                log.as_deref(),
                storage.as_deref(),
                log_format.as_ref(),
                output,
            );
            if code != exit::SUCCESS {
                std::process::exit(code);
            }
        }
        Commands::Start {
            host,
            port,
            log,
            storage,
            config_file,
            log_format,
        } => {
            let code = serve::serve_start(
                config_file.as_deref(),
                host.as_deref(),
                *port,
                log.as_deref(),
                storage.as_deref(),
                log_format.as_ref(),
                output,
            );
            if code != exit::SUCCESS {
                std::process::exit(code);
            }
        }
        Commands::Up {
            services,
            file,
            dry_run,
        } => {
            let rt = tokio::runtime::Runtime::new().map_err(|e| anyhow::anyhow!(e))?;
            rt.block_on(crate::dev::run_up(file, services, *dry_run))?;
        }
        Commands::Check => {
            let code = check::run_check(output);
            if code != exit::SUCCESS {
                std::process::exit(code);
            }
        }
        Commands::Doctor => {
            let code = doctor::run_doctor(output);
            if code != exit::SUCCESS {
                std::process::exit(code);
            }
        }
        Commands::Lint => {
            let code = lint::run_lint(output);
            if code != exit::SUCCESS {
                std::process::exit(code);
            }
        }
        Commands::Format => {
            let code = format::run_format(output);
            if code != exit::SUCCESS {
                std::process::exit(code);
            }
        }
        Commands::Test { release } => {
            let code = test::run_test(*release, output);
            if code != exit::SUCCESS {
                std::process::exit(code);
            }
        }
        Commands::Build { release } => {
            let code = build::run_build(*release, output);
            if code != exit::SUCCESS {
                std::process::exit(code);
            }
        }
        Commands::Doc => {
            let code = doc::run_doc(output);
            if code != exit::SUCCESS {
                std::process::exit(code);
            }
        }
        Commands::Thingd { command } => {
            let code = thingd_schema::run(command, output);
            if code != exit::SUCCESS {
                std::process::exit(code);
            }
        }
    }
    Ok(())
}

pub fn run() -> i32 {
    let cli = Cli::parse();
    let color = match cli.color {
        ColorChoice::Auto => "auto",
        ColorChoice::Always => "always",
        ColorChoice::Never => "never",
    };
    let output = Output::from_args(cli.json, cli.quiet, cli.verbose, color);

    match dispatch(&cli, &output) {
        Ok(()) => exit::SUCCESS,
        Err(e) => {
            output.print_error(&e.to_string());
            exit::RUNTIME
        }
    }
}