synxs-ar/laravel-worktrees

Persistent, isolated Laravel dev environments ("desks") backed by git worktrees — dynamic free-port resolution for PHP + Vite, per-desk SQLite, unique app keys and storage links.

Maintainers

Package info

github.com/synxs-ar/laravel-worktrees

pkg:composer/synxs-ar/laravel-worktrees

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.0 2026-06-07 16:35 UTC

This package is auto-updated.

Last update: 2026-06-07 17:44:44 UTC


README

Persistent, isolated Laravel dev environments — "desks" — backed by git worktrees.

Run several branches of the same Laravel app at the same time, each with its own port, database, storage and encryption key, without them stepping on each other.

Think of your project as a workshop and each worktree as a numbered desk (wt-desk-01, wt-desk-02, …). A desk is a permanent workbench: it keeps its own vendor/, node_modules/, APP_KEY, database and storage. Mount any branch or experiment on a desk, work on it, leave it — the bench stays put, the work comes and goes.

php artisan wt:new      # build the next desk
php artisan wt:up 1     # configure (first run) & serve it — PHP + Vite, free ports

Built for parallel coding agents

Modern AI coding agents make it easy to work on several features at once — until they're all editing the same checkout. They overwrite each other's files, can't hold separate branches, and fight over a single dev server and database.

laravel-worktrees gives each agent — or each feature — its own isolated desk: a real git worktree with its own branch, ports, database and storage. Point one agent at wt-desk-01, another at wt-desk-02, label each with the feature it's building, and they code in parallel without ever stepping on one another. It's the foundation for running many branches at the same time — ideal for vibe coding at scale.

Why

Spinning up a second copy of a Laravel app for a parallel branch is more painful than it should be:

  • php artisan serve always wants port 8000collisions.
  • Every copy points at the same database → data bleeds across branches.
  • A fresh worktree has no .env, no vendor/, no node_modules/, no storage link, and a stale vite.config.jsmanual setup every time.
  • Copy the .env by hand and every desk shares one APP_KEYleaked sessions and encryption between environments.

laravel-worktrees automates all of it behind four commands, and lets you decide — per desk, interactively — whether the database and storage are isolated or shared with your main checkout.

Requirements

  • PHP ^8.1
  • Laravel 10, 11 or 12
  • git on the PATH
  • For an isolated database: a reachable PostgreSQL (default) or MySQL server

Install

composer require --dev synxs-ar/laravel-worktrees

Optionally publish the config:

php artisan vendor:publish --tag=worktrees-config

Add the desk folders to your app's .gitignore:

/worktrees
/.wt-desks

Commands

Command What it does
wt:new [--ref=HEAD] Provision the next desk (worktree + deps + env). Does not touch the database.
wt:up {desk} [label] [--no-vite] [--reconfigure] Configure the desk on first run (database + storage), then serve PHP + Vite. An optional label is applied to APP_NAME.
wt:label {desk} [label] [--clear] Set, show or clear a desk's display label.
wt:list List every desk, its label and preferred-port status.
wt:rm {desk} [--force] Tear a desk down (worktree + git metadata) and free its slot.

{desk} accepts the full slug or the bare number — wt:up wt-desk-01 and wt:up 1 are equivalent.

Labels

By default a desk tags its APP_NAME with the slug — MyApp [wt-desk-01] — so you can tell environments apart in the browser, logs and mail. Give it a human-friendly label to make that obvious at a glance:

php artisan wt:up 1 checkout-redesign   # APP_NAME -> "MyApp [checkout-redesign]", then serves
php artisan wt:label 1 billing-fix      # rename it any time (no serving)
php artisan wt:label 1                  # show the current label
php artisan wt:label 1 --clear          # back to "MyApp [wt-desk-01]"

Labels are remembered in the registry and re-applied on every wt:up.

How it works

1. wt:new — build the bench

✓ Creating git worktree            git worktree add --detach worktrees/wt-desk-01
✓ Installing dependencies (composer)   an isolated vendor/
✓ Installing node modules (npm)        an isolated node_modules/ (if package.json exists)
✓ Materializing .env                   derived from your base .env, APP_NAME tagged [wt-desk-01]
✓ Bootstrapping local config (vite, …) copies gitignored *.example files (e.g. vite.config.js)
✓ Generating APP_KEY                   a UNIQUE key — sessions & encryption stay isolated

The worktree is detached — a desk is branch-agnostic. Check out whatever you want inside it; the desk's identity (port, database, storage) never moves.

