ordinary 0.11.1

Ordinary CLI
Documentation
#![doc = include_str!("../README.md")]
#![doc = include_str!("../docs/cli-reference.md")]
#![warn(clippy::all, clippy::pedantic)]
#![allow(clippy::missing_errors_doc)]

// Copyright (C) 2026 The Ordinary Authors.
//
// SPDX-License-Identifier: BSD-3-Clause

mod cmds;
mod permission;
mod units;

use clap::{Args, Parser, Subcommand};
use clap_verbosity_flag::{Verbosity, WarnLevel};
use clio::ClioPath;
use tracing::{Level, instrument};
use tracing_subscriber::fmt::format::FmtSpan;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;

use crate::cmds::host::Host;
use crate::cmds::ssg::Ssg;
use crate::cmds::utils::Utils;
pub use cmds::app::assets::Assets;
pub use cmds::app::database::Database;
pub use cmds::{accounts::Accounts, app::App};
use ordinary_host::client::{AccountMeta, OrdinaryHostClient};
pub use permission::Permission;

pub(crate) static USER_AGENT: &str =
    concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));

pub(crate) static GENERATOR: &str = concat!("Ordinary CLI ", env!("CARGO_PKG_VERSION"));

pub(crate) fn add_http(domain: &str, insecure: bool) -> String {
    if insecure {
        format!("http://{domain}")
    } else {
        format!("https://{domain}")
    }
}

#[derive(Debug, Args, Clone)]
pub struct HostCallFlags {
    /// should only be necessary with localhost or when addressing by IP
    #[arg(long)]
    pub host_domain: Option<String>,

    /// use HTTP instead of HTTPS
    #[arg(long, default_value_t = false)]
    pub insecure: bool,

    /// DANGER: only use when working with self-signed localhost certs
    #[arg(long, default_value_t = false)]
    pub danger_accept_invalid_certs: bool,
}

impl HostCallFlags {
    fn client<'a>(&'a self, account: &'a AccountMeta) -> anyhow::Result<OrdinaryHostClient<'a>> {
        let client = OrdinaryHostClient::new(
            &account.host,
            &account.name,
            self.host_domain.as_deref(),
            self.danger_accept_invalid_certs,
            USER_AGENT,
            false,
        )?;

        Ok(client)
    }
}

#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
#[command(propagate_version = true, arg_required_else_help = true)]
pub struct Cli {
    #[command(subcommand)]
    pub commands: Commands,

    #[command(flatten)]
    pub verbosity: Verbosity<WarnLevel>,

    /// whether to pretty print events to stdio
    #[arg(long, global = true, default_value_t = false)]
    pub pretty: bool,
}

#[derive(Subcommand, Debug)]
pub enum Commands {
    /// manage applications running on `ordinaryd`
    App {
        #[command(subcommand)]
        app: App,
    },
    /// manage static site configuration
    Ssg {
        /// project path
        #[arg(short, long, value_parser = clap::value_parser!(ClioPath).exists().is_dir(), default_value = ".")]
        project: ClioPath,

        #[command(subcommand)]
        ssg: Ssg,
    },
    /// manage an `ordinaryd` instance
    Host {
        #[command(flatten)]
        flags: HostCallFlags,

        #[command(subcommand)]
        host: Host,
    },
    /// ensure that all the correct system components are installed
    #[clap(visible_aliases(["dr"]))]
    Doctor {
        /// auto fix installs
        #[arg(short, long, value_delimiter = ',', num_args = 1..)]
        fix: Option<Vec<ordinary_doctor::Fix>>,
    },
    /// utility functions for aiding project development
    Utils {
        #[command(subcommand)]
        utils: Utils,
    },
}

pub fn setup(cli: &Cli) -> anyhow::Result<()> {
    let pretty_layer = if cli.pretty {
        Some(
            tracing_subscriber::fmt::layer()
                .pretty()
                .with_span_events(FmtSpan::CLOSE)
                .with_writer(std::io::stderr),
        )
    } else {
        None
    };

    let ugly_layer = if cli.pretty {
        None
    } else {
        Some(
            tracing_subscriber::fmt::layer()
                .with_span_events(FmtSpan::CLOSE)
                .with_target(false)
                .with_writer(std::io::stderr),
        )
    };

    let log_level_str = cli
        .verbosity
        .tracing_level()
        .unwrap_or(Level::INFO)
        .as_str()
        .to_ascii_lowercase();

    let directives = [
        ("ordinaryd", &log_level_str),             // daemon
        ("ordinary_modify", &log_level_str),       // modify
        ("ordinary_types", &log_level_str),        // types
        ("ordinary_build", &log_level_str),        // build
        ("ordinary_build_utils", &log_level_str),  // build_utils
        ("ordinary_test_utils", &log_level_str),   // test_utils
        ("ordinary_builtins", &log_level_str),     // builtins
        ("ordinary_assets", &log_level_str),       // assets
        ("ordinary_doctor", &log_level_str),       // doctor
        ("ordinary_server_utils", &log_level_str), // utils
        ("ordinary_auth", &log_level_str),         // auth
        ("ordinary_host", &log_level_str),         // api
        ("ordinary_app", &log_level_str),          // app
        ("ordinary_function", &log_level_str),     // functions
        ("ordinary_storage", &log_level_str),      // storage
        ("ordinary_monitor", &log_level_str),      // monitor
        ("ordinary_config", &log_level_str),       // config
        ("tower_http", &log_level_str),            // http
    ];

    let mut directives_string = format!("ordinary={log_level_str}");

    for (lib, lvl) in directives {
        directives_string = format!("{directives_string},{lib}={lvl}");
    }

    tracing_subscriber::registry()
        .with(
            tracing_subscriber::EnvFilter::try_from_default_env()
                .unwrap_or_else(|_| directives_string.into()),
        )
        .with(pretty_layer)
        .with(ugly_layer)
        .init();

    std::panic::set_hook(Box::new(|info| {
        if let Some(msg) = info.payload_as_str()
            && let Some(loc) = info.location()
        {
            tracing::error!(%loc, msg, "panic");
        } else if let Some(loc) = info.location() {
            tracing::error!(%loc, "panic");
        }
    }));

    Ok(())
}

#[allow(clippy::too_many_lines, clippy::missing_panics_doc)]
#[instrument(name = "ordinary", skip_all, err)]
pub async fn run(cli: &Cli) -> anyhow::Result<()> {
    match &cli.commands {
        Commands::Ssg { ssg, project } => {
            let project = project.to_str().expect("failed to get string from project");
            ssg.handle(project)?;
        }
        Commands::App { app } => {
            app.handle().await?;
        }
        Commands::Host { host: root, flags } => {
            root.handle(
                flags.host_domain.as_deref(),
                flags.danger_accept_invalid_certs,
                flags.insecure,
            )
            .await?;
        }
        Commands::Doctor { fix } => {
            ordinary_doctor::doctor(&fix.clone().unwrap_or(vec![]))?;
        }
        Commands::Utils { utils } => {
            utils.handle()?;
        }
    }

    Ok(())
}