//! Shared helpers used across hivectl's command handling: daemon //! request/response plumbing (`daemon_request`, `render`, //! `render_lifecycle`) and password input (`resolve_password`). use std::path::Path; use anyhow::{Context as _, Result, bail}; /// Parse a CLI-supplied agent/account name into a validated /// [`hive_types::Ident`], mapping the parse error to an `anyhow` error that /// names the offending input. Used at hivectl's `HostRequest` construction /// sites so the wire `Ident` fields are built from validated names (the daemon /// re-validates on deserialize; parsing here gives the operator an immediate, /// local error instead of a round-trip rejection). pub(crate) fn parse_ident(name: &str) -> Result { hive_types::Ident::parse(name).map_err(|e| anyhow::anyhow!("invalid agent name {name:?}: {e}")) } /// Send a provisioning request to the daemon and print its result lines. /// The daemon owns the provisioning logic; hivectl just relays the outcome, /// prefixing any error with `label` (e.g. `forge` / `github`). pub(crate) async fn daemon_request( socket: &Path, req: hive_host_sock::HostRequest, label: &str, ) -> Result<()> { // No extra context on the request: `client::request` already names the // socket and classifies the failure, and wrapping it here would put a // vaguer line on top of the actionable one. let resp = crate::client::request(socket, req).await?; if !resp.ok { bail!( "{label}: {}", resp.error.as_deref().unwrap_or("unknown error") ); } for line in &resp.messages { println!("{line}"); } Ok(()) } /// Resolve the password the caller asked for, or return `None` to fall /// back to a random throwaway. `--password ` wins outright; /// `--password-stdin` reads one line off stdin (trailing newline /// stripped). Empty stdin is treated as an error so the caller doesn't /// silently provision an empty-password account. pub(crate) fn resolve_password( password: Option<&str>, password_stdin: bool, ) -> Result> { if let Some(p) = password { return Ok(Some(p.to_owned())); } if password_stdin { use std::io::BufRead as _; let stdin = std::io::stdin(); let mut line = String::new(); stdin .lock() .read_line(&mut line) .context("read password from stdin")?; let trimmed = line.trim_end_matches(['\r', '\n']).to_owned(); if trimmed.is_empty() { bail!("--password-stdin: empty input"); } return Ok(Some(trimmed)); } Ok(None) } /// Pretty-print a `HostResponse` as JSON and bail on failure. Used by the /// agent-lifecycle + approval verbs that just relay a daemon result verbatim. pub(crate) fn render(resp: hive_host_sock::HostResponse) -> Result<()> { println!("{}", serde_json::to_string_pretty(&resp)?); if !resp.ok { bail!(resp.error.unwrap_or_else(|| "request failed".to_owned())); } Ok(()) } /// Render a hive-wide stop/start/restart response: one `: ` /// line per touched container, then surface any aggregated per-target /// failure as a non-zero exit. `verb` is the past-tense word printed per /// item (`stopped` / `started`). pub(crate) fn render_lifecycle(resp: &hive_host_sock::HostResponse, verb: &str) -> Result<()> { let items = resp.agents.as_deref().unwrap_or(&[]); if items.is_empty() { println!("{verb}: nothing matched the requested scope"); } else { for item in items { println!("{verb}: {item}"); } } if !resp.ok { bail!( "{verb}: {}", resp.error.as_deref().unwrap_or("unknown error") ); } Ok(()) } /// Query this hive's domain + browser-facing web URLs (`HostRequest::Urls`). /// /// `Ok(None)` = the daemon answered and has nothing to report; `Err` = it was /// never reached, and the error carries the actionable connect hint (see /// [`crate::client::request`]). Keep the two apart: collapsing the error into /// `None` here is what made a permission problem on the socket read as "is /// hive-c0re running?", sending the operator to fix the wrong thing. pub(crate) async fn query_hive_urls(socket: &Path) -> Result> { Ok( crate::client::request(socket, hive_host_sock::HostRequest::Urls) .await? .urls, ) } /// True when `name` matches an existing hyperhive agent — i.e. it has a /// persistent state dir under the agents root. The state dir (not the /// live container list) is the test, so kept-state tombstones still /// resolve as agents: re-provisioning a destroyed-but-kept agent should /// drop its token into the existing state tree. /// /// Asks the daemon rather than stat-ing the path. hivectl used to check /// locally, but the agents root is `0700` and owned by the daemon's /// user, so an operator without root got EACCES on traversal — which /// this function then had to report as "needs root", turning every /// caller's existence guard into a permission error. The daemon owns /// that directory and answers the same question over the socket, which /// operators can already reach via the `hive-admin` group, so the guard /// works sudoless and the remaining root requirements (if any) surface /// where they actually are. pub(crate) async fn agent_exists(socket: &Path, name: &str) -> Result { let resp = crate::client::request( socket, hive_host_sock::HostRequest::AgentExists { name: parse_ident(name)?, }, ) .await?; if !resp.ok { bail!( "check whether {name:?} is an agent: {}", resp.error.as_deref().unwrap_or("unknown error") ); } resp.agent_exists .context("daemon answered the agent-exists check without a result") }