@reservine/dx 1.0.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/.claude-plugin/marketplace.json +22 -0
- package/README.md +303 -0
- package/bin/cli.ts +549 -0
- package/package.json +26 -0
- package/plugins/reservine-dx/.claude-plugin/plugin.json +8 -0
- package/plugins/reservine-dx/commands/cherry-pick-pr.md +221 -0
- package/plugins/reservine-dx/commands/cleanup.md +297 -0
- package/plugins/reservine-dx/commands/commit.md +118 -0
- package/plugins/reservine-dx/docker/worktree/docker-compose.isolated.template.yaml +144 -0
- package/plugins/reservine-dx/docker/worktree/seed-snapshot.sh +74 -0
- package/plugins/reservine-dx/scripts/_core.sh +330 -0
- package/plugins/reservine-dx/scripts/setup-worktree-be.sh +501 -0
- package/plugins/reservine-dx/scripts/setup-worktree-fe.sh +244 -0
- package/plugins/reservine-dx/scripts/setup-worktree.sh +59 -0
- package/plugins/reservine-dx/skills/cross-plan/SKILL.md +339 -0
- package/plugins/reservine-dx/skills/implement-plan/SKILL.md +512 -0
- package/plugins/reservine-dx/skills/implement-plan/references/plugin-contract.md +82 -0
- package/plugins/reservine-dx/skills/new-feature-planning/SKILL.md +544 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# docker/worktree/docker-compose.isolated.template.yaml
|
|
2
|
+
#
|
|
3
|
+
# Template for isolated worktree Docker stacks.
|
|
4
|
+
# Generated by: setup-worktree-be.sh --isolated
|
|
5
|
+
# DO NOT EDIT the generated file — edit this template instead.
|
|
6
|
+
#
|
|
7
|
+
# All ports use deterministic offsets based on branch name hash.
|
|
8
|
+
|
|
9
|
+
services:
|
|
10
|
+
db:
|
|
11
|
+
image: 'mariadb:latest'
|
|
12
|
+
container_name: '${COMPOSE_PROJECT_NAME}-db'
|
|
13
|
+
environment:
|
|
14
|
+
MYSQL_ROOT_PASSWORD: '${DB_PASSWORD}'
|
|
15
|
+
MYSQL_DATABASE: reservine
|
|
16
|
+
MYSQL_USER: reservine
|
|
17
|
+
MYSQL_PASSWORD: '${DB_PASSWORD}'
|
|
18
|
+
volumes:
|
|
19
|
+
- 'db_data:/var/lib/mysql'
|
|
20
|
+
- './docker/mysql/:/docker-entrypoint-initdb.d'
|
|
21
|
+
- './docker/mysql/conf.d/:/etc/mysql/conf.d'
|
|
22
|
+
ports:
|
|
23
|
+
- '${DB_PORT}:3306'
|
|
24
|
+
networks:
|
|
25
|
+
- sail
|
|
26
|
+
healthcheck:
|
|
27
|
+
test: ["CMD", "mariadb-admin", "ping", "-h", "localhost", "-ureservine", "-p${DB_PASSWORD}"]
|
|
28
|
+
interval: 5s
|
|
29
|
+
timeout: 5s
|
|
30
|
+
retries: 10
|
|
31
|
+
start_period: 10s
|
|
32
|
+
|
|
33
|
+
redis:
|
|
34
|
+
image: 'redis:latest'
|
|
35
|
+
container_name: '${COMPOSE_PROJECT_NAME}-redis'
|
|
36
|
+
ports:
|
|
37
|
+
- '${REDIS_PORT}:6379'
|
|
38
|
+
command: 'redis-server --save 10 1 --loglevel warning --requirepass ${REDIS_PASSWORD}'
|
|
39
|
+
environment:
|
|
40
|
+
- REDIS_PASSWORD=${REDIS_PASSWORD}
|
|
41
|
+
volumes:
|
|
42
|
+
- 'redis:/data'
|
|
43
|
+
networks:
|
|
44
|
+
- sail
|
|
45
|
+
healthcheck:
|
|
46
|
+
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
|
|
47
|
+
interval: 5s
|
|
48
|
+
timeout: 3s
|
|
49
|
+
retries: 5
|
|
50
|
+
|
|
51
|
+
adminer:
|
|
52
|
+
image: adminer
|
|
53
|
+
container_name: '${COMPOSE_PROJECT_NAME}-adminer'
|
|
54
|
+
environment:
|
|
55
|
+
ADMINER_DESIGN: nette
|
|
56
|
+
UPLOAD_LIMIT: 2G
|
|
57
|
+
networks:
|
|
58
|
+
- sail
|
|
59
|
+
depends_on:
|
|
60
|
+
- db
|
|
61
|
+
ports:
|
|
62
|
+
- '${ADMINER_PORT}:8080'
|
|
63
|
+
|
|
64
|
+
meilisearch:
|
|
65
|
+
image: 'getmeili/meilisearch:v1.5'
|
|
66
|
+
container_name: '${COMPOSE_PROJECT_NAME}-meili'
|
|
67
|
+
ports:
|
|
68
|
+
- '${MEILI_PORT}:7700'
|
|
69
|
+
environment:
|
|
70
|
+
MEILI_NO_ANALYTICS: 'true'
|
|
71
|
+
MEILI_MASTER_KEY: 'JCkBrarWPD_8Nj9KCRW32VJjigcZm6BPAttWg4g-OOw'
|
|
72
|
+
volumes:
|
|
73
|
+
- 'meili:/meili_data'
|
|
74
|
+
networks:
|
|
75
|
+
- sail
|
|
76
|
+
healthcheck:
|
|
77
|
+
test: ["CMD", "wget", "--no-verbose", "--spider", "--header=Authorization: Bearer JCkBrarWPD_8Nj9KCRW32VJjigcZm6BPAttWg4g-OOw", "http://127.0.0.1:7700/health"]
|
|
78
|
+
interval: 10s
|
|
79
|
+
retries: 5
|
|
80
|
+
timeout: 10s
|
|
81
|
+
start_period: 15s
|
|
82
|
+
|
|
83
|
+
soketi:
|
|
84
|
+
image: 'quay.io/soketi/soketi:latest-16-alpine'
|
|
85
|
+
container_name: '${COMPOSE_PROJECT_NAME}-soketi'
|
|
86
|
+
environment:
|
|
87
|
+
SOKETI_DEBUG: '1'
|
|
88
|
+
SOKETI_METRICS_SERVER_PORT: '9601'
|
|
89
|
+
ports:
|
|
90
|
+
- '${SOKETI_PORT}:6001'
|
|
91
|
+
networks:
|
|
92
|
+
- sail
|
|
93
|
+
|
|
94
|
+
laravel.test:
|
|
95
|
+
tty: true
|
|
96
|
+
init: true
|
|
97
|
+
container_name: '${COMPOSE_PROJECT_NAME}-app'
|
|
98
|
+
dns_opt:
|
|
99
|
+
- single-request-reopen
|
|
100
|
+
build:
|
|
101
|
+
context: ./docker/8.4
|
|
102
|
+
dockerfile: Dockerfile
|
|
103
|
+
args:
|
|
104
|
+
WWWGROUP: '1000'
|
|
105
|
+
image: sail-8.4/app
|
|
106
|
+
extra_hosts:
|
|
107
|
+
- 'host.docker.internal:host-gateway'
|
|
108
|
+
ports:
|
|
109
|
+
- '${APP_PORT}:80'
|
|
110
|
+
env_file:
|
|
111
|
+
- .env
|
|
112
|
+
environment:
|
|
113
|
+
WWWUSER: '1000'
|
|
114
|
+
LARAVEL_SAIL: 1
|
|
115
|
+
SUPERVISOR_PHP_COMMAND: "/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan serve --host=0.0.0.0 --port=80"
|
|
116
|
+
DB_HOST: db
|
|
117
|
+
DB_PORT: 3306
|
|
118
|
+
DB_DATABASE: reservine
|
|
119
|
+
DB_USERNAME: reservine
|
|
120
|
+
DB_PASSWORD: '${DB_PASSWORD}'
|
|
121
|
+
REDIS_HOST: redis
|
|
122
|
+
REDIS_PASSWORD: '${REDIS_PASSWORD}'
|
|
123
|
+
REDIS_PORT: 6379
|
|
124
|
+
volumes:
|
|
125
|
+
- '.:/var/www/html'
|
|
126
|
+
- '/var/www/html/node_modules'
|
|
127
|
+
networks:
|
|
128
|
+
- sail
|
|
129
|
+
depends_on:
|
|
130
|
+
db:
|
|
131
|
+
condition: service_healthy
|
|
132
|
+
redis:
|
|
133
|
+
condition: service_healthy
|
|
134
|
+
meilisearch:
|
|
135
|
+
condition: service_healthy
|
|
136
|
+
|
|
137
|
+
networks:
|
|
138
|
+
sail:
|
|
139
|
+
driver: bridge
|
|
140
|
+
|
|
141
|
+
volumes:
|
|
142
|
+
db_data:
|
|
143
|
+
redis:
|
|
144
|
+
meili:
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Seed a worktree's isolated DB from a snapshot file.
|
|
4
|
+
# Called automatically by setup-worktree-be.sh --isolated on first run.
|
|
5
|
+
#
|
|
6
|
+
# Usage: ./seed-snapshot.sh <compose-file> <snapshot-path> [db-password] [project-name]
|
|
7
|
+
#
|
|
8
|
+
# Prerequisites:
|
|
9
|
+
# - Docker containers must be running
|
|
10
|
+
# - DB container must be healthy
|
|
11
|
+
|
|
12
|
+
set -eo pipefail
|
|
13
|
+
|
|
14
|
+
COMPOSE_FILE="${1:?Usage: seed-snapshot.sh <compose-file> <snapshot-path> [db-password] [project-name]}"
|
|
15
|
+
SNAPSHOT="${2:?Usage: seed-snapshot.sh <compose-file> <snapshot-path> [db-password] [project-name]}"
|
|
16
|
+
DB_PASS="${3:-reservine_wt_pass}"
|
|
17
|
+
PROJECT_NAME="${4:-}"
|
|
18
|
+
|
|
19
|
+
# Set compose project name if provided
|
|
20
|
+
if [[ -n "$PROJECT_NAME" ]]; then
|
|
21
|
+
export COMPOSE_PROJECT_NAME="$PROJECT_NAME"
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
# Colors
|
|
25
|
+
GREEN='\033[0;32m'
|
|
26
|
+
YELLOW='\033[1;33m'
|
|
27
|
+
BLUE='\033[0;34m'
|
|
28
|
+
NC='\033[0m'
|
|
29
|
+
|
|
30
|
+
info() { echo -e "${BLUE}ℹ${NC} $1"; }
|
|
31
|
+
success() { echo -e "${GREEN}✓${NC} $1"; }
|
|
32
|
+
warn() { echo -e "${YELLOW}⚠️${NC} $1"; }
|
|
33
|
+
|
|
34
|
+
if [[ ! -f "$SNAPSHOT" ]]; then
|
|
35
|
+
warn "Snapshot not found: $SNAPSHOT"
|
|
36
|
+
warn "Place your snapshot at: $(cd "$(dirname "$0")/../.." && pwd)/snapshots/reservine.sql.gz"
|
|
37
|
+
exit 0 # Non-fatal — worktree still usable, just empty DB
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
DB_CONTAINER=$(docker compose -f "$COMPOSE_FILE" ps -q db)
|
|
41
|
+
|
|
42
|
+
if [[ -z "$DB_CONTAINER" ]]; then
|
|
43
|
+
warn "DB container not found — skipping snapshot import"
|
|
44
|
+
exit 0
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
# Check if DB already has data
|
|
48
|
+
TABLE_COUNT=$(docker exec "$DB_CONTAINER" \
|
|
49
|
+
mariadb -ureservine -p${DB_PASS} reservine \
|
|
50
|
+
-sNe "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='reservine'" 2>/dev/null || echo "0")
|
|
51
|
+
|
|
52
|
+
if [[ "$TABLE_COUNT" -gt 5 ]]; then
|
|
53
|
+
info "DB already has $TABLE_COUNT tables — skipping snapshot import"
|
|
54
|
+
info "To re-import, run: docker compose -f $COMPOSE_FILE down -v && re-run setup"
|
|
55
|
+
exit 0
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
info "Importing snapshot ($SNAPSHOT)..."
|
|
59
|
+
SECONDS=0
|
|
60
|
+
|
|
61
|
+
if [[ "$SNAPSHOT" == *.gz ]]; then
|
|
62
|
+
gunzip -c "$SNAPSHOT" | docker exec -i "$DB_CONTAINER" \
|
|
63
|
+
mariadb -ureservine -p${DB_PASS} reservine
|
|
64
|
+
else
|
|
65
|
+
docker exec -i "$DB_CONTAINER" \
|
|
66
|
+
mariadb -ureservine -p${DB_PASS} reservine < "$SNAPSHOT"
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
success "Snapshot imported in ${SECONDS}s"
|
|
70
|
+
|
|
71
|
+
# Run pending migrations
|
|
72
|
+
info "Running pending migrations..."
|
|
73
|
+
docker compose -f "$COMPOSE_FILE" exec -T laravel.test php artisan migrate --force 2>&1 | tail -5
|
|
74
|
+
success "Migrations complete"
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Shared core utilities for Reservine worktree setup scripts.
|
|
4
|
+
#
|
|
5
|
+
# This file is sourced by setup-worktree-fe.sh and setup-worktree-be.sh.
|
|
6
|
+
# It provides: colors, logging, environment detection, ROOT_WORKTREE_PATH
|
|
7
|
+
# resolution, plan file resolution, and branch setup.
|
|
8
|
+
#
|
|
9
|
+
# Do NOT execute this file directly.
|
|
10
|
+
#
|
|
11
|
+
|
|
12
|
+
# Colors for output
|
|
13
|
+
RED='\033[0;31m'
|
|
14
|
+
GREEN='\033[0;32m'
|
|
15
|
+
YELLOW='\033[1;33m'
|
|
16
|
+
BLUE='\033[0;34m'
|
|
17
|
+
NC='\033[0m' # No Color
|
|
18
|
+
|
|
19
|
+
# Log file (set by --log flag or LOG_FILE env var)
|
|
20
|
+
LOG_FILE="${LOG_FILE:-}"
|
|
21
|
+
|
|
22
|
+
# Internal: write to log file if enabled
|
|
23
|
+
_log() {
|
|
24
|
+
if [[ -n "$LOG_FILE" ]]; then
|
|
25
|
+
echo "[$(date '+%H:%M:%S')] $1" >> "$LOG_FILE"
|
|
26
|
+
fi
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
# Logging helpers
|
|
30
|
+
info() { echo -e "${BLUE}ℹ${NC} $1"; _log "INFO $1"; }
|
|
31
|
+
success() {
|
|
32
|
+
echo -e "${GREEN}✓${NC} $1"
|
|
33
|
+
_log "OK $1"
|
|
34
|
+
command -v sayy &>/dev/null && sayy 0.3 "$1" &>/dev/null &
|
|
35
|
+
}
|
|
36
|
+
warn() { echo -e "${YELLOW}⚠️${NC} $1"; _log "WARN $1"; }
|
|
37
|
+
error() { echo -e "${RED}❌${NC} $1" >&2; _log "ERROR $1"; }
|
|
38
|
+
|
|
39
|
+
# Initialize log file (call from setup scripts after parsing --log)
|
|
40
|
+
init_log() {
|
|
41
|
+
if [[ -n "$LOG_FILE" ]]; then
|
|
42
|
+
mkdir -p "$(dirname "$LOG_FILE")"
|
|
43
|
+
echo "# Reservine-DX setup log — $(date)" > "$LOG_FILE"
|
|
44
|
+
echo "# PWD: $(pwd)" >> "$LOG_FILE"
|
|
45
|
+
echo "# Args: $*" >> "$LOG_FILE"
|
|
46
|
+
echo "" >> "$LOG_FILE"
|
|
47
|
+
info "Logging to: $LOG_FILE"
|
|
48
|
+
fi
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
# Deterministic port offset from branch name (0-99)
|
|
52
|
+
port_hash() {
|
|
53
|
+
local name="$1"
|
|
54
|
+
local hash=$(echo -n "$name" | cksum | awk '{print $1}')
|
|
55
|
+
echo $(( hash % 100 ))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
# Check if a port is in use
|
|
59
|
+
port_in_use() {
|
|
60
|
+
lsof -i ":$1" -P -n >/dev/null 2>&1
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
# Get the branch name sanitized for Docker
|
|
64
|
+
get_branch_slug() {
|
|
65
|
+
local branch
|
|
66
|
+
branch=$(git branch --show-current 2>/dev/null || echo "unknown")
|
|
67
|
+
echo "$branch" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//'
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
# Sanitize branch name for use as directory name (strip # which breaks vite @fs URLs)
|
|
71
|
+
sanitize_worktree_dir() {
|
|
72
|
+
echo "$1" | sed 's/#//g'
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
# Find the FE worktree sibling (used by Laravel for proxy config generation)
|
|
76
|
+
find_fe_worktree() {
|
|
77
|
+
local branch_name
|
|
78
|
+
branch_name=$(git branch --show-current 2>/dev/null)
|
|
79
|
+
local safe_name
|
|
80
|
+
safe_name=$(sanitize_worktree_dir "$branch_name")
|
|
81
|
+
|
|
82
|
+
local fe_base
|
|
83
|
+
if [[ -n "${ROOT_WORKTREE_PATH:-}" ]]; then
|
|
84
|
+
fe_base="$(dirname "$ROOT_WORKTREE_PATH")/reservine"
|
|
85
|
+
else
|
|
86
|
+
fe_base="$(dirname "$(dirname "$PWD")")/reservine"
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
# Try sanitized name first (no #), then exact branch name, then glob
|
|
90
|
+
for dir in \
|
|
91
|
+
"$fe_base/.worktrees/$safe_name" \
|
|
92
|
+
"$fe_base/.worktrees"/*/"$(basename "$safe_name")" \
|
|
93
|
+
"$fe_base/.worktrees/$branch_name" \
|
|
94
|
+
"$fe_base/.worktrees"/*/"$(basename "$branch_name")" \
|
|
95
|
+
"$fe_base/.worktrees"/*/*"${branch_name##*/}"*; do
|
|
96
|
+
if [[ -d "$dir" ]]; then
|
|
97
|
+
echo "$dir"
|
|
98
|
+
return 0
|
|
99
|
+
fi
|
|
100
|
+
done
|
|
101
|
+
return 1
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
# Find the BE worktree sibling (used by Angular for proxy config generation)
|
|
105
|
+
find_be_worktree() {
|
|
106
|
+
local branch_name
|
|
107
|
+
branch_name=$(git branch --show-current 2>/dev/null)
|
|
108
|
+
local safe_name
|
|
109
|
+
safe_name=$(sanitize_worktree_dir "$branch_name")
|
|
110
|
+
|
|
111
|
+
local be_base
|
|
112
|
+
if [[ -n "${ROOT_WORKTREE_PATH:-}" ]]; then
|
|
113
|
+
be_base="$(dirname "$ROOT_WORKTREE_PATH")/ReservineBack"
|
|
114
|
+
else
|
|
115
|
+
be_base="$(dirname "$(dirname "$PWD")")/ReservineBack"
|
|
116
|
+
fi
|
|
117
|
+
|
|
118
|
+
for dir in \
|
|
119
|
+
"$be_base/.worktrees/$safe_name" \
|
|
120
|
+
"$be_base/.worktrees"/*/"$(basename "$safe_name")" \
|
|
121
|
+
"$be_base/.worktrees/$branch_name" \
|
|
122
|
+
"$be_base/.worktrees"/*/"$(basename "$branch_name")"; do
|
|
123
|
+
if [[ -d "$dir" ]]; then
|
|
124
|
+
echo "$dir"
|
|
125
|
+
return 0
|
|
126
|
+
fi
|
|
127
|
+
done
|
|
128
|
+
return 1
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
# ============================================================================
|
|
132
|
+
# Resolve DX_SCRIPTS_DIR — directory containing these scripts
|
|
133
|
+
# ============================================================================
|
|
134
|
+
|
|
135
|
+
# If sourced from the marketplace install:
|
|
136
|
+
# ~/.claude/plugins/marketplaces/reservine-dx/plugins/reservine-dx/scripts/
|
|
137
|
+
# If sourced from a local checkout:
|
|
138
|
+
# /path/to/Reservine-DX/plugins/reservine-dx/scripts/
|
|
139
|
+
DX_SCRIPTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
140
|
+
DX_PLUGIN_DIR="$(dirname "$DX_SCRIPTS_DIR")"
|
|
141
|
+
|
|
142
|
+
# ============================================================================
|
|
143
|
+
# Resolve ROOT_WORKTREE_PATH
|
|
144
|
+
# ============================================================================
|
|
145
|
+
|
|
146
|
+
resolve_root_worktree_path() {
|
|
147
|
+
if [[ -n "${ROOT_WORKTREE_PATH:-}" ]]; then
|
|
148
|
+
return 0
|
|
149
|
+
fi
|
|
150
|
+
|
|
151
|
+
# Try to auto-detect from git worktree list
|
|
152
|
+
DETECTED_ROOT=$(git worktree list 2>/dev/null \
|
|
153
|
+
| grep -v ".claude-worktrees" \
|
|
154
|
+
| grep -v ".cursor" \
|
|
155
|
+
| grep -v ".worktrees" \
|
|
156
|
+
| head -1 | awk '{print $1}')
|
|
157
|
+
|
|
158
|
+
if [[ -n "$DETECTED_ROOT" ]] && [[ -d "$DETECTED_ROOT" ]]; then
|
|
159
|
+
ROOT_WORKTREE_PATH="$DETECTED_ROOT"
|
|
160
|
+
fi
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
# ============================================================================
|
|
164
|
+
# Detect environment (cloud / worktree / main repo)
|
|
165
|
+
# ============================================================================
|
|
166
|
+
|
|
167
|
+
detect_environment() {
|
|
168
|
+
IS_CLOUD=false
|
|
169
|
+
IS_WORKTREE=false
|
|
170
|
+
|
|
171
|
+
if echo "$PWD" | grep -q ".claude-worktrees"; then
|
|
172
|
+
IS_WORKTREE=true
|
|
173
|
+
info "Environment: Worktree (path contains .claude-worktrees)"
|
|
174
|
+
elif git worktree list 2>/dev/null | grep -q "^$(pwd) "; then
|
|
175
|
+
IS_WORKTREE=true
|
|
176
|
+
info "Environment: Worktree (git worktree list confirms)"
|
|
177
|
+
elif [[ "${CLAUDE_CODE_REMOTE_ENVIRONMENT_TYPE:-}" == "cloud_default" ]] || [[ -n "${CLAUDE_CODE_REMOTE:-}" ]]; then
|
|
178
|
+
IS_CLOUD=true
|
|
179
|
+
info "Environment: Cloud (CLAUDE_CODE_REMOTE or cloud_default detected)"
|
|
180
|
+
elif [[ -n "${CLAUDECODE:-}" ]] && [[ ! -d "$HOME" || -f "/.dockerenv" ]]; then
|
|
181
|
+
# CLAUDECODE set AND (no real home dir OR inside Docker container) = likely cloud
|
|
182
|
+
IS_CLOUD=true
|
|
183
|
+
info "Environment: Cloud (CLAUDECODE detected, non-local system)"
|
|
184
|
+
else
|
|
185
|
+
info "Environment: Main Repository"
|
|
186
|
+
fi
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
# ============================================================================
|
|
190
|
+
# Plan file resolution
|
|
191
|
+
# ============================================================================
|
|
192
|
+
|
|
193
|
+
resolve_plan_file() {
|
|
194
|
+
local pattern="$1"
|
|
195
|
+
PLAN_FILE=$(find .claude/plans/ -name "*${pattern}*" -type f 2>/dev/null | head -1)
|
|
196
|
+
|
|
197
|
+
if [[ -z "$PLAN_FILE" ]]; then
|
|
198
|
+
error "No plan file matching '$pattern' found in .claude/plans/"
|
|
199
|
+
echo " Available plans:" >&2
|
|
200
|
+
find .claude/plans/ -name "*.md" -type f 2>/dev/null | head -10 | sed 's/^/ /' >&2
|
|
201
|
+
exit 1
|
|
202
|
+
fi
|
|
203
|
+
|
|
204
|
+
BRANCH_NAME=$(basename "$PLAN_FILE" .md)
|
|
205
|
+
echo ""
|
|
206
|
+
echo "📋 Plan Resolution"
|
|
207
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
208
|
+
echo " Pattern: $pattern"
|
|
209
|
+
echo " Found: $PLAN_FILE"
|
|
210
|
+
echo " Branch: $BRANCH_NAME"
|
|
211
|
+
echo ""
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
# ============================================================================
|
|
215
|
+
# Branch setup (creates or checks out branch)
|
|
216
|
+
# ============================================================================
|
|
217
|
+
|
|
218
|
+
setup_branch() {
|
|
219
|
+
local branch="$1"
|
|
220
|
+
local base="$2"
|
|
221
|
+
|
|
222
|
+
echo "📦 Branch Setup"
|
|
223
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
224
|
+
|
|
225
|
+
# Check if git is clean (ignore untracked files)
|
|
226
|
+
if [[ -n "$(git status --porcelain 2>/dev/null | grep -v '^??')" ]]; then
|
|
227
|
+
error "Working directory has uncommitted changes"
|
|
228
|
+
echo " Please commit or stash changes before proceeding" >&2
|
|
229
|
+
exit 1
|
|
230
|
+
fi
|
|
231
|
+
success "Working directory is clean"
|
|
232
|
+
|
|
233
|
+
# Check if based on target branch (warning only)
|
|
234
|
+
if git rev-parse "$base" &>/dev/null; then
|
|
235
|
+
if git merge-base --is-ancestor "$(git rev-parse "$base")" HEAD 2>/dev/null; then
|
|
236
|
+
success "Current HEAD is based on $base"
|
|
237
|
+
else
|
|
238
|
+
warn "Not based on $base - consider rebasing before PR"
|
|
239
|
+
fi
|
|
240
|
+
else
|
|
241
|
+
warn "Base branch '$base' not found locally"
|
|
242
|
+
fi
|
|
243
|
+
|
|
244
|
+
# Check if branch already exists
|
|
245
|
+
if git show-ref --verify --quiet "refs/heads/$branch" 2>/dev/null; then
|
|
246
|
+
warn "Branch '$branch' already exists - checking out"
|
|
247
|
+
git checkout "$branch"
|
|
248
|
+
else
|
|
249
|
+
success "Creating branch: $branch"
|
|
250
|
+
git checkout -b "$branch"
|
|
251
|
+
fi
|
|
252
|
+
success "On branch: $(git branch --show-current)"
|
|
253
|
+
echo ""
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
# ============================================================================
|
|
257
|
+
# Pre-flight: validate ROOT_WORKTREE_PATH (for non-cloud environments)
|
|
258
|
+
# ============================================================================
|
|
259
|
+
|
|
260
|
+
preflight_check() {
|
|
261
|
+
echo ""
|
|
262
|
+
echo "🚀 Setting up worktree..."
|
|
263
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
264
|
+
|
|
265
|
+
detect_environment
|
|
266
|
+
|
|
267
|
+
if [[ "$IS_CLOUD" == false ]]; then
|
|
268
|
+
if [[ -z "${ROOT_WORKTREE_PATH:-}" ]]; then
|
|
269
|
+
error "ROOT_WORKTREE_PATH could not be auto-detected"
|
|
270
|
+
echo " Set it manually:" >&2
|
|
271
|
+
echo " ROOT_WORKTREE_PATH=~/projects/my-repo ./setup-worktree.sh" >&2
|
|
272
|
+
exit 1
|
|
273
|
+
fi
|
|
274
|
+
|
|
275
|
+
if [[ ! -d "${ROOT_WORKTREE_PATH}" ]]; then
|
|
276
|
+
error "ROOT_WORKTREE_PATH does not exist: ${ROOT_WORKTREE_PATH}"
|
|
277
|
+
exit 1
|
|
278
|
+
fi
|
|
279
|
+
|
|
280
|
+
# Safety: prevent running in the main repo root (would destroy node_modules/vendor)
|
|
281
|
+
RESOLVED_ROOT=$(cd "$ROOT_WORKTREE_PATH" && pwd -P)
|
|
282
|
+
RESOLVED_CWD=$(pwd -P)
|
|
283
|
+
if [[ "$RESOLVED_ROOT" == "$RESOLVED_CWD" ]]; then
|
|
284
|
+
error "You are running this from the main repo root, not a worktree"
|
|
285
|
+
echo "" >&2
|
|
286
|
+
echo " This script is meant to run INSIDE a worktree, not in the main repo." >&2
|
|
287
|
+
echo " Running here would destroy your node_modules/vendor directory." >&2
|
|
288
|
+
echo "" >&2
|
|
289
|
+
echo " To create a worktree first:" >&2
|
|
290
|
+
echo " git worktree add .worktrees/<branch> origin/<base> -b <branch>" >&2
|
|
291
|
+
echo " cd .worktrees/<branch>" >&2
|
|
292
|
+
echo " <then run this script>" >&2
|
|
293
|
+
echo "" >&2
|
|
294
|
+
echo " Or use --branch to create one:" >&2
|
|
295
|
+
echo " <this-script> --branch=feat/my-feature" >&2
|
|
296
|
+
exit 1
|
|
297
|
+
fi
|
|
298
|
+
|
|
299
|
+
if [[ -n "${DETECTED_ROOT:-}" ]]; then
|
|
300
|
+
info "Auto-detected main repo: ${ROOT_WORKTREE_PATH}"
|
|
301
|
+
else
|
|
302
|
+
echo " Main repo: ${ROOT_WORKTREE_PATH}"
|
|
303
|
+
fi
|
|
304
|
+
fi
|
|
305
|
+
echo " Worktree: $(pwd)"
|
|
306
|
+
echo ""
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
# ============================================================================
|
|
310
|
+
# Final state output
|
|
311
|
+
# ============================================================================
|
|
312
|
+
|
|
313
|
+
print_state() {
|
|
314
|
+
local dep_dir="$1"
|
|
315
|
+
local branch="$2"
|
|
316
|
+
|
|
317
|
+
if [[ -n "$branch" ]]; then
|
|
318
|
+
echo "📍 Current State:"
|
|
319
|
+
echo " Branch: $(git branch --show-current)"
|
|
320
|
+
echo " Path: $(pwd)"
|
|
321
|
+
if [[ -L "$dep_dir" ]]; then
|
|
322
|
+
echo " ${dep_dir}: symlinked → $(readlink "$dep_dir")"
|
|
323
|
+
else
|
|
324
|
+
echo " ${dep_dir}: installed locally"
|
|
325
|
+
fi
|
|
326
|
+
fi
|
|
327
|
+
|
|
328
|
+
# Final audio notification
|
|
329
|
+
command -v sayy &>/dev/null && sayy 0.5 "Worktree setup complete" &>/dev/null &
|
|
330
|
+
}
|