Skip to content
Open
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
302 changes: 302 additions & 0 deletions .opencode/skills/worktree/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,302 @@
---
name: worktree
description: Git worktree management skill — creates isolated sibling worktrees with auto-computed ports, .env generation, dependency install, and cleanup. Designed for Turborepo/pnpm monorepos but works with any stack.
version: 1.0.0
author: opencode
type: skill
category: development
tags:
- git
- worktree
- ports
- monorepo
- turborepo
- environment
---

# Worktree Skill

> **Purpose**: Create and manage fully isolated git worktrees as sibling directories. Each worktree gets its own port range, `.env` files, and (optionally) a Zellij terminal session — so you can run `main`, a feature branch, and a hotfix simultaneously without anything conflicting.

---

## What I Do

- **Create** a new worktree as a sibling directory with a clean name
- **Compute ports** automatically using an index scheme (no manual port tracking)
- **Generate `.env` files** for each app from a shared `.env.template`
- **Copy secrets** (`.env.local`) from the main worktree
- **Install dependencies** (pnpm / yarn / npm, auto-detected)
- **Launch Zellij** session named after the worktree (if Zellij is installed)
- **Remove** a worktree cleanly — stops Docker, kills Zellij session, prunes branch

---

## Quick Start

```bash
# Create a worktree for a new feature branch
bash .opencode/skills/worktree/router.sh create feature/auth

# Create with a custom short name (used for the directory suffix)
bash .opencode/skills/worktree/router.sh create feature/auth feature-auth

# List all worktrees and their paths
bash .opencode/skills/worktree/router.sh list

# Show port assignments for all worktrees
bash .opencode/skills/worktree/router.sh ports

# Remove a worktree after the branch is merged
bash .opencode/skills/worktree/router.sh remove feature-auth

# Remove but keep the branch
bash .opencode/skills/worktree/router.sh remove feature-auth --keep-branch
```

---

## Directory Structure

Worktrees are created as **siblings** to the main repo, not inside it:

```
~/Documents/github/
my-app/ ← main worktree (main branch)
my-app-feature-auth/ ← created by: create feature/auth
my-app-bugfix-payments/ ← created by: create bugfix/payments
my-app-experiment-ai/ ← created by: create experiment/ai
```

Each directory is a full working tree sharing the same `.git` — completely isolated, with its own `.env` files and installed packages.

---

## Port Index Scheme

Each worktree is assigned a numeric **index** derived from how many worktrees exist at the moment `create` runs. The formula is `BASE + (INDEX × 10)`.

How the index is calculated at creation time:
- Only main exists (1 line from `git worktree list`): `1 - 1 = index 0` → main
- main + 1 feature (2 lines): `2 - 1 = index 1` → first feature
- main + 2 features (3 lines): `3 - 1 = index 2` → second feature

| Index | Worktree | Web | Admin | API | DB | Redis |
|-------|----------------|------|-------|------|------|-------|
| 0 | main | 3000 | 3001 | 3002 | 5432 | 6379 |
| 1 | first feature | 3010 | 3011 | 3012 | 5442 | 6389 |
| 2 | second feature | 3020 | 3021 | 3022 | 5452 | 6399 |
| 3 | third feature | 3030 | 3031 | 3032 | 5462 | 6409 |

> **Note:** If you remove a worktree, its index slot is freed and will be reused by the next `create`. Run `ports` before creating a new worktree to see current assignments.

Run `bash .opencode/skills/worktree/router.sh ports` to see the live table.

---

## `.env.template` Setup

Add a `.env.template` to your repo root (commit it). The script replaces `__PLACEHOLDER__` tokens at worktree creation time.

```bash
# .env.template
WORKTREE_INDEX=__INDEX__
WORKTREE_NAME=__NAME__

WEB_PORT=__WEB_PORT__
ADMIN_PORT=__ADMIN_PORT__
API_PORT=__API_PORT__
DB_PORT=__DB_PORT__
REDIS_PORT=__REDIS_PORT__

DATABASE_URL=postgresql://postgres:postgres@localhost:__DB_PORT__/myapp
REDIS_URL=redis://localhost:__REDIS_PORT__
NEXT_PUBLIC_API_URL=http://localhost:__API_PORT__
```

