Laravel Workspace & Worktree Skill
This skill covers two related workflows:
- Script Generation — Generate
setup.sh,archive.sh,run.sh, and Codex environment config for a Laravel project - Worktree Management — Create isolated development environments using git worktrees, powered by those generated scripts
Part 1: Script Generation
Generate setup, archive, and run scripts for Laravel applications. Scripts support MySQL, PostgreSQL, and SQLite databases, detect project services from composer.json and .env, and include worktrunk (wt) and Codex worktree integration.
What Gets Generated
| File | Purpose |
|---|---|
scripts/setup.sh |
Install deps, create DB, configure .env, migrate & seed |
scripts/archive.sh |
Stop processes, unlink Valet, drop DB, clean up |
scripts/run.sh |
Run detected dev services (horizon, queue, vite, etc.) via concurrently |
.config/wt.toml |
Worktrunk hooks — wires setup.sh/archive.sh into wt lifecycle |
.codex/environments/environment.toml |
Codex environment config embedding the setup script |
Detection Workflow
Before generating scripts, inspect the target project to determine:
Database driver — Run php artisan about --json and read drivers.database to determine what the project actually uses. Display this in the detection summary shown to the user.
pgsql→ PostgreSQL commands (psql, createdb, dropdb, pg_isready)mysql→ MySQL commands (mysql, mysqladmin)sqlite→ File-based (touch database file, rm to clean)
This is more reliable than reading .env.example or .env because the actual running config may differ.
IMPORTANT — Dynamic scripts: The generated setup.sh and archive.sh scripts must detect the DB driver at runtime by reading DB_CONNECTION from .env after the copy step. Do NOT hardcode a single driver. Include all driver branches (pgsql, mysql, sqlite) in every generated script.
Project name — Derive via ${CLAUDE_PLUGIN_ROOT}/scripts/detect-project-name.sh. Use lowercase, hyphenated form for Valet domains and underscored form for database names.
Services — Run ${CLAUDE_PLUGIN_ROOT}/scripts/detect-services.sh from the target project root, or scan composer.json require keys and .env.example for:
| Service | Detection | Impact |
|---|---|---|
| Horizon | laravel/horizon in composer.json |
Add to run.sh, check Redis connectivity |
| Meilisearch/Scout | laravel/scout + MEILISEARCH_HOST in .env |
Add fallback to null driver if unreachable |
| Redis | REDIS_HOST in .env or predis/predis in composer.json |
Add connectivity check |
| Reverb | laravel/reverb in composer.json |
Add reverb:start to run.sh |
| Pulse | laravel/pulse in composer.json |
Note in setup output |
| Octane | laravel/octane in composer.json |
Use octane:start instead of serve in run.sh |
| Pail | laravel/pail in composer.json |
Add pail --timeout=0 to run.sh |
Frontend — Check package.json for Vite (default) or other build tools.
setup.sh Structure
Follow this order exactly:
- Tool checks — Verify php, composer, npm, valet exist (DB CLI tools checked later based on driver)
- Workspace name —
WT_WORKSPACE_NAMEfallback tobasename $PWD - Codex worktree key — Extract from path if inside
~/.codex/worktrees/<id>/ - Root path —
WT_ROOT_PATHfallback to$(dirname "$0")/.. - Install dependencies —
composer installwith retry,npm install - Setup .env — Copy from root path or
.env.example, handle broken symlinks - Valet link —
{project}-{worktree_key}.test, HTTP only (novalet secure) - Detect DB driver — Read
DB_CONNECTIONfrom.env(after copy), default tosqlite - Create database — Branch on driver: pgsql (createdb), mysql (CREATE DATABASE), sqlite (touch file). See
references/database-drivers.md - Update .env — DB_DATABASE, APP_URL, SESSION_DOMAIN, SESSION_SECURE_COOKIE, SANCTUM_STATEFUL_DOMAINS
- Service checks — Disable unreachable services (e.g., Scout → null driver)
- App key — Generate if missing
- Cache clear —
php artisan optimize:clear - Storage link —
php artisan storage:link --force - Vite port — Configure from
WT_PORTif available - Migrate & seed —
php artisan migrate --seed --force - Summary output — URL, database name/path, next steps
archive.sh Structure
- Workspace name & variables — Same derivation as setup.sh
- Stop processes — Kill Vite processes scoped to workspace directory
- Valet cleanup —
valet unsecure(backwards compat) thenvalet unlink - Drop database — Driver-specific commands (see
references/database-drivers.md) - Summary output — What was removed
run.sh Structure
Use npx concurrently with named, color-coded processes. Only include detected services:
npx concurrently -k \
-n "horizon,schedule,logs,vite" \
-c "#93c5fd,#c4b5fd,#fb7185,#fdba74" \
"php artisan horizon" \
"php artisan schedule:work" \
"php artisan pail --timeout=0" \
"npm run dev"
Common process mappings:
- Horizon →
php artisan horizon(requires Redis) - Queue (no Horizon) →
php artisan queue:listen --tries=1 - Scheduler →
php artisan schedule:work - Logs →
php artisan pail --timeout=0 - Vite →
npm run dev - Reverb →
php artisan reverb:start
wt.toml Structure
Generate .config/wt.toml so that worktrunk (wt switch --create / wt remove) automatically runs the lifecycle scripts. Adapt hooks based on detected services.
[post-start]
setup = """
echo "Pre-copying dependencies from main project..."
cp -R {{ primary_worktree_path }}/vendor vendor 2>/dev/null && echo "vendor/ copied" || echo "No vendor/ to copy"
cp -R {{ primary_worktree_path }}/node_modules node_modules 2>/dev/null && echo "node_modules/ copied" || echo "No node_modules/ to copy"
WT_ROOT_PATH={{ primary_worktree_path }} bash scripts/setup.sh
"""
[pre-commit]
pint = "vendor/bin/pint --dirty"
# Include phpstan only if phpstan.neon or phpstan.neon.dist exists
phpstan = "vendor/bin/phpstan analyse --memory-limit=512M"
[pre-merge]
test = "php artisan test --parallel --compact"
[pre-remove]
archive = "bash scripts/archive.sh"
[list]
url = "http://{project}-{{ branch | sanitize }}.test"
Key decisions:
post-start(background) instead ofpost-create(blocking) — worktree creation feels instant, setup runs in backgroundWT_ROOT_PATHmust be set so setup.sh copies.envfrom the main projectWT_WORKSPACE_NAMEis not needed — setup.sh defaults tobasename "$PWD"which is the sanitized branchpre-commithooks run duringwt mergebefore the squash commit — Pint formats dirty files, PHPStan runs static analysispre-mergeruns the full test suite before merge to target branch[list] urlshows the Valet domain inwt listoutput- Include
phpstanhook only ifphpstan.neonorphpstan.neon.distexists in the project - The
{project}placeholder must be replaced with the actual detected project name
.gitignore Update
Append /.worktrees/ to the project's .gitignore if not already present. This prevents worktree directories from being committed.
environment.toml Structure
# THIS IS AUTOGENERATED. DO NOT EDIT MANUALLY
version = 1
name = "{project-name}"
[setup]
script = '''
{contents of setup.sh}
'''
Key Patterns
.env Manipulation Helper
Include this function in both setup.sh and archive.sh:
env_value() { grep "^$1=" .env | cut -d '=' -f2- | sed "s/^[\"']//;s/[\"']$//"; }
.env Key Update Pattern (macOS-compatible sed)
if grep -q "^KEY=" .env; then
sed -i '' "s/^KEY=.*/KEY=value/" .env
else
echo "KEY=value" >> .env
fi
Worktree Key Derivation
WORKTREE_KEY="$WORKSPACE_NAME"
if [[ "$PWD" =~ /worktrees/([^/]+)/ ]]; then
WORKTREE_KEY="${BASH_REMATCH[1]}"
fi
WORKTREE_KEY=$(echo "$WORKTREE_KEY" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9-' '-')
WORKTREE_KEY="${WORKTREE_KEY#-}"
WORKTREE_KEY="${WORKTREE_KEY%-}"
if [ -z "$WORKTREE_KEY" ]; then
WORKTREE_KEY="$WORKSPACE_NAME"
fi
Database Naming
Sanitize hyphens to underscores for all DB drivers. Validate before use:
DB_NAME=$(echo "{project}_{workspace}" | tr '-' '_')
if [[ ! "$DB_NAME" =~ ^[a-zA-Z0-9_]+$ ]]; then
echo "Error: Invalid database name '$DB_NAME'"; exit 1
fi
Note: The scripts/setup.sh version uses {project}_{workspace} (2 parts). The .codex/environments/environment.toml version uses {project}_{workspace}_{worktree} (3 parts) because Codex worktrees need unique databases per worktree. Match the convention to the target.
Vite Process Killing (archive.sh)
Kill only Vite processes scoped to the current workspace directory:
WORKSPACE_DIR="$(pwd)"
VITE_KILLED=0
for pid in $(pgrep -f "node.*vite" 2>/dev/null); do
if lsof -p "$pid" 2>/dev/null | grep -q "$WORKSPACE_DIR"; then
kill "$pid" 2>/dev/null && VITE_KILLED=$((VITE_KILLED + 1)) || true
fi
done
File Permissions
After generating scripts, make them executable:
chmod +x scripts/setup.sh scripts/archive.sh scripts/run.sh
Reference Files
references/database-drivers.md— Complete database setup/teardown commands for MySQL, PostgreSQL, and SQLite with connectivity checks, creation, and cleanup
Part 2: Worktree Management
Overview
This skill manages git worktrees for Laravel projects served by Laravel Valet. It creates isolated development environments where each feature branch gets its own:
- Directory (in
.worktrees/) - Valet domain (
projectname-branchname.test) - Database (
projectname_branchname) - Vite dev server instance
This enables parallel work on multiple features without switching branches or corrupting shared state.
Initial Flow
ALWAYS start by checking for existing worktrees:
git worktree list
If worktrees exist:
Use AskUserQuestion to ask:
header: "Worktree Action"
question: "You have existing worktrees. What would you like to do?"
options:
- label: "Set up new worktree"
description: "Create a new worktree for a different feature branch"
- label: "Finish existing worktree"
description: "Complete work on a worktree (PR, merge, or abandon)"
If no worktrees exist:
Proceed directly to asking for the branch name.
Setup Workflow (Scripts-Based)
Pre-requisite: The project must have scripts/setup.sh. If missing, generate scripts first using the /scripts command or the script generation workflow in Part 1.
Step 1: Get Branch Name
Use AskUserQuestion:
header: "Branch Name"
question: "What branch name do you want to create for this worktree?"
Sanitize the branch name:
SANITIZED_BRANCH=$(echo "$BRANCH" | tr '/' '-' | tr ' ' '-' | tr '[:upper:]' '[:lower:]')
Step 2: Detect Project Name
PROJECT=$(${CLAUDE_PLUGIN_ROOT}/scripts/detect-project-name.sh)
Step 3: Detect Base Branch
BASE_BRANCH=$(git config init.defaultBranch 2>/dev/null || echo "main")
git show-ref --verify --quiet refs/heads/$BASE_BRANCH || BASE_BRANCH="master"
Step 4: Check for Scripts
if [ ! -f scripts/setup.sh ]; then
# Inform user and trigger /scripts generation first
fi
Step 5: Create Worktree
git worktree add .worktrees/$SANITIZED_BRANCH -b $BRANCH $BASE_BRANCH
Step 6: Copy Scripts
cp -r scripts/ .worktrees/$SANITIZED_BRANCH/scripts/
Step 6.5: Copy Dependencies
Copy vendor/ and node_modules/ from the main project before running setup. Since the worktree shares the same composer.lock and package-lock.json, these directories are identical — turning composer install / npm install into fast verification steps instead of full installs.
echo "Pre-copying dependencies from main project..."
cp -R vendor/ .worktrees/$SANITIZED_BRANCH/vendor/ 2>/dev/null && echo "vendor/ copied" || echo "No vendor/ to copy"
cp -R node_modules/ .worktrees/$SANITIZED_BRANCH/node_modules/ 2>/dev/null && echo "node_modules/ copied" || echo "No node_modules/ to copy"
Why here and not in
setup.sh?setup.shis also used by Codex/Conductor environments where there's no parent project to copy from. The worktree command always has a parent project available.
Step 7: Run setup.sh
cd .worktrees/$SANITIZED_BRANCH
WT_WORKSPACE_NAME=$SANITIZED_BRANCH \
WT_ROOT_PATH=$(cd ../.. && pwd) \
bash scripts/setup.sh
This single command replaces the old 15 inline steps (Valet link, .env config, DB creation, dependencies, migrations, etc.).
Step 8: Fix Vite Configuration (if needed)
Check if vite.config.js or vite.config.ts has CORS settings. If missing, add:
server: {
host: 'localhost',
cors: true,
}
Step 9: Setup Warp Launch Configuration
Skip if ~/.warp/ doesn't exist.
mkdir -p ~/.warp/launch_configurations
WORKTREE_PATH="$(pwd)"
sed -e "s|{{WORKTREE_PATH}}|$WORKTREE_PATH|g" \
-e "s|{{WORKTREE_NAME}}|$SANITIZED_BRANCH|g" \
${CLAUDE_PLUGIN_ROOT}/templates/laravel-worktree.yaml \
> ~/.warp/launch_configurations/laravel-worktree.yaml
Step 10: Display Summary
## Worktree Created Successfully
| Item | Value |
|------|-------|
| Branch | $BRANCH |
| Directory | .worktrees/$SANITIZED_BRANCH/ |
| URL | http://$PROJECT-$SANITIZED_BRANCH.test |
| Database | ${PROJECT}_${SANITIZED_BRANCH} |
### Next Steps
1. **Open Warp layout:** Press `Cmd+Ctrl+L` and select "Laravel Worktree"
2. **Start services:** Run `bash scripts/run.sh` (or use the Warp layout)
3. **Open in browser:** Run `browse` or visit the URL above
**IMPORTANT:** All subsequent work must use the worktree directory:
`.worktrees/$SANITIZED_BRANCH/`
Finishing Workflow
When the user wants to finish work on a worktree, use AskUserQuestion:
header: "Finish Worktree"
question: "How would you like to complete this worktree?"
options:
- label: "Create PR"
description: "Push branch and create a pull request on GitHub"
- label: "Transfer to main"
description: "Merge changes into main directory (no PR)"
- label: "Abandon"
description: "Discard all changes and remove worktree"
Option A: Create PR
- Gather task info (optional)
- Commit changes
- Push and create PR:
git push -u origin HEAD && gh pr create --fill - After PR is merged, cleanup using
archive.shthen git cleanup:cd .worktrees/$SANITIZED_BRANCH WT_WORKSPACE_NAME=$SANITIZED_BRANCH bash scripts/archive.sh cd ../.. git worktree remove .worktrees/$SANITIZED_BRANCH --force git branch -D $BRANCH
Option B: Transfer to Main
- Merge with no-commit:
git merge .worktrees/$SANITIZED_BRANCH --no-commit --no-ff - If conflicts, help resolve them
- Cleanup using archive.sh + git cleanup (same as above)
Option C: Abandon
- Confirm with user (destructive action)
- Run archive.sh then git cleanup (same as above)
Quick Reference
| Item | Pattern |
|---|---|
| Worktree path | .worktrees/{sanitized-branch}/ |
| Domain | {project}-{sanitized-branch}.test |
| Database | {project}_{sanitized_branch} |
| Protocol | HTTP only (no SSL) |
| Setup | bash scripts/setup.sh |
| Teardown | bash scripts/archive.sh + git cleanup |
| Services | bash scripts/run.sh |
Variable Naming
| Variable | Description | Example |
|---|---|---|
$BRANCH |
Original branch name | feature/user-auth |
$SANITIZED_BRANCH |
Filesystem-safe version | feature-user-auth |
$PROJECT |
Project name | myproject |
$BASE_BRANCH |
Main branch | main or master |
Troubleshooting
See references/troubleshooting.md for common issues including:
- 401 Unauthorized errors
- Cookie/session issues
- CORS and Vite errors
- Mixed content warnings
- Database connection problems
Important Notes
- Always use HTTP — Valet secure causes Vite mixed content issues
- Database names use underscores — MySQL doesn't like hyphens in unquoted identifiers
- Kill existing Vite — Port conflicts cause silent failures
- Storage link with --force — Overwrites existing symlinks safely
- Work from worktree directory — All commands after setup must run from
.worktrees/$SANITIZED_BRANCH/ - Scripts must exist — Generate with
/scriptsbefore creating worktrees
Converted and distributed by TomeVault — claim your Tome and manage your conversions.