use anyhow::{anyhow, Result};
use console::style;
use std::path::Path;
use crate::git;
use crate::registry::Registry;
use crate::scanner;
pub fn run(name: String, json: bool) -> Result<()> {
let reg = Registry::load()?;
let project = reg
.find(&name)
.ok_or_else(|| anyhow!("Project not found: {}. Run `devist project list`.", name))?;
if !project.path.exists() {
return Err(anyhow!(
"Project directory missing: {}",
project.path.display()
));
}
if json {
return run_json(&project.path, &name);
}
print_markdown_brief(&project.path, &name, &project.template)
}
fn run_json(path: &Path, name: &str) -> Result<()> {
let report = scanner::scan(path)?;
let log = git::recent_log(path, 10).unwrap_or_default();
let status = git::status_porcelain(path).unwrap_or_default();
let branch = git::current_branch(path).unwrap_or_else(|_| "(no git)".to_string());
let payload = serde_json::json!({
"name": name,
"scan": report,
"git": {
"branch": branch,
"recent_commits": log,
"uncommitted": status,
}
});
println!("{}", serde_json::to_string_pretty(&payload)?);
Ok(())
}
fn print_markdown_brief(path: &Path, name: &str, template: &str) -> Result<()> {
let report = scanner::scan(path)?;
let is_git = git::is_repo(path);
println!("# Project Brief: {}", name);
println!();
println!("- **Path:** `{}`", path.display());
println!("- **Template:** {}", template);
println!(
"- **Files:** {} ({} lines, {})",
report.total_files,
report.total_lines,
scanner::human_bytes(report.total_bytes)
);
println!();
println!("## Languages");
println!();
let mut langs: Vec<_> = report
.languages
.iter()
.filter(|(_, s)| s.lines > 0 || s.files > 0)
.collect();
langs.sort_by_key(|(_, s)| std::cmp::Reverse(s.lines));
if langs.is_empty() {
println!("_(no source files detected)_");
} else {
println!("| Language | Files | Lines | Size |");
println!("|---|---:|---:|---:|");
for (name, stats) in langs.iter().take(10) {
println!(
"| {} | {} | {} | {} |",
name,
stats.files,
stats.lines,
scanner::human_bytes(stats.bytes)
);
}
}
println!();
if !report.entry_points.is_empty() {
println!("## Entry Points");
println!();
for ep in &report.entry_points {
println!("- `{}`", ep);
}
println!();
}
if !report.config_files.is_empty() {
println!("## Configuration Files");
println!();
for cf in &report.config_files {
println!("- `{}`", cf);
}
println!();
}
if !report.top_files_by_size.is_empty() {
println!("## Largest Files");
println!();
println!("| File | Lines | Size |");
println!("|---|---:|---:|");
for f in &report.top_files_by_size {
println!(
"| `{}` | {} | {} |",
f.path,
f.lines,
scanner::human_bytes(f.bytes)
);
}
println!();
}
if is_git {
let branch = git::current_branch(path).unwrap_or_else(|_| "?".to_string());
println!("## Git State");
println!();
println!("- **Branch:** `{}`", branch);
let status = git::status_porcelain(path).unwrap_or_default();
if status.is_empty() {
println!("- **Working tree:** clean");
} else {
println!("- **Working tree:** {} uncommitted change(s)", status.len());
println!();
println!("```");
for line in status.iter().take(20) {
println!("{}", line);
}
if status.len() > 20 {
println!("... ({} more)", status.len() - 20);
}
println!("```");
}
println!();
let log = git::recent_log(path, 10).unwrap_or_default();
if !log.is_empty() {
println!("### Recent Commits");
println!();
println!("```");
for line in &log {
println!("{}", line);
}
println!("```");
println!();
}
let hot = git::changed_files_recent(path, 30).unwrap_or_default();
if !hot.is_empty() {
println!("### Hot Files (last 30 commits)");
println!();
println!("| File | Commits |");
println!("|---|---:|");
for (file, count) in hot.iter().take(10) {
println!("| `{}` | {} |", file, count);
}
println!();
}
} else {
println!("## Git State");
println!();
println!("_(not a git repository)_");
println!();
}
println!(
"{}",
style("Tip: pipe to `claude` or save to file with `> brief.md`").dim()
);
Ok(())
}