Initial upload: ScaffoldForge CLI tool with full codebase, tests, and CI/CD

This commit is contained in:
2026-02-04 05:37:10 +00:00
parent 7dc9bfcb5f
commit 2865eb2ebe

View 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()