semora-cli 0.3.0

Semora CLI: publish Markdown and HTML, and hear back from the page
Documentation
//! The `semora` CLI: publish Markdown and HTML to a Semora server and
//! wait for feedback from the page.
//!
//! The library exists for the binary and its tests; it is not a stable API.

pub mod args;
pub mod bundle;
pub mod client;
pub mod commands;
pub mod config;
pub mod credentials;
pub mod docref;
pub mod exit;
pub mod output;
pub mod remember;

use clap::Parser;

use crate::args::{Cli, Cmd};
use crate::config::{Flags, Settings};

/// Everything a command needs.
pub struct Ctx {
    pub settings: Settings,
    pub json: bool,
    pub store: credentials::Store,
}

impl Ctx {
    pub fn client(&self) -> anyhow::Result<client::Client> {
        client::Client::new(&self.settings)
    }
}

/// Parses arguments, runs the command and returns the process exit code.
pub async fn main_entry() -> i32 {
    config::load_dotenv();
    let cli = Cli::parse();
    let env = |k: &str| std::env::var(k).ok();
    let result = async {
        let ctx = build_ctx(&cli, &env)?;
        run(ctx, cli.cmd).await
    }
    .await;
    match result {
        Ok(code) => code,
        Err(e) => {
            match e.downcast_ref::<exit::Exit>() {
                Some(x) => eprintln!("error: {}", x.message),
                None => eprintln!("error: {e:#}"),
            }
            exit::code_of(&e)
        }
    }
}

fn build_ctx(cli: &Cli, env: &dyn Fn(&str) -> Option<String>) -> anyhow::Result<Ctx> {
    let dir = config::config_dir(env);
    let file = config::read_file_config(&dir)?;
    let no_keyring = env("SEMORA_NO_KEYRING").is_some_and(|v| !v.is_empty() && v != "0");
    let store = credentials::Store::new(&dir, !no_keyring);
    let flags = Flags {
        server: cli.server.clone(),
        api_key: cli.api_key.clone(),
    };
    let settings = config::resolve(&flags, env, &file, store.default_server(), &|u| {
        store.get(&config::origin(u))
    })?;
    Ok(Ctx {
        settings,
        json: cli.json,
        store,
    })
}

async fn run(ctx: Ctx, cmd: Cmd) -> anyhow::Result<i32> {
    use commands::*;
    match cmd {
        Cmd::Push(a) => push::run(&ctx, a).await,
        Cmd::Watch(a) => watch::run(&ctx, a).await,
        Cmd::Wait(a) => wait::run(&ctx, a).await,
        Cmd::Say {
            doc,
            text,
            reply_to,
        } => events::say(&ctx, &doc, &text, reply_to).await,
        Cmd::Ls {
            query,
            tag,
            kind,
            limit,
        } => docs::ls(&ctx, query, tag, kind, limit).await,
        Cmd::Show { doc } => docs::show(&ctx, &doc).await,
        Cmd::Get {
            doc,
            path,
            rev,
            output,
        } => docs::get(&ctx, &doc, path, rev, output).await,
        Cmd::Tag { doc, changes } => docs::tag(&ctx, &doc, &changes).await,
        Cmd::Rm { doc } => docs::rm(&ctx, &doc).await,
        Cmd::Open { doc } => docs::open(&ctx, &doc).await,
        Cmd::Whoami => account::whoami(&ctx).await,
        Cmd::Login { no_browser, name } => login::login(&ctx, no_browser, name).await,
        Cmd::Logout => login::logout(&ctx).await,
        Cmd::Keys(k) => account::keys(&ctx, k).await,
        Cmd::Skill => skill::run(&ctx),
    }
}