@lenne.tech/cli 1.41.3 → 1.42.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.
@@ -268,7 +268,24 @@ const NewCommand = {
268
268
  info(' (an explicit node blocks the parent wildcard for names below it)');
269
269
  info(' 4. Set the stage env vars in TurboOps (per stage), e.g. for production:');
270
270
  info(` NODE_ENV=production, NSC__BASE_URL=https://api.${domain},`);
271
- info(' NSC__MONGOOSE__URI, NSC__BETTER_AUTH__SECRET, NSC__AI__ENCRYPTION_SECRET,');
271
+ // The DB host is spelled out per stage on purpose. Every other variable in
272
+ // this checklist carries a concrete value; leaving this one as a bare name
273
+ // forces the reader to invent it, and the only reference in sight is the
274
+ // project's own docker-compose.yml, where the service is called `mongo`.
275
+ // `mongodb://mongo:27017/...` is the natural guess — and the wrong one: the
276
+ // short name is a Swarm alias on a network shared by every stack, so it
277
+ // resolves to a FOREIGN project's database (and to a different one on each
278
+ // connection). Symptoms are split-brain writes, sessions that vanish, and
279
+ // data quietly landing in someone else's MongoDB. See DEV-2140.
280
+ info(` NSC__MONGOOSE__URI=mongodb://<user>:<pass>@${project}-production_mongo:27017/${project}?authSource=admin,`);
281
+ info(` (dev stage: mongodb://<user>:<pass>@${project}-dev_mongo:27017/${project}?authSource=admin)`);
282
+ info(' NOTE: always the stack-prefixed host `<project>-<stage>_mongo`, never a bare');
283
+ info(' `mongo` — the short name is shared across stacks and resolves to a FOREIGN');
284
+ info(' database, non-deterministically per connection.');
285
+ info(' The stack-prefix fixes WHICH database you reach, not WHO may reach it:');
286
+ info(' the overlay network is shared, so the DB credentials are the actual');
287
+ info(' boundary. Set them in the mongo service and never deploy it open.');
288
+ info(' NSC__BETTER_AUTH__SECRET, NSC__AI__ENCRYPTION_SECRET,');
272
289
  if (isAngular) {
273
290
  info(' NSC__EMAIL__SMTP__*, NSC__EMAIL__DEFAULT_SENDER__EMAIL');
274
291
  info(' The Angular app needs no URL env vars — they are baked into');
@@ -0,0 +1,173 @@
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.help = void 0;
13
+ const vscode_settings_1 = require("../../lib/vscode-settings");
14
+ /**
15
+ * Tune VS Code's USER settings for machines that keep many lt monorepos open.
16
+ *
17
+ * Each open workspace root spawns its own pair of TypeScript servers, and the
18
+ * "semantic" one of each pair is what actually holds the memory. Eight open
19
+ * monorepos (api + app root each) therefore means 16 semantic servers — enough
20
+ * to push a 32 GB machine deep into swap. This command applies the verified
21
+ * profile in `lib/vscode-settings.ts` to every detected installation.
22
+ *
23
+ * Safety properties, all load-bearing:
24
+ * - JSONC-aware, so comments and formatting in a hand-maintained
25
+ * settings.json survive (a JSON.parse round-trip would delete them).
26
+ * - Refuses to write into a settings.json it cannot parse.
27
+ * - Backs up to `settings.json.bak` before the first write.
28
+ * - Merges the object-valued exclude maps, so hand-added entries are kept —
29
+ * and `--revert` SUBTRACTS only those same entries again, so an undo never
30
+ * takes a hand-maintained exclusion with it.
31
+ * - Keeps the FIRST `.bak`, so a later run (including the revert) cannot
32
+ * overwrite the record of the pre-tuning state.
33
+ * - No-op on re-run.
34
+ *
35
+ * `--revert` restores VS Code's default for the scalar keys rather than any
36
+ * explicit value that preceded them; the `.bak` is the recovery path for those.
37
+ */
38
+ const VsCodeCommand = {
39
+ alias: ['vsc'],
40
+ description: 'Tune VS Code memory settings',
41
+ hidden: false,
42
+ name: 'vscode',
43
+ run: (toolbox) => __awaiter(void 0, void 0, void 0, function* () {
44
+ const { parameters, print: { colors, info }, prompt: { confirm }, } = toolbox;
45
+ // `--dry-run` PREVENTS a write, so it reads presence-as-intent: `--dry-run=1`
46
+ // must not fall through and write. `--revert` / `--explain` merely enable
47
+ // something, so the usual `=== true || === 'true'` is safe there.
48
+ const dryRun = (0, vscode_settings_1.isPreventingFlagSet)(parameters.options, 'dry-run', 'dryRun');
49
+ const revert = (0, vscode_settings_1.isEnablingFlagSet)(parameters.options.revert);
50
+ const explain = (0, vscode_settings_1.isEnablingFlagSet)(parameters.options.explain);
51
+ // This command writes OUTSIDE the project, into the user's global editor
52
+ // settings. A `defaults.noConfirm` in a repo-local `lt.config.json` — which
53
+ // is discovered by walking up from cwd, i.e. can come from a cloned repo —
54
+ // must not be able to silence that prompt. Only an explicit CLI flag does.
55
+ const noConfirm = (0, vscode_settings_1.isEnablingFlagSet)(parameters.options.noConfirm);
56
+ info('');
57
+ info(colors.bold(`lt dev vscode${revert ? ' --revert' : ''}${dryRun ? ' (dry run)' : ''}`));
58
+ info(colors.dim('─'.repeat(64)));
59
+ if (explain) {
60
+ info(colors.bold('\nProfile:'));
61
+ for (const [key, entry] of Object.entries(vscode_settings_1.MEMORY_PROFILE)) {
62
+ info(` ${colors.cyan(key)}`);
63
+ info(` ${colors.dim(entry.reason)}`);
64
+ }
65
+ info(colors.bold('\nDeliberately NOT set:'));
66
+ for (const item of vscode_settings_1.EXCLUDED_FROM_PROFILE) {
67
+ info(` ${colors.yellow(item.key)}`);
68
+ info(` ${colors.dim(item.why)}`);
69
+ }
70
+ info('');
71
+ if (!parameters.options.fromGluegunMenu)
72
+ process.exit();
73
+ return 'dev vscode: explained';
74
+ }
75
+ const all = (0, vscode_settings_1.detectVariants)();
76
+ const { targets, unknownFilter } = (0, vscode_settings_1.selectVariants)(all, parameters.options.variant);
77
+ // A bare `--variant` parses to boolean `true` and matches no id. Reporting
78
+ // that as "no installation found" told users their editor was missing while
79
+ // it was installed — two different problems deserve two different messages.
80
+ if (unknownFilter) {
81
+ info(colors.yellow(` Unknown --variant "${unknownFilter}".`));
82
+ info(colors.dim(` Valid values: ${all.map((v) => v.id).join(' | ')}`));
83
+ if (!parameters.options.fromGluegunMenu)
84
+ process.exit(1);
85
+ return 'dev vscode: unknown variant';
86
+ }
87
+ if (targets.length === 0) {
88
+ info(colors.yellow(' No VS Code installation with a user settings.json found.'));
89
+ info(colors.dim(` Looked for: ${all.map((v) => v.label).join(', ')}`));
90
+ if (!parameters.options.fromGluegunMenu)
91
+ process.exit(1);
92
+ return 'dev vscode: no installation found';
93
+ }
94
+ // Preview first — the user sees the exact before/after per key before
95
+ // anything is written.
96
+ let pending = 0;
97
+ for (const target of targets) {
98
+ const preview = (0, vscode_settings_1.tuneSettingsFile)(target.settingsPath, { dryRun: true, remove: revert });
99
+ info(`\n ${colors.bold(target.label)} ${colors.dim(target.settingsPath)}`);
100
+ if (preview.error) {
101
+ info(` ${colors.red('skipped')} — ${preview.error}`);
102
+ continue;
103
+ }
104
+ for (const change of preview.changes) {
105
+ info(` ${(0, vscode_settings_1.formatChange)(change, colors)}`);
106
+ if (change.action !== 'unchanged')
107
+ pending++;
108
+ }
109
+ }
110
+ if (pending === 0) {
111
+ info(colors.green('\n✓ already up to date — nothing to do\n'));
112
+ if (!parameters.options.fromGluegunMenu)
113
+ process.exit();
114
+ return 'dev vscode: no changes needed';
115
+ }
116
+ if (dryRun) {
117
+ info(colors.dim(`\n${pending} change(s) would be applied. Re-run without --dry-run to apply.\n`));
118
+ if (!parameters.options.fromGluegunMenu)
119
+ process.exit();
120
+ return `dev vscode: dry run, ${pending} pending change(s)`;
121
+ }
122
+ if (!noConfirm && !(yield confirm(`Apply ${pending} change(s)?`, true))) {
123
+ info(colors.dim('\nAborted — nothing written.\n'));
124
+ if (!parameters.options.fromGluegunMenu)
125
+ process.exit();
126
+ return 'dev vscode: aborted';
127
+ }
128
+ let applied = 0;
129
+ for (const target of targets) {
130
+ const result = (0, vscode_settings_1.tuneSettingsFile)(target.settingsPath, { remove: revert });
131
+ if (result.error) {
132
+ info(` ${colors.red('✗')} ${target.label}: ${result.error}`);
133
+ continue;
134
+ }
135
+ if (!result.written)
136
+ continue;
137
+ applied += result.changes.filter((c) => c.action !== 'unchanged').length;
138
+ info(` ${colors.green('✓')} ${target.label} updated ${colors.dim(`(backup: ${result.backupPath})`)}`);
139
+ }
140
+ info(colors.dim('\n Restart VS Code (or run "Developer: Reload Window") for the TS servers to pick this up.\n'));
141
+ if (!parameters.options.fromGluegunMenu)
142
+ process.exit();
143
+ return `dev vscode: applied ${applied} change(s)`;
144
+ }),
145
+ };
146
+ exports.help = {
147
+ aliases: ['vsc'],
148
+ configuration: 'none (writes global editor settings — --noConfirm must be passed explicitly)',
149
+ description: "Apply a verified low-memory profile to VS Code's user settings. Targets the per-workspace TypeScript servers, which dominate memory when many monorepos are open at once.",
150
+ examples: ['dev vscode', 'dev vscode --dry-run', 'dev vscode --explain', 'dev vscode --revert'],
151
+ features: [
152
+ 'JSONC-aware — preserves comments and formatting; refuses to write an unparseable file or a symlink.',
153
+ 'Backs up to settings.json.bak (the first one is kept) and merges object-valued keys, keeping hand-added entries.',
154
+ 'Detects VS Code, Insiders, Cursor and VSCodium; idempotent, with --revert subtracting only its own entries.',
155
+ ],
156
+ name: 'vscode',
157
+ options: [
158
+ { description: 'Show what would change without writing', flag: '--dry-run', type: 'boolean' },
159
+ { description: 'Remove the profile keys again', flag: '--revert', type: 'boolean' },
160
+ {
161
+ description: 'Print the profile with reasons, plus the keys deliberately left out',
162
+ flag: '--explain',
163
+ type: 'boolean',
164
+ },
165
+ {
166
+ description: 'Limit to one installation: code | insiders | cursor | vscodium',
167
+ flag: '--variant',
168
+ type: 'string',
169
+ },
170
+ { description: 'Skip the confirmation prompt', flag: '--noConfirm', type: 'boolean' },
171
+ ],
172
+ };
173
+ module.exports = Object.assign(VsCodeCommand, { help: exports.help });
@@ -14,6 +14,7 @@ const dev_patches_1 = require("../../lib/dev-patches");
14
14
  const framework_detection_1 = require("../../lib/framework-detection");
15
15
  const frontend_framework_detection_1 = require("../../lib/frontend-framework-detection");
16
16
  const heal_check_wrapper_1 = require("../../lib/heal-check-wrapper");
17
+ const heal_vendor_migrate_store_1 = require("../../lib/heal-vendor-migrate-store");
17
18
  const vendor_claude_md_1 = require("../../lib/vendor-claude-md");
18
19
  /**
19
20
  * Update a fullstack workspace — mode-aware.
@@ -208,6 +209,22 @@ const NewCommand = {
208
209
  info('');
209
210
  success(' Added `.lt-dev/` to .gitignore');
210
211
  }
212
+ // ── Self-heal: repair the vendor-mode migration store ──────────────────
213
+ //
214
+ // `migrations-utils/migrate.js` is written ONCE, at conversion time. Projects
215
+ // converted before the template stopped requiring ts-node unconditionally keep
216
+ // the broken file forever — it is project scaffolding, not `src/core/`, so no
217
+ // update path ever revisits it. Those containers die with
218
+ // `Cannot find module 'ts-node'` before applying a single migration, and stay
219
+ // healthy while doing so, because the entrypoint degrades the failure to a
220
+ // warning on purpose. Idempotent, and deliberately blind to stores that guard
221
+ // the require their own way.
222
+ const migrateStoreAsset = (0, path_1.join)(__dirname, '..', '..', 'templates', 'vendor-scripts', 'migrate-store.js');
223
+ const changedStore = (0, heal_vendor_migrate_store_1.healVendorMigrateStore)(apiDir, migrateStoreAsset);
224
+ if (changedStore.length > 0) {
225
+ info('');
226
+ success(` Repaired the vendor migration store: ${changedStore.join(', ')}`);
227
+ }
211
228
  info('');
212
229
  info(colors.bold('For a comprehensive update of everything, use:'));
213
230
  info('');
@@ -9,17 +9,19 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  });
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.TEST_INITIAL_ADMIN_ENV = void 0;
12
+ exports.TEST_NITRO_OUTPUT_DIR = exports.TEST_NUXT_BUILD_DIR = exports.TEST_INITIAL_ADMIN_ENV = void 0;
13
13
  exports.ensurePlaywrightBrowsers = ensurePlaywrightBrowsers;
14
14
  exports.autoShardCount = autoShardCount;
15
15
  exports.bringUpTestSession = bringUpTestSession;
16
16
  exports.buildShardPlaywrightInvocation = buildShardPlaywrightInvocation;
17
+ exports.buildTestAppEnv = buildTestAppEnv;
17
18
  exports.hasTestSession = hasTestSession;
18
19
  exports.resolveTestSession = resolveTestSession;
19
20
  exports.runShardedTestSession = runShardedTestSession;
20
21
  exports.shardReportDir = shardReportDir;
21
22
  exports.tearDownAllTestSessions = tearDownAllTestSessions;
22
23
  exports.tearDownTestSession = tearDownTestSession;
24
+ exports.testAppEntryCandidates = testAppEntryCandidates;
23
25
  /**
24
26
  * Ephemeral, isolated test session for `lt dev test`.
25
27
  *
@@ -103,6 +105,47 @@ exports.TEST_INITIAL_ADMIN_ENV = {
103
105
  NSC__SYSTEM_SETUP__INITIAL_ADMIN__NAME: 'CI Admin',
104
106
  NSC__SYSTEM_SETUP__INITIAL_ADMIN__PASSWORD: 'CiThrowawayAdmin123!',
105
107
  };
108
+ /**
109
+ * The Nuxt build directory the test stack's app process uses — never the one
110
+ * `nuxt dev` / the IDE write (`.nuxt`), never the check chain's (`.nuxt-check`).
111
+ *
112
+ * `@nuxt/cli` takes its lock ON the build directory
113
+ * (`acquireLock(nuxt.options.buildDir)`), so sharing it does not merely
114
+ * interleave writes — it makes the second command ABORT: a `lt dev test` next to
115
+ * a parked `lt dev up` died with "Another Nuxt dev is already running", the test
116
+ * app never started, and every spec then failed on a missing selector. That
117
+ * reads like broken specs while being pure infrastructure, which is what made it
118
+ * expensive to diagnose. A directory of its own frees the lock and the writes in
119
+ * one move.
120
+ *
121
+ * Projects whose `nuxt.config.ts` does not (yet) read `NUXT_BUILD_DIR` simply
122
+ * ignore it, so this needs no per-project case distinction.
123
+ */
124
+ exports.TEST_NUXT_BUILD_DIR = '.nuxt-test';
125
+ /**
126
+ * The Nitro OUTPUT directory the test stack builds into (DEV-2724).
127
+ *
128
+ * A second axis from `TEST_NUXT_BUILD_DIR`, not a duplicate of it: `buildDir`
129
+ * and Nitro's `output.dir` are unrelated knobs, so isolating the former left
130
+ * `.output/` shared. That matters here more than anywhere else, because this
131
+ * stack does not run `nuxt dev` — it serves the production bundle, rebuilding on
132
+ * every run, and therefore overwrites the tree a local `pnpm run build` (or a
133
+ * server started from it) is using.
134
+ *
135
+ * NEITHER variable is framework-native — verified against `@nuxt/schema` 4.4.8,
136
+ * `nitropack` 2.13.4 and `c12`: none of them reads `NUXT_BUILD_DIR` or
137
+ * `NITRO_OUTPUT_DIR`. Both levers are opened by the project's own
138
+ * `nuxt.config.ts` (`buildDir: process.env.NUXT_BUILD_DIR || '.nuxt'`,
139
+ * `nitro.output.dir: process.env.NITRO_OUTPUT_DIR || '.output'`); nuxt-base-starter
140
+ * ≥ 2.16.0 ships both. Do NOT write "unlike NUXT_BUILD_DIR, …" here: that
141
+ * asymmetric contrast silently promotes one of them to a framework feature and
142
+ * makes readers forward only the other, which reintroduces the collision this
143
+ * whole mechanism exists to prevent.
144
+ *
145
+ * Projects that forward neither simply keep building into `.nuxt` / `.output`,
146
+ * which is why `testAppEntryCandidates()` still looks there.
147
+ */
148
+ exports.TEST_NITRO_OUTPUT_DIR = '.output-test';
106
149
  /**
107
150
  * Heuristic for the default local shard count (`--shard auto` / bare `--shard`).
108
151
  *
@@ -236,6 +279,11 @@ function bringUpTestSession(layout_1, baseIdentity_1, log_1) {
236
279
  dbName,
237
280
  identity: testIdentity,
238
281
  });
282
+ // Every app process below — the build, the built server, and the dev-server
283
+ // fallback — runs with THIS env, so the test stack never touches the build dir
284
+ // `nuxt dev` and the IDE use. Declared out here because the session context
285
+ // hands it to the Playwright child as well.
286
+ const appEnv = buildTestAppEnv(devEnv.app.env);
239
287
  const pids = {};
240
288
  // --- API: compiled (`node dist`) for stability; fall back to the project's
241
289
  // own dev start script. `skipBuild` (sibling shards) reuses the dist the
@@ -281,25 +329,41 @@ function bringUpTestSession(layout_1, baseIdentity_1, log_1) {
281
329
  // buildDevEnv sets NUXT_PUBLIC_API_PROXY=false, so the built app talks
282
330
  // cross-origin to the test API exactly like prod (the injected session cookie
283
331
  // must be a cross-subdomain DOMAIN cookie — see the project's parseCookieHeader).
284
- // Rebuilt every run so the suite never hits stale code (no build-skip / reuse). ---
332
+ // Rebuilt every run so the suite never hits stale code (no build-skip / reuse).
333
+ // `appEnv` pins NUXT_BUILD_DIR to its own dir, which is what lets this run next
334
+ // to a parked `lt dev up` at all: the Nuxt lock sits on the build dir, so the
335
+ // build below used to abort outright against a running dev server (DEV-2715).
336
+ // The fallback needs it just as much — that path IS a second `nuxt dev`.
337
+ // It also pins NITRO_OUTPUT_DIR (DEV-2724), so the rebuild below stops
338
+ // overwriting the `.output/` a local `pnpm run build` is serving — and the
339
+ // entry lookup moves with it, or the spawn would find nothing and drop to the
340
+ // slow `pnpm dev` fallback. ---
285
341
  if (layout.appDir && appPort) {
342
+ // The isolated build trees are new names this CLI invented, so a project
343
+ // whose `.gitignore` predates the starter's `.nuxt-*` / `.output-*` globs
344
+ // leaves them UNTRACKED — a `git add -A` away from committing a Nitro
345
+ // bundle (which inlines runtimeConfig defaults), and enough to make
346
+ // `lt ticket stop` see uncommitted work. Idempotent, like every other
347
+ // `addToGitignore` call.
348
+ (0, dev_patches_1.addToGitignore)(layout.appDir, '.nuxt-*');
349
+ (0, dev_patches_1.addToGitignore)(layout.appDir, '.output-*');
286
350
  const appPm = (0, dev_package_manager_1.pickPackageManager)(layout.appDir);
287
351
  let appBuild = 0;
288
352
  if (!skipBuild) {
289
353
  log.info(log.dim('Building App (nuxt build, for speed + prod-fidelity) …'));
290
354
  appBuild = yield (0, dev_process_1.runChildInherit)(appPm.bin, appPm.runScript('build'), {
291
355
  cwd: layout.appDir,
292
- env: devEnv.app.env,
356
+ env: appEnv,
293
357
  });
294
358
  }
295
- const appEntry = ['.output/server/index.mjs']
359
+ const appEntry = testAppEntryCandidates()
296
360
  .map((rel) => (0, path_1.join)(layout.appDir, rel))
297
361
  .find((p) => (0, fs_1.existsSync)(p));
298
362
  let appSpawn;
299
363
  if (appBuild === 0 && appEntry) {
300
364
  appSpawn = (0, dev_process_1.spawnDetached)('node', [appEntry], {
301
365
  cwd: layout.appDir,
302
- env: devEnv.app.env,
366
+ env: appEnv,
303
367
  logFile: (0, path_1.join)(layout.root, '.lt-dev', names.appLog),
304
368
  });
305
369
  }
@@ -307,7 +371,7 @@ function bringUpTestSession(layout_1, baseIdentity_1, log_1) {
307
371
  log.warn(`built app not available — falling back to \`${appPm.bin} dev\` (slower: cold-compiles routes).`);
308
372
  appSpawn = (0, dev_process_1.spawnDetached)(appPm.bin, appPm.runScript('dev'), {
309
373
  cwd: layout.appDir,
310
- env: devEnv.app.env,
374
+ env: appEnv,
311
375
  logFile: (0, path_1.join)(layout.root, '.lt-dev', names.appLog),
312
376
  });
313
377
  }
@@ -340,7 +404,7 @@ function bringUpTestSession(layout_1, baseIdentity_1, log_1) {
340
404
  // Expose THIS stack's API log path so the caller can point the auth E2E specs
341
405
  // (via NEST_SERVER_LOG) at the exact isolated log — correct per shard.
342
406
  const apiLogPath = layout.apiDir ? (0, path_1.join)(layout.root, '.lt-dev', names.apiLog) : undefined;
343
- return { apiLogPath, apiUrl, appEnv: devEnv.app.env, appUrl, dbName, pids, testIdentity };
407
+ return { apiLogPath, apiUrl, appEnv, appUrl, dbName, pids, testIdentity };
344
408
  });
345
409
  }
346
410
  /**
@@ -382,6 +446,20 @@ function buildShardPlaywrightInvocation(pm, shardIndex, total, forwarded, htmlRe
382
446
  },
383
447
  };
384
448
  }
449
+ /**
450
+ * The env the test stack's APP process runs with: the isolated dev env plus its
451
+ * own Nuxt build dir.
452
+ *
453
+ * The pin goes LAST on purpose — unlike `TEST_INITIAL_ADMIN_ENV`, which is
454
+ * spread first so a deliberately inherited credential still wins. Here the
455
+ * isolation IS the contract: `buildDevEnv` seeds the app env from `process.env`,
456
+ * so a shell that exports `NUXT_BUILD_DIR` (left over from debugging a check
457
+ * run, say) would otherwise hand the test stack the dev — or the gate — dir
458
+ * straight back, and the collision returns silently.
459
+ */
460
+ function buildTestAppEnv(appEnv) {
461
+ return Object.assign(Object.assign({}, appEnv), { NITRO_OUTPUT_DIR: exports.TEST_NITRO_OUTPUT_DIR, NUXT_BUILD_DIR: exports.TEST_NUXT_BUILD_DIR });
462
+ }
385
463
  /** True when a test session file exists (used by status/down). */
386
464
  function hasTestSession(root) {
387
465
  return (0, dev_state_1.loadSession)(root, dev_state_1.TEST_SESSION_FILE) !== null;
@@ -541,11 +619,45 @@ function tearDownTestSession(layout_1, baseIdentity_1, log_1) {
541
619
  delete reg.projects[testIdentity.slug];
542
620
  (0, dev_state_1.saveRegistry)(reg);
543
621
  }
622
+ // Reclaim the isolated build trees. They exist only for this stack and are
623
+ // rebuilt from scratch on every run (there is no build-skip / reuse), so
624
+ // keeping them buys nothing and costs 37-294 MB per project — permanently,
625
+ // since nothing else ever removes them. Only the SUFFIXED names are touched:
626
+ // a project that does not forward the env vars builds into the shared
627
+ // `.nuxt` / `.output`, which belong to the developer's own `pnpm run build`.
628
+ if (layout.appDir) {
629
+ for (const dir of [exports.TEST_NUXT_BUILD_DIR, exports.TEST_NITRO_OUTPUT_DIR]) {
630
+ const path = (0, path_1.join)(layout.appDir, dir);
631
+ try {
632
+ if ((0, fs_1.existsSync)(path)) {
633
+ (0, fs_1.rmSync)(path, { force: true, recursive: true });
634
+ stopped.push(`${dir}/`);
635
+ }
636
+ }
637
+ catch (_a) {
638
+ // Best effort — a locked build tree must not fail the teardown.
639
+ }
640
+ }
641
+ }
544
642
  if (!opts.silent && stopped.length > 0)
545
643
  log.info(`Stopped test stack: ${stopped.join(', ')}`);
546
644
  return { stopped };
547
645
  });
548
646
  }
