use clap::{Parser, ValueEnum};
use skelecode::ir::Language;
use skelecode::renderer::Renderer;
use skelecode::renderer::machine::MachineRenderer;
use skelecode::renderer::obsidian::ObsidianRenderer;
use std::path::PathBuf;
#[derive(Debug, Clone, ValueEnum)]
enum OutputFormat {
Vault,
Machine,
Both,
}
#[derive(Debug, Clone, ValueEnum)]
enum LangFilter {
Rust,
#[value(alias = "java", alias = "kotlin", alias = "kt")]
JavaBased,
#[value(alias = "javascript", alias = "typescript", alias = "ts")]
JsTs,
#[value(alias = "python", alias = "py")]
Python,
}
impl LangFilter {
fn to_languages(&self) -> Vec<Language> {
match self {
LangFilter::Rust => vec![Language::Rust],
LangFilter::JavaBased => vec![Language::Java, Language::Kotlin],
LangFilter::JsTs => vec![Language::JavaScript],
LangFilter::Python => vec![Language::Python],
}
}
}
#[derive(Parser, Debug)]
#[command(name = "skelecode", version, about)]
struct Cli {
path: Option<PathBuf>,
#[arg(short, long)]
format: Option<OutputFormat>,
#[arg(short, long)]
output: Option<PathBuf>,
#[arg(long)]
output_vault: Option<PathBuf>,
#[arg(long)]
output_machine: Option<PathBuf>,
#[arg(short, long)]
lang: Vec<LangFilter>,
#[arg(short, long)]
exclude: Vec<String>,
#[arg(long)]
tui: bool,
#[arg(short, long)]
verbose: bool,
}
fn main() {
let cli = Cli::parse();
let want_cli_output = cli.format.is_some()
|| cli.output.is_some()
|| cli.output_vault.is_some()
|| cli.output_machine.is_some();
let use_tui = cli.tui || !want_cli_output;
if use_tui {
match &cli.path {
None => {
if let Err(e) = skelecode::tui::run_tui_welcome() {
eprintln!("TUI error: {}", e);
std::process::exit(1);
}
}
Some(path) => {
validate_path(path);
if cli.verbose {
eprintln!("Scanning {}...", path.display());
}
let languages: Vec<Language> =
cli.lang.iter().flat_map(|l| l.to_languages()).collect();
let project = skelecode::scan_project(path, &languages, &cli.exclude);
if cli.verbose {
print_scan_stats(&project);
}
if let Err(e) = skelecode::tui::run_tui(project) {
eprintln!("TUI error: {}", e);
std::process::exit(1);
}
}
}
return;
}
let path = cli.path.as_ref().unwrap_or_else(|| {
eprintln!("Error: <PATH> is required in non-interactive (--format) mode.");
eprintln!("Usage: skelecode <PATH> [OPTIONS]");
std::process::exit(1);
});
validate_path(path);
if cli.verbose {
eprintln!("Scanning {}...", path.display());
}
let languages: Vec<Language> = cli.lang.iter().flat_map(|l| l.to_languages()).collect();
let project = skelecode::scan_project(path, &languages, &cli.exclude);
if cli.verbose {
print_scan_stats(&project);
}
let format = cli.format.unwrap_or(OutputFormat::Both);
let vault_output = match format {
OutputFormat::Vault | OutputFormat::Both => Some(ObsidianRenderer.render(&project)),
_ => None,
};
let machine_output = match format {
OutputFormat::Machine | OutputFormat::Both => Some(MachineRenderer.render(&project)),
_ => None,
};
if let Some(ref path) = cli.output_vault
&& let Some(skelecode::renderer::RenderOutput::Multiple(files)) = vault_output.as_ref()
{
write_vault(path, files);
}
if let Some(ref path) = cli.output_machine
&& let Some(skelecode::renderer::RenderOutput::Single(content)) = machine_output.as_ref()
{
write_file(path, content);
}
let mut general_machine = None;
if let Some(skelecode::renderer::RenderOutput::Single(content)) = machine_output {
if cli.output_machine.is_none() {
general_machine = Some(content);
}
}
if let Some(skelecode::renderer::RenderOutput::Multiple(files)) = vault_output {
if cli.output_vault.is_none() {
if let Some(ref path) = cli.output {
write_vault(path, &files);
} else {
eprintln!("Warning: Cannot write Vault format to stdout. Use --output or --output-vault.");
}
}
}
if let Some(content) = general_machine {
if let Some(ref path) = cli.output {
if path.is_dir() {
write_file(&path.join("MachineContext.md"), &content);
} else {
write_file(path, &content);
}
} else {
println!("{}", content);
}
}
}
fn validate_path(path: &PathBuf) {
if !path.exists() {
eprintln!("Error: path '{}' does not exist", path.display());
std::process::exit(1);
}
if !path.is_dir() {
eprintln!("Error: '{}' is not a directory", path.display());
std::process::exit(1);
}
}
fn print_scan_stats(project: &skelecode::ir::Project) {
let type_count: usize = project.modules.iter().map(|m| m.types.len()).sum();
let fn_count: usize = project.modules.iter().map(|m| m.functions.len()).sum();
eprintln!(
"Found {} modules, {} types, {} free functions",
project.modules.len(),
type_count,
fn_count,
);
}
fn write_file(path: &PathBuf, content: &str) {
if let Err(e) = std::fs::write(path, content) {
eprintln!("Error writing to {}: {}", path.display(), e);
std::process::exit(1);
}
}
fn write_vault(base_path: &PathBuf, files: &[(PathBuf, String)]) {
if let Err(e) = std::fs::create_dir_all(base_path) {
eprintln!("Error creating directory {}: {}", base_path.display(), e);
std::process::exit(1);
}
let modules_dir = base_path.join("modules");
let types_dir = base_path.join("types");
let _ = std::fs::create_dir_all(&modules_dir);
let _ = std::fs::create_dir_all(&types_dir);
for (rel_path, content) in files {
let full_path = base_path.join(rel_path);
if let Err(e) = std::fs::write(&full_path, content) {
eprintln!("Error writing vault file {}: {}", full_path.display(), e);
}
}
}