"""Project structure generation functionality.""" import os import stat from pathlib import Path from typing import Any, Dict, List, Optional from scaffoldforge.generators.code import CodeGenerator from scaffoldforge.generators.models import FileSpec from scaffoldforge.parsers import IssueData class StructureGenerator: """Generator for project directory structure and files.""" def __init__( self, output_dir: Optional[str] = None, preview: bool = False, ): """Initialize the structure generator. Args: output_dir: Base output directory for generated projects. preview: If True, don't write files, just print what would be done. """ self.output_dir = output_dir or "./generated" self.preview = preview self.created_files: List[str] = [] self.created_dirs: List[str] = [] def generate( self, language: str, issue_data: IssueData, code_generator: CodeGenerator, project_name: Optional[str] = None, ) -> None: """Generate the complete project structure. Args: language: Programming language. issue_data: Parsed issue data. code_generator: CodeGenerator instance for file content. project_name: Optional custom project name. """ if project_name is None: project_name = self._sanitize_name(issue_data.title) base_path = Path(self.output_dir) / project_name if self.preview: print(f"\n{'='*60}") print(f"PREVIEW: Project would be created at: {base_path}") print(f"{'='*60}\n") files = code_generator.generate_all_files(language, issue_data) self._create_directories(base_path, issue_data) for file_spec in files: self._write_file(base_path, file_spec) self._create_readme(base_path, issue_data, project_name) self._create_gitignore(base_path, project_name, language) if self.preview: self._print_preview_summary() def _create_directories( self, base_path: Path, issue_data: IssueData ) -> None: """Create project directories. Args: base_path: Base path for the project. issue_data: Issue data containing suggested directories. """ directories = ["src", "tests"] for dir_name in issue_data.suggested_directories: clean_name = Path(dir_name).parts[0] if clean_name not in directories: directories.append(clean_name) for directory in directories: dir_path = base_path / directory self._create_directory(dir_path) def _create_directory(self, path: Path) -> None: """Create a single directory. Args: path: Path to create. """ if self.preview: print(f"[PREVIEW] Would create directory: {path}") return try: path.mkdir(parents=True, exist_ok=True) self.created_dirs.append(str(path)) except OSError as e: raise OSError(f"Failed to create directory {path}: {e}") def _write_file(self, base_path: Path, file_spec: FileSpec) -> None: """Write a file to the project. Args: base_path: Base path for the project. file_spec: File specification. """ file_path = base_path / file_spec.path if self.preview: print(f"[PREVIEW] Would create file: {file_path}") if file_spec.executable: print(f" (executable)") return try: file_path.parent.mkdir(parents=True, exist_ok=True) mode = 0o755 if file_spec.executable else 0o644 with open(file_path, "w", encoding=file_spec.encoding) as f: f.write(file_spec.content) os.chmod(file_path, mode) self.created_files.append(str(file_path)) except OSError as e: raise OSError(f"Failed to write file {file_path}: {e}") def _create_readme( self, base_path: Path, issue_data: IssueData, project_name: str ) -> None: """Create README.md file. Args: base_path: Base path for the project. issue_data: Issue data for content. project_name: Name of the project. """ readme_content = f"""# {project_name} {issue_data.title} ## Description {issue_data.body[:500]}{'...' if len(issue_data.body) > 500 else ''} **GitHub Issue:** #{issue_data.number} **Repository:** {issue_data.repository} **URL:** {issue_data.url} ## Requirements {chr(10).join(f"- {req}" for req in issue_data.requirements) if issue_data.requirements else '- See GitHub issue for requirements'} ## TODO Items {chr(10).join(f"- [ ] {item}" for item in issue_data.get_todo_items()) if issue_data.get_todo_items() else '- No TODO items found'} ## Getting Started ### Installation ```bash # Clone the repository git clone https://github.com/{issue_data.repository}.git cd {project_name} ``` ### Usage ```bash # Run the project python main.py ``` ## License MIT """ self._write_file( base_path, FileSpec(path="README.md", content=readme_content), ) def _create_gitignore( self, base_path: Path, project_name: str, language: str ) -> None: """Create .gitignore file based on language. Args: base_path: Base path for the project. project_name: Name of the project. language: Programming language. """ gitignore_templates = { "python": """__pycache__/ *.py[cod] *$py.class *.so .Python build/ dist/ *.egg-info/ .eggs/ venv/ .env """, "javascript": """node_modules/ npm-debug.log* yarn-debug.log* yarn-error.log* dist/ coverage/ .env """, "go": """*.exe *.exe~ *.dll *.so *.dylib *.test *.out go.work """, "rust": """target/ Cargo.lock *.swp *.swo """, } gitignore_content = gitignore_templates.get(language, "") self._write_file( base_path, FileSpec(path=".gitignore", content=gitignore_content), ) def _sanitize_name(self, name: str) -> str: """Sanitize a string for use as a directory/project name. Args: name: Original name. Returns: Sanitized name. """ import re name = re.sub(r"[^a-zA-Z0-9\s_-]", "", name) name = re.sub(r"\s+", "-", name.strip()) return name.lower()[:50] or "project" def _print_preview_summary(self) -> None: """Print a summary of what would be created in preview mode.""" print(f"\n{'='*60}") print("PREVIEW SUMMARY") print(f"{'='*60}") print(f"Directories: {len(self.created_dirs)}") print(f"Files: {len(self.created_files)}") print(f"{'='*60}\n") def get_created_files(self) -> List[str]: """Get list of created files.""" return self.created_files.copy() def get_created_directories(self) -> List[str]: """Get list of created directories.""" return self.created_dirs.copy()