use std::path::{Path, PathBuf};
use anyhow::Result;
use clap::{Parser, Subcommand, ValueEnum};
use skeletree_engine as engine;
use skeletree_store::Store;
#[derive(Parser)]
#[command(
name = "skeletree",
version,
about = "Queryable graph of your repo for AI agents"
)]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
Index {
#[arg(default_value = ".")]
path: PathBuf,
},
Serve {
#[arg(default_value = ".")]
path: PathBuf,
#[arg(long)]
watch: bool,
},
Export {
#[arg(long, value_enum, default_value_t = ExportFormat::Md)]
format: ExportFormat,
#[arg(long, default_value_t = 2000)]
budget: usize,
},
Stats {
#[arg(default_value = ".")]
path: PathBuf,
#[arg(long, default_value_t = 20)]
limit: usize,
},
Init,
}
#[derive(Copy, Clone, ValueEnum)]
enum ExportFormat {
Md,
Json,
Mermaid,
}
const BANNER: &str = r"
_____ _ _ _
/ ____| | | | | |
| (___ | | _____| | ___| |_ _ __ ___ ___
\___ \| |/ / _ \ |/ _ \ __| '__/ _ \/ _ \
____) | < __/ | __/ |_| | | __/ __/
|_____/|_|\_\___|_|\___|\__|_| \___|\___|
";
fn main() -> Result<()> {
tracing_subscriber::fmt()
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
.init();
eprintln!("{BANNER}");
let cli = Cli::parse();
match cli.command {
Command::Index { path } => {
let db = engine::default_db_path(&path);
if let Some(dir) = db.parent() {
std::fs::create_dir_all(dir)?;
let ignore = dir.join(".gitignore");
if !ignore.exists() {
std::fs::write(&ignore, "*\n")?;
}
}
let mut store = Store::open(&db)?;
let stats = engine::index(&path, &mut store)?;
println!(
"indexed {} files, {} symbols ({} skipped) → {}",
stats.files,
stats.symbols,
stats.skipped,
db.display()
);
Ok(())
}
Command::Serve { path, watch: _ } => {
let db = engine::default_db_path(&path);
let store = open_existing(&db)?;
skeletree_mcp::serve_blocking(store)?;
Ok(())
}
Command::Export { format: _, budget } => todo!("export (budget={budget})"),
Command::Stats { path, limit } => {
let store = open_existing(&engine::default_db_path(&path))?;
for sym in store.top_symbols(limit)? {
println!("{:>8.4} {:<9} {}", sym.rank, sym.kind, sym.name);
}
Ok(())
}
Command::Init => todo!("init"),
}
}
fn open_existing(db: &Path) -> Result<Store> {
if !db.exists() {
anyhow::bail!("no index at {} — run `skeletree index` first", db.display());
}
Ok(Store::open(db)?)
}