647
+ /**
648
+ * Where to look for the built server entry, in priority order.
649
+ *
650
+ * The isolated dir comes FIRST and that ordering is load-bearing: `.find()` takes
651
+ * the first hit and a stale `.output/` from an earlier local build is the normal
652
+ * case, so the shared path first would serve that stale bundle while the fresh
653
+ * build sat unused. The shared path remains as a fallback for projects whose
654
+ * `nuxt.config.ts` does not forward `NITRO_OUTPUT_DIR` yet — without it they
655
+ * would find no entry at all and drop to the slow `pnpm dev` fallback, which is
656
+ * a second `nuxt dev` and re-takes the build-dir lock DEV-2715 just freed.
657
+ */
658
+ function testAppEntryCandidates() {
659
+ return [`${exports.TEST_NITRO_OUTPUT_DIR}/server/index.mjs`, '.output/server/index.mjs'];
660
+ }
549
661
  /**
550
662
  * Resolve the per-stack file/identity names. For a sharded run (`shardIndex`
551
663
  * given) everything gets a `.<i>` / `-<i>` suffix so N stacks coexist without
@@ -587,8 +587,23 @@ function runWithProjectDriver(driverPaths, action, env) {
587
587
  return { outcome: 'failed', stdout: '' };
588
588
  }
589
589
  }
590
- /** Framework-generated / ephemeral paths a dev/build run dirties (never real work). */
591
- const GENERATED_PATHS = /(^|\/)(\.nuxtrc|\.nuxt|\.nitro|\.output|dist|\.turbo|\.cache|\.eslintcache)(\/|$)|\.tsbuildinfo$/;
590
+ /**
591
+ * Framework-generated / ephemeral paths a dev/build run dirties (never real work).
592
+ *
593
+ * `.nuxt` and `.output` carry an optional `-<suffix>`: the build directory is no
594
+ * longer a single well-known name. The check chain builds into `.nuxt-check` and
595
+ * `lt dev test` into `.nuxt-test` / `.output-test`, precisely so they do not
596
+ * collide with a parked `nuxt dev` (the Nuxt lock sits on the build dir).
597
+ *
598
+ * Without the suffix these read as REAL developer work — a glob segment matches
599
+ * whole segments, so `.nuxt` never covered `.nuxt-test` — and
600
+ * `worktreeSafetyReport` then classifies a 300 MB build tree as uncommitted
601
+ * work, making `lt ticket stop` REFUSE to remove the worktree over files the
602
+ * developer never wrote. Only projects whose app `.gitignore` predates the
603
+ * starter's `.nuxt-*` globs are affected, but that is exactly the population the
604
+ * rest of this CLI's heal machinery exists to serve.
605
+ */
606
+ const GENERATED_PATHS = /(^|\/)(\.nuxtrc|\.nuxt(-[\w.]+)?|\.nitro|\.output(-[\w.]+)?|dist|\.turbo|\.cache|\.eslintcache)(\/|$)|\.tsbuildinfo$/;
592
607
  /** The three git-tracked configs `lt dev up` self-heals to be env-aware. */
593
608
  const LT_DEV_MANAGED_CONFIG = /(?:^|\/)(?:config\.env\.ts|nuxt\.config\.ts|playwright\.config\.ts)$/;
594
609
  /**