ordinary 0.6.0-pre.9

Ordinary CLI
Documentation
// Copyright (C) 2026 Ordinary Labs, LLC.
//
// SPDX-License-Identifier: AGPL-3.0-only

use crate::cmds::accounts::get_current_account;
use clap::Subcommand;
use clio::ClioPath;
use ordinary_api::client::OrdinaryApiClient;
use serde_json::Value;

#[derive(Clone, Debug)]
pub enum LogFormat {
    /// all logs that match query
    All,
    /// top logs that match query
    Top,
    /// count of logs that match query
    Count,
}

impl LogFormat {
    fn as_str(&self) -> &'static str {
        match self {
            Self::All => "all",
            Self::Top => "top",
            Self::Count => "count",
        }
    }
}

impl clap::ValueEnum for LogFormat {
    fn value_variants<'a>() -> &'a [Self] {
        &[Self::All, Self::Top, Self::Count]
    }

    fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
        match self {
            Self::All => Some(clap::builder::PossibleValue::new("all")),
            Self::Top => Some(clap::builder::PossibleValue::new("top")),
            Self::Count => Some(clap::builder::PossibleValue::new("count")),
        }
    }
}

#[derive(Subcommand, Debug)]
pub enum App {
    /// deploy a new application
    Deploy,
    /// push a configuration level change
    Patch,
    /// push a configuration change which will
    /// modify the shape of your data stores
    ///
    /// (i.e. a structural change to model or content definitions)
    Migrate,
    // /// request the current host "bridge" with an additional
    // /// `ordinaryd` instance, in order to run your application
    // /// across two hosts.
    // Bridge {
    //     /// url for another `ordinaryd` instance
    //     remote: String,
    // },
    /// kill a running instance of the application
    Kill,
    /// restart a running instance of the application
    Restart,
    // /// fully erase all content of the application from the host
    // Erase,
    /// download an application as static files
    Download {
        /// url override. will use project domain by default
        #[arg(short, long)]
        url: Option<String>,
        /// where to store downloaded files
        #[arg(short, long, value_parser = clap::value_parser!(ClioPath).exists().is_dir(), default_value = "out")]
        out: ClioPath,
    },
    /// query application logs
    Logs {
        /// format
        format: LogFormat,

        /// [reference](https://quickwit.io/docs/reference/query-language)
        query: String,

        #[arg(long)]
        /// limit (when using 'top' format)
        limit: Option<usize>,

        /// for applications that need to consume stdio or pipe to `jq`
        #[arg(long, default_value_t = false)]
        json: bool,
    },
    /// manage application accounts
    Accounts {
        #[command(subcommand)]
        accounts: Accounts,
    },
}

#[derive(Subcommand, Debug)]
pub enum Accounts {
    /// list application accounts
    List {
        /// for applications that need to consume stdio or pipe to `jq`
        #[arg(long, default_value_t = false)]
        json: bool,
    },
}

impl App {
    #[allow(clippy::missing_panics_doc)]
    pub async fn handle(
        &self,
        api_domain: Option<&str>,
        accept_invalid_certs: bool,
        project: &str,
        insecure: bool,
    ) -> anyhow::Result<()> {
        let account = get_current_account(insecure)?;
        let client = OrdinaryApiClient::new(
            &account.host,
            &account.name,
            api_domain,
            accept_invalid_certs,
            crate::USER_AGENT,
        )?;

        match self {
            Self::Deploy => {
                let port = client.deploy(project).await?;
                tracing::info!("running on port: {port}");
            }
            Self::Patch => {
                let port = client.patch(project).await?;
                tracing::info!("running on port: {port}");
            }
            Self::Migrate => {
                println!("todo: migrate");
            }
            // Self::Bridge { remote } => {
            //     tracing::info!("todo: bridge {project} to {remote}");
            // }
            Self::Kill => {
                client.kill(project).await?;
            }
            Self::Restart => {
                let port = client.restart(project).await?;
                tracing::info!("running on port: {port}");
            }
            // Self::Erase => {
            //     tracing::info!("todo: erase {project}");
            // }
            Self::Download { url, out } => {
                let out = out.to_str().expect("failed to convert to string");

                if let Some(url) = url {
                    ordinary_download::download(project, url, out).await?;
                } else {
                    ordinary_download::download(
                        project,
                        &format!("https://{}", account.project),
                        out,
                    )
                    .await?;
                }
            }
            Self::Logs {
                format,
                query,
                limit,
                json,
            } => {
                let res = client
                    .app_logs(project, query, format.as_str(), limit)
                    .await?;

                if matches!(format.as_str(), "all" | "top") {
                    if json == &true {
                        print!("{res}");
                    } else {
                        match res {
                            Value::Array(lines) => {
                                for line in lines {
                                    tracing::info!(%line);
                                }
                            }
                            _ => unreachable!(),
                        }
                    }
                } else if json == &true {
                    print!("{res}");
                } else {
                    match res {
                        Value::Number(count) => {
                            tracing::info!(count = %count);
                        }
                        _ => unreachable!(),
                    }
                }
            }
            Self::Accounts { accounts } => match accounts {
                Accounts::List { json } => {
                    let res = client.app_accounts_list(project).await?;

                    if json == &true {
                        print!("{res}");
                    } else {
                        let accounts: Vec<Value> = serde_json::from_str(&res)?;

                        for account in accounts {
                            tracing::info!(%account);
                        }
                    }
                }
            },
        }

        Ok(())
    }
}