@lenne.tech/cli 1.41.2 → 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.
@@ -0,0 +1,351 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EXCLUDED_FROM_PROFILE = exports.MEMORY_PROFILE = void 0;
4
+ exports.buildMergedValue = buildMergedValue;
5
+ exports.detectVariants = detectVariants;
6
+ exports.diffProfile = diffProfile;
7
+ exports.formatChange = formatChange;
8
+ exports.isEnablingFlagSet = isEnablingFlagSet;
9
+ exports.isPreventingFlagSet = isPreventingFlagSet;
10
+ exports.selectVariants = selectVariants;
11
+ exports.settingsPathFor = settingsPathFor;
12
+ exports.tuneSettingsFile = tuneSettingsFile;
13
+ const fs_1 = require("fs");
14
+ const os_1 = require("os");
15
+ const path_1 = require("path");
16
+ /**
17
+ * The tuning profile.
18
+ *
19
+ * Every value here was verified against the shipped extension manifests
20
+ * rather than copied from a blog post — three commonly recommended keys are
21
+ * deliberately ABSENT, see `EXCLUDED_FROM_PROFILE`.
22
+ *
23
+ * The exclude globs use `.nuxt*` / `.output*`, not `.nuxt` / `.output`. Both the
24
+ * framework and this CLI spawn SIBLING build directories — `.nuxt-check` for the
25
+ * check chain, `.nuxt-test` / `.output-test` for `lt dev test` — and a glob
26
+ * segment matches whole path segments, so `**\/.nuxt/**` does not cover
27
+ * `.nuxt-test`. A `.output-test` tree measures 37-294 MB; leaving it watched and
28
+ * indexed would undo the very saving this profile exists to make.
29
+ */
30
+ exports.MEMORY_PROFILE = {
31
+ 'files.watcherExclude': {
32
+ reason: 'Stops the file watcher from following build output and dependencies (CPU + memory per window).',
33
+ value: {
34
+ '**/.git/objects/**': true,
35
+ '**/.git/subtree-cache/**': true,
36
+ '**/.nuxt*/**': true,
37
+ '**/.output*/**': true,
38
+ '**/dist/**': true,
39
+ '**/node_modules/**': true,
40
+ },
41
+ },
42
+ 'search.exclude': {
43
+ reason: 'Keeps full-text search from indexing generated trees.',
44
+ value: {
45
+ '**/.nuxt*/**': true,
46
+ '**/.output*/**': true,
47
+ '**/dist/**': true,
48
+ '**/node_modules/**': true,
49
+ },
50
+ },
51
+ 'typescript.disableAutomaticTypeAcquisition': {
52
+ reason: 'Skips the @types download/scan pass — pointless in projects that declare their own types.',
53
+ value: true,
54
+ },
55
+ 'typescript.preferences.includePackageJsonAutoImports': {
56
+ reason: 'Auto-import no longer scans every package.json in the workspace — the single biggest monorepo win.',
57
+ value: 'off',
58
+ },
59
+ 'typescript.tsserver.maxTsServerMemory': {
60
+ reason: 'Lowers the per-server heap ceiling from the 3072 MB default; multiplies across every open root.',
61
+ value: 2048,
62
+ },
63
+ };
64
+ /**
65
+ * Keys intentionally NOT in the profile, with the measurement or manifest
66
+ * check that ruled them out. Surfaced by `lt dev vscode --explain` so the
67
+ * reasoning survives beyond the session that produced it.
68
+ */
69
+ exports.EXCLUDED_FROM_PROFILE = [
70
+ {
71
+ key: 'typescript.tsserver.useSyntaxServer: "never"',
72
+ why: 'Would drop the 2nd server per root, but those measured only ~1.7 GB across 16 roots while costing editor responsiveness.',
73
+ },
74
+ {
75
+ key: 'vue.server.hybridMode',
76
+ why: 'Does not exist in Volar 3.x — hybrid mode is unconditional there. Setting it is a silent no-op.',
77
+ },
78
+ {
79
+ key: 'typescript.tsserver.maxTsServerMemory: 4096+',
80
+ why: 'v8 pointer compression caps the heap near 4 GB, so values above it are ignored (microsoft/vscode#127105).',
81
+ },
82
+ ];
83
+ /**
84
+ * Merge semantics per value type.
85
+ *
86
+ * A plain value is replaced. An object value (the exclude maps) is merged with
87
+ * the user's existing entries winning, so tuning never silently drops a
88
+ * project-specific exclusion someone added by hand.
89
+ *
90
+ * A non-object `before` under an object-valued key (an array, say) is NOT
91
+ * mergeable and is replaced — VS Code's exclude settings are objects, so such a
92
+ * value is already being ignored by VS Code itself. That is the one case where
93
+ * the "never drops" promise above does not hold, and `--revert` cannot restore
94
+ * it either; the `.bak` is the recovery path.
95
+ */
96
+ function buildMergedValue(before, desired) {
97
+ if (!isPlainObject(desired)) {
98
+ return desired;
99
+ }
100
+ if (!isPlainObject(before)) {
101
+ return Object.assign({}, desired);
102
+ }
103
+ return Object.assign(Object.assign({}, desired), before);
104
+ }
105
+ /** Every known variant, flagged by whether its settings file exists. */
106
+ function detectVariants(platform = process.platform, home = (0, os_1.homedir)()) {
107
+ const known = [
108
+ { dir: 'Code', id: 'code', label: 'VS Code' },
109
+ { dir: 'Code - Insiders', id: 'insiders', label: 'VS Code Insiders' },
110
+ { dir: 'Cursor', id: 'cursor', label: 'Cursor' },
111
+ { dir: 'VSCodium', id: 'vscodium', label: 'VSCodium' },
112
+ ];
113
+ return known.map((k) => {
114
+ const settingsPath = settingsPathFor(k.dir, platform, home);
115
+ return { id: k.id, installed: (0, fs_1.existsSync)(settingsPath), label: k.label, settingsPath };
116
+ });
117
+ }
118
+ /**
119
+ * Compare the profile against a parsed settings object.
120
+ *
121
+ * Object-valued keys (the two exclude maps) are treated as SETS of entries in
122
+ * both directions: applying merges (the user's entries win), and reverting
123
+ * subtracts only the entries this profile contributes. Deleting the whole key on
124
+ * revert would take the user's hand-maintained exclusions with it — an undo that
125
+ * destroys data the tool never added is worse than no undo at all.
126
+ *
127
+ * Scalar keys are removed outright on revert, which restores VS Code's own
128
+ * default. An explicit pre-existing scalar (`maxTsServerMemory: 3072`, say) is
129
+ * not restored — recovering that is what the `.bak` is for.
130
+ */
131
+ function diffProfile(current, remove = false) {
132
+ return Object.entries(exports.MEMORY_PROFILE).map(([key, entry]) => {
133
+ const before = current[key];
134
+ const after = remove ? buildRevertedValue(before, entry.value) : buildMergedValue(before, entry.value);
135
+ let action;
136
+ if (remove) {
137
+ action = before === undefined || sameJson(before, after) ? 'unchanged' : 'removed';
138
+ }
139
+ else if (before === undefined) {
140
+ action = 'added';
141
+ }
142
+ else {
143
+ action = sameJson(before, after) ? 'unchanged' : 'changed';
144
+ }
145
+ return { action, after, before, key };
146
+ });
147
+ }
148
+ /**
149
+ * One `key: before → after` line, coloured by what happens to it.
150
+ *
151
+ * Lives here rather than in the command so it can be unit-tested; it takes the
152
+ * colour helpers as an argument and is otherwise pure.
153
+ */
154
+ function formatChange(change, colors) {
155
+ const short = (v) => {
156
+ if (v === undefined) {
157
+ return '—';
158
+ }
159
+ const s = JSON.stringify(v);
160
+ return s.length > 52 ? `${s.slice(0, 49)}…` : s;
161
+ };
162
+ const tag = {
163
+ added: colors.green('+'),
164
+ changed: colors.yellow('~'),
165
+ removed: colors.red('-'),
166
+ unchanged: colors.dim('='),
167
+ }[change.action];
168
+ if (change.action === 'unchanged') {
169
+ return `${tag} ${colors.dim(change.key)} ${colors.dim(short(change.before))}`;
170
+ }
171
+ return `${tag} ${change.key}: ${colors.dim(short(change.before))} → ${short(change.after)}`;
172
+ }
173
+ /**
174
+ * Whether an ENABLING flag (`--revert`, `--explain`) is set.
175
+ *
176
+ * gluegun declares no booleans to yargs-parser, so `--revert=true` arrives as
177
+ * the STRING `'true'`. For a flag that turns something ON, a parse quirk that
178
+ * reads as "not set" fails CLOSED, which is the safe direction.
179
+ */
180
+ function isEnablingFlagSet(value) {
181
+ return value === true || value === 'true';
182
+ }
183
+ /**
184
+ * Whether a PREVENTING flag (`--dry-run`) is set.
185
+ *
186
+ * Deliberately NOT `=== true || === 'true'`. That idiom is exactly backwards for
187
+ * a flag whose job is to STOP a write: `--dry-run=1` parses to the number `1`
188
+ * and `--dry-run=yes` to a string, both of which would read as "not set" and let
189
+ * the write proceed — the failure mode this repo already paid for once with
190
+ * `--keep-db` (see CLAUDE.md). Presence is intent; only an explicit negation
191
+ * proceeds.
192
+ */
193
+ function isPreventingFlagSet(options, ...names) {
194
+ return names.some((name) => {
195
+ if (!(name in options)) {
196
+ return false;
197
+ }
198
+ const v = options[name];
199
+ return v !== false && v !== 'false' && v !== 0 && v !== '0';
200
+ });
201
+ }
202
+ /**
203
+ * Split the detected variants into what to act on, given an optional `--variant`.
204
+ *
205
+ * `unknownFilter` is reported separately from "nothing installed": a bare
206
+ * `--variant` parses to boolean `true` and matches no id, and conflating that
207
+ * with "no editor found" told users VS Code was missing while it was installed.
208
+ */
209
+ function selectVariants(all, filter) {
210
+ const installed = all.filter((v) => v.installed);
211
+ if (filter === undefined || filter === null) {
212
+ return { targets: installed };
213
+ }
214
+ const wanted = String(filter);
215
+ if (!all.some((v) => v.id === wanted)) {
216
+ return { targets: [], unknownFilter: wanted };
217
+ }
218
+ return { targets: installed.filter((v) => v.id === wanted) };
219
+ }
220
+ /**
221
+ * Absolute path to a variant's user-settings file, per platform.
222
+ *
223
+ * All VS Code forks reuse the upstream layout, only the application support
224
+ * directory name differs. The joiner is selected per platform rather than taken
225
+ * from the ambient `path`, so the Windows branch produces real Windows paths
226
+ * (and stays assertable) when called from a test on macOS or Linux.
227
+ */
228
+ function settingsPathFor(dirName, platform = process.platform, home = (0, os_1.homedir)()) {
229
+ if (platform === 'win32') {
230
+ const appData = process.env.APPDATA || path_1.win32.join(home, 'AppData', 'Roaming');
231
+ return path_1.win32.join(appData, dirName, 'User', 'settings.json');
232
+ }
233
+ if (platform === 'darwin') {
234
+ return path_1.posix.join(home, 'Library', 'Application Support', dirName, 'User', 'settings.json');
235
+ }
236
+ return path_1.posix.join(home, '.config', dirName, 'User', 'settings.json');
237
+ }
238
+ /**
239
+ * Apply (or revert) the profile on one settings file.
240
+ *
241
+ * JSONC-safe: the file is edited through `jsonc-parser`, which preserves
242
+ * comments, key order and the user's formatting. A naive
243
+ * `JSON.parse` → `JSON.stringify` round-trip would silently delete every
244
+ * comment in a file people hand-maintain.
245
+ *
246
+ * `jsonc-parser` is required lazily: gluegun eagerly loads every command module
247
+ * on every `lt` invocation, and this is its only consumer, so a top-level import
248
+ * would put the load cost on `lt --version` too. Matches how the repo already
249
+ * treats `open`, `js-yaml`, `playwright-core` and `ts-morph`.
250
+ *
251
+ * Never writes when nothing would change, so re-running is a true no-op. Every
252
+ * failure is returned as `error` rather than thrown — the caller renders it per
253
+ * installation and carries on with the others.
254
+ */
255
+ function tuneSettingsFile(settingsPath, options = {}) {
256
+ var _a;
257
+ const { dryRun = false, remove = false } = options;
258
+ if (!(0, fs_1.existsSync)(settingsPath)) {
259
+ return { changes: [], error: `settings file not found: ${settingsPath}`, settingsPath, written: false };
260
+ }
261
+ // Never write THROUGH a symlink — the target may be any file the user's
262
+ // account can reach, and we were asked to tune settings, not to overwrite it.
263
+ if (isSymbolicLink(settingsPath)) {
264
+ return { changes: [], error: `refusing to write through a symlink: ${settingsPath}`, settingsPath, written: false };
265
+ }
266
+ const { applyEdits, modify, parse, printParseErrorCode } = require('jsonc-parser');
267
+ let raw;
268
+ try {
269
+ raw = (0, fs_1.readFileSync)(settingsPath, 'utf8');
270
+ }
271
+ catch (e) {
272
+ return { changes: [], error: `cannot read ${settingsPath}: ${errText(e)}`, settingsPath, written: false };
273
+ }
274
+ const errors = [];
275
+ const current = ((_a = parse(raw, errors, { allowTrailingComma: true })) !== null && _a !== void 0 ? _a : {});
276
+ // Refuse to touch a file we cannot read reliably — writing into a
277
+ // malformed settings.json risks destroying the user's configuration.
278
+ if (errors.length > 0) {
279
+ const first = errors[0];
280
+ return {
281
+ changes: [],
282
+ error: `cannot parse ${settingsPath}: ${printParseErrorCode(first.error)} at offset ${first.offset}`,
283
+ settingsPath,
284
+ written: false,
285
+ };
286
+ }
287
+ const changes = diffProfile(current, remove);
288
+ const effective = changes.filter((c) => c.action !== 'unchanged');
289
+ if (effective.length === 0 || dryRun) {
290
+ return { changes, settingsPath, written: false };
291
+ }
292
+ let content = raw;
293
+ for (const change of effective) {
294
+ // `after === undefined` deletes the key; an exclude map emptied by the
295
+ // revert subtraction lands here too.
296
+ const edits = modify(content, [change.key], change.after, {
297
+ formattingOptions: { insertSpaces: true, tabSize: 4 },
298
+ });
299
+ content = applyEdits(content, edits);
300
+ }
301
+ // Keep the FIRST backup. It is the only record of what the file looked like
302
+ // before this tool ever touched it — overwriting it on a later run (notably on
303
+ // `--revert`, which would then back up the *tuned* file) destroys exactly the
304
+ // state a user reaching for the backup wants to get back to.
305
+ const backupPath = `${settingsPath}.bak`;
306
+ try {
307
+ if (!(0, fs_1.existsSync)(backupPath)) {
308
+ (0, fs_1.copyFileSync)(settingsPath, backupPath);
309
+ }
310
+ (0, fs_1.writeFileSync)(settingsPath, content, 'utf8');
311
+ }
312
+ catch (e) {
313
+ return { changes, error: `cannot write ${settingsPath}: ${errText(e)}`, settingsPath, written: false };
314
+ }
315
+ return { backupPath, changes, settingsPath, written: true };
316
+ }
317
+ /**
318
+ * The value a key should hold after `--revert`.
319
+ *
320
+ * For an object-valued profile entry: the user's map minus our own entries, or
321
+ * `undefined` when nothing of theirs remains. For anything else: `undefined`
322
+ * (delete the key).
323
+ */
324
+ function buildRevertedValue(before, desired) {
325
+ if (!isPlainObject(desired) || !isPlainObject(before)) {
326
+ return undefined;
327
+ }
328
+ const kept = Object.fromEntries(Object.entries(before).filter(([k]) => !(k in desired)));
329
+ return Object.keys(kept).length > 0 ? kept : undefined;
330
+ }
331
+ /** Message text of an unknown thrown value. */
332
+ function errText(e) {
333
+ return e instanceof Error ? e.message : String(e);
334
+ }
335
+ /** Narrow to a non-null, non-array object. */
336
+ function isPlainObject(value) {
337
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
338
+ }
339
+ /** True when `path` is a symlink (never follows it). */
340
+ function isSymbolicLink(path) {
341
+ try {
342
+ return (0, fs_1.lstatSync)(path).isSymbolicLink();
343
+ }
344
+ catch (_a) {
345
+ return false;
346
+ }
347
+ }
348
+ /** Structural equality via JSON, sufficient for the plain values in the profile. */
349
+ function sameJson(a, b) {
350
+ return JSON.stringify(a) === JSON.stringify(b);
351
+ }
@@ -153,7 +153,7 @@ All code generators are **mode-aware**: In vendor mode they use relative paths t
153
153
  | Command | Purpose |
