use inquire::{set_global_render_config, Confirm, Select};
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use thag_styling::{auto_help, help_system::check_help_and_exit, themed_inquire_config};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let help = auto_help!();
check_help_and_exit(&help);
set_global_render_config(themed_inquire_config());
println!("đ§ Tool Migration Helper");
println!("This tool helps migrate existing tools from tools/ to src/bin/\n");
let tools_dir = Path::new("tools");
if !tools_dir.exists() {
println!("â tools/ directory not found");
return Ok(());
}
let tools = find_rust_files(tools_dir)?;
if tools.is_empty() {
println!("â No .rs files found in tools/ directory");
return Ok(());
}
println!("đ Found {} tool(s) in tools/:", tools.len());
for tool in &tools {
println!(" âĸ {}", tool.display());
}
let tool_names: Vec<String> = tools
.iter()
.map(|p| p.file_name().unwrap().to_string_lossy().to_string())
.collect();
let selected = Select::new("Select tool to migrate:", tool_names).prompt()?;
let source_path = tools_dir.join(&selected);
let dest_path = Path::new("src/bin").join(&selected);
println!("\nđ Migration Plan:");
println!(" Source: {}", source_path.display());
println!(" Destination: {}", dest_path.display());
if dest_path.exists() {
println!("â ī¸ Destination file already exists!");
if !Confirm::new("Overwrite existing file?")
.with_default(false)
.prompt()?
{
println!("â Migration cancelled");
return Ok(());
}
}
if !Confirm::new("Proceed with migration?")
.with_default(true)
.prompt()?
{
println!("â Migration cancelled");
return Ok(());
}
let is_git_repo = check_git_repo();
migrate_tool(&source_path, &dest_path, is_git_repo)?;
let cargo_updated = update_cargo_toml(&selected)?;
println!("\nâ
Migration completed successfully!");
if cargo_updated {
println!("â
Cargo.toml updated with new [[bin]] entry");
}
println!(
" 1. Test the migrated tool: cargo build --bin {} --features tools",
selected.trim_end_matches(".rs")
);
println!(
" 2. Test help: ./target/debug/{} --help",
selected.trim_end_matches(".rs")
);
if is_git_repo {
println!(" 3. The file has been moved with git to preserve history");
println!(
" 4. Commit the changes: git commit -m 'Migrate {} to src/bin with auto_help'",
selected
);
} else {
println!(" 3. Remove the original file from tools/ when satisfied");
println!(" (Note: Not in a git repo, so history wasn't preserved)");
}
Ok(())
}
fn find_rust_files(dir: &Path) -> Result<Vec<PathBuf>, std::io::Error> {
let mut rust_files = Vec::new();
for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if path.is_file() && path.extension().is_some_and(|ext| ext == "rs") {
rust_files.push(path);
}
}
rust_files.sort();
Ok(rust_files)
}
fn migrate_tool(
source: &Path,
dest: &Path,
use_git: bool,
) -> Result<(), Box<dyn std::error::Error>> {
println!("\nđ Starting migration...");
if use_git {
println!("đ Using git mv to preserve file history...");
if let Some(parent) = dest.parent() {
fs::create_dir_all(parent)?;
}
let git_mv_result = Command::new("git")
.args(["mv", &source.to_string_lossy(), &dest.to_string_lossy()])
.output()?;
if !git_mv_result.status.success() {
let error = String::from_utf8_lossy(&git_mv_result.stderr);
return Err(format!("Git mv failed: {}", error).into());
}
println!("â
File moved with git mv");
let content = fs::read_to_string(dest)?;
let transformed = transform_tool_content(&content);
fs::write(dest, transformed)?;
println!("â
File transformed with auto-help integration");
let git_add_result = Command::new("git")
.args(["add", &dest.to_string_lossy()])
.output()?;
if git_add_result.status.success() {
println!("â
Changes staged for commit");
} else {
println!("â ī¸ Warning: Could not stage changes automatically");
}
} else {
println!("đ Using regular file copy (not in git repo)...");
let content = fs::read_to_string(source)?;
let transformed = transform_tool_content(&content);
if let Some(parent) = dest.parent() {
fs::create_dir_all(parent)?;
}
fs::write(dest, transformed)?;
println!("â
File copied and transformed");
}
Ok(())
}
fn transform_tool_content(content: &str) -> String {
let lines: Vec<&str> = content.lines().collect();
let mut transformed = Vec::new();
let mut help_added = false;
for (i, &line) in lines.iter().enumerate() {
transformed.push(line.to_string());
if line.contains("fn main(") && !help_added {
let has_thag_import = lines
.iter()
.any(|l| l.contains(r"use thag_rs::{{auto_help"));
if !has_thag_import {
if let Some(toml_end) = find_toml_block_end(&lines) {
let deps_line = r#"thag_rs = { version = "0.2, thag-auto", default-features = false, features = ["core", "simplelog"] }"#.to_string();
transformed.insert(toml_end, deps_line);
}
if let Some(last_use_idx) = find_last_use_statement(&lines) {
transformed.insert(
last_use_idx + 2,
"use thag_rs::{auto_help, help_system::check_help_and_exit};".to_string(),
);
}
}
if i + 1 < lines.len() {
transformed.push(
" // Check for help first - automatically extracts from source comments"
.to_string(),
);
transformed.push(" let help = auto_help!();".to_string());
transformed.push(" check_help_and_exit(&help);".to_string());
transformed.push(String::new()); }
help_added = true;
}
}
transformed.join("\n")
}
fn find_toml_block_end(lines: &[&str]) -> Option<usize> {
let mut in_toml = false;
for (i, line) in lines.iter().enumerate() {
if line.contains("/*[toml]") {
in_toml = true;
} else if in_toml && line.contains("*/") {
return Some(i);
}
}
None
}
fn find_last_use_statement(lines: &[&str]) -> Option<usize> {
let mut last_use = None;
for (i, line) in lines.iter().enumerate() {
if line.trim().starts_with("fn ") {
break;
}
if line.trim().starts_with("use ") {
last_use = Some(i);
}
}
last_use
}
fn check_git_repo() -> bool {
let result = Command::new("git").args(["status", "--porcelain"]).output();
match result {
Ok(output) => output.status.success(),
Err(_) => false,
}
}
fn update_cargo_toml(tool_name: &str) -> Result<bool, Box<dyn std::error::Error>> {
let cargo_path = Path::new("Cargo.toml");
if !cargo_path.exists() {
println!("â ī¸ Cargo.toml not found, skipping update");
return Ok(false);
}
let content = fs::read_to_string(cargo_path)?;
let tool_name_without_ext = tool_name.trim_end_matches(".rs");
let bin_entry = format!(r#"name = "{}""#, tool_name_without_ext);
if content.contains(&bin_entry) {
println!(
"âšī¸ Cargo.toml already contains entry for {}",
tool_name_without_ext
);
return Ok(false);
}
let lines: Vec<&str> = content.lines().collect();
let mut insert_index = None;
for (i, line) in lines.iter().enumerate() {
if line.starts_with("[[bin]]") {
for j in (i + 1)..lines.len() {
if lines[j].starts_with("[[bin]]") || lines[j].starts_with('[') {
insert_index = Some(j);
break;
} else if j == lines.len() - 1 {
insert_index = Some(lines.len());
break;
}
}
}
}
if let Some(index) = insert_index {
let mut new_lines = lines[..index].to_vec();
new_lines.push("");
new_lines.push("[[bin]]");
let var_name = format!(r#"name = "{}""#, tool_name_without_ext);
new_lines.push(&var_name);
let var_name = format!(r#"path = "src/bin/{}""#, tool_name);
new_lines.push(&var_name);
new_lines.push(r#"required-features = ["tools"]"#);
new_lines.extend(&lines[index..]);
let new_content = new_lines.join("\n");
fs::write(cargo_path, new_content)?;
println!(
"đ Added [[bin]] entry for {} to Cargo.toml",
tool_name_without_ext
);
return Ok(true);
}
println!("â ī¸ Could not find appropriate location to insert [[bin]] entry");
Ok(false)
}