use anyhow::{Context, Result};
use serde::Serialize;
use tracing::info;
use crate::context::{format_context_markdown, ContextBuilder, ContextOptions};
use crate::db::Database;
use crate::types::{IndexStats, Node};
use crate::IndexConfig;
use super::db_utils::{
canonicalize_path, open_project_database, prune_cache, rebuild_project_database, resolve_db,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum OutputFormat {
#[default]
Text,
Json,
}
impl OutputFormat {
pub fn parse(s: &str) -> Option<OutputFormat> {
match s.to_ascii_lowercase().as_str() {
"text" | "txt" => Some(OutputFormat::Text),
"json" => Some(OutputFormat::Json),
_ => None,
}
}
fn is_json(self) -> bool {
matches!(self, OutputFormat::Json)
}
pub fn request_format(self) -> Option<String> {
if self.is_json() {
Some("json".to_string())
} else {
None
}
}
}
fn print_json<T: Serialize>(value: &T) -> Result<()> {
println!("{}", serde_json::to_string_pretty(value)?);
Ok(())
}
#[derive(Serialize)]
struct SearchReport {
query: String,
count: usize,
results: Vec<Node>,
}
#[derive(Serialize)]
struct StatusReport {
indexed: bool,
database: String,
strategy: String,
#[serde(skip_serializing_if = "Option::is_none")]
stats: Option<IndexStats>,
}
#[derive(Serialize)]
struct WhereReport {
project_root: String,
index_path: String,
strategy: String,
present: bool,
}
pub fn index_command(path: &str, fmt: OutputFormat) -> Result<()> {
let project_root = canonicalize_path(path)?;
let mut db = open_project_database(&project_root)?;
let config = IndexConfig {
root: project_root.clone(),
show_progress: !fmt.is_json(),
..Default::default()
};
let stats = rebuild_project_database(&mut db, &config)?;
if fmt.is_json() {
return print_json(&stats);
}
println!("\nIndexing complete!");
println!(" Files indexed: {}", stats.files);
println!(" Symbols found: {}", stats.nodes);
println!(" Relationships: {}", stats.edges);
println!(" Files skipped: {}", stats.skipped);
println!(" Refs resolved: {}", stats.resolved_refs);
if stats.errors > 0 {
println!(" Errors: {}", stats.errors);
}
Ok(())
}
pub fn status_command(path: &str, fmt: OutputFormat) -> Result<()> {
let project_root = canonicalize_path(path)?;
let resolved = resolve_db(&project_root)?;
let db_path = resolved.path;
if !db_path.exists() {
if fmt.is_json() {
return print_json(&StatusReport {
indexed: false,
database: db_path.display().to_string(),
strategy: resolved.label.to_string(),
stats: None,
});
}
println!(
"No index found at {} [{}]",
db_path.display(),
resolved.label
);
println!("Run 'symgraph index {}' first.", path);
return Ok(());
}
let db = Database::open(&db_path)?;
let stats = db.get_stats()?;
if fmt.is_json() {
return print_json(&StatusReport {
indexed: true,
database: db_path.display().to_string(),
strategy: resolved.label.to_string(),
stats: Some(stats),
});
}
println!("symgraph Index Status");
println!("=====================");
println!("Database: {} [{}]", db_path.display(), resolved.label);
println!("Files: {}", stats.total_files);
println!("Symbols: {}", stats.total_nodes);
println!("Relationships: {}", stats.total_edges);
println!("Size: {:.2} KB", stats.db_size_bytes as f64 / 1024.0);
if !stats.languages.is_empty() {
println!("\nLanguages:");
for (lang, count) in &stats.languages {
println!(" {}: {} symbols", lang.as_str(), count);
}
}
if !stats.node_kinds.is_empty() {
println!("\nSymbol Types:");
for (kind, count) in &stats.node_kinds {
println!(" {}: {}", kind.as_str(), count);
}
}
Ok(())
}
pub fn search_command(path: &str, query: &str, fmt: OutputFormat) -> Result<()> {
let project_root = canonicalize_path(path)?;
let db_path = resolve_db(&project_root)?.path;
if !db_path.exists() {
if fmt.is_json() {
return print_json(&SearchReport {
query: query.to_string(),
count: 0,
results: Vec::new(),
});
}
println!("No index found. Run 'symgraph index' first.");
return Ok(());
}
let db = Database::open(&db_path)?;
let results = db.search_nodes(query, None, 20)?;
if fmt.is_json() {
return print_json(&SearchReport {
query: query.to_string(),
count: results.len(),
results,
});
}
if results.is_empty() {
println!("No symbols found matching '{}'", query);
return Ok(());
}
println!("Found {} symbols matching '{}':\n", results.len(), query);
for node in results {
println!(
" {} {} - {}:{}",
node.kind.as_str(),
node.name,
node.file_path,
node.start_line
);
if let Some(ref sig) = node.signature {
let sig = sig.lines().next().unwrap_or(sig);
if sig.len() > 80 {
println!(" {}...", &sig[..80]);
} else {
println!(" {}", sig);
}
}
}
Ok(())
}
pub fn context_command(path: &str, task: &str, fmt: OutputFormat) -> Result<()> {
let project_root = canonicalize_path(path)?;
let db_path = resolve_db(&project_root)?.path;
if !db_path.exists() {
if fmt.is_json() {
return print_json(&serde_json::json!({ "error": "no index", "task": task }));
}
println!("No index found. Run 'symgraph index' first.");
return Ok(());
}
let db = Database::open(&db_path)?;
let builder = ContextBuilder::new(&db, project_root);
let options = ContextOptions {
max_nodes: 20,
include_code: true,
max_code_blocks: 5,
..Default::default()
};
let context = builder.build_context(task, &options)?;
if fmt.is_json() {
return print_json(&context);
}
println!("{}", format_context_markdown(&context));
Ok(())
}
pub fn where_command(path: &str, fmt: OutputFormat) -> Result<()> {
let project_root = canonicalize_path(path)?;
let resolved = resolve_db(&project_root)?;
let present = resolved.path.exists();
if fmt.is_json() {
return print_json(&WhereReport {
project_root,
index_path: resolved.path.display().to_string(),
strategy: resolved.label.to_string(),
present,
});
}
println!("Project root: {}", project_root);
println!("Index path: {}", resolved.path.display());
println!("Strategy: {}", resolved.label);
println!(
"Status: {}",
if present {
"present"
} else {
"not indexed (run 'symgraph index')"
}
);
Ok(())
}
pub fn prune_command(fmt: OutputFormat) -> Result<()> {
let removed = prune_cache()?;
if fmt.is_json() {
return print_json(&serde_json::json!({ "pruned": removed }));
}
println!("Pruned {} stale cache index(es).", removed);
Ok(())
}
pub fn initialize_server_database(in_memory: bool) -> Result<(String, Database)> {
use std::env;
let in_memory = in_memory || env::var("SYMGRAPH_IN_MEMORY").is_ok_and(|v| v == "1");
let project_root = env::var("SYMGRAPH_ROOT")
.or_else(|_| env::current_dir().map(|p| p.display().to_string()))
.context("Could not determine project root")?;
let project_root = canonicalize_path(&project_root)?;
let db = if in_memory {
info!("Using in-memory database (no filesystem writes)");
Database::in_memory()?
} else {
open_project_database(&project_root)?
};
let stats = db.get_stats()?;
if stats.total_files == 0 {
info!("No index found, consider running 'symgraph index' first");
} else {
info!(
"Index loaded: {} files, {} symbols",
stats.total_files, stats.total_nodes
);
}
Ok((project_root, db))
}