plugins/lt-dev/skills/managing-dev-servers/SKILL.md
Rules for starting, monitoring, and stopping local development servers (nuxt dev, nest start, npm/pnpm run dev, pnpm build --watch, Playwright, etc.) across all lt-dev workflows. Prefers `lt dev up/down/status/tunnel` for projects registered with the lt CLI — these serve every project under stable HTTPS URLs (`<slug>.localhost`, `api.<slug>.localhost`) via Caddy (via a dedicated LaunchAgent/systemd-user unit, NOT `brew services caddy`) and inject project-specific env vars (BASE_URL, APP_URL, NUXT_PUBLIC_*, NSC__MONGOOSE__URI, NUXT_PUBLIC_STORAGE_PREFIX, HOST=127.0.0.1, NODE_EXTRA_CA_CERTS, API_URL/SITE_URL legacy aliases) so multiple lt projects can run in parallel without port collisions or auth cross-wiring. `lt dev tunnel` exposes a running project externally via a Cloudflare Quick Tunnel. Falls back to the run_in_background / pkill contract for non-lt projects to prevent orphaned processes blocking the Claude Code session ("Unfurling..."). Activates whenever a long-running process must be started for manual validation, Chrome DevTools MCP debugging, TDD iterations, framework linking, or any E2E test run. Referenced by building-stories-with-tdd, developing-lt-frontend, generating-nest-servers, and contributing-to-lt-framework.
npx skillsauth add lennetech/claude-code managing-dev-serversInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Local development servers (npm run dev, nuxt dev, nest start, pnpm build --watch, pnpm test:watch, etc.) are long-running processes. If they are started uncontrolled in the background, Claude Code cannot reliably reclaim them, the session blocks on "Unfurling..." without consuming tokens, and the user must press ESC to continue.
Apply these rules whenever you start any such process — regardless of whether you are inside a TDD cycle, a framework-linking session, a manual validation run, or an MCP-driven debugging flow.
The plugin's detect-lt-dev hook injects one of three context blocks at the top of every prompt. Use that block as your switch:
session: yes — Project registered AND running. Use the URLs from the block. Do nothing extra; for browser tests / API calls use the URLs as-is.session: no — Project registered, not running. For the Playwright/E2E suite run lt dev test (isolated parallel stack on a dedicated <slug>-test DB — never touches dev data, auto-teardown). For manual browser tests, Chrome DevTools MCP, or API probes, run lt dev up first.lt dev init first (idempotent, safe — patches legacy ports, registers, updates CLAUDE.md). Then lt dev up. Do NOT start pnpm dev / pnpm start as a workaround.run_in_background: true + pkill pattern documented below.If lt dev up later complains that Caddy is missing or the daemon is not running, run lt dev install first. One-time per machine:
brew install caddy # macOS (Linux: https://caddyserver.com/docs/install)
lt dev install # writes + bootstraps the dedicated LaunchAgent / systemd-user unit
sudo -E HOME="$HOME" caddy trust # trust the local Caddy root CA system-wide
Do NOT use brew services start caddy — its plist hardcodes --config /opt/homebrew/etc/Caddyfile and crash-loops against the lt-dev Caddyfile at ~/.lenneTech/Caddyfile. lt dev install owns its own dedicated service (tech.lenne.lt-dev-caddy) to sidestep that entirely. The -E HOME="$HOME" on caddy trust is also mandatory — without it sudo switches HOME to /var/root, caddy fails to find its user-scoped CA, and the trust install silently does nothing.
lt dev uplt dev install # One-time per machine (dedicated LaunchAgent/systemd unit + Caddyfile stub + CA reminder)
lt dev uninstall # Remove the lt-dev service (symmetric counterpart; `--purge` also drops Caddyfile + logs)
lt dev init # Once per project (idempotent — patches + register + CLAUDE.md)
lt dev up # Start API + App behind Caddy with stable HTTPS URLs
lt dev status # Show what is running for THIS project
lt dev status --all # List every registered project + running state
lt dev down # Stop processes + remove Caddy block + clear ENV bridge
lt dev doctor # Diagnose Caddy / CA / DNS / port issues
lt dev test # ISOLATED Playwright E2E: parallel stack + dedicated DB, auto-teardown
lt dev test down # Tear the isolated test stack down (residue-free)
lt dev tunnel # Foreground Cloudflare Quick Tunnel — expose the App publicly (--api for the API)
init and install auto-chain (idempotent, one hop, no recursion): running lt dev init on a machine that isn't set up runs lt dev install first; running lt dev install inside an un-initialized project runs lt dev init afterwards. So the minimal first run in a fresh project is just lt dev init then lt dev up — no need to remember the install step. Opt out with --skip-install (init) / --skip-init (install). The former name lt dev migrate still works as an alias for lt dev init.
API E2E tests (TestHelper, in-process) — run unchanged. They start a NestJS test module in-process on a dynamic port and never touch Caddy. Use pnpm run test:e2e in projects/api as before.
App E2E tests (Playwright) — preferred: the isolated lt dev test (below). For ad-hoc runs against the dev session, the lt dev up bridge still applies:
lt dev up writes a <root>/.lt-dev/.env bridge file containing NUXT_PUBLIC_SITE_URL, NUXT_PUBLIC_API_URL, storage prefix, DB URI, and NODE_EXTRA_CA_CERTS (Caddy root CA path).lt dev init injects a tiny // >>> lt-dev:bridge >>> block at the top of playwright.config.ts that auto-loads this file. Result: any Playwright invocation (pnpm test:e2e, npx playwright test, VS Code Playwright Extension, JetBrains test runner) automatically picks up the active URLs and trusts the local CA — no parent-shell env required.process.env.NUXT_PUBLIC_SITE_URL || 'http://localhost:3001') keep working as fallback when lt dev is not active (CI, classic local dev).lt dev test — ISOLATED parallel E2E stack (preferred for the suite): spins up a SECOND, fully separate stack that runs PARALLEL to — and never touches — your lt dev up dev session:
<slug>-test.localhost / api.<slug>-test.localhost), own internal ports + Caddy block, and a dedicated DB <slug>-test — separate from both the dev DB (<slug>-local) and the API-test DB (<slug>-e2e).global-setup resets that dedicated DB once, before the first test — so a developer keeps working in their own environment while E2E runs, a run never pollutes dev data, and tests may build on each other within the run.node dist) for stable long suites; the App is a Nuxt dev server. A separate .lt-dev/.env.test bridge carries the test URLs + DB.lt dev test # isolated Playwright E2E in projects/app (auto-teardown)
lt dev test --keep # leave the test stack up afterwards (debug)
lt dev test down # tear the isolated test stack down (residue-free)
lt dev test --api # API tests in projects/api (already isolated on `<slug>-e2e` — no stack)
lt dev test --debug # PWDEBUG=1 + headed
lt dev test -- --ui login.spec.ts # forward args to Playwright
Do NOT run the Playwright suite against the lt dev up dev session — that pollutes (and global-setup would reset) your dev DB. Use lt dev test.
Limit local Playwright runs to new + affected specs to keep TDD loops fast. The full Playwright suite is slow and runs in CI. During local development / TDD, default to lt dev test -- <spec> (or lt dev test -- tests/e2e/<file>.spec.ts); the equivalent for non-lt-projects is scripts/e2e-fast.sh -- <spec> / pnpm dlx playwright test <spec>. Backend Unit + API stay unrestricted — they're fast. Only run the full local Playwright suite when the user explicitly asks (or when an orchestrator like production-ready calls for it).
Requires the lt CLI version that ships the isolated test session. Older lt CLIs run
lt dev testagainst the dev session (legacy behavior). The project'splaywright.config.tsglobal-setup must reset only the activeMONGO_URIDB and allow-list<slug>-test.
Why lt dev is the preferred path:
https://crm.localhost, https://api.crm.localhost) — bookmarks, IDE-run-configs, and Chrome DevTools MCP commands stay valid across restarts.trustedOrigins derived from APP_URL), App only talks to its own API (BASE_URL), localStorage is namespaced by NUXT_PUBLIC_STORAGE_PREFIX=<slug>, MongoDB is namespaced by NSC__MONGOOSE__URI..<slug>.localhost makes WebAuthn/Passkey-style auth realistic without same-origin tricks. The Vite-API-Proxy is OFF under lt dev up (NUXT_PUBLIC_API_PROXY=false).<project>/.lt-dev/{api,app}.log so the Claude Code session does not block. Previous logs are rotated to <name>.log.1 on each lt dev up; only one prior generation is kept (bounded disk usage even across long up/down cycles).lt dev down (process-group SIGTERM, Caddy block removed, no orphaned children).One-time setup per machine: run lt dev install once. Verifies Caddy is installed (suggests brew install caddy), creates the Caddyfile stub, writes + bootstraps the dedicated LaunchAgent / systemd-user unit (so it auto-starts on login and never collides with brew services caddy), then reminds you to run sudo -E HOME="$HOME" caddy trust so browsers accept https://*.localhost.
Sharing a running project externally (mobile preview, webhook target, teammate review): lt dev tunnel — opens a Cloudflare Quick Tunnel to the App, prints a public https://*.trycloudflare.com URL, runs in the foreground until Ctrl-C. lt dev tunnel --api exposes the API instead (start a second one in parallel for full external usage). Requires cloudflared on PATH (brew install cloudflared). Auth cookies on *.localhost are NOT valid on the tunnel URL — Better-Auth's trustedOrigins must include the random tunnel URL for login flows to succeed.
One-time setup for an existing project: run lt dev init once. Idempotent — patches legacy hardcoded ports in config.env.ts / nuxt.config.ts / playwright.config.ts to env-aware variants (defaults preserved), registers the project in ~/.lenneTech/projects.json, updates the project's CLAUDE.md with the URL block, and rewrites a leftover lt-monorepo package name to the directory basename so each project gets its own <slug>.localhost (relevant when the user git cloned the template directly instead of running lt fullstack init).
If the prompt contains "Active lt-dev project" context, NEVER start with pnpm dev / pnpm start directly — use lt dev up. The injected context block lists the actual URLs for the current project. If session is no, run lt dev up first; the URLs only resolve while the Caddy block + processes are active.
Local transactional mail is caught by a shared Mailpit instance (the modern successor to
MailHog — the lenneTech catcher was migrated; the hostname may still read mailhog.lenne.tech
but it runs Mailpit). Projects send via SMTP on port 1025; when no SMTP host is configured
nest-server uses jsonTransport and does not transmit (so test envs never send). The web
UI is basic-auth protected — credentials live in the team vault; never hardcode them.
To inspect what an app sends — or to harden email templates — Mailpit gives you, per message:
a correct HTML preview of multipart/related + inline CID images (old MailHog could not —
it showed raw MIME boundaries / =3D artifacts, a preview limitation, not a mail defect), a
responsive phone/tablet/desktop preview, an HTML Check client-compatibility score, and
a Link Check — plus a scriptable REST API (/api/v1/messages,
/api/v1/message/{id}/html-check, /api/v1/message/{id}/part/{n}, DELETE /api/v1/messages).
Verify emails via Chrome DevTools MCP (web UI) or the REST API; automated tests must assert
content without sending (jsonTransport or an EmailService recording mock).
→ Full details, endpoints, send-a-test-mail recipe, and CID-vs-URL logo trade-offs: reference/local-email-mailpit.md.
lt dev is not applicable)run_in_background: true — Claude Code tracks the process and surfaces its output on demand.curl http://localhost:3000/health, pgrep -f "nest start", or a log line in the Bash output) instead of blind sleep.pkill -f "<process-name>" (e.g. pkill -f "nuxt dev", pkill -f "nest start", pkill -f "build --watch").npm run dev & — backgrounded via shell without later cleanupsleep N after a backgrounded command without a kill steppgrep still shows the processpnpm dev directly when an "Active lt-dev project" context block is present — that bypasses Caddy and re-introduces cross-wiring riskOrphaned dev servers block the Claude Code main loop. The session appears to hang ("Unfurling..."), no tokens are consumed, and the only recovery is user interaction (ESC). This breaks the autonomous iteration contract that TDD and framework-linking workflows rely on.
run_in_background: true + eventual pkill.BASE_URL (API) and APP_URL (App). When lt dev up is used, these are set automatically to the project's HTTPS URLs (https://api.<slug>.localhost/https://<slug>.localhost) — auth works regardless of internal port. The legacy "3000/3001 only" rule applies ONLY to projects that have not yet been migrated to env-aware config (run lt dev init to migrate). For non-lt projects with hardcoded ports, the original rule still holds: starting on a different port silently breaks auth.lt dev up / lt dev test, both subdomains share the parent .<slug>.localhost so Better Auth's crossSubDomainCookies (auto-enabled in the local baseline when BASE_URL is set) makes session cookies visible across both. The NUXT_PUBLIC_API_PROXY=false default is intentional — the vite-proxy hack is no longer needed. E2E cookie INJECTION caveat: a real Set-Cookie: Domain=<slug>.localhost is a DOMAIN cookie (RFC 6265: sent to subdomains incl. api.<slug>.localhost), but Playwright addCookies({ domain }) with a BARE domain (no leading dot) is stored HOST-ONLY and is NOT sent to the API subdomain → the cross-origin /iam/get-session returns null → auto-logout to /auth/login. When injecting a captured session, prefix a leading dot for multi-label hosts (.<slug>-test.localhost); single-label hosts (CI/localhost) stay host-only. See developing-lt-frontend/reference/e2e-testing.md.pnpm build --watch is a dev server too — Framework-linking workflows run both pnpm build --watch and pnpm dev in parallel. The watch process is easy to forget in cleanup because it produces less visible output. Track it like any other server and pkill -f "build --watch" when done.pkill -f "<name>" matches too broadly with short names — pkill -f "dev" can kill unrelated processes (e.g. devtools, developer). Always match the full command: pkill -f "nuxt dev", pkill -f "nest start", pkill -f "pnpm build --watch".lt dev up (preferred) or nuxt dev (fallback) for Chrome DevTools MCP debugging, npx playwright test for E2E. Playwright reads NUXT_PUBLIC_SITE_URL so it follows whatever URL lt dev up exports.lt dev up (preferred) or nest start (fallback) for manual API probing, E2E runs against a live API.pnpm build --watch (framework side) and pnpm dev (starter side) run in parallel. Track and clean up both.localhost:3000, App localhost:3001. These remain the fallback for projects that have not been migrated to env-aware config or are run via pnpm start/pnpm dev directly.~/.lenneTech/projects.json: https://<slug>.localhost (App) + https://api.<slug>.localhost (API). Always read the active URLs from the prompt's "Active lt-dev project" context block when present, or run lt dev status to inspect.If two projects compete for the same internal port:
lt dev status --all to see which projects are registered + running.lt dev down on the other project before lt dev up on the new one — internal ports are auto-allocated, so collisions are extremely rare.lsof -iTCP -sTCP:LISTEN -nP -iTCP:<port> to find the PID, pkill -f "<matching process>" to free it.lt dev doctor will identify the culprit. Stop the conflicting webserver, then re-run lt dev install (it bootstraps the lt-dev LaunchAgent / systemd unit, no brew services involved).Do NOT pick a random alternative port for non-migrated projects — their hardcoded auth config will not match. Either migrate the project with lt dev init, or fix the collision and use the original port.
Before reporting a task complete:
lt dev up was used: lt dev down was called (or the user explicitly agreed to leave it running)run_in_background: true have been terminated with pkillpgrep -f "nuxt dev" and pgrep -f "nest start" (and any build --watch) return no matches — or the user has been asked and agreed to leave them runninglt dev status for lt-projects, lsof -i :3000 -i :3001 for the default fallback) unless the user asked for a running serverdevelopment
Single source of truth for the lenne.tech fullstack production-readiness checklist. Defines the eight pillars (configuration & secrets, observability & logging, health & lifecycle, security hardening, data durability, resilience under load, deployment hygiene, runbook & rollback) with concrete file/line evidence requirements per pillar, severity classification (Critical / Major / Minor), and a canonical machine-parseable report block. Activates whenever an agent or command needs to gate a release on production-readiness — currently used by /lt-dev:production-ready, lt-dev:production-readiness-orchestrator, and the devops-reviewer (read-only). NOT for OWASP-style code-level security review (use security-reviewer). NOT for npm dependency audits (use maintaining-npm-packages).
development
Single source of truth for executing GitLab CI/CD pipelines locally with the same image, env vars, and service containers as the real runner — so pipeline failures are caught before push. Defines pipeline discovery (.gitlab-ci.yml + includes), per-job execution via gitlab-runner exec, service-container orchestration (Mongo, Redis, MailHog), env injection without secrets, cache/artifact handling, and a job-by-job verdict report. Also describes the GitHub Actions equivalent via act for projects that mirror to GitHub. Activates whenever an agent or command needs to validate that the CI pipeline will pass — currently used by /lt-dev:production-ready and lt-dev:production-readiness-orchestrator. NOT for running the local check script (use running-check-script). NOT for writing or refactoring CI configs (use the devops agent).
development
Single source of truth for designing, running, and interpreting k6 load tests against lenne.tech fullstack APIs. Defines installation paths (brew, docker, npm), the three canonical scenarios (smoke / load / soak), endpoint discovery from the generated SDK, realistic Better-Auth login flows, threshold defaults for ~10 concurrent users (p95 < 500ms, error rate < 1%, http_req_failed < 1%), result interpretation, and the optimisation ladder when the system fails (DB indices, query rewrites, caching, connection pool sizing, rate-limit relaxation, payload trimming). Activates whenever an agent or command needs to validate that the API is stable for ~10 concurrent users performing many actions in short time, or to detect performance regressions via k6. Currently used by /lt-dev:production-ready, lt-dev:production-readiness-orchestrator, and lt-dev:performance-reviewer. NOT for Lighthouse frontend performance (use a11y-reviewer). NOT for unit performance assertions (use the test runner directly).
tools
Migrates lenne.tech projects from the legacy jest+eslint+prettier toolchain to the current vitest+oxlint+oxfmt baseline used by nest-server-starter and nuxt-base-starter. Covers swc decoratorMetadata config, the @Prop union-type fix for SWC, supertest default-import correction, ESM/CJS interop, the Nitro PORT-vs-NITRO_PORT bug, ANSI escape stripping in workspace runners (lerna/nx), free-port logic for check-server-start.sh, the offers-pattern config.env.ts (NSC__-only + fail-fast + auto-derived appUrl), and the multi-phase check-envs.sh smoke test. Activates whenever someone is migrating an existing project to the new toolchain, debugging "Cannot determine a type for the X field" Mongoose errors, ERR_SOCKET_BAD_PORT crashes from check-server-start, or wants to align an existing project with the current starter conventions.