1#![doc = include_str!("../README.md")]
2#![doc = include_str!("../docs/cli-reference.md")]
3#![warn(clippy::all, clippy::pedantic)]
4#![allow(clippy::missing_errors_doc)]
5
6mod cmds;
11mod permission;
12mod units;
13
14use clap::{Parser, Subcommand};
15use clap_verbosity_flag::{Verbosity, WarnLevel};
16use clio::ClioPath;
17use std::path::Path;
18use tracing::{Level, instrument};
19use tracing_subscriber::fmt::format::FmtSpan;
20use tracing_subscriber::layer::SubscriberExt;
21use tracing_subscriber::util::SubscriberInitExt;
22
23use crate::cmds::host::Host;
24use crate::cmds::ssg::Ssg;
25use crate::cmds::utils::Utils;
26pub use cmds::app::assets::Assets;
27pub use cmds::app::database::Database;
28pub use cmds::{accounts::Accounts, app::App};
29pub use permission::Permission;
30
31pub(crate) static USER_AGENT: &str =
32 concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));
33
34pub(crate) static GENERATOR: &str = concat!("Ordinary CLI ", env!("CARGO_PKG_VERSION"));
35
36pub(crate) fn add_http(domain: &str, insecure: bool) -> String {
37 if insecure {
38 format!("http://{domain}")
39 } else {
40 format!("https://{domain}")
41 }
42}
43
44#[derive(Parser, Debug)]
45#[command(version, about, long_about = None)]
46#[command(propagate_version = true, arg_required_else_help = true)]
47pub struct Cli {
48 #[command(subcommand)]
49 pub commands: Commands,
50
51 #[arg(short, long, global = true, value_parser = clap::value_parser!(ClioPath).exists().is_dir(), default_value = ".")]
53 pub project: ClioPath,
54
55 #[arg(long, global = true)]
57 pub api_domain: Option<String>,
58
59 #[arg(long, global = true, default_value_t = false)]
61 pub insecure: bool,
62
63 #[arg(long, global = true, default_value_t = false)]
65 pub danger_accept_invalid_certs: bool,
66
67 #[command(flatten)]
68 pub verbosity: Verbosity<WarnLevel>,
69
70 #[arg(long, global = true, default_value_t = false)]
72 pub pretty: bool,
73}
74
75#[derive(Subcommand, Debug)]
76pub enum Commands {
77 App {
79 #[command(subcommand)]
80 app: App,
81 },
82 Ssg {
84 #[command(subcommand)]
85 ssg: Ssg,
86 },
87 Host {
89 #[command(subcommand)]
90 host: Host,
91 },
92 #[clap(visible_aliases(["dr"]))]
94 Doctor {
95 #[arg(short, long, value_delimiter = ',', num_args = 1..)]
97 fix: Option<Vec<ordinary_doctor::Fix>>,
98 },
99 Utils {
101 #[command(subcommand)]
102 utils: Utils,
103 },
104}
105
106pub fn setup(cli: &Cli) -> anyhow::Result<()> {
107 let pretty_layer = if cli.pretty {
108 Some(
109 tracing_subscriber::fmt::layer()
110 .pretty()
111 .with_span_events(FmtSpan::CLOSE)
112 .with_writer(std::io::stderr),
113 )
114 } else {
115 None
116 };
117
118 let ugly_layer = if cli.pretty {
119 None
120 } else {
121 Some(
122 tracing_subscriber::fmt::layer()
123 .with_span_events(FmtSpan::CLOSE)
124 .with_target(false)
125 .with_writer(std::io::stderr),
126 )
127 };
128
129 let log_level_str = cli
130 .verbosity
131 .tracing_level()
132 .unwrap_or(Level::INFO)
133 .as_str()
134 .to_ascii_lowercase();
135
136 let directives = [
137 ("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_api", &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), ];
156
157 let mut directives_string = format!("ordinary={log_level_str}");
158
159 for (lib, lvl) in directives {
160 directives_string = format!("{directives_string},{lib}={lvl}");
161 }
162
163 tracing_subscriber::registry()
164 .with(
165 tracing_subscriber::EnvFilter::try_from_default_env()
166 .unwrap_or_else(|_| directives_string.into()),
167 )
168 .with(pretty_layer)
169 .with(ugly_layer)
170 .init();
171
172 std::panic::set_hook(Box::new(|info| {
173 if let Some(msg) = info.payload_as_str()
174 && let Some(loc) = info.location()
175 {
176 tracing::error!(%loc, msg, "panic");
177 } else if let Some(loc) = info.location() {
178 tracing::error!(%loc, "panic");
179 }
180 }));
181
182 Ok(())
183}
184
185#[allow(clippy::too_many_lines, clippy::missing_panics_doc)]
186#[instrument(name = "ordinary", skip_all, err)]
187pub async fn run(cli: &Cli) -> anyhow::Result<()> {
188 let api_domain = cli.api_domain.as_deref();
189
190 let project = cli
191 .project
192 .to_str()
193 .expect("failed to get string from path");
194
195 match &cli.commands {
196 Commands::Ssg { ssg } => {
197 ssg.handle(project)?;
198 }
199 Commands::App { app } => {
200 let env_file = Path::new(project).join(".env");
201 if env_file.exists() {
202 dotenvy::from_path(env_file)?;
203 }
204
205 app.handle(
206 api_domain,
207 cli.danger_accept_invalid_certs,
208 project,
209 cli.insecure,
210 )
211 .await?;
212 }
213 Commands::Host { host: root } => {
214 root.handle(api_domain, cli.danger_accept_invalid_certs, cli.insecure)
215 .await?;
216 }
217 Commands::Doctor { fix } => {
218 ordinary_doctor::doctor(&fix.clone().unwrap_or(vec![]))?;
219 }
220 Commands::Utils { utils } => {
221 utils.handle()?;
222 }
223 }
224
225 Ok(())
226}