@lenne.tech/cli 1.40.0 → 1.41.1

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.
@@ -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)({ pid: session === null || session === void 0 ? void 0 : session.pids.api, portBound: snap.has(e.internalPorts.api) }));
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)({ pid: session === null || session === void 0 ? void 0 : session.pids.app, portBound: snap.has(e.internalPorts.app) }));
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
- const allRunning = comps.length > 0 && comps.every((h) => h === 'running');
67
- const anyRunning = comps.some((h) => h === 'running');
68
- const anyCrashed = comps.some((h) => h === 'crashed');
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 (allRunning) {
79
+ if (stack === 'running') {
72
80
  status = colors.green('●');
73
81
  }
74
- else if (anyRunning) {
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 (anyCrashed) {
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 === 'crashed'
188
- ? colors.yellow('crashed (supervisor up, port not listening)')
189
- : colors.red('dead');
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
- const down = [
212
- apiPresent && apiHealth !== 'running' ? 'api' : null,
213
- appPresent && appHealth !== 'running' ? 'app' : null,
214
- ].filter((c) => c !== null);
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('');
@@ -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 healthy one keeps running untouched.
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)({ pid: existingSession === null || existingSession === void 0 ? void 0 : existingSession.pids.api, portBound: !!apiPort && healthSnap.has(apiPort) })
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)({ pid: existingSession === null || existingSession === void 0 ? void 0 : existingSession.pids.app, portBound: !!appPort && healthSnap.has(appPort) })
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 healthy and at least one is
293
- // down. Announce what we keep vs. restart so the user sees the honest state.
294
- const partialRestart = presentHealth.some((h) => h === 'running');
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
- if (apiHealth === 'running')
297
- info(colors.dim(`api healthy (pid ${existingSession === null || existingSession === void 0 ? void 0 : existingSession.pids.api}) → keeping`));
298
- else if (apiHealth)
299
- warning(`api ${apiHealth} (port ${apiPort} not serving) → restarting`);
300
- if (appHealth === 'running')
301
- info(colors.dim(`app healthy (pid ${existingSession === null || existingSession === void 0 ? void 0 : existingSession.pids.app}) → keeping`));
302
- else if (appHealth)
303
- warning(`app ${appHealth} (port ${appPort} not serving) → restarting`);
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 === 'running') {
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 healthy API is
363
- // kept as-is (force-restarting it would contradict the keep logic), so tell the
364
- // user how to switch a currently-running ts-node API to compiled.
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 === 'running') {
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 (healthy) + freshly started PIDs.
421
- // The registry entry (ports) was already reserved atomically above. On a
422
- // partial restart we preserve the original session start time.
423
- const startedAt = existingSession && kept.length > 0 ? existingSession.startedAt : new Date().toISOString();
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
- ? 'Nothing restarted'
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=${(_h = pids.api) !== null && _h !== void 0 ? _h : '-'}, app pid=${(_j = pids.app) !== null && _j !== void 0 ? _j : '-'}`);
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: (_k = identity.subdomains.api) === null || _k === void 0 ? void 0 : _k.hostname,
470
+ apiHostname: (_l = identity.subdomains.api) === null || _l === void 0 ? void 0 : _l.hostname,
440
471
  apiUpstreamPort: apiPort,
441
- appHostname: (_l = identity.subdomains.app) === null || _l === void 0 ? void 0 : _l.hostname,
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 (_m) {
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 (_o) {
547
+ catch (_p) {
517
548
  /* never block `up` on cleanup */
518
549
  }
519
550
  }
@@ -12,6 +12,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.FrontendHelper = void 0;
13
13
  const check_freshness_hooks_1 = require("../lib/check-freshness-hooks");
14
14
  const markdown_table_1 = require("../lib/markdown-table");
15
+ const strip_comments_1 = require("../lib/strip-comments");
15
16
  const vendor_claude_md_1 = require("../lib/vendor-claude-md");
16
17
  /**
17
18
  * Frontend helper functions for project scaffolding
@@ -813,7 +814,11 @@ class FrontendHelper {
813
814
  for (const absFile of allFiles) {
814
815
  if (skipPathContaining && absFile.includes(skipPathContaining))
815
816
  continue;
816
- const content = filesystem.read(absFile) || '';
817
+ // Strip comments first a docblock that DOCUMENTS the conversion legitimately quotes the
818
+ // very import syntax this looks for, and would otherwise be reported as a file the user has
819
+ // to fix by hand. Same false-positive class that hit the backend detector on
820
+ // nest-server-starter's bootstrap-diagnostics.spec.ts.
821
+ const content = (0, strip_comments_1.stripComments)(filesystem.read(absFile) || '');
817
822
  const matches = typeof needle === 'string' ? content.includes(needle) : needle.test(content);
818
823
  if (matches) {
819
824
  stale.push(absFile.replace(`${appDir}/`, ''));
@@ -48,6 +48,7 @@ const path_1 = require("path");
48
48
  const ts = __importStar(require("typescript"));
49
49
  const check_freshness_hooks_1 = require("../lib/check-freshness-hooks");
50
50
  const markdown_table_1 = require("../lib/markdown-table");
51
+ const strip_comments_1 = require("../lib/strip-comments");
51
52
  const vendor_claude_md_1 = require("../lib/vendor-claude-md");
52
53
  /**
53
54
  * Server helper functions
@@ -2494,7 +2495,14 @@ class Server {
2494
2495
  recursive: true,
2495
2496
  }) || [];
2496
2497
  for (const file of files) {
2497
- const content = this.filesystem.read(file) || '';
2498
+ const raw = this.filesystem.read(file) || '';
2499
+ // Strip comments before matching. The keyword-anchored pattern is not enough on its own:
2500
+ // a docblock that DOCUMENTS the conversion legitimately quotes the very syntax it looks
2501
+ // for — nest-server-starter's `tests/unit/bootstrap-diagnostics.spec.ts` contains
2502
+ // "rewrites `from '@lenne.tech/nest-server'` to a relative `./core` path", which matched
2503
+ // and told the user to rewrite imports that file does not have. A detector that reads
2504
+ // comments as code produces false alarms on exactly the files that explain it best.
2505
+ const content = (0, strip_comments_1.stripComments)(raw);
2498
2506
  if (pattern ? pattern.test(content) : content.includes(needle)) {
2499
2507
  stale.push(file.replace(`${dest}/`, ''));
2500
2508
  }
@@ -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
- child = (0, child_process_1.spawn)(cmd, args, {
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,
@@ -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
- return opts.portBound ? 'running' : 'crashed';
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';
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.stripComments = stripComments;
37
+ const ts = __importStar(require("typescript"));
38
+ /**
39
+ * Removes comments from TypeScript/JavaScript source, preserving everything else verbatim.
40
+ *
41
+ * Exists because detectors that search source for import specifiers must not read comments as
42
+ * code. A keyword-anchored regex is not sufficient on its own: a docblock that DOCUMENTS an import
43
+ * rewrite legitimately quotes the exact syntax the detector looks for. nest-server-starter's
44
+ * `tests/unit/bootstrap-diagnostics.spec.ts` contains
45
+ *
46
+ * * CLI's vendor conversion rewrites `from '@lenne.tech/nest-server'` to a relative `./core` path
47
+ *
48
+ * which matched `/(?:from|import|…)\s*['"]@lenne\.tech\/nest-server['"]/` and made
49
+ * `lt fullstack init --framework-mode vendor` warn about imports that file does not have.
50
+ *
51
+ * Uses the TypeScript scanner rather than a regex, so string literals, template literals and
52
+ * regex literals containing `//` or `/*` are handled correctly by construction — a hand-rolled
53
+ * stripper trips over `'https://…'` and over `/* ` inside a string.
54
+ *
55
+ * Comment characters are replaced with spaces instead of being deleted, so byte offsets and line
56
+ * numbers of the surrounding code stay unchanged — a caller can still report a meaningful
57
+ * position from a match.
58
+ *
59
+ * @param source - TypeScript or JavaScript source text
60
+ * @returns The source with every comment blanked out
61
+ *
62
+ * @example
63
+ * stripComments("// from 'pkg'\nimport x from 'pkg';")
64
+ * // => " \nimport x from 'pkg';"
65
+ */
66
+ function stripComments(source) {
67
+ const scanner = ts.createScanner(ts.ScriptTarget.Latest, /* skipTrivia */ false, ts.LanguageVariant.Standard, source);
68
+ let result = '';
69
+ let token = scanner.scan();
70
+ while (token !== ts.SyntaxKind.EndOfFileToken) {
71
+ const text = scanner.getTokenText();
72
+ const isComment = token === ts.SyntaxKind.SingleLineCommentTrivia || token === ts.SyntaxKind.MultiLineCommentTrivia;
73
+ // Keep newlines so line numbers survive; blank everything else in the comment.
74
+ result += isComment ? text.replace(/[^\n]/g, ' ') : text;
75
+ token = scanner.scan();
76
+ }
77
+ return result;
78
+ }
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
- - **Some serving, some down** restarts ONLY the down component(s); a healthy one
471
- keeps running untouched and its PID is preserved in the session.
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 supervisor that survived a ts-node crash (PID alive, port free) is shown
538
- as `crashed (supervisor up, port not listening)` instead of the old misleading
539
- `running`, with a hint to run `lt dev up` to restart just that one.
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lenne.tech/cli",
3
- "version": "1.40.0",
3
+ "version": "1.41.1",
4
4
  "description": "lenne.Tech CLI: lt",
5
5
  "keywords": [
6
6
  "lenne.Tech",
@@ -100,10 +100,16 @@
100
100
  "ts-jest": "29.4.11"
101
101
  },
102
102
  "//overrides": {
103
- "semver@*": "Force latest semver 7.x across all sub-deps; gluegun@5.2.2 pins semver@7.7.0 which is stale - remove once gluegun updates its dep."
103
+ "semver@*": "Force latest semver 7.x across all sub-deps; gluegun@5.2.2 pins semver@7.7.0 which is stale - remove once gluegun updates its dep.",
104
+ "brace-expansion@<1.1.16": "DoS via exponential-time expansion of consecutive non-expanding {} groups (GHSA-3jxr-9vmj-r5cp, high). Transitive via dotgitignore/eslint/fs-jetpack/glob/test-exclude > minimatch. One bounded key per affected major so each can only raise a vulnerable version, never cap a patched one - remove once minimatch requests the patched ranges.",
105
+ "brace-expansion@>=2.0.0 <2.1.2": "Same advisory, 2.x line.",
106
+ "brace-expansion@>=5.0.0 <5.0.7": "Same advisory, 5.x line. Floored at >=5.0.0 so a future 3.x/4.x dependency is not silently forced across two majors."
104
107
  },
105
108
  "overrides": {
106
- "semver@*": "7.8.5"
109
+ "semver@*": "7.8.5",
110
+ "brace-expansion@<1.1.16": "1.1.16",
111
+ "brace-expansion@>=2.0.0 <2.1.2": "2.1.2",
112
+ "brace-expansion@>=5.0.0 <5.0.7": "5.0.7"
107
113
  },
108
114
  "jest": {
109
115
  "testEnvironment": "node",