271 lines
7.1 KiB
Python
271 lines
7.1 KiB
Python
"""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()
|