@lenne.tech/cli 1.21.0 → 1.23.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,152 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.paths = void 0;
4
+ exports.allocateInternalPort = allocateInternalPort;
5
+ exports.clearSession = clearSession;
6
+ exports.isPidAlive = isPidAlive;
7
+ exports.isValidPid = isValidPid;
8
+ exports.loadRegistry = loadRegistry;
9
+ exports.loadSession = loadSession;
10
+ exports.saveRegistry = saveRegistry;
11
+ exports.saveSession = saveSession;
12
+ exports.takenInternalPorts = takenInternalPorts;
13
+ /**
14
+ * State persistence for `lt dev`.
15
+ *
16
+ * Two stores:
17
+ * - Central registry at `~/.lenneTech/projects.json` — index of all
18
+ * known projects, used by `lt dev status --all`, the Claude Code
19
+ * plugin hook, and conflict detection.
20
+ * - Per-project state at `<root>/.lt-dev/state.json` — PIDs of the
21
+ * currently running `lt dev up` session.
22
+ *
23
+ * Both files are JSON, atomically written, and schema-versioned.
24
+ */
25
+ const fs_1 = require("fs");
26
+ const os_1 = require("os");
27
+ const path_1 = require("path");
28
+ const REGISTRY_PATH = process.env.LT_DEV_REGISTRY_PATH || (0, path_1.join)((0, os_1.homedir)(), '.lenneTech', 'projects.json');
29
+ const SESSION_DIR = '.lt-dev';
30
+ const SESSION_FILE = 'state.json';
31
+ /**
32
+ * Allocate a free internal port for a Caddy upstream.
33
+ *
34
+ * Strategy: try sequential ports starting from `start`, skipping any
35
+ * that are already in use. The range 4000-4999 is conventional for
36
+ * lt dev internal ports — well above the deprecated 3000/3001 range
37
+ * and safely below most reserved/system ranges.
38
+ */
39
+ function allocateInternalPort(start, taken) {
40
+ for (let p = start; p < start + 1000; p++) {
41
+ if (!taken.has(p))
42
+ return p;
43
+ }
44
+ throw new Error(`No free internal port in range [${start}, ${start + 1000})`);
45
+ }
46
+ /** Remove session state file (called by `lt dev down`). */
47
+ function clearSession(root) {
48
+ const file = (0, path_1.join)(root, SESSION_DIR, SESSION_FILE);
49
+ if ((0, fs_1.existsSync)(file)) {
50
+ try {
51
+ (0, fs_1.rmSync)(file);
52
+ }
53
+ catch (_a) {
54
+ /* best-effort */
55
+ }
56
+ }
57
+ }
58
+ /** Check whether a process with the given PID is currently alive. */
59
+ function isPidAlive(pid) {
60
+ if (!isValidPid(pid))
61
+ return false;
62
+ try {
63
+ process.kill(pid, 0);
64
+ return true;
65
+ }
66
+ catch (_a) {
67
+ return false;
68
+ }
69
+ }
70
+ /** Validate a PID — positive integer, within plausible range. */
71
+ function isValidPid(pid) {
72
+ return typeof pid === 'number' && Number.isInteger(pid) && pid > 0 && pid < 4194304;
73
+ }
74
+ /** Load the central registry; returns an empty one if missing or unreadable. */
75
+ function loadRegistry() {
76
+ if (!(0, fs_1.existsSync)(REGISTRY_PATH))
77
+ return { projects: {}, version: 1 };
78
+ try {
79
+ const parsed = JSON.parse((0, fs_1.readFileSync)(REGISTRY_PATH, 'utf8'));
80
+ if (parsed && typeof parsed === 'object' && parsed.version === 1 && typeof parsed.projects === 'object') {
81
+ return parsed;
82
+ }
83
+ }
84
+ catch (_a) {
85
+ /* fall through */
86
+ }
87
+ return { projects: {}, version: 1 };
88
+ }
89
+ /** Load session state for a project root. */
90
+ function loadSession(root) {
91
+ const file = (0, path_1.join)(root, SESSION_DIR, SESSION_FILE);
92
+ if (!(0, fs_1.existsSync)(file))
93
+ return null;
94
+ try {
95
+ const parsed = JSON.parse((0, fs_1.readFileSync)(file, 'utf8'));
96
+ if (parsed &&
97
+ typeof parsed === 'object' &&
98
+ typeof parsed.pids === 'object' &&
99
+ typeof parsed.startedAt === 'string') {
100
+ // Validate PIDs
101
+ const pids = {};
102
+ if (isValidPid(parsed.pids.api))
103
+ pids.api = parsed.pids.api;
104
+ if (isValidPid(parsed.pids.app))
105
+ pids.app = parsed.pids.app;
106
+ return { pids, startedAt: parsed.startedAt };
107
+ }
108
+ }
109
+ catch (_a) {
110
+ /* fall through */
111
+ }
112
+ return null;
113
+ }
114
+ /** Atomically persist the registry. */
115
+ function saveRegistry(reg) {
116
+ (0, fs_1.mkdirSync)((0, path_1.dirname)(REGISTRY_PATH), { recursive: true });
117
+ const tmp = `${REGISTRY_PATH}.tmp`;
118
+ (0, fs_1.writeFileSync)(tmp, JSON.stringify(reg, null, 2), 'utf8');
119
+ // rename is atomic on POSIX
120
+ (0, fs_1.writeFileSync)(REGISTRY_PATH, (0, fs_1.readFileSync)(tmp, 'utf8'), 'utf8');
121
+ try {
122
+ (0, fs_1.rmSync)(tmp);
123
+ }
124
+ catch (_a) {
125
+ /* best-effort */
126
+ }
127
+ }
128
+ /** Persist session state for a project root. */
129
+ function saveSession(root, state) {
130
+ const dir = (0, path_1.join)(root, SESSION_DIR);
131
+ (0, fs_1.mkdirSync)(dir, { recursive: true });
132
+ (0, fs_1.writeFileSync)((0, path_1.join)(dir, SESSION_FILE), JSON.stringify(state, null, 2), 'utf8');
133
+ }
134
+ /** Collect all internal ports already claimed across the registry. */
135
+ function takenInternalPorts(reg, excludeSlug) {
136
+ const ports = new Set();
137
+ for (const [slug, entry] of Object.entries(reg.projects)) {
138
+ if (slug === excludeSlug)
139
+ continue;
140
+ if (entry.internalPorts.api)
141
+ ports.add(entry.internalPorts.api);
142
+ if (entry.internalPorts.app)
143
+ ports.add(entry.internalPorts.app);
144
+ }
145
+ return ports;
146
+ }
147
+ /** Path constants exported for tests + status displays. */
148
+ exports.paths = {
149
+ registry: REGISTRY_PATH,
150
+ sessionDir: SESSION_DIR,
151
+ sessionFile: SESSION_FILE,
152
+ };
package/docs/commands.md CHANGED
@@ -286,188 +286,261 @@ For mode-aware update workflows after conversion, use:
286
286
 
