diff --git a/.cursor/Dockerfile b/.cursor/Dockerfile new file mode 100644 index 000000000000..cae9ef18b192 --- /dev/null +++ b/.cursor/Dockerfile @@ -0,0 +1,85 @@ +FROM ubuntu:24.04 + +# Thin Cloud Agent toolchain only: Node (matches .node-version), pnpm, and Docker. +# Plane is not installed or booted in this image. Contributors start it with +# ./setup.sh, docker compose -f docker-compose-local.yml, and pnpm dev +# (see CONTRIBUTING.md). Cloud Agents run the safe equivalents in +# .cursor/install.sh and .cursor/start.sh. + +ENV DEBIAN_FRONTEND=noninteractive \ + COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \ + COREPACK_DEFAULT_TO_LATEST=0 \ + COREPACK_HOME=/usr/local/share/corepack \ + PATH="/usr/local/bin:${PATH}" \ + BASH_ENV="/etc/profile.d/99-local-bin-path.sh" + +# git and curl are required by Cloud Agents. +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + ca-certificates \ + curl \ + git \ + gnupg \ + sudo \ + xz-utils \ + && rm -rf /var/lib/apt/lists/* + +# Matches .node-version and package.json engines.node (>=22.22.0). +# Installed under /usr/local/bin so login shells find it without rc files. +ARG NODE_VERSION=22.22.0 +RUN curl --retry 3 --retry-delay 5 -fsSL \ + "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.xz" \ + | tar -xJ -C /usr/local --strip-components=1 --no-same-owner \ + && mkdir -p /usr/local/share/corepack \ + && corepack enable \ + && corepack prepare pnpm@11.10.0 --activate \ + && chmod -R a+rX /usr/local/share/corepack + +RUN printf '%s\n' 'export PATH="/usr/local/bin:${PATH}"' > /etc/profile.d/99-local-bin-path.sh \ + && printf '%s\n%s\n' 'export PATH="/usr/local/bin:${PATH}"' "$(cat /etc/bash.bashrc)" > /etc/bash.bashrc + +######################################################## +# DOCKER INSTALLATION +# https://cursor.com/docs/cloud-agent/setup#running-docker +######################################################## + +RUN install -m 0755 -d /etc/apt/keyrings \ + && curl --retry 3 --retry-delay 5 -fsSL https://download.docker.com/linux/ubuntu/gpg \ + | gpg --dearmor -o /etc/apt/keyrings/docker.gpg \ + && chmod a+r /etc/apt/keyrings/docker.gpg \ + && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ + $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null \ + && apt-get update \ + && apt-get install -y \ + docker-ce=5:28.5.2-1~ubuntu.24.04~noble \ + docker-ce-cli=5:28.5.2-1~ubuntu.24.04~noble \ + docker-ce-rootless-extras=5:28.5.2-1~ubuntu.24.04~noble \ + containerd.io \ + docker-buildx-plugin \ + docker-compose-plugin \ + && rm -rf /var/lib/apt/lists/* + +RUN apt-get update \ + && apt-get install -y -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" fuse-overlayfs \ + && rm -rf /var/lib/apt/lists/* +RUN mkdir -p /etc/docker \ + && printf '%s\n' '{' \ + ' "storage-driver": "fuse-overlayfs"' \ + '}' > /etc/docker/daemon.json +RUN apt-get update && apt-get install -y iptables && rm -rf /var/lib/apt/lists/* +RUN update-alternatives --set iptables /usr/sbin/iptables-legacy \ + && update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy + +# ubuntu:24.04 already has the ubuntu user. Do not useradd it. +# The base image does not install OpenSSH, so create the drop-in directory first. +RUN mkdir -p /etc/ssh/sshd_config.d \ + && printf '%s\n' 'PasswordAuthentication no' 'ChallengeResponseAuthentication no' 'UsePAM no' > /etc/ssh/sshd_config.d/disable_password_auth.conf \ + && groupadd -f docker \ + && usermod -aG docker ubuntu \ + && usermod -aG sudo ubuntu \ + && echo "ubuntu ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/ubuntu \ + && chmod 0440 /etc/sudoers.d/ubuntu \ + && chmod 1777 /usr/local/share/corepack + +USER ubuntu +WORKDIR /workspace diff --git a/.cursor/common.sh b/.cursor/common.sh new file mode 100755 index 000000000000..ffff2b11ba06 --- /dev/null +++ b/.cursor/common.sh @@ -0,0 +1,100 @@ +#!/usr/bin/env bash +# Shared helpers for the Cloud Agent install and start scripts. + +export PATH="/usr/local/bin:${PATH}" +export COREPACK_ENABLE_DOWNLOAD_PROMPT=0 +export COREPACK_DEFAULT_TO_LATEST=0 +export COREPACK_HOME="${COREPACK_HOME:-/usr/local/share/corepack}" + +# Resolve once, at source time, so later cdirs do not reinterpret a relative path. +_PLANE_COMMON_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +_PLANE_ROOT="$(cd "${_PLANE_COMMON_DIR}/.." && pwd)" + +plane_root() { + printf '%s\n' "$_PLANE_ROOT" +} + +copy_env_if_missing() { + local source=$1 + local destination=$2 + if [ -f "$destination" ]; then + echo "env exists: $destination" + return 0 + fi + if [ ! -f "$source" ]; then + echo "missing env example: $source" >&2 + return 1 + fi + cp "$source" "$destination" + echo "copied $destination" +} + +ensure_secret_key() { + local env_file="apps/api/.env" + if [ ! -f "$env_file" ]; then + echo "apps/api/.env is missing; cannot add SECRET_KEY" >&2 + return 1 + fi + if grep -q '^SECRET_KEY=' "$env_file"; then + echo "SECRET_KEY already set" + return 0 + fi + local secret_key + # Same alphabet as setup.sh (a-z0-9, 50 chars). Generate with node so the + # image does not need Python. Do not pipe /dev/urandom into head: with + # pipefail, head closing the pipe exits 141 before the key is written. + secret_key="$(node -e 'const {randomInt}=require("crypto"); const alphabet="abcdefghijklmnopqrstuvwxyz0123456789"; let secret=""; for (let i=0;i<50;i++) secret+=alphabet[randomInt(alphabet.length)]; process.stdout.write(secret);')" + if [ -z "$secret_key" ]; then + echo "failed to generate SECRET_KEY" >&2 + return 1 + fi + printf '\nSECRET_KEY="%s"\n' "$secret_key" >> "$env_file" + echo "added SECRET_KEY to apps/api/.env" +} + +ensure_docker() { + if docker info >/dev/null 2>&1; then + return 0 + fi + echo "starting docker daemon" + sudo service docker start >/tmp/docker-service.log 2>&1 || true + local attempt + for attempt in $(seq 1 45); do + if [ -S /var/run/docker.sock ]; then + sudo chmod 666 /var/run/docker.sock 2>/dev/null || true + fi + if docker info >/dev/null 2>&1; then + return 0 + fi + sleep 1 + done + echo "service docker did not become ready; starting dockerd directly" >&2 + sudo dockerd >/tmp/dockerd.log 2>&1 & + for attempt in $(seq 1 45); do + if [ -S /var/run/docker.sock ]; then + sudo chmod 666 /var/run/docker.sock 2>/dev/null || true + fi + if docker info >/dev/null 2>&1; then + return 0 + fi + sleep 1 + done + echo "Docker daemon failed to start. See /tmp/docker-service.log and /tmp/dockerd.log" >&2 + return 1 +} + +wait_for_http() { + local url=$1 + local attempts=${2:-180} + local attempt code + for attempt in $(seq 1 "$attempts"); do + code="$(curl -s -o /dev/null -w '%{http_code}' --max-time 2 "$url" || true)" + if [ -n "$code" ] && [ "$code" != "000" ]; then + echo "ready $url ($code)" + return 0 + fi + sleep 2 + done + echo "timed out waiting for $url" >&2 + return 1 +} diff --git a/.cursor/environment.json b/.cursor/environment.json new file mode 100644 index 000000000000..c3daad899b47 --- /dev/null +++ b/.cursor/environment.json @@ -0,0 +1,32 @@ +{ + "name": "Plane", + "user": "ubuntu", + "build": { + "dockerfile": "Dockerfile" + }, + "install": "bash .cursor/install.sh", + "start": "bash .cursor/start.sh", + "terminals": [ + { + "name": "backend", + "command": "bash .cursor/logs-backend.sh", + "description": "Compose logs for the local API, worker, and beat" + }, + { + "name": "frontend", + "command": "bash .cursor/run-frontend.sh", + "description": "pnpm dev: web :3000, admin :3001, space :3002, live :3100" + } + ], + "ports": [ + { "name": "web", "port": 3000 }, + { "name": "admin", "port": 3001 }, + { "name": "space", "port": 3002 }, + { "name": "live", "port": 3100 }, + { "name": "api", "port": 8000 }, + { "name": "postgres", "port": 5432 }, + { "name": "redis", "port": 6379 }, + { "name": "minio", "port": 9000 }, + { "name": "minio console", "port": 9090 } + ] +} diff --git a/.cursor/install.sh b/.cursor/install.sh new file mode 100755 index 000000000000..c71ccd58f62a --- /dev/null +++ b/.cursor/install.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +# Safe equivalent of ./setup.sh for Cloud Agents, plus the image pulls and +# builds docker-compose-local.yml needs before `up`. +# +# Unlike ./setup.sh, this never overwrites an existing .env file and never +# appends a second SECRET_KEY. +# +# When stack.sh lands in the repo, call it from here instead of the steps below. +# Do not add a second way to boot Plane. +set -euo pipefail + +source "$(dirname "${BASH_SOURCE[0]}")/common.sh" +cd "$(plane_root)" + +/usr/local/bin/node -e ' +const [major, minor] = process.versions.node.split(".").map(Number); +if (major < 22 || (major === 22 && minor < 22)) { + console.error(`Node ${process.versions.node} is older than 22.22.0`); + process.exit(1); +} +console.log(`node v${process.versions.node}`); +' + +# setup.sh copies these six files. Copy only when the destination is missing. +copy_env_if_missing ".env.example" ".env" +copy_env_if_missing "apps/web/.env.example" "apps/web/.env" +copy_env_if_missing "apps/api/.env.example" "apps/api/.env" +copy_env_if_missing "apps/space/.env.example" "apps/space/.env" +copy_env_if_missing "apps/admin/.env.example" "apps/admin/.env" +copy_env_if_missing "apps/live/.env.example" "apps/live/.env" +ensure_secret_key + +# packageManager pins pnpm@11.10.0. Activate it if the image shim is stale. +if ! command -v pnpm >/dev/null 2>&1 || [ "$(pnpm -v 2>/dev/null || true)" != "11.10.0" ]; then + sudo mkdir -p /usr/local/share/corepack + sudo chmod 1777 /usr/local/share/corepack + sudo /usr/local/bin/corepack enable + # Prepare as the runtime user so the package is not stored only in root's home. + /usr/local/bin/corepack prepare pnpm@11.10.0 --activate +fi +echo "pnpm $(pnpm -v)" + +pnpm install --frozen-lockfile + +# CONTRIBUTING assumes Docker is already running. Cloud Agent VMs start the +# daemon here so the image pulls below can run during install. +ensure_docker + +echo "pulling local infrastructure images" +docker compose -f docker-compose-local.yml pull plane-db plane-redis plane-mq plane-minio + +# api, worker, beat-worker, and migrator share apps/api/Dockerfile.dev. +# Build api first so the other services reuse the layer cache. +echo "building local API image" +docker compose -f docker-compose-local.yml build api +docker compose -f docker-compose-local.yml build worker beat-worker migrator + +echo "install complete" diff --git a/.cursor/logs-backend.sh b/.cursor/logs-backend.sh new file mode 100755 index 000000000000..fdd35bb98b30 --- /dev/null +++ b/.cursor/logs-backend.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# Backend terminal. Follow compose logs for the local API once start.sh has +# created the containers. This does not start the stack. +set -euo pipefail + +source "$(dirname "${BASH_SOURCE[0]}")/common.sh" +cd "$(plane_root)" + +attempt=0 +until docker compose -f docker-compose-local.yml ps -q api | grep -q .; do + attempt=$((attempt + 1)) + if [ "$attempt" -gt 180 ]; then + echo "api container was not created" >&2 + exit 1 + fi + sleep 2 +done + +exec docker compose -f docker-compose-local.yml logs -f api worker beat-worker diff --git a/.cursor/run-frontend.sh b/.cursor/run-frontend.sh new file mode 100755 index 000000000000..54c9104ad21a --- /dev/null +++ b/.cursor/run-frontend.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# Frontend terminal. CONTRIBUTING starts the web apps with `pnpm dev` +# (web :3000, admin :3001, space :3002, live :3100), separate from compose. +# Terminals start beside the boot script, so wait until the API is accepting connections. +set -euo pipefail + +source "$(dirname "${BASH_SOURCE[0]}")/common.sh" +cd "$(plane_root)" + +if curl -sf -o /dev/null --max-time 2 http://127.0.0.1:3000/ \ + && curl -sf -o /dev/null --max-time 2 http://127.0.0.1:3001/; then + echo "dev servers already listening on :3000 and :3001" + exec sleep infinity +fi + +if ! wait_for_http "http://127.0.0.1:8000/api/instances/" 180; then + echo "API was not ready; starting pnpm dev anyway" >&2 +fi + +exec pnpm dev diff --git a/.cursor/start.sh b/.cursor/start.sh new file mode 100755 index 000000000000..9f3497884520 --- /dev/null +++ b/.cursor/start.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# Backend start from CONTRIBUTING.md: +# docker compose -f docker-compose-local.yml up -d +# Then wait until the API answers on :8000. +# +# Frontends stay out of this script. A terminal runs `pnpm dev`. +# When stack.sh lands in the repo, call it from here instead. +set -euo pipefail + +source "$(dirname "${BASH_SOURCE[0]}")/common.sh" +cd "$(plane_root)" + +# Cloud Agent VMs have no running Docker daemon until this starts it. +# The Plane stack itself is only the compose file below. +ensure_docker + +if [ ! -f apps/api/.env ] || [ ! -f .env ]; then + echo "env files missing; run bash .cursor/install.sh first" >&2 + exit 1 +fi + +docker compose -f docker-compose-local.yml up -d + +echo "waiting for Plane API on :8000" +if ! wait_for_http "http://127.0.0.1:8000/api/instances/" 180; then + docker compose -f docker-compose-local.yml ps >&2 || true + docker compose -f docker-compose-local.yml logs --tail 120 api migrator >&2 || true + exit 1 +fi + +echo "local stack is up" diff --git a/AGENTS.md b/AGENTS.md index 5dabb62afab4..5cae6ce82bd8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,7 +2,7 @@ ## Commands -- `pnpm dev` - Start all dev servers (web:3000, admin:3001) +- `pnpm dev` - Start frontend dev servers (web:3000, admin:3001, space:3002, live:3100) - `pnpm build` - Build all packages and apps - `pnpm check` - Run all checks (format, lint, types) - `pnpm check:lint` - OxLint across all packages @@ -10,6 +10,18 @@ - `pnpm fix` - Auto-fix format and lint issues - `pnpm turbo run --filter=` - Target specific package/app +## Cloud Agent + +`.cursor/environment.json` follows the local setup in CONTRIBUTING.md. The image is only the Cloud Agent toolchain: Node 22.22.0 (`.node-version`), pnpm 11.10.0, and Docker with fuse-overlayfs. It does not install Plane or boot the app. + +`bash .cursor/install.sh` is a safe `./setup.sh`. It copies missing `.env` files from the examples, adds `SECRET_KEY` to `apps/api/.env` only when that line is absent, and runs `pnpm install --frozen-lockfile`. It also pulls and builds the images `docker-compose-local.yml` needs. It never overwrites an existing env file and never appends a second `SECRET_KEY`. + +`bash .cursor/start.sh` runs `docker compose -f docker-compose-local.yml up -d` and waits until the API is healthy on :8000. The `frontend` terminal runs `pnpm dev` (web :3000, admin :3001, space :3002, live :3100). The `backend` terminal follows compose logs for the API, worker, and beat. + +When `stack.sh` lands in the repo, install and start should call that script instead of these compose and pnpm steps. + +Do not re-run `./setup.sh` on a checkout that already has `.env` files. It overwrites those files and appends another `SECRET_KEY`. + ## Code Style - **Imports**: Use `workspace:*` for internal packages, `catalog:` for external deps