pub mod refac;
pub mod scrap;
pub mod st8;
pub mod ldiff;
pub mod fleet;
pub mod logging;
pub mod workspace_state;
pub mod entities;
pub mod mcp_protocol;
pub mod code_analysis;
pub mod interactive_tree;
use anyhow::{Context, Result};
use std::path::Path;
pub fn get_version() -> &'static str {
include_str!("../version.txt").trim()
}
pub use refac::cli as cli;
pub use refac::cli::{Args, Mode};
pub use refac::rename_engine::RenameEngine;
pub use refac::patterns_file::PatternPair;
pub use scrap::scrap_common::{ScrapMetadata, ScrapEntry};
pub use scrap::{run_scrap, run_unscrap};
pub use ldiff::run_ldiff;
pub use st8::{St8Config, VersionInfo};
pub fn run_refac(args: Args) -> Result<()> {
use refac::patterns_file::{PatternPair, parse_patterns_file};
let pairs: Vec<PatternPair> = match (&args.pattern, &args.substitute, &args.patterns_file) {
(Some(p), Some(s), None) => {
vec![PatternPair {
pattern: p.clone(),
substitute: s.clone(),
}]
}
(None, None, Some(pf)) => {
parse_patterns_file(pf)?
}
_ => {
anyhow::bail!("Invalid argument combination: provide PATTERN SUBSTITUTE or --patterns-file");
}
};
if args.should_process_names() {
let invalid_chars = ['/', '\\', ':', '*', '?', '"', '<', '>', '|', '\0'];
for (idx, pair) in pairs.iter().enumerate() {
if let Some(ch) = pair.substitute.chars().find(|c| invalid_chars.contains(c)) {
anyhow::bail!(
"Pattern pair {} ('{}' -> '{}'): substitute cannot contain path-unsafe characters ({}) when processing names. Use --content-only.",
idx + 1, pair.pattern, pair.substitute, ch
);
}
}
}
let engine = RenameEngine::new_with_pairs(args, pairs)?;
engine.execute()
}
#[derive(Debug, Clone)]
pub struct RenameItem {
pub original_path: std::path::PathBuf,
pub new_path: std::path::PathBuf,
pub item_type: ItemType,
pub depth: usize,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ItemType {
File,
Directory,
}
#[derive(Debug, Clone)]
pub struct RenameConfig {
pub root_dir: std::path::PathBuf,
pub pattern: String,
pub substitute: String,
pub assume_yes: bool,
pub verbose: bool,
pub follow_symlinks: bool,
pub backup: bool,
}
impl RenameConfig {
pub fn new<P: AsRef<Path>>(
root_dir: P,
pattern: String,
substitute: String,
) -> Result<Self> {
let root_path = root_dir.as_ref().canonicalize()
.with_context(|| format!("Failed to resolve root directory: {}", root_dir.as_ref().display()))?;
if pattern.is_empty() {
anyhow::bail!("Pattern cannot be empty");
}
if substitute.is_empty() {
anyhow::bail!("Substitute cannot be empty");
}
Ok(Self {
root_dir: root_path,
pattern,
substitute,
assume_yes: false,
verbose: false,
follow_symlinks: false,
backup: false,
})
}
pub fn with_assume_yes(mut self, assume_yes: bool) -> Self {
self.assume_yes = assume_yes;
self
}
pub fn with_verbose(mut self, verbose: bool) -> Self {
self.verbose = verbose;
self
}
pub fn with_follow_symlinks(mut self, follow_symlinks: bool) -> Self {
self.follow_symlinks = follow_symlinks;
self
}
pub fn with_backup(mut self, backup: bool) -> Self {
self.backup = backup;
self
}
}
#[derive(Debug, Default, Clone)]
pub struct RenameStats {
pub files_with_content_changes: usize,
pub files_renamed: usize,
pub directories_renamed: usize,
pub files_processed: usize,
pub errors: Vec<String>,
}
impl RenameStats {
pub fn total_changes(&self) -> usize {
self.files_with_content_changes + self.files_renamed + self.directories_renamed
}
pub fn add_error(&mut self, error: String) {
self.errors.push(error);
}
}
pub mod utils {
use std::path::Path;
pub fn calculate_depth(path: &Path, root: &Path) -> usize {
path.strip_prefix(root)
.map(|p| p.components().count())
.unwrap_or(0)
}
pub fn contains_pattern(text: &str, pattern: &str) -> bool {
text.contains(pattern)
}
pub fn replace_all(text: &str, old: &str, new: &str) -> String {
text.replace(old, new)
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_rename_config_creation() {
let temp_dir = TempDir::new().unwrap();
let config = RenameConfig::new(
temp_dir.path(),
"old".to_string(),
"new".to_string(),
).unwrap();
assert_eq!(config.pattern, "old");
assert_eq!(config.substitute, "new");
assert!(!config.assume_yes);
}
#[test]
fn test_rename_config_validation() {
let temp_dir = TempDir::new().unwrap();
assert!(RenameConfig::new(temp_dir.path(), "".to_string(), "new".to_string()).is_err());
assert!(RenameConfig::new(temp_dir.path(), "old".to_string(), "".to_string()).is_err());
assert!(RenameConfig::new(temp_dir.path(), "old".to_string(), "new/path".to_string()).is_ok());
assert!(RenameConfig::new(temp_dir.path(), "old".to_string(), "new\\path".to_string()).is_ok());
}
#[test]
fn test_utils_replace_all() {
assert_eq!(utils::replace_all("hello world", "l", "x"), "hexxo worxd");
assert_eq!(utils::replace_all("test test test", "test", "demo"), "demo demo demo");
assert_eq!(utils::replace_all("no match", "xyz", "abc"), "no match");
}
#[test]
fn test_utils_contains_pattern() {
assert!(utils::contains_pattern("hello world", "hello"));
assert!(utils::contains_pattern("hello world", "world"));
assert!(utils::contains_pattern("hello world", "o w"));
assert!(!utils::contains_pattern("hello world", "xyz"));
}
#[test]
fn test_get_version() {
let version = get_version();
assert!(!version.is_empty());
assert!(version.contains('.'));
let parts: Vec<&str> = version.split('.').collect();
assert!(parts.len() >= 2, "Version should have at least major.minor format");
for part in &parts {
assert!(part.parse::<u32>().is_ok(), "Version part '{}' should be numeric", part);
}
}
#[test]
fn test_version_consistency() {
let version_content = std::fs::read_to_string("version.txt").expect("version.txt should exist");
let file_version = version_content.trim();
assert_eq!(get_version(), file_version);
}
}