@lenne.tech/cli 1.48.0 → 1.50.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.
Files changed (40) hide show
  1. package/build/commands/dev/doctor.js +27 -10
  2. package/build/commands/dev/down.js +48 -5
  3. package/build/commands/dev/status.js +32 -8
  4. package/build/commands/dev/test.js +5 -3
  5. package/build/commands/dev/up.js +59 -21
  6. package/build/commands/fullstack/init.js +12 -3
  7. package/build/commands/git/squash.js +4 -1
  8. package/build/extensions/api-mode.js +69 -0
  9. package/build/extensions/frontend-helper.js +11 -12
  10. package/build/extensions/server.js +43 -33
  11. package/build/lib/caddy.js +17 -10
  12. package/build/lib/check-template-sync.js +62 -2
  13. package/build/lib/config-env-graphql.js +106 -0
  14. package/build/lib/cross-env.js +94 -0
  15. package/build/lib/dev-api-launch.js +34 -8
  16. package/build/lib/dev-env-bridge.js +7 -0
  17. package/build/lib/dev-env.js +44 -4
  18. package/build/lib/dev-log-tail.js +91 -0
  19. package/build/lib/dev-package-manager.js +13 -9
  20. package/build/lib/dev-patches.js +15 -5
  21. package/build/lib/dev-process.js +362 -127
  22. package/build/lib/dev-project.js +14 -1
  23. package/build/lib/dev-prune.js +15 -5
  24. package/build/lib/dev-service.js +29 -15
  25. package/build/lib/dev-state.js +40 -4
  26. package/build/lib/dev-test-session.js +141 -73
  27. package/build/lib/dev-ticket.js +13 -1
  28. package/build/lib/fail-run.js +11 -0
  29. package/build/lib/heal-vendor-migrate-store.js +4 -3
  30. package/build/lib/hoist-workspace-pnpm-config.js +17 -6
  31. package/build/lib/migrate-scripts.js +69 -0
  32. package/build/lib/module-specifiers.js +144 -0
  33. package/build/lib/platform.js +53 -1
  34. package/build/lib/spinner-lifetime.js +245 -0
  35. package/build/templates/check/check.mjs +59 -9
  36. package/build/templates/check/lt-monorepo-pin.json +4 -4
  37. package/build/templates/nest-server-starter/README.md.ejs +13 -6
  38. package/docs/commands.md +16 -12
  39. package/package.json +4 -1
  40. package/build/lib/strip-comments.js +0 -78
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.buildDevEnv = buildDevEnv;
4
+ exports.internalUrl = internalUrl;
4
5
  /**
5
6
  * Build environment variables for `lt dev up`.
6
7
  *
@@ -41,13 +42,16 @@ function buildDevEnv(input) {
41
42
  const appSub = identity.subdomains.app;
42
43
  const apiUrl = apiSub ? `https://${apiSub.hostname}` : '';
43
44
  const appUrl = appSub ? `https://${appSub.hostname}` : '';
45
+ // Same two services, addressed the way Node can reach them. See `internalUrl`.
46
+ const apiInternal = internalUrl(apiInternalPort);
47
+ const appInternal = internalUrl(appInternalPort);
44
48
  const caPath = (0, dev_env_bridge_1.detectCaddyRootCa)();
45
- const sharedKeys = Object.assign(Object.assign(Object.assign(Object.assign({
49
+ const sharedKeys = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({
46
50
  // Marks the API + App processes as running under `lt dev`. Consumed by the
47
51
  // backend to relax dev-only behaviour (rate limiting, Better-Auth
48
52
  // user-cache) so E2E suites run without a separate VITEST/PLAYWRIGHT flag.
49
53
  // (Also written to the .lt-dev/.env bridge for external test runners.)
50
- LT_DEV_ACTIVE: 'true' }, (apiUrl ? { BASE_URL: apiUrl, NSC__BASE_URL: apiUrl } : {})), (appUrl ? { APP_URL: appUrl, NSC__APP_URL: appUrl } : {})), (dbName ? { DATABASE_URL: buildPostgresUrl(dbName), NSC__MONGOOSE__URI: `mongodb://127.0.0.1/${dbName}` } : {})), (caPath ? { NODE_EXTRA_CA_CERTS: caPath } : {}));
54
+ LT_DEV_ACTIVE: 'true' }, (apiInternal ? { LT_DEV_API_INTERNAL_URL: apiInternal } : {})), (appInternal ? { LT_DEV_APP_INTERNAL_URL: appInternal } : {})), (apiUrl ? { BASE_URL: apiUrl, NSC__BASE_URL: apiUrl } : {})), (appUrl ? { APP_URL: appUrl, NSC__APP_URL: appUrl } : {})), (dbName ? { DATABASE_URL: buildPostgresUrl(dbName), NSC__MONGOOSE__URI: `mongodb://127.0.0.1/${dbName}` } : {})), (caPath ? { NODE_EXTRA_CA_CERTS: caPath } : {}));
51
55
  return {
52
56
  api: {
53
57
  env: Object.assign(Object.assign(Object.assign({}, baseEnv), sharedKeys), {
@@ -59,10 +63,10 @@ function buildDevEnv(input) {
59
63
  internalPort: apiInternalPort,
60
64
  },
61
65
  app: {
62
- env: Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, baseEnv), sharedKeys), {
66
+ env: Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, baseEnv), sharedKeys), {
63
67
  // See API note above: pin the dev server to IPv4 so Caddy's
64
68
  // `127.0.0.1` upstream is unambiguous.
65
- HOST: '127.0.0.1', NITRO_HOST: '127.0.0.1' }), (apiUrl ? { API_URL: apiUrl, NUXT_API_URL: apiUrl, NUXT_PUBLIC_API_URL: apiUrl } : {})), (appUrl ? { NUXT_PUBLIC_SITE_URL: appUrl, SITE_URL: appUrl } : {})), {
69
+ HOST: '127.0.0.1', NITRO_HOST: '127.0.0.1' }), (apiUrl ? { API_URL: apiUrl, NUXT_PUBLIC_API_URL: apiUrl } : {})), (apiInternal || apiUrl ? { NUXT_API_URL: apiInternal || apiUrl } : {})), (appUrl ? { NUXT_PUBLIC_SITE_URL: appUrl, SITE_URL: appUrl } : {})), {
66
70
  // Vite-API-Proxy is OFF by default in lt dev mode — Caddy serves
67
71
  // both subdomains under HTTPS with shared cookie domain, so
68
72
  // same-origin trickery is no longer required.
@@ -75,6 +79,42 @@ function buildDevEnv(input) {
75
79
  },
76
80
  };
77
81
  }
82
+ /**
83
+ * The loopback address of a component, for anything that RESOLVES a name.
84
+ *
85
+ * `lt dev` is URL-first: a project is reached at `https://<slug>.localhost`, and
86
+ * Caddy proxies that to an opaque internal port. That promise holds for
87
+ * **browsers**. It does not hold for Node: on Windows `*.localhost` subdomains do
88
+ * not resolve at all (`dns.lookup('api.demo.localhost')` → ENOTFOUND, and `curl`
89
+ * agrees), while Chromium resolves them internally without asking a resolver.
90
+ *
91
+ * So the question at every call site is not "which URL" but **"who reads it"**:
92
+ *
93
+ * | | example | address to use |
94
+ * |---|---|---|
95
+ * | a browser resolves it | Playwright `baseURL`, a printed link | the public name |
96
+ * | Node resolves it | readiness probes, SSR fetches, a proxy target | **this** |
97
+ * | another binary resolves it | `cloudflared` | **this** |
98
+ * | nobody resolves it — it is COMPARED | `APP_URL` (CORS / `trustedOrigins`), the Caddy vhost matcher, the tunnel `Host:` header | the public name, untouched |
99
+ *
100
+ * That last row is why "rewrite every internal URL to 127.0.0.1" would be wrong:
101
+ * `APP_URL` is matched as a string against the `Origin` header a browser sends,
102
+ * and the browser arrives from `https://<slug>.localhost`. Rewriting it would
103
+ * break every login — on the platform where only a health probe was broken before.
104
+ *
105
+ * **Loopback on every platform, not only on Windows.** 127.0.0.1 works everywhere,
106
+ * so there is no branch to get wrong, and the path Windows depends on is the one
107
+ * macOS exercises daily. A branch only the other platform runs is an unchecked
108
+ * branch.
109
+ *
110
+ * One consequence to keep in mind: this goes PAST Caddy. For "is the component
111
+ * alive?" that is an advantage — it measures the component, not the proxy. For
112
+ * "does the routing work?" it is the wrong question, and that one belongs to
113
+ * `lt dev doctor` over the public name.
114
+ */
115
+ function internalUrl(port) {
116
+ return port ? `http://127.0.0.1:${port}` : '';
117
+ }
78
118
  /** Postgres convenience URL — used by Postgres-based projects (e.g. nest-base). */
