@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.
- package/build/commands/deployment/create.js +18 -1
- package/build/commands/dev/vscode.js +173 -0
- package/build/commands/fullstack/update.js +17 -0
- package/build/lib/dev-test-session.js +177 -21
- package/build/lib/dev-ticket.js +17 -2
- package/build/lib/heal-vendor-migrate-store.js +285 -0
- package/build/lib/hoist-workspace-pnpm-config.js +59 -2
- package/build/lib/vscode-settings.js +351 -0
- package/docs/LT-ECOSYSTEM-GUIDE.md +1 -1
- package/docs/commands.md +131 -1
- package/package.json +31 -16
|
@@ -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
|
|
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
|
|
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.
|
|
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.
|
|
60
|
+
"@aws-sdk/client-s3": "3.1100.0",
|
|
61
61
|
"@lenne.tech/cli-plugin-helper": "0.0.14",
|
|
62
|
-
"axios": "1.
|
|
62
|
+
"axios": "1.19.0",
|
|
63
63
|
"bcrypt": "6.0.0",
|
|
64
|
-
"defuddle": "0.19.
|
|
64
|
+
"defuddle": "0.19.2",
|
|
65
65
|
"glob": "13.0.6",
|
|
66
66
|
"gluegun": "5.2.2",
|
|
67
|
-
"js-sha256": "0.
|
|
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.
|
|
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.
|
|
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.
|
|
88
|
+
"@types/node": "26.1.2",
|
|
88
89
|
"@types/turndown": "5.0.6",
|
|
89
90
|
"ejs": "6.0.1",
|
|
90
|
-
"eslint": "
|
|
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.
|
|
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.
|
|
100
|
-
"brace-expansion@>=2.0.0 <2.1.
|
|
101
|
-
"brace-expansion@>=5.0.0 <5.0.
|
|
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.
|
|
106
|
-
"brace-expansion@>=2.0.0 <2.1.
|
|
107
|
-
"brace-expansion@>=5.0.0 <5.0.
|
|
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
|
],
|