use std::time::Duration;
use secrecy::SecretString;
use crate::api::client::ApiClient;
use crate::auth::{CredentialStore, FileCredentialStore, KeyringCredentialStore};
use crate::config::{self, Config};
use crate::error::{OlError, OL_4200_TOKEN_EXPIRED};
const DEFAULT_API_URL: &str = "https://api.openlatch.ai";
pub async fn make_client() -> Result<ApiClient, OlError> {
let token = retrieve_token().await?;
let cfg = Config::load().unwrap_or_default();
let api_url = effective_api_url(&cfg);
ApiClient::new(api_url, token)
}
pub async fn retrieve_token() -> Result<SecretString, OlError> {
let store = KeyringCredentialStore::new();
if let Ok(s) = store.retrieve_async().await {
return Ok(s);
}
if let Ok(val) = std::env::var("OPENLATCH_TOKEN") {
if !val.is_empty() {
return Ok(SecretString::from(val));
}
}
let path = config::provider_dir().join("credentials.enc");
let machine_id = config::machine_id_or_init().unwrap_or_else(|_| "unknown".into());
FileCredentialStore::new(path, machine_id)
.retrieve()
.map_err(|_| {
OlError::new(
OL_4200_TOKEN_EXPIRED,
"no editor token found in keyring, OPENLATCH_TOKEN env, or credentials file",
)
.with_suggestion("Run `openlatch-provider login` to authenticate.")
})
}
pub fn effective_api_url(cfg: &Config) -> String {
if let Ok(env) = std::env::var("OPENLATCH_API_URL") {
if !env.is_empty() {
return env;
}
}
if let Some(profile) = cfg.profiles.get("default") {
if let Some(ref url) = profile.api_url {
return url.clone();
}
}
DEFAULT_API_URL.to_string()
}
pub fn hard_confirm(expected: &str, interactive: bool, yes: bool) -> Result<(), OlError> {
if yes {
return Ok(());
}
if !interactive {
return Err(OlError::new(
OL_4200_TOKEN_EXPIRED,
"destructive operation requires --yes in non-interactive mode",
));
}
use std::io::{BufRead, Write};
let mut stdout = std::io::stdout();
write!(
stdout,
"Type `{expected}` to confirm (or anything else to cancel): "
)
.ok();
stdout.flush().ok();
let stdin = std::io::stdin();
let mut line = String::new();
stdin
.lock()
.read_line(&mut line)
.map_err(|e| OlError::new(OL_4200_TOKEN_EXPIRED, format!("read confirmation: {e}")))?;
let typed = line.trim();
if typed != expected {
return Err(OlError::new(
OL_4200_TOKEN_EXPIRED,
"confirmation text did not match — operation cancelled",
));
}
Ok(())
}
#[doc(hidden)]
pub async fn _yield_short() {
tokio::time::sleep(Duration::from_millis(1)).await;
}
pub async fn read_last_lines(
path: &std::path::Path,
n: usize,
) -> Result<std::collections::VecDeque<String>, OlError> {
use tokio::io::AsyncBufReadExt;
let file = tokio::fs::File::open(path).await.map_err(|e| {
OlError::new(
crate::error::OL_4270_CONFIG_UNREADABLE,
format!("open {}: {e}", path.display()),
)
})?;
let reader = tokio::io::BufReader::new(file);
let mut lines = reader.lines();
let mut buf: std::collections::VecDeque<String> = std::collections::VecDeque::with_capacity(n);
while let Some(line) = lines.next_line().await.transpose() {
let line = line.map_err(|e| {
OlError::new(
crate::error::OL_4270_CONFIG_UNREADABLE,
format!("read {}: {e}", path.display()),
)
})?;
if buf.len() == n {
buf.pop_front();
}
buf.push_back(line);
}
Ok(buf)
}
pub async fn print_tail(path: &std::path::Path, n: usize) -> Result<(), OlError> {
for line in read_last_lines(path, n).await? {
println!("{line}");
}
Ok(())
}
pub async fn follow_file(path: &std::path::Path, tail: usize) -> Result<(), OlError> {
print_tail(path, tail).await?;
use tokio::io::AsyncBufReadExt;
use tokio::io::AsyncSeekExt;
let mut file = tokio::fs::File::open(path).await.map_err(|e| {
OlError::new(
crate::error::OL_4270_CONFIG_UNREADABLE,
format!("open {}: {e}", path.display()),
)
})?;
file.seek(std::io::SeekFrom::End(0)).await.map_err(|e| {
OlError::new(
crate::error::OL_4270_CONFIG_UNREADABLE,
format!("seek: {e}"),
)
})?;
let mut reader = tokio::io::BufReader::new(file);
let mut line = String::new();
loop {
line.clear();
let read = reader.read_line(&mut line).await.map_err(|e| {
OlError::new(
crate::error::OL_4270_CONFIG_UNREADABLE,
format!("read {}: {e}", path.display()),
)
})?;
if read == 0 {
tokio::time::sleep(Duration::from_millis(250)).await;
continue;
}
if line.ends_with('\n') {
line.pop();
if line.ends_with('\r') {
line.pop();
}
}
println!("{line}");
}
}