diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index ffd6d0c..e150f07 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -7,12 +7,16 @@ on:
jobs:
test:
runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ python-version: ['3.10', '3.11', '3.12']
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
- python-version: '3.11'
+ python-version: ${{ matrix.python-version }}
- run: python -m pip install --upgrade pip
- run: python -m pip install -e '.[dev]'
- run: ruff check .
- - run: pytest
+ - run: pytest -q
diff --git a/.gitignore b/.gitignore
index 6b5b6d1..c2571d0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,3 +12,6 @@ backup-*.tar.gz
# Local configuration
.env
+
+# Generated release screenshots (avoid binary files in Codex diffs)
+docs/screenshots/*-release.png
diff --git a/README.md b/README.md
index ff843c9..d30efb1 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,16 @@
# Universal Project Compiler Agent
+
+
+
+
+
+
+
+ Android-first compiler agent for turning requirements into secure, runnable project scaffolds.
+ Preview the landing page · Deploy on Vercel
+
+
Android-first, Termux-first development agent that transforms documents, specifications, repositories, OCR text, Markdown, or natural language requests into complete, runnable, maintainable software project scaffolds.
The original product specification is preserved in [docs/SPECIFICATION.md](docs/SPECIFICATION.md).
@@ -58,14 +69,33 @@ tests/ Unit tests
.github/workflows/ CI checks
```
+
+## Vercel landing page
+
+A polished static introduction page is included in `public/` with animated SVG badges and Vercel routing/security headers in `vercel.json`.
+
+```bash
+npm i -g vercel
+vercel deploy --prod
+```
+
+If deploying from CI, provide your Vercel token as an environment secret and run `vercel deploy --prod --token "$VERCEL_TOKEN"`.
+
## Development
```bash
python -m pip install -e '.[dev]'
-ruff check .
-pytest
+./scripts/check.sh
```
+`./scripts/check.sh` runs Ruff and the full pytest suite so release checks match CI. The Vercel landing page lives in `public/` and is served as a static site.
+
+## Current release
+
+- Version: 0.1.1
+- Release date: 2026-06-02
+- Release focus: CLI dry-run previews, safe output directory validation, dashboard route alignment, and test workflow hardening.
+
## Security model
- Never hardcode secrets in generated output.
diff --git a/app/universal_compiler_agent/__init__.py b/app/universal_compiler_agent/__init__.py
index 04fee82..adaab43 100644
--- a/app/universal_compiler_agent/__init__.py
+++ b/app/universal_compiler_agent/__init__.py
@@ -1,4 +1,4 @@
"""Universal Project Compiler Agent package."""
__all__ = ["__version__"]
-__version__ = "0.1.0"
+__version__ = "0.1.1"
diff --git a/app/universal_compiler_agent/cli.py b/app/universal_compiler_agent/cli.py
index 41d8ca0..d2457ad 100644
--- a/app/universal_compiler_agent/cli.py
+++ b/app/universal_compiler_agent/cli.py
@@ -19,6 +19,14 @@ def _read_input(args: argparse.Namespace) -> str:
return "Universal Project Compiler Agent"
+def _safe_output_dir(value: str) -> Path:
+ output_dir = Path(value)
+ if output_dir.is_absolute() or ".." in output_dir.parts:
+ msg = "output_dir must be a safe relative path"
+ raise argparse.ArgumentTypeError(msg)
+ return output_dir
+
+
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Compile requirements into a runnable project scaffold."
@@ -37,7 +45,15 @@ def build_parser() -> argparse.ArgumentParser:
compile_cmd.add_argument("--text", help="Inline requirements text.")
compile_cmd.add_argument("--name", help="Override generated project name.")
compile_cmd.add_argument(
- "--output-dir", default="generated", help="Directory that will receive output."
+ "--dry-run",
+ action="store_true",
+ help="Print the generated plan as JSON without writing files.",
+ )
+ compile_cmd.add_argument(
+ "--output-dir",
+ default=Path("generated"),
+ type=_safe_output_dir,
+ help="Safe relative directory that will receive output.",
)
return parser
@@ -47,12 +63,12 @@ def main(argv: list[str] | None = None) -> int:
args = parser.parse_args(argv)
requirements = _read_input(args)
- if args.command == "plan":
+ if args.command == "plan" or args.dry_run:
plan = build_plan(requirements, args.name)
print(json.dumps(asdict(plan), indent=2))
return 0
- result = compile_project(requirements, Path(args.output_dir), args.name)
+ result = compile_project(requirements, args.output_dir, args.name)
print(f"Generated {result.file_count} files in {result.root}")
return 0
diff --git a/app/universal_compiler_agent/server.py b/app/universal_compiler_agent/server.py
index aff944b..1bf05b7 100644
--- a/app/universal_compiler_agent/server.py
+++ b/app/universal_compiler_agent/server.py
@@ -11,6 +11,9 @@
from .compiler import compile_project
from .planner import build_plan
+from .templates import INDEX_HTML
+
+APP_VERSION = "0.1.1"
class PlanRequest(BaseModel):
@@ -22,8 +25,15 @@ class CompileRequest(PlanRequest):
output_dir: str = Field(default="generated", max_length=240)
+def _safe_output_dir(value: str) -> Path:
+ output_dir = Path(value)
+ if output_dir.is_absolute() or ".." in output_dir.parts:
+ raise HTTPException(status_code=400, detail="output_dir must be a safe relative path")
+ return output_dir
+
+
def create_app() -> FastAPI:
- app = FastAPI(title="Universal Project Compiler Agent", version="0.1.0")
+ app = FastAPI(title="Universal Project Compiler Agent", version=APP_VERSION)
@app.middleware("http")
async def security_headers(request: Request, call_next): # type: ignore[no-untyped-def]
@@ -36,38 +46,7 @@ async def security_headers(request: Request, call_next): # type: ignore[no-unty
@app.get("/", response_class=HTMLResponse)
def index() -> str:
- return """
-
-
-
-
-
- Universal Project Compiler Agent
-
-
-
-
- Android-first • Termux-first • Production-ready
- Compile requirements into runnable software projects.
- Use POST /plan to analyze requirements or POST /compile to emit a secure, maintainable scaffold with docs, tests, and scripts.
-
- CLI
upca compile --input-file spec.md
- API
JSON endpoints for automation and future UI integrations.
- Security
Path safety, secret redaction, and hardened HTTP headers.
-
-"""
+ return INDEX_HTML
@app.get("/health")
def health() -> dict[str, str]:
@@ -80,10 +59,11 @@ def plan(request: PlanRequest) -> JSONResponse:
@app.post("/compile")
def compile_endpoint(request: CompileRequest) -> dict[str, object]:
- output_dir = Path(request.output_dir)
- if output_dir.is_absolute() or ".." in output_dir.parts:
- raise HTTPException(status_code=400, detail="output_dir must be a safe relative path")
- result = compile_project(request.requirements, output_dir, request.project_name)
+ result = compile_project(
+ request.requirements,
+ _safe_output_dir(request.output_dir),
+ request.project_name,
+ )
return {"root": str(result.root), "file_count": result.file_count, "slug": result.plan.slug}
return app
diff --git a/app/universal_compiler_agent/templates.py b/app/universal_compiler_agent/templates.py
index 380cb8e..806a73e 100644
--- a/app/universal_compiler_agent/templates.py
+++ b/app/universal_compiler_agent/templates.py
@@ -10,7 +10,6 @@
Universal Project Compiler Agent
-