pub mod oauth;
pub mod provisioner;
pub mod registry;
pub mod scanner;
pub use oauth::{DeviceCodeResponse, OAuthBroker, PollOutcome, PollResponse};
pub use provisioner::{InstallOutput, install as install_spec};
pub use registry::{CredentialResolver, CredentialStatus, Integration, IntegrationRegistry};
pub use scanner::{DetectedTool, HostProbe, HostToolScanner, RealProbe, ToolSource};
use std::sync::Arc;
pub struct HostToolsApi {
scanner: Arc<HostToolScanner>,
registry: IntegrationRegistry,
oauth: OAuthBroker,
}
impl HostToolsApi {
pub fn new() -> Self {
let registry = Self::load_registry().unwrap_or_else(|e| {
tracing::warn!(error = %e, "failed to load integration registry; using empty");
IntegrationRegistry::default()
});
Self {
scanner: Arc::new(HostToolScanner::real()),
registry,
oauth: OAuthBroker::new(),
}
}
pub fn with_scanner(scanner: Arc<HostToolScanner>) -> Self {
Self {
scanner,
registry: IntegrationRegistry::default(),
oauth: OAuthBroker::new(),
}
}
fn load_registry() -> anyhow::Result<IntegrationRegistry> {
const EMBEDDED: &str = include_str!("../../share/default-integrations.toml");
let home = crate::config::expand_home("~/.oxios");
let fs_defaults = home.join("share/default-integrations.toml");
let defaults_text = if fs_defaults.exists() {
std::fs::read_to_string(&fs_defaults).unwrap_or_else(|_| EMBEDDED.to_string())
} else {
EMBEDDED.to_string()
};
let overrides = crate::config::expand_home("~/.oxios/integrations.d");
IntegrationRegistry::load_text(&defaults_text, &overrides)
}
pub async fn detect(&self, name: &str) -> Option<DetectedTool> {
self.scanner.detect(name).await
}
pub async fn detect_many(&self, names: &[String]) -> Vec<DetectedTool> {
self.scanner.detect_many(names).await
}
pub fn invalidate(&self) {
self.scanner.invalidate();
}
pub fn integrations(&self) -> Vec<&Integration> {
self.registry.all()
}
pub fn integration(&self, id: &str) -> Option<&Integration> {
self.registry.get(id)
}
pub fn integration_cli_names(&self) -> Vec<String> {
self.registry.cli_names()
}
pub fn credential_status(&self, id: &str) -> Option<CredentialStatus> {
self.registry
.get(id)
.map(|i| CredentialStatus::resolve(&i.credential))
}
pub async fn install(&self, id: &str) -> anyhow::Result<InstallOutput> {
let it = self
.registry
.get(id)
.ok_or_else(|| anyhow::anyhow!("integration '{id}' not found"))?;
anyhow::ensure!(
!it.install.is_empty(),
"integration '{id}' has no install specs"
);
install_spec(&it.install).await
}
pub async fn oauth_start(&self, id: &str) -> anyhow::Result<DeviceCodeResponse> {
let it = self
.registry
.get(id)
.ok_or_else(|| anyhow::anyhow!("integration '{id}' not found"))?;
let (provider, store_key, scopes) = match &it.credential {
CredentialResolver::OAuth {
provider,
store_key,
scopes,
} => (provider.clone(), store_key.clone(), scopes.clone()),
other => anyhow::bail!("integration '{id}' is {:?}, not oauth", other),
};
self.oauth.start(&provider, &store_key, &scopes).await
}
pub async fn oauth_poll(&self, handle: &str) -> anyhow::Result<PollResponse> {
self.oauth.poll(handle).await
}
}
impl Default for HostToolsApi {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Debug for HostToolsApi {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("HostToolsApi").finish_non_exhaustive()
}
}