154
154
  |---------|---------|
155
155
  | `lt fullstack init` | Create fullstack monorepo (see above) |
156
- | `lt fullstack update` | Show mode-specific update instructions for backend AND frontend |
156
+ | `lt fullstack update` | Show mode-specific update instructions **and self-heal generated project scaffolding** (see `docs/commands.md`) |
157
157
  | **`lt fullstack convert-mode`** | **Convert backend AND frontend between npm and vendor mode in a single command** |
158
158
 
159
159
  **Fullstack mode conversion in one command:**
package/docs/commands.md CHANGED
@@ -606,6 +606,46 @@ lt dev doctor
606
606
 
607
607
  ---
608
608
 
609
+ ### `lt dev vscode`
610
+
611
+ Apply a verified low-memory profile to VS Code's **user** settings. Targets the per-workspace TypeScript servers, which dominate memory once several monorepos are open at once: every workspace root spawns its own semantic TS server, so eight monorepos with an `api` and an `app` root each add up to 16 of them.
612
+
613
+ **Usage:**
614
+ ```bash
615
+ lt dev vscode # preview, then apply after confirmation
616
+ lt dev vscode --dry-run # show the diff, write nothing
617
+ lt dev vscode --explain # print the profile with reasons + the keys left out on purpose
618
+ lt dev vscode --revert # remove the profile keys again
619
+ lt dev vscode --variant cursor # limit to one: code | insiders | cursor | vscodium
620
+ lt dev vscode --noConfirm # skip the confirmation prompt
621
+ ```
622
+
623
+ **Alias:** `lt d vsc`
624
+
625
+ **Profile:**
626
+
627
+ | Key | Value | Why |
628
+ |---|---|---|
629
+ | `typescript.tsserver.maxTsServerMemory` | 2048 | Per-server heap ceiling; the default is 3072 and multiplies across every open root |
630
+ | `typescript.preferences.includePackageJsonAutoImports` | `off` | Auto-import stops scanning every package.json — the largest monorepo win |
631
+ | `typescript.disableAutomaticTypeAcquisition` | `true` | Skips the `@types` download/scan pass |
632
+ | `files.watcherExclude` | node_modules, dist, `.nuxt*`, `.output*`, .git internals | Keeps the file watcher off generated trees |
633
+ | `search.exclude` | node_modules, dist, `.nuxt*`, `.output*` | Keeps full-text search from indexing them |
634
+
635
+ The globs are `.nuxt*` / `.output*`, not the bare names: a glob segment matches whole path segments, so `**/.nuxt/**` would leave the sibling build dirs (`.nuxt-check` from the check chain, `.nuxt-test` / `.output-test` from `lt dev test`) watched and indexed — and a `.output-test` tree is 37-294 MB.
636
+
637
+ **Safety:** JSONC-aware via `jsonc-parser`, so comments and formatting in a hand-maintained `settings.json` survive. Refuses to write into a file it cannot parse or a symlink, backs up to `settings.json.bak` (the **first** backup is kept, so a later run — including `--revert` — cannot overwrite the record of the pre-tuning state), and merges the object-valued exclude maps so hand-added entries are kept. Re-running is a true no-op.
638
+
639
+ **`--revert`** subtracts only the entries this profile contributed, so a hand-maintained exclusion is never removed along with them. For the scalar keys it deletes the key, restoring VS Code's own default — an explicit value that preceded the tuning is not restored; that is what the `.bak` is for.
640
+
641
+ **Detects:** VS Code, VS Code Insiders, Cursor, VSCodium — every variant whose user settings file exists.
642
+
643
+ **After applying**, restart VS Code (or run *Developer: Reload Window*) — the TypeScript servers read these settings at startup, so nothing changes until they do.
644
+
645
+ Run `--explain` to see three commonly recommended keys the profile deliberately omits, each with the manifest check or measurement that ruled it out.
646
+
647
+ ---
648
+
609
649
  ### `lt dev test`
