@smicolon/ai-kit 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/packs/worktree/.claude-plugin/plugin.json +2 -2
- package/packs/worktree/CHANGELOG.md +24 -3
- package/packs/worktree/README.md +118 -9
- package/packs/worktree/commands/wt.md +30 -10
- package/packs/worktree/scripts/wt.sh +609 -49
- package/packs/worktree/skills/worktree-manager/SKILL.md +58 -13
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "worktree",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Git worktree manager for parallel development with
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Git worktree manager for parallel development with env isolation, Docker port offsets, and database auto-creation",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Smicolon",
|
|
7
7
|
"email": "dev@smicolon.com",
|
|
@@ -1,12 +1,33 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
All notable changes to
|
|
3
|
+
All notable changes to worktree will be documented in this file.
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.2.0] - 2026-02-15
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `.worktreeinclude` config file — gitignore-style patterns for files to copy
|
|
12
|
+
- Auto-generates `.worktreeinclude` with sensible defaults on first `wt create`
|
|
13
|
+
- `[rewrite]` section — auto-suffixes DB_NAME, DATABASE_URL, COMPOSE_PROJECT_NAME per branch
|
|
14
|
+
- `{{BRANCH}}` template support for custom env var rewriting
|
|
15
|
+
- `[docker]` section — generates `docker-compose.worktree.yml` with port offsets
|
|
16
|
+
- Deterministic port offset via branch name hash (1-100 range)
|
|
17
|
+
- Custom compose file path via `file=` directive (supports monorepo nested paths)
|
|
18
|
+
- Auto-creates Postgres databases (Docker containers or local)
|
|
19
|
+
- `docker compose down` on `wt remove` before removing worktree
|
|
20
|
+
- Port mapping summary in create output
|
|
21
|
+
|
|
7
22
|
### Changed
|
|
8
|
-
|
|
9
|
-
-
|
|
23
|
+
|
|
24
|
+
- `wt create` now uses `.worktreeinclude` patterns instead of hardcoded `.env*` copying
|
|
25
|
+
- `wt remove` stops Docker containers and notes that databases are preserved
|
|
26
|
+
- `wt help` documents `.worktreeinclude` sections
|
|
27
|
+
|
|
28
|
+
### Removed
|
|
29
|
+
|
|
30
|
+
- Hardcoded `copy_env_files()` function (replaced by `.worktreeinclude` pattern matching)
|
|
10
31
|
|
|
11
32
|
## [0.1.0] - 2026-01-17
|
|
12
33
|
|
package/packs/worktree/README.md
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
# worktree
|
|
2
2
|
|
|
3
|
-
Git worktree manager for parallel development with automatic environment
|
|
3
|
+
Git worktree manager for parallel development with automatic environment isolation.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
7
|
- **Sibling naming**: `project--branch-name/` convention
|
|
8
|
-
-
|
|
8
|
+
- **`.worktreeinclude`**: Configurable file copying (replaces hardcoded `.env*`)
|
|
9
|
+
- **Env rewriting**: Auto-suffixes DB names, URLs, and compose project names per branch
|
|
10
|
+
- **Docker isolation**: Port-offset overrides so worktrees don't conflict
|
|
11
|
+
- **Database auto-creation**: Creates Postgres databases for rewritten DB names
|
|
9
12
|
- **Package manager detection**: bun → pnpm → yarn → npm
|
|
10
13
|
- **Monorepo aware**: Detects workspaces, turbo, nx, lerna
|
|
11
14
|
- **Editor integration**: Open in Cursor, Antigravity, or VS Code
|
|
@@ -16,6 +19,98 @@ Git worktree manager for parallel development with automatic environment setup.
|
|
|
16
19
|
/plugin install worktree
|
|
17
20
|
```
|
|
18
21
|
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
/wt create feature/auth
|
|
26
|
+
# → Creates worktree at ~/code/project--feature-auth/
|
|
27
|
+
# → Copies files per .worktreeinclude
|
|
28
|
+
# → Rewrites DB_NAME, DATABASE_URL with branch suffix
|
|
29
|
+
# → Generates docker-compose.worktree.yml with offset ports
|
|
30
|
+
# → Creates database in running Postgres
|
|
31
|
+
# → Installs dependencies
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## `.worktreeinclude`
|
|
35
|
+
|
|
36
|
+
Auto-generated on first `wt create` if missing. Commit this file so your team shares the same config.
|
|
37
|
+
|
|
38
|
+
```ini
|
|
39
|
+
# .worktreeinclude — Files to copy to new worktrees
|
|
40
|
+
.env*
|
|
41
|
+
|
|
42
|
+
# Monorepo (uncomment as needed)
|
|
43
|
+
# apps/*/.env*
|
|
44
|
+
# packages/*/.env*
|
|
45
|
+
# services/*/.env*
|
|
46
|
+
|
|
47
|
+
[rewrite]
|
|
48
|
+
auto
|
|
49
|
+
|
|
50
|
+
[docker]
|
|
51
|
+
auto
|
|
52
|
+
# file=apps/backend/docker-compose.local.yml
|
|
53
|
+
# port_offset=10
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### File Patterns (top section)
|
|
57
|
+
|
|
58
|
+
Gitignore-style glob patterns for untracked files to copy from main worktree:
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
.env*
|
|
62
|
+
apps/*/.env*
|
|
63
|
+
config/local.yml
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### `[rewrite]` — Env Var Isolation
|
|
67
|
+
|
|
68
|
+
**`auto` mode** detects and suffixes known keys with a branch slug:
|
|
69
|
+
|
|
70
|
+
| Key | Example | Result |
|
|
71
|
+
|-----|---------|--------|
|
|
72
|
+
| `DB_NAME` | `myapp` | `myapp_feature_auth` |
|
|
73
|
+
| `POSTGRES_DB` | `myapp` | `myapp_feature_auth` |
|
|
74
|
+
| `DATABASE_URL` | `postgres://...host/myapp` | `postgres://...host/myapp_feature_auth` |
|
|
75
|
+
| `COMPOSE_PROJECT_NAME` | `myapp` | `myapp_feature_auth` |
|
|
76
|
+
|
|
77
|
+
**Template mode** uses `{{BRANCH}}` for custom vars:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
MY_CUSTOM_DB=app_{{BRANCH}}
|
|
81
|
+
REDIS_PREFIX={{BRANCH}}_
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Both modes work together — template takes precedence over auto.
|
|
85
|
+
|
|
86
|
+
Branch slug: `feature/auth-v2` → `feature_auth_v2` (lowercase, special chars → `_`, max 30 chars).
|
|
87
|
+
|
|
88
|
+
### `[docker]` — Docker Compose Isolation
|
|
89
|
+
|
|
90
|
+
Generates a `docker-compose.worktree.yml` override with port offsets:
|
|
91
|
+
|
|
92
|
+
- **`auto`**: Auto-detects compose file (`local.yml` → `docker-compose.local.yml` → `docker-compose.yml` → etc.)
|
|
93
|
+
- **`file=path`**: Specify compose file path (supports nested monorepo paths)
|
|
94
|
+
- **`port_offset=N`**: Override the auto-calculated offset
|
|
95
|
+
|
|
96
|
+
```ini
|
|
97
|
+
[docker]
|
|
98
|
+
auto
|
|
99
|
+
file=apps/backend/docker-compose.local.yml
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**How it works:**
|
|
103
|
+
|
|
104
|
+
1. Parses ports from your compose file
|
|
105
|
+
2. Calculates a deterministic offset from branch name (1-100 via `cksum`)
|
|
106
|
+
3. Generates `docker-compose.worktree.yml` next to the original
|
|
107
|
+
4. Sets `COMPOSE_FILE=original.yml:docker-compose.worktree.yml` in `.env`
|
|
108
|
+
5. Sets `COMPOSE_PROJECT_NAME` for container/volume/network isolation
|
|
109
|
+
|
|
110
|
+
After setup, `docker compose up -d` in the worktree automatically picks up both files.
|
|
111
|
+
|
|
112
|
+
**Auto-creates databases** in running Postgres containers (Docker or local). Idempotent — safe to run multiple times.
|
|
113
|
+
|
|
19
114
|
## Usage
|
|
20
115
|
|
|
21
116
|
### Create Worktree
|
|
@@ -26,7 +121,10 @@ Git worktree manager for parallel development with automatic environment setup.
|
|
|
26
121
|
```
|
|
27
122
|
|
|
28
123
|
Creates worktree at `~/code/project--feature-authentication/` with:
|
|
29
|
-
-
|
|
124
|
+
- Files copied per `.worktreeinclude`
|
|
125
|
+
- Env vars rewritten with branch suffix
|
|
126
|
+
- Docker port offsets applied
|
|
127
|
+
- Database created
|
|
30
128
|
- Dependencies installed
|
|
31
129
|
|
|
32
130
|
### List Worktrees
|
|
@@ -43,6 +141,8 @@ Creates worktree at `~/code/project--feature-authentication/` with:
|
|
|
43
141
|
/wt rm feat-payments -d # also delete branch
|
|
44
142
|
```
|
|
45
143
|
|
|
144
|
+
Stops Docker containers before removing. Databases are preserved (drop manually if no longer needed).
|
|
145
|
+
|
|
46
146
|
### Open in Editor
|
|
47
147
|
|
|
48
148
|
```bash
|
|
@@ -73,12 +173,6 @@ Priority: flag > `WT_EDITOR` > auto-detect (Cursor → Antigravity → VS Code)
|
|
|
73
173
|
|
|
74
174
|
## Auto-Setup Details
|
|
75
175
|
|
|
76
|
-
### Environment Files
|
|
77
|
-
|
|
78
|
-
Copies from:
|
|
79
|
-
- Root: `.env`, `.env.local`, `.env.development`, etc.
|
|
80
|
-
- Nested: `apps/*/.env*`, `packages/*/.env*`, `services/*/.env*`
|
|
81
|
-
|
|
82
176
|
### Package Manager Detection
|
|
83
177
|
|
|
84
178
|
Checks in order:
|
|
@@ -96,6 +190,19 @@ Detects via:
|
|
|
96
190
|
- `lerna.json`
|
|
97
191
|
- `"workspaces"` in `package.json`
|
|
98
192
|
|
|
193
|
+
### Compose File Detection Order
|
|
194
|
+
|
|
195
|
+
When `[docker] auto` is set and no `file=` specified:
|
|
196
|
+
|
|
197
|
+
1. `local.yml`
|
|
198
|
+
2. `docker-compose.local.yml`
|
|
199
|
+
3. `docker-compose.yml`
|
|
200
|
+
4. `docker-compose.yaml`
|
|
201
|
+
5. `compose.yml`
|
|
202
|
+
6. `compose.yaml`
|
|
203
|
+
|
|
204
|
+
Also searches `apps/*/` and `services/*/` subdirectories.
|
|
205
|
+
|
|
99
206
|
## Skill Triggers
|
|
100
207
|
|
|
101
208
|
The worktree-manager skill auto-activates when you mention:
|
|
@@ -104,6 +211,8 @@ The worktree-manager skill auto-activates when you mention:
|
|
|
104
211
|
- "feature branch setup"
|
|
105
212
|
- "work on multiple branches"
|
|
106
213
|
- "separate workspace for branch"
|
|
214
|
+
- "docker port conflict" or "database isolation"
|
|
215
|
+
- "worktreeinclude" or "env isolation"
|
|
107
216
|
|
|
108
217
|
## License
|
|
109
218
|
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: wt
|
|
3
|
-
description: Git worktree manager - create, list, remove, and open worktrees with
|
|
3
|
+
description: Git worktree manager - create, list, remove, and open worktrees with env isolation, Docker port offsets, and database auto-creation
|
|
4
4
|
argument-hint: '<command> [branch] [options]'
|
|
5
5
|
allowed-tools: ["Bash(${CLAUDE_PLUGIN_ROOT}/scripts/wt.sh:*)"]
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Git Worktree Manager
|
|
9
9
|
|
|
10
|
-
Manage git worktrees with automatic
|
|
10
|
+
Manage git worktrees with automatic isolation for parallel development.
|
|
11
11
|
|
|
12
12
|
## Usage
|
|
13
13
|
|
|
@@ -19,9 +19,9 @@ Manage git worktrees with automatic setup for parallel development.
|
|
|
19
19
|
|
|
20
20
|
| Command | Alias | Description |
|
|
21
21
|
|---------|-------|-------------|
|
|
22
|
-
| `create <branch>` | `c` | Create worktree
|
|
22
|
+
| `create <branch>` | `c` | Create worktree with isolation (env, docker, db) |
|
|
23
23
|
| `list` | `ls` | List all worktrees for current repo |
|
|
24
|
-
| `remove <branch>` | `rm` | Remove worktree (add `-d` to delete branch) |
|
|
24
|
+
| `remove <branch>` | `rm` | Remove worktree (stops Docker, add `-d` to delete branch) |
|
|
25
25
|
| `open <branch> [--editor]` | `o` | Open worktree (--cursor\|-c, --agy\|-a, --code\|-v) |
|
|
26
26
|
|
|
27
27
|
## Instructions
|
|
@@ -35,7 +35,7 @@ Run the worktree manager script:
|
|
|
35
35
|
## Examples
|
|
36
36
|
|
|
37
37
|
```bash
|
|
38
|
-
# Create worktree for new feature
|
|
38
|
+
# Create worktree for new feature (with full isolation)
|
|
39
39
|
/wt create feature/authentication
|
|
40
40
|
|
|
41
41
|
# Short form
|
|
@@ -44,7 +44,7 @@ Run the worktree manager script:
|
|
|
44
44
|
# List all worktrees
|
|
45
45
|
/wt ls
|
|
46
46
|
|
|
47
|
-
# Remove worktree
|
|
47
|
+
# Remove worktree (stops Docker containers first)
|
|
48
48
|
/wt rm feature/authentication
|
|
49
49
|
|
|
50
50
|
# Remove worktree AND delete branch
|
|
@@ -67,7 +67,27 @@ Run the worktree manager script:
|
|
|
67
67
|
|
|
68
68
|
## Auto-Setup on Create
|
|
69
69
|
|
|
70
|
-
1.
|
|
71
|
-
2. Copies
|
|
72
|
-
3.
|
|
73
|
-
4.
|
|
70
|
+
1. Loads `.worktreeinclude` (generates default if missing)
|
|
71
|
+
2. Copies files matching glob patterns
|
|
72
|
+
3. Rewrites env vars (DB_NAME, DATABASE_URL, etc.) with branch suffix
|
|
73
|
+
4. Generates `docker-compose.worktree.yml` with port offsets
|
|
74
|
+
5. Auto-creates database in running Postgres
|
|
75
|
+
6. Detects package manager (bun → pnpm → yarn → npm)
|
|
76
|
+
7. Runs install at root (monorepo-aware)
|
|
77
|
+
|
|
78
|
+
## `.worktreeinclude` Format
|
|
79
|
+
|
|
80
|
+
```ini
|
|
81
|
+
# File patterns to copy
|
|
82
|
+
.env*
|
|
83
|
+
apps/*/.env*
|
|
84
|
+
|
|
85
|
+
[rewrite]
|
|
86
|
+
auto # suffix DB_NAME, DATABASE_URL, etc.
|
|
87
|
+
# MY_DB=app_{{BRANCH}} # template mode
|
|
88
|
+
|
|
89
|
+
[docker]
|
|
90
|
+
auto # auto-detect compose file
|
|
91
|
+
# file=apps/backend/docker-compose.local.yml
|
|
92
|
+
# port_offset=10
|
|
93
|
+
```
|
|
@@ -11,6 +11,7 @@ RED='\033[0;31m'
|
|
|
11
11
|
GREEN='\033[0;32m'
|
|
12
12
|
YELLOW='\033[1;33m'
|
|
13
13
|
BLUE='\033[0;34m'
|
|
14
|
+
CYAN='\033[0;36m'
|
|
14
15
|
NC='\033[0m' # No Color
|
|
15
16
|
|
|
16
17
|
# Helpers
|
|
@@ -58,66 +59,574 @@ detect_pkg_manager() {
|
|
|
58
59
|
# Detect if monorepo
|
|
59
60
|
is_monorepo() {
|
|
60
61
|
local dir="${1:-$REPO_ROOT}"
|
|
61
|
-
# Check for monorepo indicators
|
|
62
62
|
[[ -f "$dir/pnpm-workspace.yaml" ]] && return 0
|
|
63
63
|
[[ -f "$dir/turbo.json" ]] && return 0
|
|
64
64
|
[[ -f "$dir/nx.json" ]] && return 0
|
|
65
65
|
[[ -f "$dir/lerna.json" ]] && return 0
|
|
66
|
-
# Check for workspaces in package.json
|
|
67
66
|
if [[ -f "$dir/package.json" ]]; then
|
|
68
67
|
grep -q '"workspaces"' "$dir/package.json" 2>/dev/null && return 0
|
|
69
68
|
fi
|
|
70
69
|
return 1
|
|
71
70
|
}
|
|
72
71
|
|
|
73
|
-
#
|
|
74
|
-
|
|
72
|
+
# Get worktree path from branch name
|
|
73
|
+
get_worktree_path() {
|
|
74
|
+
local branch="$1"
|
|
75
|
+
local safe_branch="${branch//\//-}"
|
|
76
|
+
echo "${REPO_PARENT}/${REPO_NAME}--${safe_branch}"
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# Strip main repo name prefix from worktree basename
|
|
80
|
+
get_branch_from_worktree() {
|
|
81
|
+
local wt_path="$1"
|
|
82
|
+
local wt_name=$(basename "$wt_path")
|
|
83
|
+
echo "${wt_name#${REPO_NAME}--}"
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
# =============================================================================
|
|
87
|
+
# Phase 1: .worktreeinclude Parsing & File Copying
|
|
88
|
+
# =============================================================================
|
|
89
|
+
|
|
90
|
+
# Generate default .worktreeinclude if it doesn't exist
|
|
91
|
+
generate_default_worktreeinclude() {
|
|
92
|
+
local target="$REPO_ROOT/.worktreeinclude"
|
|
93
|
+
[[ -f "$target" ]] && return 0
|
|
94
|
+
|
|
95
|
+
cat > "$target" << 'TEMPLATE'
|
|
96
|
+
# .worktreeinclude — Files to copy to new worktrees
|
|
97
|
+
# Commit this file so your team shares the same config
|
|
98
|
+
.env*
|
|
99
|
+
|
|
100
|
+
# Monorepo (uncomment as needed)
|
|
101
|
+
# apps/*/.env*
|
|
102
|
+
# packages/*/.env*
|
|
103
|
+
# services/*/.env*
|
|
104
|
+
|
|
105
|
+
[rewrite]
|
|
106
|
+
auto
|
|
107
|
+
|
|
108
|
+
[docker]
|
|
109
|
+
auto
|
|
110
|
+
# file=apps/backend/docker-compose.local.yml
|
|
111
|
+
# port_offset=10
|
|
112
|
+
TEMPLATE
|
|
113
|
+
info "Generated .worktreeinclude (commit this file)"
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
# Parse .worktreeinclude into 3 arrays: file patterns, rewrite lines, docker lines
|
|
117
|
+
parse_worktreeinclude() {
|
|
118
|
+
local include_file="$REPO_ROOT/.worktreeinclude"
|
|
119
|
+
WT_FILE_PATTERNS=()
|
|
120
|
+
WT_REWRITE_LINES=()
|
|
121
|
+
WT_DOCKER_LINES=()
|
|
122
|
+
|
|
123
|
+
[[ -f "$include_file" ]] || return 0
|
|
124
|
+
|
|
125
|
+
local current_section="files"
|
|
126
|
+
while IFS= read -r line || [[ -n "$line" ]]; do
|
|
127
|
+
# Strip trailing whitespace
|
|
128
|
+
line="${line%"${line##*[![:space:]]}"}"
|
|
129
|
+
# Skip empty lines and comments
|
|
130
|
+
[[ -z "$line" ]] && continue
|
|
131
|
+
[[ "$line" == \#* ]] && continue
|
|
132
|
+
|
|
133
|
+
# Detect section headers
|
|
134
|
+
if [[ "$line" == "[rewrite]" ]]; then
|
|
135
|
+
current_section="rewrite"
|
|
136
|
+
continue
|
|
137
|
+
elif [[ "$line" == "[docker]" ]]; then
|
|
138
|
+
current_section="docker"
|
|
139
|
+
continue
|
|
140
|
+
fi
|
|
141
|
+
|
|
142
|
+
case "$current_section" in
|
|
143
|
+
files) WT_FILE_PATTERNS+=("$line") ;;
|
|
144
|
+
rewrite) WT_REWRITE_LINES+=("$line") ;;
|
|
145
|
+
docker) WT_DOCKER_LINES+=("$line") ;;
|
|
146
|
+
esac
|
|
147
|
+
done < "$include_file"
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
# Expand glob patterns against main worktree, return matched files (relative paths)
|
|
151
|
+
match_files_by_patterns() {
|
|
152
|
+
local src="$1"
|
|
153
|
+
MATCHED_FILES=()
|
|
154
|
+
|
|
155
|
+
for pattern in "${WT_FILE_PATTERNS[@]}"; do
|
|
156
|
+
# Use subshell with nullglob to safely expand globs
|
|
157
|
+
while IFS= read -r -d '' file; do
|
|
158
|
+
local rel="${file#"$src"/}"
|
|
159
|
+
MATCHED_FILES+=("$rel")
|
|
160
|
+
done < <(
|
|
161
|
+
cd "$src"
|
|
162
|
+
shopt -s nullglob
|
|
163
|
+
shopt -s globstar 2>/dev/null || true # bash 4+ only, needed for **
|
|
164
|
+
for f in $pattern; do
|
|
165
|
+
[[ -f "$f" ]] && printf '%s\0' "$src/$f"
|
|
166
|
+
done
|
|
167
|
+
)
|
|
168
|
+
done
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
# Copy matched files preserving directory structure
|
|
172
|
+
copy_matched_files() {
|
|
75
173
|
local src="$1"
|
|
76
174
|
local dst="$2"
|
|
77
175
|
local count=0
|
|
78
176
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
[[
|
|
82
|
-
|
|
83
|
-
cp "$envfile" "$dst/$filename"
|
|
177
|
+
for rel in "${MATCHED_FILES[@]+"${MATCHED_FILES[@]}"}"; do
|
|
178
|
+
local dir=$(dirname "$rel")
|
|
179
|
+
[[ "$dir" != "." ]] && mkdir -p "$dst/$dir"
|
|
180
|
+
cp "$src/$rel" "$dst/$rel"
|
|
84
181
|
((count++))
|
|
85
182
|
done
|
|
86
183
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
184
|
+
echo "$count"
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
# =============================================================================
|
|
188
|
+
# Phase 2: Env Var Rewriting
|
|
189
|
+
# =============================================================================
|
|
190
|
+
|
|
191
|
+
# Branch name → safe slug for suffixing (lowercase, special chars → _, max 30)
|
|
192
|
+
sanitize_branch_for_suffix() {
|
|
193
|
+
local branch="$1"
|
|
194
|
+
local slug
|
|
195
|
+
slug=$(printf '%s' "$branch" | tr '[:upper:]' '[:lower:]' | tr -c '[:alnum:]' '_')
|
|
196
|
+
# Trim leading/trailing underscores
|
|
197
|
+
slug="${slug#_}"
|
|
198
|
+
slug="${slug%_}"
|
|
199
|
+
# Truncate to 30 chars
|
|
200
|
+
printf '%s' "${slug:0:30}"
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
# Rewrite a single .env file — auto-detect known keys + template {{BRANCH}}
|
|
204
|
+
rewrite_env_file() {
|
|
205
|
+
local env_file="$1"
|
|
206
|
+
local branch_slug="$2"
|
|
207
|
+
local has_auto=false
|
|
208
|
+
|
|
209
|
+
# Check if auto mode is enabled
|
|
210
|
+
for line in "${WT_REWRITE_LINES[@]}"; do
|
|
211
|
+
[[ "$line" == "auto" ]] && has_auto=true
|
|
212
|
+
done
|
|
213
|
+
|
|
214
|
+
local tmpfile
|
|
215
|
+
tmpfile=$(mktemp)
|
|
216
|
+
local changed=false
|
|
217
|
+
|
|
218
|
+
while IFS= read -r line || [[ -n "$line" ]]; do
|
|
219
|
+
local newline="$line"
|
|
220
|
+
|
|
221
|
+
# Template: replace {{BRANCH}} placeholders
|
|
222
|
+
if [[ "$line" == *'{{BRANCH}}'* ]]; then
|
|
223
|
+
newline="${line//\{\{BRANCH\}\}/$branch_slug}"
|
|
224
|
+
changed=true
|
|
225
|
+
elif [[ "$has_auto" == true ]]; then
|
|
226
|
+
# Auto-detect known keys (only lines with = that aren't comments)
|
|
227
|
+
if [[ "$line" =~ ^[[:space:]]*([A-Za-z_][A-Za-z0-9_]*)=(.*) ]]; then
|
|
228
|
+
local key="${BASH_REMATCH[1]}"
|
|
229
|
+
local val="${BASH_REMATCH[2]}"
|
|
230
|
+
# Strip surrounding quotes from val for processing
|
|
231
|
+
local raw_val="$val"
|
|
232
|
+
raw_val="${raw_val#\"}"
|
|
233
|
+
raw_val="${raw_val%\"}"
|
|
234
|
+
raw_val="${raw_val#\'}"
|
|
235
|
+
raw_val="${raw_val%\'}"
|
|
236
|
+
|
|
237
|
+
case "$key" in
|
|
238
|
+
DB_NAME|POSTGRES_DB|MYSQL_DATABASE|DATABASE_NAME)
|
|
239
|
+
# Suffix the value
|
|
240
|
+
newline="${key}=${raw_val}_${branch_slug}"
|
|
241
|
+
changed=true
|
|
242
|
+
;;
|
|
243
|
+
DATABASE_URL|POSTGRES_URL)
|
|
244
|
+
# Parse URL: scheme://user:pass@host:port/dbname?params
|
|
245
|
+
# Suffix the database name portion
|
|
246
|
+
if [[ "$raw_val" =~ ^(.*://[^/]*/?)([^?]+)(.*) ]]; then
|
|
247
|
+
local prefix="${BASH_REMATCH[1]}"
|
|
248
|
+
local dbname="${BASH_REMATCH[2]}"
|
|
249
|
+
local suffix="${BASH_REMATCH[3]}"
|
|
250
|
+
newline="${key}=${prefix}${dbname}_${branch_slug}${suffix}"
|
|
251
|
+
changed=true
|
|
252
|
+
fi
|
|
253
|
+
;;
|
|
254
|
+
COMPOSE_PROJECT_NAME)
|
|
255
|
+
newline="${key}=${raw_val}_${branch_slug}"
|
|
256
|
+
changed=true
|
|
257
|
+
;;
|
|
258
|
+
esac
|
|
259
|
+
fi
|
|
260
|
+
fi
|
|
261
|
+
|
|
262
|
+
printf '%s\n' "$newline"
|
|
263
|
+
done < "$env_file" > "$tmpfile"
|
|
264
|
+
|
|
265
|
+
if [[ "$changed" == true ]]; then
|
|
266
|
+
mv "$tmpfile" "$env_file"
|
|
267
|
+
else
|
|
268
|
+
rm -f "$tmpfile"
|
|
269
|
+
fi
|
|
270
|
+
|
|
271
|
+
echo "$changed"
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
# Find and rewrite all .env* files in the worktree
|
|
275
|
+
rewrite_all_env_files() {
|
|
276
|
+
local wt_path="$1"
|
|
277
|
+
local branch_slug="$2"
|
|
278
|
+
local count=0
|
|
279
|
+
|
|
280
|
+
# Only proceed if there are rewrite lines configured
|
|
281
|
+
[[ ${#WT_REWRITE_LINES[@]} -gt 0 ]] || return 0
|
|
282
|
+
|
|
283
|
+
while IFS= read -r -d '' env_file; do
|
|
284
|
+
local basename_f
|
|
285
|
+
basename_f=$(basename "$env_file")
|
|
286
|
+
# Only process .env* files (not .envrc or similar non-env files)
|
|
287
|
+
[[ "$basename_f" == .env* ]] || continue
|
|
288
|
+
local result
|
|
289
|
+
result=$(rewrite_env_file "$env_file" "$branch_slug")
|
|
290
|
+
[[ "$result" == "true" ]] && ((count++))
|
|
291
|
+
done < <(find "$wt_path" -name '.env*' -not -path '*/node_modules/*' -not -path '*/.git/*' -print0 2>/dev/null)
|
|
292
|
+
|
|
293
|
+
echo "$count"
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
# =============================================================================
|
|
297
|
+
# Phase 3: Docker Compose Isolation
|
|
298
|
+
# =============================================================================
|
|
299
|
+
|
|
300
|
+
# Deterministic port offset from branch name (1-100 range via cksum)
|
|
301
|
+
branch_to_port_offset() {
|
|
302
|
+
local branch="$1"
|
|
303
|
+
local hash
|
|
304
|
+
hash=$(printf '%s' "$branch" | cksum | awk '{print $1}')
|
|
305
|
+
echo $(( (hash % 100) + 1 ))
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
# Minimal YAML parser: extract host ports from docker-compose services
|
|
309
|
+
# Output format: service_name:host_port:container_port (one per line)
|
|
310
|
+
parse_docker_compose_ports() {
|
|
311
|
+
local compose_file="$1"
|
|
312
|
+
awk '
|
|
313
|
+
/^[[:space:]]*[a-zA-Z_][a-zA-Z0-9_-]*:/ && !/^\s*-/ && !/ports:/ && !/image:/ && !/container_name:/ {
|
|
314
|
+
# Top-level or nested service name
|
|
315
|
+
indent = 0
|
|
316
|
+
for (i = 1; i <= length($0); i++) {
|
|
317
|
+
if (substr($0, i, 1) == " ") indent++
|
|
318
|
+
else break
|
|
319
|
+
}
|
|
320
|
+
line = $0
|
|
321
|
+
gsub(/^[[:space:]]+/, "", line)
|
|
322
|
+
gsub(/:.*/, "", line)
|
|
323
|
+
if (indent <= 4) current_service = line
|
|
324
|
+
}
|
|
325
|
+
/ports:/ {
|
|
326
|
+
in_ports = 1
|
|
327
|
+
next
|
|
328
|
+
}
|
|
329
|
+
in_ports && /^[[:space:]]*-[[:space:]]*"?[0-9]/ {
|
|
330
|
+
line = $0
|
|
331
|
+
gsub(/^[[:space:]]*-[[:space:]]*/, "", line)
|
|
332
|
+
gsub(/"/, "", line)
|
|
333
|
+
gsub(/[[:space:]].*/, "", line)
|
|
334
|
+
# line is now like "5432:5432" or "8025:8025"
|
|
335
|
+
split(line, parts, ":")
|
|
336
|
+
if (length(parts) >= 2) {
|
|
337
|
+
print current_service ":" parts[1] ":" parts[2]
|
|
338
|
+
}
|
|
339
|
+
next
|
|
340
|
+
}
|
|
341
|
+
in_ports && /^[[:space:]]*[^-[:space:]]/ {
|
|
342
|
+
in_ports = 0
|
|
343
|
+
}
|
|
344
|
+
' "$compose_file"
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
# Detect compose file path — checks [docker] file= directive, then auto-detects
|
|
348
|
+
detect_compose_file() {
|
|
349
|
+
local wt_path="$1"
|
|
350
|
+
local compose_file=""
|
|
351
|
+
|
|
352
|
+
# Check for file= directive in [docker] config
|
|
353
|
+
for line in "${WT_DOCKER_LINES[@]}"; do
|
|
354
|
+
if [[ "$line" =~ ^file=(.+) ]]; then
|
|
355
|
+
local specified="${BASH_REMATCH[1]}"
|
|
356
|
+
specified="${specified#"${specified%%[![:space:]]*}"}" # trim leading
|
|
357
|
+
specified="${specified%"${specified##*[![:space:]]}"}" # trim trailing
|
|
358
|
+
if [[ -f "$wt_path/$specified" ]]; then
|
|
359
|
+
echo "$specified"
|
|
360
|
+
return 0
|
|
361
|
+
else
|
|
362
|
+
warn "Specified compose file not found: $specified"
|
|
363
|
+
fi
|
|
364
|
+
fi
|
|
365
|
+
done
|
|
366
|
+
|
|
367
|
+
# Auto-detect in standard locations
|
|
368
|
+
local candidates=(
|
|
369
|
+
"local.yml"
|
|
370
|
+
"docker-compose.local.yml"
|
|
371
|
+
"docker-compose.yml"
|
|
372
|
+
"docker-compose.yaml"
|
|
373
|
+
"compose.yml"
|
|
374
|
+
"compose.yaml"
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
# Check root first
|
|
378
|
+
for candidate in "${candidates[@]}"; do
|
|
379
|
+
if [[ -f "$wt_path/$candidate" ]]; then
|
|
380
|
+
echo "$candidate"
|
|
381
|
+
return 0
|
|
382
|
+
fi
|
|
383
|
+
done
|
|
384
|
+
|
|
385
|
+
# Check nested app directories
|
|
386
|
+
for subdir in apps services; do
|
|
387
|
+
[[ -d "$wt_path/$subdir" ]] || continue
|
|
388
|
+
for app_dir in "$wt_path/$subdir"/*/; do
|
|
91
389
|
[[ -d "$app_dir" ]] || continue
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
cp "$envfile" "$dst/$subdir/$app_name/$filename"
|
|
99
|
-
((count++))
|
|
390
|
+
for candidate in "${candidates[@]}"; do
|
|
391
|
+
if [[ -f "$app_dir$candidate" ]]; then
|
|
392
|
+
local rel="${app_dir#"$wt_path"/}$candidate"
|
|
393
|
+
echo "$rel"
|
|
394
|
+
return 0
|
|
395
|
+
fi
|
|
100
396
|
done
|
|
101
397
|
done
|
|
102
398
|
done
|
|
103
399
|
|
|
104
|
-
|
|
400
|
+
return 1
|
|
105
401
|
}
|
|
106
402
|
|
|
107
|
-
#
|
|
108
|
-
|
|
109
|
-
local
|
|
110
|
-
|
|
111
|
-
local
|
|
112
|
-
|
|
403
|
+
# Generate docker-compose.worktree.yml override with port offsets
|
|
404
|
+
generate_docker_compose_override() {
|
|
405
|
+
local compose_file="$1" # full path to original compose file
|
|
406
|
+
local offset="$2"
|
|
407
|
+
local override_dir
|
|
408
|
+
override_dir=$(dirname "$compose_file")
|
|
409
|
+
local override_file="$override_dir/docker-compose.worktree.yml"
|
|
410
|
+
|
|
411
|
+
local ports_data
|
|
412
|
+
ports_data=$(parse_docker_compose_ports "$compose_file")
|
|
413
|
+
|
|
414
|
+
[[ -z "$ports_data" ]] && return 1
|
|
415
|
+
|
|
416
|
+
local tmpfile
|
|
417
|
+
tmpfile=$(mktemp)
|
|
418
|
+
|
|
419
|
+
cat > "$tmpfile" << 'HEADER'
|
|
420
|
+
# Auto-generated by wt — DO NOT EDIT
|
|
421
|
+
# Port offsets for worktree isolation
|
|
422
|
+
services:
|
|
423
|
+
HEADER
|
|
424
|
+
|
|
425
|
+
local current_service=""
|
|
426
|
+
while IFS=: read -r service host_port container_port; do
|
|
427
|
+
[[ -z "$service" ]] && continue
|
|
428
|
+
local new_port=$((host_port + offset))
|
|
429
|
+
|
|
430
|
+
if [[ "$service" != "$current_service" ]]; then
|
|
431
|
+
printf ' %s:\n' "$service" >> "$tmpfile"
|
|
432
|
+
printf ' ports:\n' >> "$tmpfile"
|
|
433
|
+
current_service="$service"
|
|
434
|
+
fi
|
|
435
|
+
printf ' - "%d:%s"\n' "$new_port" "$container_port" >> "$tmpfile"
|
|
436
|
+
done <<< "$ports_data"
|
|
437
|
+
|
|
438
|
+
mv "$tmpfile" "$override_file"
|
|
439
|
+
echo "$override_file"
|
|
113
440
|
}
|
|
114
441
|
|
|
115
|
-
#
|
|
116
|
-
|
|
442
|
+
# Orchestrate Docker isolation: detect, offset, generate override, update .env
|
|
443
|
+
setup_docker_isolation() {
|
|
117
444
|
local wt_path="$1"
|
|
118
|
-
local
|
|
119
|
-
|
|
120
|
-
|
|
445
|
+
local branch="$2"
|
|
446
|
+
local branch_slug="$3"
|
|
447
|
+
|
|
448
|
+
# Check if docker section has 'auto' or any config
|
|
449
|
+
local has_docker=false
|
|
450
|
+
for line in "${WT_DOCKER_LINES[@]}"; do
|
|
451
|
+
[[ "$line" == "auto" || "$line" =~ ^file= ]] && has_docker=true
|
|
452
|
+
done
|
|
453
|
+
[[ "$has_docker" == true ]] || return 0
|
|
454
|
+
|
|
455
|
+
local compose_rel
|
|
456
|
+
compose_rel=$(detect_compose_file "$wt_path") || {
|
|
457
|
+
warn "No docker-compose file found, skipping Docker isolation"
|
|
458
|
+
return 0
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
local compose_full="$wt_path/$compose_rel"
|
|
462
|
+
local offset
|
|
463
|
+
offset=$(branch_to_port_offset "$branch")
|
|
464
|
+
|
|
465
|
+
# Check for custom port_offset
|
|
466
|
+
for line in "${WT_DOCKER_LINES[@]}"; do
|
|
467
|
+
if [[ "$line" =~ ^port_offset=([0-9]+) ]]; then
|
|
468
|
+
offset="${BASH_REMATCH[1]}"
|
|
469
|
+
fi
|
|
470
|
+
done
|
|
471
|
+
|
|
472
|
+
info "Docker isolation: $compose_rel (port offset +$offset)"
|
|
473
|
+
|
|
474
|
+
local override_file
|
|
475
|
+
override_file=$(generate_docker_compose_override "$compose_full" "$offset") || {
|
|
476
|
+
warn "No ports found in compose file, skipping port override"
|
|
477
|
+
return 0
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
local override_rel="${override_file#"$wt_path"/}"
|
|
481
|
+
success "Generated $override_rel"
|
|
482
|
+
|
|
483
|
+
# Set COMPOSE_FILE and COMPOSE_PROJECT_NAME in root .env
|
|
484
|
+
local root_env="$wt_path/.env"
|
|
485
|
+
local compose_file_val="${compose_rel}:${override_rel}"
|
|
486
|
+
local project_name="${REPO_NAME}_${branch_slug}"
|
|
487
|
+
|
|
488
|
+
# Append or update COMPOSE_FILE in .env
|
|
489
|
+
if [[ -f "$root_env" ]]; then
|
|
490
|
+
local tmpfile
|
|
491
|
+
tmpfile=$(mktemp)
|
|
492
|
+
local found_cf=false
|
|
493
|
+
local found_cpn=false
|
|
494
|
+
while IFS= read -r line || [[ -n "$line" ]]; do
|
|
495
|
+
if [[ "$line" =~ ^COMPOSE_FILE= ]]; then
|
|
496
|
+
printf 'COMPOSE_FILE=%s\n' "$compose_file_val"
|
|
497
|
+
found_cf=true
|
|
498
|
+
elif [[ "$line" =~ ^COMPOSE_PROJECT_NAME= ]]; then
|
|
499
|
+
printf 'COMPOSE_PROJECT_NAME=%s\n' "$project_name"
|
|
500
|
+
found_cpn=true
|
|
501
|
+
else
|
|
502
|
+
printf '%s\n' "$line"
|
|
503
|
+
fi
|
|
504
|
+
done < "$root_env" > "$tmpfile"
|
|
505
|
+
[[ "$found_cf" == false ]] && printf 'COMPOSE_FILE=%s\n' "$compose_file_val" >> "$tmpfile"
|
|
506
|
+
[[ "$found_cpn" == false ]] && printf 'COMPOSE_PROJECT_NAME=%s\n' "$project_name" >> "$tmpfile"
|
|
507
|
+
mv "$tmpfile" "$root_env"
|
|
508
|
+
else
|
|
509
|
+
printf 'COMPOSE_FILE=%s\n' "$compose_file_val" > "$root_env"
|
|
510
|
+
printf 'COMPOSE_PROJECT_NAME=%s\n' "$project_name" >> "$root_env"
|
|
511
|
+
fi
|
|
512
|
+
|
|
513
|
+
# Print port mapping summary
|
|
514
|
+
local ports_data
|
|
515
|
+
ports_data=$(parse_docker_compose_ports "$compose_full")
|
|
516
|
+
if [[ -n "$ports_data" ]]; then
|
|
517
|
+
echo ""
|
|
518
|
+
echo -e " ${CYAN}Port mappings:${NC}"
|
|
519
|
+
while IFS=: read -r service host_port container_port; do
|
|
520
|
+
[[ -z "$service" ]] && continue
|
|
521
|
+
local new_port=$((host_port + offset))
|
|
522
|
+
echo -e " ${service}: ${host_port} → ${GREEN}${new_port}${NC}"
|
|
523
|
+
done <<< "$ports_data"
|
|
524
|
+
fi
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
# =============================================================================
|
|
528
|
+
# Phase 4: Database Auto-Creation
|
|
529
|
+
# =============================================================================
|
|
530
|
+
|
|
531
|
+
# Detect Postgres — checks Docker containers first, then local
|
|
532
|
+
detect_postgres() {
|
|
533
|
+
# Check Docker containers for postgres
|
|
534
|
+
if command -v docker &>/dev/null; then
|
|
535
|
+
local pg_container
|
|
536
|
+
pg_container=$(docker ps --filter "ancestor=postgres" --filter "status=running" --format '{{.Names}}' 2>/dev/null | head -1)
|
|
537
|
+
if [[ -n "$pg_container" ]]; then
|
|
538
|
+
echo "docker:$pg_container"
|
|
539
|
+
return 0
|
|
540
|
+
fi
|
|
541
|
+
# Also check by port binding (for custom images)
|
|
542
|
+
pg_container=$(docker ps --filter "publish=5432" --filter "status=running" --format '{{.Names}}' 2>/dev/null | head -1)
|
|
543
|
+
if [[ -n "$pg_container" ]]; then
|
|
544
|
+
echo "docker:$pg_container"
|
|
545
|
+
return 0
|
|
546
|
+
fi
|
|
547
|
+
fi
|
|
548
|
+
|
|
549
|
+
# Check local postgres
|
|
550
|
+
if command -v pg_isready &>/dev/null && pg_isready -q 2>/dev/null; then
|
|
551
|
+
echo "local"
|
|
552
|
+
return 0
|
|
553
|
+
fi
|
|
554
|
+
|
|
555
|
+
return 1
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
# Create database if it doesn't exist (idempotent)
|
|
559
|
+
create_database_if_not_exists() {
|
|
560
|
+
local db_name="$1"
|
|
561
|
+
local pg_source="$2" # "docker:container_name" or "local"
|
|
562
|
+
|
|
563
|
+
local exists_query="SELECT 1 FROM pg_database WHERE datname='$db_name'"
|
|
564
|
+
local create_query="CREATE DATABASE \"$db_name\""
|
|
565
|
+
local result=""
|
|
566
|
+
|
|
567
|
+
if [[ "$pg_source" == local ]]; then
|
|
568
|
+
result=$(psql -tAc "$exists_query" postgres 2>/dev/null || true)
|
|
569
|
+
if [[ "$result" != "1" ]]; then
|
|
570
|
+
psql -c "$create_query" postgres 2>/dev/null && return 0 || return 1
|
|
571
|
+
fi
|
|
572
|
+
elif [[ "$pg_source" == docker:* ]]; then
|
|
573
|
+
local container="${pg_source#docker:}"
|
|
574
|
+
result=$(docker exec "$container" psql -U postgres -tAc "$exists_query" postgres 2>/dev/null || true)
|
|
575
|
+
if [[ "$result" != "1" ]]; then
|
|
576
|
+
docker exec "$container" psql -U postgres -c "$create_query" postgres 2>/dev/null && return 0 || return 1
|
|
577
|
+
fi
|
|
578
|
+
fi
|
|
579
|
+
|
|
580
|
+
return 0 # Already exists
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
# Read rewritten DB name from .env files and auto-create databases
|
|
584
|
+
auto_create_databases() {
|
|
585
|
+
local wt_path="$1"
|
|
586
|
+
local pg_source
|
|
587
|
+
|
|
588
|
+
pg_source=$(detect_postgres) || {
|
|
589
|
+
warn "Postgres not running, skipping database auto-creation"
|
|
590
|
+
return 0
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
# Collect unique database names from all .env files
|
|
594
|
+
local db_names=()
|
|
595
|
+
while IFS= read -r -d '' env_file; do
|
|
596
|
+
while IFS= read -r line; do
|
|
597
|
+
if [[ "$line" =~ ^(DB_NAME|POSTGRES_DB|MYSQL_DATABASE|DATABASE_NAME)=(.+) ]]; then
|
|
598
|
+
local val="${BASH_REMATCH[2]}"
|
|
599
|
+
val="${val#\"}" ; val="${val%\"}"
|
|
600
|
+
val="${val#\'}" ; val="${val%\'}"
|
|
601
|
+
# Only add if not already in array
|
|
602
|
+
local found=false
|
|
603
|
+
for existing in "${db_names[@]+"${db_names[@]}"}"; do
|
|
604
|
+
[[ "$existing" == "$val" ]] && found=true
|
|
605
|
+
done
|
|
606
|
+
[[ "$found" == false ]] && db_names+=("$val")
|
|
607
|
+
elif [[ "$line" =~ ^(DATABASE_URL|POSTGRES_URL)=(.+) ]]; then
|
|
608
|
+
local url="${BASH_REMATCH[2]}"
|
|
609
|
+
url="${url#\"}" ; url="${url%\"}"
|
|
610
|
+
# Extract db name from URL: scheme://user:pass@host:port/dbname
|
|
611
|
+
if [[ "$url" =~ ://[^/]*/([^?]+) ]]; then
|
|
612
|
+
local val="${BASH_REMATCH[1]}"
|
|
613
|
+
local found=false
|
|
614
|
+
for existing in "${db_names[@]+"${db_names[@]}"}"; do
|
|
615
|
+
[[ "$existing" == "$val" ]] && found=true
|
|
616
|
+
done
|
|
617
|
+
[[ "$found" == false ]] && db_names+=("$val")
|
|
618
|
+
fi
|
|
619
|
+
fi
|
|
620
|
+
done < "$env_file"
|
|
621
|
+
done < <(find "$wt_path" -maxdepth 3 -name '.env*' -not -path '*/node_modules/*' -not -path '*/.git/*' -print0 2>/dev/null)
|
|
622
|
+
|
|
623
|
+
for db_name in "${db_names[@]+"${db_names[@]}"}"; do
|
|
624
|
+
if create_database_if_not_exists "$db_name" "$pg_source"; then
|
|
625
|
+
success "Database ready: $db_name"
|
|
626
|
+
else
|
|
627
|
+
warn "Could not create database: $db_name"
|
|
628
|
+
fi
|
|
629
|
+
done
|
|
121
630
|
}
|
|
122
631
|
|
|
123
632
|
# =============================================================================
|
|
@@ -141,9 +650,9 @@ cmd_create() {
|
|
|
141
650
|
exit 1
|
|
142
651
|
fi
|
|
143
652
|
|
|
653
|
+
# --- Step 1: Create git worktree ---
|
|
144
654
|
info "Creating worktree for branch: $branch"
|
|
145
655
|
|
|
146
|
-
# Check if branch exists (local or remote)
|
|
147
656
|
if git show-ref --verify --quiet "refs/heads/$branch" 2>/dev/null; then
|
|
148
657
|
info "Checking out existing local branch: $branch"
|
|
149
658
|
git worktree add "$wt_path" "$branch"
|
|
@@ -156,16 +665,47 @@ cmd_create() {
|
|
|
156
665
|
fi
|
|
157
666
|
success "Worktree created at: $wt_path"
|
|
158
667
|
|
|
159
|
-
#
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
668
|
+
# --- Step 2: Load .worktreeinclude ---
|
|
669
|
+
generate_default_worktreeinclude
|
|
670
|
+
parse_worktreeinclude
|
|
671
|
+
|
|
672
|
+
# --- Step 3: Copy files matching patterns ---
|
|
673
|
+
if [[ ${#WT_FILE_PATTERNS[@]} -gt 0 ]]; then
|
|
674
|
+
info "Copying files from .worktreeinclude..."
|
|
675
|
+
match_files_by_patterns "$REPO_ROOT"
|
|
676
|
+
local file_count
|
|
677
|
+
file_count=$(copy_matched_files "$REPO_ROOT" "$wt_path")
|
|
678
|
+
if [[ "$file_count" -gt 0 ]]; then
|
|
679
|
+
success "Copied $file_count file(s)"
|
|
680
|
+
else
|
|
681
|
+
warn "No matching files found to copy"
|
|
682
|
+
fi
|
|
166
683
|
fi
|
|
167
684
|
|
|
168
|
-
#
|
|
685
|
+
# --- Step 4: Rewrite env vars ---
|
|
686
|
+
local branch_slug
|
|
687
|
+
branch_slug=$(sanitize_branch_for_suffix "$branch")
|
|
688
|
+
|
|
689
|
+
if [[ ${#WT_REWRITE_LINES[@]} -gt 0 ]]; then
|
|
690
|
+
info "Rewriting env vars (suffix: _$branch_slug)..."
|
|
691
|
+
local rewrite_count
|
|
692
|
+
rewrite_count=$(rewrite_all_env_files "$wt_path" "$branch_slug")
|
|
693
|
+
if [[ "$rewrite_count" -gt 0 ]]; then
|
|
694
|
+
success "Rewrote $rewrite_count .env file(s)"
|
|
695
|
+
fi
|
|
696
|
+
fi
|
|
697
|
+
|
|
698
|
+
# --- Step 5: Docker Compose isolation ---
|
|
699
|
+
if [[ ${#WT_DOCKER_LINES[@]} -gt 0 ]]; then
|
|
700
|
+
setup_docker_isolation "$wt_path" "$branch" "$branch_slug"
|
|
701
|
+
fi
|
|
702
|
+
|
|
703
|
+
# --- Step 6: Auto-create database ---
|
|
704
|
+
if [[ ${#WT_REWRITE_LINES[@]} -gt 0 ]]; then
|
|
705
|
+
auto_create_databases "$wt_path"
|
|
706
|
+
fi
|
|
707
|
+
|
|
708
|
+
# --- Step 7: Install dependencies ---
|
|
169
709
|
local pkg_mgr=$(detect_pkg_manager "$wt_path")
|
|
170
710
|
if [[ -n "$pkg_mgr" ]]; then
|
|
171
711
|
info "Detected package manager: $pkg_mgr"
|
|
@@ -185,6 +725,7 @@ cmd_create() {
|
|
|
185
725
|
success "Dependencies installed"
|
|
186
726
|
fi
|
|
187
727
|
|
|
728
|
+
# --- Step 8: Summary ---
|
|
188
729
|
echo ""
|
|
189
730
|
success "Worktree ready!"
|
|
190
731
|
echo ""
|
|
@@ -197,10 +738,8 @@ cmd_list() {
|
|
|
197
738
|
info "Worktrees for: $REPO_NAME"
|
|
198
739
|
echo ""
|
|
199
740
|
|
|
200
|
-
# Get all worktrees
|
|
201
741
|
local found=0
|
|
202
742
|
while IFS= read -r line; do
|
|
203
|
-
# Parse worktree output (format: "path HEAD branch")
|
|
204
743
|
local wt_path=$(echo "$line" | awk '{print $1}')
|
|
205
744
|
local wt_branch=$(echo "$line" | awk '{print $3}' | sed 's/\[//;s/\]//')
|
|
206
745
|
|
|
@@ -223,7 +762,6 @@ cmd_remove() {
|
|
|
223
762
|
local branch="${1:-}"
|
|
224
763
|
local delete_branch=false
|
|
225
764
|
|
|
226
|
-
# Check for --delete-branch flag
|
|
227
765
|
if [[ "${2:-}" == "--delete-branch" ]] || [[ "${2:-}" == "-d" ]]; then
|
|
228
766
|
delete_branch=true
|
|
229
767
|
fi
|
|
@@ -243,9 +781,23 @@ cmd_remove() {
|
|
|
243
781
|
exit 1
|
|
244
782
|
fi
|
|
245
783
|
|
|
784
|
+
# Stop Docker containers if compose setup exists
|
|
785
|
+
if [[ -f "$wt_path/.env" ]]; then
|
|
786
|
+
local compose_file_val=""
|
|
787
|
+
while IFS= read -r line; do
|
|
788
|
+
[[ "$line" =~ ^COMPOSE_FILE=(.+) ]] && compose_file_val="${BASH_REMATCH[1]}"
|
|
789
|
+
done < "$wt_path/.env"
|
|
790
|
+
|
|
791
|
+
if [[ -n "$compose_file_val" ]] && command -v docker &>/dev/null; then
|
|
792
|
+
info "Stopping Docker containers..."
|
|
793
|
+
(cd "$wt_path" && docker compose down 2>/dev/null) || warn "Could not stop containers (may not be running)"
|
|
794
|
+
fi
|
|
795
|
+
fi
|
|
796
|
+
|
|
246
797
|
info "Removing worktree: $wt_path"
|
|
247
798
|
git worktree remove "$wt_path" --force
|
|
248
799
|
success "Worktree removed"
|
|
800
|
+
info "Note: databases are preserved — drop manually if no longer needed"
|
|
249
801
|
|
|
250
802
|
if [[ "$delete_branch" == true ]]; then
|
|
251
803
|
info "Deleting branch: $branch"
|
|
@@ -258,7 +810,6 @@ cmd_open() {
|
|
|
258
810
|
local branch=""
|
|
259
811
|
local editor=""
|
|
260
812
|
|
|
261
|
-
# Parse arguments
|
|
262
813
|
while [[ $# -gt 0 ]]; do
|
|
263
814
|
case "$1" in
|
|
264
815
|
--cursor|-c)
|
|
@@ -299,7 +850,6 @@ cmd_open() {
|
|
|
299
850
|
exit 1
|
|
300
851
|
fi
|
|
301
852
|
|
|
302
|
-
# Priority: flag > WT_EDITOR env > auto-detect
|
|
303
853
|
if [[ -z "$editor" && -n "${WT_EDITOR:-}" ]]; then
|
|
304
854
|
editor="$WT_EDITOR"
|
|
305
855
|
fi
|
|
@@ -312,7 +862,6 @@ cmd_open() {
|
|
|
312
862
|
info "Opening in $editor: $wt_path"
|
|
313
863
|
"$editor" "$wt_path"
|
|
314
864
|
else
|
|
315
|
-
# Auto-detect: Cursor → Antigravity → VS Code
|
|
316
865
|
if command -v cursor &>/dev/null; then
|
|
317
866
|
info "Opening in Cursor: $wt_path"
|
|
318
867
|
cursor "$wt_path"
|
|
@@ -336,7 +885,7 @@ cmd_help() {
|
|
|
336
885
|
echo "Usage: wt <command> [args]"
|
|
337
886
|
echo ""
|
|
338
887
|
echo "Commands:"
|
|
339
|
-
echo " create, c <branch> Create worktree
|
|
888
|
+
echo " create, c <branch> Create worktree with isolation (env, docker, db)"
|
|
340
889
|
echo " list, ls List all worktrees for current repo"
|
|
341
890
|
echo " remove, rm <branch> Remove worktree (add -d to delete branch too)"
|
|
342
891
|
echo " open, o <branch> Open worktree (flag > WT_EDITOR > auto-detect)"
|
|
@@ -346,6 +895,17 @@ cmd_help() {
|
|
|
346
895
|
echo "Environment:"
|
|
347
896
|
echo " WT_EDITOR Default editor (cursor, agy, code)"
|
|
348
897
|
echo ""
|
|
898
|
+
echo "Isolation (.worktreeinclude):"
|
|
899
|
+
echo " Auto-generated on first 'wt create' if missing."
|
|
900
|
+
echo " Controls which files to copy and how to isolate services."
|
|
901
|
+
echo ""
|
|
902
|
+
echo " Sections:"
|
|
903
|
+
echo " (top) Glob patterns for files to copy (e.g., .env*)"
|
|
904
|
+
echo " [rewrite] 'auto' to suffix DB_NAME, DATABASE_URL, etc."
|
|
905
|
+
echo " Use {{BRANCH}} for custom templates"
|
|
906
|
+
echo " [docker] 'auto' to generate port-offset override"
|
|
907
|
+
echo " 'file=path' for custom compose file"
|
|
908
|
+
echo ""
|
|
349
909
|
echo "Naming convention:"
|
|
350
910
|
echo " ~/code/project/ → main repo"
|
|
351
911
|
echo " ~/code/project--feat-auth/ → worktree for feat-auth branch"
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: worktree-manager
|
|
3
|
-
description: This skill should be used when the user mentions "worktree", "wt", "new branch workspace", "parallel development", "feature branch setup", "work on multiple branches", "separate workspace for branch", or wants to manage git worktrees for parallel feature development.
|
|
4
|
-
version:
|
|
3
|
+
description: This skill should be used when the user mentions "worktree", "wt", "new branch workspace", "parallel development", "feature branch setup", "work on multiple branches", "separate workspace for branch", "docker port conflict", "database isolation", "worktreeinclude", "env isolation", or wants to manage git worktrees for parallel feature development.
|
|
4
|
+
version: 2.0.0
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Git Worktree Manager Skill
|
|
8
8
|
|
|
9
|
-
Manages git worktrees for parallel development with automatic environment
|
|
9
|
+
Manages git worktrees for parallel development with automatic environment isolation.
|
|
10
10
|
|
|
11
11
|
## Activation Triggers
|
|
12
12
|
|
|
@@ -17,14 +17,17 @@ This skill activates when the user:
|
|
|
17
17
|
- Needs a "separate workspace for a feature"
|
|
18
18
|
- Asks about "parallel development" setup
|
|
19
19
|
- Wants to "set up a new branch workspace"
|
|
20
|
+
- Has "docker port conflicts" between branches
|
|
21
|
+
- Needs "database isolation" for parallel work
|
|
22
|
+
- Asks about ".worktreeinclude" or "env isolation"
|
|
20
23
|
|
|
21
24
|
## Commands Reference
|
|
22
25
|
|
|
23
26
|
| Command | Alias | Description |
|
|
24
27
|
|---------|-------|-------------|
|
|
25
|
-
| `/wt create <branch>` | `/wt c` | Create worktree with
|
|
28
|
+
| `/wt create <branch>` | `/wt c` | Create worktree with full isolation |
|
|
26
29
|
| `/wt list` | `/wt ls` | Show all worktrees |
|
|
27
|
-
| `/wt remove <branch>` | `/wt rm` | Remove worktree |
|
|
30
|
+
| `/wt remove <branch>` | `/wt rm` | Remove worktree (stops Docker first) |
|
|
28
31
|
| `/wt open <branch> [--editor]` | `/wt o` | Open (--cursor\|-c, --agy\|-a, --code\|-v) |
|
|
29
32
|
|
|
30
33
|
## Naming Convention
|
|
@@ -36,23 +39,55 @@ Worktrees are created as siblings with `--` separator:
|
|
|
36
39
|
~/[PARENT_DIRECTORY]/[REPO_NAME]--[BRANCH_NAME]/ # worktree for [BRANCH_NAME]
|
|
37
40
|
```
|
|
38
41
|
|
|
42
|
+
## Three-Layer Isolation
|
|
43
|
+
|
|
44
|
+
### Layer 1: `.worktreeinclude` — File Selection
|
|
45
|
+
|
|
46
|
+
Gitignore-style file at repo root controlling which untracked files to copy. Auto-generated with sensible defaults on first `wt create`.
|
|
47
|
+
|
|
48
|
+
```ini
|
|
49
|
+
.env*
|
|
50
|
+
# apps/*/.env*
|
|
51
|
+
|
|
52
|
+
[rewrite]
|
|
53
|
+
auto
|
|
54
|
+
|
|
55
|
+
[docker]
|
|
56
|
+
auto
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Layer 2: `[rewrite]` — Env Var Isolation
|
|
60
|
+
|
|
61
|
+
- **`auto`**: Suffixes DB_NAME, POSTGRES_DB, DATABASE_URL, COMPOSE_PROJECT_NAME with branch slug
|
|
62
|
+
- **`{{BRANCH}}`**: Template placeholder for custom vars
|
|
63
|
+
|
|
64
|
+
### Layer 3: `[docker]` — Docker Compose Override
|
|
65
|
+
|
|
66
|
+
- Generates `docker-compose.worktree.yml` with port offsets
|
|
67
|
+
- Sets `COMPOSE_FILE` and `COMPOSE_PROJECT_NAME` in `.env`
|
|
68
|
+
- Auto-creates databases in running Postgres
|
|
69
|
+
|
|
39
70
|
## Auto-Setup Features
|
|
40
71
|
|
|
41
72
|
When creating a worktree, the following happens automatically:
|
|
42
73
|
|
|
43
74
|
1. **Branch handling**: Creates new branch or checks out existing (local/remote)
|
|
44
|
-
2. **
|
|
45
|
-
3. **
|
|
46
|
-
4. **
|
|
75
|
+
2. **File copying**: Copies files matching `.worktreeinclude` patterns
|
|
76
|
+
3. **Env rewriting**: Suffixes database names and URLs with branch slug
|
|
77
|
+
4. **Docker isolation**: Generates port-offset override, sets compose config
|
|
78
|
+
5. **Database creation**: Creates database in running Postgres (idempotent)
|
|
79
|
+
6. **Package manager**: Detects bun/pnpm/yarn/npm from lockfiles
|
|
80
|
+
7. **Dependencies**: Runs install at root (monorepo-aware)
|
|
47
81
|
|
|
48
82
|
## Behavioral Expectations
|
|
49
83
|
|
|
50
84
|
When user asks about worktrees or parallel development:
|
|
51
85
|
|
|
52
86
|
1. Suggest using `/wt create <branch>` for new worktrees
|
|
53
|
-
2. Explain the
|
|
54
|
-
3. Mention
|
|
87
|
+
2. Explain the three-layer isolation if they have Docker/database conflicts
|
|
88
|
+
3. Mention `.worktreeinclude` for customizing which files are copied
|
|
55
89
|
4. Show the `cd` command output for easy navigation
|
|
90
|
+
5. Explain that `wt remove` stops Docker containers but preserves databases
|
|
56
91
|
|
|
57
92
|
## Example Interactions
|
|
58
93
|
|
|
@@ -60,9 +95,19 @@ When user asks about worktrees or parallel development:
|
|
|
60
95
|
|
|
61
96
|
**Response**: Use `/wt create feature/auth` to create a parallel workspace. This will:
|
|
62
97
|
- Create `project--feature-auth/` as sibling directory
|
|
63
|
-
- Copy your `.env` files
|
|
98
|
+
- Copy your `.env` files and rewrite DB names with `_feature_auth` suffix
|
|
99
|
+
- Generate Docker port offsets so both worktrees can run simultaneously
|
|
64
100
|
- Install dependencies
|
|
65
101
|
|
|
66
|
-
**User**: "
|
|
102
|
+
**User**: "My worktrees are conflicting on port 5432"
|
|
103
|
+
|
|
104
|
+
**Response**: The `.worktreeinclude` file's `[docker]` section handles this. With `auto` enabled, `wt create` generates a `docker-compose.worktree.yml` override that offsets all ports. Each worktree gets a deterministic offset based on the branch name. Run `docker compose config` to verify the merged ports.
|
|
67
105
|
|
|
68
|
-
**
|
|
106
|
+
**User**: "How do I customize which files are copied to worktrees?"
|
|
107
|
+
|
|
108
|
+
**Response**: Edit `.worktreeinclude` in your repo root. It uses gitignore-style patterns:
|
|
109
|
+
```
|
|
110
|
+
.env*
|
|
111
|
+
config/local.yml
|
|
112
|
+
apps/*/.env*
|
|
113
|
+
```
|