tangled-cli 0.1.0

CLI for interacting with Tangled, an AT Protocol-based git collaboration platform
//! Orchestration layer between the CLI commands and the low-level
//! `tangled-api` builders.
//!
//! `tangled-api` only knows how to build single XRPC requests from the
//! lexicons; everything that composes multiple calls (record CRUD on the PDS,
//! ServiceAuth minting, legacy wire-shape fallbacks) lives here.

pub mod auth;
pub mod issue;
pub mod oauth;
pub mod pull;
pub mod repo;
pub mod secrets;
pub mod service_auth;
pub mod session;
pub mod types;

use std::sync::OnceLock;

use serde::de::DeserializeOwned;
use tangled_api::xrpc::{self, XrpcError};

/// Base URL for Tangled's read-only XRPC API/AppView (Bobbin).
pub const DEFAULT_API_BASE: &str = "https://api.tangled.org";

/// Default knot host used when creating new repositories.
pub const DEFAULT_KNOT_HOST: &str = "knot1.tangled.sh";

/// Base URL for the default knot.
pub const DEFAULT_KNOT: &str = "https://knot1.tangled.sh";

/// Shared HTTP client (connection pooling across all ops).
pub(crate) fn http() -> &'static reqwest::Client {
    static HTTP: OnceLock<reqwest::Client> = OnceLock::new();
    HTTP.get_or_init(|| {
        reqwest::Client::builder()
            .user_agent(concat!("tangled-cli/", env!("CARGO_PKG_VERSION")))
            .build()
            .expect("static HTTP client configuration must be valid")
    })
}

pub(crate) async fn xrpc_send<T>(
    rb: reqwest::RequestBuilder,
) -> Result<T, XrpcError>
where
    T: DeserializeOwned,
{
    crate::progress::with_spinner("Contacting server...", xrpc::send(rb)).await
}

pub(crate) async fn xrpc_send_unit(
    rb: reqwest::RequestBuilder,
) -> Result<(), XrpcError> {
    crate::progress::with_spinner("Contacting server...", xrpc::send_unit(rb))
        .await
}

#[allow(dead_code)]
pub(crate) async fn xrpc_send_bytes(
    rb: reqwest::RequestBuilder,
) -> Result<Vec<u8>, XrpcError> {
    crate::progress::with_spinner("Contacting server...", xrpc::send_bytes(rb))
        .await
}

/// Extracts the rkey from an at-uri (`at://did/collection/rkey`).
pub(crate) fn uri_rkey(uri: &str) -> Option<String> {
    uri.rsplit('/').next().map(|s| s.to_string())
}

/// Extracts the DID from an at-uri (`at://did/collection/rkey`).
pub(crate) fn uri_did(uri: &str) -> Option<String> {
    let parts: Vec<&str> = uri.split('/').collect();
    if parts.len() >= 3 {
        Some(parts[2].to_string())
    } else {
        None
    }
}

pub(crate) fn now_rfc3339() -> String {
    chrono::Utc::now().to_rfc3339()
}