use std::env;
use std::path::PathBuf;
use anyhow::Result;
use colored::Colorize;
use crate::config::{find_sysmap_root, map_path};
use crate::map::SystemMap;
use crate::scanner::{estimate_tokens, format_tokens};
pub fn execute(file: PathBuf, reverse: bool, json: bool) -> Result<()> {
let cwd = env::current_dir()?;
let root = find_sysmap_root(&cwd)
.ok_or_else(|| anyhow::anyhow!("No sysmap found. Run 'sysmap init' first."))?;
let map = SystemMap::load(&map_path(&root))?;
let file_path = normalize_path(&file, &cwd, &map.root)?;
if reverse {
show_reverse_deps(&map, &file_path, json)
} else {
show_deps(&map, &file_path, json)
}
}
fn normalize_path(file: &PathBuf, cwd: &PathBuf, project_root: &PathBuf) -> Result<PathBuf> {
let absolute = if file.is_absolute() {
file.clone()
} else {
cwd.join(file)
};
if let Ok(rel) = absolute.strip_prefix(project_root) {
Ok(rel.to_path_buf())
} else if let Ok(rel) = file.strip_prefix(project_root) {
Ok(rel.to_path_buf())
} else {
Ok(file.clone())
}
}
fn show_deps(map: &SystemMap, file_path: &PathBuf, json: bool) -> Result<()> {
let internal = map.dependencies.internal.get(file_path);
let external = map.dependencies.external.get(file_path);
let file_info = find_file_info(&map, file_path);
if json {
print_json_deps(file_path, internal, external, file_info)
} else {
print_human_deps(file_path, internal, external, file_info)
}
}
fn show_reverse_deps(map: &SystemMap, file_path: &PathBuf, json: bool) -> Result<()> {
let mut dependents: Vec<&PathBuf> = Vec::new();
for (source, deps) in &map.dependencies.internal {
if deps.contains(file_path) {
dependents.push(source);
}
}
dependents.sort();
let file_info = find_file_info(&map, file_path);
if json {
print_json_reverse_deps(file_path, &dependents, file_info)
} else {
print_human_reverse_deps(file_path, &dependents, file_info)
}
}
struct FileInfo {
lines: Option<usize>,
chars: Option<usize>,
language: Option<String>,
}
fn find_file_info(map: &SystemMap, file_path: &PathBuf) -> Option<FileInfo> {
find_file_info_recursive(&map.tree, file_path)
}
fn find_file_info_recursive(node: &crate::map::FileNode, target: &PathBuf) -> Option<FileInfo> {
match node {
crate::map::FileNode::File { path, lines, chars, language, .. } => {
if path == target {
Some(FileInfo {
lines: *lines,
chars: *chars,
language: language.clone(),
})
} else {
None
}
}
crate::map::FileNode::Directory { children, .. } => {
for child in children {
if let Some(info) = find_file_info_recursive(child, target) {
return Some(info);
}
}
None
}
crate::map::FileNode::Collapsed { .. } => None,
}
}
fn print_human_deps(
file_path: &PathBuf,
internal: Option<&Vec<PathBuf>>,
external: Option<&Vec<String>>,
file_info: Option<FileInfo>,
) -> Result<()> {
print!("{}", file_path.display().to_string().bold());
if let Some(info) = &file_info {
let mut parts = Vec::new();
if let Some(lines) = info.lines {
parts.push(format!("{} lines", lines));
}
if let Some(chars) = info.chars {
let tokens = estimate_tokens(chars);
parts.push(format!("{} chars ({})", chars, format_tokens(tokens)));
}
if !parts.is_empty() {
print!(" {}", format!("({})", parts.join(", ")).dimmed());
}
}
println!();
let has_internal = internal.map(|v| !v.is_empty()).unwrap_or(false);
let has_external = external.map(|v| !v.is_empty()).unwrap_or(false);
if !has_internal && !has_external {
println!(" {}", "No dependencies found".dimmed());
return Ok(());
}
if has_internal {
println!(" {}:", "internal".cyan());
for dep in internal.unwrap() {
println!(" ├── {}", dep.display());
}
}
if has_external {
println!(" {}:", "external".yellow());
let ext = external.unwrap();
for (i, dep) in ext.iter().enumerate() {
let prefix = if i == ext.len() - 1 { "└──" } else { "├──" };
println!(" {} {}", prefix, dep);
}
}
Ok(())
}
fn print_human_reverse_deps(
file_path: &PathBuf,
dependents: &[&PathBuf],
file_info: Option<FileInfo>,
) -> Result<()> {
print!("{}", file_path.display().to_string().bold());
if let Some(info) = &file_info {
let mut parts = Vec::new();
if let Some(lines) = info.lines {
parts.push(format!("{} lines", lines));
}
if !parts.is_empty() {
print!(" {}", format!("({})", parts.join(", ")).dimmed());
}
}
println!();
if dependents.is_empty() {
println!(" {}", "No files depend on this".dimmed());
return Ok(());
}
println!(" {}:", "imported by".green());
for (i, dep) in dependents.iter().enumerate() {
let prefix = if i == dependents.len() - 1 { "└──" } else { "├──" };
println!(" {} {}", prefix, dep.display());
}
Ok(())
}
fn print_json_deps(
file_path: &PathBuf,
internal: Option<&Vec<PathBuf>>,
external: Option<&Vec<String>>,
file_info: Option<FileInfo>,
) -> Result<()> {
let output = serde_json::json!({
"file": file_path,
"lines": file_info.as_ref().and_then(|i| i.lines),
"chars": file_info.as_ref().and_then(|i| i.chars),
"language": file_info.as_ref().and_then(|i| i.language.clone()),
"dependencies": {
"internal": internal.unwrap_or(&Vec::new()),
"external": external.unwrap_or(&Vec::new())
}
});
println!("{}", serde_json::to_string_pretty(&output)?);
Ok(())
}
fn print_json_reverse_deps(
file_path: &PathBuf,
dependents: &[&PathBuf],
file_info: Option<FileInfo>,
) -> Result<()> {
let output = serde_json::json!({
"file": file_path,
"lines": file_info.as_ref().and_then(|i| i.lines),
"chars": file_info.as_ref().and_then(|i| i.chars),
"language": file_info.as_ref().and_then(|i| i.language.clone()),
"imported_by": dependents
});
println!("{}", serde_json::to_string_pretty(&output)?);
Ok(())
}