@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.
@@ -0,0 +1,501 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Reservine-DX — Laravel Backend Worktree Setup
4
+ #
5
+ # Sets up a git worktree for the ReservineBack Laravel backend:
6
+ # - Copies .env / .env.local from main repo
7
+ # - Symlinks (or copies) vendor/ from the main repo
8
+ # - Starts Sail containers (shared with main repo)
9
+ # - Optionally creates an isolated Docker stack (--isolated)
10
+ # with its own DB, Redis, Meilisearch, Soketi, and deterministic ports
11
+ # - Seeds the isolated DB from a snapshot
12
+ # - Generates FE proxy config for the sibling Angular worktree
13
+ #
14
+ # Usage:
15
+ # ./setup-worktree-be.sh [OPTIONS]
16
+ #
17
+ # Options:
18
+ # --plan=<name> Search .claude/plans/ for matching file, extract branch name
19
+ # --branch=<name> Create and checkout this branch
20
+ # --base=<branch> Base branch for PR (default: dev)
21
+ # --no-vendor-link Run composer install instead of symlinking vendor/
22
+ # --skip-sail Skip Sail container startup
23
+ # --isolated Create isolated Docker stack with unique ports
24
+ # --teardown Stop and remove the isolated Docker stack (destroys data)
25
+ # --reseed Re-import the DB snapshot (destroys existing data)
26
+ # --status Show all running worktree Docker stacks
27
+ # -h, --help Show this help message
28
+ #
29
+
30
+ set -euo pipefail
31
+
32
+ # Source shared core
33
+ SCRIPTS_DIR="$(cd "$(dirname "$0")" && pwd)"
34
+ source "$SCRIPTS_DIR/_core.sh"
35
+
36
+ # ─── Defaults ───────────────────────────────────────────────────────────────────
37
+
38
+ VENDOR_LINK=true
39
+ SKIP_SAIL=false
40
+ BRANCH_NAME=""
41
+ BASE_BRANCH="dev"
42
+ PLAN_PATTERN=""
43
+ ISOLATED=false
44
+ TEARDOWN=false
45
+ SHOW_STATUS=false
46
+ RESEED=false
47
+
48
+ # ─── Help ───────────────────────────────────────────────────────────────────────
49
+
50
+ show_help() {
51
+ cat << 'EOF'
52
+ Reservine-DX — Laravel Backend Worktree Setup
53
+
54
+ Usage: ./setup-worktree-be.sh [OPTIONS]
55
+
56
+ Options:
57
+ --plan=<name> Search .claude/plans/ for matching file, extract branch name
58
+ --branch=<name> Create and checkout this branch
59
+ --base=<branch> Base branch for PR validation (default: dev)
60
+ --no-vendor-link Run composer install instead of symlinking vendor/
61
+ --skip-sail Skip Sail container startup even in local environment
62
+ --isolated Create isolated Docker stack with unique ports (no shared services)
63
+ --teardown Stop and remove the isolated Docker stack (destroys data)
64
+ --reseed Re-import the DB snapshot (destroys existing data)
65
+ --status Show all running worktree Docker stacks
66
+ -h, --help Show this help message
67
+
68
+ Environment Variables:
69
+ ROOT_WORKTREE_PATH Path to main repo (auto-detected from git worktree list)
70
+
71
+ Examples:
72
+ # Simple setup — auto-detects everything
73
+ ./setup-worktree-be.sh
74
+
75
+ # Setup with explicit branch
76
+ ./setup-worktree-be.sh --branch=feat/my-feature
77
+
78
+ # Isolated Docker environment (own DB, Redis, ports)
79
+ ./setup-worktree-be.sh --isolated
80
+
81
+ # Tear down an isolated environment
82
+ ./setup-worktree-be.sh --teardown
83
+
84
+ # Re-seed the isolated DB from snapshot
85
+ ./setup-worktree-be.sh --reseed
86
+ EOF
87
+ }
88
+
89
+ # ─── Parse arguments ────────────────────────────────────────────────────────────
90
+
91
+ while [[ $# -gt 0 ]]; do
92
+ case $1 in
93
+ --plan=*) PLAN_PATTERN="${1#*=}"; shift ;;
94
+ --branch=*) BRANCH_NAME="${1#*=}"; shift ;;
95
+ --base=*) BASE_BRANCH="${1#*=}"; shift ;;
96
+ --no-vendor-link) VENDOR_LINK=false; shift ;;
97
+ --skip-sail) SKIP_SAIL=true; shift ;;
98
+ --isolated) ISOLATED=true; shift ;;
99
+ --teardown) TEARDOWN=true; shift ;;
100
+ --status) SHOW_STATUS=true; shift ;;
101
+ --reseed) RESEED=true; shift ;;
102
+ --log) LOG_FILE="$(pwd)/setup.log"; shift ;;
103
+ --log=*) LOG_FILE="${1#*=}"; shift ;;
104
+ -h|--help) show_help; exit 0 ;;
105
+ *) error "Unknown option: $1"; show_help; exit 1 ;;
106
+ esac
107
+ done
108
+
109
+ init_log "$@"
110
+
111
+ # --isolated requires its own vendor (autoloader paths must match container paths)
112
+ if [[ "$ISOLATED" == true ]]; then
113
+ VENDOR_LINK=false
114
+ fi
115
+
116
+ # ─── Plan resolution ────────────────────────────────────────────────────────────
117
+
118
+ if [[ -n "$PLAN_PATTERN" ]]; then
119
+ resolve_plan_file "$PLAN_PATTERN"
120
+ fi
121
+
122
+ # ─── ROOT_WORKTREE_PATH ─────────────────────────────────────────────────────────
123
+
124
+ resolve_root_worktree_path
125
+
126
+ # ─── Quick commands (can run before full pre-flight) ─────────────────────────────
127
+
128
+ if [[ "$SHOW_STATUS" == true ]]; then
129
+ echo ""
130
+ echo "📊 Worktree Docker Stacks"
131
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
132
+ docker ps --filter "name=wt-" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" 2>/dev/null || echo "No worktree containers found"
133
+ echo ""
134
+ exit 0
135
+ fi
136
+
137
+ if [[ "$TEARDOWN" == true ]]; then
138
+ if [[ -f "docker-compose.isolated.yaml" ]]; then
139
+ echo "🗑️ Tearing down isolated stack..."
140
+ docker compose -f docker-compose.isolated.yaml down -v
141
+ rm -f docker-compose.isolated.yaml .env.isolated
142
+ success "Isolated stack removed (volumes destroyed)"
143
+ else
144
+ warn "No docker-compose.isolated.yaml found in current directory"
145
+ fi
146
+ exit 0
147
+ fi
148
+
149
+ if [[ "$RESEED" == true ]]; then
150
+ if [[ -z "${ROOT_WORKTREE_PATH:-}" ]]; then
151
+ DETECTED_ROOT=$(git worktree list 2>/dev/null | grep -v ".worktrees" | head -1 | awk '{print $1}')
152
+ ROOT_WORKTREE_PATH="${DETECTED_ROOT:-$(pwd)}"
153
+ fi
154
+ SNAPSHOT_PATH="${ROOT_WORKTREE_PATH}/snapshots/reservine.sql.gz"
155
+ SEED_SCRIPT="${DX_PLUGIN_DIR}/docker/worktree/seed-snapshot.sh"
156
+ # Fallback to main repo's copy if DX script not found
157
+ [[ ! -f "$SEED_SCRIPT" ]] && SEED_SCRIPT="${ROOT_WORKTREE_PATH}/docker/worktree/seed-snapshot.sh"
158
+ if [[ -f "docker-compose.isolated.yaml" ]]; then
159
+ echo "🌱 Re-seeding database..."
160
+ docker compose -f docker-compose.isolated.yaml down -v
161
+ docker compose -f docker-compose.isolated.yaml up -d
162
+ echo "Waiting for DB to be healthy..."
163
+ sleep 15
164
+ BRANCH_SLUG=$(get_branch_slug)
165
+ "$SEED_SCRIPT" \
166
+ docker-compose.isolated.yaml "$SNAPSHOT_PATH" "reservine_wt_pass" "wt-${BRANCH_SLUG}"
167
+ success "Database re-seeded"
168
+ else
169
+ warn "No docker-compose.isolated.yaml found — run --isolated first"
170
+ fi
171
+ exit 0
172
+ fi
173
+
174
+ # ─── Pre-flight ─────────────────────────────────────────────────────────────────
175
+
176
+ preflight_check
177
+
178
+ info "Stack: Laravel (composer)"
179
+
180
+ # ─── Branch setup ────────────────────────────────────────────────────────────────
181
+
182
+ if [[ -n "$BRANCH_NAME" ]]; then
183
+ setup_branch "$BRANCH_NAME" "$BASE_BRANCH"
184
+ fi
185
+
186
+ # ─── Environment setup (.env, vendor) ────────────────────────────────────────────
187
+
188
+ if [[ "$IS_CLOUD" == true ]]; then
189
+ info "Cloud environment — skipping .env and vendor setup"
190
+ info "Use setupClaudeWeb.sh for cloud configuration"
191
+ else
192
+ echo "⚙️ Laravel Environment Setup"
193
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
194
+
195
+ # ── .env ──
196
+
197
+ if [[ ! -f ".env" ]]; then
198
+ if [[ -f "${ROOT_WORKTREE_PATH}/.env" ]]; then
199
+ cp "${ROOT_WORKTREE_PATH}/.env" .env
200
+ success "Copied .env from main repo"
201
+ else
202
+ warn "No .env found in main repo — you may need to create one"
203
+ fi
204
+ else
205
+ info ".env already exists"
206
+ fi
207
+
208
+ if [[ ! -f ".env.local" ]] && [[ -f "${ROOT_WORKTREE_PATH}/.env.local" ]]; then
209
+ cp "${ROOT_WORKTREE_PATH}/.env.local" .env.local
210
+ success "Copied .env.local from main repo"
211
+ fi
212
+
213
+ # ── vendor ──
214
+
215
+ if [[ "$VENDOR_LINK" == true ]]; then
216
+ if [[ -d "vendor" ]] && [[ ! -L "vendor" ]]; then
217
+ warn "vendor/ is a directory — removing to create symlink"
218
+ rm -rf vendor
219
+ fi
220
+
221
+ if [[ -L "vendor" ]]; then
222
+ info "vendor/ symlink already exists → $(readlink vendor)"
223
+ elif [[ -d "${ROOT_WORKTREE_PATH}/vendor" ]]; then
224
+ ln -s "${ROOT_WORKTREE_PATH}/vendor" vendor
225
+ success "Symlinked vendor/ → main repo"
226
+ else
227
+ warn "Main repo has no vendor/ — running composer install"
228
+ composer install --no-interaction --prefer-dist
229
+ success "Installed vendor/"
230
+ fi
231
+ else
232
+ if [[ -d "vendor" ]] && [[ ! -L "vendor" ]]; then
233
+ info "vendor/ directory already exists (local copy)"
234
+ else
235
+ [[ -L "vendor" ]] && rm vendor
236
+ if [[ -d "${ROOT_WORKTREE_PATH}/vendor" ]]; then
237
+ info "Copying vendor/ from main repo (this takes ~10s)..."
238
+ cp -R "${ROOT_WORKTREE_PATH}/vendor" vendor
239
+ success "Copied vendor/ (local copy for isolated mode)"
240
+ elif command -v composer &>/dev/null; then
241
+ composer install --no-interaction --prefer-dist
242
+ success "Installed vendor/"
243
+ else
244
+ error "No vendor/ source and no composer available"
245
+ exit 1
246
+ fi
247
+ fi
248
+ fi
249
+ echo ""
250
+ fi
251
+
252
+ # ─── Sail containers (shared, local only) ────────────────────────────────────────
253
+
254
+ if [[ "$IS_CLOUD" == true ]]; then
255
+ info "Cloud environment — skipping Sail"
256
+ elif [[ "$SKIP_SAIL" == true ]]; then
257
+ info "Sail startup skipped (--skip-sail)"
258
+ elif [[ "$ISOLATED" == false ]]; then
259
+ echo "🐳 Sail Containers"
260
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
261
+
262
+ if [[ -x "./vendor/bin/sail" ]] || [[ -L "vendor" && -x "$(readlink -f vendor)/bin/sail" ]]; then
263
+ if ./vendor/bin/sail ps 2>/dev/null | grep -q "Up"; then
264
+ info "Sail containers already running"
265
+ else
266
+ info "Starting Sail containers..."
267
+ ./vendor/bin/sail up -d
268
+ success "Sail started"
269
+ fi
270
+
271
+ if [[ -f ".env" ]] && ! grep -qE "^APP_KEY=base64:" .env 2>/dev/null; then
272
+ ./vendor/bin/sail artisan key:generate
273
+ success "Generated app key"
274
+ fi
275
+ else
276
+ warn "Sail not available — skipping container startup"
277
+ fi
278
+ echo ""
279
+ fi
280
+
281
+ # ─── Isolated Docker stack (--isolated) ──────────────────────────────────────────
282
+
283
+ if [[ "$ISOLATED" == true ]] && [[ "$IS_CLOUD" == false ]]; then
284
+ echo "🔒 Isolated Environment Setup"
285
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
286
+
287
+ BRANCH_SLUG=$(get_branch_slug)
288
+ OFFSET=$(port_hash "$BRANCH_SLUG")
289
+
290
+ # Calculate ports
291
+ ISO_DB_PORT=$((4300 + OFFSET))
292
+ ISO_REDIS_PORT=$((7300 + OFFSET))
293
+ ISO_APP_PORT=$((8000 + OFFSET))
294
+ ISO_ADMINER_PORT=$((9000 + OFFSET))
295
+ ISO_MEILI_PORT=$((8700 + OFFSET))
296
+ ISO_SOKETI_PORT=$((7100 + OFFSET))
297
+
298
+ # Collision check
299
+ COLLISION_FOUND=true
300
+ ATTEMPTS=0
301
+ while [[ "$COLLISION_FOUND" == true ]] && [[ $ATTEMPTS -lt 10 ]]; do
302
+ COLLISION_FOUND=false
303
+ for port in $ISO_DB_PORT $ISO_REDIS_PORT $ISO_APP_PORT $ISO_ADMINER_PORT $ISO_MEILI_PORT $ISO_SOKETI_PORT; do
304
+ if port_in_use "$port"; then
305
+ if docker ps --filter "name=wt-${BRANCH_SLUG}" --format "{{.Ports}}" 2>/dev/null | grep -q ":${port}->"; then
306
+ continue
307
+ fi
308
+ COLLISION_FOUND=true
309
+ OFFSET=$(( (OFFSET + 1) % 100 ))
310
+ ISO_DB_PORT=$((4300 + OFFSET))
311
+ ISO_REDIS_PORT=$((7300 + OFFSET))
312
+ ISO_APP_PORT=$((8000 + OFFSET))
313
+ ISO_ADMINER_PORT=$((9000 + OFFSET))
314
+ ISO_MEILI_PORT=$((8700 + OFFSET))
315
+ ISO_SOKETI_PORT=$((7100 + OFFSET))
316
+ break
317
+ fi
318
+ done
319
+ ATTEMPTS=$((ATTEMPTS + 1))
320
+ done
321
+
322
+ info "Branch: $BRANCH_SLUG → offset $OFFSET"
323
+ info "Ports: App=$ISO_APP_PORT DB=$ISO_DB_PORT Redis=$ISO_REDIS_PORT"
324
+
325
+ # Resolve template — try DX plugin first, then main repo fallback
326
+ TEMPLATE_PATH="${DX_PLUGIN_DIR}/docker/worktree/docker-compose.isolated.template.yaml"
327
+ [[ ! -f "$TEMPLATE_PATH" ]] && TEMPLATE_PATH="${ROOT_WORKTREE_PATH}/docker/worktree/docker-compose.isolated.template.yaml"
328
+ if [[ ! -f "$TEMPLATE_PATH" ]]; then
329
+ error "Docker template not found"
330
+ echo " Expected at: ${DX_PLUGIN_DIR}/docker/worktree/docker-compose.isolated.template.yaml" >&2
331
+ exit 1
332
+ fi
333
+
334
+ export COMPOSE_PROJECT_NAME="wt-${BRANCH_SLUG}"
335
+ export DB_PORT="$ISO_DB_PORT"
336
+ export DB_PASSWORD="reservine_wt_pass"
337
+ export REDIS_PASSWORD="t8eGxQbCd6uYTEPrGN2Rgta9SULy6qar"
338
+ export REDIS_PORT="$ISO_REDIS_PORT"
339
+ export APP_PORT="$ISO_APP_PORT"
340
+ export ADMINER_PORT="$ISO_ADMINER_PORT"
341
+ export MEILI_PORT="$ISO_MEILI_PORT"
342
+ export SOKETI_PORT="$ISO_SOKETI_PORT"
343
+
344
+ # Resolve vendor path (follow symlink to real directory)
345
+ if [[ -L "vendor" ]]; then
346
+ export VENDOR_PATH="$(readlink -f vendor)"
347
+ else
348
+ export VENDOR_PATH="$(pwd)/vendor"
349
+ fi
350
+
351
+ # Mount main repo at its original path so Composer's absolute path references resolve
352
+ export ROOT_REPO_PATH="$ROOT_WORKTREE_PATH"
353
+
354
+ envsubst < "$TEMPLATE_PATH" > docker-compose.isolated.yaml
355
+ success "Generated docker-compose.isolated.yaml"
356
+
357
+ # Patch .env with isolated credentials
358
+ if [[ -f ".env" ]]; then
359
+ sed -i.bak \
360
+ -e "s|^DB_HOST=.*|DB_HOST=db|" \
361
+ -e "s|^DB_PORT=.*|DB_PORT=3306|" \
362
+ -e "s|^DB_PASSWORD=.*|DB_PASSWORD=${DB_PASSWORD}|" \
363
+ -e "s|^DB_USERNAME=.*|DB_USERNAME=reservine|" \
364
+ -e "s|^DB_DATABASE=.*|DB_DATABASE=reservine|" \
365
+ -e "s|^REDIS_HOST=.*|REDIS_HOST=redis|" \
366
+ -e "s|^REDIS_PORT=.*|REDIS_PORT=6379|" \
367
+ .env
368
+ rm -f .env.bak
369
+ success "Patched .env with isolated connection settings"
370
+ fi
371
+
372
+ # Start the stack
373
+ info "Starting isolated Docker stack..."
374
+ COMPOSE_PROJECT_NAME="wt-${BRANCH_SLUG}" \
375
+ docker compose -f docker-compose.isolated.yaml up -d 2>&1 | tail -5
376
+ success "Docker stack started"
377
+
378
+ # Regenerate Composer autoloader inside container
379
+ info "Regenerating autoloader..."
380
+ docker exec "wt-${BRANCH_SLUG}-app" composer dump-autoload -d /var/www/html --quiet 2>/dev/null || true
381
+ success "Autoloader regenerated"
382
+
383
+ # Wait for DB healthy
384
+ info "Waiting for DB to be healthy..."
385
+ for i in $(seq 1 30); do
386
+ if docker exec "wt-${BRANCH_SLUG}-db" mariadb-admin ping -h localhost -ureservine -preservine_wt_pass >/dev/null 2>&1; then
387
+ success "DB is healthy"
388
+ break
389
+ fi
390
+ if [[ $i -eq 30 ]]; then
391
+ error "DB failed to become healthy after 30 attempts"
392
+ exit 1
393
+ fi
394
+ sleep 2
395
+ done
396
+
397
+ # Auto-import snapshot
398
+ SNAPSHOT_PATH="${ROOT_WORKTREE_PATH}/snapshots/reservine.sql.gz"
399
+ SEED_SCRIPT="${DX_PLUGIN_DIR}/docker/worktree/seed-snapshot.sh"
400
+ [[ ! -f "$SEED_SCRIPT" ]] && SEED_SCRIPT="${ROOT_WORKTREE_PATH}/docker/worktree/seed-snapshot.sh"
401
+ "$SEED_SCRIPT" \
402
+ docker-compose.isolated.yaml "$SNAPSHOT_PATH" "$DB_PASSWORD" "$COMPOSE_PROJECT_NAME"
403
+
404
+ # Generate FE proxy config for sibling worktree
405
+ FE_WORKTREE=$(find_fe_worktree || echo "")
406
+ if [[ -n "$FE_WORKTREE" ]]; then
407
+ cat > "$FE_WORKTREE/proxy.conf.json" <<PROXY_EOF
408
+ {
409
+ "/api": {
410
+ "target": "http://localhost:${ISO_APP_PORT}",
411
+ "secure": false,
412
+ "changeOrigin": true
413
+ },
414
+ "/sanctum": {
415
+ "target": "http://localhost:${ISO_APP_PORT}",
416
+ "secure": false,
417
+ "changeOrigin": true
418
+ }
419
+ }
420
+ PROXY_EOF
421
+ success "Generated FE proxy: $FE_WORKTREE/proxy.conf.json"
422
+
423
+ # Copy existing .claude/config.local from FE main repo (has developer's credentials)
424
+ # then append/override the isolated BE API URL
425
+ mkdir -p "$FE_WORKTREE/.claude"
426
+ FE_MAIN_CONFIG_LOCAL=""
427
+ if [[ -n "${ROOT_WORKTREE_PATH:-}" ]]; then
428
+ FE_MAIN_CONFIG_LOCAL="$(dirname "$ROOT_WORKTREE_PATH")/reservine/.claude/config.local"
429
+ fi
430
+ if [[ -f "$FE_MAIN_CONFIG_LOCAL" ]]; then
431
+ cp "$FE_MAIN_CONFIG_LOCAL" "$FE_WORKTREE/.claude/config.local"
432
+ # Append/override the isolated BE API URL
433
+ sed -i.bak \
434
+ -e "s|^MCP_LOCALBE_API_URL=.*|MCP_LOCALBE_API_URL=http://localhost:${ISO_APP_PORT}/api|" \
435
+ "$FE_WORKTREE/.claude/config.local"
436
+ # If the key didn't exist, append it
437
+ grep -q "^MCP_LOCALBE_API_URL=" "$FE_WORKTREE/.claude/config.local" || \
438
+ echo "MCP_LOCALBE_API_URL=http://localhost:${ISO_APP_PORT}/api" >> "$FE_WORKTREE/.claude/config.local"
439
+ rm -f "$FE_WORKTREE/.claude/config.local.bak"
440
+ else
441
+ # No main config.local found — create minimal one with just the API URL
442
+ cat > "$FE_WORKTREE/.claude/config.local" <<CONFIG_EOF
443
+ # Generated by setup-worktree-be.sh --isolated
444
+ # Add your MCP_DEV_EMAIL and MCP_DEV_PASSWORD here
445
+ MCP_LOCALBE_BASE_URL=http://localhost:1112
446
+ MCP_LOCALBE_API_URL=http://localhost:${ISO_APP_PORT}/api
447
+ CONFIG_EOF
448
+ warn "No .claude/config.local found in FE main repo — created minimal config"
449
+ info "Add your MCP_DEV_EMAIL/PASSWORD to: $FE_WORKTREE/.claude/config.local"
450
+ fi
451
+ success "Generated FE config: $FE_WORKTREE/.claude/config.local"
452
+ else
453
+ warn "No FE worktree found — skipping proxy config generation"
454
+ info "To generate manually, create proxy.conf.json targeting http://localhost:${ISO_APP_PORT}"
455
+ fi
456
+
457
+ echo ""
458
+ echo "🎉 Isolated environment ready!"
459
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
460
+ echo ""
461
+ echo " BE API: http://localhost:${ISO_APP_PORT}/api"
462
+ echo " Adminer: http://localhost:${ISO_ADMINER_PORT}"
463
+ echo " DB Port: ${ISO_DB_PORT}"
464
+ echo " Redis Port: ${ISO_REDIS_PORT}"
465
+ echo ""
466
+ if [[ -n "$FE_WORKTREE" ]]; then
467
+ echo " FE serve command:"
468
+ echo " cd $FE_WORKTREE"
469
+ echo " nx serve reservine --port 1112 --configuration=isolatedBE --proxy-config=proxy.conf.json"
470
+ echo ""
471
+ fi
472
+ echo " Teardown: ./setup-worktree-be.sh --teardown"
473
+ echo " Re-seed: ./setup-worktree-be.sh --reseed"
474
+ echo ""
475
+
476
+ SKIP_SAIL=true
477
+ fi
478
+
479
+ # ─── Final output ───────────────────────────────────────────────────────────────
480
+
481
+ echo "✨ Worktree setup complete!"
482
+ echo ""
483
+
484
+ if [[ "$IS_CLOUD" == false ]] && [[ "$ISOLATED" == false ]]; then
485
+ echo "⚠️ IMPORTANT: Service Locality Warning"
486
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
487
+ echo "These commands use SHARED local services (database, redis, meilisearch):"
488
+ echo " • sail artisan / php artisan"
489
+ echo " • sail test"
490
+ echo ""
491
+ echo "PHPStan can safely use main repo's vendor:"
492
+ echo " vendor/bin/phpstan analyze <files>"
493
+ echo ""
494
+ echo "Services available at:"
495
+ echo " Laravel: http://localhost:80"
496
+ echo " Vite: http://localhost:5173"
497
+ echo " Adminer: http://localhost:8080"
498
+ echo ""
499
+ fi
500
+
501
+ print_state "vendor" "$BRANCH_NAME"