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 GITHUB_TOKEN=your_github_token_here
SCAFFOLD_TEMPLATE_DIR=
# Default template directory path SCAFFOLD_OUTPUT_DIR=./generated
# SCAFFOLD_TEMPLATE_DIR=./templates
# Default output directory
# SCAFFOLD_OUTPUT_DIR=./output

View File

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

185
.gitignore vendored
View File

@@ -1,138 +1,47 @@
# Byte-compiled / optimized / DLL files {
__pycache__/ "Python": [
*.py[cod] "__pycache__/",
*$py.class "*.py[cod]",
"*$py.class",
# C extensions "*.so",
*.so ".Python",
"build/",
# Distribution / packaging "dist/",
.Python "*.egg-info/",
build/ ".eggs/",
develop-eggs/ "venv/",
dist/ ".env"
downloads/ ],
eggs/ "Node": [
.eggs/ "node_modules/",
lib/ "npm-debug.log*",
lib64/ "yarn-debug.log*",
parts/ "yarn-error.log*",
sdist/ "dist/",
var/ "coverage/"
wheels/ ],
pip-wheel-metadata/ "Go": [
share/python-wheels/ "*.exe",
*.egg-info/ "*.exe~",
.installed.cfg "*.dll",
*.egg "*.so",
MANIFEST "*.dylib",
"*.test",
# PyInstaller "*.out",
*.manifest "go.work"
*.spec ],
"Rust": [
# Installer logs "target/",
pip-log.txt "Cargo.lock",
pip-delete-this-directory.txt "*.swp",
"*.swo"
# Unit test / coverage reports ],
htmlcov/ "General": [
.tox/ ".DS_Store",
.nox/ ".vscode/",
.coverage ".idea/",
.coverage.* "*.swp",
.cache "*.swo",
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/

139
README.md
View File

@@ -1,21 +1,7 @@
# ScaffoldForge # 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. 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 ## Installation
```bash ```bash
@@ -25,7 +11,7 @@ pip install scaffoldforge
Or from source: Or from source:
```bash ```bash
git clone https://7000pct.gitea.bloupla.net/7000pctAUTO/scaffoldforge.git git clone https://github.com/yourusername/scaffoldforge.git
cd scaffoldforge cd scaffoldforge
pip install -e . pip install -e .
``` ```
@@ -35,12 +21,34 @@ pip install -e .
1. Set up your GitHub Personal Access Token: 1. Set up your GitHub Personal Access Token:
```bash ```bash
export GITHUB_TOKEN=your_github_token_here export GITHUB_TOKEN=your_github_token_here
``` ```
2. Generate a project from a GitHub issue: 2. Generate a project from a GitHub issue:
```bash ```bash
scaffoldforge generate https://github.com/owner/repo/issues/123 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 ## Usage
@@ -89,12 +97,52 @@ Interactive mode:
scaffoldforge generate https://github.com/owner/repo/issues/123 --interactive scaffoldforge generate https://github.com/owner/repo/issues/123 --interactive
``` ```
List available templates: ### List Available Templates
```bash ```bash
scaffoldforge list-templates --language python 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 ## Supported Languages
- **Python** - Generates `main.py`, `utils.py`, `models.py`, `pyproject.toml` - **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` - **Go** - Generates `main.go`, `utils.go`, `go.mod`
- **Rust** - Generates `main.rs`, `lib.rs`, `Cargo.toml` - **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 ## How It Works
1. **Issue Parsing**: Fetches the GitHub issue and parses title, description, checklist items, and labels 1. **Issue Parsing**: ScaffoldForge fetches the GitHub issue and parses:
2. **Language Detection**: Automatically detects programming language from labels or content - Title and description
3. **Template Selection**: Selects appropriate templates based on language - Checklist items (converted to TODO comments)
4. **Context Generation**: Creates template context with issue data - Labels (used for language detection)
5. **File Generation**: Renders templates and writes files - Requirements sections
6. **Documentation**: Generates README.md and .gitignore - 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 ## Contributing

View File

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

View File

@@ -1,15 +1,12 @@
"""CLI commands for ScaffoldForge.""" """CLI commands for ScaffoldForge."""
import os
import re import re
import sys
from pathlib import Path
from typing import Optional from typing import Optional
import click import click
from scaffoldforge.config import get_config 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.parsers import IssueParser
from scaffoldforge.templates import TemplateEngine from scaffoldforge.templates import TemplateEngine

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,8 @@
"""Project structure generation functionality.""" """Project structure generation functionality."""
import os import os
import stat
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List, Optional from typing import Optional
from scaffoldforge.generators.code import CodeGenerator from scaffoldforge.generators.code import CodeGenerator
from scaffoldforge.generators.models import FileSpec from scaffoldforge.generators.models import FileSpec
@@ -26,8 +25,8 @@ class StructureGenerator:
""" """
self.output_dir = output_dir or "./generated" self.output_dir = output_dir or "./generated"
self.preview = preview self.preview = preview
self.created_files: List[str] = [] self.created_files: list[str] = []
self.created_dirs: List[str] = [] self.created_dirs: list[str] = []
def generate( def generate(
self, self,
@@ -50,9 +49,9 @@ class StructureGenerator:
base_path = Path(self.output_dir) / project_name base_path = Path(self.output_dir) / project_name
if self.preview: if self.preview:
print(f"\n{'='*60}") print(f"\n{'=' * 60}")
print(f"PREVIEW: Project would be created at: {base_path}") print(f"PREVIEW: Project would be created at: {base_path}")
print(f"{'='*60}\n") print(f"{'=' * 60}\n")
files = code_generator.generate_all_files(language, issue_data) files = code_generator.generate_all_files(language, issue_data)
@@ -67,9 +66,7 @@ class StructureGenerator:
if self.preview: if self.preview:
self._print_preview_summary() self._print_preview_summary()
def _create_directories( def _create_directories(self, base_path: Path, issue_data: IssueData) -> None:
self, base_path: Path, issue_data: IssueData
) -> None:
"""Create project directories. """Create project directories.
Args: Args:
@@ -115,7 +112,7 @@ class StructureGenerator:
if self.preview: if self.preview:
print(f"[PREVIEW] Would create file: {file_path}") print(f"[PREVIEW] Would create file: {file_path}")
if file_spec.executable: if file_spec.executable:
print(f" (executable)") print(" (executable)")
return return
try: try:
@@ -128,9 +125,7 @@ class StructureGenerator:
except OSError as e: except OSError as e:
raise OSError(f"Failed to write file {file_path}: {e}") raise OSError(f"Failed to write file {file_path}: {e}")
def _create_readme( def _create_readme(self, base_path: Path, issue_data: IssueData, project_name: str) -> None:
self, base_path: Path, issue_data: IssueData, project_name: str
) -> None:
"""Create README.md file. """Create README.md file.
Args: Args:
@@ -144,7 +139,7 @@ class StructureGenerator:
## Description ## 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} **GitHub Issue:** #{issue_data.number}
**Repository:** {issue_data.repository} **Repository:** {issue_data.repository}
@@ -152,11 +147,19 @@ class StructureGenerator:
## Requirements ## 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 ## 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 ## Getting Started
@@ -184,9 +187,7 @@ MIT
FileSpec(path="README.md", content=readme_content), FileSpec(path="README.md", content=readme_content),
) )
def _create_gitignore( def _create_gitignore(self, base_path: Path, project_name: str, language: str) -> None:
self, base_path: Path, project_name: str, language: str
) -> None:
"""Create .gitignore file based on language. """Create .gitignore file based on language.
Args: Args:
@@ -248,23 +249,24 @@ Cargo.lock
Sanitized name. Sanitized name.
""" """
import re import re
name = re.sub(r"[^a-zA-Z0-9\s_-]", "", name) name = re.sub(r"[^a-zA-Z0-9\s_-]", "", name)
name = re.sub(r"\s+", "-", name.strip()) name = re.sub(r"\s+", "-", name.strip())
return name.lower()[:50] or "project" return name.lower()[:50] or "project"
def _print_preview_summary(self) -> None: def _print_preview_summary(self) -> None:
"""Print a summary of what would be created in preview mode.""" """Print a summary of what would be created in preview mode."""
print(f"\n{'='*60}") print(f"\n{'=' * 60}")
print("PREVIEW SUMMARY") print("PREVIEW SUMMARY")
print(f"{'='*60}") print(f"{'=' * 60}")
print(f"Directories: {len(self.created_dirs)}") print(f"Directories: {len(self.created_dirs)}")
print(f"Files: {len(self.created_files)}") print(f"Files: {len(self.created_files)}")
print(f"{'='*60}\n") print(f"{'=' * 60}\n")
def get_created_files(self) -> List[str]: def get_created_files(self) -> list[str]:
"""Get list of created files.""" """Get list of created files."""
return self.created_files.copy() return self.created_files.copy()
def get_created_directories(self) -> List[str]: def get_created_directories(self) -> list[str]:
"""Get list of created directories.""" """Get list of created directories."""
return self.created_dirs.copy() return self.created_dirs.copy()

View File

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

View File

@@ -1,5 +1,5 @@
"""Parsers module for ScaffoldForge.""" """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"] __all__ = ["IssueParser", "IssueData", "ChecklistItem"]

View File

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

View File

@@ -1,137 +1,54 @@
"""Template engine for ScaffoldForge.""" """Template rendering functionality."""
import os import os
import re
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List, Optional from typing import Any
from jinja2 import ( from jinja2 import BaseLoader, Environment, FileSystemLoader, TemplateSyntaxError
BaseLoader,
Environment,
FileSystemLoader,
PackageLoader,
TemplateSyntaxError,
)
from scaffoldforge.parsers import IssueData from scaffoldforge.parsers import IssueData
class TemplateEngine: 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. """Initialize the template engine.
Args: Args:
template_dir: Path to custom template directory. template_dir: Optional custom template directory.
""" """
self.template_dir = template_dir self.template_dir = template_dir
self.env = self._create_environment() self._templates: dict[str, Any] = {}
self._loaded_templates: Dict[str, Dict[str, Any]] = {} self._loaded: dict[str, dict[str, Any]] = {}
def _create_environment(self) -> Environment: def load_templates(self, language: str, template_name: str = "default") -> None:
"""Create Jinja2 environment with appropriate loader.""" """Load templates for a specific language and template.
if self.template_dir and Path(self.template_dir).exists():
loader = FileSystemLoader(self.template_dir) Args:
language: Programming language.
template_name: Name of the template set.
"""
if self.template_dir:
base_dir = self.template_dir
else: else:
loader = PackageLoader("scaffoldforge", "templates") base_dir = str(Path(__file__).parent)
return Environment( template_path = Path(base_dir) / language / template_name
loader=loader,
autoescape=True,
trim_blocks=True,
lstrip_blocks=True,
)
def load_templates( if not template_path.exists():
self, language: str, template_name: str = "default" return
) -> Dict[str, str]:
"""Load templates for a specific language and template type.
Args: loader = FileSystemLoader(str(template_path))
language: Programming language. env = Environment(loader=loader, autoescape=True)
template_name: Template variant name.
Returns: self._templates[language] = env
Dictionary mapping template names to rendered content. self._loaded[language] = {
""" "path": str(template_path),
key = f"{language}/{template_name}" "templates": list(env.list_templates()),
if key in self._loaded_templates: }
return self._loaded_templates[key]
templates = {} def list_available_templates(self, language: str) -> list[str]:
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]:
"""List available templates for a language. """List available templates for a language.
Args: Args:
@@ -140,85 +57,105 @@ class TemplateEngine:
Returns: Returns:
List of template names. List of template names.
""" """
templates_dir = Path(__file__).parent / language if self.template_dir:
if not templates_dir.exists(): base_dir = self.template_dir
else:
base_dir = str(Path(__file__).parent)
template_path = Path(base_dir) / language
if not template_path.exists():
return [] return []
templates = [] return [
for item in templates_dir.iterdir(): d.name
if item.is_dir(): for d in template_path.iterdir()
templates.append(item.name) if d.is_dir() and not d.name.startswith("_")
return sorted(templates) ]
@staticmethod def render(
def list_available_languages() -> List[str]: self, template_name: str, context: dict[str, Any], language: str
"""List all available programming languages. ) -> str:
"""Render a template with context.
Args:
template_name: Name of the template file.
context: Context dictionary for template rendering.
language: Programming language.
Returns: Returns:
List of language identifiers. Rendered template string.
""" """
templates_dir = Path(__file__).parent if language not in self._templates:
if not templates_dir.exists(): self.load_templates(language)
return []
languages = [] if language not in self._templates:
for item in templates_dir.iterdir(): raise ValueError(f"Templates not loaded for language: {language}")
if item.is_dir() and not item.name.startswith("_"):
languages.append(item.name)
return sorted(languages)
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. """Generate template context from issue data.
Args: Args:
issue_data: IssueData object. issue_data: IssueData object.
Returns: 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": project_name,
"project_name_kebab": self._to_kebab_case(project_name), "project_name_kebab": project_name.lower().replace("_", "-"),
"project_name_snake": self._to_snake_case(project_name), "project_name_snake": project_name.lower().replace("-", "_"),
"project_name_pascal": self._to_pascal_case(project_name), "project_name_pascal": "".join(
word.capitalize() for word in re.findall(r"[a-zA-Z]+", project_name)
),
"issue_number": issue_data.number, "issue_number": issue_data.number,
"issue_title": issue_data.title, "issue_title": issue_data.title,
"issue_url": issue_data.url, "issue_url": issue_data.url,
"repository": issue_data.repository, "repository": issue_data.repository,
"author": issue_data.author, "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(), "todo_items": issue_data.get_todo_items(),
"completed_items": issue_data.get_completed_items(), "completed_items": issue_data.get_completed_items(),
"requirements": issue_data.requirements, "requirements": issue_data.requirements,
"acceptance_criteria": issue_data.acceptance_criteria, "acceptance_criteria": issue_data.acceptance_criteria,
"checklist": issue_data.checklist,
} }
def _generate_project_name(self, issue_data: IssueData) -> str: return context
"""Generate a project name from issue title.
def _sanitize_name(self, name: str) -> str:
"""Sanitize a string for use as a project name.
Args: Args:
issue_data: IssueData object. name: Original name.
Returns: Returns:
Project name string. Sanitized name.
""" """
title = issue_data.title import re
title = re.sub(r"[^a-zA-Z0-9\s]", "", title)
title = re.sub(r"\s+", "_", title.strip())
return title.lower()[:50]
def _to_kebab_case(self, text: str) -> str: name = re.sub(r"[^a-zA-Z0-9\s]", "", name)
"""Convert text to kebab-case.""" name = re.sub(r"\s+", " ", name.strip())
return re.sub(r"[^a-zA-Z0-9]+", "-", text).strip("-").lower() 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: def get_template_engine(template_dir: str | None = None) -> TemplateEngine:
"""Convert text to PascalCase.""" """Get a template engine instance.
words = re.findall(r"[a-zA-Z0-9]+", text)
return "".join(word.title() for word in words) 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 }} // Generated from GitHub Issue #{{ issue_number }}: {{ issue_title }}
// URL: {{ issue_url }}
package main package main
import "fmt" import "fmt"
{% for item in todo_items %}
// TODO #{{ loop.index }}: {{ item }}
{% endfor %}
func main() { func main() {
// TODO: Implement main functionality
// {% for item in todo_items %}
// TODO #{{ loop.index }}: {{ item }}
// {% endfor %}
fmt.Println("{{ project_name }} is running") fmt.Println("{{ project_name }} is running")
fmt.Printf("Repository: {{ repository }}\n") fmt.Printf("Repository: {{ repository }}\n")
fmt.Printf("Issue: {{ issue_number }}\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 }} // Generated from GitHub Issue #{{ issue_number }}
package main package main
// TODO: Implement helper function // TODO: Implement utility functions
func HelperFunction() {
// TODO: Implement
}

View File

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

View File

@@ -1,12 +1,15 @@
{ {
"env": { "env": {
"browser": true, "browser": true,
"commonjs": true, "es2022": true,
"es2021": true "node": true
}, },
"extends": "eslint:recommended", "extends": "eslint:recommended",
"parserOptions": { "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 }} * 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 }} // TODO #{{ loop.index }}: {{ item }}
{% endfor %} // {% endfor %}
console.log("{{ project_name }} is running"); 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 }} * Generated from GitHub Issue #{{ issue_number }}
*/ */
/** // TODO: Implement utility functions
* TODO: Implement helper function
*/
export function helperFunction() {
// TODO: Implement
}

View File

@@ -7,6 +7,8 @@
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
}, },
"keywords": [], "keywords": [],
"author": "", "author": "{{ author }}",
"license": "MIT" "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 }} Generated from GitHub Issue #{{ issue_number }}
""" """
@@ -7,7 +7,10 @@ import click
@click.command() @click.command()
@click.option("--option", help="TODO: Describe option") def cli():
def cli(option: str):
"""TODO: Implement CLI command.""" """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 }} Generated from GitHub Issue #{{ issue_number }}
""" """
from dataclasses import dataclass
class DataModel:
"""TODO: Implement data model class."""
pass
class AnotherModel: @dataclass
"""TODO: Implement another model class.""" class Item:
pass """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 pass
def another_helper(): # TODO: Add more utility functions based on requirements
"""TODO: Implement another helper."""
pass

View File

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

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 }} // Generated from GitHub Issue #{{ issue_number }}
{% for item in todo_items %} // {% for item in todo_items %}
// TODO #{{ loop.index }}: {{ item }} // TODO #{{ loop.index }}: {{ item }}
{% endfor %} // {% endfor %}
pub fn helper_function() {
// TODO: Implement
}

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 }} // 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 }} // TODO #{{ loop.index }}: {{ item }}
{% endfor %} // {% endfor %}
fn main() { fn main() {
println!("{{ project_name }} is running"); 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( setup(
name="scaffoldforge", name="scaffoldforge",
version="0.1.0", version="0.1.0",
packages=find_packages(), py_modules=["scaffoldforge"],
packages=find_packages(exclude=["tests*"]),
python_requires=">=3.9",
install_requires=[ install_requires=[
"click>=8.1.7", "click>=8.1.7",
"pygithub>=2.3.0", "pygithub>=2.3.0",
@@ -23,5 +28,4 @@ setup(
"scaffoldforge=scaffoldforge.main:main", "scaffoldforge=scaffoldforge.main:main",
], ],
}, },
python_requires=">=3.9",
) )

View File

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

View File

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

View File

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

View File

@@ -1,11 +1,8 @@
"""Unit tests for the templates module.""" """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.templates import TemplateEngine
from scaffoldforge.parsers import IssueData, ChecklistItem
class TestTemplateEngine: class TestTemplateEngine: