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/
24 changes: 24 additions & 0 deletions PLAN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Creer — Final Plan

## Done

- **v0.1** — FastAPI planner/generator + VS Code write-to-workspace (+ optional git)
- **v0.2** — Preview, GitHub create/push, templates, overwrite protection, `/creer` chat
- **v0.3** — Streaming, offline/local models, bake-ins, SecretStorage + GIT_ASKPASS
- **v0.4** — Stream cancel, selectable license/CI bake-ins, quality gates
- **v0.5** — Content diff preview before write, multi-root workspace targeting, installable JSON/YAML template packs
- **v0.6** — Pack marketplace + remote URL install/delete, side-by-side conflict diffs, publish packaging
- **v0.7** — Self-hosted pack registry (`/registry` + download), extension icon, Browse Pack Registry, release changelog

## Optional next

- Signed publisher release with real Marketplace/Open VSX tokens (human step)
- Federated multi-host registry discovery

## Non-goals

- 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.

**Current version: 0.7.0**

## Architecture

```
creer/
├── backend/ # Python FastAPI AI engine (+ packs/ + registry)
└── extension/ # VS Code extension (icon in media/)
```

## Quick start

```bash
# Backend
cd backend && python -m venv venv && source venv/bin/activate
pip install -r requirements.txt && cp .env.example .env
uvicorn main:app --reload --port 8000

# Extension
cd extension && npm install && npm run compile
# F5 → Creer: Create New Repo
```

## Registry (v0.7)

Self-hosted pack catalog:

| Method | Path | Description |
|---|---|---|
| `GET` | `/registry?q=&source=` | Searchable pack list |
| `GET` | `/registry/packs/{id}` | Pack metadata |
| `GET` | `/registry/packs/{id}/download` | Portable JSON pack (installable URL) |
| `GET` | `/marketplace` | Curated featured view |

Install from another Creer host:

```bash
curl -X POST http://localhost:8000/packs/install \
-H 'Content-Type: application/json' \
-d '{"url":"http://other-host:8000/registry/packs/fastapi-crud/download"}'
```

Set `CREER_PUBLIC_BASE_URL` for absolute download links in registry responses.

## Extension commands

| Command | Title |
|---|---|
| `creer.createRepo` | Create New Repo |
| `creer.createRepoFromChat` | Create from Chat Prompt |
| `creer.browseMarketplace` | Browse Pack Marketplace |
| `creer.browseRegistry` | Browse Pack Registry |
| `creer.installPackFromUrl` | Install Pack from URL |
| `creer.setGitHubToken` / `clearGitHubToken` | SecretStorage token |

## Publishing

See [`extension/PUBLISH.md`](extension/PUBLISH.md). Package:

```bash
cd extension && npm run compile && npm run package
# → creer-0.7.0.vsix (includes media/icon.png)
```

Signed Marketplace / Open VSX publish requires your own `VSCE_PAT` / `OVSX_PAT` (never commit tokens).

## License

MIT
23 changes: 23 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# OpenAI API key (required unless CREER_OFFLINE=1 or using a local server that ignores it)
OPENAI_API_KEY=sk-your-key-here

# Optional OpenAI-compatible base URL for local/offline backends (Ollama, LM Studio, vLLM, etc.)
# Example: http://127.0.0.1:11434/v1
OPENAI_BASE_URL=

# Model name (OpenAI or local-compatible)
CREER_MODEL=gpt-4o-mini

# Template-only / stub generation — never calls the LLM
# Set to 1, true, or yes to enable
CREER_OFFLINE=

# Optional extra packs directory (JSON/YAML). Merged with backend/packs;
# user packs override built-in packs on the same id.
# When set, remote installs (POST /packs/install) write here.
# When unset, installs go to backend/packs/installed/.
# CREER_PACKS_DIR=/path/to/my-packs

# Optional public base URL for absolute registry download links
# Example: http://localhost:8000 or https://creer.example.com
# CREER_PUBLIC_BASE_URL=http://localhost:8000
8 changes: 8 additions & 0 deletions backend/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.env
venv/
__pycache__/
*.pyc
.pytest_cache/
.mypy_cache/
packs/installed/*
!packs/installed/.gitkeep
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."""
245 changes: 245 additions & 0 deletions backend/app/bakeins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
"""Open-source bake-ins merged into scaffolds (LICENSE, README, CI)."""

from __future__ import annotations

from typing import Any, Literal

LicenseId = Literal["mit", "apache-2.0", "none"]
CiId = Literal["auto", "python", "node", "none"]


def list_bakein_options() -> dict[str, list[dict[str, str]]]:
return {
"licenses": [
{"id": "mit", "name": "MIT"},
{"id": "apache-2.0", "name": "Apache 2.0"},
{"id": "none", "name": "No license"},
],
"ci": [
{"id": "auto", "name": "Auto-detect"},
{"id": "python", "name": "Python"},
{"id": "node", "name": "Node"},
{"id": "none", "name": "No CI"},
],
}


def _normalize_options(options: dict[str, Any] | None) -> dict[str, Any]:
opts = options or {}
license_id = opts.get("license") or "mit"
ci_id = opts.get("ci") or "auto"
include_readme = opts.get("include_readme")
if include_readme is None:
include_readme = True
if license_id not in ("mit", "apache-2.0", "none"):
license_id = "mit"
if ci_id not in ("auto", "python", "node", "none"):
ci_id = "auto"
return {
"license": license_id,
"ci": ci_id,
"include_readme": bool(include_readme),
}


def _mit_license(project_name: str) -> str:
holder = project_name.strip() if project_name and project_name.strip() else "Creer Scaffold"
return f"""MIT License

Copyright (c) 2026 {holder}

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
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
SOFTWARE.
"""


def _apache_license(project_name: str) -> str:
holder = project_name.strip() if project_name and project_name.strip() else "Creer Scaffold"
return f"""Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/

Copyright 2026 {holder}

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""


def _default_readme(plan: dict) -> str:
name = plan.get("project_name") or "project"
stack = plan.get("stack") or ""
desc = plan.get("description") or f"Scaffolded by Creer for {name}."
lines = [
f"# {name}",
"",
desc,
"",
]
if stack:
lines.extend([f"**Stack:** {stack}", ""])
lines.extend(
[
"## Getting started",
"",
"This project was generated with Creer.",
"Install dependencies and follow stack-specific docs in this repo.",
"",
]
)
return "\n".join(lines)


def _ci_python() -> str:
return """name: CI

on:
push:
branches: [main, master]
pull_request:

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
if [ -f pyproject.toml ]; then pip install -e ".[dev]" || pip install -e .; fi
pip install pytest
- name: Run tests
run: pytest -q || echo "No tests yet — scaffold CI ok"
"""


def _ci_node() -> str:
return """name: CI

on:
push:
branches: [main, master]
pull_request:

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: npm
- name: Install
run: npm ci || npm install
- name: Test
run: npm test --if-present
"""


def _ci_generic() -> str:
return """name: CI

on:
push:
branches: [main, master]
pull_request:

jobs:
scaffold:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Scaffold CI
run: echo "Creer scaffold CI — add stack-specific checks as the project grows"
"""


def _ci_workflow(plan: dict, ci_id: str) -> str | None:
if ci_id == "none":
return None
if ci_id == "python":
return _ci_python()
if ci_id == "node":
return _ci_node()

# auto
stack = (plan.get("stack") or "").lower()
paths = " ".join(plan.get("files") or []).lower()
blob = f"{stack} {paths}"

is_python = any(
tok in blob
for tok in ("python", "fastapi", "uvicorn", "pytest", "requirements.txt", "pyproject.toml")
)
is_node = any(
tok in blob for tok in ("node", "express", "next", "npm", "package.json", "react")
)

if is_python:
return _ci_python()
if is_node:
return _ci_node()
return _ci_generic()


def apply_bakeins(
plan: dict,
files: dict[str, str],
options: dict[str, Any] | None = None,
) -> dict[str, str]:
"""
Merge open-source bake-ins into generated files.

- LICENSE per options.license (mit | apache-2.0 | none); never overwrite existing
- README.md created only if missing and include_readme
- .github/workflows/ci.yml per options.ci when missing
"""
opts = _normalize_options(options)
out = dict(files)
project_name = plan.get("project_name") or "Creer Scaffold"

if opts["license"] != "none" and "LICENSE" not in out:
if opts["license"] == "apache-2.0":
out["LICENSE"] = _apache_license(project_name)
else:
out["LICENSE"] = _mit_license(project_name)

if opts["include_readme"] and "README.md" not in out:
out["README.md"] = _default_readme(plan)

ci_path = ".github/workflows/ci.yml"
if ci_path not in out:
ci_body = _ci_workflow(plan, opts["ci"])
if ci_body is not None:
out[ci_path] = ci_body

return out
Loading