Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.DS_Store
*.log
.env
**/venv/
**/__pycache__/
**/node_modules/
**/out/
*.pyc
.pytest_cache/
26 changes: 26 additions & 0 deletions PLAN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Creer — Final Plan (post v0.1)

v0.1 delivers the foundation: FastAPI planner/generator + VS Code command that writes a generated repo into the workspace (optional git init).

## v0.2 priorities

1. **Preview before writing** — show planned file tree / diffs and require confirm before disk writes.
2. **GitHub repo creation** — create remote repo and push the scaffolded project.
3. **Curated templates** — expand `backend/app/templates.py` into a real template system (stack presets + AI fill).
4. **Overwrite protection** — stronger conflict detection per-file (not only folder-level).
5. **Chat command `/creer`** — invoke scaffolding from chat / agent surface.

## Stretch (v0.3+)

- Production-grade validation layer (schema, path sandbox, content size limits)
- Streaming generation progress to the extension UI
- Local/offline model backends
- Open-source README / LICENSE / CI templates baked into every scaffold

## Non-goals (keep out of early versions)

- Multi-agent orchestration
- Memory graphs
- Overengineered plugin frameworks

Stay power-focused: idea → plan → files → workspace.
73 changes: 73 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Creer

AI-powered repo scaffolding inside your workspace.

Describe an idea → Creer plans a clean structure → writes files into your VS Code workspace → optionally initializes git.

## Architecture

```
creer/
├── backend/ # Python FastAPI AI engine
└── extension/ # VS Code extension
```

## Prerequisites

- Python 3.10+
- Node.js 18+
- OpenAI API key
- VS Code / Cursor

## Backend (v0.1)

```bash
cd backend
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install -r requirements.txt
cp .env.example .env
# set OPENAI_API_KEY in .env
uvicorn main:app --reload --port 8000
```

Health check:

```bash
curl http://localhost:8000/health
```

Generate:

```bash
curl -X POST http://localhost:8000/generate \
-H "Content-Type: application/json" \
-d '{"idea":"Build a FastAPI todo app"}'
```

## Extension (v0.1)

```bash
cd extension
npm install
npm run compile
```

In VS Code / Cursor:

1. Open the `extension/` folder
2. Press **F5** (Run Extension)
3. In the Extension Development Host, open a workspace folder
4. Command Palette → **Creer: Create New Repo**
5. Enter an idea

Settings:

| Setting | Default | Description |
|---|---|---|
| `creer.backendUrl` | `http://localhost:8000` | Backend base URL |
| `creer.initGit` | `true` | Run `git init` + initial commit after scaffolding |

## License

MIT
2 changes: 2 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
OPENAI_API_KEY=sk-your-key-here
CREER_MODEL=gpt-4o-mini
6 changes: 6 additions & 0 deletions backend/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.env
venv/
__pycache__/
*.pyc
.pytest_cache/
.mypy_cache/
1 change: 1 addition & 0 deletions backend/app/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Creer backend package."""
49 changes: 49 additions & 0 deletions backend/app/generator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from openai import OpenAI
from config import OPENAI_API_KEY, MODEL

_client: OpenAI | None = None


def _get_client() -> OpenAI:
global _client
if _client is None:
if not OPENAI_API_KEY:
raise ValueError("OPENAI_API_KEY is not set")
_client = OpenAI(api_key=OPENAI_API_KEY)
return _client


def generate_files(plan: dict) -> dict[str, str]:
"""Generate file contents one-by-one from a project plan."""
files_output: dict[str, str] = {}

for file_path in plan["files"]:
prompt = f"""
Generate the full content for file: {file_path}

Project Stack: {plan["stack"]}
Project Name: {plan["project_name"]}

Follow best practices.
Return ONLY the file content — no markdown fences, no explanation.
"""

response = _get_client().chat.completions.create(
model=MODEL,
messages=[{"role": "user", "content": prompt}],
temperature=0.3,
)

content = response.choices[0].message.content or ""
# Strip accidental markdown fences
if content.startswith("```"):
lines = content.splitlines()
if lines and lines[0].startswith("```"):
lines = lines[1:]
if lines and lines[-1].strip() == "```":
lines = lines[:-1]
content = "\n".join(lines)

files_output[file_path] = content

return files_output
58 changes: 58 additions & 0 deletions backend/app/planner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
from openai import OpenAI
import json
import re
from config import OPENAI_API_KEY, MODEL

_client: OpenAI | None = None


def _get_client() -> OpenAI:
global _client
if _client is None:
if not OPENAI_API_KEY:
raise ValueError("OPENAI_API_KEY is not set")
_client = OpenAI(api_key=OPENAI_API_KEY)
return _client


def _parse_json(content: str) -> dict:
"""Parse model JSON, tolerating optional markdown fences."""
text = content.strip()
fence = re.search(r"```(?:json)?\s*([\s\S]*?)```", text)
if fence:
text = fence.group(1).strip()
return json.loads(text)


def plan_project(idea: str) -> dict:
prompt = f"""
You are a senior software architect.

