use std::path::PathBuf;
use std::sync::Arc;
use std::time::Instant;
use anyhow::Result;
use serde::Serialize;
use sqry_core::graph::unified::persistence::GraphStorage;
use sqry_core::workspace::{
LogicalWorkspace, MemberFolder, MemberReason, SourceRootIndexState, SourceRootStatus,
WorkspaceIndexStatus,
};
use crate::engine::engine_for_workspace;
use crate::execution::types::ToolExecution;
use crate::execution::utils::duration_to_ms;
use crate::workspace_session::current_logical_workspace;
#[derive(Debug, Clone, Serialize)]
pub struct WorkspaceStatusData {
pub workspace_id_short: String,
pub workspace_id_full: String,
pub aggregate: WorkspaceIndexStatus,
pub project_root_mode: String,
pub source_roots: Vec<PathBuf>,
pub member_folders: Vec<MemberFolderInfo>,
pub exclusions: Vec<PathBuf>,
pub requested_workspace_id: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct MemberFolderInfo {
pub path: PathBuf,
pub reason: MemberReason,
}
#[derive(Debug, Clone, Default)]
pub struct WorkspaceStatusArgs {
pub workspace_id: Option<String>,
pub path: String,
}
pub fn execute_workspace_status(
args: &WorkspaceStatusArgs,
) -> Result<ToolExecution<WorkspaceStatusData>> {
let start = Instant::now();
let resolved = if args.path == "." {
None
} else {
Some(PathBuf::from(&args.path))
};
let engine = engine_for_workspace(resolved.as_ref())?;
let workspace_root = engine.workspace_root().to_path_buf();
let workspace_arc = match current_logical_workspace() {
Some(arc) => arc,
None => Arc::new(
LogicalWorkspace::single_root(workspace_root.clone()).map_err(|err| {
anyhow::anyhow!(
"Failed to build single-root LogicalWorkspace for {}: {err}",
workspace_root.display()
)
})?,
),
};
let data = build_status(workspace_arc.as_ref(), args.workspace_id.clone());
Ok(ToolExecution {
data,
used_index: false,
used_graph: false,
graph_metadata: None,
execution_ms: duration_to_ms(start.elapsed()),
next_page_token: None,
total: Some(1),
truncated: Some(false),
candidates_scanned: None,
workspace_path: crate::execution::symbol_utils::path_to_forward_slash(&workspace_root),
})
}
fn build_status(workspace: &LogicalWorkspace, requested: Option<String>) -> WorkspaceStatusData {
let aggregate = aggregate_workspace_index_status(workspace);
let source_roots = workspace
.source_roots()
.iter()
.map(|r| r.path.clone())
.collect();
let member_folders = workspace
.member_folders()
.iter()
.map(|m: &MemberFolder| MemberFolderInfo {
path: m.path.clone(),
reason: m.reason,
})
.collect();
WorkspaceStatusData {
workspace_id_short: workspace.workspace_id().as_short_hex(),
workspace_id_full: workspace.workspace_id().as_full_hex(),
aggregate,
project_root_mode: workspace.project_root_mode().to_string(),
source_roots,
member_folders,
exclusions: workspace.exclusions().to_vec(),
requested_workspace_id: requested,
}
}
fn aggregate_workspace_index_status(workspace: &LogicalWorkspace) -> WorkspaceIndexStatus {
let mut entries = Vec::with_capacity(workspace.source_roots().len());
for source_root in workspace.source_roots() {
let storage = GraphStorage::new(&source_root.path);
let snapshot_path = storage.snapshot_path();
let lock_path = source_root.path.join(".sqry/graph/build.lock");
let lock_present = lock_path.is_file();
let (status, last_indexed_at) = if lock_present {
(SourceRootIndexState::Building, None)
} else if !storage.exists() || !storage.snapshot_exists() {
(SourceRootIndexState::Missing, None)
} else {
match std::fs::metadata(snapshot_path) {
Ok(meta) => (SourceRootIndexState::Ok, meta.modified().ok()),
Err(_) => (SourceRootIndexState::Error, None),
}
};
entries.push(SourceRootStatus {
path: source_root.path.clone(),
status,
last_indexed_at,
symbol_count: None,
classpath_dir: source_root.classpath_dir.clone(),
});
}
WorkspaceIndexStatus::from_source_root_statuses(entries)
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn build_status_matches_logical_workspace_identity() {
let tmp = TempDir::new().unwrap();
std::fs::create_dir_all(tmp.path().join(".sqry/graph")).unwrap();
let workspace = LogicalWorkspace::single_root(tmp.path().to_path_buf()).unwrap();
let data = build_status(&workspace, Some("client-supplied".to_string()));
assert_eq!(data.workspace_id_short.len(), 16);
assert_eq!(data.workspace_id_full.len(), 64);
assert!(
data.workspace_id_full
.starts_with(data.workspace_id_short.as_str())
);
assert_eq!(
data.requested_workspace_id.as_deref(),
Some("client-supplied")
);
assert_eq!(data.source_roots.len(), 1);
assert_eq!(data.member_folders.len(), 0);
assert_eq!(data.exclusions.len(), 0);
assert_eq!(data.aggregate.source_root_statuses.len(), 1);
}
#[test]
fn build_status_surfaces_multi_root_structure() {
let tmp = TempDir::new().unwrap();
let root_a = tmp.path().join("repo_a");
let root_b = tmp.path().join("repo_b");
std::fs::create_dir_all(root_a.join(".sqry/graph")).unwrap();
std::fs::create_dir_all(root_b.join(".sqry/graph")).unwrap();
let workspace =
LogicalWorkspace::anonymous_multi_root(vec![root_a.clone(), root_b.clone()]).unwrap();
let data = build_status(&workspace, None);
assert_eq!(
data.source_roots.len(),
2,
"multi-root workspace must surface every source root"
);
assert_eq!(
data.aggregate.source_root_statuses.len(),
2,
"aggregate WorkspaceIndexStatus must report one entry per source root"
);
assert!(data.workspace_id_full.starts_with(&data.workspace_id_short));
}
}