use std::collections::HashMap;
use std::env;
use anyhow::Result;
use colored::Colorize;
use crate::config::{find_sysmap_root, map_path};
use crate::map::{FileNode, SystemMap};
use crate::scanner::{estimate_tokens, format_tokens};
pub fn execute(json: bool, yaml: bool, counts: 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))?;
if json {
print_json_summary(&map, counts)?;
} else if yaml {
print_yaml_summary(&map, counts)?;
} else {
print_human_summary(&map, counts);
}
Ok(())
}
fn print_human_summary(map: &SystemMap, counts: bool) {
let project_name = map.root
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_else(|| "unknown".to_string());
let project_type = if map.project_type.languages.is_empty() {
"Unknown".to_string()
} else if map.project_type.languages.len() > 1 {
let langs: Vec<String> = map.project_type.languages.iter()
.map(|l| capitalize(l))
.collect();
let type_str = langs.join(", ");
match &map.project_type.framework {
Some(fw) => format!("{} ({})", type_str, capitalize(fw)),
None => type_str,
}
} else {
let lang = &map.project_type.languages[0];
match &map.project_type.framework {
Some(fw) => format!("{} ({})", capitalize(lang), capitalize(fw)),
None => capitalize(lang),
}
};
println!("{} {}", "Project:".bold(), project_name);
println!("{} {}", "Type:".bold(), project_type);
println!();
let analysis = analyze_tree(&map.tree);
println!("{}", "Structure:".bold());
for (dir_name, stats) in &analysis.source_dirs {
println!(" {:<14} {} {} files{}",
format!("{}/", dir_name),
stats.file_count.to_string().yellow(),
stats.primary_language.as_deref().unwrap_or(""),
stats.lines.map(|l| format!(" ({} lines)", l)).unwrap_or_default()
);
}
for (dir_name, stats) in &analysis.test_dirs {
println!(" {:<14} {} test files",
format!("{}/", dir_name),
stats.file_count.to_string().yellow()
);
}
if !analysis.config_files.is_empty() {
println!(" {:<14} {}",
"Config:",
analysis.config_files.join(", ")
);
}
if !analysis.entry_points.is_empty() {
println!();
println!("{}", "Entry points:".bold());
for entry in &analysis.entry_points {
println!(" {}", entry);
}
}
if !analysis.key_dirs.is_empty() {
println!();
println!("{}", "Key directories:".bold());
for (dir_path, contents) in &analysis.key_dirs {
println!(" {:<14} {}",
format!("{}/", dir_path),
contents.join(", ")
);
}
}
if !analysis.dependencies.is_empty() {
println!();
println!("{}", "Dependencies:".bold());
println!(" {}", analysis.dependencies.join(", "));
}
if !analysis.purposes_found.is_empty() || !analysis.languages_found.is_empty() {
println!();
println!("{}", "File metadata:".bold());
if !analysis.purposes_found.is_empty() {
println!(" {:<14} {}",
"Purposes:",
analysis.purposes_found.join(", ")
);
}
if !analysis.languages_found.is_empty() {
println!(" {:<14} {}",
"Languages:",
analysis.languages_found.join(", ")
);
}
}
if !map.patterns_matched.is_empty() {
println!();
println!("{}", "Collapsed:".bold().dimmed());
for pattern in &map.patterns_matched {
println!(" {:<14} {} ({} files)",
format!("{}/", pattern.path.display()).dimmed(),
pattern.pattern.dimmed(),
pattern.files_collapsed.to_string().dimmed()
);
}
}
if counts {
let (total_lines, total_chars) = count_totals(&map.tree);
let tokens = estimate_tokens(total_chars);
println!();
println!("{} {} lines | {} chars ({})",
"Indexed:".bold(),
format_number(total_lines),
format_number(total_chars),
format_tokens(tokens)
);
}
}
fn print_json_summary(map: &SystemMap, counts: bool) -> Result<()> {
let analysis = analyze_tree(&map.tree);
let (total_lines, total_chars) = if counts {
count_totals(&map.tree)
} else {
(0, 0)
};
let summary = serde_json::json!({
"name": map.root.file_name().map(|n| n.to_string_lossy().to_string()),
"languages": map.project_type.languages,
"framework": map.project_type.framework,
"structure": {
"source_dirs": analysis.source_dirs.iter().map(|(name, stats)| {
serde_json::json!({
"path": name,
"files": stats.file_count,
"lines": stats.lines,
"language": stats.primary_language
})
}).collect::<Vec<_>>(),
"test_dirs": analysis.test_dirs.iter().map(|(name, stats)| {
serde_json::json!({
"path": name,
"files": stats.file_count
})
}).collect::<Vec<_>>(),
"config_files": analysis.config_files
},
"entry_points": analysis.entry_points,
"key_directories": analysis.key_dirs.iter().map(|(path, contents)| {
serde_json::json!({
"path": path,
"contents": contents
})
}).collect::<Vec<_>>(),
"dependencies": {
"packages": analysis.dependencies
},
"collapsed": map.patterns_matched.iter().map(|p| {
serde_json::json!({
"path": p.path,
"reason": p.pattern,
"file_count": p.files_collapsed
})
}).collect::<Vec<_>>(),
"meta": {
"indexed_files": map.meta.indexed_files,
"total_files": map.meta.total_files,
"last_updated": map.scanned_at,
"purposes_found": analysis.purposes_found,
"file_languages": analysis.languages_found,
"total_lines": if counts { Some(total_lines) } else { None },
"total_chars": if counts { Some(total_chars) } else { None },
"estimated_tokens": if counts { Some(estimate_tokens(total_chars)) } else { None }
}
});
println!("{}", serde_json::to_string_pretty(&summary)?);
Ok(())
}
fn print_yaml_summary(map: &SystemMap, counts: bool) -> Result<()> {
eprintln!("{}", "YAML output not yet implemented, showing JSON:".yellow());
print_json_summary(map, counts)
}
struct DirStats {
file_count: usize,
lines: Option<usize>,
primary_language: Option<String>,
}
struct TreeAnalysis {
source_dirs: Vec<(String, DirStats)>,
test_dirs: Vec<(String, DirStats)>,
config_files: Vec<String>,
entry_points: Vec<String>,
key_dirs: Vec<(String, Vec<String>)>,
dependencies: Vec<String>,
purposes_found: Vec<String>,
languages_found: Vec<String>,
}
fn analyze_tree(tree: &FileNode) -> TreeAnalysis {
let mut analysis = TreeAnalysis {
source_dirs: Vec::new(),
test_dirs: Vec::new(),
config_files: Vec::new(),
entry_points: Vec::new(),
key_dirs: Vec::new(),
dependencies: Vec::new(),
purposes_found: Vec::new(),
languages_found: Vec::new(),
};
let source_dir_names = ["src", "lib", "app", "pkg", "internal", "cmd"];
let test_dir_names = ["tests", "test", "spec", "__tests__"];
let config_extensions = ["yaml", "yml", "toml", "json", "ini", "cfg"];
let config_names = ["config", "settings", ".env.example", "Makefile", "Dockerfile"];
collect_metadata(tree, &mut analysis.purposes_found, &mut analysis.languages_found);
if let FileNode::Directory { children, .. } = tree {
for child in children {
match child {
FileNode::Directory { name, children: dir_children, .. } => {
let stats = compute_dir_stats(dir_children);
if source_dir_names.contains(&name.as_str()) {
analysis.source_dirs.push((name.clone(), stats));
let key_subdirs = find_key_subdirs(dir_children);
if !key_subdirs.is_empty() {
for (subdir_name, contents) in key_subdirs {
analysis.key_dirs.push((
format!("{}/{}", name, subdir_name),
contents
));
}
}
} else if test_dir_names.contains(&name.as_str()) {
analysis.test_dirs.push((name.clone(), stats));
}
}
FileNode::File { name, purpose, .. } => {
let is_config = config_names.iter().any(|c| name.contains(c))
|| name.split('.').last()
.map(|ext| config_extensions.contains(&ext))
.unwrap_or(false);
if is_config && !name.starts_with('.') {
analysis.config_files.push(name.clone());
}
if purpose.as_deref() == Some("entry") {
analysis.entry_points.push(name.clone());
}
if name == "pyproject.toml" || name == "requirements.txt" {
} else if name == "package.json" {
} else if name == "Cargo.toml" {
}
}
_ => {}
}
}
}
analysis.config_files.sort();
analysis
}
fn compute_dir_stats(children: &[FileNode]) -> DirStats {
let mut file_count = 0;
let mut total_lines = 0;
let mut lang_counts: HashMap<String, usize> = HashMap::new();
count_recursive(children, &mut file_count, &mut total_lines, &mut lang_counts);
let primary_language = lang_counts
.into_iter()
.max_by_key(|(_, count)| *count)
.map(|(lang, _)| lang);
DirStats {
file_count,
lines: if total_lines > 0 { Some(total_lines) } else { None },
primary_language,
}
}
fn count_recursive(
nodes: &[FileNode],
file_count: &mut usize,
total_lines: &mut usize,
lang_counts: &mut HashMap<String, usize>,
) {
for node in nodes {
match node {
FileNode::File { lines, language, .. } => {
*file_count += 1;
if let Some(l) = lines {
*total_lines += l;
}
if let Some(lang) = language {
*lang_counts.entry(lang.clone()).or_insert(0) += 1;
}
}
FileNode::Directory { children, .. } => {
count_recursive(children, file_count, total_lines, lang_counts);
}
FileNode::Collapsed { file_count: fc, .. } => {
*file_count += fc;
}
}
}
}
fn find_key_subdirs(children: &[FileNode]) -> Vec<(String, Vec<String>)> {
let mut result = Vec::new();
for child in children {
if let FileNode::Directory { name, children: subchildren, .. } = child {
if name.starts_with('.') || name == "__pycache__" {
continue;
}
let file_names: Vec<String> = subchildren
.iter()
.filter_map(|c| {
if let FileNode::File { name, .. } = c {
let base = name.split('.').next().unwrap_or(name);
if base != "__init__" && base != "mod" && base != "index" {
return Some(base.to_string());
}
}
None
})
.collect();
if !file_names.is_empty() {
result.push((name.clone(), file_names));
}
}
}
result
}
fn collect_metadata(node: &FileNode, purposes: &mut Vec<String>, languages: &mut Vec<String>) {
match node {
FileNode::File { purpose, language, .. } => {
if let Some(p) = purpose {
if !purposes.contains(p) {
purposes.push(p.clone());
}
}
if let Some(l) = language {
if !languages.contains(l) {
languages.push(l.clone());
}
}
}
FileNode::Directory { children, .. } => {
for child in children {
collect_metadata(child, purposes, languages);
}
}
FileNode::Collapsed { .. } => {}
}
}
fn capitalize(s: &str) -> String {
let mut chars = s.chars();
match chars.next() {
None => String::new(),
Some(c) => c.to_uppercase().chain(chars).collect(),
}
}
fn count_totals(node: &FileNode) -> (usize, usize) {
let mut total_lines = 0;
let mut total_chars = 0;
count_totals_recursive(node, &mut total_lines, &mut total_chars);
(total_lines, total_chars)
}
fn count_totals_recursive(node: &FileNode, lines: &mut usize, chars: &mut usize) {
match node {
FileNode::File { lines: l, chars: c, .. } => {
if let Some(line_count) = l {
*lines += line_count;
}
if let Some(char_count) = c {
*chars += char_count;
}
}
FileNode::Directory { children, .. } => {
for child in children {
count_totals_recursive(child, lines, chars);
}
}
FileNode::Collapsed { .. } => {
}
}
}
fn format_number(n: usize) -> String {
let s = n.to_string();
let mut result = String::new();
for (i, c) in s.chars().rev().enumerate() {
if i > 0 && i % 3 == 0 {
result.insert(0, ',');
}
result.insert(0, c);
}
result
}