#![doc = include_str!("../README.md")]
#![doc = include_str!("../docs/cli-reference.md")]
#![warn(clippy::all, clippy::pedantic)]
#![allow(clippy::missing_errors_doc)]
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 {
#[arg(long)]
pub host_domain: Option<String>,
#[arg(long, default_value_t = false)]
pub insecure: bool,
#[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>,
#[arg(long, global = true, default_value_t = false)]
pub pretty: bool,
}
#[derive(Subcommand, Debug)]
pub enum Commands {
App {
#[command(subcommand)]
app: App,
},
Ssg {
#[arg(short, long, value_parser = clap::value_parser!(ClioPath).exists().is_dir(), default_value = ".")]
project: ClioPath,
#[command(subcommand)]
ssg: Ssg,
},
Host {
#[command(flatten)]
flags: HostCallFlags,
#[command(subcommand)]
host: Host,
},
#[clap(visible_aliases(["dr"]))]
Doctor {
#[arg(short, long, value_delimiter = ',', num_args = 1..)]
fix: Option<Vec<ordinary_doctor::Fix>>,
},
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), ("ordinary_modify", &log_level_str), ("ordinary_types", &log_level_str), ("ordinary_build", &log_level_str), ("ordinary_build_utils", &log_level_str), ("ordinary_test_utils", &log_level_str), ("ordinary_builtins", &log_level_str), ("ordinary_assets", &log_level_str), ("ordinary_doctor", &log_level_str), ("ordinary_server_utils", &log_level_str), ("ordinary_auth", &log_level_str), ("ordinary_host", &log_level_str), ("ordinary_app", &log_level_str), ("ordinary_function", &log_level_str), ("ordinary_storage", &log_level_str), ("ordinary_monitor", &log_level_str), ("ordinary_config", &log_level_str), ("tower_http", &log_level_str), ];
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(())
}