287
287
  ## Local Development Commands
288
288
 
289
- Orchestrate parallel lt projects on the same machine without port collisions. Each project gets a deterministic port slot derived from its slug; API/App ports are always slot-paired (`3000+slot*10` / `3001+slot*10`). Slot allocation is reproducible across machines (FNV-1a hash) and persisted in `~/.lenneTech/ports.json`.
289
+ Run multiple lt projects in parallel without port collisions or cross-wiring.
290
+ **URL-first**: every project gets stable HTTPS URLs (`<slug>.localhost`,
291
+ `api.<slug>.localhost`) served by Caddy. Internal ports are opaque and
292
+ auto-allocated. Cross-project state (database, storage, cookies) is
293
+ namespaced by slug so projects cannot accidentally interfere.
290
294
 
291
- ### `lt local`
295
+ ### `lt dev`
292
296
 
293
297
  Open the local-orchestration submenu.
294
298
 
295
299
  **Usage:**
296
300
  ```bash
297
- lt local
301
+ lt dev
298
302
  ```
299
303
 
300
- **Alias:** `lt l`
304
+ **Alias:** `lt d`
301
305
 
302
306
  ---
303
307
 
304
- ### `lt local init`
308
+ ### `lt dev install`
305
309
 
306
- Register the current project in the central port registry, optionally patching legacy hardcoded ports to be env-aware.
310
+ One-time per-machine setup. Idempotent re-run anytime to diagnose what's missing. Owns the full Caddy lifecycle via a dedicated LaunchAgent (macOS) / systemd-user unit (Linux) — **does not** use `brew services caddy`, whose hardcoded `/opt/homebrew/etc/Caddyfile` path would crash-loop against our `~/.lenneTech/Caddyfile`.
307
311
 
