This project runs on lerd, a Podman-based PHP development environment. It is framework-agnostic: Laravel, Symfony, WordPress, Drupal, Magento, CakePHP and any custom framework are all driven by a framework definition (YAML), never by lerd hardcoding a framework's name. The lerd MCP server is available — use it to manage the environment without leaving the chat.
The MCP surface is twelve grouped tools, each driven by an action argument: site, service, db, env, runtime, worker, exec, framework, diag, logs, worktree, workspace. Always pass action. Most actions also accept an optional path that defaults to the directory the assistant was opened in (then LERD_SITE_PATH if set), so you can usually omit it. Start by calling site with action: "list" to discover sites.
- PHP runs in Podman containers named
lerd-php<version>-fpm(e.g.lerd-php84-fpm); each container includes composer and node/npm; the PHP version is resolved from.lerd.yaml→.php-version→composer.jsonrequire.phpconstraint (matched against installed versions) → global default - Nginx routes
*.testdomains to the correct PHP-FPM container - Services (MySQL, Redis, PostgreSQL, etc.) and custom services run as Podman containers via systemd quadlets
- Node.js versions run through a version manager, fnm (bundled, fetched on demand) or user nvm (declining managed Node picks nvm;
node.manager+node.nvm_dir;node:manager/dashboard; nvm keeps PATH, fnm uses shims); per-project version via a.node-versionfile. The package manager is the project's, not lerd's: apackageManagerpin inpackage.jsonwins, then the lockfile (pnpm-lock.yaml→ pnpm,yarn.lock→ yarn,bun.lock*→ bun, else npm). pnpm and yarn run through corepack, and installs use the manager's frozen-lockfile mode (pnpm install --frozen-lockfile,yarn install --immutable,npm ci). Never assumenpm run dev/npm ci— the worker command and the setup steps follow the detected manager - Framework workers (queue, schedule, reverb, horizon, messenger, vite, etc.) run as systemd user services named
lerd-<worker>-<sitename>; commands are defined per-framework in YAML; Laravel Horizon is auto-detected fromcomposer.jsonand replaces the queue toggle when installed; Laravel ships with avitehost worker that runs the project's dev script on the host for HMR, through whichever package manager the project pins; workers and setup commands support optionalcheck(fileorcomposer) for conditional visibility; workers withconflicts_withauto-stop conflicting workers on start. Per-worker flags:host: true(run on host via the version manager instead of in FPM container — HMR-sensitive Node tools),per_worktree: true(worker runs independently per worktree underlerd-<worker>-<site>-<branch>),replaces_build: true(worker provides asset manifest while running, so a worktree add skips the staticnpm run buildstep when this worker is opted in) - Custom workers can be added per-project (
.lerd.yamlcustom_workers) or globally (~/.config/lerd/frameworks/<name>.yaml); use theworkertool'sadd/removeactions — both survive framework store updates - Framework setup commands (one-off bootstrap steps like migrations, storage links) are defined in the framework YAML and shown by
frameworkaction: "setup"; Laravel has built-in storage:link/migrate/db:seed; custom frameworks can define their own - Service version placeholders (
{{mysql_version}},{{postgres_version}},{{redis_version}},{{meilisearch_version}}) are available in framework env vars and resolved from the service image tag at env-setup time - Custom containers: non-PHP sites (Node.js, Python, Go, etc.) can define a
Containerfile.lerdand acontainer:section in.lerd.yamlwith a port; lerd builds a per-project image, runs it aslerd-custom-<sitename>, and nginx reverse-proxies to it; the project directory is volume-mounted at its host path with--workdirset automatically — do NOT addWORKDIRorCOPYto the Containerfile; workers exec into the custom container; services are accessible by name on the sharedlerdPodman network; hot-reload file watchers must use polling on macOS (inotify does not fire across Podman Machine's virtiofs mount) — nodemon:--legacy-watch, Vite:server.watch.usePolling: true, webpack:watchOptions: { poll: 1000 } - Custom-image PHP sites (custom-FPM): a PHP project can define a
Containerfile.lerd(must buildFROM lerd-php<ver>-fpm:local) plus acontainer:section with no port; lerd builds a per-site image (lerd-custom-<site>:local), runs a dedicated FPM containerlerd-cfpm-<site>, and serves it by fastcgi instead of the sharedlerd-php<ver>-fpm. It is a normal PHP site otherwise (xdebug, dumps, profiler,lerd shell,php/artisan/composer/tinker, queue/horizon all run in the per-site container). The PHP version is fixed by theFROMline (the UI version selector is read-only);lerd rebuildrebuilds the image. Same key as custom containers, the port is the discriminator: with a port it is a reverse-proxied non-PHP app, without a port it is a fastcgi PHP image.runtimefor these reportsfpm-custom. - Git worktrees automatically get a
<branch>.<site>.testsubdomain (deep*.<branch>.<site>.testwildcard cert + nginxserver_nameon secured sites);vendor/,node_modules/,.envare seeded from the main checkout..lerd.yamlenv_overridesdeclares templated env vars ({{domain}},{{scheme}},{{site}}) layered on the defaultAPP_URLrewrite — for multi-tenant apps (per-branch cookies, signed-URL hosts, tenant routing)
Lerd has two install-time DNS modes recorded in ~/.config/lerd/config.yaml:
- Managed (default):
dns.enabled: true,dns.tld: test. Sites at*.testvia lerd-dns + mkcert;sitetls_enableworks. - Disabled:
dns.enabled: false,dns.tld: localhost. Sites at*.localhostvia RFC 6761; no mkcert CA, TLS toggling unavailable.
Read diag action: "status" for dns.tld and dns.enabled instead of assuming .test; do not propose tls_enable when dns.enabled is false.
Twelve grouped tools, each selecting behaviour via action.
Actions: list (discover sites — CALL FIRST), link, unlink, domain_add, domain_remove, group_assign, group_unassign, group_label, group_db, group_list, tls_enable, tls_disable, tls_renew, php, node, pause, unpause, restart, rebuild, runtime, nginx_read, nginx_write, nginx_reset, park, unpark.
linkregisters a directory; non-PHP sites need.lerd.yamlcontainer.port+ a Containerfile first, or they register as PHP (wrong)linkrunslerd linkand returns its output verbatim, so read the reply. It will NOT start aproxy.commanddev server (command not approved); ask the user to runlerd link --yesdomain_*take a domain without the.testTLD; you can't remove the last domaingroup_*nest a secondary site under a main's subdomain (one level deep): they identify the secondary bypath(defaults to cwd), not bysite;group_assignwithmain+label(+ optionalshare_db),group_db= share|separate- a group secondary follows its main's HTTPS:
group_assignunder a secured main secures the secondary,tls_enableon a main secures its secondaries too, andtls_disableon a secondary is refused while its main is secured (the main's*.<main>wildcard would answer the subdomain and serve the main's app). Disable the main's TLS first - certificates renew themselves before they expire;
tls_renewforces it by hand for one site php/nodetakeversion; passbranchto pin the override on a worktree's checkoutruntimeswitchesfpm↔frankenphp(worker: trueenables frankenphp worker mode)nginx_writesaves a custom override (runsnginx -t, backs up, reloads);branchtargets a worktreeparkregisters a parent dir and auto-registers every PHP project under it;unparkreverses it (project files kept)
Actions: start, stop, restart, pin, unpin, update, rollback, migrate, remove, reinstall, add, expose, port, env, config_read, config_write, config_restore, config_reset, config_list_backups, preset_list, preset_search, preset_install, check_updates, entities, entity_action.
updatepulls a newer image (safe, in-strategy);migratedumps + restores across a cross-strategy upgrade;reinstallwithreset_data: truewipes data and reprovisions;removewithremove_data: truerenames the data dir asidestopmarks the service paused —lerd startskips it until started again;pinkeeps it always runningaddregisters a custom OCI service (depends_onwires dependencies,init: truefor mysql/mariadb); preferpreset_installfor anything inpreset_list(phpmyadmin, pgadmin, mongo, mongo-express, selenium, stripe-mock, mysql, mariadb…)preset_listreturns the installable presets with the metadata each one declares:category(the discovery heading),icon, andadmin_for— the services this preset's admin UI administers, which is notdepends_on. phpMyAdmin depends on mysql but administers mariadb too, and RedisInsight administers valkey without depending on it. To answer "which dashboard administers this database", readadmin_for, notdepends_on.preset_searchqueries the store bynamefor presets that are not bundled locallyenvreturns the recommended.envconnection keys;exposepublishes an extrahost:containerportportmoves the service's primary published host port (published_port, orreset: truefor the default); it stays bound to 127.0.0.1, the container-internal port is unchanged, and a host-proxy site that points at the old port is realigned automaticallyentitieslists what a service holds that is not a database: the kinds its preset declares (RustFS buckets today, more later), each kind's rows and the actions it supports. Databases have their own tool, so they are not repeated here.entity_actionruns one of those declared actions (kind,entity,entity_action); export and import stream a file and stay on the CLI and the dashboardconfig_*read/write/restore/reset a service's runtime tuning override
Actions: list, set, move, create, export, import, snapshot, snapshots, restore, snapshot_delete, extension_list, extension_add.
listreports an engine's databases with sizes;servicepicks the engine, else it resolves from the project. No introspect command, nothing to reportsetpicks the project DB (database: sqlite, mysql, postgres, or a family alternate like mariadb / postgres-pgvector / postgres-timescaledb / mysql-5-7); persists to.lerd.yaml, rewritesDB_keys, starts the service, creates the DB +_testingmovemigrates sites between two installed same-family services (from/to,sites: [...]orall: true) and repoints each.env; source data is left intactcreate/export/importauto-detect service and database; passserviceto override.importdrops a hosted provider's ownership/DEFINER statements (which can never apply here) and creates any extension the dump's types need; passfresh: trueto empty the database first so a dump replaces what is there instead of colliding with it. What an engine can list and act on is declared in its preset, so this is not a mysql/postgres-only setextension_list/extension_addare postgres-only. Animportalready creates whatever extension the dump's types reach for, so use these to see what the engine offers and what the database has, or to add one (extension: postgis) before any dump arrivessnapshot/snapshots/restore/snapshot_deleteare named, restorable snapshots (MySQL/MariaDB/PostgreSQL);restoreis destructive;all_databasescovers the whole service
Actions: setup, check, override.
setupconfigures services, DBs, APP_KEY and APP_URL; on a fresh Laravel clone calldbsetfirst to move off sqlite, thenenv setup, then ALWAYSframework setupor migrations never runcheckcompares.envagainst.env.exampleoverridemanages the personal, gitignored.env.lerd_override(itssetKEY=VALUE win over lerd defaults;LERD_EXTERNAL_SERVICES=<svc,svc>marks vars lerd writes but won't start)
Actions: versions, node_install, node_uninstall, node_manager, php_list, ext_list, ext_add, ext_remove, ports_list, ports_add, ports_remove, ini_read, ini_write, ini_reset.
ext_add/ext_removechange one declared set applying to EVERY PHP version, so a site keeps its extensions across a version change. They rebuild one version's FPM container now (slow); others rebuild on next use.ext_addacceptsapk_depsfor extra Alpine build packagesext_listreports the declared set plus, per version: has it, predates the set (rebuild fixes), or cannot load it (rebuild won't). Never assume a declared ext is present:mongodbneeds 8.1+, 7.4/8.0 are Alpine 3.16node_managerwith no argument reports the version manager lerd drives, whether nvm is present and whether lerd manages Node at all; withmanager: fnm|nvmit switches, which also rewrites the PATH shims and regenerates host workersphp_listsetsbase_updatewhen the published base image a version was built from has been republished (an upstream PHP or Alpine fix);lerd php:rebuild <version>picks it upports_add/ports_remove/ports_listpublish extra host ports on a PHP version's shell (FPM) container so a process started inlerd shellis reachable atlocalhost:PORT.ports_addtakeshostand optionalcontainer(defaults tohost); a busy host port shifts to the next free one. Per version and independent, loopback-bound (followslan:expose), restarts that version's FPM. Prefer host-proxy or a worker+proxy for a single site. CLI:lerd php:ports add/remove/list [--php version]ini_read/ini_write/ini_resetedit php.ini: passversionfor a per-version file, orshared: truefor the shared file applied to every version. The shared file loads below the per-version one, so a per-version key still wins and an unknown key on some version is ignored (not fatal). Prefer shared for a setting you want everywhere, so a version change never drops it.ini_writetakes fullcontent, backs up, and restarts the affected FPM containers. CLI:lerd php:ini [version|shared]- extra Alpine packages:
lerd php:pkg add/remove/list <packages>(CLI) bakes runtime apk packages (CLI tools, libs) into every FPM image, saved in config underphp.packagesand re-applied on every rebuild, so they survivephp:rebuildand base image updates. One declared set applies to every PHP version. Layered onto the shared image, not the published base. - Pest browser testing (CLI-only):
lerd pest:browser install|doctor|remove [version]sets uppestphp/pest-plugin-browserinside the FPM container by baking Alpine's musl chromium into the images. Chromium only, and current PHP versions only (the 7.4/8.0 legacy tier is rejected). Needs theplaywrightnpm package first; re-run install after bumping it. Tests then run through the normallerd test/lerd pest. - bun: lerd never installs or version-manages bun. On the host, JS install/dev/build run through bun when the project is a bun project (
bun.lockb/bun.lock/bunfig.toml/packageManager: bun) or when Node is unmanaged, no system Node exists, and bun is present. CLI-only:lerd node:manage/node:unmanageopt in or out of lerd-managed Node (unmanage drops fnm versions, never a user's nvm ones),lerd js:runtime [bun|node|auto]pins one site's runtime, andlerd php:bun install|update|versionmanages an in-container bun forlerd shell. These are host operations, not container exec actions.
Actions: list (CALL FIRST), start, stop, add, remove, health, heal, mode_get, mode_set, and the framework workers queue_start, queue_stop, horizon_start, horizon_stop, reverb_start, reverb_stop, schedule_start, schedule_stop, stripe_start, stripe_stop, stripe_config.
- call
listto discover a site's workers beforestart; passbranchto target a per-worktree unit - use
horizon_*instead ofqueue_*when laravel/horizon is installed (mutually exclusive);queue_startneeds Redis running whenQUEUE_CONNECTION=redis addsaves a custom worker to.lerd.yaml(or the user overlay withglobal: true); does not auto-starthealthreports unhealthy units (read-only);healresets and restarts them (unitfor one, omit for all);mode_getreports the macOS worker runtime,mode_setswitches it (mode: exec|container)- a worker's health
stateis one offailed(the unit died),expected-but-stopped(it should be running and isn't), orunreachable— the unit is happily active but the server it publishes no longer accepts connections, a dev server that wedged without exiting. All three are heal-able;healrestarts the unit. Per-worktree units (lerd-<worker>-<site>-<branch>) are covered by the same pass, so a dead per-worktree Vite heals like any other. A worker with ascheduleis a oneshot driven by a timer and is idle between ticks by design, which is healthy - Stripe secret is read from
.env(STRIPE_SECRET / STRIPE_SECRET_KEY / STRIPE_API_KEY);stripe_configsets webhook_path / secret_env_key in.lerd.yaml - Auto-reload (CLI-only):
lerd horizon:reload [on|off]andlerd octane:reload [on|off](FrankenPHP worker mode) restart workers on file changes; both need the project'schokidarnpm package - Idle-suspend (CLI-only):
lerd idle on/offtoggles activity-driven suspension globally; suspended workers stop after the idle timeout (lerd idle timeout <dur>) and resume on the next request/CLI/MCP/file-save.lerd idle pin/unpin <site>exempts a site;lerd idle statusreports policy and last-active. A worker shown as suspended is healthy, not failed, so do nothealit
Actions: artisan (Laravel), console (other frameworks), composer, vendor_bins, vendor_run, commands_list, commands_run, command_add, command_remove.
artisan/console/composertakeargs(array); tinker must use--execute=<code>for non-interactive usevendor_runis the right way to run project tooling (pest, phpunit, pint, phpstan, rector) — callvendor_binsfirst to discover what's installed, thenvendor_runwithbin+args; prefer it overcomposer execcommands_*/command_*list, run, add and remove the on-demand commands in a site's.lerd.yamlcommands:block;commands_runneedsforce: truefor confirm-gated commands- composer over git SSH (CLI-only): when
composerneeds a private repo reachable only over SSH,lerd auth sshstarts a shared ssh-agent container and loads the host's~/.ssh/id_*(or named keys) so passphrase-protected keys work in the FPM container;lerd auth ssh --listshows loaded keys,--removeflushes them. Keys live only in agent memory and clear on machine restart
Actions: list, add, remove, prune, search, update, project_new, setup.
addwithname: "laravel"merges custom workers/setup into the built-in frameworkremoverefuses to drop a definition a linked site still uses (passforce: trueto override);pruneremoves every framework definition no site usessearch/updateuse the community store; definitions auto-fetch on link, soupdateis the manual refresh (nonamerefreshes the catalogue and all installed definitions; withnameit fetches that one, auto-detecting version fromcomposer.lock)project_newscaffolds a new project (requires absolutepath, default framework laravel); follow withsitelink+envsetupsetupruns the framework's post-install steps (migrations, storage:link…) — MANDATORY afterenv setupon new/cloned projects; idempotent
Actions: status, doctor, doctor_fix, site_doctor, which, check, dns_diagnose, bug_report, analyze_queries, route_timing, optimize_route, dumps_recent, dumps_status, dumps_clear, dumps_toggle, profiler_toggle, profiler_status, profiler_clear, profiler_report, xdebug_on, xdebug_off, xdebug_status.
status(DNS/nginx/FPM/watcher/tools health) anddoctor(JSON findings, each tagged with a fix tier) are the first stops when something is broken;dns_diagnosewalks the DNS chaindoctor_fixapplies the safe (non-heavy, non-sudo) automatic repairs for environment findings; package installs,lerd install, andlerd cleanupstay manualsite_doctorruns framework-agnostic app-level checks for one site (env file, env drift, app key, composer/node dependency install + lock,composer audit/npm audit, PHP range, aslow_routeswarning for routes whose p95 runs well above the site's typical time or over a second, plus the framework's own checks); passsite(name or domain) orpath, defaults to cwd. A failing check carries aseverityand, when one applies, afixnaming the command that resolves it — run that yourself (exec, or the named lerd command); site_doctor itself is read-only.slow_routesis the exception: it reads the watcher's request-timing snapshot and has no command fix, the remedy is to profile the route (profiler_toggle)- reading logs lives in the
logstool (below), not here whichshows resolved PHP/Node/docroot/nginx for a site;checkvalidates.lerd.yaml- debug bridge loop:
dumps_toggle(enable) →dumps_clear→ hit the page →analyze_queries(N+1 / slow-query report with file:line) ordumps_recent(filter by site/branch/ctx/kind/since/limit) route_timingreturns the per-site response-time table: the typical (median) time and the routes whose p95 runs well above it (method, example path, p95, multiplier, samples), read from the watcher's request-timing snapshot. Works from recorded traffic alone, no capture needed.siteaccepts either the site name or its domain, as doanalyze_queries,optimize_route, anddumps_recentoptimize_routeis the join: each slow route paired with the N+1 and slow-query findings captured against that same route (with the caller file:line), so you get the symptom and its cause in one call. Needs the query capture on (dumps_toggleenable) plus a few real hits. When the SPX profiler was on for the route's traffic, each slow route also carries aprofileblock, the top functions by exclusive wall time from the freshest capture, distilled to a few outliers (not the raw trace), so a CPU-bound route shows where its time went next to its queries- optimizing a slow site: don't read controllers to guess at N+1s, drive it from real traffic.
route_timingto see which routes are slow →dumps_toggle(enable) andprofiler_toggle(enable) → hit the slow route a few times →optimize_routeto get its N+1/slow queries with file:line and, from the profiler, the top CPU functions behind it → fix the caller (eager-load, index, cache) or the hot function - stay ahead of regressions: when you change request handling or database code on a PHP site, verify you did not introduce a slowdown before moving on, enable dumps, hit the affected route, run
optimize_route, and treat a new N+1 or aslow_routesdoctor warning as work to finish rather than noise. On an unfamiliar site,route_timingshows the slow routes worth attention first. The timing panel is a live signal (recent traffic only, in memory), so a route you fixed clears once it stops being slow, but the durable catch is theslow_routepush notification that fires the moment a route crosses into slow. Periodic checking belongs to the user's own scheduler (a routine, a cron job), not to lerd. profiler_*toggle the global SPX profiler and surface the flame-graph UI;profiler_report(site +args, e.g.["artisan","app:heavy-report"]) runs that command under SPX and returns a text flat profile, the top functions by wall time and call count, the CPU-bound analog ofanalyze_queriesfor when a slow route's cost is not in its queries (a reproducible CLI command, not a live HTTP request);xdebug_*control Xdebug on port 9003 (modedefaults to debug)bug_reportwrites an anonymised diagnostic report for a GitHub issue- disk cleanup (CLI-only):
lerd cleanupreclaims podman disk from orphaned lerd images (--dry-runto preview,--deepfor the aggressive tier); a daily safe-tier sweep plus post-rebuild/service-change reaping runs automatically, toggled withlerd cleanup auto on|off|status
Actions: sources, fetch. Debug without opening files by hand.
sourceslists every queryable source for the site plus shared infra:app:<file>(framework log files),fpm,worker:<name>(queue/horizon/schedule/custom), and the globalsnginx,dns,watcher,ui, services,php<ver>. Call it first to learn the namesfetch source=<name>reads one source. Filter withgrep(regex, falls back to literal substring),since/until(relative like15m/1h/2h30m, or a timestamp),level(app logs only: error/warning/info/debug), andlines(default 50)- streaming is polling: every
fetchreturns an opaquecursor; call again withsince=<cursor>(orcursor=<cursor>) to get only the new lines. The cursor format differs per backend, so treat it as opaque and echo it back - entries come back chronological (oldest first). Raw logs with no timestamps ignore
since/leveland just return the last N; a not-running container returns partial output, not an error
Actions: list, add, remove, wait, db_isolate, db_share.
addinstalls deps and offers an asset-worker / build-step prompt; secured sites get*.<branch>.<site>.testwildcard cert SANs + nginxserver_nameautomatically. It waits for setup and reportsprovisioned(false+ note means still running, not failed;timeout_secondsdefault 300)waitis that readiness check alone, for a worktree made with plaingit worktree add. Never judge readiness from the tree:node_modules/exists from the first extracted package and composer fills existingvendor/<org>/dirs, so both read as finished mid-install, and racing the watcher is howvendor/ends up with noautoload.phpdb_isolategives a worktree its own database (seed viasource: empty|main|);db_sharepoints it back at the main;removekeeps an isolated DB unlesskeep_db: false- a framework definition can declare what its worktrees need (an isolated database, what it is cloned from, console commands to run once it is in place), so
adddoes that work rather than leaving it to be run by hand - request timing is recorded per worktree; pass
branchtoroute_timing,optimize_routeanddumps_recentto read one branch's traffic
Actions: list, create, rename, delete, assign, move.
- a workspace is a display-only bucket of sites, shown in the dashboard sidebar and the TUI. It never touches nginx, domains, certificates or
.env. This is not the same thing as thesitetool'sgroup_*actions, which nest a real site under another's subdomain and regenerate vhosts and certs — reach forgroup_*when a site should be served at<label>.<main>.test, and forworkspacewhen the user just wants their site list organised assigntakessites(names or domains) and aworkspace, creating it if new;workspace: "none"ungroups them.movereorders a workspace with a zero-basedpositiondeletedrops the workspace and ungroups its members; no site is touched
- Pass
actionon every tool;pathis optional on most and defaults to the directory the assistant was opened in - Discover before acting:
sitelistfor sites,workerlistfor a site's workers,servicepreset_listbeforepreset_install,execvendor_binsbeforevendor_run - On a fresh Laravel clone (DB_CONNECTION=sqlite), call
dbsetbeforeenvsetupto choose a database deliberately, then runframeworksetup - Domain conflicts on link: a link drops a domain another site owns (reported in
warnings) and registers the survivors, falling back to<dirname>.<tld>;.lerd.yamlis untouched.domain_addstill hard-errors - Custom APP_URL:
envsetupwrites<scheme>://<primary-domain>; override viaapp_urlin.lerd.yaml(committed) or the per-machinesites.yamlentry, then re-runenv setup - Built-in service hosts follow
lerd-<name>(e.g.lerd-mysql,lerd-redis,lerd-postgres); default DB credentials are usernameroot, passwordlerd - Custom container sites (Node.js, Python, Go, …) — mandatory order: (1) write a Containerfile (default
Containerfile.lerd); (2) write.lerd.yamlwithcontainer: {port: <N>}(plus optionaldomains,services,secured); (3) configure the project's.envwith service hosts (lerd-mysql, etc.) and start needed services viaservicestart; (4) callsitelink. Never link before steps 1–3 or the site registers as PHP-FPM; if that happens,siteunlink, write the files, then link again - Worker unit names follow
lerd-<worker>-<site>(per-worktree:lerd-<worker>-<site>-<branch>) - Host tools (CLI-only):
diagstatusreports Composer, fnm and mkcert against the versions lerd pins, and flags any that differ. Applying an update islerd tools:update, which has no tool here, so tell the user to run it rather than looking for an action - Sharing a site is CLI-only and deliberate:
lerd share(ngrok, cloudflared, Expose, serveo, localhost.run, Pinggy) and the dashboard's share menu put a site on the public internet, the same menu's public share serves it through the user's own reverse proxy on a base domain they control instead of a tunnel service, andlerd lan:exposeputs it on the local network. None is exposed here, so never claim you can share a site; hand the user the command and let them decide
===
=== foundation rules ===The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to ensure the best experience when building Laravel applications.
This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions.
- php - 8.5
- filament/filament (FILAMENT) - v5
- laravel/fortify (FORTIFY) - v1
- laravel/framework (LARAVEL) - v13
- laravel/horizon (HORIZON) - v5
- laravel/octane (OCTANE) - v2
- laravel/prompts (PROMPTS) - v0
- laravel/pulse (PULSE) - v1
- laravel/reverb (REVERB) - v1
- laravel/sanctum (SANCTUM) - v4
- laravel/socialite (SOCIALITE) - v5
- laravel/telescope (TELESCOPE) - v5
- livewire/livewire (LIVEWIRE) - v4
- larastan/larastan (LARASTAN) - v3
- laravel/boost (BOOST) - v2
- laravel/mcp (MCP) - v0
- laravel/pint (PINT) - v1
- laravel/sail (SAIL) - v1
- pestphp/pest (PEST) - v5
- phpunit/phpunit (PHPUNIT) - v13
- rector/rector (RECTOR) - v2
- tailwindcss (TAILWINDCSS) - v4
This project has domain-specific skills available in **/skills/**. You MUST activate the relevant skill whenever you work in that domain—don't wait until you're stuck.
- You must follow all existing code conventions used in this application. When creating or editing a file, check sibling files for the correct structure, approach, and naming.
- Use descriptive names for variables and methods. For example,
isRegisteredForDiscounts, notdiscount(). - Check for existing components to reuse before writing a new one.
- Do not create verification scripts or tinker when tests cover that functionality and prove they work. Unit and feature tests are more important.
- Stick to existing directory structure; don't create new base folders without approval.
- Do not change the application's dependencies without approval.
- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run
npm run build,npm run dev, orcomposer run dev. Ask them.
- You must only create documentation files if explicitly requested by the user.
- Be concise in your explanations - focus on what's important rather than explaining obvious details.
=== boost rules ===
- Laravel Boost is an MCP server with tools designed specifically for this application. Prefer Boost tools over manual alternatives like shell commands or file reads.
- Use
database-queryto run read-only queries against the database instead of writing raw SQL in tinker. - Use
database-schemato inspect table structure before writing migrations or models. - Use
get-absolute-urlto resolve the correct scheme, domain, and port for project URLs. Always use this before sharing a URL with the user. - Use
browser-logsto read browser logs, errors, and exceptions. Only recent logs are useful, ignore old entries.
- Always use
search-docsbefore making code changes. Do not skip this step. It returns version-specific docs based on installed packages automatically. - Pass a
packagesarray to scope results when you know which packages are relevant. - Use multiple broad, topic-based queries:
['rate limiting', 'routing rate limiting', 'routing']. Expect the most relevant results first. - Do not add package names to queries because package info is already shared. Use
test resource table, notfilament 4 test resource table.
- Use words for auto-stemmed AND logic:
rate limitmatches both "rate" AND "limit". - Use
"quoted phrases"for exact position matching:"infinite scroll"requires adjacent words in order. - Combine words and phrases for mixed queries:
middleware "rate limit". - Use multiple queries for OR logic:
queries=["authentication", "middleware"].
- Run Artisan commands directly via the command line (e.g.,
php artisan route:list). Usephp artisan listto discover available commands andphp artisan [command] --helpto check parameters. - Inspect routes with
php artisan route:list. Filter with:--method=GET,--name=users,--path=api,--except-vendor,--only-vendor. - Read configuration values using dot notation:
php artisan config:show app.name,php artisan config:show database.default. Or read config files directly from theconfig/directory.
- Execute PHP in app context for debugging and testing code. Do not create models without user approval, prefer tests with factories instead. Prefer existing Artisan commands over custom tinker code.
- Always use single quotes to prevent shell expansion:
php artisan tinker --execute 'Your::code();'- Double quotes for PHP strings inside:
php artisan tinker --execute 'User::where("active", true)->count();'
- Double quotes for PHP strings inside:
=== php rules ===
- Always use curly braces for control structures, even for single-line bodies.
- Use PHP 8 constructor property promotion:
public function __construct(public GitHub $github) { }. Do not leave empty zero-parameter__construct()methods unless the constructor is private. - Use explicit return type declarations and type hints for all method parameters:
function isAccessible(User $user, ?string $path = null): bool - Use TitleCase for Enum keys:
FavoritePerson,BestLake,Monthly. - Prefer PHPDoc blocks over inline comments. Only add inline comments for exceptionally complex logic.
- Use array shape type definitions in PHPDoc blocks.
=== deployments rules ===
- Laravel can be deployed using Laravel Cloud, which is the fastest way to deploy and scale production Laravel applications.
=== laravel/core rules ===
- Use
php artisan make:commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands usingphp artisan listand check their parameters withphp artisan [command] --help. - If you're creating a generic PHP class, use
php artisan make:class. - Pass
--no-interactionto all Artisan commands to ensure they work without user input. You should also pass the correct--optionsto ensure correct behavior.
- When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using
php artisan make:model --helpto check the available options.
- For APIs, default to using Eloquent API Resources and API versioning unless existing API routes do not, then you should follow existing application convention.
- When generating links to other pages, prefer named routes and the
route()function.
- When creating models for tests, use the factories for the models. Check if the factory has custom states that can be used before manually setting up the model.
- Faker: Use methods such as
$this->faker->word()orfake()->randomDigit(). Follow existing conventions whether to use$this->fakerorfake(). - When creating tests, make use of
php artisan make:test [options] {name}to create a feature test, and pass--unitto create a unit test. Most tests should be feature tests.
- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run
npm run buildor ask the user to runnpm run devorcomposer run dev.
=== octane/core rules ===
This application uses Laravel Octane, a long-running PHP server. The application bootstraps once and handles many requests within the same process.
- Never store request-specific state in singletons or static properties, because it can leak across requests.
- Use
config('octane.server')to detect the active driver (swoole,roadrunner, orfrankenphp). - Prefer scoped bindings (
$this->app->scoped()) over singletons for per-request services.
When working on Octane-specific features (concurrency, shared tables, memory, driver configuration, testing), invoke octane-development for detailed rules.
=== livewire/core rules ===
- Livewire allow to build dynamic, reactive interfaces in PHP without writing JavaScript.
- You can use Alpine.js for client-side interactions instead of JavaScript frameworks.
- Keep state server-side so the UI reflects it. Validate and authorize in actions as you would in HTTP requests.
=== pint/core rules ===
- If you have modified any PHP files, you must run
vendor/bin/pint --dirty --format agentbefore finalizing changes to ensure your code matches the project's expected style. - Do not run
vendor/bin/pint --test --format agent, simply runvendor/bin/pint --format agentto fix any formatting issues.
=== pest/core rules ===
- This project uses Pest for testing. Create tests:
php artisan make:test --pest {name}. - The
{name}argument should not include the test suite directory. Usephp artisan make:test --pest SomeFeatureTestinstead ofphp artisan make:test --pest Feature/SomeFeatureTest. - Run tests:
php artisan test --compactor filter:php artisan test --compact --filter=testName. - Do NOT delete tests without approval.