use anyhow::Result;
use clap::Args;
use serde::Serialize;
use std::path::Path;
use crate::config::workspace::WorkspaceConfig;
use crate::core::graph::Graph;
use crate::output::formatter;
use crate::output::json::JsonOutput;
use crate::Context;
#[derive(Args, Debug)]
pub struct StatusArgs {
#[arg(long, short = 'j')]
pub json: bool,
}
#[derive(Debug, Serialize)]
pub struct StatusData {
pub index_exists: bool,
pub symbol_count: usize,
pub file_count: usize,
pub last_indexed_at: Option<i64>,
pub last_indexed_relative: Option<String>,
pub edge_count: usize,
}
#[derive(Debug, Serialize)]
pub struct WorkspaceStatusData {
pub workspace_name: String,
pub members: Vec<MemberStatusData>,
pub totals: StatusData,
}
#[derive(Debug, Serialize)]
pub struct MemberStatusData {
pub name: String,
#[serde(flatten)]
pub status: StatusData,
}
pub fn run(args: &StatusArgs, ctx: &Context) -> Result<()> {
match ctx {
Context::SingleProject { root } => run_single(args, root),
Context::Workspace {
workspace_root,
config,
..
} => run_workspace(args, workspace_root, config),
}
}
fn run_single(args: &StatusArgs, project_root: &Path) -> Result<()> {
let scope_dir = project_root.join(".scope");
if !scope_dir.exists() {
if args.json {
let data = StatusData {
index_exists: false,
symbol_count: 0,
file_count: 0,
last_indexed_at: None,
last_indexed_relative: None,
edge_count: 0,
};
let output = JsonOutput {
command: "status",
symbol: None,
data,
truncated: false,
total: 0,
};
println!("{}", serde_json::to_string_pretty(&output)?);
} else {
println!("Index status: not initialised");
println!(" Run 'scope init' to set up Scope for this project.");
}
return Ok(());
}
let db_path = scope_dir.join("graph.db");
if !db_path.exists() {
if args.json {
let data = StatusData {
index_exists: false,
symbol_count: 0,
file_count: 0,
last_indexed_at: None,
last_indexed_relative: None,
edge_count: 0,
};
let output = JsonOutput {
command: "status",
symbol: None,
data,
truncated: false,
total: 0,
};
println!("{}", serde_json::to_string_pretty(&output)?);
} else {
println!("Index status: not built");
println!(" Run 'scope index' to build the index.");
}
return Ok(());
}
let graph = Graph::open(&db_path)?;
let symbol_count = graph.symbol_count()?;
let file_count = graph.file_count()?;
let edge_count = graph.edge_count()?;
let last_indexed_at = graph.last_indexed_at()?;
let last_indexed_relative = last_indexed_at.map(format_relative_time);
let status_label = if symbol_count == 0 {
"empty"
} else {
"up to date"
};
if args.json {
let data = StatusData {
index_exists: true,
symbol_count,
file_count,
last_indexed_at,
last_indexed_relative,
edge_count,
};
let output = JsonOutput {
command: "status",
symbol: None,
data,
truncated: false,
total: symbol_count,
};
println!("{}", serde_json::to_string_pretty(&output)?);
} else {
formatter::print_status(
status_label,
symbol_count,
file_count,
edge_count,
last_indexed_relative.as_deref(),
);
}
Ok(())
}
fn run_workspace(args: &StatusArgs, workspace_root: &Path, config: &WorkspaceConfig) -> Result<()> {
let mut member_statuses: Vec<MemberStatusData> = Vec::new();
let mut total_symbols = 0usize;
let mut total_files = 0usize;
let mut total_edges = 0usize;
let mut latest_indexed: Option<i64> = None;
let mut all_indexed = true;
for entry in &config.workspace.members {
let name = WorkspaceConfig::resolve_member_name(entry);
let member_path = workspace_root.join(&entry.path);
let db_path = member_path.join(".scope").join("graph.db");
let status = if !db_path.exists() {
all_indexed = false;
StatusData {
index_exists: false,
symbol_count: 0,
file_count: 0,
last_indexed_at: None,
last_indexed_relative: None,
edge_count: 0,
}
} else {
match Graph::open(&db_path) {
Ok(graph) => {
let sc = graph.symbol_count().unwrap_or(0);
let fc = graph.file_count().unwrap_or(0);
let ec = graph.edge_count().unwrap_or(0);
let lia = graph.last_indexed_at().unwrap_or(None);
total_symbols += sc;
total_files += fc;
total_edges += ec;
if let Some(ts) = lia {
latest_indexed = Some(latest_indexed.map_or(ts, |prev: i64| prev.max(ts)));
}
StatusData {
index_exists: true,
symbol_count: sc,
file_count: fc,
last_indexed_at: lia,
last_indexed_relative: lia.map(format_relative_time),
edge_count: ec,
}
}
Err(_) => {
all_indexed = false;
StatusData {
index_exists: false,
symbol_count: 0,
file_count: 0,
last_indexed_at: None,
last_indexed_relative: None,
edge_count: 0,
}
}
}
};
member_statuses.push(MemberStatusData { name, status });
}
if args.json {
let data = WorkspaceStatusData {
workspace_name: config.workspace.name.clone(),
members: member_statuses,
totals: StatusData {
index_exists: all_indexed,
symbol_count: total_symbols,
file_count: total_files,
last_indexed_at: latest_indexed,
last_indexed_relative: latest_indexed.map(format_relative_time),
edge_count: total_edges,
},
};
let output = JsonOutput {
command: "status",
symbol: None,
data,
truncated: false,
total: total_symbols,
};
println!("{}", serde_json::to_string_pretty(&output)?);
} else {
formatter::print_workspace_status(
&config.workspace.name,
&member_statuses,
total_symbols,
total_files,
total_edges,
);
}
Ok(())
}
pub(crate) fn format_relative_time(unix_ts: i64) -> String {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
let delta = now - unix_ts;
if delta < 0 {
return "just now".to_string();
}
let seconds = delta;
let minutes = seconds / 60;
let hours = minutes / 60;
let days = hours / 24;
if seconds < 60 {
"just now".to_string()
} else if minutes == 1 {
"1 minute ago".to_string()
} else if minutes < 60 {
format!("{minutes} minutes ago")
} else if hours == 1 {
"1 hour ago".to_string()
} else if hours < 24 {
format!("{hours} hours ago")
} else if days == 1 {
"yesterday".to_string()
} else {
format!("{days} days ago")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_relative_time_just_now() {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs() as i64;
assert_eq!(format_relative_time(now), "just now");
assert_eq!(format_relative_time(now - 30), "just now");
}
#[test]
fn test_format_relative_time_minutes() {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs() as i64;
assert_eq!(format_relative_time(now - 60), "1 minute ago");
assert_eq!(format_relative_time(now - 120), "2 minutes ago");
assert_eq!(format_relative_time(now - 300), "5 minutes ago");
}
#[test]
fn test_format_relative_time_hours() {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs() as i64;
assert_eq!(format_relative_time(now - 3600), "1 hour ago");
assert_eq!(format_relative_time(now - 7200), "2 hours ago");
}
#[test]
fn test_format_relative_time_days() {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs() as i64;
assert_eq!(format_relative_time(now - 86400), "yesterday");
assert_eq!(format_relative_time(now - 172800), "2 days ago");
}
}