308
312
  **Usage:**
309
313
  ```bash
310
- lt local init [options]
314
+ lt dev install
311
315
  ```
312
316
 
313
- **Alias:** `lt l i`
314
-
315
- **Options:**
316
- | Option | Description |
317
- |--------|-------------|
318
- | `--slot <n>` | Force a specific slot index (0..89) instead of the deterministic slug hash |
319
- | `--patch` | Apply env-aware port patches non-interactively |
320
- | `--no-patch` | Skip the patch detection / prompt entirely |
321
- | `--noConfirm` | Skip confirmation prompts (without `--patch`, patches are skipped) |
317
+ **Alias:** `lt d i`
322
318
 
323
319
  **What it does:**
324
- 1. Detects the workspace layout (monorepo with `projects/api/` + `projects/app/`, or standalone API/App project).
325
- 2. Looks up or allocates a slot via FNV-1a hash of the project slug. If the slot is taken, falls through linearly to the next free slot.
326
- 3. Detects legacy hardcoded ports in `config.env.ts` (`port: 3000`), `nuxt.config.ts` (`port: 3001`, vite proxy `target: 'http://localhost:3000'`), and `playwright.config.ts` (`baseURL`/`host`/`url: 'http://localhost:3001'`). If `--patch` (or interactive confirm), rewrites them to env-overridable form (`Number(process.env.PORT) || 3000`, `process.env.NUXT_API_URL || …`, etc.) — defaults preserved, idempotent.
327
- 4. Persists the entry to `~/.lenneTech/ports.json`.
328
- 5. Adds `.lt-local/` to the project's `.gitignore`.
329
- 6. Injects (or refreshes) a "Local Development (lt local)" port block into `CLAUDE.md` files at the workspace root and inside each subproject — bracketed by HTML comment markers so it can be replaced cleanly when ports change.
320
+ 1. Verifies `caddy` is on PATH (suggests `brew install caddy` if missing).
321
+ 2. Creates `~/.lenneTech/Caddyfile` stub if absent.
322
+ 3. Detects a conflicting `brew services caddy` registration and asks you to stop it.
323
+ 4. Writes + bootstraps a dedicated service:
324
+ - **macOS:** `~/Library/LaunchAgents/tech.lenne.lt-dev-caddy.plist` via `launchctl bootstrap gui/<uid>`.
325
+ - **Linux:** `~/.config/systemd/user/lt-dev-caddy.service` via `systemctl --user enable --now`.
326
+ 5. Waits up to 8s for Caddy's admin endpoint (`http://127.0.0.1:2019/config/`) to respond.
327
+ 6. Validates the Caddyfile.
328
+ 7. Reminds you to run the CA trust command **with HOME preserved**:
329
+ ```bash
330
+ sudo -E HOME="$HOME" caddy trust
331
+ ```
332
+ Without `-E HOME="$HOME"`, sudo switches HOME to `/var/root` and caddy cannot find its user-scoped CA — this was the bug that blocked the very first install attempt.
330
333
 
