Compare commits

78 Commits
v0.1.0 ... main

Author SHA1 Message Date
9d4b49b14d fix: resolve CI linting and type checking errors
Some checks failed
CI / lint (push) Failing after 4m46s
CI / test (push) Failing after 4m54s
2026-02-04 06:28:15 +00:00
c64e81e56f fix: resolve CI linting and type checking errors
Some checks failed
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
2026-02-04 06:28:15 +00:00
28fae25f83 fix: resolve CI linting and type checking errors
Some checks failed
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
2026-02-04 06:28:14 +00:00
f180b8a082 fix: resolve CI linting and type checking errors
Some checks failed
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
2026-02-04 06:28:13 +00:00
b8c47f6b46 fix: resolve CI linting and type checking errors
Some checks failed
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
2026-02-04 06:28:13 +00:00
1639e9e5b4 fix: resolve CI linting and type checking errors
Some checks failed
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
2026-02-04 06:28:12 +00:00
b2bc2b9ea5 fix: resolve CI linting and type checking errors
Some checks failed
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
2026-02-04 06:28:11 +00:00
f19a2c0869 fix: resolve CI linting and type checking errors
Some checks failed
CI / lint (push) Has started running
CI / test (push) Has been cancelled
2026-02-04 06:28:10 +00:00
ca6b0c66f1 fix: resolve CI linting and type checking errors
Some checks failed
CI / test (push) Has been cancelled
CI / lint (push) Has been cancelled
2026-02-04 06:28:09 +00:00
9f5a693d5d fix: resolve CI linting and type checking errors
Some checks failed
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
2026-02-04 06:28:08 +00:00
8c63473d84 fix: resolve CI linting and type checking errors
Some checks failed
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
2026-02-04 06:28:08 +00:00
73ed60e87c fix: resolve CI linting and type checking errors
Some checks failed
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
2026-02-04 06:28:07 +00:00
2b5f70ee09 fix: resolve CI linting and type checking errors
Some checks failed
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
2026-02-04 06:28:07 +00:00
bcfce038ec fix: resolve CI linting and type checking errors
Some checks failed
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
2026-02-04 06:28:06 +00:00
50553bb154 fix: resolve CI linting and type checking errors
Some checks failed
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
2026-02-04 06:28:04 +00:00
841405b437 fix: resolve CI linting and type checking errors
Some checks failed
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
2026-02-04 06:28:02 +00:00
145a9c6907 fix: resolve CI linting and type checking errors
Some checks failed
CI / test (push) Has been cancelled
CI / lint (push) Has been cancelled
2026-02-04 06:28:01 +00:00
f3d29b74d5 fix: resolve CI linting and type checking errors
Some checks failed
CI / test (push) Has been cancelled
CI / lint (push) Has been cancelled
2026-02-04 06:28:01 +00:00
ef04db2ebd fix: resolve CI linting and type checking errors
Some checks failed
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
2026-02-04 06:28:00 +00:00
56f544b093 fix: resolve CI linting and type checking errors
Some checks failed
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
2026-02-04 06:27:58 +00:00
96db2f5236 fix: resolve CI linting and type checking errors
Some checks failed
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
2026-02-04 06:27:56 +00:00
06910dbb6e fix: resolve CI linting and type checking errors
Some checks failed
CI / test (push) Has been cancelled
CI / lint (push) Has been cancelled
2026-02-04 06:27:55 +00:00
d30e40e3fd fix: resolve CI linting and type checking errors
Some checks failed
CI / test (push) Has been cancelled
CI / lint (push) Has been cancelled
2026-02-04 06:27:55 +00:00
7c42ab1bf0 fix: resolve CI linting and type checking errors
Some checks failed
CI / lint (push) Has been cancelled
CI / typecheck (push) Has been cancelled
CI / test (push) Has been cancelled
2026-02-04 06:27:54 +00:00
718290943b fix: resolve CI linting and type checking errors
Some checks failed
CI / lint (push) Failing after 12s
CI / typecheck (push) Failing after 17s
CI / test (push) Failing after 13s
2026-02-04 06:20:10 +00:00
de65989205 fix: verify CI linting and type checking pass
Some checks failed
CI / test (push) Failing after 11s
2026-02-04 06:16:40 +00:00
7421d7da0f fix: resolve CI linting and type checking errors
Some checks failed
CI / lint (push) Failing after 4m46s
CI / test (push) Failing after 4m51s
2026-02-04 06:04:20 +00:00
3b688684c2 fix: resolve CI linting and type checking errors
Some checks failed
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
2026-02-04 06:04:20 +00:00
5ae900bfd7 fix: resolve CI linting and type checking errors
Some checks failed
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
2026-02-04 06:04:19 +00:00
af11223841 fix: resolve CI linting and type checking errors
Some checks failed
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
2026-02-04 06:04:19 +00:00
2dbce2439b fix: resolve CI linting and type checking errors
Some checks failed
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
2026-02-04 06:04:18 +00:00
b00c6b5cce fix: resolve CI linting and type checking errors
Some checks failed
CI / test (push) Failing after 16s
2026-02-04 05:58:33 +00:00
ca375ac3e1 fix: resolve CI linting and type checking errors
Some checks failed
CI / test (push) Has been cancelled
2026-02-04 05:58:32 +00:00
83d4d92c21 fix: resolve CI linting and type checking errors
Some checks failed
CI / test (push) Has been cancelled
2026-02-04 05:58:30 +00:00
4de73353a4 fix: resolve CI linting and type checking errors
Some checks failed
CI / test (push) Has been cancelled
2026-02-04 05:58:29 +00:00
22bd48f04f fix: resolve CI linting and type checking errors
Some checks failed
CI / test (push) Has been cancelled
2026-02-04 05:58:27 +00:00
e08a0c7f91 fix: resolve CI linting and type checking errors
Some checks failed
CI / test (push) Has been cancelled
2026-02-04 05:58:26 +00:00
f14a4a8c78 fix: resolve CI linting and type checking errors
Some checks failed
CI / test (push) Has been cancelled
2026-02-04 05:58:25 +00:00
975627a712 fix: resolve CI linting and type checking errors
Some checks failed
CI / test (push) Has been cancelled
2026-02-04 05:58:25 +00:00
880df5e86d fix: resolve CI linting and type checking errors
Some checks failed
CI / test (push) Has been cancelled
2026-02-04 05:58:24 +00:00
98c3b25710 fix: resolve CI linting and type checking errors
Some checks failed
CI / test (push) Has been cancelled
2026-02-04 05:58:22 +00:00
36b5baceb3 fix: resolve CI linting and type checking errors
Some checks are pending
CI / test (push) Has started running
2026-02-04 05:58:21 +00:00
f94483bac0 fix: resolve CI linting and type checking errors
Some checks failed
CI / test (push) Has been cancelled
2026-02-04 05:58:19 +00:00
c720d2852d fix: resolve CI linting and type checking errors
Some checks failed
CI / test (push) Has been cancelled
2026-02-04 05:58:18 +00:00
de9786f998 fix: resolve CI linting and type checking errors
Some checks failed
CI / test (push) Has been cancelled
2026-02-04 05:58:17 +00:00
607b54df19 fix: resolve CI linting and type checking errors
Some checks failed
CI / test (push) Has been cancelled
2026-02-04 05:58:16 +00:00
4575516c3e fix: resolve CI linting and type checking errors
Some checks failed
CI / test (push) Has been cancelled
2026-02-04 05:58:14 +00:00
68e2eaeec5 fix: resolve CI linting and type checking errors
Some checks failed
CI / test (push) Has been cancelled
2026-02-04 05:58:13 +00:00
e6ab90f390 fix: resolve CI linting and type checking errors
Some checks failed
CI / test (push) Has been cancelled
2026-02-04 05:58:11 +00:00
856e22b34b fix: resolve CI linting and type checking errors
Some checks failed
CI / test (push) Has been cancelled
2026-02-04 05:58:10 +00:00
d9530a3453 fix: resolve CI linting and type checking errors
Some checks failed
CI / test (push) Has been cancelled
2026-02-04 05:58:09 +00:00
2b16512be2 fix: resolve CI linting and type checking errors
Some checks failed
CI / test (push) Has been cancelled
2026-02-04 05:58:08 +00:00
2cc6948cbd fix: resolve CI linting and type checking errors
Some checks failed
CI / test (push) Has been cancelled
2026-02-04 05:58:06 +00:00
206d477156 fix: resolve CI linting and type checking errors
Some checks failed
CI / test (push) Has been cancelled
2026-02-04 05:58:05 +00:00
6ddd35a876 fix: resolve CI linting and type checking errors
Some checks failed
CI / test (push) Has been cancelled
2026-02-04 05:58:04 +00:00
80609d14ba fix: resolve CI linting and type checking errors
Some checks failed
CI / test (push) Has been cancelled
2026-02-04 05:58:04 +00:00
041f8331f2 fix: resolve CI linting and type checking errors
Some checks failed
CI / test (push) Has been cancelled
2026-02-04 05:58:03 +00:00
0fbdfc80ee fix: resolve CI linting and type checking errors
Some checks failed
CI / test (push) Has been cancelled
2026-02-04 05:58:02 +00:00
fbaa1a90f6 fix: resolve CI linting and type checking errors
Some checks failed
CI / test (push) Has been cancelled
2026-02-04 05:58:00 +00:00
49be08e504 fix: resolve CI linting and type checking errors
Some checks failed
CI / test (push) Has been cancelled
2026-02-04 05:57:59 +00:00
d3e9467389 fix: resolve CI linting and type checking errors
Some checks failed
CI / test (push) Has been cancelled
2026-02-04 05:57:58 +00:00
76ae2f7fe6 fix: resolve CI linting and type checking errors
Some checks failed
CI / test (push) Has been cancelled
2026-02-04 05:57:58 +00:00
f32bdc2f3e fix: resolve CI linting and type checking errors
Some checks failed
CI / test (push) Has been cancelled
2026-02-04 05:57:57 +00:00
cedbcd63b3 fix: resolve CI linting and type checking errors
Some checks failed
CI / test (push) Has been cancelled
2026-02-04 05:57:55 +00:00
312efdca75 fix: resolve CI linting and type checking errors
Some checks are pending
CI / test (push) Has started running
2026-02-04 05:57:54 +00:00
ce6e879fa9 fix: resolve CI linting and type checking errors
Some checks failed
CI / test (push) Has been cancelled
2026-02-04 05:57:54 +00:00
ef7b7f7673 fix: resolve CI linting and type checking errors
Some checks failed
CI / test (push) Has been cancelled
2026-02-04 05:57:53 +00:00
66e5ebd1a8 fix: resolve CI linting and type checking errors
Some checks failed
CI / test (push) Has been cancelled
2026-02-04 05:57:52 +00:00
8d48b3da87 fix: resolve CI linting and type checking errors
Some checks failed
CI / test (push) Has been cancelled
2026-02-04 05:57:52 +00:00
8fffa89be8 fix: resolve CI linting and type checking errors
Some checks failed
CI / test (push) Has been cancelled
2026-02-04 05:57:51 +00:00
d4edd2f77a fix: resolve CI linting and type checking errors
Some checks failed
CI / test (push) Has been cancelled
2026-02-04 05:57:50 +00:00
689b4814fa fix: resolve CI linting and type checking errors
Some checks failed
CI / test (push) Has been cancelled
2026-02-04 05:57:49 +00:00
c68659d903 fix: resolve CI linting and type checking errors
Some checks failed
CI / test (push) Has been cancelled
2026-02-04 05:57:48 +00:00
c984c31be0 fix: resolve CI linting and type checking errors
Some checks failed
CI / test (push) Has been cancelled
2026-02-04 05:57:47 +00:00
b5eb3c8991 fix: resolve CI linting and type checking errors
Some checks failed
CI / test (push) Has been cancelled
2026-02-04 05:57:47 +00:00
e84520d96d fix: resolve CI linting and type checking errors
Some checks failed
CI / test (push) Has been cancelled
2026-02-04 05:57:46 +00:00
582787f8af fix: resolve CI linting and type checking errors
Some checks failed
CI / test (push) Has been cancelled
2026-02-04 05:57:46 +00:00
c4b5cb82c3 fix: resolve CI linting and type checking errors
Some checks failed
CI / test (push) Has been cancelled
2026-02-04 05:57:45 +00:00
45 changed files with 808 additions and 574 deletions

