#!/usr/bin/env python3 import os import json import subprocess import argparse from pathlib import Path import fnmatch class TreeClone: def __init__(self): self.default_exclude_dirs = {'.git', '__pycache__', 'node_modules', '.DS_Store', '.vscode'} self.noclone_filename = '.noclone' def parse_noclone_file(self, noclone_path): """Parse .noclone file and return patterns to exclude.""" patterns = [] try: with open(noclone_path, 'r') as f: for line in f: line = line.strip() # Skip empty lines and comments if line and not line.startswith('#'): patterns.append(line) except FileNotFoundError: pass return patterns def collect_noclone_patterns(self, root_path): """Collect all .noclone patterns from root and subdirectories.""" all_patterns = {} root = Path(root_path) # Find all .noclone files in the directory tree for noclone_file in root.rglob(self.noclone_filename): # Get patterns from this .noclone file patterns = self.parse_noclone_file(noclone_file) if patterns: # Store patterns with their directory context relative_dir = noclone_file.parent.relative_to(root) all_patterns[str(relative_dir)] = patterns return all_patterns def should_exclude_item(self, item_path, root_path, noclone_patterns): """Check if an item should be excluded based on .noclone patterns.""" # Check default exclusions if any(excluded in item_path.parts for excluded in self.default_exclude_dirs): return True root = Path(root_path) relative_path = item_path.relative_to(root) # Check patterns from all applicable .noclone files for pattern_dir, patterns in noclone_patterns.items(): pattern_path = Path(pattern_dir) # Check if this pattern directory applies to our item try: # If the item is in this directory or a subdirectory if pattern_path == Path('.') or pattern_path in relative_path.parents or pattern_path == relative_path.parent: # Test each pattern for pattern in patterns: # Support both file names and paths if fnmatch.fnmatch(item_path.name, pattern) or fnmatch.fnmatch(str(relative_path), pattern): return True except ValueError: # relative_path calculation failed, skip this pattern continue return False def capture_with_tree(self, directory, output_file): """Capture using tree command if available.""" try: result = subprocess.run( ['tree', '-J', directory], capture_output=True, text=True, check=True ) structure = json.loads(result.stdout) with open(output_file, 'w') as f: json.dump(structure, f, indent=2) return True except (subprocess.CalledProcessError, FileNotFoundError, json.JSONDecodeError): return False def capture_manual(self, directory, output_file, include_files=True, max_depth=None): """Manual capture without tree command, respecting .noclone files.""" root = Path(directory) # Collect all .noclone patterns noclone_patterns = self.collect_noclone_patterns(root) print(f"Found .noclone patterns: {noclone_patterns}") def scan_directory(path, current_depth=0): if max_depth is not None and current_depth > max_depth: return [] items = [] try: for item in sorted(path.iterdir()): # Check if item should be excluded if self.should_exclude_item(item, root, noclone_patterns): print(f"Excluding: {item.relative_to(root)}") continue if item.is_dir(): children = scan_directory(item, current_depth + 1) items.append({ 'name': item.name, 'type': 'directory', 'children': children }) elif include_files: items.append({ 'name': item.name, 'type': 'file' }) except PermissionError: pass return items structure = { 'root': root.name, 'noclone_patterns': noclone_patterns, 'structure': scan_directory(root) } with open(output_file, 'w') as f: json.dump(structure, f, indent=2) def recreate_structure(self, structure_file, target_directory, create_files=True): """Recreate directory structure from JSON.""" with open(structure_file, 'r') as f: structure = json.load(f) target = Path(target_directory) def create_items(items, current_path): for item in items: item_path = current_path / item['name'] if item['type'] == 'directory': item_path.mkdir(parents=True, exist_ok=True) print(f"📁 {item_path}") if 'children' in item: create_items(item['children'], item_path) elif item['type'] == 'file' and create_files: item_path.touch() print(f"📄 {item_path}") root_path = target / structure['root'] root_path.mkdir(parents=True, exist_ok=True) # Recreate .noclone files if they were captured if 'noclone_patterns' in structure: for pattern_dir, patterns in structure['noclone_patterns'].items(): noclone_path = root_path / pattern_dir / self.noclone_filename noclone_path.parent.mkdir(parents=True, exist_ok=True) with open(noclone_path, 'w') as f: f.write("# .noclone file - patterns to exclude from cloning\n") for pattern in patterns: f.write(f"{pattern}\n") print(f"📝 Created .noclone: {noclone_path}") if 'structure' in structure: create_items(structure['structure'], root_path) return root_path def create_sample_noclone(self, directory): """Create a sample .noclone file with common patterns.""" noclone_path = Path(directory) / self.noclone_filename sample_content = """# .noclone file - patterns to exclude from TreeClone # Lines starting with # are comments # Supports glob patterns like *.log, temp*, etc. # Common files to exclude *.log *.tmp *.cache *.pid .env .env.local # Directories to exclude temp/ logs/ cache/ build/ dist/ coverage/ # IDE and editor files .vscode/ .idea/ *.swp *.swo *~ # OS files .DS_Store Thumbs.db desktop.ini # Add your custom patterns below: """ with open(noclone_path, 'w') as f: f.write(sample_content) print(f"Created sample .noclone file at: {noclone_path}") return noclone_path def main(): parser = argparse.ArgumentParser(description='TreeClone - Directory structure cloning tool') subparsers = parser.add_subparsers(dest='command', help='Available commands') # Capture command capture_parser = subparsers.add_parser('capture', help='Capture directory structure') capture_parser.add_argument('directory', help='Directory to capture') capture_parser.add_argument('-o', '--output', default='structure.json', help='Output file') capture_parser.add_argument('--no-files', action='store_true', help='Only capture directories') capture_parser.add_argument('--max-depth', type=int, help='Maximum depth') # Recreate command recreate_parser = subparsers.add_parser('recreate', help='Recreate directory structure') recreate_parser.add_argument('structure_file', help='JSON structure file') recreate_parser.add_argument('target_directory', help='Target directory') recreate_parser.add_argument('--no-files', action='store_true', help='Only create directories') # Init command for creating sample .noclone init_parser = subparsers.add_parser('init', help='Create sample .noclone file') init_parser.add_argument('directory', nargs='?', default='.', help='Directory to create .noclone in') args = parser.parse_args() tool = TreeClone() if args.command == 'capture': print(f"Capturing structure of {args.directory}...") # Always use manual capture to respect .noclone files print("Using manual capture to respect .noclone files...") tool.capture_manual( args.directory, args.output, include_files=not args.no_files, max_depth=args.max_depth ) print(f"Structure saved to {args.output}") elif args.command == 'recreate': print(f"Recreating structure from {args.structure_file}...") created_path = tool.recreate_structure( args.structure_file, args.target_directory, create_files=not args.no_files ) print(f"Structure recreated at: {created_path}") elif args.command == 'init': print(f"Creating sample .noclone file in {args.directory}...") tool.create_sample_noclone(args.directory) print("Edit the .noclone file to customize exclusion patterns.") else: parser.print_help() if __name__ == '__main__': main()