331
- **Examples:**
334
+ **Logs:** `~/.lenneTech/caddy.log`, `~/.lenneTech/caddy.err.log`.
335
+
336
+ ---
337
+
338
+ ### `lt dev uninstall`
339
+
340
+ Symmetric counterpart to `lt dev install`. Removes the LaunchAgent / systemd-user unit and stops the Caddy daemon. Does **not** remove the caddy binary itself.
341
+
342
+ **Usage:**
332
343
  ```bash
333
- # Inside a workspace or standalone project
334
- lt local init
344
+ lt dev uninstall # interactive: asks whether to purge Caddyfile + logs
345
+ lt dev uninstall --purge # also remove ~/.lenneTech/Caddyfile + caddy logs
346
+ lt dev uninstall --noConfirm # skip the purge prompt (keep files)
347
+ ```
348
+
349
+ **Alias:** `lt d un`
350
+
351
+ **What it does NOT touch:**
352
+ - the `caddy` binary (use `brew uninstall caddy` if you want to remove the tool too)
353
+ - per-project state under `<project>/.lt-dev/` (use `lt dev down`)
354
+ - the trusted CA in the system keychain (use `sudo -E HOME="$HOME" caddy untrust` if desired)
355
+
356
+ ---
335
357
 
336
- # Force slot 5 for predictable cross-team ports
337
- lt local init --slot 5 --noConfirm --patch
358
+ ### `lt dev migrate`
338
359
 
339
- # Register without touching any source files
340
- lt local init --no-patch --noConfirm
360
+ Register an existing project with `lt dev` and apply idempotent env-aware patches. Safe to run multiple times; safe to run after `lt fullstack init`.
361
+
362
+ **Usage:**
363
+ ```bash
364
+ lt dev migrate
341
365
  ```
342
366
 
367
+ **Alias:** `lt d m`
368
+
369
+ **What it does:**
370
+ 1. Detects the workspace layout (monorepo `projects/api`+`projects/app`, or standalone).
371
+ 2. Builds the project identity (slug from `package.json` "name", subdomains).
372
+ 3. Patches legacy hardcoded ports in `config.env.ts` (`port: 3000`), `nuxt.config.ts` (`port: 3001`, vite proxy target), `playwright.config.ts` (`baseURL`/`host`/`url`) to env-overridable form. Defaults preserved, idempotent.
373
+ 4. Persists the entry to `~/.lenneTech/projects.json` (override path via `LT_DEV_REGISTRY_PATH`).
374
+ 5. Adds `.lt-dev/` to the project's `.gitignore`.
375
+ 6. Injects (or refreshes) a "Local Development (lt dev)" URL block into `CLAUDE.md` files at the workspace root and inside each subproject — bracketed by HTML comment markers.
376
+
343
377
  ---
344
378
 
345
- ### `lt local up`
379
+ ### `lt dev up`
346
380
 
347
- Start the API + App with project-specific ports. Spawns `pnpm start` (api) and `pnpm dev` (app) detached; persists PIDs to `<root>/.lt-local/state.json`.
381
+ Start API + App behind Caddy. Allocates internal ports (4000+), spawns processes detached, persists PIDs to `<root>/.lt-dev/state.json`.
348
382
 
349
383
  **Usage:**
350
384
  ```bash
351
- lt local up
385
+ lt dev up
352
386
  ```
353
387
 
354
- **Alias:** `lt l u`
388
+ **Alias:** `lt d u`
355
389
 
356
- **Environment variables injected into both children:**
390
+ **Environment variables injected:**
357
391
  | Variable | Consumer | Example value |
358
392
  |----------|----------|---------------|
