Compare commits
78 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9d4b49b14d | |||
| c64e81e56f | |||
| 28fae25f83 | |||
| f180b8a082 | |||
| b8c47f6b46 | |||
| 1639e9e5b4 | |||
| b2bc2b9ea5 | |||
| f19a2c0869 | |||
| ca6b0c66f1 | |||
| 9f5a693d5d | |||
| 8c63473d84 | |||
| 73ed60e87c | |||
| 2b5f70ee09 | |||
| bcfce038ec | |||
| 50553bb154 | |||
| 841405b437 | |||
| 145a9c6907 | |||
| f3d29b74d5 | |||
| ef04db2ebd | |||
| 56f544b093 | |||
| 96db2f5236 | |||
| 06910dbb6e | |||
| d30e40e3fd | |||
| 7c42ab1bf0 | |||
| 718290943b | |||
| de65989205 | |||
| 7421d7da0f | |||
| 3b688684c2 | |||
| 5ae900bfd7 | |||
| af11223841 | |||
| 2dbce2439b | |||
| b00c6b5cce | |||
| ca375ac3e1 | |||
| 83d4d92c21 | |||
| 4de73353a4 | |||
| 22bd48f04f | |||
| e08a0c7f91 | |||
| f14a4a8c78 | |||
| 975627a712 | |||
| 880df5e86d | |||
| 98c3b25710 | |||
| 36b5baceb3 | |||
| f94483bac0 | |||
| c720d2852d | |||
| de9786f998 | |||
| 607b54df19 | |||
| 4575516c3e | |||
| 68e2eaeec5 | |||
| e6ab90f390 | |||
| 856e22b34b | |||
| d9530a3453 | |||
| 2b16512be2 | |||
| 2cc6948cbd | |||
| 206d477156 | |||
| 6ddd35a876 | |||
| 80609d14ba | |||
| 041f8331f2 | |||
| 0fbdfc80ee | |||
| fbaa1a90f6 | |||
| 49be08e504 | |||
| d3e9467389 | |||
| 76ae2f7fe6 | |||
| f32bdc2f3e | |||
| cedbcd63b3 | |||
| 312efdca75 | |||
| ce6e879fa9 | |||
| ef7b7f7673 | |||
| 66e5ebd1a8 | |||
| 8d48b3da87 | |||
| 8fffa89be8 | |||
| d4edd2f77a | |||
| 689b4814fa | |||
| c68659d903 | |||
| c984c31be0 | |||
| b5eb3c8991 | |||
| e84520d96d | |||
| 582787f8af | |||
| c4b5cb82c3 |
14
.env.example
14
.env.example
@@ -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
|
|
||||||
@@ -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
185
.gitignore
vendored
@@ -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/
|
|
||||||
2
LICENSE
2
LICENSE
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
SOFTWARE.
|
SOFTWARE.
|
||||||
141
README.md
141
README.md
@@ -1,21 +1,7 @@
|
|||||||
# ScaffoldForge
|
# ScaffoldForge
|
||||||
|
|
||||||
[](https://opensource.org/licenses/MIT)
|
|
||||||
[](https://www.python.org/)
|
|
||||||
[](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
|
||||||
|
|
||||||
@@ -148,4 +187,4 @@ cp .env.example .env
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT License - see LICENSE file for details.
|
MIT License - see LICENSE file for details.
|
||||||
@@ -52,4 +52,4 @@ target-version = "py39"
|
|||||||
|
|
||||||
[tool.ruff.lint]
|
[tool.ruff.lint]
|
||||||
select = ["E", "F", "W", "I", "N", "UP"]
|
select = ["E", "F", "W", "I", "N", "UP"]
|
||||||
ignore = []
|
ignore = []
|
||||||
@@ -5,4 +5,4 @@ project scaffolds with starter code, file structures, and TODO comments.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
__version__ = "0.1.0"
|
__version__ = "0.1.0"
|
||||||
__author__ = "ScaffoldForge Contributors"
|
__author__ = "ScaffoldForge Contributors"
|
||||||
@@ -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")
|
||||||
@@ -27,4 +33,4 @@ def cli(ctx: click.Context, verbose: bool):
|
|||||||
|
|
||||||
cli.add_command(generate, "generate")
|
cli.add_command(generate, "generate")
|
||||||
cli.add_command(preview, "preview")
|
cli.add_command(preview, "preview")
|
||||||
cli.add_command(list_templates, "list-templates")
|
cli.add_command(list_templates, "list-templates")
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -206,4 +203,4 @@ def list_templates(ctx: click.Context, language: Optional[str]):
|
|||||||
templates = engine.list_available_templates(lang)
|
templates = engine.list_available_templates(lang)
|
||||||
click.echo(f"{lang}:")
|
click.echo(f"{lang}:")
|
||||||
for t in templates:
|
for t in templates:
|
||||||
click.echo(f" - {t}")
|
click.echo(f" - {t}")
|
||||||
@@ -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."""
|
||||||
@@ -126,4 +128,4 @@ def load_config(config_path: str) -> None:
|
|||||||
|
|
||||||
def get_config() -> Config:
|
def get_config() -> Config:
|
||||||
"""Get the global configuration instance."""
|
"""Get the global configuration instance."""
|
||||||
return _config
|
return _config
|
||||||
@@ -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"]
|
||||||
@@ -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.
|
||||||
|
|
||||||
@@ -219,4 +201,4 @@ class CodeGenerator:
|
|||||||
return self.template_engine.render(filename, context, language)
|
return self.template_engine.render(filename, context, language)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
empty_file = self._create_empty_file(filename, language, {})
|
empty_file = self._create_empty_file(filename, language, {})
|
||||||
return empty_file.content
|
return empty_file.content
|
||||||
@@ -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
|
||||||
@@ -11,4 +10,4 @@ class FileSpec:
|
|||||||
path: str
|
path: str
|
||||||
content: str
|
content: str
|
||||||
encoding: str = "utf-8"
|
encoding: str = "utf-8"
|
||||||
executable: bool = False
|
executable: bool = False
|
||||||
@@ -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()
|
||||||
@@ -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
|
||||||
@@ -20,4 +18,4 @@ def main():
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
@@ -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"]
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -337,4 +334,4 @@ class IssueParser:
|
|||||||
if any(kw in body_lower for kw in ["library", "package", "module"]):
|
if any(kw in body_lower for kw in ["library", "package", "module"]):
|
||||||
return "library"
|
return "library"
|
||||||
|
|
||||||
return "application"
|
return "application"
|
||||||
@@ -2,4 +2,4 @@
|
|||||||
|
|
||||||
from scaffoldforge.templates.engine import TemplateEngine
|
from scaffoldforge.templates.engine import TemplateEngine
|
||||||
|
|
||||||
__all__ = ["TemplateEngine"]
|
__all__ = ["TemplateEngine"]
|
||||||
@@ -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)
|
||||||
1
scaffoldforge/templates/go/__init__.py
Normal file
1
scaffoldforge/templates/go/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Go template files."""
|
||||||
@@ -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")
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
|
|||||||
1
scaffoldforge/templates/javascript/__init__.py
Normal file
1
scaffoldforge/templates/javascript/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""JavaScript template files."""
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 }}");
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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": {}
|
||||||
}
|
}
|
||||||
|
|||||||
1
scaffoldforge/templates/python/__init__.py
Normal file
1
scaffoldforge/templates/python/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Python template files."""
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
1
scaffoldforge/templates/rust/__init__.py
Normal file
1
scaffoldforge/templates/rust/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Rust template files."""
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
12
setup.py
12
setup.py
@@ -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",
|
)
|
||||||
)
|
|
||||||
@@ -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
86
tests/test_extractors.py
Normal 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
105
tests/test_formatters.py
Normal 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
113
tests/test_integration.py
Normal 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
50
tests/test_parser.py
Normal 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"]
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user