610
650
 
611
651
  One-shot E2E wrapper: ensure `up`, wait for the App URL, run `pnpm run test:e2e` with the `.lt-dev/.env` bridge loaded. Optional teardown after.
@@ -649,7 +689,37 @@ lt dev test -- --ui spec.ts # everything after `--` is forwarded to playwri
649
689
  | `LT_DEV_ACTIVE`, `LT_DEV_DB_NAME` | Marker keys for consumers |
650
690
  | `NODE_EXTRA_CA_CERTS` | Path to Caddy's root CA cert (auto-detected) |
651
691
 
652
- `lt dev init` injects a tiny `// >>> lt-dev:bridge >>>` block at the top of `playwright.config.ts` that loads this file at config-load time — making Playwright (CLI, IDE, VS Code extension) automatically use the `lt dev` URLs and trust the local CA, without inheriting the parent shell.
692
+ Additionally, `lt dev test` exports two build-directory keys into the app process it
693
+ spawns. They are **not** written to the bridge file — they scope one run, not the
694
+ project:
695
+
696
+ | Key | Value | Why |
697
+ |-----|-------|-----|
698
+ | `NUXT_BUILD_DIR` | `.nuxt-test` | Nuxt holds its lock on the build dir (`acquireLock(nuxt.options.buildDir)`). Sharing `.nuxt` with a parked `nuxt dev` did not interleave writes — it made the test build **abort** with "Another Nuxt dev is already running", so the app never came up and every spec failed on a missing selector. That reads like broken specs while being pure infrastructure. |
699
+ | `NITRO_OUTPUT_DIR` | `.output-test` | A separate axis: `buildDir` and Nitro's `output.dir` are unrelated knobs. `lt dev test` serves the production bundle, so it rebuilds on every run and used to overwrite the `.output` a local `pnpm run build` was using. |
700
+
701
+ **The project must forward both**, or the isolation silently degrades to the shared
702
+ directories. `nuxt-base-starter` ≥ 2.16.0 does this out of the box:
703
+
704
+ ```ts
705
+ // nuxt.config.ts
706
+ buildDir: process.env.NUXT_BUILD_DIR || '.nuxt',
707
+ nitro: { output: { dir: process.env.NITRO_OUTPUT_DIR || '.output' } },
708
+ ```
709
+
710
+ **Neither key is framework-native**, despite the prefixes — verified against
711
+ `@nuxt/schema`, `nitropack` and `c12`: none of them reads `NUXT_BUILD_DIR` or
712
+ `NITRO_OUTPUT_DIR`. Both levers are opened by the project's own `nuxt.config.ts`,
713
+ which is why the two-line snippet above is required rather than optional. (Singling
714
+ one of them out as "not a framework feature" reads as if the other one were, and a
715
+ reader acting on that forwards only half — leaving exactly the collision this
716
+ section exists to prevent.) For projects that have not adopted it, the CLI keeps a
717
+ `.output/server/index.mjs` fallback when locating the built server.
718
+
719
+ A project that ignores both keys still works; it just loses the isolation, so a
720
+ `lt dev test` run and a parked `nuxt dev` collide again.
721
+
722
+ `lt dev init` injects a tiny `// >>> lt-dev:bridge v2 >>>` block at the top of `playwright.config.ts` that loads this file at config-load time — making Playwright (CLI, IDE, VS Code extension) automatically use the `lt dev` URLs and trust the local CA, without inheriting the parent shell.
653
723
 