359
- | `PORT` | Nest (api) / Nuxt dev server (app) | slot-derived |
360
- | `BASE_URL` | nest-server config.env.ts (canonical API base) | `http://localhost:3030` |
361
- | `APP_URL` | nest-server config.env.ts (frontend origin for redirects/CORS) | `http://localhost:3031` |
362
- | `NUXT_API_URL` | Nuxt vite-proxy target for `/api`, `/iam`, … | `http://localhost:3030` |
363
- | `NUXT_PUBLIC_API_URL` | Nuxt `useRuntimeConfig().public.apiUrl` | `http://localhost:3030` |
364
- | `NUXT_PUBLIC_SITE_URL` | Nuxt `useRuntimeConfig().public.siteUrl` + Playwright | `http://localhost:3031` |
365
- | `NUXT_PUBLIC_STORAGE_PREFIX` | namespaces sessionStorage/localStorage so parallel projects don't share auth tokens | `crm-local` |
366
- | `NSC__MONGOOSE__URI` | nest-server-config Mongoose URI (only when `dbName` is known) | `mongodb://127.0.0.1/crm-local` |
367
-
368
- **Override the binary** used for both spawns by setting `LT_PNPM_BIN` (e.g. `LT_PNPM_BIN=/usr/local/bin/pnpm lt local up`).
393
+ | `PORT` | Nest (api) / Nuxt (app) | auto-allocated 4000+ |
394
+ | `BASE_URL` / `NSC__BASE_URL` | nest-server canonical API URL | `https://api.crm.localhost` |
395
+ | `APP_URL` / `NSC__APP_URL` | nest-server frontend origin (CORS, BetterAuth) | `https://crm.localhost` |
396
+ | `NUXT_API_URL` | Nuxt vite-proxy target for `/api`, `/iam`, … | `https://api.crm.localhost` |
397
+ | `NUXT_PUBLIC_API_URL` | Nuxt `useRuntimeConfig().public.apiUrl` | `https://api.crm.localhost` |
398
+ | `NUXT_PUBLIC_SITE_URL` | Nuxt `useRuntimeConfig().public.siteUrl` + Playwright | `https://crm.localhost` |
399
+ | `NUXT_PUBLIC_STORAGE_PREFIX` | namespaces sessionStorage/localStorage | `crm` |
400
+ | `NUXT_PUBLIC_API_PROXY` | always `false` Caddy + cookie-domain make it obsolete | `false` |
401
+ | `NSC__MONGOOSE__URI` | nest-server Mongoose URI | `mongodb://127.0.0.1/crm-local` |
402
+ | `DATABASE_URL` | Postgres convenience URL (for nest-base-style projects) | `postgresql://crm-local:crm-local@localhost:5432/crm-local` |
403
+
404
+ **Override the binary** for both spawns via `LT_PNPM_BIN` (e.g. `LT_PNPM_BIN=/usr/local/bin/pnpm lt dev up`).
369
405
 
370
406
  **Pre-flight guards (exit code 1 each):**
371
- - Project not registered (`lt local init` first)
372
- - Already running (run `lt local down` first)
373
- - Port already in use by another process
407
+ - Caddy not installed (`lt dev install` first)
408
+ - Caddy daemon not running (run `lt dev install` — it bootstraps the lt-dev service)
409
+ - Already running for this project (`lt dev down` first)
410
+ - Internal port already in use
374
411
 
375
- **Logs:** `<root>/.lt-local/api.log`, `<root>/.lt-local/app.log` (append-mode).
412
+ **Logs:** `<root>/.lt-dev/api.log`, `<root>/.lt-dev/app.log` (append-mode).
376
413
 
377
414
  ---
378
415
 
379
- ### `lt local down`
416
+ ### `lt dev down`
380
417
 
381
- Stop processes started by `lt local up`. Sends `SIGTERM` to the detached process group (negative PID) so descendants — Vite, the Nest watcher, etc. — receive the signal too. Falls back to single-PID kill if the process group send fails (`EPERM`).
418
+ Stop processes started by `lt dev up` and remove the project's Caddy block.
382
419
 
383
420
  **Usage:**
384
421
  ```bash
385
- lt local down
422
+ lt dev down
386
423
  ```
387
424
 
388
- **Alias:** `lt l d`
425
+ **Alias:** `lt d d`
389
426
 
390
- PID values from `state.json` are validated (positive integer in `[100, 2^31)`) before any signal is sent, so a tampered state file cannot cause `lt local down` to signal arbitrary process groups.
427
+ Sends `SIGTERM` to the detached process group (negative PID) so descendants Vite, the Nest watcher, etc. receive the signal too. PID values from `state.json` are validated before signaling. Best-effort: removes the project's Caddy block and reloads even if no session was active.
391
428
 
392
429
  ---
393
430
 
394
- ### `lt local status`
431
+ ### `lt dev status`
395
432
 