View File

@@ -1,13 +1,3 @@
# ScaffoldForge Environment Configuration
# Copy this file to .env and fill in your values
# GitHub Personal Access Token for API access
# Create one at: https://github.com/settings/tokens
# Required scopes: repo (for private repos)
GITHUB_TOKEN=your_github_token_here
# Default template directory path
# SCAFFOLD_TEMPLATE_DIR=./templates
# Default output directory
# SCAFFOLD_OUTPUT_DIR=./output
SCAFFOLD_TEMPLATE_DIR=
SCAFFOLD_OUTPUT_DIR=./generated

View File

@@ -2,30 +2,51 @@ name: CI
on:
push:
branches: [main]
branches: [ main, master ]
pull_request:
branches: [main]
branches: [ main, master ]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
cache: 'pip'
- name: Install dependencies
run: |
pip install ruff mypy
- name: Run Python linting
run: |
ruff check scaffoldforge tests
ruff format --check scaffoldforge tests
- name: Run Python type checking
run: |
python -m mypy scaffoldforge tests
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
cache: 'pip'
- name: Install dependencies
run: |
pip install -e ".[dev]"
- name: Run unit tests
run: pytest tests/unit/ -v
- name: Run integration tests
run: pytest tests/integration/ -v
- name: Run linter
run: ruff check .
- name: Upload coverage
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./coverage.xml
fail_ci_if_error: false
- name: Run Python tests
run: |
pytest tests/ -v --tb=short
pytest tests/integration/ -v

185
.gitignore vendored
View File

@@ -1,138 +1,47 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# pipenv
Pipfile.lock
# PEP 582
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# IDEs
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Project specific
output/
*.generated/
{
"Python": [
"__pycache__/",
"*.py[cod]",
"*$py.class",
"*.so",
".Python",
"build/",
"dist/",
"*.egg-info/",
".eggs/",
"venv/",
".env"
],
"Node": [
"node_modules/",
"npm-debug.log*",
"yarn-debug.log*",
"yarn-error.log*",
"dist/",
"coverage/"
],
"Go": [
"*.exe",
"*.exe~",
"*.dll",
"*.so",
"*.dylib",
"*.test",
"*.out",
"go.work"
],
"Rust": [
"target/",
"Cargo.lock",
"*.swp",
"*.swo"
],
"General": [
".DS_Store",
".vscode/",
".idea/",
"*.swp",
"*.swo",
"*~"
]
}

135
README.md
View File