654
724
  `lt dev down` removes the bridge file so subsequent runs without `lt dev up` fall back cleanly to the classic `localhost:3000`/`localhost:3001` defaults.
655
725
 
@@ -1127,6 +1197,36 @@ lt fullstack add-app [options]
1127
1197
 
1128
1198
  ---
1129
1199
 
1200
+ ### `lt fullstack update`
1201
+
1202
+ Prints the mode-specific update entry points for backend and frontend — **and repairs generated project scaffolding on the way**. The name undersells it: this command writes files.
1203
+
1204
+ **Usage:**
1205
+ ```bash
1206
+ lt fullstack update
1207
+ ```
1208
+
1209
+ **Self-heals** (each is idempotent and a no-op when nothing is wrong):
1210
+
1211
+ | What | Why it needs healing |
1212
+ |---|---|
1213
+ | `.gitignore` — adds `.lt-dev/` | Added after many projects were scaffolded |
1214
+ | `check` wrapper script | Same |
1215
+ | Vendor `CLAUDE.md` | Same |
1216
+ | `migrations-utils/migrate.js` | Written **once**, at vendor-conversion time. It is project scaffolding, not `src/core/`, so no update path ever revisits it — a project converted before the template stopped requiring `ts-node` unconditionally keeps the broken file forever, and every deployed container then dies with `Cannot find module 'ts-node'` before applying a single migration (silently, because the entrypoint degrades a migration failure to a warning on purpose). |
1217
+
1218
+ The migration-store repair is deliberately narrow. It acts **only** when the `require('./ts-compiler')` is a top-level, unconditional statement — the one shape that provably cannot survive a production image where `ts-node` was pruned. Any conditional form (inside `try`, `if`, a function, a ternary) is the project's own working solution and is left untouched, because the replacement is not behaviour-neutral: the bundled template hardcodes the collection name and takes its URI from `./mongo-uri`, so overwriting a customized store would empty the migration ledger and re-run every historical migration.
1219
+
1220
+ Before overwriting, the command establishes that the change is undoable. A file that git tracks and that is unmodified is simply replaced (git has the copy). A file git cannot recover — untracked, `.gitignore`d, or outside a repo — gets a `.bak` first. A tracked file with **uncommitted** changes is never touched and is reported as skipped:
1221
+
1222
+ ```
1223
+ migrations-utils/migrate.js (skipped: uncommitted changes — commit or discard them, then re-run)
1224
+ ```
1225
+
1226
+ That skip line is the only signal that a repair was needed but not applied — commit or discard, then re-run.
1227
+
1228
+ ---
1229
+
1130
1230
  ### `lt fullstack convert-mode`
