Canopydocs v0.4.7

Other stacks

Canopy's detection is Node-centric: it reads package.json scripts to propose services and database commands. Its execution isn't. Every command runs through your login shell, so any stack that works in a terminal works here. For a non-Node repository, detection identifies the stack and you configure the services and commands yourself.

Stack-specific behaviour#

FeatureStack-specific?
Worktree create, switch, pull, removeNo, it's pure git.
Ports (basePort + index × 10)No.
Provisioned files (dotenv / json / yaml / text)No.
Setup, migrate and teardown commandsNo, any shell command.
Services, logs, CPU/memory, restartNo.
Terminals and agentsNo.
Node version pinningNode-specific (.nvmrc, .node-version, .tool-versions).
Service and command auto-detectionNode-specific.
Database tools (switch, snapshot, export, restore)Postgres-specific.

Other language version managers (rbenv, pyenv, asdf for Ruby, Python or Go) resolve automatically, because commands run through a login shell in the worktree directory and those managers' shims read the directory's version file the same way they do for you.

Rails#

{
  "provision": [
    {
      "path": "config/database.yml",
      "format": "yaml",
      "keys": { "development.database": "${WT_DB_NAME}" }
    },
    {
      "path": ".env",
      "format": "dotenv",
      "keys": { "PORT": "${WT_WEB_PORT}", "PG_DB": "${WT_DB_NAME}" }
    }
  ],
  "setup": ["bundle install", "bin/rails db:create db:migrate"],
  "migrate": ["bin/rails db:migrate"],
  "teardown": ["bin/rails db:drop"]
}

Services:

idnamekindcommandbasePort
webWebwebbin/rails server -p $PORT3000
jobsJobsworkerbundle exec sidekiq(none)

.env also carries PG_DB so the database dialog can read the connection. It reads PG_* keys from the worktree's .env whatever else your app uses.

Django#

{
  "provision": [
    { "path": ".env", "format": "dotenv",
      "keys": { "PG_DB": "${WT_DB_NAME}", "PORT": "${WT_WEB_PORT}", "DATABASE_URL": "postgres://postgres@localhost:5432/${WT_DB_NAME}" } }
  ],
  "setup": [
    "python -m venv .venv",
    ".venv/bin/pip install -r requirements.txt",
    "createdb ${WT_DB_NAME}",
    ".venv/bin/python manage.py migrate"
  ],
  "migrate": [".venv/bin/python manage.py migrate"],
  "teardown": ["dropdb --if-exists ${WT_DB_NAME}"]
}

Service: web · web · .venv/bin/python manage.py runserver 0.0.0.0:$PORT · basePort 8000.

A per-worktree virtualenv inside the worktree is the simplest thing that works, since each worktree is its own directory anyway.

Go#

{
  "provision": [
    { "path": ".env", "format": "dotenv", "keys": { "PORT": "${WT_API_PORT}", "PG_DB": "${WT_DB_NAME}" } }
  ],
  "setup": ["go mod download", "go build ./...", "migrate -database postgres://localhost/${WT_DB_NAME} up"]
}

Service: api · server · go run ./cmd/api, reading PORT from the environment · basePort 8080.

Go's module cache is shared, so setup is quick and a whole worktree is often ready in seconds.

Rust#

{
  "provision": [
    { "path": ".env", "format": "dotenv", "keys": { "PORT": "${WT_API_PORT}" } }
  ],
  "setup": ["cargo fetch", "cargo build"]
}

Service: api · server · cargo run --release · basePort 8080.

Docker Compose#

Compose works, with one thing to watch: the project name has to be per worktree, or two branches will share the same containers.

{
  "provision": [
    { "path": ".env", "format": "dotenv",
      "keys": { "COMPOSE_PROJECT_NAME": "app_${WT_SLUG}", "WEB_PORT": "${WT_WEB_PORT}", "DB_PORT": "${WT_DB_PORT}" } }
  ],
  "setup": ["docker compose pull"]
}

Service: web · web · docker compose up · basePort 8000, and map the host ports from ${WEB_PORT} and ${DB_PORT} in your compose file. Canopy's stop sends SIGTERM to the process group, which docker compose up handles by stopping the stack. A docker compose down custom command makes a good companion.

Canopy's database tools talk to a Postgres server directly over PG_* settings. With the database inside Compose, point PG_HOST and PG_PORT at the published port and they work. Otherwise treat the database as part of the service and skip those actions.

A stack with no database at all#

Nothing requires one. Leave PG_DB out and the database chip and dialog simply don't appear:

{
  "provision": [{ "path": ".env", "format": "dotenv", "keys": { "PORT": "${WT_WEB_PORT}" } }],
  "setup": ["pnpm install"]
}

Polyglot repositories#

A repository can mix all of these. A Go API, a Vite frontend and a Python worker are three services with three commands and three base ports. Canopy doesn't care what language a service is written in, only what command starts it and which port it should get.

Documentation for Canopy 0.4.7. Controls marked coming soon are present in the interface but have no implementation behind them yet.