396
- Show what is registered + running for the current project: slot, ports, db URI, PIDs (alive/dead), and live `lsof` state of the assigned ports.
433
+ Show what is registered + running.
397
434
 
398
435
  **Usage:**
399
436
  ```bash
400
- lt local status
437
+ lt dev status # current project
438
+ lt dev status --all # every project in the registry
401
439
  ```
402
440
 
403
- **Alias:** `lt l s`
404
-
405
- ---
441
+ **Alias:** `lt d s`
406
442
 
407
- ## Ports Commands
443
+ The current-project view shows subdomains → upstream ports, db URI, session PIDs (alive/dead), and live `lsof` state. The `--all` view lists every project, with a `●`/`○` indicator for running state.
408
444
 
409
- Inspect the port registry and currently-bound dev ports across all your lt projects. Useful for diagnosing collisions and rebuilding the registry from disk.
445
+ ---
410
446
 
411
- ### `lt ports`
447
+ ### `lt dev tunnel`
412
448
 
413
- List all reserved registry entries side-by-side with the live `lsof` state. Issues a single `lsof` call internally for the entire slot range (3000–3899) instead of per port — runs in ~150ms regardless of how many projects are registered.
449
+ Expose a running `lt dev up` project to the public internet via a Cloudflare Quick Tunnel. Foreground command — runs until Ctrl-C.
414
450
 
415
451
  **Usage:**
416
452
  ```bash
417
- lt ports
453
+ lt dev tunnel # tunnel the App
454
+ lt dev tunnel --api # tunnel the API instead
418
455
  ```
419
456
 
420
- **Alias:** `lt p`
457
+ **Alias:** `lt d tun`
421
458
 
422
- **Output sections:**
423
- 1. **Reserved ports (registry)** every project entry with a `●` (bound) or `○` (free) indicator per port.
424
- 2. **Currently bound dev ports (3000–3899)** — every port in the slot range that currently has a LISTEN socket, with command + PID + owning registry entry (if any).
459
+ **What it does:**
460
+ 1. Checks `cloudflared` is on PATH (suggests `brew install cloudflared` otherwise).
461
+ 2. Confirms the Caddy daemon is up (`lt dev install` must have run).
462
+ 3. Spawns `cloudflared tunnel --url https://<slug>.localhost --http-host-header <slug>.localhost --no-tls-verify`. The host-header rewrite is required — without it Caddy would not match the project block for the random `*.trycloudflare.com` hostname.
463
+ 4. Waits for cloudflared to publish the public URL (usually 5-10s) and prints it prominently.
464
+
465
+ **Caveats (also printed at runtime):**
466
+ - Auth cookies on the localhost domain are NOT valid on the `*.trycloudflare.com` URL — users log in again on the tunnel URL.
467
+ - Better-Auth's `trustedOrigins` won't include the random tunnel URL — login flows that validate the origin reject the request unless the URL is added explicitly to the API config.
468
+ - Default tunnels expose ONLY the App. For full external usage (e.g. external client calling the API), start a second `lt dev tunnel --api` in another shell — the API will be reachable on its own `*.trycloudflare.com` URL.
469
+
470
+ **Not yet supported (intentional scope limit):**
471
+ - Named tunnels with a persistent URL (`cloudflared tunnel create`)
472
+ - Multi-host tunneling in one process
473
+ - Background/detached mode
425
474
 
426
475
  ---
427
476
 
428
- ### `lt ports check <port>`
477
+ ### `lt dev doctor`
429
478
 
430
- Exit-coded port probe useful in shell scripts.
479
+ Diagnose Caddy / CA / DNS / port issues. Exit code 0 = all green, 1 = at least one FAIL.
431
480
 
432
481
  **Usage:**
433
482
  ```bash
434
- lt ports check <port>
483
+ lt dev doctor
435
484
  ```
436
485
 
437
- **Exit codes:**
438
- | Code | Meaning |
439
- |------|---------|
440
- | `0` | Port is free |
441
- | `1` | Port is in use |
442
- | `2` | `lsof` not available, or `<port>` argument missing/invalid |
486
+ **Alias:** `lt d doc`
443
487
 
