`hivectl open forge` on a host where the daemon is fine and the socket is fine printed "could not reach the hive-c0re daemon for URLs — is hive-c0re running?". It was running. The operator was not in `hive-admin` in that shell, and the connect got EACCES. The message was a guess, not a diagnosis, because `query_hive_urls` returned `Option` and threw the cause away with `.ok()`. Three different failures — not in the group, no socket at all, nobody listening — all arrived as the same sentence, and only one of the three is fixed by looking at the daemon. Classify the connect error in `client::request`, which every daemon-assisted verb goes through, and keep the io error as the anyhow cause so the output reads fix-first. EACCES names `hive-admin`, `services.hyperhive.adminUsers`, and — the part that actually bites — the re-login, since secondary group membership is only applied at login, so a shell opened before the grant still cannot connect. ENOENT and ECONNREFUSED point at the units instead. Then stop discarding it: `query_hive_urls` returns `Result<Option<_>>`, `open` and `require_hive_domain` propagate, and `daemon_request` drops its own "connect to daemon socket" context, which only buried the actionable line under a vaguer one. `wg init`'s domain lookup stays best-effort by an explicit `.ok().flatten()` rather than by accident. Same footgun `agent_exists` was already fixed for: a permission error collapsed into a value that reads as a different, wrong story.
46 lines
2 KiB
Rust
46 lines
2 KiB
Rust
//! `hivectl open <home|forge|matrix>` — resolve a hive surface URL from the
|
|
//! daemon, print it, and best-effort `xdg-open` it.
|
|
|
|
use std::path::Path;
|
|
|
|
use anyhow::{Context as _, Result};
|
|
|
|
use crate::cli::OpenTarget;
|
|
use crate::util::query_hive_urls;
|
|
|
|
/// `open <home|forge|matrix>` — resolve the surface URL from the daemon,
|
|
/// print it, then best-effort `xdg-open` it. Printing is the reliable
|
|
/// core (headless / SSH hosts where no browser opener exists); the open
|
|
/// is convenience on top, so a missing/failed `xdg-open` is not an error.
|
|
pub(crate) async fn open_url(socket: &Path, target: OpenTarget) -> Result<()> {
|
|
// The connect error is already actionable (`client::request` classifies
|
|
// it), so propagate it rather than restating a guess about the cause.
|
|
let urls = query_hive_urls(socket)
|
|
.await
|
|
.context("could not read this hive's URLs from the daemon")?
|
|
.context("the daemon reported no URLs — `services.hyperhive.domain` is unset")?;
|
|
let (url, hint) = match target {
|
|
OpenTarget::Home => (
|
|
urls.home,
|
|
"the dashboard URL needs `services.hyperhive.domain` to be set",
|
|
),
|
|
OpenTarget::Forge => (
|
|
urls.forge,
|
|
"the public forge URL needs `services.hyperhive.forge.behindGateway = true`",
|
|
),
|
|
OpenTarget::Matrix => (
|
|
urls.matrix,
|
|
"the matrix GUI URL needs `services.hyperhive.matrix.gui.enable = true`",
|
|
),
|
|
};
|
|
let url = url.with_context(|| format!("no URL available for this surface — {hint}"))?;
|
|
println!("{url}");
|
|
// Best-effort: many hosts are headless, so a missing opener or a
|
|
// non-zero exit is fine — the URL is already printed.
|
|
match std::process::Command::new("xdg-open").arg(&url).status() {
|
|
Ok(status) if status.success() => {}
|
|
Ok(status) => eprintln!("note: xdg-open exited with {status} (URL printed above)"),
|
|
Err(e) => eprintln!("note: could not run xdg-open ({e}) (URL printed above)"),
|
|
}
|
|
Ok(())
|
|
}
|