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
85 changes: 85 additions & 0 deletions .cursor/Dockerfile
Original file line number Diff line number Diff line change
@@ -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
100 changes: 100 additions & 0 deletions .cursor/common.sh
Original file line number Diff line number Diff line change
@@ -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
}
32 changes: 32 additions & 0 deletions .cursor/environment.json
Original file line number Diff line number Diff line change
@@ -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 }
]
}
58 changes: 58 additions & 0 deletions .cursor/install.sh
Original file line number Diff line number Diff line change
@@ -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"
19 changes: 19 additions & 0 deletions .cursor/logs-backend.sh
Original file line number Diff line number Diff line change
@@ -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
20 changes: 20 additions & 0 deletions .cursor/run-frontend.sh
Original file line number Diff line number Diff line change
@@ -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
31 changes: 31 additions & 0 deletions .cursor/start.sh
Original file line number Diff line number Diff line change
@@ -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"
14 changes: 13 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,26 @@

## 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
- `pnpm check:types` - TypeScript type checking
- `pnpm fix` - Auto-fix format and lint issues
- `pnpm turbo run <command> --filter=<package>` - 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
Expand Down
Loading