444
- **Example:**
445
- ```bash
446
- if lt ports check 3000; then
447
- echo "API port free"
448
- else
449
- echo "API port already bound"
450
- fi
451
- ```
488
+ **Checks:**
489
+ 1. `caddy` on PATH
490
+ 2. Caddy daemon running (admin endpoint `:2019` reachable)
491
+ 3. Caddyfile validates
492
+ 4. Ports 80 + 443 free or held by Caddy
493
+ 5. `*.localhost` resolves to `127.0.0.1` (RFC 6761)
494
+ 6. Registry status
452
495
 
453
496
  ---
454
497
 
455
- ### `lt ports scan [dir]`
498
+ ### `lt dev test`
456
499
 
457
- Rebuild the registry from the filesystem. Walks the given directory (default: cwd) up to depth 3, looking for `lt.config.json` + `package.json` pairs or workspace markers (`pnpm-workspace.yaml`, `projects/`). Re-allocates a slot for new projects; preserves slots for existing entries (only refreshing the path if it moved). Writes only when the registry actually changed (no mtime churn for cloud-sync tools).
500
+ One-shot E2E wrapper: ensure `up`, wait for the App URL, run `pnpm run test:e2e` with the `.lt-dev/.env` bridge loaded. Optional teardown after.
458
501
 
459
502
  **Usage:**
460
503
  ```bash
461
- lt ports scan [dir]
504
+ lt dev test # App E2E (projects/app)
505
+ lt dev test --api # API E2E (projects/api) — no Caddy required
506
+ lt dev test --teardown # plus `lt dev down` after
507
+ lt dev test --debug # PWDEBUG=1 + HEADED=1
508
+ lt dev test -- --ui spec.ts # everything after `--` is forwarded to playwright
462
509
  ```
463
510
 
464
- **Examples:**
465
- ```bash
466
- lt ports scan # scan from cwd
467
- lt ports scan ~/code/lenneTech # scan a specific tree
468
- ```
511
+ **Alias:** `lt d t`
512
+
513
+ **Behaviour:**
514
+ 1. Pre-flight: Caddy installed + daemon running (App mode only).
515
+ 2. If no `lt dev up` session is alive: invokes `lt dev up` first.
516
+ 3. Waits up to 30 s for the App URL to respond.
517
+ 4. Reads `<root>/.lt-dev/.env` and merges into the spawn env (existing process.env wins for keys it defines).
518
+ 5. Spawns `pnpm run test:e2e [forwarded args]` in `projects/api` (with `--api`) or `projects/app` (default).
519
+ 6. With `--teardown`, runs `lt dev down` after.
520
+
521
+ **When to use this vs. `pnpm run test:e2e` directly:**
522
+ - Use **`lt dev test`** for TDD loops, ad-hoc reproduction, or when you want a single-command "ensure-up + run + teardown" flow.
523
+ - Use **direct `pnpm run test:e2e`** (or VS Code Playwright Extension, IDE test runners) for everyday work — the auto-injected `playwright.config.ts` bridge loads the `.lt-dev/.env` automatically, so the env is correct without the wrapper.
524
+
525
+ ---
526
+
527
+ ### ENV bridge for external test runners
528
+
529
+ `lt dev up` writes a `<root>/.lt-dev/.env` file with the following keys:
530
+
531
+ | Key | Source |
532
+ |-----|--------|
533
+ | `BASE_URL`, `APP_URL`, `NSC__BASE_URL`, `NSC__APP_URL` | Identity → `https://api.<slug>.localhost` / `https://<slug>.localhost` |
534
+ | `NUXT_API_URL`, `NUXT_PUBLIC_API_URL`, `NUXT_PUBLIC_SITE_URL` | Same URLs for Nuxt |
535
+ | `NUXT_PUBLIC_STORAGE_PREFIX` | Project slug |
536
+ | `NUXT_PUBLIC_API_PROXY` | Always `false` under `lt dev` |
537
+ | `NSC__MONGOOSE__URI`, `DATABASE_URL` | Project-namespaced DB URI (when `dbName` known) |
538
+ | `LT_DEV_ACTIVE`, `LT_DEV_DB_NAME` | Marker keys for consumers |
539
+ | `NODE_EXTRA_CA_CERTS` | Path to Caddy's root CA cert (auto-detected) |
540
+
541
+ `lt dev migrate` injects a tiny `// >>> lt-dev:bridge >>>` block at the top of `playwright.config.ts` that loads this file at config-load time — making Playwright (CLI, IDE, VS Code extension) automatically use the `lt dev` URLs and trust the local CA, without inheriting the parent shell.
469
542
 
