@lenne.tech/cli 1.40.0 → 1.41.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/build/commands/dev/status.js +39 -15
- package/build/commands/dev/up.js +62 -31
- package/build/lib/dev-process.js +21 -1
- package/build/lib/dev-state.js +54 -2
- package/docs/commands.md +13 -5
- package/package.json +1 -1
|
@@ -58,25 +58,38 @@ const StatusCommand = {
|
|
|
58
58
|
// port) count toward the health summary.
|
|
59
59
|
const comps = [];
|
|
60
60
|
if (e.internalPorts.api) {
|
|
61
|
-
comps.push((0, dev_state_1.classifyComponentHealth)({
|
|
61
|
+
comps.push((0, dev_state_1.classifyComponentHealth)({
|
|
62
|
+
pid: session === null || session === void 0 ? void 0 : session.pids.api,
|
|
63
|
+
portBound: snap.has(e.internalPorts.api),
|
|
64
|
+
startedAt: session === null || session === void 0 ? void 0 : session.startedAt,
|
|
65
|
+
}));
|
|
62
66
|
}
|
|
63
67
|
if (e.internalPorts.app) {
|
|
64
|
-
comps.push((0, dev_state_1.classifyComponentHealth)({
|
|
68
|
+
comps.push((0, dev_state_1.classifyComponentHealth)({
|
|
69
|
+
pid: session === null || session === void 0 ? void 0 : session.pids.app,
|
|
70
|
+
portBound: snap.has(e.internalPorts.app),
|
|
71
|
+
startedAt: session === null || session === void 0 ? void 0 : session.startedAt,
|
|
72
|
+
}));
|
|
65
73
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const
|
|
74
|
+
// Aggregate the per-component health into one honest stack glyph
|
|
75
|
+
// (pure, unit-tested in dev-state). Presentation (glyph + note) stays here.
|
|
76
|
+
const stack = (0, dev_state_1.summarizeStackHealth)(comps);
|
|
69
77
|
let status;
|
|
70
78
|
let note = '';
|
|
71
|
-
if (
|
|
79
|
+
if (stack === 'running') {
|
|
72
80
|
status = colors.green('●');
|
|
73
81
|
}
|
|
74
|
-
else if (
|
|
82
|
+
else if (stack === 'degraded') {
|
|
75
83
|
// Some up, some down — honest "partially up" rather than green.
|
|
76
84
|
status = colors.yellow('◐');
|
|
77
85
|
note = colors.yellow(' degraded — `lt dev up` to restart the down half');
|
|
78
86
|
}
|
|
79
|
-
else if (
|
|
87
|
+
else if (stack === 'starting') {
|
|
88
|
+
// Still booting after a recent `lt dev up` — not crashed, give it a moment.
|
|
89
|
+
status = colors.cyan('◐');
|
|
90
|
+
note = colors.dim(' starting — booting, give it a moment');
|
|
91
|
+
}
|
|
92
|
+
else if (stack === 'crashed') {
|
|
80
93
|
status = colors.yellow('◐');
|
|
81
94
|
note = colors.yellow(' crashed — `lt dev up` to restart');
|
|
82
95
|
}
|
|
@@ -177,16 +190,20 @@ const StatusCommand = {
|
|
|
177
190
|
const apiHealth = (0, dev_state_1.classifyComponentHealth)({
|
|
178
191
|
pid: session.pids.api,
|
|
179
192
|
portBound: entry.internalPorts.api ? snap.has(entry.internalPorts.api) : false,
|
|
193
|
+
startedAt: session.startedAt,
|
|
180
194
|
});
|
|
181
195
|
const appHealth = (0, dev_state_1.classifyComponentHealth)({
|
|
182
196
|
pid: session.pids.app,
|
|
183
197
|
portBound: entry.internalPorts.app ? snap.has(entry.internalPorts.app) : false,
|
|
198
|
+
startedAt: session.startedAt,
|
|
184
199
|
});
|
|
185
200
|
const label = (health) => health === 'running'
|
|
186
201
|
? colors.green('running')
|
|
187
|
-
: health === '
|
|
188
|
-
? colors.
|
|
189
|
-
:
|
|
202
|
+
: health === 'starting'
|
|
203
|
+
? colors.cyan('starting (booting — port not bound yet)')
|
|
204
|
+
: health === 'crashed'
|
|
205
|
+
? colors.yellow('crashed (supervisor up, port not listening)')
|
|
206
|
+
: colors.red('dead');
|
|
190
207
|
if (session.pids.api !== undefined || entry.internalPorts.api) {
|
|
191
208
|
info(` api: ${label(apiHealth)} (pid ${(_a = session.pids.api) !== null && _a !== void 0 ? _a : '-'})`);
|
|
192
209
|
}
|
|
@@ -208,10 +225,17 @@ const StatusCommand = {
|
|
|
208
225
|
// half and leaves the healthy one running.
|
|
209
226
|
const apiPresent = session.pids.api !== undefined || !!entry.internalPorts.api;
|
|
210
227
|
const appPresent = session.pids.app !== undefined || !!entry.internalPorts.app;
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
228
|
+
// A `starting` component is booting, not down — don't tell the user to
|
|
229
|
+
// restart a healthy stack that just needs a few more seconds. Partition is
|
|
230
|
+
// pure + unit-tested in dev-state.
|
|
231
|
+
const { down, starting } = (0, dev_state_1.partitionComponentStates)([
|
|
232
|
+
{ health: apiHealth, name: 'api', present: apiPresent },
|
|
233
|
+
{ health: appHealth, name: 'app', present: appPresent },
|
|
234
|
+
]);
|
|
235
|
+
if (starting.length > 0) {
|
|
236
|
+
info('');
|
|
237
|
+
info(colors.cyan(` ${starting.join(' + ')} still booting — give it a few seconds, then re-run \`lt dev status\`.`));
|
|
238
|
+
}
|
|
215
239
|
if (down.length > 0) {
|
|
216
240
|
const crashed = (apiPresent && apiHealth === 'crashed') || (appPresent && appHealth === 'crashed');
|
|
217
241
|
info('');
|
package/build/commands/dev/up.js
CHANGED
|
@@ -86,7 +86,7 @@ const UpCommand = {
|
|
|
86
86
|
hidden: false,
|
|
87
87
|
name: 'up',
|
|
88
88
|
run: (toolbox) => __awaiter(void 0, void 0, void 0, function* () {
|
|
89
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
|
|
89
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
|
|
90
90
|
const { filesystem, parameters, print: { colors, error, info, success, warning }, } = toolbox;
|
|
91
91
|
const layout = (0, dev_project_1.resolveLayout)(filesystem.cwd(), filesystem);
|
|
92
92
|
if (!layout.apiDir && !layout.appDir) {
|
|
@@ -260,16 +260,33 @@ const UpCommand = {
|
|
|
260
260
|
// ── Health-aware (re)start decision ──────────────────────────────────────
|
|
261
261
|
// Probe the just-resolved ports so we can tell a still-serving component
|
|
262
262
|
// from a crashed one (supervisor PID alive, port free). Only dead/crashed
|
|
263
|
-
// components get (re)started; a
|
|
263
|
+
// components get (re)started; a running one keeps serving untouched — and a
|
|
264
|
+
// still-BOOTING one (`starting`: PID alive, port not bound yet, within the
|
|
265
|
+
// startup grace window after a recent `up`) is ALSO kept, not force-restarted.
|
|
266
|
+
// Passing `startedAt` is what lets the classifier distinguish that boot window
|
|
267
|
+
// from a real crash — otherwise a re-run of `lt dev up` during the API's slow
|
|
268
|
+
// boot would kill the still-booting component and reset its progress (the very
|
|
269
|
+
// false-positive the `starting` state exists to prevent).
|
|
264
270
|
const hasApi = Boolean(layout.apiDir && (0, fs_1.existsSync)((0, path_1.join)(layout.apiDir, 'package.json')) && apiPort);
|
|
265
271
|
const hasApp = Boolean(layout.appDir && (0, fs_1.existsSync)((0, path_1.join)(layout.appDir, 'package.json')) && appPort);
|
|
266
272
|
const healthSnap = yield (0, dev_process_1.listenSnapshot)([apiPort, appPort].filter((p) => typeof p === 'number'));
|
|
267
273
|
const apiHealth = hasApi
|
|
268
|
-
? (0, dev_state_1.classifyComponentHealth)({
|
|
274
|
+
? (0, dev_state_1.classifyComponentHealth)({
|
|
275
|
+
pid: existingSession === null || existingSession === void 0 ? void 0 : existingSession.pids.api,
|
|
276
|
+
portBound: !!apiPort && healthSnap.has(apiPort),
|
|
277
|
+
startedAt: existingSession === null || existingSession === void 0 ? void 0 : existingSession.startedAt,
|
|
278
|
+
})
|
|
269
279
|
: undefined;
|
|
270
280
|
const appHealth = hasApp
|
|
271
|
-
? (0, dev_state_1.classifyComponentHealth)({
|
|
281
|
+
? (0, dev_state_1.classifyComponentHealth)({
|
|
282
|
+
pid: existingSession === null || existingSession === void 0 ? void 0 : existingSession.pids.app,
|
|
283
|
+
portBound: !!appPort && healthSnap.has(appPort),
|
|
284
|
+
startedAt: existingSession === null || existingSession === void 0 ? void 0 : existingSession.startedAt,
|
|
285
|
+
})
|
|
272
286
|
: undefined;
|
|
287
|
+
// A component we leave running untouched is either serving (`running`) or
|
|
288
|
+
// still within its boot grace window (`starting`).
|
|
289
|
+
const isKeepable = (h) => h === 'running' || h === 'starting';
|
|
273
290
|
// All present components already serving → nothing to do.
|
|
274
291
|
const presentHealth = [apiHealth, appHealth].filter((h) => h !== undefined);
|
|
275
292
|
if (presentHealth.length > 0 && presentHealth.every((h) => h === 'running')) {
|
|
@@ -289,18 +306,21 @@ const UpCommand = {
|
|
|
289
306
|
process.exit();
|
|
290
307
|
return 'dev up: already running';
|
|
291
308
|
}
|
|
292
|
-
// Partial restart — at least one component is
|
|
293
|
-
// down. Announce what we keep vs. restart so the user sees
|
|
294
|
-
|
|
309
|
+
// Partial restart — at least one component is kept (running or still booting)
|
|
310
|
+
// and at least one is down. Announce what we keep vs. restart so the user sees
|
|
311
|
+
// the honest state. A `starting` component is kept (booting), NOT restarted.
|
|
312
|
+
const partialRestart = presentHealth.some(isKeepable);
|
|
295
313
|
if (partialRestart) {
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
314
|
+
const announceDecision = (name, h, pid, port) => {
|
|
315
|
+
if (h === 'running')
|
|
316
|
+
info(colors.dim(`${name} healthy (pid ${pid}) → keeping`));
|
|
317
|
+
else if (h === 'starting')
|
|
318
|
+
info(colors.cyan(`${name} still booting (pid ${pid}) → keeping (grace window)`));
|
|
319
|
+
else if (h)
|
|
320
|
+
warning(`${name} ${h} (port ${port} not serving) → restarting`);
|
|
321
|
+
};
|
|
322
|
+
announceDecision('api', apiHealth, existingSession === null || existingSession === void 0 ? void 0 : existingSession.pids.api, apiPort);
|
|
323
|
+
announceDecision('app', appHealth, existingSession === null || existingSession === void 0 ? void 0 : existingSession.pids.app, appPort);
|
|
304
324
|
}
|
|
305
325
|
// Caddy block + reload.
|
|
306
326
|
const routes = [];
|
|
@@ -356,14 +376,15 @@ const UpCommand = {
|
|
|
356
376
|
yield (0, dev_process_1.terminateProcessGroup)(bound.pid);
|
|
357
377
|
});
|
|
358
378
|
if (hasApi && layout.apiDir && apiPort) {
|
|
359
|
-
if (apiHealth
|
|
379
|
+
if (isKeepable(apiHealth)) {
|
|
360
380
|
pids.api = existingSession === null || existingSession === void 0 ? void 0 : existingSession.pids.api;
|
|
361
381
|
kept.push('api');
|
|
362
|
-
// `--api-compiled` only takes effect when the API (re)starts; a
|
|
363
|
-
// kept as-is (force-restarting it would contradict the
|
|
364
|
-
// user how to switch a currently-
|
|
382
|
+
// `--api-compiled` only takes effect when the API (re)starts; a running or
|
|
383
|
+
// still-booting API is kept as-is (force-restarting it would contradict the
|
|
384
|
+
// keep logic), so tell the user how to switch a currently-live ts-node API
|
|
385
|
+
// to compiled.
|
|
365
386
|
if (apiCompiled)
|
|
366
|
-
info(colors.dim(' --api-compiled: API already running — `lt dev down` first to switch it.'));
|
|
387
|
+
info(colors.dim(' --api-compiled: API already running/booting — `lt dev down` first to switch it.'));
|
|
367
388
|
}
|
|
368
389
|
else {
|
|
369
390
|
yield reclaimPort(existingSession === null || existingSession === void 0 ? void 0 : existingSession.pids.api, apiPort, apiHealth !== null && apiHealth !== void 0 ? apiHealth : 'dead');
|
|
@@ -396,7 +417,7 @@ const UpCommand = {
|
|
|
396
417
|
}
|
|
397
418
|
}
|
|
398
419
|
if (hasApp && layout.appDir && appPort) {
|
|
399
|
-
if (appHealth
|
|
420
|
+
if (isKeepable(appHealth)) {
|
|
400
421
|
pids.app = existingSession === null || existingSession === void 0 ? void 0 : existingSession.pids.app;
|
|
401
422
|
kept.push('app');
|
|
402
423
|
}
|
|
@@ -417,28 +438,38 @@ const UpCommand = {
|
|
|
417
438
|
}
|
|
418
439
|
}
|
|
419
440
|
}
|
|
420
|
-
// Persist the session (PIDs) — merging kept (
|
|
421
|
-
// The registry entry (ports) was already reserved atomically
|
|
422
|
-
//
|
|
423
|
-
|
|
441
|
+
// Persist the session (PIDs) — merging kept (running/booting) + freshly
|
|
442
|
+
// started PIDs. The registry entry (ports) was already reserved atomically
|
|
443
|
+
// above. Reset the session clock ONLY when something was actually (re)started:
|
|
444
|
+
// - A freshly restarted component MUST get "now" so its startup grace window
|
|
445
|
+
// is measured from its real start — inheriting a stale `startedAt` would
|
|
446
|
+
// make a just-restarted, still-booting component read as `crashed` during
|
|
447
|
+
// its own boot (a kept `running` component is classified by port-bound, not
|
|
448
|
+
// `startedAt`, so a fresh timestamp never mis-ages it).
|
|
449
|
+
// - When nothing was started (all kept — e.g. both still booting), preserve
|
|
450
|
+
// the original so repeated `up` during a boot can't keep extending the
|
|
451
|
+
// grace window indefinitely.
|
|
452
|
+
const startedAt = started.length > 0 ? new Date().toISOString() : ((_h = existingSession === null || existingSession === void 0 ? void 0 : existingSession.startedAt) !== null && _h !== void 0 ? _h : new Date().toISOString());
|
|
424
453
|
(0, dev_state_1.saveSession)(layout.root, { pids, startedAt });
|
|
425
454
|
// Write the ENV bridge so external tools (Playwright, IDE test runners,
|
|
426
455
|
// custom shell scripts) can pick up the URLs without inheriting our shell.
|
|
427
456
|
const bridgePath = (0, dev_env_bridge_1.writeEnvBridge)(layout.root, devEnv, dbName);
|
|
428
457
|
info(colors.dim(`ENV bridge: ${bridgePath}`));
|
|
429
458
|
const summary = started.length === 0
|
|
430
|
-
?
|
|
459
|
+
? kept.length > 0
|
|
460
|
+
? `Kept ${kept.join('+')} (already running or booting)`
|
|
461
|
+
: 'Nothing restarted'
|
|
431
462
|
: kept.length > 0
|
|
432
463
|
? `Restarted ${started.join('+')} (kept ${kept.join('+')})`
|
|
433
464
|
: `Started ${started.join('+')}`;
|
|
434
|
-
success(`${summary}: api pid=${(
|
|
465
|
+
success(`${summary}: api pid=${(_j = pids.api) !== null && _j !== void 0 ? _j : '-'}, app pid=${(_k = pids.app) !== null && _k !== void 0 ? _k : '-'}`);
|
|
435
466
|
// Echo the bound URLs next to the PIDs as well — the "Starting" block
|
|
436
467
|
// prints them before the spawn, but on a long boot log they scroll out
|
|
437
468
|
// of view, so repeating them here keeps PID + URL visually grouped.
|
|
438
469
|
printProjectUrls(info, {
|
|
439
|
-
apiHostname: (
|
|
470
|
+
apiHostname: (_l = identity.subdomains.api) === null || _l === void 0 ? void 0 : _l.hostname,
|
|
440
471
|
apiUpstreamPort: apiPort,
|
|
441
|
-
appHostname: (
|
|
472
|
+
appHostname: (_m = identity.subdomains.app) === null || _m === void 0 ? void 0 : _m.hostname,
|
|
442
473
|
appUpstreamPort: appPort,
|
|
443
474
|
dbName: identity.subdomains.api ? dbName : undefined,
|
|
444
475
|
});
|
|
@@ -466,7 +497,7 @@ const UpCommand = {
|
|
|
466
497
|
try {
|
|
467
498
|
mainRepoRoot = (0, dev_ticket_1.gitMainRepoRoot)(layout.root);
|
|
468
499
|
}
|
|
469
|
-
catch (
|
|
500
|
+
catch (_o) {
|
|
470
501
|
/* not a git repo — registry prune still applies */
|
|
471
502
|
}
|
|
472
503
|
const mainLayout = (0, dev_project_1.resolveLayout)(mainRepoRoot, filesystem);
|
|
@@ -513,7 +544,7 @@ const UpCommand = {
|
|
|
513
544
|
}
|
|
514
545
|
}
|
|
515
546
|
}
|
|
516
|
-
catch (
|
|
547
|
+
catch (_p) {
|
|
517
548
|
/* never block `up` on cleanup */
|
|
518
549
|
}
|
|
519
550
|
}
|
package/build/lib/dev-process.js
CHANGED
|
@@ -223,7 +223,27 @@ function spawnDetached(cmd, args, opts) {
|
|
|
223
223
|
const out = (0, fs_1.openSync)(opts.logFile, 'a');
|
|
224
224
|
let child;
|
|
225
225
|
try {
|
|
226
|
-
|
|
226
|
+
// Raise the soft file-descriptor limit before exec-ing the real command.
|
|
227
|
+
// macOS's default soft RLIMIT_NOFILE is 256 (launchd/system default), inherited
|
|
228
|
+
// by the terminal that runs `lt dev up` and therefore by these detached children
|
|
229
|
+
// — it is NOT a consequence of the lt-dev LaunchAgent (which runs only Caddy).
|
|
230
|
+
// The dev file-watcher (nest/nuxt → chokidar) exhausts a soft-256 limit on a
|
|
231
|
+
// monorepo → intermittent "EMFILE: too many open files, watch" crashes on boot
|
|
232
|
+
// that force a manual `lt dev up`. We wrap the command in `sh -c "ulimit …; exec …"`:
|
|
233
|
+
// - `exec` replaces the shell IN PLACE → the recorded PID and the detached
|
|
234
|
+
// process group are still the real process (PID tracking + group-kill in
|
|
235
|
+
// `terminateProcessGroup` keep working).
|
|
236
|
+
// - `"$0" "$@"` pass cmd + args verbatim — no shell-quoting / injection.
|
|
237
|
+
// - the cascade tries a high limit first, falling back on machines with a
|
|
238
|
+
// lower `kern.maxfilesperproc`; `2>/dev/null` keeps it best-effort.
|
|
239
|
+
// Note: because the outer `spawn('/bin/sh', …)` almost always succeeds, a bogus
|
|
240
|
+
// `cmd` no longer surfaces as `pid === undefined` here — the inner `exec` fails
|
|
241
|
+
// (exit 127) a few ms later. Callers briefly record a live-then-dead PID, which
|
|
242
|
+
// `classifyComponentHealth` reaps as `dead`/`crashed` on the next status/up. We
|
|
243
|
+
// deliberately don't watch for that here: a detached, unref'd child's 127 exit is
|
|
244
|
+
// racy to observe, and callers must be self-correcting against real crashes anyway.
|
|
245
|
+
const raiseFdLimit = 'ulimit -n 65536 2>/dev/null || ulimit -n 10240 2>/dev/null || true';
|
|
246
|
+
child = (0, child_process_1.spawn)('/bin/sh', ['-c', `${raiseFdLimit}; exec "$0" "$@"`, cmd, ...args], {
|
|
227
247
|
cwd: opts.cwd,
|
|
228
248
|
detached: true,
|
|
229
249
|
env: opts.env,
|
package/build/lib/dev-state.js
CHANGED
|
@@ -9,8 +9,10 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
9
9
|
});
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
-
exports.paths = exports.TEST_SESSION_FILE = void 0;
|
|
12
|
+
exports.paths = exports.TEST_SESSION_FILE = exports.STARTUP_GRACE_MS = void 0;
|
|
13
13
|
exports.classifyComponentHealth = classifyComponentHealth;
|
|
14
|
+
exports.partitionComponentStates = partitionComponentStates;
|
|
15
|
+
exports.summarizeStackHealth = summarizeStackHealth;
|
|
14
16
|
exports.allocateInternalPort = allocateInternalPort;
|
|
15
17
|
exports.clearSession = clearSession;
|
|
16
18
|
exports.detectSlugConflict = detectSlugConflict;
|
|
@@ -38,16 +40,66 @@ exports.withRegistryLock = withRegistryLock;
|
|
|
38
40
|
const fs_1 = require("fs");
|
|
39
41
|
const os_1 = require("os");
|
|
40
42
|
const path_1 = require("path");
|
|
43
|
+
/**
|
|
44
|
+
* How long after `lt dev up` a not-yet-bound-but-PID-alive component is treated
|
|
45
|
+
* as `starting` (booting) rather than `crashed`. Covers the slow API boot
|
|
46
|
+
* (compile + Mongo + Better Auth + AI tools + migrations) with headroom.
|
|
47
|
+
*/
|
|
48
|
+
exports.STARTUP_GRACE_MS = 60000;
|
|
41
49
|
/**
|
|
42
50
|
* Classify a component's true health from its recorded wrapper PID plus whether
|
|
43
51
|
* its internal port is actually bound (caller provides the port probe result,
|
|
44
52
|
* typically from a single {@link import('./dev-process').listenSnapshot} call).
|
|
53
|
+
*
|
|
54
|
+
* When `startedAt` is supplied, a PID-alive-but-port-unbound component is reported
|
|
55
|
+
* as `starting` (booting) rather than `crashed` for the first {@link STARTUP_GRACE_MS}
|
|
56
|
+
* after `lt dev up` — see the {@link ComponentHealth} doc block for the full state
|
|
57
|
+
* model and the false-positive it prevents.
|
|
45
58
|
*/
|
|
46
59
|
function classifyComponentHealth(opts) {
|
|
60
|
+
var _a;
|
|
47
61
|
const wrapperAlive = typeof opts.pid === 'number' && isPidAlive(opts.pid);
|
|
48
62
|
if (!wrapperAlive)
|
|
49
63
|
return 'dead';
|
|
50
|
-
|
|
64
|
+
if (opts.portBound)
|
|
65
|
+
return 'running';
|
|
66
|
+
// Wrapper alive but the port is not bound. Within the startup grace window after
|
|
67
|
+
// `lt dev up` this is a still-BOOTING component, not a crash (avoids the
|
|
68
|
+
// false-positive that told users to restart a healthy, still-booting stack).
|
|
69
|
+
const graceMs = (_a = opts.startupGraceMs) !== null && _a !== void 0 ? _a : exports.STARTUP_GRACE_MS;
|
|
70
|
+
if (opts.startedAt) {
|
|
71
|
+
const ageMs = Date.now() - new Date(opts.startedAt).getTime();
|
|
72
|
+
if (Number.isFinite(ageMs) && ageMs >= 0 && ageMs < graceMs)
|
|
73
|
+
return 'starting';
|
|
74
|
+
}
|
|
75
|
+
return 'crashed';
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Split present components into those still booting (`starting`) vs. genuinely
|
|
79
|
+
* down (present, not running, not starting). A `starting` component is booting,
|
|
80
|
+
* not down — it must NEVER appear in the "restart the down half" hint, otherwise
|
|
81
|
+
* the user is told to restart a healthy, still-booting stack.
|
|
82
|
+
*/
|
|
83
|
+
function partitionComponentStates(components) {
|
|
84
|
+
const starting = components.filter((c) => c.present && c.health === 'starting').map((c) => c.name);
|
|
85
|
+
const down = components
|
|
86
|
+
.filter((c) => c.present && c.health !== 'running' && c.health !== 'starting')
|
|
87
|
+
.map((c) => c.name);
|
|
88
|
+
return { down, starting };
|
|
89
|
+
}
|
|
90
|
+
/** See {@link StackHealth} for the precedence rules this implements. */
|
|
91
|
+
function summarizeStackHealth(components) {
|
|
92
|
+
if (components.length === 0)
|
|
93
|
+
return 'stopped';
|
|
94
|
+
if (components.every((h) => h === 'running'))
|
|
95
|
+
return 'running';
|
|
96
|
+
if (components.some((h) => h === 'running'))
|
|
97
|
+
return 'degraded';
|
|
98
|
+
if (components.some((h) => h === 'starting'))
|
|
99
|
+
return 'starting';
|
|
100
|
+
if (components.some((h) => h === 'crashed'))
|
|
101
|
+
return 'crashed';
|
|
102
|
+
return 'stopped';
|
|
51
103
|
}
|
|
52
104
|
const REGISTRY_PATH = process.env.LT_DEV_REGISTRY_PATH || (0, path_1.join)((0, os_1.homedir)(), '.lenneTech', 'projects.json');
|
|
53
105
|
const SESSION_DIR = '.lt-dev';
|
package/docs/commands.md
CHANGED
|
@@ -467,8 +467,11 @@ a still-serving component from a *crashed* one (the supervisor / nodemon survive
|
|
|
467
467
|
ts-node crash and the recorded PID stays alive while nothing listens on the port).
|
|
468
468
|
Behaviour:
|
|
469
469
|
- **All present components truly serving** → no-op, exits 0 with "already running".
|
|
470
|
-
- **
|
|
471
|
-
|
|
470
|
+
- **Still booting** (PID alive, port not bound yet, within the 60 s startup grace
|
|
471
|
+
window) → KEPT, not restarted. Re-running `lt dev up` while the API is still
|
|
472
|
+
booting must not kill and restart the still-booting component.
|
|
473
|
+
- **Some serving, some down** → restarts ONLY the down (`crashed`/`dead`)
|
|
474
|
+
component(s); a running or still-booting one keeps its PID untouched.
|
|
472
475
|
- Before respawning a crashed component it terminates that supervisor's whole
|
|
473
476
|
process group (so its idle `nodemon` doesn't leak / stack a second one) and
|
|
474
477
|
reclaims any orphaned listener still squatting the reused port.
|
|
@@ -534,12 +537,17 @@ lt dev status --all # every project in the registry
|
|
|
534
537
|
The current-project view shows subdomains → upstream ports, db URI, per-component
|
|
535
538
|
health, and live `lsof` state. **Health is honest:** a component is reported
|
|
536
539
|
`running` only when its supervisor PID is alive AND its internal port is actually
|
|
537
|
-
bound. A
|
|
538
|
-
|
|
539
|
-
|
|
540
|
+
bound. A component that is PID-alive but not yet bound is `starting (booting — port
|
|
541
|
+
not bound yet)` during the first 60 s after `lt dev up` (the slow API boot: swc
|
|
542
|
+
compile + Mongo + Better Auth + migrations) — booting, not down, so no restart is
|
|
543
|
+
suggested. Once that grace window elapses with the port still free, a supervisor
|
|
544
|
+
that survived a ts-node crash (PID alive, port free) is shown as `crashed
|
|
545
|
+
(supervisor up, port not listening)` instead of the old misleading `running`, with
|
|
546
|
+
a hint to run `lt dev up` to restart just that one.
|
|
540
547
|
|
|
541
548
|
The `--all` view lists every project with a single indicator:
|
|
542
549
|
- `●` (green) — all present components serving
|
|
550
|
+
- `◐` (cyan) — `starting` (booting within the startup grace window — give it a moment)
|
|
543
551
|
- `◐` (yellow) — `degraded` (some up, some down) or `crashed` (supervisor up, port free)
|
|
544
552
|
- `○` (dim) — stopped
|
|
545
553
|
|