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/.python-version b/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/.vercelignore b/.vercelignore new file mode 100644 index 0000000..e79dcea --- /dev/null +++ b/.vercelignore @@ -0,0 +1,10 @@ +.git +.github +.pytest_cache +.ruff_cache +.venv +tests +docs +generated +backup-*.tar.gz +docs/screenshots/*-release.png diff --git a/README.md b/README.md index ff843c9..8e4ab3c 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,17 @@ # Universal Project Compiler Agent +

+ CI + Python + FastAPI + Vercel + Version + License +

+ 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). +The original product specification is preserved in [docs/SPECIFICATION.md](docs/SPECIFICATION.md). The project now ships with a polished dashboard, live API docs, GitHub badges, CI checks, and Vercel deployment wiring. ## What is included @@ -12,7 +21,7 @@ The original product specification is preserved in [docs/SPECIFICATION.md](docs/ - A safe compiler that generates runnable Python project scaffolds with docs, tests, and scripts. - Secret redaction, safe slug generation, path traversal protection, and HTTP security headers. - Termux-friendly setup, start, update, and backup scripts. -- CI, tests, and architecture documentation. +- CI, tests, architecture documentation, Vercel configuration, and GitHub project badges. ## Quick start @@ -38,6 +47,18 @@ pkg install python git The default architecture avoids Docker, Kubernetes, and heavy services so it can run on low-memory Android devices. +## Deploy to Vercel + +This repo includes a Vercel ASGI entrypoint and `vercel.json`, so the FastAPI dashboard and API can run as a Python Function. + +```bash +npm i -g vercel +vercel login +vercel deploy --prod +``` + +Vercel routes every request to `api/index.py`, while generated compile output is redirected to `/tmp/upca` through `UPCA_OUTPUT_BASE` for serverless-safe writes. + ## API examples ```bash @@ -62,10 +83,17 @@ tests/ Unit tests ```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. + +## Current release + +- Version: 0.1.2 +- Release date: 2026-06-03 +- Release focus: redesigned project landing page, GitHub badges, Vercel deployment wiring, and serverless-safe compile output paths. + ## Security model - Never hardcode secrets in generated output. diff --git a/api/__init__.py b/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/index.py b/api/index.py new file mode 100644 index 0000000..9125fa9 --- /dev/null +++ b/api/index.py @@ -0,0 +1,12 @@ +"""Vercel ASGI entrypoint for Universal Project Compiler Agent.""" + +from __future__ import annotations + +import sys +from pathlib import Path + +APP_DIR = Path(__file__).resolve().parents[1] / "app" +if str(APP_DIR) not in sys.path: + sys.path.insert(0, str(APP_DIR)) + +from universal_compiler_agent.server import app # noqa: E402,F401 diff --git a/app/universal_compiler_agent/__init__.py b/app/universal_compiler_agent/__init__.py index 04fee82..a518c3a 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.2" 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..6566d73 100644 --- a/app/universal_compiler_agent/server.py +++ b/app/universal_compiler_agent/server.py @@ -2,6 +2,7 @@ from __future__ import annotations +import os from dataclasses import asdict from pathlib import Path @@ -11,6 +12,9 @@ from .compiler import compile_project from .planner import build_plan +from .templates import INDEX_HTML + +APP_VERSION = "0.1.2" class PlanRequest(BaseModel): @@ -22,8 +26,19 @@ 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 _output_base() -> Path: + return Path(os.environ.get("UPCA_OUTPUT_BASE", ".")) + + 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 +51,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 +64,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, + _output_base() / _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..e00db44 100644 --- a/app/universal_compiler_agent/templates.py +++ b/app/universal_compiler_agent/templates.py @@ -10,66 +10,110 @@ Universal Project Compiler Agent - @@ -77,19 +121,34 @@
-

Turn rough specs into runnable software projects.

-

Analyze requirements, receive a prioritized implementation plan, then generate a clean scaffold with docs, tests, scripts, security defaults, and mobile-friendly workflows.

+

Android-first • Termux-first • Serverless-ready

+

Compile rough specs into ship-ready projects.

+

Analyze requirements, produce a prioritized implementation plan, and generate a clean scaffold with docs, tests, scripts, security defaults, and mobile-friendly workflows.

+ +
+
3interfaces: CLI, API, dashboard
+
4priority levels for task planning
+
0Docker required for Termux use
+
PlanCritical/High/Medium/Low tasks with impact and file scope.
CompileSafe project generation with secret redaction and path checks.
-
ShipTermux-friendly setup, start, update, backup, tests, and CI.
+
ShipTermux-friendly setup, start, update, backup, tests, CI, and Vercel deployment.
+
@@ -107,6 +166,21 @@
+
+

Built for fast release paths.

+

The project now includes the pieces a public project page needs: live API docs, a hands-on compiler form, GitHub badges, CI checks, and Vercel serverless deployment wiring.

+
+
GitHub-ready

README badges advertise CI, Python, FastAPI, Vercel readiness, license, and release version.

+
Vercel-ready

A lightweight ASGI entrypoint and runtime config route every request to the FastAPI app.

+
Termux-ready

Local scripts stay minimal for Android devices with limited memory and storage.

+
+
+
+ $ python -m pip install -e '.[dev]' +$ ./scripts/check.sh +$ vercel deploy --prod +
+
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index f047b8f..4886f16 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## 0.1.2 - 2026-06-03 + +- Redesigned the dashboard into a polished project landing page with animated visual elements, release highlights, and deploy guidance. +- Added GitHub README badges for CI, Python versions, FastAPI, Vercel readiness, release, and license. +- Added Vercel deployment wiring with an ASGI entrypoint, `vercel.json`, runtime requirements, and serverless-safe compile output routing through `/tmp/upca`. +- Kept generated release screenshots out of review diffs to avoid unsupported binary-file rendering. + +## 0.1.1 - 2026-06-02 + +- Added CLI `compile --dry-run` support for release-safe plan previews. +- Rejected unsafe CLI and API output directories before project generation. +- Served the maintained dashboard template from the FastAPI root route and aligned its buttons with `/plan` and `/compile`. +- Updated release metadata and development dependencies for the current test client stack. + ## 0.1.0 - Implemented the initial Universal Project Compiler Agent CLI and FastAPI service. diff --git a/docs/SPECIFICATION.md b/docs/SPECIFICATION.md index 1cef87b..a1fd42e 100644 --- a/docs/SPECIFICATION.md +++ b/docs/SPECIFICATION.md @@ -1,6 +1,4 @@ -AGENTS.md - -Universal Project Compiler Agent +# Universal Project Compiler Agent Android-First • Termux-First • Codex CLI • Claude Code diff --git a/pyproject.toml b/pyproject.toml index 0221590..311d009 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "universal-project-compiler-agent" -version = "0.1.0" +version = "0.1.2" description = "Termux-first compiler agent that turns requirements into runnable project scaffolds." readme = "README.md" requires-python = ">=3.10" @@ -20,7 +20,8 @@ dependencies = [ dev = [ "pytest>=8.0,<9.0", "ruff>=0.6,<1.0", - "httpx>=0.27,<1.0" + "httpx>=0.27,<1.0", + "httpx2>=2.3,<3.0" ] [project.scripts] @@ -45,3 +46,4 @@ select = ["E", "F", "I", "B", "UP", "SIM"] [tool.ruff.lint.per-file-ignores] "app/universal_compiler_agent/server.py" = ["E501"] +"app/universal_compiler_agent/templates.py" = ["E501"] diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..96fbc8d --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +fastapi>=0.115,<1.0 +uvicorn[standard]>=0.30,<1.0 +pydantic>=2.8,<3.0 diff --git a/scripts/check.sh b/scripts/check.sh new file mode 100755 index 0000000..7f5a70c --- /dev/null +++ b/scripts/check.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env sh +set -eu +ruff check . +pytest -q diff --git a/tests/test_server.py b/tests/test_server.py index 8e7f7e2..7006dec 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -1,7 +1,32 @@ +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + from fastapi.testclient import TestClient from universal_compiler_agent.server import app +def test_dashboard_uses_public_api_routes() -> None: + response = TestClient(app).get("/") + + assert response.status_code == 200 + assert "submit('/plan'" in response.text + assert "submit('/compile'" in response.text + assert "/api/plan" not in response.text + assert "/api/compile" not in response.text + + +def test_dashboard_includes_release_landing_content() -> None: + response = TestClient(app).get("/") + + assert response.status_code == 200 + assert "Vercel-ready" in response.text + assert "Built for fast release paths" in response.text + assert "vercel deploy --prod" in response.text + + def test_health_endpoint() -> None: response = TestClient(app).get("/health") @@ -17,3 +42,60 @@ def test_plan_endpoint() -> None: assert response.status_code == 200 assert response.json()["slug"] == "portal" + + +def test_compile_endpoint_rejects_unsafe_output_dir() -> None: + response = TestClient(app).post( + "/compile", + json={"requirements": "# Portal", "output_dir": "../escape"}, + ) + + assert response.status_code == 400 + assert response.json()["detail"] == "output_dir must be a safe relative path" + + +def test_compile_endpoint_writes_scaffold(tmp_path: Path, monkeypatch) -> None: # type: ignore[no-untyped-def] + monkeypatch.chdir(tmp_path) + + response = TestClient(app).post( + "/compile", + json={"requirements": "# Portal", "output_dir": "generated"}, + ) + + assert response.status_code == 200 + body = response.json() + assert body["slug"] == "portal" + assert (tmp_path / body["root"] / "README.md").exists() + + +def test_compile_endpoint_honors_output_base(tmp_path: Path, monkeypatch) -> None: # type: ignore[no-untyped-def] + monkeypatch.setenv("UPCA_OUTPUT_BASE", str(tmp_path / "serverless")) + + response = TestClient(app).post( + "/compile", + json={"requirements": "# Portal", "output_dir": "generated"}, + ) + + assert response.status_code == 200 + body = response.json() + assert body["root"].startswith(str(tmp_path / "serverless")) + assert (Path(body["root"]) / "compile-manifest.json").exists() + + +def test_vercel_entrypoint_exports_fastapi_app() -> None: + spec = importlib.util.spec_from_file_location("vercel_entrypoint", "api/index.py") + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + + assert module.app is app + + +def test_vercel_config_routes_to_python_entrypoint() -> None: + config = json.loads(Path("vercel.json").read_text(encoding="utf-8")) + + assert config["rewrites"] == [{"source": "/(.*)", "destination": "/api/index.py"}] + assert config["env"]["UPCA_OUTPUT_BASE"] == "/tmp/upca" + assert "api/index.py" in config["functions"] + assert Path(".python-version").read_text(encoding="utf-8").strip() == "3.12" diff --git a/vercel.json b/vercel.json new file mode 100644 index 0000000..6b66d81 --- /dev/null +++ b/vercel.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://openapi.vercel.sh/vercel.json", + "functions": { + "api/index.py": { + "excludeFiles": "{tests/**,docs/**,scripts/**,generated/**,backup-*.tar.gz,.github/**,.pytest_cache/**,.ruff_cache/**}" + } + }, + "rewrites": [ + { + "source": "/(.*)", + "destination": "/api/index.py" + } + ], + "env": { + "UPCA_OUTPUT_BASE": "/tmp/upca" + } +}