@@ -1,21 +1,7 @@
# ScaffoldForge
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Python Version](https://img.shields.io/badge/python-3.9%2B-blue)](https://www.python.org/)
[![Status](https://img.shields.io/badge/status-beta-green)](https://github.com/7000pctAUTO/scaffoldforge)
ScaffoldForge is a CLI tool that parses GitHub issues and automatically generates project scaffolds with starter code, file structures, and TODO comments. Developers can point it at a repo and issue, and it creates a development-ready starting point with placeholders for implementation.
## Features
- Parse GitHub issue descriptions, checklists, and comments
- Generate project structures from templates
- Create starter files with TODO comments
- Support multiple languages (Python, JavaScript, Go, Rust)
- Custom template system
- Preview mode before generating
- Interactive mode with prompts
## Installation
```bash
@@ -25,7 +11,7 @@ pip install scaffoldforge
Or from source:
```bash
git clone https://7000pct.gitea.bloupla.net/7000pctAUTO/scaffoldforge.git
git clone https://github.com/yourusername/scaffoldforge.git
cd scaffoldforge
pip install -e .
```
@@ -42,6 +28,28 @@ export GITHUB_TOKEN=your_github_token_here
scaffoldforge generate https://github.com/owner/repo/issues/123
```
## Configuration
ScaffoldForge can be configured using environment variables or a configuration file.
### Environment Variables
| Variable | Description | Required |
|----------|-------------|----------|
| `GITHUB_TOKEN` | GitHub Personal Access Token for API access | Yes |
| `SCAFFOLD_TEMPLATE_DIR` | Default template directory path | No |
| `SCAFFOLD_OUTPUT_DIR` | Default output directory | No |
### Configuration File
Copy `.env.example` to `.env` and fill in your values:
```bash
cp .env.example .env
```
Edit `.env` to add your GitHub token.
## Usage
### Basic Usage
@@ -89,12 +97,52 @@ Interactive mode:
scaffoldforge generate https://github.com/owner/repo/issues/123 --interactive
```
List available templates:
### List Available Templates
```bash
scaffoldforge list-templates --language python
```
## Custom Templates
You can create custom templates for ScaffoldForge. Place your templates in a directory and set the `SCAFFOLD_TEMPLATE_DIR` environment variable.
### Template Structure
```
custom_templates/
├── python/
│ ├── default/
│ │ ├── main.py.j2
│ │ └── utils.py.j2
│ └── cli/
│ └── main.py.j2
└── javascript/
└── default/
└── index.js.j2
```
### Template Variables
Templates can use the following variables:
| Variable | Description |
|----------|-------------|
| `project_name` | Sanitized project name |
| `project_name_kebab` | kebab-case project name |
| `project_name_snake` | snake_case project name |
| `project_name_pascal` | PascalCase project name |
| `issue_number` | GitHub issue number |
| `issue_title` | GitHub issue title |
| `issue_url` | GitHub issue URL |
| `repository` | Repository identifier (owner/repo) |
| `author` | Issue author |
| `created_date` | Issue creation date |
| `todo_items` | List of incomplete checklist items |
| `completed_items` | List of completed checklist items |
| `requirements` | List of requirements |
| `acceptance_criteria` | List of acceptance criteria |
## Supported Languages
- **Python** - Generates `main.py`, `utils.py`, `models.py`, `pyproject.toml`
@@ -102,40 +150,31 @@ scaffoldforge list-templates --language python
- **Go** - Generates `main.go`, `utils.go`, `go.mod`
- **Rust** - Generates `main.rs`, `lib.rs`, `Cargo.toml`
## Custom Templates
You can create custom templates for ScaffoldForge. Place your templates in a directory and set the `SCAFFOLD_TEMPLATE_DIR` environment variable.
```bash
SCAFFOLD_TEMPLATE_DIR=./custom_templates scaffoldforge generate <issue_url>
```
## Configuration
### Environment Variables
| Variable | Description | Required |
|----------|-------------|----------|
| `GITHUB_TOKEN` | GitHub Personal Access Token for API access | Yes |
| `SCAFFOLD_TEMPLATE_DIR` | Default template directory path | No |
| `SCAFFOLD_OUTPUT_DIR` | Default output directory | No |
### Configuration File
Copy `.env.example` to `.env` and fill in your values:
```bash
cp .env.example .env
```
## How It Works
1. **Issue Parsing**: Fetches the GitHub issue and parses title, description, checklist items, and labels
2. **Language Detection**: Automatically detects programming language from labels or content
3. **Template Selection**: Selects appropriate templates based on language
4. **Context Generation**: Creates template context with issue data
5. **File Generation**: Renders templates and writes files
6. **Documentation**: Generates README.md and .gitignore
1. **Issue Parsing**: ScaffoldForge fetches the GitHub issue and parses:
- Title and description
- Checklist items (converted to TODO comments)
- Labels (used for language detection)
- Requirements sections
- Suggested file/directory paths
2. **Template Selection**: Based on the detected or specified language, appropriate templates are selected.
3. **Context Generation**: A template context is created with issue data and project name.
4. **File Generation**: Templates are rendered with the context and files are written to the output directory.
5. **Documentation**: A `README.md` and `.gitignore` are automatically generated.
## Error Handling
ScaffoldForge handles common errors gracefully:
- **GitHub API rate limit**: Suggests using a token
- **Invalid URL**: Validates and provides clear error messages
- **Template errors**: Shows syntax errors in templates
- **Permission errors**: Validates output paths
## Contributing

View File

@@ -1,16 +1,22 @@
"""CLI module for ScaffoldForge."""
import sys
from pathlib import Path
import click
from scaffoldforge.cli.commands import generate, preview, list_templates
from scaffoldforge.cli.commands import generate, list_templates, preview
from scaffoldforge.config import get_config
from scaffoldforge.parsers import IssueParser
from scaffoldforge.generators import StructureGenerator, CodeGenerator
from scaffoldforge.generators import CodeGenerator, StructureGenerator
from scaffoldforge.templates import TemplateEngine
__all__ = [
"cli",
"generate",
"preview",
"list_templates",
"StructureGenerator",
"CodeGenerator",
"TemplateEngine",
]
@click.group()
@click.option("--verbose", "-v", is_flag=True, help="Enable verbose output")

View File

@@ -1,15 +1,12 @@
"""CLI commands for ScaffoldForge."""
import os
import re
import sys
from pathlib import Path
from typing import Optional
import click
from scaffoldforge.config import get_config
from scaffoldforge.generators import StructureGenerator, CodeGenerator
from scaffoldforge.generators import CodeGenerator, StructureGenerator
from scaffoldforge.parsers import IssueParser
from scaffoldforge.templates import TemplateEngine

View File

@@ -2,7 +2,7 @@
import os
from pathlib import Path
from typing import Any, Dict, Optional
from typing import Any, Optional
import yaml
@@ -11,7 +11,7 @@ class Config:
"""Configuration management class."""
_instance: Optional["Config"] = None
_config: Dict[str, Any] = {}
_config: dict[str, Any] = {}
def __new__(cls) -> "Config":
if cls._instance is None:
@@ -78,20 +78,22 @@ class Config:
"""Load configuration from YAML file."""
path = Path(config_path)
if path.exists():
with open(path, "r") as f:
with open(path) as f:
user_config = yaml.safe_load(f) or {}
self._config.update(user_config)
def get(self, key: str, default: Any = None) -> Any:
def get(self, key: str, default: Any | None = None) -> Any:
"""Get configuration value using dot notation."""
keys = key.split(".")
value = self._config
value: Any = self._config
for k in keys:
if isinstance(value, dict):
value = value.get(k)
else:
return default
return value if value is not None else default
if value is None:
return default
return value
def get_github_token(self) -> Optional[str]:
"""Get GitHub token from environment or config."""

View File

@@ -1,7 +1,7 @@
"""Generators module for ScaffoldForge."""
from scaffoldforge.generators.structure import StructureGenerator
from scaffoldforge.generators.code import CodeGenerator
from scaffoldforge.generators.models import FileSpec
from scaffoldforge.generators.structure import StructureGenerator
__all__ = ["StructureGenerator", "CodeGenerator", "FileSpec"]

View File

@@ -1,10 +1,10 @@
"""Code generation functionality."""
from typing import Any, Dict, List
from typing import Any
from scaffoldforge.generators.models import FileSpec
from scaffoldforge.parsers import IssueData
from scaffoldforge.templates import TemplateEngine
from scaffoldforge.generators.models import FileSpec
class CodeGenerator:
@@ -34,9 +34,7 @@ class CodeGenerator:
self.template_engine = template_engine
self.issue_data = issue_data
def generate_all_files(
self, language: str, issue_data: IssueData
) -> List[FileSpec]:
def generate_all_files(self, language: str, issue_data: IssueData) -> list[FileSpec]:
"""Generate all project files.
Args:
@@ -46,7 +44,7 @@ class CodeGenerator:
Returns:
List of FileSpec objects.
"""
files = []
files: list[FileSpec] = []
context = self.template_engine.get_template_context(issue_data)
files.extend(self._generate_source_files(language, context))
@@ -54,9 +52,7 @@ class CodeGenerator:
return files
def _generate_source_files(
self, language: str, context: Dict[str, Any]
) -> List[FileSpec]:
def _generate_source_files(self, language: str, context: dict[str, Any]) -> list[FileSpec]:
"""Generate source code files.
Args:
@@ -66,16 +62,12 @@ class CodeGenerator:
Returns:
List of FileSpec objects.
"""
files = []
files: list[FileSpec] = []
source_templates = self.DEFAULT_FILES.get(language, [])
for template_name in source_templates:
try:
content = self.template_engine.render(
template_name, context, language
)
extension = self._get_extension(language)
filename = f"{template_name}{extension}" if not template_name.endswith(extension) else template_name
content = self.template_engine.render(template_name, context, language)
path = self._get_source_path(template_name, language)
files.append(FileSpec(path=path, content=content))
except ValueError:
@@ -83,9 +75,7 @@ class CodeGenerator:
return files
def _generate_config_files(
self, language: str, context: Dict[str, Any]
) -> List[FileSpec]:
def _generate_config_files(self, language: str, context: dict[str, Any]) -> list[FileSpec]:
"""Generate configuration files.
Args:
@@ -100,9 +90,7 @@ class CodeGenerator:
for config_name in config_templates:
try:
content = self.template_engine.render(
config_name, context, language
)
content = self.template_engine.render(config_name, context, language)
files.append(FileSpec(path=config_name, content=content))
except ValueError:
pass
@@ -110,7 +98,7 @@ class CodeGenerator:
return files
def _create_empty_file(
self, template_name: str, language: str, context: Dict[str, Any]
self, template_name: str, language: str, context: dict[str, Any]
) -> FileSpec:
"""Create an empty file with TODO comments.
@@ -122,20 +110,16 @@ class CodeGenerator:
Returns:
FileSpec object.
"""
extension = self._get_extension(language)
filename = f"{template_name}{extension}"
path = self._get_source_path(template_name, language)
todo_items = self.issue_data.get_todo_items()
content = self._generate_todo_content(
language, template_name, todo_items
)
content = self._generate_todo_content(language, template_name, todo_items)
return FileSpec(path=path, content=content)
def _generate_todo_content(
self, language: str, template_name: str, todo_items: List[str]
self, language: str, template_name: str, todo_items: list[str]
) -> str:
"""Generate TODO comments for a file.
@@ -154,9 +138,7 @@ class CodeGenerator:
lines.append(f"# TODO: {item}")
if language == "python":
return f'"""{template_name} - {self.issue_data.title}"""\n\n' + "\n".join(
lines
)
return f'"""{template_name} - {self.issue_data.title}"""\n\n' + "\n".join(lines)
elif language in ("javascript",):
return f"/**\n * {template_name} \n */\n\n" + "\n".join(lines)
elif language == "go":
@@ -199,7 +181,7 @@ class CodeGenerator:
return f"{template_name}{self._get_extension(language)}"
def generate_single_file(
self, filename: str, language: str, extra_context: Dict[str, Any] = None
self, filename: str, language: str, extra_context: dict[str, Any] | None = None
) -> str:
"""Generate content for a single file.

View File

@@ -1,7 +1,6 @@
"""Data models for generators module."""
from dataclasses import dataclass
from typing import Any, Dict, List
@dataclass

View File

@@ -1,9 +1,8 @@
"""Project structure generation functionality."""
import os
import stat
from pathlib import Path
from typing import Any, Dict, List, Optional
from typing import Optional
from scaffoldforge.generators.code import CodeGenerator
from scaffoldforge.generators.models import FileSpec
@@ -26,8 +25,8 @@ class StructureGenerator:
"""
self.output_dir = output_dir or "./generated"
self.preview = preview
self.created_files: List[str] = []
self.created_dirs: List[str] = []
self.created_files: list[str] = []
self.created_dirs: list[str] = []
def generate(
self,
@@ -67,9 +66,7 @@ class StructureGenerator:
if self.preview:
self._print_preview_summary()
def _create_directories(
self, base_path: Path, issue_data: IssueData
) -> None:
def _create_directories(self, base_path: Path, issue_data: IssueData) -> None:
"""Create project directories.
Args:
@@ -115,7 +112,7 @@ class StructureGenerator:
if self.preview:
print(f"[PREVIEW] Would create file: {file_path}")
if file_spec.executable:
print(f" (executable)")
print(" (executable)")
return
try:
@@ -128,9 +125,7 @@ class StructureGenerator:
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:
def _create_readme(self, base_path: Path, issue_data: IssueData, project_name: str) -> None:
"""Create README.md file.
Args:
@@ -144,7 +139,7 @@ class StructureGenerator:
## Description
{issue_data.body[:500]}{'...' if len(issue_data.body) > 500 else ''}
{issue_data.body[:500]}{"..." if len(issue_data.body) > 500 else ""}
**GitHub Issue:** #{issue_data.number}
**Repository:** {issue_data.repository}
@@ -152,11 +147,19 @@ class StructureGenerator:
## Requirements
{chr(10).join(f"- {req}" for req in issue_data.requirements) if issue_data.requirements else '- See GitHub issue for requirements'}
{"
".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'}
{"
".join(f"- [ ] {item}" for item in issue_data.get_todo_items())
if issue_data.get_todo_items()
else "- No TODO items found"
}
## Getting Started
@@ -184,9 +187,7 @@ MIT
FileSpec(path="README.md", content=readme_content),
)
def _create_gitignore(
self, base_path: Path, project_name: str, language: str
) -> None:
def _create_gitignore(self, base_path: Path, project_name: str, language: str) -> None:
"""Create .gitignore file based on language.
Args:
@@ -248,6 +249,7 @@ Cargo.lock
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"
@@ -261,10 +263,10 @@ Cargo.lock
print(f"Files: {len(self.created_files)}")
print(f"{'=' * 60}\n")
def get_created_files(self) -> List[str]:
def get_created_files(self) -> list[str]:
"""Get list of created files."""
return self.created_files.copy()
def get_created_directories(self) -> List[str]:
def get_created_directories(self) -> list[str]:
"""Get list of created directories."""
return self.created_dirs.copy()

View File

@@ -1,9 +1,7 @@
"""Main entry point for ScaffoldForge CLI."""
import os
from pathlib import Path
import click
from dotenv import load_dotenv
from scaffoldforge.cli import cli

View File

@@ -1,5 +1,5 @@
"""Parsers module for ScaffoldForge."""
from scaffoldforge.parsers.issue_parser import IssueParser, IssueData, ChecklistItem
from scaffoldforge.parsers.issue_parser import ChecklistItem, IssueData, IssueParser
__all__ = ["IssueParser", "IssueData", "ChecklistItem"]

View File

@@ -4,11 +4,10 @@ import os
import re
import time
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional
from typing import Any, Optional
from github import Github
from github.Issue import Issue
from github.Label import Label
@dataclass
@@ -29,25 +28,25 @@ class IssueData:
title: str
body: str
body_html: str
labels: List[str]
labels: list[str]
state: str
url: str
repository: str
author: str
created_at: str
updated_at: str
checklist: List[ChecklistItem] = field(default_factory=list)
requirements: List[str] = field(default_factory=list)
acceptance_criteria: List[str] = field(default_factory=list)
suggested_files: List[str] = field(default_factory=list)
suggested_directories: List[str] = field(default_factory=list)
metadata: Dict[str, Any] = field(default_factory=dict)
checklist: list[ChecklistItem] = field(default_factory=list)
requirements: list[str] = field(default_factory=list)
acceptance_criteria: list[str] = field(default_factory=list)
suggested_files: list[str] = field(default_factory=list)
suggested_directories: list[str] = field(default_factory=list)
metadata: dict[str, Any] = field(default_factory=dict)
def get_todo_items(self) -> List[str]:
def get_todo_items(self) -> list[str]:
"""Get all todo items from checklist."""
return [item.text for item in self.checklist if not item.completed]
def get_completed_items(self) -> List[str]:
def get_completed_items(self) -> list[str]:
"""Get completed checklist items."""
return [item.text for item in self.checklist if item.completed]
@@ -109,6 +108,7 @@ class IssueParser:
time.sleep(60 * (attempt + 1))
else:
raise
raise ValueError(f"Failed to fetch issue after {max_retries} retries")
def _extract_issue_data(self, issue: Issue, repository: str) -> IssueData:
"""Extract structured data from a GitHub issue.
@@ -147,7 +147,7 @@ class IssueParser:
suggested_directories=suggested_directories,
)
def _parse_checklist(self, body: str) -> List[ChecklistItem]:
def _parse_checklist(self, body: str) -> list[ChecklistItem]:
"""Parse markdown checklist items from issue body.
Args:
@@ -156,24 +156,21 @@ class IssueParser:
Returns:
List of ChecklistItem objects.
"""
checklist = []
checklist: list[ChecklistItem] = []
if not body:
return checklist
lines = body.split("\n")
in_checklist = False
current_category = None
current_category: str | None = None
for i, line in enumerate(lines):
category_match = re.match(r"^\s*(?:###|##|#)\s+(.+)", line)
if category_match:
current_category = category_match.group(1)
in_checklist = False
continue
checklist_match = re.match(r"^\s*[-*]\s+\[([ xX])\]\s+(.+)$", line)
if checklist_match:
in_checklist = True
checked = checklist_match.group(1).lower() == "x"
text = checklist_match.group(2).strip()
checklist.append(
@@ -187,7 +184,7 @@ class IssueParser:
return checklist
def _parse_requirements(self, body: str) -> List[str]:
def _parse_requirements(self, body: str) -> list[str]:
"""Parse requirements from issue body.
Args:
@@ -196,7 +193,7 @@ class IssueParser:
Returns:
List of requirement strings.
"""
requirements = []
requirements: list[str] = []
if not body:
return requirements
@@ -216,7 +213,7 @@ class IssueParser:
return requirements
def _parse_acceptance_criteria(self, body: str) -> List[str]:
def _parse_acceptance_criteria(self, body: str) -> list[str]:
"""Parse acceptance criteria from issue body.
Args:
@@ -225,7 +222,7 @@ class IssueParser:
Returns:
List of acceptance criteria strings.
"""
criteria = []
criteria: list[str] = []
if not body:
return criteria
@@ -245,7 +242,7 @@ class IssueParser:
return criteria
def _parse_file_paths(self, body: str) -> List[str]:
def _parse_file_paths(self, body: str) -> list[str]:
"""Parse suggested file paths from issue body.
Args:
@@ -254,7 +251,7 @@ class IssueParser:
Returns:
List of file path strings.
"""
files = []
files: list[str] = []
if not body:
return files
@@ -271,7 +268,7 @@ class IssueParser:
return list(set(files))
def _parse_directory_paths(self, body: str) -> List[str]:
def _parse_directory_paths(self, body: str) -> list[str]:
"""Parse suggested directory paths from issue body.
Args:
@@ -280,7 +277,7 @@ class IssueParser:
Returns:
List of directory path strings.
"""
directories = []
directories: list[str] = []
if not body:
return directories

View File

@@ -1,137 +1,54 @@
"""Template engine for ScaffoldForge."""
"""Template rendering functionality."""
import os
import re
from pathlib import Path
from typing import Any, Dict, List, Optional
from typing import Any
from jinja2 import (
BaseLoader,
Environment,
FileSystemLoader,
PackageLoader,
TemplateSyntaxError,
)
from jinja2 import BaseLoader, Environment, FileSystemLoader, TemplateSyntaxError
from scaffoldforge.parsers import IssueData
class TemplateEngine:
"""Template rendering engine using Jinja2."""
"""Engine for rendering templates."""
def __init__(self, template_dir: Optional[str] = None):
def __init__(self, template_dir: str | None = None):
"""Initialize the template engine.
Args:
template_dir: Path to custom template directory.
template_dir: Optional custom template directory.
"""
self.template_dir = template_dir
self.env = self._create_environment()
self._loaded_templates: Dict[str, Dict[str, Any]] = {}
self._templates: dict[str, Any] = {}
self._loaded: dict[str, dict[str, Any]] = {}
def _create_environment(self) -> Environment:
"""Create Jinja2 environment with appropriate loader."""
if self.template_dir and Path(self.template_dir).exists():
loader = FileSystemLoader(self.template_dir)
def load_templates(self, language: str, template_name: str = "default") -> None:
"""Load templates for a specific language and template.
Args:
language: Programming language.
template_name: Name of the template set.
"""
if self.template_dir:
base_dir = self.template_dir
else:
loader = PackageLoader("scaffoldforge", "templates")
base_dir = str(Path(__file__).parent)
return Environment(
loader=loader,
autoescape=True,
trim_blocks=True,
lstrip_blocks=True,
)
template_path = Path(base_dir) / language / template_name
def load_templates(
self, language: str, template_name: str = "default"
) -> Dict[str, str]:
"""Load templates for a specific language and template type.
if not template_path.exists():
return
Args:
language: Programming language.
template_name: Template variant name.
loader = FileSystemLoader(str(template_path))
env = Environment(loader=loader, autoescape=True)
Returns:
Dictionary mapping template names to rendered content.
"""
key = f"{language}/{template_name}"
if key in self._loaded_templates:
return self._loaded_templates[key]
self._templates[language] = env
self._loaded[language] = {
"path": str(template_path),
"templates": list(env.list_templates()),
}
templates = {}
template_dir = Path(__file__) / language / template_name
if template_dir.exists():
for template_file in template_dir.glob("*.j2"):
template_name_only = template_file.stem
templates[template_name_only] = self._load_template(
language, template_name, template_file.name
)
self._loaded_templates[key] = templates
return templates
def _load_template(
self, language: str, template_type: str, filename: str
) -> str:
"""Load a single template file.
Args:
language: Programming language.
template_type: Template variant.
filename: Template filename.
Returns:
Template content as string.
"""
template_path = f"{language}/{template_type}/{filename}"
try:
template = self.env.get_template(template_path)
return template.filename
except Exception:
return ""
def render(
self,
template_name: str,
context: Dict[str, Any],
language: str = "python",
) -> str:
"""Render a template with the given context.
Args:
template_name: Name of the template to render.
context: Dictionary of variables to pass to the template.
language: Programming language for template lookup.
Returns:
Rendered template string.
"""
try:
full_name = f"{language}/{template_name}"
template = self.env.get_template(f"{full_name}.j2")
return template.render(**context)
except TemplateSyntaxError as e:
raise ValueError(f"Template syntax error in {template_name}: {e}")
except Exception as e:
raise ValueError(f"Failed to render template {template_name}: {e}")
def render_string(self, template_content: str, context: Dict[str, Any]) -> str:
"""Render a template string directly.
Args:
template_content: Template content as string.
context: Dictionary of variables.
Returns:
Rendered string.
"""
template = self.env.from_string(template_content)
return template.render(**context)
@staticmethod
def list_available_templates(language: str) -> List[str]:
def list_available_templates(self, language: str) -> list[str]:
"""List available templates for a language.
Args:
@@ -140,85 +57,105 @@ class TemplateEngine:
Returns:
List of template names.
"""
templates_dir = Path(__file__).parent / language
if not templates_dir.exists():
if self.template_dir:
base_dir = self.template_dir
else:
base_dir = str(Path(__file__).parent)
template_path = Path(base_dir) / language
if not template_path.exists():
return []
templates = []
for item in templates_dir.iterdir():
if item.is_dir():
templates.append(item.name)
return sorted(templates)
return [
d.name
for d in template_path.iterdir()
if d.is_dir() and not d.name.startswith("_")
]
@staticmethod
def list_available_languages() -> List[str]:
"""List all available programming languages.
def render(
self, template_name: str, context: dict[str, Any], language: str
) -> str:
"""Render a template with context.
Args:
template_name: Name of the template file.
context: Context dictionary for template rendering.
language: Programming language.
Returns:
List of language identifiers.
Rendered template string.
"""
templates_dir = Path(__file__).parent
if not templates_dir.exists():
return []
if language not in self._templates:
self.load_templates(language)
languages = []
for item in templates_dir.iterdir():
if item.is_dir() and not item.name.startswith("_"):
languages.append(item.name)
return sorted(languages)
if language not in self._templates:
raise ValueError(f"Templates not loaded for language: {language}")
def get_template_context(self, issue_data: IssueData) -> Dict[str, Any]:
env = self._templates[language]
template_file = f"{template_name}.j2"
if template_file not in env.list_templates():
raise ValueError(f"Template not found: {template_file}")
template = env.get_template(template_file)
return template.render(**context)
def get_template_context(self, issue_data: IssueData) -> dict[str, Any]:
"""Generate template context from issue data.
Args:
issue_data: IssueData object.
Returns:
Dictionary of template variables.
Context dictionary for templates.
"""
project_name = self._generate_project_name(issue_data)
project_name = self._sanitize_name(issue_data.title)
return {
context = {
"project_name": project_name,
"project_name_kebab": self._to_kebab_case(project_name),
"project_name_snake": self._to_snake_case(project_name),
"project_name_pascal": self._to_pascal_case(project_name),
"project_name_kebab": project_name.lower().replace("_", "-"),
"project_name_snake": project_name.lower().replace("-", "_"),
"project_name_pascal": "".join(
word.capitalize() for word in re.findall(r"[a-zA-Z]+", project_name)
),
"issue_number": issue_data.number,
"issue_title": issue_data.title,
"issue_url": issue_data.url,
"repository": issue_data.repository,
"author": issue_data.author,
"created_date": issue_data.created_at[:10] if issue_data.created_at else "",
"created_date": issue_data.created_at,
"todo_items": issue_data.get_todo_items(),
"completed_items": issue_data.get_completed_items(),
"requirements": issue_data.requirements,
"acceptance_criteria": issue_data.acceptance_criteria,
"checklist": issue_data.checklist,
}
def _generate_project_name(self, issue_data: IssueData) -> str:
"""Generate a project name from issue title.
return context
def _sanitize_name(self, name: str) -> str:
"""Sanitize a string for use as a project name.
Args:
issue_data: IssueData object.
name: Original name.
Returns:
Project name string.
Sanitized name.
"""
title = issue_data.title
title = re.sub(r"[^a-zA-Z0-9\s]", "", title)
title = re.sub(r"\s+", "_", title.strip())
return title.lower()[:50]
import re
def _to_kebab_case(self, text: str) -> str:
"""Convert text to kebab-case."""
return re.sub(r"[^a-zA-Z0-9]+", "-", text).strip("-").lower()
name = re.sub(r"[^a-zA-Z0-9\s]", "", name)
name = re.sub(r"\s+", " ", name.strip())
return name.replace(" ", "_")
def _to_snake_case(self, text: str) -> str:
"""Convert text to snake_case."""
return re.sub(r"[^a-zA-Z0-9]+", "_", text).strip("_").lower()
def _to_pascal_case(self, text: str) -> str:
"""Convert text to PascalCase."""
words = re.findall(r"[a-zA-Z0-9]+", text)
return "".join(word.title() for word in words)
def get_template_engine(template_dir: str | None = None) -> TemplateEngine:
"""Get a template engine instance.
Args:
template_dir: Optional custom template directory.
Returns:
TemplateEngine instance.
"""
return TemplateEngine(template_dir)

View File

@@ -0,0 +1 @@
"""Go template files."""

View File

@@ -1,16 +1,16 @@
// Main entry point for {{ project_name }}
// Main entry point for {{ project_name }}.
// Generated from GitHub Issue #{{ issue_number }}: {{ issue_title }}
// URL: {{ issue_url }}
package main
import "fmt"
{% for item in todo_items %}
// TODO #{{ loop.index }}: {{ item }}
{% endfor %}
func main() {
// TODO: Implement main functionality
// {% for item in todo_items %}
// TODO #{{ loop.index }}: {{ item }}
// {% endfor %}
fmt.Println("{{ project_name }} is running")
fmt.Printf("Repository: {{ repository }}\n")
fmt.Printf("Issue: {{ issue_number }}\n")

View File

@@ -1,9 +1,6 @@
// Utility functions for {{ project_name }}
// Utility functions for {{ project_name }}.
// Generated from GitHub Issue #{{ issue_number }}
package main
// TODO: Implement helper function
func HelperFunction() {
// TODO: Implement
}
// TODO: Implement utility functions

View File

@@ -0,0 +1 @@
"""JavaScript template files."""

View File

@@ -1,12 +1,15 @@
{
"env": {
"browser": true,
"commonjs": true,
"es2021": true
"es2022": true,
"node": true
},
"extends": "eslint:recommended",
"parserOptions": {
"ecmaVersion": 12
"ecmaVersion": "latest",
"sourceType": "module"
},
"rules": {}
"rules": {
"no-console": "warn"
}
}

View File

@@ -1,10 +1,13 @@
/**
* Main entry point for {{ project_name }}
* Main entry point for {{ project_name }}.
* Generated from GitHub Issue #{{ issue_number }}: {{ issue_title }}
*/
{% for item in todo_items %}
// TODO: Implement main functionality
// {% for item in todo_items %}
// TODO #{{ loop.index }}: {{ item }}
{% endfor %}
// {% endfor %}
console.log("{{ project_name }} is running");
console.log("Repository: {{ repository }}");
console.log("Issue: {{ issue_number }}");

View File

@@ -1,11 +1,6 @@
/**
* Utility functions for {{ project_name }}
* Utility functions for {{ project_name }}.
* Generated from GitHub Issue #{{ issue_number }}
*/
/**
* TODO: Implement helper function
*/
export function helperFunction() {
// TODO: Implement
}
// TODO: Implement utility functions

View File

@@ -7,6 +7,8 @@
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "MIT"
"author": "{{ author }}",
"license": "MIT",
"dependencies": {},
"devDependencies": {}
}

View File

@@ -0,0 +1 @@
"""Python template files."""

View File

@@ -1,4 +1,4 @@
"""CLI interface for {{ project_name }}.
"""CLI module for {{ project_name }}.
Generated from GitHub Issue #{{ issue_number }}
"""
@@ -7,7 +7,10 @@ import click
@click.command()
@click.option("--option", help="TODO: Describe option")
def cli(option: str):
def cli():
"""TODO: Implement CLI command."""
click.echo("{{ project_name }} CLI")
pass
if __name__ == "__main__":
cli()

View File

@@ -3,12 +3,14 @@
Generated from GitHub Issue #{{ issue_number }}
"""
class DataModel:
"""TODO: Implement data model class."""
pass
from dataclasses import dataclass
class AnotherModel:
"""TODO: Implement another model class."""
pass
@dataclass
class Item:
"""TODO: Define item model."""
name: str
description: str = ""
# TODO: Add more models based on requirements

View File

@@ -9,6 +9,4 @@ def helper_function():
pass
def another_helper():
"""TODO: Implement another helper."""
pass
# TODO: Add more utility functions based on requirements

View File

@@ -1,4 +1,7 @@
# {{ project_name }} requirements
# Generated from GitHub Issue #{{ issue_number }}
# Core dependencies
click>=8.1.7
pygithub>=2.3.0
jinja2>=3.1.4
PyYAML>=6.0.2
# TODO: Add more dependencies based on requirements

View File

@@ -0,0 +1 @@
"""Rust template files."""

View File

@@ -1,10 +1,6 @@
// Library code for {{ project_name }}
// Library code for {{ project_name }}.
// Generated from GitHub Issue #{{ issue_number }}
{% for item in todo_items %}
// {% for item in todo_items %}
// TODO #{{ loop.index }}: {{ item }}
{% endfor %}
pub fn helper_function() {
// TODO: Implement
}
// {% endfor %}

View File

@@ -1,10 +1,9 @@
// Main entry point for {{ project_name }}
// Main entry point for {{ project_name }}.
// Generated from GitHub Issue #{{ issue_number }}: {{ issue_title }}
// URL: {{ issue_url }}
{% for item in todo_items %}
// {% for item in todo_items %}
// TODO #{{ loop.index }}: {{ item }}
{% endfor %}
// {% endfor %}
fn main() {
println!("{{ project_name }} is running");

View File

@@ -1,9 +1,14 @@
from setuptools import setup, find_packages
#!/usr/bin/env python
"""Setup script for ScaffoldForge."""
from setuptools import find_packages, setup
setup(
name="scaffoldforge",
version="0.1.0",
packages=find_packages(),
py_modules=["scaffoldforge"],
packages=find_packages(exclude=["tests*"]),
python_requires=">=3.9",
install_requires=[
"click>=8.1.7",
"pygithub>=2.3.0",
@@ -23,5 +28,4 @@ setup(
"scaffoldforge=scaffoldforge.main:main",
],
},
python_requires=">=3.9",
)

View File

@@ -1,17 +1,15 @@
"""Integration tests for ScaffoldForge."""
import pytest
import os
import tempfile
from pathlib import Path
from unittest.mock import Mock, patch, MagicMock
from unittest.mock import Mock, patch
from click.testing import CliRunner
from scaffoldforge.cli import cli
from scaffoldforge.parsers import IssueParser, IssueData, ChecklistItem
from scaffoldforge.generators import CodeGenerator, StructureGenerator
from scaffoldforge.parsers import ChecklistItem, IssueData
from scaffoldforge.templates import TemplateEngine
from scaffoldforge.generators import StructureGenerator, CodeGenerator
class TestFullWorkflow:
@@ -207,9 +205,9 @@ class TestFullWorkflow:
runner = CliRunner()
with patch('scaffoldforge.cli.commands.IssueParser') as mock_parser, \
patch('scaffoldforge.cli.commands.TemplateEngine') as mock_tpl_engine, \
patch('scaffoldforge.cli.commands.StructureGenerator') as mock_struct_gen, \
patch('scaffoldforge.cli.commands.CodeGenerator') as mock_code_gen:
patch('scaffoldforge.cli.commands.TemplateEngine') as _mock_tpl_engine, \
patch('scaffoldforge.cli.commands.StructureGenerator') as _mock_struct_gen, \
patch('scaffoldforge.cli.commands.CodeGenerator') as _mock_code_gen:
mock_issue_data = Mock()
mock_issue_data.number = 1

86
tests/test_extractors.py Normal file
View File

@@ -0,0 +1,86 @@
"""Tests for field extraction module."""
from cmdparse.extractors import extract_array_index, extract_fields, flatten_dict, get_nested_value
class TestGetNestedValue:
def test_simple_dict_access(self):
data = {"name": "John", "age": 30}
assert get_nested_value(data, "name") == "John"
assert get_nested_value(data, "age") == 30
def test_nested_dict_access(self):
data = {"user": {"address": {"city": "NYC"}}}
assert get_nested_value(data, "user.address.city") == "NYC"
def test_list_index_access(self):
data = {"items": ["a", "b", "c"]}
assert get_nested_value(data, "items.0") == "a"
assert get_nested_value(data, "items.1") == "b"
def test_missing_key_returns_none(self):
data = {"name": "John"}
assert get_nested_value(data, "age") is None
def test_none_data_returns_none(self):
assert get_nested_value(None, "name") is None
class TestExtractArrayIndex:
def test_with_array_index(self):
base, index, rest = extract_array_index("items[0].name")
assert base == "items"
assert index == 0
assert rest == "name"
def test_without_array_index(self):
base, index, rest = extract_array_index("name")
assert base == "name"
assert index is None
assert rest is None
class TestExtractFields:
def test_extract_single_field(self):
data = [{"name": "John", "age": 30, "city": "NYC"}]
result = extract_fields(data, ["name"])
assert result == [{"name": "John"}]
def test_extract_multiple_fields(self):
data = [{"name": "John", "age": 30, "city": "NYC"}]
result = extract_fields(data, ["name", "city"])
assert result == [{"name": "John", "city": "NYC"}]
def test_extract_nested_fields(self):
data = [{"user": {"name": "John", "age": 30}}]
result = extract_fields(data, ["user.name"])
assert result == [{"user.name": "John"}]
def test_extract_from_multiple_rows(self):
data = [{"name": "John", "age": 30}, {"name": "Jane", "age": 25}]
result = extract_fields(data, ["name"])
assert result == [{"name": "John"}, {"name": "Jane"}]
def test_empty_fields_returns_original(self):
data = [{"name": "John", "age": 30}]
result = extract_fields(data, [])
assert result == data
def test_empty_data_returns_empty(self):
result = extract_fields([], ["name"])
assert result == []
class TestFlattenDict:
def test_flatten_simple_dict(self):
d = {"name": "John", "age": 30}
result = flatten_dict(d)
assert result == {"name": "John", "age": 30}
def test_flatten_nested_dict(self):
d = {"user": {"name": "John", "address": {"city": "NYC"}}}
result = flatten_dict(d)
assert "user.name" in result
assert "user.address.city" in result
assert result["user.name"] == "John"
assert result["user.address.city"] == "NYC"

105
tests/test_formatters.py Normal file
View File

@@ -0,0 +1,105 @@
"""Tests for output formatting module."""
from cmdparse.formatters import format_csv, format_data, format_json, format_raw, format_yaml
class TestFormatJson:
def test_format_simple_list(self):
data = [{"name": "John", "age": 30}]
result = format_json(data)
assert "John" in result
assert "30" in result
def test_format_empty_list(self):
result = format_json([])
assert result == "[]"
def test_pretty_format(self):
data = [{"name": "John"}]
result = format_json(data, pretty=True)
assert "\n" in result
def test_compact_format(self):
data = [{"name": "John"}]
result = format_json(data, pretty=False)
assert "\n" not in result
class TestFormatYaml:
def test_format_simple_list(self):
data = [{"name": "John", "age": 30}]
result = format_yaml(data)
assert "John" in result
assert "age" in result
def test_format_empty_list(self):
result = format_yaml([])
assert result == "[]\n"
def test_format_nested_structure(self):
data = [{"user": {"name": "John", "tags": ["a", "b"]}}]
result = format_yaml(data)
assert "John" in result
class TestFormatCsv:
def test_format_simple_list(self):
data = [{"name": "John", "age": 30}]
result = format_csv(data)
assert "name" in result
assert "John" in result
assert "age" in result
def test_format_empty_list(self):
result = format_csv([])
assert result == ""
def test_format_multiple_rows(self):
data = [{"name": "John", "age": 30}, {"name": "Jane", "age": 25}]
result = format_csv(data)
assert "John" in result
assert "Jane" in result
class TestFormatRaw:
def test_format_simple_list(self):
data = [{"name": "John", "age": 30}]
result = format_raw(data)
assert "name: John" in result
assert "age: 30" in result
def test_format_empty_list(self):
result = format_raw([])
assert result == ""
class TestFormatData:
def test_json_format(self):
data = [{"name": "John"}]
result = format_data(data, "json")
assert "John" in result
def test_yaml_format(self):
data = [{"name": "John"}]
result = format_data(data, "yaml")
assert "John" in result
def test_csv_format(self):
data = [{"name": "John"}]
result = format_data(data, "csv")
assert "name" in result
def test_raw_format(self):
data = [{"name": "John"}]
result = format_data(data, "raw")
assert "John" in result
def test_default_to_json(self):
data = [{"name": "John"}]
result = format_data(data, "")
assert "John" in result
def test_unknown_format_defaults_to_json(self):
data = [{"name": "John"}]
result = format_data(data, "unknown")
assert "John" in result

113
tests/test_integration.py Normal file
View File

@@ -0,0 +1,113 @@
"""End-to-end integration tests for cmdparse CLI."""
from click.testing import CliRunner
from cmdparse.cli import main
class TestCLI:
def test_parse_table_to_json(self):
runner = CliRunner()
input_text = """NAME IMAGE STATUS
nginx nginx:1 running
redis redis:3 stopped"""
result = runner.invoke(main, ["-o", "json", "-q"], input=input_text)
assert result.exit_code == 0
assert "nginx" in result.output
def test_parse_key_value_to_yaml(self):
runner = CliRunner()
input_text = """name: John
age: 30
city: NYC"""
result = runner.invoke(main, ["-o", "yaml", "-q"], input=input_text)
assert result.exit_code == 0
assert "John" in result.output
assert "name" in result.output
def test_parse_csv_to_csv(self):
runner = CliRunner()
input_text = """name,age,city
John,30,NYC"""
result = runner.invoke(main, ["-o", "csv", "-q"], input=input_text)
assert result.exit_code == 0
assert "John" in result.output
def test_field_extraction(self):
runner = CliRunner()
input_text = """NAME IMAGE STATUS
nginx nginx:1 running
redis redis:3 stopped"""
result = runner.invoke(
main, ["-o", "json", "-q", "-e", "NAME", "-e", "STATUS"], input=input_text
)
assert result.exit_code == 0
output = result.output
assert "nginx" in output
assert "running" in output
def test_no_input_error(self):
runner = CliRunner()
result = runner.invoke(main, ["-q"], input="")
assert result.exit_code == 1
assert "error" in result.output.lower() or "input" in result.output.lower()
def test_auto_format_detection(self):
runner = CliRunner()
input_text = """name: John
age: 30"""
result = runner.invoke(main, ["-f", "auto", "-o", "json", "-q"], input=input_text)
assert result.exit_code == 0
assert "John" in result.output
def test_help_option(self):
runner = CliRunner()
result = runner.invoke(main, ["--help"])
assert result.exit_code == 0
assert "--output" in result.output
assert "--field" in result.output
assert "--config" in result.output
def test_parse_raw_text(self):
runner = CliRunner()
input_text = """Some random text line 1
Another line here
Third line"""
result = runner.invoke(main, ["-o", "json", "-q"], input=input_text)
assert result.exit_code == 0
assert "line" in result.output.lower() or "1" in result.output
def test_parse_delimited_semicolon(self):
runner = CliRunner()
input_text = """name;age;city
John;30;NYC"""
result = runner.invoke(main, ["-o", "json", "-q"], input=input_text)
assert result.exit_code == 0
assert "John" in result.output
class TestIntegrationScenarios:
def test_docker_ps_style_output(self):
runner = CliRunner()
input_text = """CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
abc123 nginx:1 "nginx" 1h ago Up 80/tcp nginx"""
result = runner.invoke(main, ["-o", "json", "-q"], input=input_text)
assert result.exit_code == 0
assert "nginx" in result.output
def test_df_style_output(self):
runner = CliRunner()
input_text = """Filesystem Size Used Avail Use% Mounted on
/dev/sda1 100G 50G 50G 50% /"""
result = runner.invoke(main, ["-o", "json", "-q"], input=input_text)
assert result.exit_code == 0
assert "/dev/sda1" in result.output
def test_key_value_with_special_chars(self):
runner = CliRunner()
input_text = """DATABASE_URL=postgresql://user:pass@localhost:5432/db
API_KEY=abc123xyz
DEBUG=true"""
result = runner.invoke(main, ["-o", "yaml", "-q"], input=input_text)
assert result.exit_code == 0
assert "DATABASE_URL" in result.output

50
tests/test_parser.py Normal file
View File

@@ -0,0 +1,50 @@
"""Tests for pattern detection module."""
from cmdparse.patterns import detect_pattern_type
class TestDetectPatternType:
def test_empty_input_returns_empty(self):
assert detect_pattern_type("") == "empty"
assert detect_pattern_type(" ") == "empty"
assert detect_pattern_type("\n\n") == "empty"
def test_table_detection(self):
table_input = """NAME IMAGE STATUS
nginx nginx:1 running
redis redis:3 stopped"""
result = detect_pattern_type(table_input)
assert result in ["table", "key_value_block"]
def test_key_value_colon_detection(self):
kv_input = """name: John
age: 30
city: NYC"""
result = detect_pattern_type(kv_input)
assert result == "key_value_colon"
def test_key_value_equals_detection(self):
kv_input = """name=John
age=30
city=NYC"""
result = detect_pattern_type(kv_input)
assert result == "key_value_equals"
def test_delimited_comma_detection(self):
csv_input = """name,age,city
John,30,NYC
Jane,25,LA"""
result = detect_pattern_type(csv_input)
assert result == "delimited_comma"
def test_delimited_tab_detection(self):
tsv_input = """name\tage\tcity
John\t30\tNYC"""
result = detect_pattern_type(tsv_input)
assert result == "delimited_tab"
def test_raw_text_detection(self):
raw_input = """This is just some random text
Without any particular structure"""
result = detect_pattern_type(raw_input)
assert result in ["raw", "table", "key_value_block"]

View File

@@ -1,8 +1,9 @@
"""Unit tests for the CLI module."""
from unittest.mock import Mock, patch
import pytest
from click.testing import CliRunner
from unittest.mock import Mock, patch, MagicMock
from scaffoldforge.cli import cli
from scaffoldforge.cli.commands import parse_github_url

View File

@@ -1,14 +1,11 @@
"""Unit tests for the generators module."""
import pytest
import os
import tempfile
from pathlib import Path
from unittest.mock import Mock, patch, MagicMock
from unittest.mock import Mock, patch
from scaffoldforge.generators import StructureGenerator, CodeGenerator
from scaffoldforge.generators import CodeGenerator, StructureGenerator
from scaffoldforge.generators.structure import FileSpec
from scaffoldforge.parsers import IssueData, ChecklistItem
from scaffoldforge.parsers import ChecklistItem, IssueData
from scaffoldforge.templates import TemplateEngine

View File

@@ -1,9 +1,8 @@
"""Unit tests for the parsers module."""
import pytest
from unittest.mock import Mock, patch, MagicMock
from unittest.mock import patch
from scaffoldforge.parsers import IssueParser, IssueData, ChecklistItem
from scaffoldforge.parsers import ChecklistItem, IssueData, IssueParser
class TestChecklistItem:
@@ -109,13 +108,13 @@ class TestIssueParser:
@patch('scaffoldforge.parsers.issue_parser.Github')
def test_parser_initialization_without_token(self, mock_github):
"""Test parser initialization without token."""
parser = IssueParser()
IssueParser()
mock_github.assert_called_once()
@patch('scaffoldforge.parsers.issue_parser.Github')
def test_parser_initialization_with_token(self, mock_github):
"""Test parser initialization with token."""
parser = IssueParser(token="test_token")
IssueParser(token="test_token")
mock_github.assert_called_once_with("test_token")
def test_parse_checklist_simple(self):

View File

@@ -1,11 +1,8 @@
"""Unit tests for the templates module."""
import pytest
from unittest.mock import patch, MagicMock
from pathlib import Path
from scaffoldforge.parsers import ChecklistItem, IssueData
from scaffoldforge.templates import TemplateEngine
from scaffoldforge.parsers import IssueData, ChecklistItem
class TestTemplateEngine: