ordinary 0.5.51

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

mod cmds;
mod permission;

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

pub use cmds::{
    accounts::Accounts, actions::Actions, app::App, assets::Assets, content::Content,
    integrations::Integrations, models::Models, templates::Templates,
};
pub use permission::Permission;

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

#[derive(Debug, Args)]
pub struct Globals {
    /// whether to pretty print events to stdio
    #[arg(long, default_value_t = false)]
    pub pretty: bool,

    /// project path
    #[arg(short, long, value_parser = clap::value_parser!(ClioPath).exists().is_dir(), default_value = ".")]
    pub project: ClioPath,

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

    /// should only be necessary with localhost or when addressing by IP
    #[arg(long)]
    pub host_domain: Option<String>,

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

#[derive(Parser)]
#[command(version, about, long_about = None)]
#[command(propagate_version = true)]
pub struct Cli {
    #[command(flatten)]
    pub verbosity: Verbosity,

    #[command(flatten)]
    pub globals: Globals,

    #[command(subcommand)]
    pub command: Commands,
}

#[derive(Subcommand)]
pub enum Commands {
    /// create a new Ordinary project
    New {
        /// project path
        path: String,
        /// project domain
        domain: String,
    },
    /// build your Ordinary project
    Build {
        /// build project without checking the cache
        #[arg(short, long, default_value_t = false)]
        ignore_cache: bool,
    },

    /// manage templates in your Ordinary project
    Templates {
        #[command(subcommand)]
        templates: Templates,
    },
    /// manage content in your Ordinary project
    Content {
        #[command(subcommand)]
        content: Content,
    },
    /// manage assets in your Ordinary project
    Assets {
        #[command(subcommand)]
        assets: Assets,
    },

    /// manage models in your Ordinary project
    Models {
        #[command(subcommand)]
        models: Models,
    },
    /// manage actions in your Ordinary project
    Actions {
        #[command(subcommand)]
        actions: Actions,
    },
    /// manage integrations in your Ordinary project
    Integrations {
        #[command(subcommand)]
        integrations: Integrations,
    },

    /// manage accounts connected to `ordinaryd`
    Accounts {
        #[command(subcommand)]
        accounts: Accounts,
    },
    /// manage applications running on `ordinaryd`
    App {
        #[command(subcommand)]
        app: App,
    },
    // /// list the API accounts
    // ListAdminAccounts {
    //     /// Ordinary server
    //     domain: String,
    //     account: String,
    //
    //     /// App domain
    //     app_domain: String,
    //
    //     /// for applications that need to consume stdio
    //     #[arg(long, default_value_t = false)]
    //     clean_json_to_stdio: bool,
    // },
    // /// delete an API account
    // DeleteAdminAccount {
    //     /// Ordinary server
    //     domain: String,
    //     account: String,
    //
    //     /// App domain
    //     app_domain: String,
    //
    //     /// Account name
    //     delete_account: String,
    //
    //     /// for applications that need to consume stdio
    //     #[arg(long, default_value_t = false)]
    //     clean_json_to_stdio: bool,
    // },
    //
    // /// list the apps running on an Ordinary Server
    // RootListApps { domain: String },
    // /// all Ordinary server logs
    // RootLogs {
    //     /// Ordinary server
    //     domain: String,
    //
    //     /// for applications that need to consume stdio
    //     #[arg(long, default_value_t = false)]
    //     clean_json_to_stdio: bool,
    // },
}

pub fn setup(cli: &Cli) -> Result<(), Box<dyn Error>> {
    let pretty_layer = if cli.globals.pretty {
        Some(
            tracing_subscriber::fmt::layer()
                .pretty()
                .with_span_events(FmtSpan::CLOSE),
        )
    } else {
        None
    };

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

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

    let directives = [
        ("ordinaryd", &log_level_str),        // studio
        ("itsalive", &log_level_str),         // studio
        ("hammertime", &log_level_str),       // utils
        ("krystalship", &log_level_str),      // studio
        ("hydrospanner", &log_level_str),     // utils
        ("certifiably", &log_level_str),      // certs
        ("cbwaw", &log_level_str),            // auth
        ("godfather", &log_level_str),        // api
        ("skyhook", &log_level_str),          // app
        ("stencilr", &log_level_str),         // templates
        ("startmeup", &log_level_str),        // actions
        ("gobetween", &log_level_str),        // integrations
        ("stewball", &log_level_str),         // storage
        ("insightful", &log_level_str),       // storage
        ("tower_http", &log_level_str),       // http
        ("axum::rejection", &"trace".into()), // http
    ];

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

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

    if cli.verbosity.is_present() || cli.globals.pretty {
        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(skip(cli), err)]
pub async fn run(cli: &Cli) -> Result<(), Box<dyn Error>> {
    let host_domain = cli.globals.host_domain.as_deref();

    let project = cli
        .globals
        .project
        .to_str()
        .expect("failed to get string from path");

    match &cli.command {
        Commands::New { path, domain } => ordinary_modify::create_project(path, domain)?,
        Commands::Build { ignore_cache } => ordinary_build::build(project, *ignore_cache)?,
        Commands::Templates { templates } => {
            templates
                .handle(
                    host_domain,
                    cli.globals.danger_accept_invalid_certs,
                    project,
                    cli.globals.insecure,
                )
                .await?;
        }
        Commands::Content { content } => {
            content
                .handle(
                    host_domain,
                    cli.globals.danger_accept_invalid_certs,
                    project,
                    cli.globals.insecure,
                )
                .await?;
        }
        Commands::Assets { assets } => {
            assets
                .handle(
                    host_domain,
                    cli.globals.danger_accept_invalid_certs,
                    project,
                    cli.globals.insecure,
                )
                .await?;
        }
        Commands::Models { models } => {
            models
                .handle(
                    host_domain,
                    cli.globals.danger_accept_invalid_certs,
                    project,
                    cli.globals.insecure,
                )
                .await?;
        }
        Commands::Actions { actions } => {
            actions
                .handle(
                    host_domain,
                    cli.globals.danger_accept_invalid_certs,
                    project,
                    cli.globals.insecure,
                )
                .await?;
        }
        Commands::Integrations { integrations } => {
            integrations.handle(project)?;
        }
        Commands::Accounts { accounts } => {
            accounts
                .handle(
                    host_domain,
                    cli.globals.danger_accept_invalid_certs,
                    cli.globals.insecure,
                )
                .await?;
        }
        Commands::App { app } => {
            app.handle(
                host_domain,
                cli.globals.danger_accept_invalid_certs,
                project,
                cli.globals.insecure,
            )
            .await?;
        }
    }

    Ok(())
}