ordinary 0.6.0-pre.14

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

use crate::cmds::accounts::get_current_account;
use crate::cmds::logs::{Logs, Sync, print_logs_metadata_table};
use anyhow::bail;
use clap::Subcommand;
use clio::ClioPath;
use ordinary_api::client::OrdinaryApiClient;
use ordinary_build::traverse;
use ordinary_config::OrdinaryConfig;
use std::path::Path;

#[derive(Subcommand, Debug)]
pub enum App {
    /// deploy a changes to ordinary.json
    Deploy,
    /// 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 {
        #[command(subcommand)]
        logs: Logs,
    },
    /// manage application accounts
    Accounts {
        #[command(subcommand)]
        accounts: Accounts,
    },
    /// list all HTTP routes
    Routes,
}

#[derive(Subcommand, Debug)]
pub enum Accounts {
    /// list application accounts
    List,
}

impl App {
    #[allow(clippy::missing_panics_doc, clippy::too_many_lines)]
    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,
            false,
        )?;

        match self {
            Self::Deploy => {
                let port = client.deploy(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 => {
                client.erase(project).await?;
            }
            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 { logs } => match logs {
                Logs::Search {
                    format,
                    query,
                    limit,
                    sync,
                } => {
                    if sync == &Some(true) {
                        client.app_logs_sync(project, None, None).await?;
                    }

                    let res = client.app_logs_search(project, query, format.as_str(), limit)?;

                    print!("{res}");
                }
                Logs::Sync { sync } => match sync {
                    Sync::Info => {
                        let remote_metadata = client.app_logs_remote_metadata(project).await?;
                        let local_metadata = client.app_logs_local_metadata(project)?;

                        print_logs_metadata_table(remote_metadata, local_metadata);
                    }
                    Sync::All { force } => {
                        client.app_logs_sync(project, *force, None).await?;
                    }
                    Sync::File { name } => {
                        client.app_logs_sync(project, None, Some(name)).await?;
                    }
                },
            },
            Self::Accounts { accounts } => match accounts {
                Accounts::List => {
                    let res = client.app_accounts_list(project).await?;

                    print!("{res}");
                }
            },
            Self::Routes => {
                let config = OrdinaryConfig::get(project)?;

                println!("ROUTES\n");

                if let Some(assets) = config.assets {
                    let Some(assets_dir_path) = &assets.dir_path else {
                        bail!("assets.dir_path cannot be unset");
                    };

                    println!("assets:");

                    let base_route = assets.base_route.as_str();
                    let assets_dir = Path::new(project).join(assets_dir_path);

                    traverse(&assets_dir, &|entry| {
                        let path = entry.path();
                        let path = path.strip_prefix(&assets_dir)?;

                        if base_route == "/" {
                            println!("- GET /{}", path.display());
                        } else {
                            println!("- GET {base_route}/{}", path.display());
                        }

                        Ok(())
                    })?;

                    println!();
                }

                if let Some(templates) = config.templates {
                    println!("templates:");

                    for template in templates {
                        println!("- GET {}", template.route);
                    }

                    println!();
                }

                if let Some(redirects) = config.redirects
                    && let Some(redirects) = redirects.route
                {
                    println!("redirects:");

                    for redirect in redirects {
                        println!(
                            "- {} {} [ {} -> {} ]",
                            redirect.method, redirect.condition, redirect.rule.0, redirect.rule.1
                        );
                    }

                    println!();
                }
            }
        }

        Ok(())
    }
}