Initial upload: ScaffoldForge CLI tool with full codebase, tests, and CI/CD
This commit is contained in:
270
scaffoldforge/generators/structure.py
Normal file
270
scaffoldforge/generators/structure.py
Normal file
@@ -0,0 +1,270 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user