79
119
  function buildPostgresUrl(dbName) {
80
120
  return `postgresql://${dbName}:${dbName}@localhost:5432/${dbName}`;
@@ -0,0 +1,91 @@
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.describeLog = describeLog;
13
+ exports.diagnoseLog = diagnoseLog;
14
+ exports.findEarlyExits = findEarlyExits;
15
+ exports.isSilentLog = isSilentLog;
16
+ exports.tailLines = tailLines;
17
+ /**
18
+ * What a component's log can tell someone whose component just died.
19
+ *
20
+ * Two silent failures made this necessary, both seen on a Windows laptop:
21
+ * `lt dev up` reported success for an API that was dead seconds later, and its
22
+ * log file existed, was even rotated, but stayed at 0 bytes, so nobody looked in
23
+ * the extra console window that held the actual error. A diagnosis therefore has
24
+ * to tell "the log says X" apart from "the log is empty", and say the latter out
25
+ * loud instead of printing nothing.
26
+ */
27
+ const fs_1 = require("fs");
28
+ /**
29
+ * The lines to print under "<name> is not running". Never an empty list: an
30
+ * empty or missing log is itself the finding.
31
+ */
32
+ function describeLog(d) {
33
+ if (d.state === 'missing')
34
+ return [`No log at ${d.file}.`];
35
+ if (d.state === 'empty') {
36
+ return [`${d.file} is EMPTY — the output was not captured, so the reason is not in the log.`];
37
+ }
38
+ return [`Last lines of ${d.file}:`, ...d.tail.map((l) => ` ${l}`)];
39
+ }
40
+ /** Read `file` and keep its last `lines` non-empty lines. */
41
+ function diagnoseLog(file, lines = 20) {
42
+ if (!(0, fs_1.existsSync)(file))
43
+ return { file, size: -1, state: 'missing', tail: [] };
44
+ const size = (0, fs_1.statSync)(file).size;
45
+ if (size === 0)
46
+ return { file, size, state: 'empty', tail: [] };
47
+ return { file, size, state: 'ok', tail: tailLines((0, fs_1.readFileSync)(file, 'utf8'), lines) };
48
+ }
49
+ /**
50
+ * Components whose pid is gone within `budgetMs` of their start.
51
+ *
52
+ * Catches what dies at once: a missing script, a package manager that cannot
53
+ * start, a crash before any watcher takes over. A crash under nodemon keeps the
54
+ * supervisor alive and is `lt dev status`'s job (`crashed` after the grace window).
55
+ */
56
+ function findEarlyExits(components, opts) {
57
+ return __awaiter(this, void 0, void 0, function* () {
58
+ var _a;
59
+ const sleep = (_a = opts.sleep) !== null && _a !== void 0 ? _a : ((ms) => new Promise((r) => setTimeout(r, ms)));
60
+ const watched = components.filter((c) => typeof c.pid === 'number');
61
+ const dead = new Set();
62
+ const deadline = Date.now() + opts.budgetMs;
63
+ do {
64
+ for (const c of watched)
65
+ if (!dead.has(c.name) && !opts.isAlive(c.pid))
66
+ dead.add(c.name);
67
+ if (dead.size === watched.length)
68
+ break;
69
+ yield sleep(250);
70
+ } while (Date.now() < deadline);
71
+ return watched.filter((c) => dead.has(c.name)).map((c) => c.name);
72
+ });
73
+ }
74
+ /**
75
+ * True when a component has been up for `minAgeMs` and its log is still empty.
76
+ * A booting app writes within seconds (nuxt/nest print a banner), so an empty
77
+ * log after that means output is going somewhere else, not that nothing happened.
78
+ */
79
+ function isSilentLog(d, startedAt, now, minAgeMs = 15000) {
80
+ if (d.state !== 'empty' || !startedAt)
81
+ return false;
82
+ const age = now - new Date(startedAt).getTime();
83
+ return Number.isFinite(age) && age >= minAgeMs;
84
+ }
85
+ /** Last `n` non-empty lines of `content`, oldest first. */
86
+ function tailLines(content, n) {
87
+ return content
88
+ .split(/\r?\n/)
89
+ .filter((l) => l.trim().length > 0)
90
+ .slice(-n);
91
+ }
@@ -74,16 +74,20 @@ function buildCommand(bin, name) {
74
74
  },
75
75
  };
