@lenne.tech/cli 1.20.0 → 1.21.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.
- package/README.md +19 -0
- package/build/commands/local/down.js +71 -0
- package/build/commands/local/init.js +162 -0
- package/build/commands/local/local.js +30 -0
- package/build/commands/local/status.js +69 -0
- package/build/commands/local/up.js +148 -0
- package/build/commands/ports/ports.js +118 -0
- package/build/commands/ports/scan.js +131 -0
- package/build/commands/status.js +37 -6
- package/build/extensions/server.js +14 -4
- package/build/lib/local-patches.js +175 -0
- package/build/lib/local-project.js +101 -0
- package/build/lib/port-registry.js +304 -0
- package/docs/commands.md +189 -0
- package/package.json +7 -10
|
@@ -0,0 +1,304 @@
|
|
|
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
|
+
exports.SLOT_PORT_RANGE_END = exports.SLOT_MAX = exports.SLOT_STEP = exports.SLOT_BASE_API = void 0;
|
|
13
|
+
exports.allocatedSlots = allocatedSlots;
|
|
14
|
+
exports.allocateSlot = allocateSlot;
|
|
15
|
+
exports.checkPortInUse = checkPortInUse;
|
|
16
|
+
exports.clearLocalState = clearLocalState;
|
|
17
|
+
exports.isPidAlive = isPidAlive;
|
|
18
|
+
exports.isValidPid = isValidPid;
|
|
19
|
+
exports.listenSnapshot = listenSnapshot;
|
|
20
|
+
exports.loadLocalState = loadLocalState;
|
|
21
|
+
exports.loadRegistry = loadRegistry;
|
|
22
|
+
exports.localStatePath = localStatePath;
|
|
23
|
+
exports.portsForSlot = portsForSlot;
|
|
24
|
+
exports.projectSlug = projectSlug;
|
|
25
|
+
exports.registryPath = registryPath;
|
|
26
|
+
exports.saveLocalState = saveLocalState;
|
|
27
|
+
exports.saveRegistry = saveRegistry;
|
|
28
|
+
exports.slotFromSlug = slotFromSlug;
|
|
29
|
+
/**
|
|
30
|
+
* Port registry helpers for `lt local` and `lt ports`.
|
|
31
|
+
*
|
|
32
|
+
* Provides:
|
|
33
|
+
* - Deterministic slot allocation from a project slug (hash-based, reproducible across machines)
|
|
34
|
+
* - Persistent registry at ~/.lenneTech/ports.json
|
|
35
|
+
* - Live port introspection via `lsof` (single-call snapshot for batch checks)
|
|
36
|
+
* - Process state tracking under <project>/.lt-local/state.json
|
|
37
|
+
*/
|
|
38
|
+
const child_process_1 = require("child_process");
|
|
39
|
+
const fs_1 = require("fs");
|
|
40
|
+
const os_1 = require("os");
|
|
41
|
+
const path_1 = require("path");
|
|
42
|
+
/** Lowest API port: slot 0 maps to 3000/3001, slot 1 to 3010/3011, … */
|
|
43
|
+
exports.SLOT_BASE_API = 3000;
|
|
44
|
+
/** Distance between two adjacent slots' API ports. */
|
|
45
|
+
exports.SLOT_STEP = 10;
|
|
46
|
+
/** Number of slots [0..SLOT_MAX). API range = [SLOT_BASE_API, SLOT_BASE_API + SLOT_MAX*SLOT_STEP). */
|
|
47
|
+
exports.SLOT_MAX = 90;
|
|
48
|
+
/** Highest port (exclusive) covered by the slot range. Useful for live-port sweeps. */
|
|
49
|
+
exports.SLOT_PORT_RANGE_END = exports.SLOT_BASE_API + exports.SLOT_MAX * exports.SLOT_STEP;
|
|
50
|
+
/** All currently allocated slots in the registry. */
|
|
51
|
+
function allocatedSlots(registry) {
|
|
52
|
+
return new Set(Object.values(registry.projects).map((p) => p.slot));
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Allocate a slot for a project. Returns deterministic slot from slug if free,
|
|
56
|
+
* otherwise scans linearly until a free slot is found. Throws if all are taken.
|
|
57
|
+
*
|
|
58
|
+
* The linear scan loops `i in [1, SLOT_MAX)` (not `<=`) because `i = 0` is the
|
|
59
|
+
* preferred slot already checked in the fast path above.
|
|
60
|
+
*/
|
|
61
|
+
function allocateSlot(slug, registry) {
|
|
62
|
+
const taken = allocatedSlots(registry);
|
|
63
|
+
const preferred = slotFromSlug(slug);
|
|
64
|
+
if (!taken.has(preferred)) {
|
|
65
|
+
return preferred;
|
|
66
|
+
}
|
|
67
|
+
for (let i = 1; i < exports.SLOT_MAX; i++) {
|
|
68
|
+
const candidate = (preferred + i) % exports.SLOT_MAX;
|
|
69
|
+
if (!taken.has(candidate)) {
|
|
70
|
+
return candidate;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
throw new Error('No free port slot available (all 90 slots are taken).');
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Check via `lsof` whether a single TCP port is currently bound by a LISTEN socket.
|
|
77
|
+
*
|
|
78
|
+
* For multi-port checks prefer {@link listenSnapshot} — it issues a single `lsof`
|
|
79
|
+
* call instead of one per port (~50ms vs ~50ms × N).
|
|
80
|
+
*
|
|
81
|
+
* Note: `-iTCP:<port>` selects connections by *service port* — both LISTEN
|
|
82
|
+
* sockets and remote endpoints. We filter explicitly to LISTEN via
|
|
83
|
+
* `-sTCP:LISTEN` AND post-filter the NAME column for `*:<port>` /
|
|
84
|
+
* `<addr>:<port>` (LISTEN) so outgoing connections whose remote port is
|
|
85
|
+
* `<port>` don't trigger a false positive.
|
|
86
|
+
*
|
|
87
|
+
* Returns null if lsof is unavailable.
|
|
88
|
+
*/
|
|
89
|
+
function checkPortInUse(port) {
|
|
90
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
91
|
+
return new Promise((resolve) => {
|
|
92
|
+
var _a;
|
|
93
|
+
const child = (0, child_process_1.spawn)('lsof', ['-iTCP', `-sTCP:LISTEN`, '-nP', `-iTCP:${port}`], {
|
|
94
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
95
|
+
});
|
|
96
|
+
let out = '';
|
|
97
|
+
(_a = child.stdout) === null || _a === void 0 ? void 0 : _a.on('data', (chunk) => (out += chunk.toString()));
|
|
98
|
+
child.on('error', () => resolve(null));
|
|
99
|
+
child.on('close', () => {
|
|
100
|
+
const lines = out.trim().split('\n').slice(1); // skip header
|
|
101
|
+
const portRe = new RegExp(`:${port}\\s+\\(LISTEN\\)\\s*$`);
|
|
102
|
+
const match = lines.find((l) => portRe.test(l));
|
|
103
|
+
if (!match) {
|
|
104
|
+
resolve({ inUse: false });
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const cols = match.split(/\s+/);
|
|
108
|
+
resolve({ command: cols[0], inUse: true, pid: Number(cols[1]) });
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
/** Reset state file to an empty record. Called by `lt local down` after stopping processes. */
|
|
114
|
+
function clearLocalState(projectPath) {
|
|
115
|
+
const path = localStatePath(projectPath);
|
|
116
|
+
if ((0, fs_1.existsSync)(path)) {
|
|
117
|
+
(0, fs_1.writeFileSync)(path, `${JSON.stringify({ pids: {}, ports: { api: 0, app: 0 }, startedAt: '' }, null, 2)}\n`, 'utf8');
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Check whether a PID is still alive without sending any signal.
|
|
122
|
+
* `process.kill(pid, 0)` performs a permission check; ESRCH means dead.
|
|
123
|
+
*
|
|
124
|
+
* Refuses non-positive / non-integer PIDs to prevent accidental probes
|
|
125
|
+
* of process groups (negative PID) or every user-owned process (PID 0/-1).
|
|
126
|
+
*/
|
|
127
|
+
function isPidAlive(pid) {
|
|
128
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
129
|
+
return false;
|
|
130
|
+
try {
|
|
131
|
+
process.kill(pid, 0);
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
catch (e) {
|
|
135
|
+
return e.code === 'EPERM';
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Validate a value parsed from `state.json` as a plausible PID.
|
|
140
|
+
*
|
|
141
|
+
* Accepts: positive integer in [100, 2^31 - 1] or undefined.
|
|
142
|
+
* The lower bound 100 excludes init / kernel / login PIDs that should
|
|
143
|
+
* never be the result of a `pnpm start` spawn.
|
|
144
|
+
*/
|
|
145
|
+
function isValidPid(value) {
|
|
146
|
+
if (value === undefined)
|
|
147
|
+
return true;
|
|
148
|
+
if (typeof value !== 'number')
|
|
149
|
+
return false;
|
|
150
|
+
return Number.isInteger(value) && value >= 100 && value <= 0x7fffffff;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* One-shot listener snapshot for an arbitrary set of ports.
|
|
154
|
+
*
|
|
155
|
+
* Issues a single `lsof -iTCP -sTCP:LISTEN -nP` call and filters in memory.
|
|
156
|
+
* ~50ms total regardless of port count, vs ~50ms × N for sequential
|
|
157
|
+
* {@link checkPortInUse} calls.
|
|
158
|
+
*
|
|
159
|
+
* Returns an empty Map if `lsof` is unavailable.
|
|
160
|
+
*/
|
|
161
|
+
function listenSnapshot(ports) {
|
|
162
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
163
|
+
return new Promise((resolve) => {
|
|
164
|
+
var _a;
|
|
165
|
+
const child = (0, child_process_1.spawn)('lsof', ['-iTCP', '-sTCP:LISTEN', '-nP'], { stdio: ['ignore', 'pipe', 'ignore'] });
|
|
166
|
+
let out = '';
|
|
167
|
+
(_a = child.stdout) === null || _a === void 0 ? void 0 : _a.on('data', (chunk) => (out += chunk.toString()));
|
|
168
|
+
child.on('error', () => resolve(new Map()));
|
|
169
|
+
child.on('close', () => {
|
|
170
|
+
const wanted = new Set(ports);
|
|
171
|
+
const result = new Map();
|
|
172
|
+
const lines = out.trim().split('\n').slice(1);
|
|
173
|
+
const re = /:(\d+)\s+\(LISTEN\)\s*$/;
|
|
174
|
+
for (const line of lines) {
|
|
175
|
+
const m = re.exec(line);
|
|
176
|
+
if (!m)
|
|
177
|
+
continue;
|
|
178
|
+
const port = Number(m[1]);
|
|
179
|
+
if (!wanted.has(port))
|
|
180
|
+
continue;
|
|
181
|
+
if (result.has(port))
|
|
182
|
+
continue; // first hit wins (IPv4 before IPv6)
|
|
183
|
+
const cols = line.split(/\s+/);
|
|
184
|
+
result.set(port, { command: cols[0], pid: Number(cols[1]) });
|
|
185
|
+
}
|
|
186
|
+
resolve(result);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Load the local state JSON for a project.
|
|
193
|
+
*
|
|
194
|
+
* Returns null when the file is missing, unreadable, malformed, or contains
|
|
195
|
+
* structurally invalid data (see {@link isValidPid}). The schema validation
|
|
196
|
+
* here is the authoritative gate: it prevents `process.kill(-pid, …)` in
|
|
197
|
+
* `lt local down` from receiving anything but a plausible PID we ourselves
|
|
198
|
+
* could have written.
|
|
199
|
+
*/
|
|
200
|
+
function loadLocalState(projectPath) {
|
|
201
|
+
const path = localStatePath(projectPath);
|
|
202
|
+
if (!(0, fs_1.existsSync)(path))
|
|
203
|
+
return null;
|
|
204
|
+
try {
|
|
205
|
+
const parsed = JSON.parse((0, fs_1.readFileSync)(path, 'utf8'));
|
|
206
|
+
if (!parsed || typeof parsed !== 'object')
|
|
207
|
+
return null;
|
|
208
|
+
const obj = parsed;
|
|
209
|
+
const pids = obj.pids;
|
|
210
|
+
const ports = obj.ports;
|
|
211
|
+
if (!pids || typeof pids !== 'object' || !ports || typeof ports !== 'object')
|
|
212
|
+
return null;
|
|
213
|
+
if (!isValidPid(pids.api) || !isValidPid(pids.app))
|
|
214
|
+
return null;
|
|
215
|
+
if (typeof ports.api !== 'number' || typeof ports.app !== 'number')
|
|
216
|
+
return null;
|
|
217
|
+
if (typeof obj.startedAt !== 'string')
|
|
218
|
+
return null;
|
|
219
|
+
return {
|
|
220
|
+
pids: { api: pids.api, app: pids.app },
|
|
221
|
+
ports: { api: ports.api, app: ports.app },
|
|
222
|
+
startedAt: obj.startedAt,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
catch (_a) {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Load registry; returns empty if missing or corrupt.
|
|
231
|
+
*
|
|
232
|
+
* Prints a warning when a corrupt or schema-incompatible file is encountered
|
|
233
|
+
* so the user notices the silent reset rather than discovering stale port
|
|
234
|
+
* allocations later.
|
|
235
|
+
*/
|
|
236
|
+
function loadRegistry() {
|
|
237
|
+
const path = registryPath();
|
|
238
|
+
if (!(0, fs_1.existsSync)(path)) {
|
|
239
|
+
return { projects: {}, version: 1 };
|
|
240
|
+
}
|
|
241
|
+
try {
|
|
242
|
+
const raw = (0, fs_1.readFileSync)(path, 'utf8');
|
|
243
|
+
const parsed = JSON.parse(raw);
|
|
244
|
+
if ((parsed === null || parsed === void 0 ? void 0 : parsed.version) !== 1 || typeof (parsed === null || parsed === void 0 ? void 0 : parsed.projects) !== 'object') {
|
|
245
|
+
console.warn(`[lt] ports.json has wrong schema (got version=${parsed === null || parsed === void 0 ? void 0 : parsed.version}); starting with empty registry.`);
|
|
246
|
+
return { projects: {}, version: 1 };
|
|
247
|
+
}
|
|
248
|
+
return parsed;
|
|
249
|
+
}
|
|
250
|
+
catch (e) {
|
|
251
|
+
console.warn(`[lt] ports.json was unreadable (${e.message}); starting with empty registry.`);
|
|
252
|
+
return { projects: {}, version: 1 };
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
/** Path to the local state file inside a project. */
|
|
256
|
+
function localStatePath(projectPath) {
|
|
257
|
+
return (0, path_1.join)(projectPath, '.lt-local', 'state.json');
|
|
258
|
+
}
|
|
259
|
+
/** Convert a slot to its API+App port pair. */
|
|
260
|
+
function portsForSlot(slot) {
|
|
261
|
+
const api = exports.SLOT_BASE_API + slot * exports.SLOT_STEP;
|
|
262
|
+
return { api, app: api + 1 };
|
|
263
|
+
}
|
|
264
|
+
/** Convert any project path → a stable slug (basename, lowercase, alpha-num + dashes). */
|
|
265
|
+
function projectSlug(projectPath) {
|
|
266
|
+
const base = projectPath.replace(/\/+$/, '').split('/').pop() || 'project';
|
|
267
|
+
return base
|
|
268
|
+
.toLowerCase()
|
|
269
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
270
|
+
.replace(/^-+|-+$/g, '');
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Path to the central registry.
|
|
274
|
+
*
|
|
275
|
+
* Honors `LT_PORTS_REGISTRY_PATH` for tests / non-default workspaces;
|
|
276
|
+
* falls back to `~/.lenneTech/ports.json`.
|
|
277
|
+
*/
|
|
278
|
+
function registryPath() {
|
|
279
|
+
return process.env.LT_PORTS_REGISTRY_PATH || (0, path_1.join)((0, os_1.homedir)(), '.lenneTech', 'ports.json');
|
|
280
|
+
}
|
|
281
|
+
/** Persist local state to <project>/.lt-local/state.json (creates the parent directory if needed). */
|
|
282
|
+
function saveLocalState(projectPath, state) {
|
|
283
|
+
const path = localStatePath(projectPath);
|
|
284
|
+
(0, fs_1.mkdirSync)((0, path_1.dirname)(path), { recursive: true });
|
|
285
|
+
(0, fs_1.writeFileSync)(path, `${JSON.stringify(state, null, 2)}\n`, 'utf8');
|
|
286
|
+
}
|
|
287
|
+
/** Save registry, creating parent directory if needed. */
|
|
288
|
+
function saveRegistry(registry) {
|
|
289
|
+
const path = registryPath();
|
|
290
|
+
(0, fs_1.mkdirSync)((0, path_1.dirname)(path), { recursive: true });
|
|
291
|
+
(0, fs_1.writeFileSync)(path, `${JSON.stringify(registry, null, 2)}\n`, 'utf8');
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Deterministic slot from project slug — same slug yields the same slot on every machine.
|
|
295
|
+
* Uses the lower 32 bits of FNV-1a then modulo SLOT_MAX.
|
|
296
|
+
*/
|
|
297
|
+
function slotFromSlug(slug) {
|
|
298
|
+
let hash = 2166136261; // FNV-1a 32-bit offset basis
|
|
299
|
+
for (let i = 0; i < slug.length; i++) {
|
|
300
|
+
hash ^= slug.charCodeAt(i);
|
|
301
|
+
hash = Math.imul(hash, 16777619);
|
|
302
|
+
}
|
|
303
|
+
return Math.abs(hash | 0) % exports.SLOT_MAX;
|
|
304
|
+
}
|
package/docs/commands.md
CHANGED
|
@@ -12,6 +12,8 @@ This document provides a comprehensive reference for all `lt` CLI commands. For
|
|
|
12
12
|
|
|
13
13
|
- [CLI Commands](#cli-commands)
|
|
14
14
|
- [Server Commands](#server-commands)
|
|
15
|
+
- [Local Development Commands](#local-development-commands)
|
|
16
|
+
- [Ports Commands](#ports-commands)
|
|
15
17
|
- [Git Commands](#git-commands)
|
|
16
18
|
- [Fullstack Commands](#fullstack-commands)
|
|
17
19
|
- [Deployment Commands](#deployment-commands)
|
|
@@ -282,6 +284,193 @@ For mode-aware update workflows after conversion, use:
|
|
|
282
284
|
|
|
283
285
|
---
|
|
284
286
|
|
|
287
|
+
## Local Development Commands
|
|
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`.
|
|
290
|
+
|
|
291
|
+
### `lt local`
|
|
292
|
+
|
|
293
|
+
Open the local-orchestration submenu.
|
|
294
|
+
|
|
295
|
+
**Usage:**
|
|
296
|
+
```bash
|
|
297
|
+
lt local
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
**Alias:** `lt l`
|
|
301
|
+
|
|
302
|
+
---
|
|
303
|
+
|
|
304
|
+
### `lt local init`
|
|
305
|
+
|
|
306
|
+
Register the current project in the central port registry, optionally patching legacy hardcoded ports to be env-aware.
|
|
307
|
+
|
|
308
|
+
**Usage:**
|
|
309
|
+
```bash
|
|
310
|
+
lt local init [options]
|
|
311
|
+
```
|
|
312
|
+
|
|
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) |
|
|
322
|
+
|
|
323
|
+
**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.
|
|
330
|
+
|
|
331
|
+
**Examples:**
|
|
332
|
+
```bash
|
|
333
|
+
# Inside a workspace or standalone project
|
|
334
|
+
lt local init
|
|
335
|
+
|
|
336
|
+
# Force slot 5 for predictable cross-team ports
|
|
337
|
+
lt local init --slot 5 --noConfirm --patch
|
|
338
|
+
|
|
339
|
+
# Register without touching any source files
|
|
340
|
+
lt local init --no-patch --noConfirm
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
---
|
|
344
|
+
|
|
345
|
+
### `lt local up`
|
|
346
|
+
|
|
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`.
|
|
348
|
+
|
|
349
|
+
**Usage:**
|
|
350
|
+
```bash
|
|
351
|
+
lt local up
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
**Alias:** `lt l u`
|
|
355
|
+
|
|
356
|
+
**Environment variables injected into both children:**
|
|
357
|
+
| Variable | Consumer | Example value |
|
|
358
|
+
|----------|----------|---------------|
|
|
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`).
|
|
369
|
+
|
|
370
|
+
**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
|
|
374
|
+
|
|
375
|
+
**Logs:** `<root>/.lt-local/api.log`, `<root>/.lt-local/app.log` (append-mode).
|
|
376
|
+
|
|
377
|
+
---
|
|
378
|
+
|
|
379
|
+
### `lt local down`
|
|
380
|
+
|
|
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`).
|
|
382
|
+
|
|
383
|
+
**Usage:**
|
|
384
|
+
```bash
|
|
385
|
+
lt local down
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
**Alias:** `lt l d`
|
|
389
|
+
|
|
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.
|
|
391
|
+
|
|
392
|
+
---
|
|
393
|
+
|
|
394
|
+
### `lt local status`
|
|
395
|
+
|
|
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.
|
|
397
|
+
|
|
398
|
+
**Usage:**
|
|
399
|
+
```bash
|
|
400
|
+
lt local status
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
**Alias:** `lt l s`
|
|
404
|
+
|
|
405
|
+
---
|
|
406
|
+
|
|
407
|
+
## Ports Commands
|
|
408
|
+
|
|
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.
|
|
410
|
+
|
|
411
|
+
### `lt ports`
|
|
412
|
+
|
|
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.
|
|
414
|
+
|
|
415
|
+
**Usage:**
|
|
416
|
+
```bash
|
|
417
|
+
lt ports
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
**Alias:** `lt p`
|
|
421
|
+
|
|
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).
|
|
425
|
+
|
|
426
|
+
---
|
|
427
|
+
|
|
428
|
+
### `lt ports check <port>`
|
|
429
|
+
|
|
430
|
+
Exit-coded port probe — useful in shell scripts.
|
|
431
|
+
|
|
432
|
+
**Usage:**
|
|
433
|
+
```bash
|
|
434
|
+
lt ports check <port>
|
|
435
|
+
```
|
|
436
|
+
|
|
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 |
|
|
443
|
+
|
|
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
|
+
```
|
|
452
|
+
|
|
453
|
+
---
|
|
454
|
+
|
|
455
|
+
### `lt ports scan [dir]`
|
|
456
|
+
|
|
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).
|
|
458
|
+
|
|
459
|
+
**Usage:**
|
|
460
|
+
```bash
|
|
461
|
+
lt ports scan [dir]
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
**Examples:**
|
|
465
|
+
```bash
|
|
466
|
+
lt ports scan # scan from cwd
|
|
467
|
+
lt ports scan ~/code/lenneTech # scan a specific tree
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
Symlinks are skipped to avoid traversal loops; dotdirs and `node_modules` are not descended into.
|
|
471
|
+
|
|
472
|
+
---
|
|
473
|
+
|
|
285
474
|
## Git Commands
|
|
286
475
|
|
|
287
476
|
All git commands support the `--noConfirm` flag and can be configured via `defaults.noConfirm` or `commands.git.noConfirm`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lenne.tech/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.21.0",
|
|
4
4
|
"description": "lenne.Tech CLI: lt",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"lenne.Tech",
|
|
@@ -58,16 +58,16 @@
|
|
|
58
58
|
"bin"
|
|
59
59
|
],
|
|
60
60
|
"dependencies": {
|
|
61
|
-
"@aws-sdk/client-s3": "3.
|
|
61
|
+
"@aws-sdk/client-s3": "3.1045.0",
|
|
62
62
|
"@lenne.tech/cli-plugin-helper": "0.0.14",
|
|
63
|
-
"axios": "1.
|
|
63
|
+
"axios": "1.16.0",
|
|
64
64
|
"bcrypt": "6.0.0",
|
|
65
|
-
"defuddle": "0.
|
|
65
|
+
"defuddle": "0.18.1",
|
|
66
66
|
"glob": "13.0.6",
|
|
67
67
|
"gluegun": "5.2.2",
|
|
68
68
|
"js-sha256": "0.11.1",
|
|
69
69
|
"js-yaml": "4.1.1",
|
|
70
|
-
"jsdom": "29.
|
|
70
|
+
"jsdom": "29.1.1",
|
|
71
71
|
"lodash": "4.18.1",
|
|
72
72
|
"open": "11.0.0",
|
|
73
73
|
"playwright-core": "1.59.1",
|
|
@@ -85,15 +85,12 @@
|
|
|
85
85
|
"@types/js-yaml": "4.0.9",
|
|
86
86
|
"@types/jsdom": "28.0.1",
|
|
87
87
|
"@types/lodash": "4.17.24",
|
|
88
|
-
"@types/node": "25.6.
|
|
88
|
+
"@types/node": "25.6.2",
|
|
89
89
|
"@types/turndown": "5.0.6",
|
|
90
|
-
"@typescript-eslint/eslint-plugin": "8.58.2",
|
|
91
|
-
"@typescript-eslint/parser": "8.58.2",
|
|
92
90
|
"ejs": "5.0.2",
|
|
93
91
|
"eslint": "9.39.4",
|
|
94
|
-
"eslint-config-prettier": "10.1.8",
|
|
95
92
|
"husky": "9.1.7",
|
|
96
|
-
"jest": "30.
|
|
93
|
+
"jest": "30.4.2",
|
|
97
94
|
"prettier": "3.8.3",
|
|
98
95
|
"rimraf": "6.1.3",
|
|
99
96
|
"standard-version": "9.5.0",
|