1131
1231
 
1132
1232
  Convert **both** backend (`projects/api/`) and frontend (`projects/app/`) of a fullstack monorepo between npm mode and vendor mode in a single command. Auto-detects the subprojects, shows the plan for each side, and orchestrates the backend + frontend conversions sequentially.
@@ -1220,6 +1320,36 @@ them from the TurboOps stage env at runtime, so nothing is patched there):
1220
1320
  `environment.ts` (local dev) is never touched. Only the URL origin is replaced, so
1221
1321
  custom paths (`/v2/graphql`) survive and a re-run with a new domain updates them.
1222
1322
 
1323
+ #### Database host: always stack-prefixed, never bare `mongo`
1324
+
1325
+ The printed checklist spells the DB URI out per stage, and the exact host matters:
1326
+
1327
+ ```
1328
+ NSC__MONGOOSE__URI=mongodb://<user>:<pass>@<project>-production_mongo:27017/<db>?authSource=admin
1329
+ NSC__MONGOOSE__URI=mongodb://<user>:<pass>@<project>-dev_mongo:27017/<db>?authSource=admin
1330
+ ```
1331
+
1332
+ **Never `mongodb://mongo:27017/<db>`.** The project's own `docker-compose.yml` names
1333
+ the service `mongo`, which makes the short name the obvious guess — and the wrong
1334
+ one. TurboOps deploys every stack onto a shared overlay network, where the bare
1335
+ service name is an alias that *every* stack's `mongo` answers to. The connection
1336
+ then lands on a foreign project's database, and on a different one per connection.
1337
+
1338
+ The symptoms do not look like a configuration problem: writes split across two
1339
+ databases, sessions that vanish after a reconnect, files whose bytes are "sometimes"
1340
+ missing. Meanwhile the application's own database sits empty. A project lost a day of
1341
+ debugging to this (DEV-2140) with the application code fully correct.
1342
+
1343
+ TurboOps rejects bare DB hosts and isolates bare-named DB services from the shared
1344
+ overlay since v1.72.0 — but only from that version, and only for stacks deployed
1345
+ after it. The stack-prefixed host is correct either way, so use it unconditionally.
1346
+
1347
+ The prefix fixes **which** database you reach, not **who** may reach it: the overlay
1348
+ network stays shared, so the database credentials are the actual boundary. Give the
1349
+ mongo service a user and password and connect with
1350
+ `mongodb://<user>:<pass>@<project>-<stage>_mongo:27017/<db>?authSource=admin` — an
1351
+ unauthenticated instance is readable and writable by every co-tenant stack.
1352
+
1223
1353
  **Usage:**