76
76
  }
77
+ /**
78
+ * Manager behind an override like `LT_PM_BIN`.
79
+ *
80
+ * Reads the file NAME, split on both separators and stripped of a Windows launcher
81
+ * extension: there the binary is `C:\\…\\pnpm.cmd`, which neither `endsWith('pnpm')`
82
+ * nor `includes('/pnpm')` matched — the override then resolved to `unknown` and the
83
+ * caller drove the wrong manager.
84
+ */
77
85
  function inferNameFromBin(bin) {
78
- const lower = bin.toLowerCase();
79
- if (lower.endsWith('pnpm') || lower.includes('/pnpm')) {
80
- return 'pnpm';
81
- }
82
- if (lower.endsWith('yarn') || lower.includes('/yarn')) {
83
- return 'yarn';
84
- }
85
- if (lower.endsWith('npm') || lower.includes('/npm')) {
86
- return 'npm';
86
+ var _a;
87
+ const file = (_a = bin.toLowerCase().split(/[\\/]/).pop()) !== null && _a !== void 0 ? _a : '';
88
+ const name = file.replace(/\.(bat|cmd|exe|ps1)$/, '');
89
+ if (name === 'pnpm' || name === 'yarn' || name === 'npm') {
90
+ return name;
87
91
  }
88
92
  return 'unknown';
89
93
  }
@@ -125,7 +125,7 @@ function patchApiConfig(file) {
125
125
  * in place.
126
126
  */
127
127
  function patchClaudeMd(file, options) {
128
- const { dbName, identity } = options;
128
+ const { apiPort, appPort, dbName, identity } = options;
129
129
  const startMarker = '<!-- lt-dev:url-block:start -->';
130
130
  const endMarker = '<!-- lt-dev:url-block:end -->';
131
131
  const apiSub = identity.subdomains.api;
@@ -150,10 +150,20 @@ function patchClaudeMd(file, options) {
150
150
  '**Active URLs for THIS project:**',
151
151
  '',
152
152
  ];
153
- if (appSub)
154
- lines.push(`- App: \`https://${appSub.hostname}\``);
155
- if (apiSub)
156
- lines.push(`- API: \`https://${apiSub.hostname}\``);
153
+ // Both addresses, with who each one is for. The block tells an agent never to
154
+ // assume `localhost:3000/3001` — so if it only named the `*.localhost` host, an
155
+ // agent following it would `curl` a name that does not resolve for Node on
156
+ // Windows, and fail silently. The browser address stays first: it is the one a
157
+ // human opens.
158
+ if (appSub) {
159
+ lines.push(`- App: \`https://${appSub.hostname}\`${appPort ? ` — from a script: \`http://127.0.0.1:${appPort}\`` : ''}`);
160
+ }
161
+ if (apiSub) {
162
+ lines.push(`- API: \`https://${apiSub.hostname}\`${apiPort ? ` — from a script: \`http://127.0.0.1:${apiPort}\`` : ''}`);
163
+ }
164
+ if (appPort || apiPort) {
165
+ lines.push('- The `*.localhost` names are resolved by BROWSERS. Node, `curl` and other tools may not resolve them (they do not on Windows) — use the loopback address from a script.');
166
+ }
157
167
  if (dbName)
158
168
  lines.push(`- DB: \`mongodb://127.0.0.1/${dbName}\``);
159
169
  lines.push('');