diff --git a/dev-server-provision/coder/main.tf b/dev-server-provision/coder/main.tf index bd6b55c..3a53b22 100644 --- a/dev-server-provision/coder/main.tf +++ b/dev-server-provision/coder/main.tf @@ -19,6 +19,12 @@ variable "docker_socket" { type = string } +variable "starter_template" { + default = "none" + description = "Starter app template to bootstrap in new workspaces. Options: none, fullstack-baseline" + type = string +} + provider "docker" { host = var.docker_socket != "" ? var.docker_socket : null } @@ -55,6 +61,35 @@ resource "coder_agent" "main" { else echo "[remotevibe] WARNING: $AGENT_ENV not found — AI agents may not be authenticated" fi + + # ── Bootstrap starter app template ────────────────────────────────────── + STARTER_TEMPLATE="$${STARTER_TEMPLATE:-none}" + if [ "$STARTER_TEMPLATE" = "fullstack-baseline" ]; then + TMPL_DIR="/home/coder/fullstack-baseline" + if [ ! -f "$TMPL_DIR/.bootstrapped" ]; then + echo "[remotevibe] Bootstrapping fullstack-baseline (tiangolo/full-stack-fastapi-template)…" + git clone --depth=1 https://github.com/tiangolo/full-stack-fastapi-template "$TMPL_DIR" 2>&1 | sed 's/^/[remotevibe] /' + cd "$TMPL_DIR" + + # Generate .env from the example — patch project name and secret key + cp .env "$TMPL_DIR/.env.bak" 2>/dev/null || true + sed \ + -e "s|^PROJECT_NAME=.*|PROJECT_NAME=fullstack-baseline|" \ + -e "s|^SECRET_KEY=.*|SECRET_KEY=$(openssl rand -hex 32)|" \ + -e "s|^FIRST_SUPERUSER=.*|FIRST_SUPERUSER=admin@example.com|" \ + -e "s|^FIRST_SUPERUSER_PASSWORD=.*|FIRST_SUPERUSER_PASSWORD=changeme123|" \ + .env > .env.patched && mv .env.patched .env + + docker compose up -d 2>&1 | sed 's/^/[remotevibe] /' + touch "$TMPL_DIR/.bootstrapped" + echo "[remotevibe] fullstack-baseline stack started." + echo "[remotevibe] Frontend : http://localhost:5173" + echo "[remotevibe] API docs : http://localhost:8000/docs" + else + echo "[remotevibe] fullstack-baseline already bootstrapped — ensuring stack is up…" + docker compose -f "$TMPL_DIR/docker-compose.yml" up -d 2>&1 | sed 's/^/[remotevibe] /' || true + fi + fi # ── Configure Coder CLI for workspace template management ──────────────── if [ -f /run/secrets/coder-token ]; then _tok=$(cat /run/secrets/coder-token | tr -d '[:space:]') @@ -200,6 +235,7 @@ resource "docker_container" "workspace" { "CODER_URL=${data.coder_workspace.me.access_url}", "CODER_TEMPLATE_NAME=remotevibe", "CODER_TEMPLATE_DIR=/workspace/template", + "STARTER_TEMPLATE=${var.starter_template}", ] host { diff --git a/dev-server-provision/configurator/cli.py b/dev-server-provision/configurator/cli.py index d3e5a97..7e6325e 100644 --- a/dev-server-provision/configurator/cli.py +++ b/dev-server-provision/configurator/cli.py @@ -578,6 +578,52 @@ def _ask_agents(config: dict[str, Any]) -> None: _success("Reusing Google API key already provided.") +# --------------------------------------------------------------------------- +# Step 4b — Starter app template +# --------------------------------------------------------------------------- + +_STARTER_TEMPLATE_CHOICES = [ + { + "name": "none — plain workspace (no pre-installed app)", + "value": "none", + }, + { + "name": "fullstack-baseline — FastAPI + PostgreSQL + React/Vite (tiangolo/full-stack-fastapi-template)", + "value": "fullstack-baseline", + }, +] + + +def _ask_starter_template(config: dict[str, Any]) -> None: + _heading("Starter App Template (optional)") + print( + " Optionally pre-install a starter application into every new workspace.\n" + f"\n" + f" {_BOLD}fullstack-baseline{_RESET} bootstraps " + f"{_CYAN}tiangolo/full-stack-fastapi-template{_RESET}:\n" + f" • FastAPI backend (Python, SQLModel, Alembic migrations)\n" + f" • PostgreSQL database\n" + f" • React + Vite frontend\n" + f" • JWT auth + RBAC + CRUD scaffolding\n" + f" • Docker Compose stack (ready in ~2 min)\n" + f"\n" + f" The stack starts automatically on first workspace launch and is\n" + f" available at http://localhost:5173 (frontend) and :8000 (API).\n" + ) + + current = config.get("starter_template") or "none" + config["starter_template"] = inquirer.select( + message="Starter template:", + choices=_STARTER_TEMPLATE_CHOICES, + default=current, + ).execute() + + if config["starter_template"] == "fullstack-baseline": + _success("fullstack-baseline selected — stack will auto-start on first workspace launch.") + else: + _success("No starter template — plain workspace.") + + # --------------------------------------------------------------------------- # Step 4 — Coder admin password # --------------------------------------------------------------------------- @@ -782,6 +828,9 @@ def run() -> None: # 5. AI agents _ask_agents(config) + # 5b. Starter app template + _ask_starter_template(config) + # 6. Provider options _ask_provider_options(provider, deploy_config) diff --git a/dev-server-provision/configurator/generator.py b/dev-server-provision/configurator/generator.py index 7aa9cbd..e428896 100644 --- a/dev-server-provision/configurator/generator.py +++ b/dev-server-provision/configurator/generator.py @@ -84,6 +84,12 @@ # ── OpenCode Provider Selection ──────────────────────────────────── OPENCODE_PROVIDER={opencode_provider} + # ── Starter App Template ─────────────────────────────────────────── + # none — no template (plain workspace) + # fullstack-baseline — tiangolo/full-stack-fastapi-template + # (FastAPI + PostgreSQL + React/Vite + Auth + RBAC) + STARTER_TEMPLATE={starter_template} + - path: /etc/dev-server/bootstrap.sh permissions: "0700" owner: root:root @@ -192,6 +198,7 @@ def default_config() -> dict[str, Any]: "github_token": "", "codex_openai_auth_code": "", "opencode_provider": "", + "starter_template": "none", } @@ -219,6 +226,7 @@ def default_config() -> dict[str, Any]: "github_token", "codex_openai_auth_code", "opencode_provider", + "starter_template", ] diff --git a/dev-server-provision/configurator/tests/test_validators.py b/dev-server-provision/configurator/tests/test_validators.py index 3e8a132..4765d95 100644 --- a/dev-server-provision/configurator/tests/test_validators.py +++ b/dev-server-provision/configurator/tests/test_validators.py @@ -12,6 +12,7 @@ validate_domain, validate_email, validate_oauth_client_id, + validate_starter_template, validate_subdomain, ) @@ -155,6 +156,7 @@ def _full_config(self, **overrides): "email": "admin@example.com", "cloudflare_api_token": "a" * 40, "cloudflare_zone_id": "a" * 32, + "coder_admin_password": "TestPass123", "enable_agent_copilot": False, "enable_agent_claude": False, "enable_agent_gemini": False, @@ -344,6 +346,31 @@ def test_opencode_unknown_provider_rejected(self): self.assertIn("not supported", agent_check.message) +class TestValidateStarterTemplate(unittest.TestCase): + def test_none_accepted(self): + self.assertTrue(validate_starter_template("none")) + + def test_fullstack_baseline_accepted(self): + self.assertTrue(validate_starter_template("fullstack-baseline")) + + def test_case_insensitive(self): + self.assertTrue(validate_starter_template("Fullstack-Baseline")) + self.assertTrue(validate_starter_template("NONE")) + + def test_unknown_rejected(self): + result = validate_starter_template("my-custom-template") + self.assertIsInstance(result, str) + self.assertIn("my-custom-template", result) + + def test_empty_rejected(self): + result = validate_starter_template("") + self.assertIsInstance(result, str) + + def test_whitespace_stripped(self): + self.assertTrue(validate_starter_template(" none ")) + self.assertTrue(validate_starter_template(" fullstack-baseline ")) + + class TestPreflightResult(unittest.TestCase): def test_repr_pass(self): r = PreflightResult("test", True, "OK") diff --git a/dev-server-provision/configurator/validators.py b/dev-server-provision/configurator/validators.py index b7bf262..df54eb9 100644 --- a/dev-server-provision/configurator/validators.py +++ b/dev-server-provision/configurator/validators.py @@ -81,7 +81,7 @@ def validate_coder_password(value: str) -> str | bool: return True - +def validate_api_key_optional(value: str) -> str | bool: """Accept empty or any non-whitespace string.""" return True @@ -113,6 +113,24 @@ def validate_callback_url_or_code(value: str) -> str | bool: return True +_VALID_STARTER_TEMPLATES = ("none", "fullstack-baseline") + + +def validate_starter_template(value: str) -> str | bool: + """Validate the starter template selection. + + Accepted values: ``"none"`` (no template) or ``"fullstack-baseline"`` + (tiangolo/full-stack-fastapi-template — FastAPI + PostgreSQL + React). + """ + value = value.strip().lower() + if value not in _VALID_STARTER_TEMPLATES: + return ( + f"Unknown starter template '{value}'. " + f"Valid options: {', '.join(_VALID_STARTER_TEMPLATES)}." + ) + return True + + # --------------------------------------------------------------------------- # Preflight checks # --------------------------------------------------------------------------- diff --git a/dev-server-provision/docs/deployment.md b/dev-server-provision/docs/deployment.md index 9e7539e..956169e 100644 --- a/dev-server-provision/docs/deployment.md +++ b/dev-server-provision/docs/deployment.md @@ -204,6 +204,59 @@ which codex # if ENABLE_AGENT_CODEX=true which opencode # if ENABLE_AGENT_OPENCODE=true ``` +## Step 7: (Optional) Starter App Templates + +RemoteVibeServer can automatically bootstrap a production-ready starter +application inside every new Coder workspace. + +### Available templates + +| Value | Description | +|---------------------|------------------------------------------------------------| +| `none` | No template — plain workspace (default) | +| `fullstack-baseline`| FastAPI + PostgreSQL + React/Vite + Auth + RBAC (tiangolo) | + +### Enabling a template + +Set `STARTER_TEMPLATE` in `/etc/dev-server/env` **before** provisioning: + +```yaml +# In RVSconfig.yml / cloud-init.yaml: +STARTER_TEMPLATE: "fullstack-baseline" +``` + +Or via the configurator CLI — a new step `Starter App Template` appears after +the AI Agents step. + +### What fullstack-baseline provides + +Based on **[tiangolo/full-stack-fastapi-template](https://github.com/tiangolo/full-stack-fastapi-template)**: + +- **FastAPI** backend (Python, SQLModel, Alembic migrations) +- **PostgreSQL 16** database +- **React + Vite** frontend (TypeScript, Chakra UI) +- JWT auth + RBAC (superuser / regular-user roles) +- Docker Compose stack — starts automatically on workspace launch + +**Default ports inside the workspace:** + +| Service | URL | +|------------------|-----------------------------------| +| Frontend | http://localhost:5173 | +| Backend API | http://localhost:8000 | +| Interactive docs | http://localhost:8000/docs | + +**Default credentials:** `admin@example.com` / `changeme123` (change on first login) + +### Template lifecycle + +- The stack is cloned and started on the **first workspace launch** (~2 min) +- Subsequent starts run `docker compose up -d` (fast — containers already exist) +- A `.bootstrapped` marker prevents re-cloning; local edits persist across restarts +- Remove `.bootstrapped` and restart the workspace to reset to a clean clone + +For full documentation see [`templates/fullstack-baseline/README.md`](../../templates/fullstack-baseline/README.md). + ## Troubleshooting ### Provisioning failed diff --git a/dev-server-provision/infra/agents.sh b/dev-server-provision/infra/agents.sh index 0c3580e..7cd2323 100755 --- a/dev-server-provision/infra/agents.sh +++ b/dev-server-provision/infra/agents.sh @@ -39,7 +39,8 @@ log "Writing agent API key file → $AGENT_ENV_FILE …" for key in GITHUB_TOKEN OPENAI_API_KEY ANTHROPIC_API_KEY GOOGLE_API_KEY \ CODEX_OPENAI_AUTH_CODE OPENCODE_PROVIDER \ ENABLE_AGENT_COPILOT ENABLE_AGENT_CLAUDE ENABLE_AGENT_GEMINI \ - ENABLE_AGENT_CODEX ENABLE_AGENT_OPENCODE; do + ENABLE_AGENT_CODEX ENABLE_AGENT_OPENCODE \ + STARTER_TEMPLATE; do value="$(grep "^${key}=" "$ENV_FILE" | cut -d= -f2- || true)" echo "${key}=${value}" done diff --git a/templates/fullstack-baseline/README.md b/templates/fullstack-baseline/README.md new file mode 100644 index 0000000..d1bfb5b --- /dev/null +++ b/templates/fullstack-baseline/README.md @@ -0,0 +1,85 @@ +# fullstack-baseline — Starter App Template + +A production-ready fullstack application that is automatically bootstrapped +inside every new Coder workspace when `STARTER_TEMPLATE=fullstack-baseline` is +configured. + +It is based on **[tiangolo/full-stack-fastapi-template](https://github.com/tiangolo/full-stack-fastapi-template)** +— the official FastAPI project template maintained by the FastAPI author. + +--- + +## What you get + +| Layer | Technology | +|-------------|-----------------------------------------------| +| Backend API | FastAPI (Python) + SQLModel ORM | +| Database | PostgreSQL 16 | +| Migrations | Alembic | +| Frontend | React + Vite + TypeScript + Chakra UI | +| Auth | JWT bearer tokens (login / refresh) | +| RBAC | Superuser + regular-user roles | +| CRUD | Items resource (scaffold for your own models) | +| Dev stack | Docker Compose | + +--- + +## Ports (inside the workspace) + +| Service | URL | +|------------------|-----------------------------------| +| Frontend | http://localhost:5173 | +| Backend API | http://localhost:8000 | +| Interactive docs | http://localhost:8000/docs | +| Adminer (DB UI) | http://localhost:8080 | + +--- + +## Default credentials + +| Field | Value | +|----------|------------------------| +| Email | `admin@example.com` | +| Password | `changeme123` | + +Change the password immediately after first login. + +--- + +## How it works + +When a workspace starts with `STARTER_TEMPLATE=fullstack-baseline`, the +`startup_script` in `coder/main.tf`: + +1. Clones `tiangolo/full-stack-fastapi-template` into `~/fullstack-baseline` +2. Patches `.env` (project name, random `SECRET_KEY`, default superuser) +3. Runs `docker compose up -d` +4. Creates `~/fullstack-baseline/.bootstrapped` to make subsequent starts + idempotent + +The stack is started on every workspace launch (via `docker compose up -d`), +and stopped automatically when the workspace is stopped. + +--- + +## Re-running the bootstrap + +If the bootstrap was interrupted or you want a clean slate: + +```bash +rm ~/fullstack-baseline/.bootstrapped +# restart the workspace, or run the startup script manually +``` + +## Customising + +Fork the template or edit files directly in `~/fullstack-baseline`. The +`.bootstrapped` marker prevents re-cloning, so local changes persist across +workspace restarts. + +For a clean re-clone: + +```bash +rm -rf ~/fullstack-baseline +# restart the workspace +```