1#![doc = include_str!("../docs/cli-reference.md")]
2#![warn(clippy::all, clippy::pedantic)]
3#![allow(clippy::missing_errors_doc)]
4
5mod cmds;
10mod permission;
11mod units;
12
13use clap::{Parser, Subcommand};
14use clap_verbosity_flag::{Verbosity, WarnLevel};
15use clio::ClioPath;
16use std::path::Path;
17use tracing::{Level, instrument};
18use tracing_subscriber::fmt::format::FmtSpan;
19use tracing_subscriber::layer::SubscriberExt;
20use tracing_subscriber::util::SubscriberInitExt;
21
22use crate::cmds::accounts::get_current_account;
23use crate::cmds::secrets::Secrets;
24
25use crate::cmds::root::Root;
26use crate::cmds::ssg::Ssg;
27use crate::cmds::utils::Utils;
28pub use cmds::{
29 accounts::Accounts, actions::Actions, app::App, assets::Assets, content::Content,
30 integrations::Integrations, models::Models, templates::Templates,
31};
32use ordinary_api::client::OrdinaryApiClient;
33use ordinary_config::OrdinaryConfig;
34use ordinaryd::{AppApi, GlobalArgs};
35pub use permission::Permission;
36
37pub(crate) static USER_AGENT: &str =
38 concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));
39
40pub(crate) static GENERATOR: &str = concat!("Ordinary CLI ", env!("CARGO_PKG_VERSION"));
41
42pub(crate) fn add_http(domain: &str, insecure: bool) -> String {
43 if insecure {
44 format!("http://{domain}")
45 } else {
46 format!("https://{domain}")
47 }
48}
49
50#[derive(Parser, Debug)]
51#[command(version, about, long_about = None)]
52#[command(propagate_version = true)]
53pub struct Cli {
54 #[command(subcommand)]
55 pub commands: Commands,
56
57 #[arg(short, long, global = true, value_parser = clap::value_parser!(ClioPath).exists().is_dir(), default_value = ".")]
59 pub project: ClioPath,
60
61 #[arg(long, global = true)]
63 pub api_domain: Option<String>,
64
65 #[arg(long, global = true, default_value_t = false)]
67 pub insecure: bool,
68
69 #[arg(long, global = true, default_value_t = false)]
71 pub danger_accept_invalid_certs: bool,
72
73 #[command(flatten)]
74 pub verbosity: Verbosity<WarnLevel>,
75
76 #[arg(long, global = true, default_value_t = false)]
78 pub pretty: bool,
79}
80
81#[derive(Subcommand, Debug)]
82pub enum Commands {
83 New {
85 domain: String,
87 #[arg(long, default_value = ".")]
89 path: String,
90 },
91 Ssg {
93 #[command(subcommand)]
94 ssg: Ssg,
95 },
96 Build {
100 #[arg(short, long, default_value_t = false)]
102 ignore_cache: bool,
103 },
104 Start {
106 #[command(flatten)]
107 app_api: AppApi,
108
109 #[command(flatten)]
110 global_args: GlobalArgs,
111
112 #[arg(short, long, default_value_t = false)]
118 disable_defaults: bool,
119 },
120 Publish,
123
124 Templates {
126 #[command(subcommand)]
127 templates: Templates,
128 },
129 Content {
131 #[command(subcommand)]
132 content: Content,
133 },
134 Assets {
136 #[command(subcommand)]
137 assets: Assets,
138 },
139
140 Models {
142 #[command(subcommand)]
143 models: Models,
144 },
145 Actions {
147 #[command(subcommand)]
148 actions: Actions,
149 },
150 Integrations {
152 #[command(subcommand)]
153 integrations: Integrations,
154 },
155
156 Accounts {
158 #[command(subcommand)]
159 accounts: Accounts,
160 },
161 App {
163 #[command(subcommand)]
164 app: App,
165 },
166 Secrets {
168 #[command(subcommand)]
169 secrets: Secrets,
170 },
171 Root {
172 #[command(subcommand)]
173 root: Root,
174 },
175 Doctor {
177 #[arg(short, long, value_delimiter = ',', num_args = 1..)]
179 fix: Option<Vec<ordinary_doctor::Fix>>,
180 },
181 Utils {
183 #[command(subcommand)]
184 utils: Utils,
185 },
186}
187
188pub fn setup(cli: &Cli) -> anyhow::Result<()> {
189 let pretty_layer = if cli.pretty {
190 Some(
191 tracing_subscriber::fmt::layer()
192 .pretty()
193 .with_span_events(FmtSpan::CLOSE)
194 .with_writer(std::io::stderr),
195 )
196 } else {
197 None
198 };
199
200 let ugly_layer = if cli.pretty {
201 None
202 } else {
203 Some(
204 tracing_subscriber::fmt::layer()
205 .with_span_events(FmtSpan::CLOSE)
206 .with_target(false)
207 .with_writer(std::io::stderr),
208 )
209 };
210
211 let log_level_str = cli
212 .verbosity
213 .tracing_level()
214 .unwrap_or(Level::INFO)
215 .as_str()
216 .to_ascii_lowercase();
217
218 let directives = [
219 ("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), ("ordinary_config", &log_level_str), ("tower_http", &log_level_str), ];
236
237 let mut directives_string = format!("ordinary={}", &log_level_str);
238
239 for (lib, lvl) in directives {
240 directives_string = format!("{directives_string},{lib}={lvl}");
241 }
242
243 tracing_subscriber::registry()
244 .with(
245 tracing_subscriber::EnvFilter::try_from_default_env()
246 .unwrap_or_else(|_| directives_string.into()),
247 )
248 .with(pretty_layer)
249 .with(ugly_layer)
250 .init();
251
252 std::panic::set_hook(Box::new(|info| {
253 if let Some(msg) = info.payload_as_str()
254 && let Some(loc) = info.location()
255 {
256 tracing::error!(%loc, msg, "panic");
257 } else if let Some(loc) = info.location() {
258 tracing::error!(%loc, "panic");
259 }
260 }));
261
262 Ok(())
263}
264
265#[allow(clippy::too_many_lines, clippy::missing_panics_doc)]
266#[instrument(name = "ordinary", skip_all, err)]
267pub async fn run(cli: &Cli) -> anyhow::Result<()> {
268 let api_domain = cli.api_domain.as_deref();
269
270 let project = cli
271 .project
272 .to_str()
273 .expect("failed to get string from path");
274
275 match &cli.commands {
276 Commands::New { path, domain } => ordinary_modify::project::new(path, domain)?,
277 Commands::Ssg { ssg } => {
278 ssg.handle(project)?;
279 }
280 Commands::Build { ignore_cache } => {
281 let env_file = Path::new(project).join(".env");
282 if env_file.exists() {
283 dotenv::from_path(env_file)?;
284 }
285
286 ordinary_build::build(project, *ignore_cache, "ordinary")?;
287 }
288 Commands::Publish => {
289 let env_file = Path::new(project).join(".env");
290 if env_file.exists() {
291 dotenv::from_path(env_file)?;
292 }
293
294 ordinary_build::build(project, true, GENERATOR)?;
295
296 let account = get_current_account(cli.insecure)?;
297 let client = OrdinaryApiClient::new(
298 &account.host,
299 &account.name,
300 api_domain,
301 cli.danger_accept_invalid_certs,
302 USER_AGENT,
303 true,
304 )?;
305
306 let config = OrdinaryConfig::get(project)?;
307
308 client.deploy(project).await?;
309
310 if config.content.is_some() {
311 client.update(project).await?;
312 }
313
314 if config.assets.is_some() {
315 client.write_all(project).await?;
316 }
317
318 if config.templates.is_some() {
319 client.upload_all(project).await?;
320 }
321
322 if config.actions.is_some() {
323 client.install_all(project).await?;
324 }
325 }
326 Commands::Templates { templates } => {
327 templates
328 .handle(
329 api_domain,
330 cli.danger_accept_invalid_certs,
331 project,
332 cli.insecure,
333 )
334 .await?;
335 }
336 Commands::Content { content } => {
337 content
338 .handle(
339 api_domain,
340 cli.danger_accept_invalid_certs,
341 project,
342 cli.insecure,
343 )
344 .await?;
345 }
346 Commands::Assets { assets } => {
347 assets
348 .handle(
349 api_domain,
350 cli.danger_accept_invalid_certs,
351 project,
352 cli.insecure,
353 )
354 .await?;
355 }
356 Commands::Models { models } => {
357 models
358 .handle(
359 api_domain,
360 cli.danger_accept_invalid_certs,
361 project,
362 cli.insecure,
363 )
364 .await?;
365 }
366 Commands::Actions { actions } => {
367 actions
368 .handle(
369 api_domain,
370 cli.danger_accept_invalid_certs,
371 project,
372 cli.insecure,
373 )
374 .await?;
375 }
376 Commands::Integrations { integrations } => {
377 integrations.handle(project)?;
378 }
379 Commands::Accounts { accounts } => {
380 accounts
381 .handle(api_domain, cli.danger_accept_invalid_certs, cli.insecure)
382 .await?;
383 }
384 Commands::App { app } => {
385 let env_file = Path::new(project).join(".env");
386 if env_file.exists() {
387 dotenv::from_path(env_file)?;
388 }
389
390 app.handle(
391 api_domain,
392 cli.danger_accept_invalid_certs,
393 project,
394 cli.insecure,
395 )
396 .await?;
397 }
398 Commands::Secrets { secrets } => {
399 secrets
400 .handle(
401 api_domain,
402 cli.danger_accept_invalid_certs,
403 project,
404 cli.insecure,
405 )
406 .await?;
407 }
408 Commands::Root { root } => {
409 root.handle(api_domain, cli.danger_accept_invalid_certs, cli.insecure)
410 .await?;
411 }
412 Commands::Doctor { fix } => {
413 ordinary_doctor::doctor(&fix.clone().unwrap_or(vec![]))?;
414 }
415 Commands::Utils { utils } => {
416 utils.handle()?;
417 }
418 Commands::Start { .. } => unreachable!("checked in main.rs"),
419 }
420
421 Ok(())
422}