use std::path::PathBuf;
use url::Url;
use crate::backend::error::{Error, Result};
pub fn default_snapshots_root() -> PathBuf {
default_lgtm_home().join("snapshots")
}
pub fn default_database_url() -> Result<String> {
for var in ["LGTM_DATABASE_URL", "DATABASE_URL"] {
if let Ok(url) = std::env::var(var)
&& !url.trim().is_empty()
{
return Ok(url.trim().to_owned());
}
}
let lgtm_home = default_lgtm_home();
std::fs::create_dir_all(&lgtm_home).map_err(|err| {
Error::Invalid(format!(
"failed to create LGTM home directory '{}': {err}",
lgtm_home.display()
))
})?;
let db_path = lgtm_home.join("lgtm.db");
Ok(format!("sqlite://{}?mode=rwc", db_path.display()))
}
fn default_lgtm_home() -> PathBuf {
if let Ok(home) = std::env::var("LGTM_HOME")
&& !home.trim().is_empty()
{
return PathBuf::from(home.trim());
}
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".lgtm")
}
pub fn normalize_repository_url(raw_remote: &str) -> Result<String> {
let trimmed = raw_remote.trim();
if trimmed.is_empty() {
return Err(Error::Invalid(
"repository_url must not be empty".to_owned(),
));
}
if let Some(rest) = trimmed.strip_prefix("git@") {
let (host, path) = rest
.split_once(':')
.ok_or_else(|| Error::Invalid(format!("invalid SSH remote address '{trimmed}'")))?;
let clean_path = path.trim_start_matches('/').trim_end_matches(".git");
if clean_path.split('/').count() < 2 {
return Err(Error::Invalid(format!(
"repository_url '{trimmed}' must include owner and repository name"
)));
}
return Ok(format!("https://{host}/{clean_path}"));
}
let parsed = Url::parse(trimmed)
.map_err(|err| Error::Invalid(format!("invalid repository_url '{trimmed}': {err}")))?;
let scheme = match parsed.scheme() {
"http" | "https" => parsed.scheme(),
"ssh" => "https",
other => {
return Err(Error::Invalid(format!(
"unsupported repository_url scheme '{other}'"
)));
}
};
let host = parsed
.host_str()
.ok_or_else(|| Error::Invalid(format!("repository_url '{trimmed}' is missing a host")))?;
let clean_path = parsed
.path()
.trim_start_matches('/')
.trim_end_matches('/')
.trim_end_matches(".git");
if clean_path
.split('/')
.filter(|part| !part.is_empty())
.count()
< 2
{
return Err(Error::Invalid(format!(
"repository_url '{trimmed}' must include owner and repository name"
)));
}
Ok(format!("{scheme}://{host}/{clean_path}"))
}
pub fn parse_github_owner_repo(repository_url: &str) -> Result<(String, String)> {
let normalized = normalize_repository_url(repository_url)?;
let parsed = Url::parse(&normalized)
.map_err(|err| Error::Invalid(format!("invalid repository_url '{normalized}': {err}")))?;
let mut segments = parsed
.path_segments()
.ok_or_else(|| Error::Invalid(format!("invalid repository_url '{normalized}'")))?
.filter(|segment| !segment.is_empty());
let owner = segments
.next()
.ok_or_else(|| Error::Invalid(format!("missing owner in '{normalized}'")))?
.to_owned();
let repo = segments
.next()
.ok_or_else(|| Error::Invalid(format!("missing repository name in '{normalized}'")))?
.to_owned();
Ok((owner, repo))
}
pub fn parse_agent_descriptor(raw: &str) -> Result<(String, String, String)> {
let trimmed = raw.trim();
if trimmed.is_empty() {
return Err(Error::Invalid(
"agent identifier must not be empty".to_owned(),
));
}
if let Some((provider, model)) = trimmed.split_once('/') {
let provider = provider.trim();
let model = model.trim();
if provider.is_empty() || model.is_empty() {
return Err(Error::Invalid(format!(
"invalid agent identifier '{trimmed}'; expected '<provider>/<model>' or '<model>'"
)));
}
return Ok((model.to_owned(), provider.to_owned(), model.to_owned()));
}
let lower = trimmed.to_ascii_lowercase();
let provider = if lower.starts_with("claude") || lower.contains("anthropic") {
"anthropic"
} else if lower.starts_with("gpt") || lower.starts_with("o1") || lower.starts_with("o3") {
"openai"
} else if lower.starts_with("gemini") {
"google"
} else {
"custom"
};
Ok((trimmed.to_owned(), provider.to_owned(), trimmed.to_owned()))
}