@lenne.tech/cli 1.39.0 → 1.40.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.
@@ -9,9 +9,11 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  });
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.help = void 0;
12
13
  const fs_1 = require("fs");
13
14
  const path_1 = require("path");
14
15
  const caddy_1 = require("../../lib/caddy");
16
+ const dev_api_launch_1 = require("../../lib/dev-api-launch");
15
17
  const dev_env_1 = require("../../lib/dev-env");
16
18
  const dev_env_bridge_1 = require("../../lib/dev-env-bridge");
17
19
  const dev_identity_1 = require("../../lib/dev-identity");
@@ -112,6 +114,10 @@ const UpCommand = {
112
114
  // suffixed so the stack is fully isolated from the base dev session and every
113
115
  // other ticket. Without a ticket this is the plain project identity.
114
116
  const { dbName, identity, ticket } = (0, dev_ticket_1.resolveDevIdentity)(layout, { ticket: parameters.options.ticket });
117
+ // `--api-compiled`: run the API compiled (`node dist`) instead of ts-node.
118
+ // Trades hot reload for stability — ts-node intermittently dies under browser
119
+ // load without a stacktrace (DEV-2525); `lt dev test` already runs compiled.
120
+ const apiCompiled = (0, dev_api_launch_1.isApiCompiledRequested)(parameters.options);
115
121
  // Guard against two checkouts of the SAME project (same package.json "name"
116
122
  // → same slug → shared URLs / ports / DB / Caddy block). If another checkout
117
123
  // is already RUNNING under this slug, abort with a clear message — otherwise
@@ -321,7 +327,7 @@ const UpCommand = {
321
327
  if (identity.subdomains.app)
322
328
  info(` app: https://${identity.subdomains.app.hostname} → 127.0.0.1:${appPort}`);
323
329
  if (identity.subdomains.api)
324
- info(` api: https://${identity.subdomains.api.hostname} → 127.0.0.1:${apiPort}`);
330
+ info(` api: https://${identity.subdomains.api.hostname} → 127.0.0.1:${apiPort}${apiCompiled ? colors.dim(' (compiled, no hot reload)') : ''}`);
325
331
  if (identity.subdomains.api)
326
332
  info(` db: mongodb://127.0.0.1/${dbName}`);
327
333
  info('');
@@ -353,6 +359,11 @@ const UpCommand = {
353
359
  if (apiHealth === 'running') {
354
360
  pids.api = existingSession === null || existingSession === void 0 ? void 0 : existingSession.pids.api;
355
361
  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.
365
+ if (apiCompiled)
366
+ info(colors.dim(' --api-compiled: API already running — `lt dev down` first to switch it.'));
356
367
  }
357
368
  else {
358
369
  yield reclaimPort(existingSession === null || existingSession === void 0 ? void 0 : existingSession.pids.api, apiPort, apiHealth !== null && apiHealth !== void 0 ? apiHealth : 'dead');
@@ -361,11 +372,20 @@ const UpCommand = {
361
372
  // regenerate a foreign lockfile + crash on un-approved build
362
373
  // scripts when run against an npm-only project.
363
374
  const apiPm = (0, dev_package_manager_1.pickPackageManager)(layout.apiDir);
364
- const apiResult = (0, dev_process_1.spawnDetached)(apiPm.bin, apiPm.runScript('start'), {
365
- cwd: layout.apiDir,
366
- env: devEnv.api.env,
367
- logFile: (0, path_1.join)(layout.root, '.lt-dev', 'api.log'),
368
- });
375
+ const apiLogFile = (0, path_1.join)(layout.root, '.lt-dev', 'api.log');
376
+ const apiResult = apiCompiled
377
+ ? yield (0, dev_api_launch_1.startCompiledApi)({
378
+ apiDir: layout.apiDir,
379
+ env: devEnv.api.env,
380
+ log: { info, warn: warning },
381
+ logFile: apiLogFile,
382
+ pm: apiPm,
383
+ })
384
+ : (0, dev_process_1.spawnDetached)(apiPm.bin, apiPm.runScript('start'), {
385
+ cwd: layout.apiDir,
386
+ env: devEnv.api.env,
387
+ logFile: apiLogFile,
388
+ });
369
389
  if (apiResult) {
370
390
  pids.api = apiResult.pid;
371
391
  started.push('api');
@@ -502,4 +522,28 @@ const UpCommand = {
502
522
  return `dev up: api=${pids.api}, app=${pids.app}`;
503
523
  }),
504
524
  };
505
- module.exports = UpCommand;
525
+ exports.help = {
526
+ aliases: ['u'],
527
+ configuration: 'Ephemeral dev-orchestration flags — no lt.config counterpart. Override the spawn binary for both processes via the LT_PNPM_BIN env var.',
528
+ description: 'Start the API + App behind Caddy under stable https://<slug>.localhost URLs. Health-aware and idempotent: re-running (re)starts only the component(s) that are not truly serving.',
529
+ examples: ['dev up', 'dev up --api-compiled', 'dev up --ticket DEV-1234'],
530
+ features: [
531
+ 'Registers a Caddy block and allocates opaque internal ports (4000+).',
532
+ 'Spawns API + App detached; persists PIDs to <root>/.lt-dev/state.json.',
533
+ 'Self-heals legacy hardcoded ports and restarts only the down component(s).',
534
+ ],
535
+ name: 'up',
536
+ options: [
537
+ {
538
+ description: 'Run the API compiled (node dist/src/main.js) instead of ts-node — trades hot reload for stability under browser load (DEV-2525). Builds + migrates first; falls back to the ts-node start if the build fails. Only applies when the API (re)starts.',
539
+ flag: '--api-compiled',
540
+ type: 'boolean',
541
+ },
542
+ {
543
+ description: 'Suffix the slug / URLs / DB for an isolated ticket stack (also auto-detected from a .lt-dev/ticket marker).',
544
+ flag: '--ticket',
545
+ type: 'string',
546
+ },
547
+ ],
548
+ };
549
+ module.exports = Object.assign(UpCommand, { help: exports.help });
@@ -0,0 +1,89 @@
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.findCompiledEntry = findCompiledEntry;
13
+ exports.isApiCompiledRequested = isApiCompiledRequested;
14
+ exports.startCompiledApi = startCompiledApi;
15
+ /**
16
+ * Launch strategy for the API process under `lt dev up`.
17
+ *
18
+ * By default `lt dev up` runs the API via ts-node (`<pm> run start` → nodemon →
19
+ * ts-node src/main.ts) for hot reload. Under a browser driving the app, that
20
+ * ts-node process intermittently dies WITHOUT a stacktrace (dev-SSR load plus
21
+ * ts-node's heavier footprint) — see DEV-2525. `lt dev test` already sidesteps
22
+ * this by running the API COMPILED (`node dist/src/main.js`); this module brings
23
+ * the same option to `lt dev up`, opt-in via `--api-compiled`. The trade-off is
24
+ * NO hot reload, so it stays opt-in — the caller decides stability vs. reload.
25
+ */
26
+ const node_fs_1 = require("node:fs");
27
+ const node_path_1 = require("node:path");
28
+ const dev_process_1 = require("./dev-process");
29
+ /** Candidate compiled entry points, in preference order. Single-sourced so `lt dev test` agrees. */
30
+ const COMPILED_ENTRIES = ['dist/src/main.js', 'dist/main.js'];
31
+ /** Resolve the compiled API entry point in `apiDir`, or `undefined` if none was built. */
32
+ function findCompiledEntry(apiDir) {
33
+ return COMPILED_ENTRIES.map((rel) => (0, node_path_1.join)(apiDir, rel)).find((candidate) => (0, node_fs_1.existsSync)(candidate));
34
+ }
35
+ /**
36
+ * True when the caller opted into the compiled API via `--api-compiled`.
37
+ *
38
+ * gluegun parses argv with yargs-parser and declares no booleans, so the flag
39
+ * arrives in several shapes: a value-less `--api-compiled` → boolean `true`, but
40
+ * `--api-compiled=true` → the STRING `'true'` and `--api-compiled=1` → the NUMBER
41
+ * `1`. A bare `=== true` check silently ignores the latter two and drops the very
42
+ * stability fix the user asked for. This is an ENABLE flag, so a mis-parse fails
43
+ * SAFE (default ts-node) — but the repo convention is to honour `true`/`'true'`
44
+ * too (see `dev-ticket.ts#keepDbFlag` for the destructive-flag counterpart).
45
+ */
46
+ function isApiCompiledRequested(options = {}) {
47
+ const affirmative = (value) => value === true || ['1', 'true', 'yes'].includes(String(value).toLowerCase());
48
+ return affirmative(options.apiCompiled) || affirmative(options['api-compiled']);
49
+ }
50
+ /**
51
+ * Build the API and start it compiled (`node dist/src/main.js`). Applies pending
52
+ * migrations first for parity with the ts-node path it replaces (`<pm> run start`
53
+ * = `migrate:up && start:local`). Falls back to the ts-node start when the build
54
+ * fails or produces no dist entry, so this never leaves the developer with a dead
55
+ * API. Returns the detached spawn result (`undefined` when nothing was started).
56
+ */
57
+ function startCompiledApi(options) {
58
+ return __awaiter(this, void 0, void 0, function* () {
59
+ const { apiDir, env, log, logFile, pm } = options;
60
+ log.info('Building API (compiled, for stability — no hot reload) …');
61
+ const build = yield (0, dev_process_1.runChildInherit)(pm.bin, pm.runScript('build'), { cwd: apiDir, env });
62
+ const entry = findCompiledEntry(apiDir);
63
+ if (build === 0 && entry) {
64
+ if (hasScript(apiDir, 'migrate:up')) {
65
+ const migrate = yield (0, dev_process_1.runChildInherit)(pm.bin, pm.runScript('migrate:up'), { cwd: apiDir, env });
66
+ if (migrate !== 0) {
67
+ // Parity with `migrate:up && start:local`: a failed migration must PREVENT the server
68
+ // from starting rather than boot it against a half-migrated DB behind a "Started" banner.
69
+ log.warn(`migrate:up failed (exit ${String(migrate)}) — API NOT started (would run on an un-migrated DB).`);
70
+ return undefined;
71
+ }
72
+ }
73
+ return (0, dev_process_1.spawnDetached)('node', [entry], { cwd: apiDir, env: Object.assign(Object.assign({}, env), { NODE_ENV: 'local' }), logFile });
74
+ }
75
+ log.warn(`compiled API unavailable (build exit ${String(build)}) — falling back to \`${pm.bin} start\` (ts-node).`);
76
+ return (0, dev_process_1.spawnDetached)(pm.bin, pm.runScript('start'), { cwd: apiDir, env, logFile });
77
+ });
78
+ }
79
+ /** True when `package.json` in `apiDir` defines a script named `name`. */
80
+ function hasScript(apiDir, name) {
81
+ var _a;
82
+ try {
83
+ const pkg = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(apiDir, 'package.json'), 'utf8'));
84
+ return typeof ((_a = pkg.scripts) === null || _a === void 0 ? void 0 : _a[name]) === 'string';
85
+ }
86
+ catch (_b) {
87
+ return false;
88
+ }
89
+ }
@@ -47,6 +47,7 @@ const fs_1 = require("fs");
47
47
  const os_1 = require("os");
48
48
  const path_1 = require("path");
49
49
  const caddy_1 = require("./caddy");
50
+ const dev_api_launch_1 = require("./dev-api-launch");
50
51
  const dev_env_1 = require("./dev-env");
51
52
  const dev_env_bridge_1 = require("./dev-env-bridge");
52
53
  const dev_identity_1 = require("./dev-identity");
@@ -245,9 +246,7 @@ function bringUpTestSession(layout_1, baseIdentity_1, log_1) {
245
246
  log.info(log.dim('Building API (compiled, for stable long runs) …'));
246
247
  build = yield (0, dev_process_1.runChildInherit)(apiPm.bin, apiPm.runScript('build'), { cwd: layout.apiDir, env: process.env });
247
248
  }
248
- const entry = ['dist/src/main.js', 'dist/main.js']
249
- .map((rel) => (0, path_1.join)(layout.apiDir, rel))
250
- .find((p) => (0, fs_1.existsSync)(p));
249
+ const entry = (0, dev_api_launch_1.findCompiledEntry)(layout.apiDir);
251
250
  // Seed a throwaway initial admin into the fresh, isolated test DB so the
252
251
  // standard auth E2E specs run against a set-up system — locally exactly like
253
252
  // the lt-monorepo template CI. Defaults first so an explicitly inherited
package/docs/commands.md CHANGED
@@ -433,6 +433,13 @@ lt dev up
433
433
 
434
434
  **Alias:** `lt d u`
435
435
 
436
+ **Flags:**
437
+ - `--api-compiled` — run the API **compiled** (`node dist/src/main.js`) instead of ts-node. Trades hot reload for stability: under sustained browser / dev-SSR load the ts-node API process intermittently dies without a stacktrace (DEV-2525); the compiled `node` process does not. Opt-in — omit it for the default ts-node hot-reload start. Details:
438
+ - Builds the API first, then applies pending migrations (`migrate:up`) for parity with the default `migrate:up && start:local`; a failed migration aborts the start rather than booting against a half-migrated DB.
439
+ - Auto-falls-back to the ts-node `start` if the build fails or produces no `dist` entry, so you never end up with a dead API.
440
+ - Only takes effect when the API actually (re)starts. A healthy running API is kept as-is — run `lt dev down` first to switch a live ts-node API to compiled.
441
+ - Accepted spellings: `--api-compiled` or `--api-compiled=true`.
442
+
436
443
  **Environment variables injected:**
437
444
  | Variable | Consumer | Example value |
438
445
  |----------|----------|---------------|
@@ -467,8 +474,10 @@ Behaviour:
467
474
  reclaims any orphaned listener still squatting the reused port.
468
475
 
469
476
  This is the fix for the "`status` says api running but no data loads" case: a
470
- crashed ts-node dev API is healed by simply re-running `lt dev up` (it does not
471
- fall back to compiled `node dist` — ts-node is kept so code edits hot-reload).
477
+ crashed ts-node dev API is healed by simply re-running `lt dev up`. The **automatic**
478
+ heal keeps ts-node (it does not silently switch to compiled `node dist`, so code
479
+ edits still hot-reload); pass the explicit `--api-compiled` flag (see **Flags** above)
480
+ when you deliberately want to trade hot reload for compiled stability.
472
481
 
473
482
  **Logs:** `<root>/.lt-dev/api.log`, `<root>/.lt-dev/app.log` (append-mode).
474
483
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lenne.tech/cli",
3
- "version": "1.39.0",
3
+ "version": "1.40.0",
4
4
  "description": "lenne.Tech CLI: lt",
5
5
  "keywords": [
6
6
  "lenne.Tech",