use anyhow::{bail, Result};
use clap::{Parser, Subcommand, ValueEnum};
use ai_refactor_cli::ast;
use ai_refactor_cli::detectors;
use ai_refactor_cli::report;
use ai_refactor_cli::scanner;
#[derive(Parser, Debug)]
#[command(
name = "ai-refactor",
version,
about = "Rule-based legacy code refactoring CLI",
long_about = "Scan and rewrite legacy code patterns using tree-sitter AST analysis. \
v0.2.0 adds real --apply for Django FBV→CBV conversion."
)]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand, Debug)]
enum Command {
Scan {
path: String,
#[arg(long, value_enum, default_value_t = Format::Text)]
format: Format,
#[arg(long)]
rule: Option<String>,
},
Fix {
path: String,
#[arg(long)]
rule: String,
#[arg(long, default_value_t = false)]
apply: bool,
#[arg(long, default_value_t = false)]
dry_run: bool,
},
}
#[derive(Copy, Clone, Debug, ValueEnum)]
enum Format {
Text,
Json,
}
#[tokio::main]
async fn main() -> Result<()> {
let cli = Cli::parse();
match cli.command {
Command::Scan { path, format, rule } => {
let findings = scanner::scan_path(&path, rule.as_deref())?;
match format {
Format::Text => report::print_text(&findings),
Format::Json => report::print_json(&findings)?,
}
if !findings.is_empty() {
std::process::exit(1);
}
}
Command::Fix {
path,
rule,
apply,
dry_run,
} => {
let do_apply = apply && !dry_run;
let findings = scanner::scan_path(&path, Some(&rule))?;
report::print_text(&findings);
if findings.is_empty() {
return Ok(());
}
if !do_apply {
eprintln!(
"[ai-refactor] Dry-run mode: {} finding(s) detected. \
Re-run with --apply to write changes.",
findings.len()
);
std::process::exit(1);
}
match rule.as_str() {
"django-fbv" => {
apply_django_fbv(&path)?;
}
other => {
bail!(
"--apply is not yet implemented for rule `{}`. \
Supported: django-fbv",
other
);
}
}
if !findings.is_empty() {
std::process::exit(1);
}
}
}
Ok(())
}
fn apply_django_fbv(path: &str) -> Result<()> {
use std::path::Path;
use walkdir::WalkDir;
let mut py_parser = ast::python::make_parser()?;
let root = Path::new(path);
let walker: Box<dyn Iterator<Item = walkdir::DirEntry>> = if root.is_file() {
Box::new(WalkDir::new(root).into_iter().filter_map(|e| e.ok()))
} else {
Box::new(
WalkDir::new(root)
.into_iter()
.filter_entry(|e| {
e.depth() == 0
|| !{
let name = e.file_name().to_string_lossy();
name.starts_with('.')
|| matches!(
name.as_ref(),
"node_modules"
| "target"
| "dist"
| "build"
| "__pycache__"
| "venv"
| ".venv"
)
}
})
.filter_map(|e| e.ok()),
)
};
for entry in walker {
if !entry.file_type().is_file() {
continue;
}
if entry.path().extension().and_then(|s| s.to_str()) != Some("py") {
continue;
}
let file_path = entry.path().display().to_string();
let result = detectors::django::fbv_to_cbv::apply(&file_path, &mut py_parser)?;
for msg in &result.rewrites_applied {
println!("[ai-refactor] Applied: {}", msg);
}
for warn in &result.warnings {
eprintln!("[ai-refactor] Warning: {}", warn);
}
if let Some(bak) = &result.backup_path {
println!("[ai-refactor] Backup written to {}", bak);
}
}
Ok(())
}