Generated `.env` files are placed at:
- `{worktree}/.env` — root
- `{worktree}/apps/web/.env` — if directory exists
- `{worktree}/apps/admin/.env` — if directory exists
- `{worktree}/apps/api/.env` — if directory exists

> **Limitation:** Per-app `.env` generation is hardcoded to `apps/web`, `apps/admin`, and `apps/api`. Directories that don't exist are silently skipped. If your monorepo uses different app names, edit `wt-new.sh` lines ~148-151 to match your structure.

Add to `.gitignore`:
```
.env
.env.local
apps/**/.env
apps/**/.env.local
```

---

## Per-App Port Configuration

### Next.js (`apps/web`, `apps/admin`)

Next.js does not read `PORT` from `.env` automatically — pass it via the script:

```json
{
"scripts": {
"dev": "dotenv -e .env -- next dev -p $WEB_PORT"
}
}
```

Install `dotenv-cli` once at the workspace root:
```bash
pnpm add -Dw dotenv-cli
```

### Vite (`apps/admin` or any Vite app)

`vite.config.ts`:
```ts
import { defineConfig, loadEnv } from 'vite';

export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), '');
return {
server: {
port: Number(env.WEB_PORT || 5173),
strictPort: true,
},
};
});
```

### NestJS (`apps/api`)

`src/main.ts`:
```ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(process.env.API_PORT || 3002);
}
bootstrap();
```

NestJS picks up `.env` via `@nestjs/config` automatically.

### Turborepo `turbo.json`

Declare port env vars so Turbo doesn't use stale cache across worktrees:

```json
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"dev": {
"cache": false,
"env": ["WEB_PORT", "ADMIN_PORT", "API_PORT", "DB_PORT", "REDIS_PORT", "WORKTREE_INDEX"]
},
"build": {
"dependsOn": ["^build"],
"env": ["WEB_PORT", "ADMIN_PORT", "API_PORT"]
}
}
}
```

---

## Docker Compose

Use the same env values for fully isolated DB/Redis volumes per worktree:

```yaml
# docker-compose.yml
services:
postgres:
image: postgres:16
ports:
- "${DB_PORT:-5432}:5432"
environment:
POSTGRES_DB: myapp_${WORKTREE_NAME:-main}
volumes:
- pgdata_${WORKTREE_INDEX:-0}:/var/lib/postgresql/data

redis:
image: redis:7
ports:
- "${REDIS_PORT:-6379}:6379"

volumes:
pgdata_0:
pgdata_1:
pgdata_2:
pgdata_3:
```

Run from worktree root:
```bash
docker compose --env-file .env up -d
```

---

## Everyday Workflow

```bash
# 1. Create a worktree for a new feature
bash .opencode/skills/worktree/router.sh create feature/checkout feature-checkout

# 2. In the new directory, start everything
cd ~/Documents/github/my-app-feature-checkout
pnpm dev # all apps on unique ports
docker compose --env-file .env up -d # DB + Redis isolated

# 3. Check what's running
bash .opencode/skills/worktree/router.sh list
bash .opencode/skills/worktree/router.sh ports

# 4. Clean up after merge
bash .opencode/skills/worktree/router.sh remove feature-checkout
```

---

## Command Reference

| Command | Description |
|---------|-------------|
| `create <branch> [name]` | Create worktree from branch (creates branch if new) |
| `remove <name> [--keep-branch]` | Remove worktree, stop Docker, kill Zellij session |
| `list` | Show all worktrees with paths and branches |
| `ports` | Show port index table for all worktrees |
| `help` | Show usage |

---

## File Structure

```
.opencode/skills/worktree/
├── SKILL.md # This file
├── router.sh # CLI entry point
└── scripts/
├── wt-new.sh # Create worktree logic
└── wt-close.sh # Remove worktree logic
```

---

## Troubleshooting

### "Branch already exists"
The script detects existing local and remote branches and checks them out instead of creating new ones.

### "Directory already exists"
The target sibling directory is already present. Remove it manually or use a different name.

### ".env.template not found"
`.env` generation is skipped. Create a `.env.template` in your repo root to enable it.

### Ports already in use
Run `ports` to see current assignments. If a worktree was removed without pruning, run `git worktree prune` in the main repo to reclaim the index slot.

### Zellij not launching
Zellij is optional. If not installed, the script prints the `cd` path and exits normally.
Loading
Loading