470
- Symlinks are skipped to avoid traversal loops; dotdirs and `node_modules` are not descended into.
543
+ `lt dev down` removes the bridge file so subsequent runs without `lt dev up` fall back cleanly to the classic `localhost:3000`/`localhost:3001` defaults.
471
544
 
472
545
  ---
473
546
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lenne.tech/cli",
3
- "version": "1.21.0",
3
+ "version": "1.23.0",
4
4
  "description": "lenne.Tech CLI: lt",
5
5
  "keywords": [
6
6
  "lenne.Tech",
@@ -1,71 +0,0 @@
1
- "use strict";
2
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
- return new (P || (P = Promise))(function (resolve, reject) {
5
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
- step((generator = generator.apply(thisArg, _arguments || [])).next());
9
- });
10
- };
11
- Object.defineProperty(exports, "__esModule", { value: true });
12
- const local_project_1 = require("../../lib/local-project");
13
- const port_registry_1 = require("../../lib/port-registry");
14
- /**
15
- * Stop processes started by `lt local up`. Sends SIGTERM to the
16
- * detached process group (negative PID) so descendants — Vite,
17
- * the Nest watcher etc. — receive the signal too.
18
- */
19
- const DownCommand = {
20
- alias: ['d'],
21
- description: 'Stop API + App',
22
- hidden: false,
23
- name: 'down',
24
- run: (toolbox) => __awaiter(void 0, void 0, void 0, function* () {
25
- const { filesystem, parameters, print: { colors, info, success, warning }, } = toolbox;
26
- const layout = (0, local_project_1.resolveLayout)(filesystem.cwd(), filesystem);
27
- const state = (0, port_registry_1.loadLocalState)(layout.root);
28
- if (!state || (!state.pids.api && !state.pids.app)) {
29
- info(colors.dim('No running processes registered for this project.'));
30
- if (!parameters.options.fromGluegunMenu)
31
- process.exit();
32
- return 'local down: nothing to stop';
33
- }
34
- const stopped = [];
35
- for (const [name, pid] of Object.entries(state.pids)) {
36
- if (!pid)
37
- continue;
38
- // Defense-in-depth: refuse anything that loadLocalState's schema gate
39
- // wouldn't have accepted. Prevents a tampered state.json from causing
40
- // process.kill(-pid, …) to signal arbitrary process groups.
41
- if (!(0, port_registry_1.isValidPid)(pid)) {
42
- warning(`Refusing to signal suspicious pid ${pid} for ${name} (state.json tampered?)`);
43
- continue;
44
- }
45
- if (!(0, port_registry_1.isPidAlive)(pid)) {
46
- stopped.push(`${name} (pid ${pid}, already dead)`);
47
- continue;
48
- }
49
- try {
50
- // Negative PID kills the process group of a detached process.
51
- process.kill(-pid, 'SIGTERM');
52
- stopped.push(`${name} (pid ${pid})`);
53
- }
54
- catch (_a) {
55
- try {
56
- process.kill(pid, 'SIGTERM');
57
- stopped.push(`${name} (pid ${pid}, single)`);
58
- }
59
- catch (_b) {
60
- warning(`Failed to stop ${name} (pid ${pid})`);
61
- }
62
- }
63
- }
64
- (0, port_registry_1.clearLocalState)(layout.root);
65
- success(`Stopped: ${stopped.join(', ')}`);
66
- if (!parameters.options.fromGluegunMenu)
67
- process.exit();
68
- return `local down: ${stopped.length} stopped`;
69
- }),
70
- };
71
- module.exports = DownCommand;