2. wt:up — choose & serve

The first time you bring a desk up (or any time with --reconfigure) it asks you two questions and remembers the answers:

? Database for this desk?     [isolated / shared]
    isolated → prompts host / port / user / password / name
               offers to CREATE the database if it doesn't exist yet
               runs your migrations against it
    shared   → uses the project's database from the base .env (never migrated)

? Storage for this desk?      [isolated / shared]
    shared   → media in storage/app/public is shared with your main checkout
               (logs, cache and sessions stay isolated)
    isolated → the desk gets its own storage

After that (and on every later wt:up) it just serves, on dynamically resolved free ports:

  PHP  http://127.0.0.1:18001
  Vite http://127.0.0.1:27501

  wt-desk-01 is up. Ctrl+C to stop.

Ctrl+C cleanly stops the whole process tree (PHP server and Vite/esbuild) — no orphans left holding your files.

Smart ports

Each desk has preferred ports (base + desk number, e.g. PHP 18001, Vite 27501), but the preferred port is only a hint: wt:up probes for a real free port at launch, so a port held by Docker or another desk never blocks you.

All ports stay above a configurable floor (default 10001) — on Windows with Hyper-V / WSL2 / Docker the ranges below 10000 are reserved and throw error 10013 on bind.

Vite

If the desk has a package.json, wt:up runs npm run dev -- --port <free> --strictPort alongside the PHP server. The port is forced via the CLI (which beats vite.config.js), so the project needs no config changes. Skip it with --no-vite.

Database strategies

Chosen per desk at first wt:up:

  • Isolated — a dedicated database (default driver pgsql, or mysql), created automatically from your base credentials. If the server is up but the database doesn't exist, you're offered to create it. Then your migrations run against it. Real isolation, your data can't leak.
  • Shared — the desk uses your project's existing database from the base .env. Migrations are never auto-run against a shared database.

The isolated database name defaults to {base}_{slug} (e.g. myapp_wt_desk_01).

Why Postgres by default? PostgreSQL has full ALTER TABLE support, so your existing migrations run unchanged. SQLite's limited ALTER TABLE (it can't drop a column referenced by a foreign key) breaks many real-world migration suites.

Storage strategies

Chosen per desk at first wt:up:

  • Isolated — the desk has its own storage/. storage:link as usual.
  • Shared — the desk's storage/app/public is junctioned to your main checkout's, so uploaded media is shared. Logs, cache and sessions stay isolated.

Configuration

config/worktrees.php:

Key Env Default Notes
host WORKTREES_HOST 127.0.0.1 Bind host for serving & port probing.
port_floor WORKTREES_PORT_FLOOR 10001 No resolved port goes below this.
php_base_port WORKTREES_PHP_BASE_PORT 18000 Preferred PHP port = base + desk number.
vite_base_port WORKTREES_VITE_BASE_PORT 27500 Preferred Vite port = base + desk number.
database.engine WORKTREES_DB_ENGINE pgsql Driver for an isolated database (pgsql / mysql).
database.name WORKTREES_DB_NAME {base}_{slug} Isolated database name template.
bootstrap_files vite.config.js ⇐ vite.config.example.js Gitignored files seeded from a committed example.
registry_path .wt-desks/registry.json The "workshop blueprint" (keep out of git).
worktrees_path worktrees Where desks are created.
php / composer / npm WORKTREES_PHP / …COMPOSER / …NPM php / composer / npm Binaries used to drive desks.

Isolation, the careful bits

  • Unique APP_KEY per desk, so sessions and encrypted payloads never cross.
  • Environment stripping — desk child processes are spawned by your main app's artisan command and would otherwise inherit its putenv()'d values (DB, APP_KEY, APP_URL); Laravel's immutable Dotenv then refuses to override them. Each desk-owned key is stripped from the inherited environment so the worktree .env always wins.
  • APP_URL is written into the desk .env to match the resolved port (php artisan serve forwards only a whitelist of env vars to its php -S worker, so a runtime-injected value never arrives), and also injected into the Vite process (Node does inherit, so it must be overridden there).

Windows notes

Everything works on Windows out of the box:

  • storage:link falls back to a directory junction (mklink /J) when the symlink needs elevation it doesn't have.
  • wt:rm removes storage junctions as links (never following them into their target) and handles node_modules paths that exceed MAX_PATH.
  • Ctrl+C terminates the full child process tree (taskkill /T).

License

MIT © Synxs