Skip to main content

ordinary/
lib.rs

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
6// Copyright (C) 2026 The Ordinary Authors.
7//
8// SPDX-License-Identifier: BSD-3-Clause
9
10mod 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    /// project path
52    #[arg(short, long, global = true, value_parser = clap::value_parser!(ClioPath).exists().is_dir(), default_value = ".")]
53    pub project: ClioPath,
54
55    /// should only be necessary with localhost or when addressing by IP
56    #[arg(long, global = true)]
57    pub api_domain: Option<String>,
58
59    /// use HTTP instead of HTTPS
60    #[arg(long, global = true, default_value_t = false)]
61    pub insecure: bool,
62
63    /// DANGER: only use when working with self-signed localhost certs
64    #[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    /// whether to pretty print events to stdio
71    #[arg(long, global = true, default_value_t = false)]
72    pub pretty: bool,
73}
74
75#[derive(Subcommand, Debug)]
76pub enum Commands {
77    /// manage applications running on `ordinaryd`
78    App {
79        #[command(subcommand)]
80        app: App,
81    },
82    /// manage static site configuration
83    Ssg {
84        #[command(subcommand)]
85        ssg: Ssg,
86    },
87    /// manage an `ordinaryd` instance
88    Host {
89        #[command(subcommand)]
90        host: Host,
91    },
92    /// ensure that all the correct system components are installed
93    #[clap(visible_aliases(["dr"]))]
94    Doctor {
95        /// auto fix installs
96        #[arg(short, long, value_delimiter = ',', num_args = 1..)]
97        fix: Option<Vec<ordinary_doctor::Fix>>,
98    },
99    /// utility functions for aiding project development
100    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),             // daemon
138        ("ordinary_modify", &log_level_str),       // modify
139        ("ordinary_types", &log_level_str),        // types
140        ("ordinary_build", &log_level_str),        // build
141        ("ordinary_build_utils", &log_level_str),  // build_utils
142        ("ordinary_test_utils", &log_level_str),   // test_utils
143        ("ordinary_builtins", &log_level_str),     // builtins
144        ("ordinary_assets", &log_level_str),       // assets
145        ("ordinary_doctor", &log_level_str),       // doctor
146        ("ordinary_server_utils", &log_level_str), // utils
147        ("ordinary_auth", &log_level_str),         // auth
148        ("ordinary_api", &log_level_str),          // api
149        ("ordinary_app", &log_level_str),          // app
150        ("ordinary_function", &log_level_str),     // functions
151        ("ordinary_storage", &log_level_str),      // storage
152        ("ordinary_monitor", &log_level_str),      // monitor
153        ("ordinary_config", &log_level_str),       // config
154        ("tower_http", &log_level_str),            // http
155    ];
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}