Skip to main content

hm_plugin_cloud/
settings.rs

1//! Cloud client builders for the `hm cloud` verbs.
2//!
3//! Config and credentials are owned by the shared [`hm_config`] crate:
4//!
5//! - layered config (user `~/.config/hm/config.toml` + project
6//!   `.hm/config.toml` + `HM_*` env) supplies the API base
7//!   (`cloud.api_url`) and the active org (`cloud.org`);
8//! - bearer tokens live in `hm_config::creds`, keyed by API base, with
9//!   `HM_API_TOKEN` taking precedence.
10//!
11//! This module only assembles an SDK client from that config; it does not own
12//! any config or credential storage of its own.
13
14use anyhow::{Context, Result};
15use harmont_cloud::HarmontClient;
16
17/// Resolved cloud context for the `hm cloud` verbs.
18#[derive(Debug, Clone)]
19pub struct ResolvedCtx {
20    /// Effective API base URL.
21    pub api: String,
22    /// Configured organization slug, if set.
23    pub org: Option<String>,
24}
25
26impl ResolvedCtx {
27    /// The configured org, or a clear error telling the user how to set it.
28    ///
29    /// # Errors
30    ///
31    /// Returns an error if no organization is configured.
32    pub fn org(&self) -> Result<String> {
33        self.org.clone().context(
34            "no organization — set `[cloud] org = \"…\"` in ~/.config/hm/config.toml (or .hm/config.toml), or run `hm cloud org switch <slug>`")
35    }
36}
37
38/// An authenticated cloud client built from the layered config + stored token.
39///
40/// Fails fast with a clear message when no token is present.
41///
42/// # Errors
43///
44/// Returns an error if config can't be loaded or no token is available.
45pub fn client() -> Result<(HarmontClient, ResolvedCtx)> {
46    let cfg = hm_config::Config::load(None).context("loading config")?; // user + env layering
47    let api = cfg.cloud.api_url.clone();
48    let token = hm_config::creds::cloud_token(&api)
49        .context("not logged in — run `hm cloud login` or set HM_API_TOKEN")?;
50    let client = HarmontClient::with_base_url(token, &api);
51    Ok((
52        client,
53        ResolvedCtx {
54            api,
55            org: cfg.cloud.org,
56        },
57    ))
58}
59
60/// An anonymous client (for the login flow) + the resolved API base.
61///
62/// # Errors
63///
64/// Returns an error if config can't be loaded.
65pub fn anon_client() -> Result<(HarmontClient, String)> {
66    let cfg = hm_config::Config::load(None).context("loading config")?;
67    let api = cfg.cloud.api_url.clone();
68    Ok((HarmontClient::anonymous(&api), api))
69}
70
71/// Render preferences for cloud commands that stream through `hm-render`.
72///
73/// Both fields are derived from `hm-render`'s shared TTY/color helpers (the
74/// single source of truth, also used by `hm/src/context.rs`).
75#[derive(Debug, Clone, Copy)]
76pub struct RenderPrefs {
77    /// ANSI enabled when `NO_COLOR` is unset and stderr is a TTY.
78    ///
79    /// The plugin verbs have no `--no-color` flag, so we pass `false` for the
80    /// flag; the `--no-color` asymmetry vs. the host `hm run` path is explicit
81    /// here at the call site.
82    pub color: bool,
83    /// Force the streaming `HumanRenderer` over the live `ProgressRenderer`.
84    ///
85    /// True when stdout is **not** an interactive terminal (CI / pipe / log
86    /// file), so nothing animates into a non-TTY sink.
87    pub logs: bool,
88}
89
90impl RenderPrefs {
91    /// Detect render preferences from the live environment via `hm-render`'s
92    /// shared TTY/color helpers — the single source of truth, also used by
93    /// `hm/src/context.rs`. This inspects `NO_COLOR` and the stdout/stderr
94    /// TTY state at call time, so it is a deliberate observation of the
95    /// environment rather than a constant default.
96    #[must_use]
97    pub fn detect() -> Self {
98        Self {
99            color: hm_render::color_enabled(false),
100            logs: hm_render::stdout_piped(),
101        }
102    }
103}
104
105/// Map a raw generated-client error into an `anyhow` error with a readable
106/// message. The raw `Error<E>` renders the server's error body (status,
107/// headers, decoded value) via its `Display` impl, which holds for any
108/// `E: Debug` — true of the generated `types::Error` body.
109pub fn map_raw<E: std::fmt::Debug>(e: harmont_cloud_raw::Error<E>) -> anyhow::Error {
110    anyhow::anyhow!("{e}")
111}