#![doc = include_str!("../docs/cli-reference.md")]
#![warn(clippy::all, clippy::pedantic)]
#![allow(clippy::missing_errors_doc)]
mod cmds;
mod permission;
use clap::{Parser, Subcommand};
use clap_verbosity_flag::Verbosity;
use clio::ClioPath;
use std::path::Path;
use tracing::{Level, instrument};
use tracing_subscriber::fmt::format::FmtSpan;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
use crate::cmds::accounts::get_current_account;
use crate::cmds::secrets::Secrets;
pub use cmds::{
accounts::Accounts, actions::Actions, app::App, assets::Assets, content::Content,
integrations::Integrations, models::Models, templates::Templates,
};
use ordinary_api::client::OrdinaryApiClient;
pub use permission::Permission;
pub(crate) static USER_AGENT: &str =
concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));
pub(crate) fn add_http(domain: &str, insecure: bool) -> String {
if insecure {
format!("http://{domain}")
} else {
format!("https://{domain}")
}
}
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
#[command(propagate_version = true)]
pub struct Cli {
#[command(subcommand)]
pub commands: Commands,
#[arg(short, long, global = true, value_parser = clap::value_parser!(ClioPath).exists().is_dir(), default_value = ".")]
pub project: ClioPath,
#[arg(long, global = true)]
pub api_domain: Option<String>,
#[arg(long, global = true, default_value_t = false)]
pub insecure: bool,
#[arg(long, global = true, default_value_t = false)]
pub danger_accept_invalid_certs: bool,
#[command(flatten)]
pub verbosity: Verbosity,
#[arg(long, global = true, default_value_t = false)]
pub pretty: bool,
}
#[derive(Subcommand, Debug)]
pub enum Commands {
New {
domain: String,
#[arg(long, default_value = ".")]
path: String,
},
Build {
#[arg(short, long, default_value_t = false)]
ignore_cache: bool,
},
Publish,
Templates {
#[command(subcommand)]
templates: Templates,
},
Content {
#[command(subcommand)]
content: Content,
},
Assets {
#[command(subcommand)]
assets: Assets,
},
Models {
#[command(subcommand)]
models: Models,
},
Actions {
#[command(subcommand)]
actions: Actions,
},
Integrations {
#[command(subcommand)]
integrations: Integrations,
},
Accounts {
#[command(subcommand)]
accounts: Accounts,
},
App {
#[command(subcommand)]
app: App,
},
Secrets {
#[command(subcommand)]
secrets: Secrets,
},
Doctor {
#[arg(short, long, value_delimiter = ',', num_args = 1..)]
fix: Option<Vec<ordinary_doctor::Fix>>,
},
}
pub fn setup(cli: &Cli) -> anyhow::Result<()> {
let pretty_layer = if cli.pretty {
Some(
tracing_subscriber::fmt::layer()
.pretty()
.with_span_events(FmtSpan::CLOSE),
)
} else {
None
};
let ugly_layer = if cli.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), ("ordinary_modify", &log_level_str), ("ordinary_build", &log_level_str), ("ordinary_doctor", &log_level_str), ("ordinary_studio", &log_level_str), ("ordinary_utils", &log_level_str), ("ordinary_auth", &log_level_str), ("ordinary_api", &log_level_str), ("ordinary_app", &log_level_str), ("ordinary_template", &log_level_str), ("ordinary_action", &log_level_str), ("ordinary_integration", &log_level_str), ("ordinary_storage", &log_level_str), ("ordinary_monitor", &log_level_str), ("tower_http", &log_level_str), ("axum::rejection", &"trace".into()), ];
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.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) -> anyhow::Result<()> {
let api_domain = cli.api_domain.as_deref();
let project = cli
.project
.to_str()
.expect("failed to get string from path");
match &cli.commands {
Commands::New { path, domain } => ordinary_modify::create_project(path, domain)?,
Commands::Build { ignore_cache } => {
if Path::new(project).join(".env").exists() {
dotenv::dotenv()?;
}
ordinary_build::build(project, *ignore_cache)?;
}
Commands::Publish => {
if Path::new(project).join(".env").exists() {
dotenv::dotenv()?;
}
ordinary_build::build(project, true)?;
let account = get_current_account(cli.insecure)?;
let client = OrdinaryApiClient::new(
&account.host,
&account.name,
api_domain,
cli.danger_accept_invalid_certs,
USER_AGENT,
)?;
client.update(project).await?;
client.write_all(project).await?;
client.upload_all(project).await?;
client.install_all(project).await?;
}
Commands::Templates { templates } => {
templates
.handle(
api_domain,
cli.danger_accept_invalid_certs,
project,
cli.insecure,
)
.await?;
}
Commands::Content { content } => {
content
.handle(
api_domain,
cli.danger_accept_invalid_certs,
project,
cli.insecure,
)
.await?;
}
Commands::Assets { assets } => {
assets
.handle(
api_domain,
cli.danger_accept_invalid_certs,
project,
cli.insecure,
)
.await?;
}
Commands::Models { models } => {
models
.handle(
api_domain,
cli.danger_accept_invalid_certs,
project,
cli.insecure,
)
.await?;
}
Commands::Actions { actions } => {
actions
.handle(
api_domain,
cli.danger_accept_invalid_certs,
project,
cli.insecure,
)
.await?;
}
Commands::Integrations { integrations } => {
integrations.handle(project)?;
}
Commands::Accounts { accounts } => {
accounts
.handle(api_domain, cli.danger_accept_invalid_certs, cli.insecure)
.await?;
}
Commands::App { app } => {
if Path::new(project).join(".env").exists() {
dotenv::dotenv()?;
}
app.handle(
api_domain,
cli.danger_accept_invalid_certs,
project,
cli.insecure,
)
.await?;
}
Commands::Secrets { secrets } => {
secrets
.handle(
api_domain,
cli.danger_accept_invalid_certs,
project,
cli.insecure,
)
.await?;
}
Commands::Doctor { fix } => {
ordinary_doctor::doctor(&fix.clone().unwrap_or(vec![]))?;
}
}
Ok(())
}