use mimalloc::MiMalloc;
#[global_allocator]
static GLOBAL: MiMalloc = MiMalloc;
use clap::Parser as ClapParser;
use colored::Colorize;
use rayon::prelude::*;
use std::path::PathBuf;
use std::time::Instant;
use codeskeleton::analyze;
use codeskeleton::cache;
use codeskeleton::cluster;
use codeskeleton::detect;
use codeskeleton::export;
use codeskeleton::extract;
use codeskeleton::graph::KnowledgeGraph;
use codeskeleton::languages::SupportedLanguage;
use codeskeleton::report;
use codeskeleton::types::Extraction;
const VERSION: &str = env!("CARGO_PKG_VERSION");
const OUTPUT_DIR: &str = "codeskeleton-out";
const CACHE_DIR: &str = "codeskeleton-out/cache";
#[derive(ClapParser)]
#[command(
name = "codeskeleton",
about = "Turn any folder of code into a queryable knowledge graph",
version = VERSION,
after_help = "Examples:\n codeskeleton . Analyze the current directory\n codeskeleton ./src Analyze a specific folder\n codeskeleton . --no-cache Force full re-extraction"
)]
struct Cli {
#[arg()]
path: Option<PathBuf>,
#[arg(long)]
no_cache: bool,
}
fn main() {
let cli = Cli::parse();
let start = Instant::now();
println!(
"\n {} {} {}\n",
"⬡".bright_cyan(),
"codeskeleton".bold(),
format!("v{}", VERSION).dimmed()
);
let root = if let Some(p) = cli.path {
p.canonicalize().unwrap_or_else(|_| {
eprintln!(
" {} Path not found: {}",
"✗".red(),
p.display()
);
std::process::exit(1);
})
} else {
println!(" {} Opening file picker to choose a repository...", "→".bright_cyan());
if let Some(folder) = rfd::FileDialog::new().pick_folder() {
folder
} else {
eprintln!(" {} No repository selected. Exiting.", "✗".red());
std::process::exit(1);
}
};
let all_files = detect::collect_files(&root);
if all_files.is_empty() {
eprintln!(" {} No code files found in {}", "✗".red(), root.display());
std::process::exit(1);
}
println!(
" {} Found {} code files",
"→".bright_cyan(),
all_files.len().to_string().bold()
);
let cache_dir = root.join(CACHE_DIR);
let (files_to_extract, manifest) = if cli.no_cache {
(all_files.clone(), cache::CacheManifest::new())
} else {
cache::check_cache(&all_files, &cache_dir)
};
let cached_count = all_files.len() - files_to_extract.len();
if cached_count > 0 {
println!(
" {} {} files cached, extracting {}",
"→".bright_cyan(),
cached_count.to_string().dimmed(),
files_to_extract.len().to_string().bold()
);
}
let extract_start = Instant::now();
let extractions: Vec<Extraction> = files_to_extract
.par_iter()
.filter_map(|path| {
let ext = path.extension()?.to_str()?;
let lang = SupportedLanguage::from_extension(ext)?;
match extract::extract_file(path, lang) {
Ok(extraction) => Some(extraction),
Err(e) => {
eprintln!(" {} {}: {}", "⚠".yellow(), path.display(), e);
None
}
}
})
.collect();
let total_nodes: usize = extractions.iter().map(|e| e.nodes.len()).sum();
let total_edges: usize = extractions.iter().map(|e| e.edges.len()).sum();
let extract_time = extract_start.elapsed();
println!(
" {} Extracted {} nodes, {} edges in {:.1}s",
"✓".green(),
total_nodes.to_string().bold(),
total_edges.to_string().bold(),
extract_time.as_secs_f64()
);
let mut kg = KnowledgeGraph::from_extractions(&extractions);
println!(
" {} Graph: {} nodes, {} edges",
"✓".green(),
kg.node_count().to_string().bold(),
kg.edge_count().to_string().bold()
);
let communities = cluster::cluster(&kg);
kg.set_communities(&communities);
println!(
" {} {} communities detected",
"✓".green(),
communities.len().to_string().bold()
);
let analysis = analyze::analyze(&kg, &communities);
if let Some(top) = analysis.god_nodes.first() {
println!(
" {} God node: {} ({} connections)",
"★".bright_yellow(),
top.label.bold(),
top.degree
);
}
let out_dir = root.join(OUTPUT_DIR);
export::export_json(&kg, &communities, &analysis, &out_dir)
.expect("Failed to write graph.json");
export::export_html(&kg, &communities, &analysis, &out_dir)
.expect("Failed to write graph.html");
let report_content = report::render_report(&analysis, &communities);
std::fs::write(out_dir.join("GRAPH_REPORT.md"), &report_content)
.expect("Failed to write GRAPH_REPORT.md");
if !cli.no_cache {
cache::save_manifest(&cache_dir, &manifest);
}
let total_time = start.elapsed();
println!();
println!(
" {} {}",
"✓".green().bold(),
"Output:".bold()
);
println!(" {} graph.json — queryable graph data", "→".dimmed());
println!(" {} graph.html — interactive visualization", "→".dimmed());
println!(" {} GRAPH_REPORT.md — god nodes, communities, questions", "→".dimmed());
println!();
println!(
" {} in {:.2}s",
"Done".green().bold(),
total_time.as_secs_f64()
);
println!();
}