use anyhow::{anyhow, Result};
use std::fs;
use std::path::Path;
use crate::registry::Registry;
use crate::scanner;
pub fn run(name: String, target: String, max_lines: usize) -> Result<()> {
let reg = Registry::load()?;
let project = reg
.find(&name)
.ok_or_else(|| anyhow!("Project not found: {}", name))?;
if !project.path.exists() {
return Err(anyhow!(
"Project directory missing: {}",
project.path.display()
));
}
let resolved = scanner::find_path(&project.path, &target).ok_or_else(|| {
anyhow!(
"Could not find `{}` inside project. Try a relative path or filename.",
target
)
})?;
if resolved.is_dir() {
explain_directory(&resolved, &project.path, &target)
} else {
explain_file(&resolved, &project.path, max_lines)
}
}
fn explain_file(path: &Path, project_root: &Path, max_lines: usize) -> Result<()> {
let rel = path
.strip_prefix(project_root)
.unwrap_or(path)
.to_string_lossy()
.to_string();
let metadata = fs::metadata(path)?;
let bytes = metadata.len();
let language = guess_language(path);
println!("# File: `{}`", rel);
println!();
println!(
"- **Size:** {} ยท **Language:** {}",
scanner::human_bytes(bytes),
language
);
if bytes > 1024 * 1024 {
println!();
println!(
"_(file too large to display inline; {})_",
scanner::human_bytes(bytes)
);
return Ok(());
}
let content = match fs::read_to_string(path) {
Ok(c) => c,
Err(_) => {
println!();
println!("_(binary or non-UTF-8 file)_");
return Ok(());
}
};
let total_lines = content.lines().count();
println!("- **Lines:** {}", total_lines);
println!();
let lang_hint = language_hint(&language);
println!("```{}", lang_hint);
let take = max_lines.min(total_lines);
for (i, line) in content.lines().take(take).enumerate() {
println!("{:>4} {}", i + 1, line);
}
if total_lines > take {
println!(
"... ({} more lines, use --max-lines to see more)",
total_lines - take
);
}
println!("```");
Ok(())
}
fn explain_directory(dir: &Path, project_root: &Path, query: &str) -> Result<()> {
let rel = dir
.strip_prefix(project_root)
.unwrap_or(dir)
.to_string_lossy()
.to_string();
println!(
"# Directory: `{}`",
if rel.is_empty() {
query.to_string()
} else {
rel
}
);
println!();
println!("## Tree");
println!();
println!("```");
let lines = scanner::build_tree(dir, 4, 200);
let display = dir
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_else(|| query.to_string());
println!("{}/", display);
for line in lines {
println!("{}", line);
}
println!("```");
println!();
println!("## Immediate Children");
println!();
let mut entries: Vec<_> = fs::read_dir(dir)?.flatten().collect();
entries.sort_by_key(|e| e.file_name());
for entry in entries {
let name = entry.file_name().to_string_lossy().to_string();
if name.starts_with('.') && name != ".env.example" {
continue;
}
let kind = if entry.file_type().map(|t| t.is_dir()).unwrap_or(false) {
"dir"
} else {
"file"
};
println!("- `{}` ({})", name, kind);
}
Ok(())
}
fn guess_language(path: &Path) -> String {
let ext = path
.extension()
.and_then(|e| e.to_str())
.unwrap_or("")
.to_lowercase();
match ext.as_str() {
"rs" => "Rust",
"py" => "Python",
"ts" | "tsx" => "TypeScript",
"js" | "jsx" | "mjs" | "cjs" => "JavaScript",
"dart" => "Dart",
"go" => "Go",
"kt" => "Kotlin",
"swift" => "Swift",
"html" => "HTML",
"css" | "scss" => "CSS",
"md" => "Markdown",
"json" => "JSON",
"yaml" | "yml" => "YAML",
"toml" => "TOML",
"sh" | "bash" => "Shell",
"tmpl" => "Template",
_ => "Other",
}
.to_string()
}
fn language_hint(lang: &str) -> &'static str {
match lang {
"Rust" => "rust",
"Python" => "python",
"TypeScript" => "ts",
"JavaScript" => "js",
"Dart" => "dart",
"Go" => "go",
"Kotlin" => "kotlin",
"Swift" => "swift",
"HTML" => "html",
"CSS" => "css",
"Markdown" => "md",
"JSON" => "json",
"YAML" => "yaml",
"TOML" => "toml",
"Shell" => "bash",
_ => "",
}
}