1224
1354
  ```bash
1225
1355
  lt deployment create [name] [domain] [options]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lenne.tech/cli",
3
- "version": "1.41.2",
3
+ "version": "1.42.0",
4
4
  "description": "lenne.Tech CLI: lt",
5
5
  "keywords": [
6
6
  "lenne.Tech",
@@ -57,19 +57,20 @@
57
57
  "bin"
58
58
  ],
59
59
  "dependencies": {
60
- "@aws-sdk/client-s3": "3.1090.0",
60
+ "@aws-sdk/client-s3": "3.1100.0",
61
61
  "@lenne.tech/cli-plugin-helper": "0.0.14",
62
- "axios": "1.18.1",
62
+ "axios": "1.19.0",
63
63
  "bcrypt": "6.0.0",
64
- "defuddle": "0.19.1",
64
+ "defuddle": "0.19.2",
65
65
  "glob": "13.0.6",
66
66
  "gluegun": "5.2.2",
67
- "js-sha256": "0.11.1",
67
+ "js-sha256": "1.0.0",
68
68
  "js-yaml": "4.3.0",
69
69
  "jsdom": "29.1.1",
70
+ "jsonc-parser": "3.3.1",
70
71
  "lodash": "4.18.1",
71
72
  "open": "11.0.0",
72
- "playwright-core": "1.61.1",
73
+ "playwright-core": "1.62.1",
73
74
  "ts-morph": "28.0.0",
74
75
  "ts-node": "10.9.2",
75
76
  "turndown": "7.2.4",
@@ -77,39 +78,53 @@
77
78
  "typescript": "6.0.3"
78
79
  },
79
80
  "devDependencies": {
80
- "@lenne.tech/eslint-config-ts": "2.1.4",
81
+ "@lenne.tech/eslint-config-ts": "2.3.0",
81
82
  "@lenne.tech/npm-package-helper": "0.0.12",
82
83
  "@types/ejs": "3.1.5",
83
84
  "@types/jest": "30.0.0",
84
85
  "@types/js-yaml": "4.0.9",
85
86
  "@types/jsdom": "28.0.1",
86
87
  "@types/lodash": "4.17.24",
87
- "@types/node": "26.1.1",
88
+ "@types/node": "26.1.2",
88
89
  "@types/turndown": "5.0.6",
89
90
  "ejs": "6.0.1",
90
- "eslint": "9.39.4",
91
+ "eslint": "10.8.0",
91
92
  "husky": "9.1.7",
92
93
  "jest": "30.4.2",
94
+ "minimatch": "10.2.6",
93
95
  "prettier": "3.8.3",
94
96
  "rimraf": "6.1.3",
95
- "ts-jest": "29.4.11"
97
+ "ts-jest": "29.4.12"
96
98
  },
97
99
  "//overrides": {
98
100
  "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.",
99
- "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.",
100
- "brace-expansion@>=2.0.0 <2.1.2": "Same advisory, 2.x line.",
101
- "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."
101
+ "brace-expansion@<1.1.18": "Two DoS advisories: exponential-time expansion of consecutive non-expanding {} groups (GHSA-3jxr-9vmj-r5cp, high) and unbounded expansion length causing an OOM crash (GHSA-mh99-v99m-4gvg, high). Transitive via dotgitignore/eslint/fs-jetpack/glob/test-exclude/ts-morph > 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.",
102
+ "brace-expansion@>=2.0.0 <2.1.4": "Same advisories, 2.x line.",
103
+ "brace-expansion@>=5.0.0 <5.0.9": "Same advisories, 5.x line. Floored at >=5.0.0 so a future 3.x/4.x dependency is not silently forced across two majors.",
104
+ "//minimatch-note": "brace-expansion 1.x and 2.x are END OF LINE for GHSA-mh99-v99m-4gvg: the advisory range is <=5.0.7 across ALL majors and upstream patched ONLY 5.0.8+. 1.1.18 and 2.1.4 are the newest releases of their majors and remain vulnerable. Forcing brace-expansion 5.x globally is NOT an option: 5.x is ESM/tshy and exports an object ({ expand, ... }) while 1.x/2.x export the function itself, so minimatch 3.x/5.x would die on `expand is not a function`. The only real fix is to raise each CONSUMER off minimatch 3.x/5.x, which is what the scoped entries below do. Each was verified against the consumer's actual call site before being added. The eslint / @eslint/eslintrc / @eslint/config-array entries were REMOVED in 1.42.0: eslint 10 requests minimatch ^10.2.5 itself, config-array ^10.2.4, and eslintrc is no longer in the tree at all.",
105
+ "minimatch@>=4 <10": "Bounded raise of every minimatch 4.x-9.x install to 10.2.6, whose brace-expansion ^5.0.8 is patched. Covers filelist (gluegun > ejs > jake > filelist, PRODUCTION-reachable, minimatch ^5.0.1), @typescript-eslint/typescript-estree (^9.0.4) and jest-config/jest-runtime/@jest/reporters (^9). Verified safe per consumer: filelist calls only the static helper `minimatch.match(files, pat, opts)`; typescript-estree calls the named `minimatch(filePath, pattern, { dot: true })`; the three jest packages declare minimatch but never require it. Floored at >=4 so the 3.x line is NOT swept in: minimatch 3.x is callable while 9.x/10.x export an object with __esModule but no `default` key, so sweeping it would break any consumer that calls the default export. Drop once these consumers request minimatch >=10 themselves.",
106
+ "babel-plugin-istanbul > test-exclude@<8": "test-exclude 6.0.0 pins minimatch ^3.0.4 and glob ^7. test-exclude 8.0.0 uses minimatch ^10.2.2 + glob ^13 (the glob major this project already ships) and is still CJS with the same `module.exports = TestExclude` class shape, so babel-plugin-istanbul's _interopRequireDefault + `new TestExclude(opts)` keeps working. Drop once babel-plugin-istanbul widens its ^6.0.0 range.",
107
+ "fs-jetpack > minimatch@<10": "Same chain, PRODUCTION-reachable (gluegun > fs-jetpack). Safe: lib/utils/matcher.js does `require('minimatch').Minimatch` and uses only `new Minimatch(pattern, { matchBase, nocomment, nocase, dot })`, `.negate` and `.match()` - all verified against 10.x. Not fixable by upgrading fs-jetpack: gluegun@5.2.2 is the latest release and fs-jetpack@5.1.0 still requests minimatch ^5.1.0 (brace-expansion ^2.0.1, also unpatched)."
102
108
  },
103
109
  "overrides": {
104
110
  "semver@*": "7.8.5",
105
- "brace-expansion@<1.1.16": "1.1.16",
106
- "brace-expansion@>=2.0.0 <2.1.2": "2.1.2",
107
- "brace-expansion@>=5.0.0 <5.0.7": "5.0.7"
111
+ "brace-expansion@<1.1.18": "1.1.18",
112
+ "brace-expansion@>=2.0.0 <2.1.4": "2.1.4",
113
+ "brace-expansion@>=5.0.0 <5.0.9": "5.0.9",
114
+ "fs-jetpack": {
115
+ "minimatch@<10": "10.2.6"
116
+ },
117
+ "babel-plugin-istanbul": {
118
+ "test-exclude@<8": "8.0.0"
119
+ },
120
+ "minimatch@>=4 <10": "10.2.6"
108
121
  },
122
+ "//jest.workerIdleMemoryLimit": "Recycle a ts-jest worker once its heap passes this. Guards against an intermittent `A jest worker process was terminated by another process: signal=SIGSEGV` that kills ONE suite while every other test passes (seen twice in marketplace*, both under `npm run check`, never reproducible on demand - 0 in 9 targeted runs incl. --maxWorkers=16). It is a worker crash, NOT an assertion failure: the signature is `Test suite failed to run` with the remaining count still green. All versions are current and in-range (node 24 / jest 30 / ts-jest 29 / ts 6), so this is a resilience measure against unbounded worker heap growth, not a proven root-cause fix - if it recurs, capture the suite name and re-open.",
109
123
  "jest": {
110
124
  "testEnvironment": "node",
111
125
  "rootDir": "__tests__",
112
126
  "testTimeout": 60000,
127
+ "workerIdleMemoryLimit": "512MB",
113
128
  "testMatch": [
114
129
  "<rootDir>/*.test.ts"
115
130
  ],