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}