Skip to main content

semora_cli/
lib.rs

1//! The `semora` CLI: publish Markdown and HTML to a Semora server and
2//! wait for feedback from the page.
3//!
4//! The library exists for the binary and its tests; it is not a stable API.
5
6pub mod args;
7pub mod bundle;
8pub mod cachefile;
9pub mod checkout;
10pub mod client;
11pub mod commands;
12pub mod config;
13pub mod credentials;
14pub mod docref;
15pub mod exit;
16pub mod local_links;
17pub mod output;
18pub mod project_link;
19pub mod remember;
20pub mod server_id;
21
22use clap::Parser;
23
24use crate::args::{Cli, Cmd};
25use crate::config::{Flags, Settings};
26
27/// Everything a command needs.
28pub struct Ctx {
29    pub settings: Settings,
30    pub json: bool,
31    pub store: credentials::Store,
32}
33
34impl Ctx {
35    pub fn client(&self) -> anyhow::Result<client::Client> {
36        client::Client::new(&self.settings)
37    }
38}
39
40/// Parses arguments, runs the command and returns the process exit code.
41pub async fn main_entry() -> i32 {
42    config::load_dotenv();
43    let cli = match Cli::try_parse() {
44        Ok(cli) => cli,
45        Err(e) => {
46            print_banner(&e);
47            e.exit()
48        }
49    };
50    let env = |k: &str| std::env::var(k).ok();
51    let result = async {
52        let ctx = build_ctx(&cli, &env)?;
53        run(ctx, cli.cmd).await
54    }
55    .await;
56    match result {
57        Ok(code) => code,
58        Err(e) => {
59            match e.downcast_ref::<exit::Exit>() {
60                Some(x) => output::error(&x.message),
61                None => output::error(format!("{e:#}")),
62            }
63            exit::code_of(&e)
64        }
65    }
66}
67
68/// The brand's line above what clap prints for `semora --version` and for `semora` alone.
69/// Only where a person is reading: a piped stream gets what it always got, so a script that
70/// reads the version still reads one line.
71fn print_banner(e: &clap::Error) {
72    use std::io::IsTerminal;
73    if e.kind() == clap::error::ErrorKind::DisplayVersion {
74        if std::io::stdout().is_terminal() {
75            anstream::println!("{}", output::banner(output::out()));
76        }
77    } else if std::env::args_os().len() == 1 && std::io::stderr().is_terminal() {
78        // The help for a missing command goes to stderr, and so does its banner.
79        anstream::eprintln!("{}\n", output::banner(output::err()));
80    }
81}
82
83fn build_ctx(cli: &Cli, env: &dyn Fn(&str) -> Option<String>) -> anyhow::Result<Ctx> {
84    let dir = config::config_dir(env);
85    let file = config::read_file_config(&dir)?;
86    let no_keyring = env("SEMORA_NO_KEYRING").is_some_and(|v| !v.is_empty() && v != "0");
87    let store = credentials::Store::new(&dir, !no_keyring);
88    let flags = Flags {
89        server: cli.server.clone(),
90        api_key: cli.api_key.clone(),
91    };
92    let settings = config::resolve(&flags, env, &file, store.default_server(), &|u| {
93        store.get(&config::origin(u))
94    })?;
95    Ok(Ctx {
96        settings,
97        json: cli.json,
98        store,
99    })
100}
101
102async fn run(ctx: Ctx, cmd: Cmd) -> anyhow::Result<i32> {
103    use commands::*;
104    match cmd {
105        Cmd::Push(a) => push::run(&ctx, a).await,
106        Cmd::Watch(a) => watch::run(&ctx, a).await,
107        Cmd::Pull(a) => pull::pull(&ctx, a).await,
108        Cmd::Status { path } => pull::status(&ctx, path).await,
109        Cmd::Wait(a) => wait::run(&ctx, a).await,
110        Cmd::Say {
111            doc,
112            text,
113            reply_to,
114        } => events::say(&ctx, &doc, &text, reply_to).await,
115        Cmd::Ls {
116            query,
117            tag,
118            kind,
119            project,
120            limit,
121        } => docs::ls(&ctx, query, tag, kind, project, limit).await,
122        Cmd::Show { doc } => docs::show(&ctx, &doc).await,
123        Cmd::Get {
124            doc,
125            path,
126            rev,
127            output,
128        } => docs::get(&ctx, &doc, path, rev, output).await,
129        Cmd::Tag { doc, changes } => docs::tag(&ctx, &doc, &changes).await,
130        Cmd::Rm { doc } => docs::rm(&ctx, &doc).await,
131        Cmd::Open { doc } => docs::open(&ctx, &doc).await,
132        Cmd::Whoami => account::whoami(&ctx).await,
133        Cmd::Login { no_browser, name } => login::login(&ctx, no_browser, name).await,
134        Cmd::Logout => login::logout(&ctx).await,
135        Cmd::Keys(k) => account::keys(&ctx, k).await,
136        Cmd::Project(p) => project::run(&ctx, p).await,
137        Cmd::Skill => skill::run(&ctx),
138    }
139}