use anyhow::Result;
use reqwest::Client;
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct ResolvedWorkspace {
pub id: String,
pub path: String,
}
pub async fn resolve_workspace_ids(
client: &Client,
server: &str,
token: &Option<String>,
workspace_roots: &[String],
) -> Result<Vec<ResolvedWorkspace>> {
let mut req = client.get(format!(
"{}/v1/agent/workspaces",
server.trim_end_matches('/')
));
if let Some(t) = token {
req = req.bearer_auth(t);
}
let res = req.send().await?;
if !res.status().is_success() {
tracing::debug!(
status = %res.status(),
"Workspace ID resolution: server returned non-success"
);
return Ok(Vec::new());
}
let data: serde_json::Value = res.json().await?;
let entries = data
.as_array()
.cloned()
.or_else(|| data["workspaces"].as_array().cloned())
.or_else(|| data["codebases"].as_array().cloned())
.unwrap_or_default();
let mut resolved = Vec::new();
for entry in &entries {
let id = match entry["id"].as_str() {
Some(id) if !id.trim().is_empty() => id.trim().to_string(),
_ => continue,
};
let path = match entry["path"].as_str() {
Some(p) if !p.trim().is_empty() => p.trim().to_string(),
_ => continue,
};
if is_under_workspace_root(&path, workspace_roots) {
resolved.push(ResolvedWorkspace { id, path });
}
}
Ok(resolved)
}
fn is_under_workspace_root(path: &str, roots: &[String]) -> bool {
let path = std::path::Path::new(path);
roots.iter().any(|root| {
let root = std::path::Path::new(root);
path == root || path.starts_with(root)
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn path_under_root_matches() {
let roots = vec!["/workspace".to_string()];
assert!(is_under_workspace_root("/workspace", &roots));
assert!(is_under_workspace_root("/workspace/spotlessbinco", &roots));
assert!(!is_under_workspace_root("/other/path", &roots));
}
#[test]
fn empty_roots_match_nothing() {
assert!(!is_under_workspace_root("/workspace/foo", &[]));
}
#[test]
fn exact_root_match() {
let roots = vec!["/workspace".to_string()];
assert!(is_under_workspace_root("/workspace", &roots));
}
}