@lenne.tech/cli 1.24.1 → 1.26.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.
@@ -35,7 +35,12 @@ function buildDevEnv(input) {
35
35
  const apiUrl = apiSub ? `https://${apiSub.hostname}` : '';
36
36
  const appUrl = appSub ? `https://${appSub.hostname}` : '';
37
37
  const caPath = (0, dev_env_bridge_1.detectCaddyRootCa)();
38
- const sharedKeys = Object.assign(Object.assign(Object.assign(Object.assign({}, (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 } : {}));
38
+ const sharedKeys = Object.assign(Object.assign(Object.assign(Object.assign({
39
+ // Marks the API + App processes as running under `lt dev`. Consumed by the
40
+ // backend to relax dev-only behaviour (rate limiting, Better-Auth
41
+ // user-cache) so E2E suites run without a separate VITEST/PLAYWRIGHT flag.
42
+ // (Also written to the .lt-dev/.env bridge for external test runners.)
43
+ 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 } : {}));
39
44
  return {
40
45
  api: {
41
46
  env: Object.assign(Object.assign(Object.assign({}, baseEnv), sharedKeys), {
@@ -0,0 +1,136 @@
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.runInstall = runInstall;
13
+ const caddy_1 = require("./caddy");
14
+ const dev_service_1 = require("./dev-service");
15
+ /**
16
+ * Run the one-time per-machine `lt dev` setup. Idempotent — safe to
17
+ * re-run. When `opts.auto` is set the heading reflects that it was
18
+ * triggered by another command (e.g. `lt dev init`).
19
+ */
20
+ function runInstall(toolbox_1) {
21
+ return __awaiter(this, arguments, void 0, function* (toolbox, opts = {}) {
22
+ const { print: { colors, error, info, success, warning }, } = toolbox;
23
+ info('');
24
+ info(colors.bold(opts.auto ? 'Preparing this machine for lt dev (lt dev install)' : 'lt dev install — one-time per-machine setup'));
25
+ info(colors.dim('─'.repeat(60)));
26
+ const plat = (0, dev_service_1.platformSupported)();
27
+ if (plat === 'unsupported') {
28
+ error(`Service management is not supported on ${process.platform}. Only macOS and Linux are covered.`);
29
+ info(` Workaround: run \`${colors.cyan(`caddy run --config ${caddy_1.paths.caddyfile}`)}\` manually.`);
30
+ return { blocked: true, caddyMissing: false, ok: false, unsupported: true };
31
+ }
32
+ let blocked = false;
33
+ // 1. caddy on PATH
34
+ const hasCaddy = yield (0, caddy_1.caddyAvailable)();
35
+ if (hasCaddy) {
36
+ success('caddy is on PATH');
37
+ }
38
+ else {
39
+ warning('caddy is not installed.');
40
+ info(` → macOS: ${colors.cyan('brew install caddy')}`);
41
+ info(` → Linux: ${colors.cyan('https://caddyserver.com/docs/install')}`);
42
+ info(' (Do NOT start it via `brew services` — `lt dev install` runs its own service.)');
43
+ blocked = true;
44
+ }
45
+ // 2. Caddyfile stub
46
+ (0, caddy_1.writeCaddyfile)('# lt dev — managed Caddyfile\n# Add per-project blocks via `lt dev up`.\n');
47
+ success(`Caddyfile present at ${caddy_1.paths.caddyfile}`);
48
+ if (!hasCaddy) {
49
+ info('');
50
+ error('Cannot continue setup until Caddy is installed. Re-run `lt dev install` afterwards.');
51
+ return { blocked: true, caddyMissing: true, ok: false, unsupported: false };
52
+ }
53
+ // 3. brew services conflict warning
54
+ const brewConflict = yield detectBrewCaddyConflict();
55
+ if (brewConflict) {
56
+ warning('A `brew services caddy` instance is registered.');
57
+ info(` Stop it (it crash-loops against our Caddyfile): ${colors.cyan('brew services stop caddy')}`);
58
+ info(' `lt dev install` runs its own service — the brew one is no longer needed.');
59
+ }
60
+ // 4. Install our LaunchAgent / systemd unit
61
+ const paths = (0, dev_service_1.getServicePaths)();
62
+ info('');
63
+ info(`Installing ${plat === 'darwin' ? 'LaunchAgent' : 'systemd-user unit'} at:`);
64
+ info(colors.dim(` ${paths.unitFile}`));
65
+ const installResult = yield (0, dev_service_1.installService)();
66
+ if (!installResult.ok) {
67
+ error(installResult.message);
68
+ blocked = true;
69
+ }
70
+ else if (installResult.created) {
71
+ success(installResult.message);
72
+ }
73
+ else {
74
+ info(colors.dim(installResult.message));
75
+ }
76
+ // 5. Wait for admin endpoint
77
+ if (installResult.ok) {
78
+ info(colors.dim('Waiting for Caddy admin endpoint (:2019) ...'));
79
+ const ready = yield (0, dev_service_1.waitForServiceReady)(8000);
80
+ const status = yield (0, dev_service_1.getServiceStatus)();
81
+ if (ready && status.daemonReachable) {
82
+ success(`Caddy daemon ready${status.pid ? ` (pid ${status.pid})` : ''}.`);
83
+ }
84
+ else if (status.loaded && !status.daemonReachable) {
85
+ warning('Service is loaded but admin endpoint did not respond within 8s.');
86
+ info(colors.dim(` Logs: ${paths.logFile} / ${paths.errFile}`));
87
+ blocked = true;
88
+ }
89
+ else {
90
+ warning('Caddy daemon did not start. See logs:');
91
+ info(colors.dim(` ${paths.logFile}`));
92
+ info(colors.dim(` ${paths.errFile}`));
93
+ blocked = true;
94
+ }
95
+ }
96
+ // 6. Validate Caddyfile
97
+ if (installResult.ok) {
98
+ const validation = yield (0, caddy_1.validateCaddyfile)();
99
+ if (validation.ok)
100
+ success('Caddyfile validates');
101
+ else
102
+ warning(`Caddyfile validation: ${validation.stderr.split('\n').slice(0, 2).join(' / ')}`);
103
+ }
104
+ // 7. CA trust
105
+ info('');
106
+ info(colors.bold('Local CA trust'));
107
+ info(' Caddy creates its local CA on first run. To trust it system-wide,');
108
+ info(' run this once (HOME must be preserved so sudo keeps the user-scoped');
109
+ info(' CA, otherwise caddy looks in /var/root and fails):');
110
+ info(` ${colors.cyan('sudo -E HOME="$HOME" caddy trust')}`);
111
+ info(` Browsers will then accept ${colors.cyan('https://*.localhost')} without warnings.`);
112
+ return { blocked, caddyMissing: false, ok: installResult.ok, unsupported: false };
113
+ });
114
+ }
115
+ /**
116
+ * Quick `brew services list` scan for a registered caddy service.
117
+ * Returns true on macOS if any entry contains "caddy" — error/started
118
+ * alike, both are conflicts. Always returns false on non-darwin or
119
+ * when `brew` is unavailable (no false positives).
120
+ */
121
+ function detectBrewCaddyConflict() {
122
+ if (process.platform !== 'darwin')
123
+ return Promise.resolve(false);
124
+ return new Promise((resolve) => {
125
+ var _a;
126
+ const { spawn } = require('child_process');
127
+ const child = spawn('brew', ['services', 'list'], { stdio: ['ignore', 'pipe', 'ignore'] });
128
+ let out = '';
129
+ (_a = child.stdout) === null || _a === void 0 ? void 0 : _a.on('data', (b) => (out += String(b)));
130
+ child.on('error', () => resolve(false));
131
+ child.on('close', () => {
132
+ const conflict = /\bcaddy\b/.test(out) && !/^caddy\s+none\b/m.test(out);
133
+ resolve(conflict);
134
+ });
135
+ });
136
+ }
@@ -1,20 +1,63 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.printMigrateResult = printMigrateResult;
3
4
  exports.runMigrate = runMigrate;
4
5
  /**
5
- * Reusable migrate logic — used by `commands/dev/migrate.ts` (interactive
6
- * + verbose) and `commands/fullstack/init.ts` (silent best-effort after
7
- * project creation).
6
+ * Reusable init/migrate logic — used by `commands/dev/init.ts` (interactive
7
+ * + verbose, also chained from `commands/dev/install.ts`) and
8
+ * `commands/fullstack/init.ts` (silent best-effort after project creation).
8
9
  *
9
10
  * Returns a structured result so callers can decide what to print.
10
11
  * Idempotent — safe to run multiple times.
11
12
  */
12
13
  const fs_1 = require("fs");
14
+ const gluegun_1 = require("gluegun");
13
15
  const path_1 = require("path");
14
16
  const dev_identity_1 = require("./dev-identity");
15
17
  const dev_patches_1 = require("./dev-patches");
16
18
  const dev_project_1 = require("./dev-project");
17
19
  const dev_state_1 = require("./dev-state");
20
+ const package_name_1 = require("./package-name");
21
+ /**
22
+ * Print a `runMigrate` result via the toolbox. Shared by `lt dev init`
23
+ * and the auto-init step of `lt dev install` so both render identically.
24
+ * Does NOT print Caddy-install hints — chaining handles that separately.
25
+ */
26
+ function printMigrateResult(toolbox, result) {
27
+ const { print: { colors, info, success }, } = toolbox;
28
+ info('');
29
+ info(colors.bold(`Initializing "${result.identity.slug}" for lt dev`));
30
+ info(colors.dim('─'.repeat(60)));
31
+ if (result.identity.subdomains.app)
32
+ info(` App URL: https://${result.identity.subdomains.app.hostname}`);
33
+ if (result.identity.subdomains.api)
34
+ info(` API URL: https://${result.identity.subdomains.api.hostname}`);
35
+ info(` DB: mongodb://127.0.0.1/${result.dbName}`);
36
+ info('');
37
+ if (result.renamedTemplatePackage) {
38
+ success(`renamed root package.json name → "${result.renamedTemplatePackage}" (was unmodified template default)`);
39
+ }
40
+ if (result.codePatches.length > 0) {
41
+ for (const r of result.codePatches) {
42
+ if (r.patched)
43
+ success(`patched ${r.replacements}× in ${r.file}`);
44
+ else
45
+ info(colors.dim(`already patched: ${r.file}`));
46
+ }
47
+ }
48
+ else {
49
+ info(colors.dim(' patches: not needed (already env-aware)'));
50
+ }
51
+ result.claudePatches.filter((r) => r.patched).forEach((r) => success(`updated CLAUDE.md URL block: ${r.file}`));
52
+ if (result.registryUpdated) {
53
+ success(`registered in ${process.env.LT_DEV_REGISTRY_PATH || '~/.lenneTech/projects.json'}`);
54
+ }
55
+ if (result.addedGitignoreEntry)
56
+ success('added `.lt-dev/` to .gitignore');
57
+ if (result.alreadyMigrated) {
58
+ info(colors.dim(' Project was already initialized — nothing changed.'));
59
+ }
60
+ }
18
61
  /**
19
62
  * Run all migration steps for a resolved project.
20
63
  *
@@ -22,6 +65,17 @@ const dev_state_1 = require("./dev-state");
22
65
  */
23
66
  function runMigrate(input) {
24
67
  const { layout } = input;
68
+ // 0. If the root package.json still carries an unmodified starter-template
69
+ // name (e.g. `lt-monorepo` from a raw `git clone`), rewrite it to the
70
+ // directory basename before deriving identity. Otherwise every cloned
71
+ // project would slug to `lt-monorepo` and collide on
72
+ // `https://lt-monorepo.localhost`. `lt fullstack init` already handles
73
+ // this — the call here is the safety net for projects that bypassed
74
+ // init (e.g. manual `git clone lenneTech/lt-monorepo my-project`).
75
+ const renamedTemplatePackage = (0, package_name_1.renameUnmodifiedTemplatePackage)({
76
+ filesystem: gluegun_1.filesystem,
77
+ projectRoot: layout.root,
78
+ });
25
79
  const identity = (0, dev_identity_1.buildIdentity)(layout.root);
26
80
  const dbName = (0, dev_project_1.deriveDbName)(layout.apiDir, identity.slug);
27
81
  // 1. Code patches (config.env.ts, nuxt.config.ts, playwright.config.ts).
@@ -68,7 +122,7 @@ function runMigrate(input) {
68
122
  const addedGitignoreEntry = (0, dev_patches_1.addToGitignore)(layout.root, '.lt-dev/');
69
123
  const codePatched = codePatches.filter((r) => r.patched).length > 0;
70
124
  const claudePatched = claudePatches.filter((r) => r.patched).length > 0;
71
- const alreadyMigrated = !codePatched && !claudePatched && !registryChanged && !addedGitignoreEntry;
125
+ const alreadyMigrated = !codePatched && !claudePatched && !registryChanged && !addedGitignoreEntry && !renamedTemplatePackage;
72
126
  return {
73
127
  addedGitignoreEntry,
74
128
  alreadyMigrated,
@@ -77,5 +131,6 @@ function runMigrate(input) {
77
131
  dbName,
78
132
  identity,
79
133
  registryUpdated: registryChanged,
134
+ renamedTemplatePackage,
80
135
  };
81
136
  }
@@ -7,7 +7,7 @@ exports.patchClaudeMd = patchClaudeMd;
7
7
  exports.patchNuxtConfig = patchNuxtConfig;
8
8
  exports.patchPlaywrightConfig = patchPlaywrightConfig;
9
9
  /**
10
- * Idempotent patches applied by `lt dev migrate`.
10
+ * Idempotent patches applied by `lt dev init`.
11
11
  *
12
12
  * Goal: take a project that still has hardcoded `localhost:3000`
13
13
  * defaults and make it env-aware so it can be served behind Caddy
@@ -40,16 +40,32 @@ function autoPatch(file) {
40
40
  return patchPlaywrightConfig(file);
41
41
  return { file, patched: false, replacements: 0 };
42
42
  }
43
- /** API: `port: 3000,` → `port: Number(process.env.PORT) || 3000,`. */
43
+ /**
44
+ * API: make the server listen port honour `process.env.PORT` (injected by
45
+ * `lt dev up` for its Caddy upstream). Handles two patterns found in
46
+ * nest-server `config.env.ts` files:
47
+ *
48
+ * - the legacy literal `port: 3000,` (e.g. in `deployedConfig()`)
49
+ * - the offers-pattern `port: process.env.NSC__PORT ? parseInt(process.env.NSC__PORT, 10) : 3000`
50
+ * found in `localConfig()` — `lt dev` runs the API in local mode, so this
51
+ * line MUST be patched too or the API ignores the assigned port.
52
+ *
53
+ * Idempotent — lines that already read `process.env.PORT` are left untouched.
54
+ */
44
55
  function patchApiConfig(file) {
45
56
  if (!(0, fs_1.existsSync)(file))
46
57
  return { file, patched: false, replacements: 0 };
47
58
  const before = (0, fs_1.readFileSync)(file, 'utf8');
48
59
  let count = 0;
49
- const after = before.replace(/^(\s*)port:\s*3000\s*,$/gm, (_m, indent) => {
60
+ let after = before.replace(/^(\s*)port:\s*3000\s*,$/gm, (_m, indent) => {
50
61
  count++;
51
62
  return `${indent}port: Number(process.env.PORT) || 3000,`;
52
63
  });
64
+ // localConfig() keeps the NSC__PORT operator override; PORT (lt dev) wins.
65
+ after = after.replace(/^(\s*)port:\s*process\.env\.NSC__PORT\s*\?[^\n]*:\s*3000\s*,$/gm, (_m, indent) => {
66
+ count++;
67
+ return `${indent}port: Number(process.env.PORT) || (process.env.NSC__PORT ? parseInt(process.env.NSC__PORT, 10) : 3000),`;
68
+ });
53
69
  if (count === 0)
54
70
  return { file, patched: false, replacements: 0 };
55
71
  (0, fs_1.writeFileSync)(file, after, 'utf8');
@@ -69,6 +85,7 @@ function patchClaudeMd(file, options) {
69
85
  const appSub = identity.subdomains.app;
70
86
  const lines = [
71
87
  startMarker,
88
+ '',
72
89
  '## Local Development (lt dev)',
73
90
  '',
74
91
  `This project is registered with \`lt dev\` (slug: \`${identity.slug}\`). Use these commands to run alongside other lt projects without cross-wiring or port collisions:`,
@@ -77,9 +94,12 @@ function patchClaudeMd(file, options) {
77
94
  'lt dev up # Start API + App behind Caddy with project-specific URLs',
78
95
  'lt dev down # Stop the detached processes + remove Caddy block',
79
96
  'lt dev status # Show running PIDs + bound URLs',
97
+ 'lt dev test # Ensure up + run the E2E suite with project URLs injected',
80
98
  'lt dev doctor # Diagnose Caddy/CA/DNS/port issues',
81
99
  '```',
82
100
  '',
101
+ '**Start and test local apps via `lt dev`** — never `pnpm dev` / `pnpm start` / a bare `playwright test` directly; those bind the framework default ports (3000/3001) and collide with parallel projects.',
102
+ '',
83
103
  '**Active URLs for THIS project:**',
84
104
  '',
85
105
  ];
@@ -88,7 +108,7 @@ function patchClaudeMd(file, options) {
88
108
  if (apiSub)
89
109
  lines.push(`- API: \`https://${apiSub.hostname}\``);
90
110
  if (dbName)
91
- lines.push(`- DB: \`mongodb://127.0.0.1/${dbName}\``);
111
+ lines.push(`- DB: \`mongodb://127.0.0.1/${dbName}\``);
92
112
  lines.push('');
93
113
  lines.push('Env vars set automatically by `lt dev up`: `BASE_URL`, `APP_URL`, `NUXT_API_URL`, `NUXT_PUBLIC_API_URL`, `NUXT_PUBLIC_SITE_URL`, `NUXT_PUBLIC_STORAGE_PREFIX`, `NSC__MONGOOSE__URI`, `DATABASE_URL`. **Never assume `localhost:3000` / `localhost:3001` for this project** — those are the framework defaults, not the active URLs.');
94
114
  lines.push('');
@@ -159,30 +179,52 @@ function patchPlaywrightConfig(file) {
159
179
  return `${key}: process.env.NUXT_PUBLIC_SITE_URL || 'http://localhost:3001'`;
160
180
  });
161
181
  }
162
- // 2. Top-of-file dotenv bridge — only inject if not already present.
182
+ // 2. Top-of-file dotenv bridge — inject it, or replace an outdated block.
183
+ // The loader walks UP from cwd to find `.lt-dev/.env`: that file lives
184
+ // at the repo root, while playwright.config.ts (and the process cwd of
185
+ // a direct `playwright test` run) usually sit in `projects/app`. The
186
+ // original cwd-only resolve missed it, so direct runs fell back to
187
+ // `localhost:3001` and could collide with a parallel project.
163
188
  const bridgeStart = '// >>> lt-dev:bridge >>>';
164
189
  const bridgeEnd = '// <<< lt-dev:bridge <<<';
165
- if (!after.includes(bridgeStart)) {
166
- const bridgeBlock = [
167
- bridgeStart,
168
- '// Auto-load <root>/.lt-dev/.env when `lt dev up` is active so',
169
- '// external test runners (CLI, IDE, VS Code Playwright Extension)',
170
- '// pick up project URLs + Caddy CA without inheriting the parent shell.',
171
- "import { existsSync as __ltDevExists, readFileSync as __ltDevRead } from 'node:fs';",
172
- "import { resolve as __ltDevResolve } from 'node:path';",
173
- "const __ltDevEnvFile = __ltDevResolve(process.cwd(), '.lt-dev/.env');",
174
- 'if (__ltDevExists(__ltDevEnvFile)) {',
175
- ' for (const __ln of __ltDevRead(__ltDevEnvFile, "utf8").split(/\\r?\\n/)) {',
176
- ' const __m = __ln.match(/^([A-Z][A-Z0-9_]*)=(.*)$/);',
177
- ' if (__m && process.env[__m[1]] === undefined) process.env[__m[1]] = __m[2];',
178
- ' }',
179
- '}',
180
- bridgeEnd,
181
- '',
182
- ].join('\n');
183
- after = bridgeBlock + after;
190
+ const bridgeBlock = [
191
+ bridgeStart,
192
+ '// Auto-load <root>/.lt-dev/.env when `lt dev up` is active so',
193
+ '// external test runners (CLI, IDE, VS Code Playwright Extension)',
194
+ '// pick up project URLs + Caddy CA without inheriting the parent shell.',
195
+ '// Searches upward from cwd because `.lt-dev/` sits at the repo root',
196
+ '// while playwright.config.ts (and cwd) usually sit in projects/app.',
197
+ "import { existsSync as __ltDevExists, readFileSync as __ltDevRead } from 'node:fs';",
198
+ "import { dirname as __ltDevDirname, resolve as __ltDevResolve } from 'node:path';",
199
+ "let __ltDevEnvFile = '';",
200
+ 'for (let __ltDevDir = process.cwd(), __i = 0; __i < 6; __i++) {',
201
+ " const __candidate = __ltDevResolve(__ltDevDir, '.lt-dev/.env');",
202
+ ' if (__ltDevExists(__candidate)) { __ltDevEnvFile = __candidate; break; }',
203
+ ' const __parent = __ltDevDirname(__ltDevDir);',
204
+ ' if (__parent === __ltDevDir) break;',
205
+ ' __ltDevDir = __parent;',
206
+ '}',
207
+ 'if (__ltDevEnvFile) {',
208
+ ' for (const __ln of __ltDevRead(__ltDevEnvFile, "utf8").split(/\\r?\\n/)) {',
209
+ ' const __m = __ln.match(/^([A-Z][A-Z0-9_]*)=(.*)$/);',
210
+ ' if (__m && process.env[__m[1]] === undefined) process.env[__m[1]] = __m[2];',
211
+ ' }',
212
+ '}',
213
+ bridgeEnd,
214
+ ].join('\n');
215
+ const bridgeStartIdx = after.indexOf(bridgeStart);
216
+ const bridgeEndIdx = after.indexOf(bridgeEnd);
217
+ if (bridgeStartIdx === -1) {
218
+ after = `${bridgeBlock}\n${after}`;
184
219
  count++;
185
220
  }
221
+ else if (bridgeEndIdx !== -1) {
222
+ const rebuilt = after.slice(0, bridgeStartIdx) + bridgeBlock + after.slice(bridgeEndIdx + bridgeEnd.length);
223
+ if (rebuilt !== after) {
224
+ after = rebuilt;
225
+ count++;
226
+ }
227
+ }
186
228
  if (count === 0)
187
229
  return { file, patched: false, replacements: 0 };
188
230
  (0, fs_1.writeFileSync)(file, after, 'utf8');
@@ -12,6 +12,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.checkPortInUse = checkPortInUse;
13
13
  exports.killProcessGroup = killProcessGroup;
14
14
  exports.listenSnapshot = listenSnapshot;
15
+ exports.rotateLogFile = rotateLogFile;
15
16
  exports.spawnDetached = spawnDetached;
16
17
  /**
17
18
  * Process + port helpers for `lt dev`.
@@ -114,9 +115,43 @@ function listenSnapshot(ports) {
114
115
  });
115
116
  });
116
117
  }
118
+ /**
119
+ * Rotate a log file: rename existing `<logFile>` to `<logFile>.1`, dropping
120
+ * any previous `.1`. Keeps exactly one prior generation so the most recent
121
+ * `lt dev down`-able session stays inspectable without unbounded growth.
122
+ *
123
+ * Returns `{ rotated: false }` when no prior log exists.
124
+ */
125
+ function rotateLogFile(logFile) {
126
+ let previousSize;
127
+ try {
128
+ previousSize = (0, fs_1.statSync)(logFile).size;
129
+ }
130
+ catch (_a) {
131
+ return { rotated: false };
132
+ }
133
+ const archivePath = `${logFile}.1`;
134
+ try {
135
+ (0, fs_1.unlinkSync)(archivePath);
136
+ }
137
+ catch (_b) {
138
+ /* nothing to remove */
139
+ }
140
+ try {
141
+ (0, fs_1.renameSync)(logFile, archivePath);
142
+ }
143
+ catch (_c) {
144
+ return { rotated: false };
145
+ }
146
+ return { archivePath, previousSize, rotated: true };
147
+ }
117
148
  /**
118
149
  * Spawn a detached child whose stdio is redirected to a log file.
119
150
  *
151
+ * Rotates any previous log first (one generation kept as `<logFile>.1`) so
152
+ * each session starts with a fresh file. Prevents the multi-day accumulation
153
+ * that produced ~10 GB logs under continuous `up`/`down` cycles.
154
+ *
120
155
  * The parent's copy of the log file descriptor is closed in `finally`
121
156
  * — the child has already inherited its own fd before `spawn` returns,
122
157
  * so closing prevents fd leaks and avoids racing-write artifacts on
@@ -126,6 +161,7 @@ function listenSnapshot(ports) {
126
161
  */
127
162
  function spawnDetached(cmd, args, opts) {
128
163
  (0, fs_1.mkdirSync)((0, path_1.dirname)(opts.logFile), { recursive: true });
164
+ const rotated = rotateLogFile(opts.logFile);
129
165
  const out = (0, fs_1.openSync)(opts.logFile, 'a');
130
166
  let child;
131
167
  try {
@@ -136,7 +172,9 @@ function spawnDetached(cmd, args, opts) {
136
172
  stdio: ['ignore', out, out],
137
173
  });
138
174
  child.unref();
139
- return child.pid;
175
+ if (child.pid === undefined)
176
+ return undefined;
177
+ return { pid: child.pid, rotated };
140
178
  }
141
179
  catch (_a) {
142
180
  return undefined;