use std::fs::{self, File}; use std::io::{self, Read, Write}; use std::path::{Path, PathBuf}; use std::process::Command; /// Directories to always skip, regardless of .gitignore const IGNORED_DIRS: &[&str] = &[ "target", "node_modules", ".git", "dist", "build", ".next", ".nuxt", ".cache", ".turbo", "out", "pkg", "vendor", "coverage", "__pycache__", ]; /// Files to always skip const IGNORED_FILES: &[&str] = &[ "package-lock.json", "yarn.lock", "pnpm-lock.yaml", "Cargo.lock", "concatenated_code.txt", ]; fn is_ignored_dir(path: &Path) -> bool { path.file_name() .and_then(|name| name.to_str()) .map(|name| IGNORED_DIRS.contains(&name)) .unwrap_or(false) } fn is_ignored_file(path: &Path) -> bool { path.file_name() .and_then(|name| name.to_str()) .map(|name| IGNORED_FILES.contains(&name)) .unwrap_or(false) } fn is_hidden(path: &Path) -> bool { path.file_name() .and_then(|name| name.to_str()) .map(|name| name.starts_with('.')) .unwrap_or(false) } fn is_git_ignored(root: &Path, path: &Path) -> bool { let relative_path = path.strip_prefix(root).unwrap_or(path); let relative_path_str = match relative_path.to_str() { Some(s) => s, None => return false, }; let output = Command::new("git") .args(["check-ignore", "-q", relative_path_str]) .current_dir(root) .output(); match output { Ok(output) => output.status.success(), Err(_) => false, // git not available — don't skip } } fn should_skip(root: &Path, path: &Path) -> bool { if is_hidden(path) { println!(" skip (hidden): {}", path.display()); return true; } if path.is_dir() && is_ignored_dir(path) { println!(" skip (ignored dir): {}", path.display()); return true; } if path.is_file() && is_ignored_file(path) { println!(" skip (ignored file): {}", path.display()); return true; } if is_git_ignored(root, path) { println!(" skip (gitignored): {}", path.display()); return true; } false } const VALID_EXTENSIONS: &[&str] = &[ "rs", "toml", "mjs", "md", "txt", "js", "ts", "tsx", "html", "css", "json", "yaml", "yml", "env", "sh", "sql", "graphql", "proto", "dockerfile", ]; fn has_valid_extension(path: &Path) -> bool { // Also allow files with no extension but known names let known_extensionless = &["Makefile", "Dockerfile", "Justfile", ".env", ".env.example"]; if let Some(name) = path.file_name().and_then(|n| n.to_str()) { if known_extensionless.contains(&name) { return true; } } path.extension() .and_then(|ext| ext.to_str()) .map(|ext| VALID_EXTENSIONS.contains(&ext.to_lowercase().as_str())) .unwrap_or(false) } fn copy_files_to_single_file( source_dir: &Path, output_file: &mut File, root: &Path, output_file_abs_path: &Path, ) -> Result<(), io::Error> { // Read and sort entries for deterministic output let mut entries: Vec<_> = fs::read_dir(source_dir)? .filter_map(|e| e.ok()) .collect(); entries.sort_by_key(|e| e.path()); for entry in entries { let path = entry.path(); if should_skip(root, &path) { continue; } if path.is_dir() { copy_files_to_single_file(&path, output_file, root, output_file_abs_path)?; } else if path.is_file() { // Skip the output file itself let file_abs_path = fs::canonicalize(&path)?; if file_abs_path == output_file_abs_path { println!(" skip (output file): {}", path.display()); continue; } if !has_valid_extension(&path) { println!(" skip (extension): {}", path.display()); continue; } let relative_path = path.strip_prefix(root).unwrap_or(&path); writeln!(output_file, "// File: {}\n", relative_path.display())?; let mut file = File::open(&path)?; let mut contents = String::new(); file.read_to_string(&mut contents)?; writeln!(output_file, "{}\n", contents)?; println!(" ✓ included: {}", relative_path.display()); } } Ok(()) } fn main() -> Result<(), io::Error> { // let project_root = PathBuf::from("./src/gateway/io_backend"); // let project_root = PathBuf::from("./src/gateway/optimized"); // let project_root = PathBuf::from("./src/gateway/inbox"); // let project_root = PathBuf::from("./src/gateway/inbox"); // let project_root = PathBuf::from("./crates/kra-etims-sdk"); // let project_root = PathBuf::from("./src/observability"); // let project_root = PathBuf::from("./src/monitoring"); let project_root = PathBuf::from("./src/routes"); // let project_root = PathBuf::from("./src/proxy"); // let project_root = PathBuf::from("./src"); let output_file_path = project_root.join("concatenated_code.txt"); // let output_file_abs_path = fs::canonicalize(&output_file_path).unwrap_or_else(|_| { // std::env::current_dir() // .unwrap() // .join("concatenated_code.txt") // });/Users/aok/Projects/rustdev/CratesCode/nimanyatta/src/gateway/optimized // Ensure the output file exists so canonicalize works if !output_file_path.exists() { File::create(&output_file_path)?; } let output_file_abs_path = fs::canonicalize(&output_file_path)?; let mut output_file = File::create(&output_file_path)?; println!("╔══════════════════════════════════════════╗"); println!("║ Code Concatenation Tool ║"); println!("╚══════════════════════════════════════════╝"); println!(); println!("Root: {}", project_root.display()); println!("Output: {}", output_file_abs_path.display()); println!(); println!("Ignored dirs: {}", IGNORED_DIRS.join(", ")); println!("Ignored files: {}", IGNORED_FILES.join(", ")); println!("Valid exts: {}", VALID_EXTENSIONS.join(", ")); println!(); copy_files_to_single_file( &project_root, &mut output_file, &project_root, &output_file_abs_path, )?; // Print summary let output_size = fs::metadata(&output_file_path)?.len(); println!(); println!("════════════════════════════════════════════"); println!( "Done. Output: {} ({} bytes)", output_file_path.display(), output_size ); Ok(()) }