Convert the following idea into a clean project structure.

Return ONLY valid JSON (no markdown):
{{
"project_name": "...",
"stack": "...",
"files": ["path/file.py", ...]
}}

Rules:
- project_name must be a valid folder name (lowercase, hyphens ok)
- files should be a focused, production-ready starter set (typically 5–15 files)
- include README.md and a dependency manifest appropriate for the stack

Idea: {idea}
"""

response = _get_client().chat.completions.create(
model=MODEL,
messages=[{"role": "user", "content": prompt}],
temperature=0.2,
response_format={"type": "json_object"},
)

plan = _parse_json(response.choices[0].message.content)
if not isinstance(plan.get("files"), list) or not plan.get("project_name"):
raise ValueError("Planner returned an invalid plan structure")
return plan
17 changes: 17 additions & 0 deletions backend/app/templates.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"""Curated starter templates (stub for v0.2 template system)."""

TEMPLATES: dict[str, dict] = {
"fastapi-minimal": {
"stack": "FastAPI + Uvicorn",
"files": [
"main.py",
"requirements.txt",
"README.md",
".env.example",
],
},
}


def get_template(name: str) -> dict | None:
return TEMPLATES.get(name)
31 changes: 31 additions & 0 deletions backend/app/validator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""Lightweight plan/file validation for v0.1."""

import re

SAFE_NAME = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$")
SAFE_PATH = re.compile(r"^(?!/)(?!.*(?:^|/)\.\.(?:/|$))[a-zA-Z0-9._/-]+$")


def validate_plan(plan: dict) -> None:
name = plan.get("project_name")
files = plan.get("files")

if not isinstance(name, str) or not SAFE_NAME.match(name):
raise ValueError(f"Invalid project_name: {name!r}")

if not isinstance(files, list) or not files:
raise ValueError("Plan must include a non-empty files list")

for path in files:
if not isinstance(path, str) or not SAFE_PATH.match(path):
raise ValueError(f"Unsafe or invalid file path: {path!r}")


def validate_files(files: dict) -> None:
if not isinstance(files, dict) or not files:
raise ValueError("Generated files must be a non-empty mapping")
for path, content in files.items():
if not isinstance(path, str) or not SAFE_PATH.match(path):
raise ValueError(f"Unsafe or invalid file path: {path!r}")
if not isinstance(content, str):
raise ValueError(f"File content for {path!r} must be a string")
7 changes: 7 additions & 0 deletions backend/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import os
from dotenv import load_dotenv

load_dotenv()

OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
MODEL = os.getenv("CREER_MODEL", "gpt-4o-mini")
36 changes: 36 additions & 0 deletions backend/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field

from app.planner import plan_project
from app.generator import generate_files
from app.validator import validate_plan, validate_files

app = FastAPI(title="Creer", version="0.1.0")


class ProjectRequest(BaseModel):
idea: str = Field(..., min_length=3, max_length=4000)


@app.get("/health")
def health():
return {"status": "ok", "version": "0.1.0"}


@app.post("/generate")
def generate_project(request: ProjectRequest):
try:
plan = plan_project(request.idea)
validate_plan(plan)
files = generate_files(plan)
validate_files(files)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
except Exception as exc:
raise HTTPException(status_code=500, detail=f"Generation failed: {exc}") from exc

return {
"project_name": plan["project_name"],
"stack": plan.get("stack"),
"files": files,
}
5 changes: 5 additions & 0 deletions backend/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
fastapi>=0.115.0
uvicorn[standard]>=0.32.0
openai>=1.55.0
python-dotenv>=1.0.0
pydantic>=2.9.0
4 changes: 4 additions & 0 deletions extension/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules/
out/
*.vsix
.vscode-test/
13 changes: 13 additions & 0 deletions extension/.vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Run Extension",
"type": "extensionHost",
"request": "launch",
"args": ["--extensionDevelopmentPath=${workspaceFolder}"],
"outFiles": ["${workspaceFolder}/out/**/*.js"],
"preLaunchTask": "npm: compile"
}
]
}
11 changes: 11 additions & 0 deletions extension/.vscode/tasks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "compile",
"group": "build",
"problemMatcher": ["$tsc"]
}
]
}
Loading