@lenne.tech/cli 1.41.3 → 1.43.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/add-api.js +6 -0
- package/build/commands/fullstack/add-app.js +7 -0
- package/build/commands/fullstack/init.js +32 -3
- package/build/commands/fullstack/update.js +17 -0
- package/build/commands/git/reset.js +1 -1
- package/build/commands/git/update.js +2 -2
- package/build/extensions/frontend-helper.js +19 -0
- package/build/extensions/git.js +23 -2
- package/build/extensions/server.js +28 -3
- package/build/lib/adopt-upstream-build-allowlist.js +140 -0
- package/build/lib/dev-test-session.js +119 -7
- package/build/lib/dev-ticket.js +17 -2
- package/build/lib/fail-run.js +38 -0
- package/build/lib/heal-vendor-migrate-store.js +285 -0
- package/build/lib/hoist-workspace-pnpm-config.js +230 -5
- package/build/lib/strip-vendor-schema-augmentation.js +213 -0
- package/build/lib/vendor-claude-md.js +15 -0
- package/build/lib/vscode-settings.js +351 -0
- package/docs/LT-ECOSYSTEM-GUIDE.md +1 -1
- package/docs/VENDOR-MODE-WORKFLOW.md +35 -0
- package/docs/commands.md +140 -1
- package/package.json +36 -17
|
@@ -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
|
-
|
|
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 });
|
|
@@ -10,6 +10,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.help = void 0;
|
|
13
|
+
const fail_run_1 = require("../../lib/fail-run");
|
|
13
14
|
const workspace_integration_1 = require("../../lib/workspace-integration");
|
|
14
15
|
/**
|
|
15
16
|
* Add an API (`projects/api/`) to a fullstack workspace that currently
|
|
@@ -68,6 +69,7 @@ const NewCommand = {
|
|
|
68
69
|
info('Add API to fullstack workspace');
|
|
69
70
|
toolbox.tools.nonInteractiveHint('lt fullstack add-api --api-mode <Rest|GraphQL|Both> --framework-mode <npm|vendor> [--api-branch <ref>] [--next] [--dry-run] --noConfirm');
|
|
70
71
|
if (!(yield git.gitInstalled())) {
|
|
72
|
+
(0, fail_run_1.failRun)(toolbox);
|
|
71
73
|
return;
|
|
72
74
|
}
|
|
73
75
|
const ltConfig = config.loadConfig();
|
|
@@ -126,10 +128,12 @@ const NewCommand = {
|
|
|
126
128
|
const layout = (0, workspace_integration_1.detectWorkspaceLayout)(workspaceDir, filesystem);
|
|
127
129
|
if (!layout.hasWorkspace) {
|
|
128
130
|
error(`No fullstack workspace detected at "${workspaceDir}". Expected pnpm-workspace.yaml, package.json#workspaces, or a projects/ directory. Use \`lt fullstack init\` for a fresh workspace.`);
|
|
131
|
+
(0, fail_run_1.failRun)(toolbox);
|
|
129
132
|
return;
|
|
130
133
|
}
|
|
131
134
|
if (layout.hasApi) {
|
|
132
135
|
error(`An API already exists at "${workspaceDir}/projects/api". Remove it first or use \`lt fullstack init\` in a fresh directory.`);
|
|
136
|
+
(0, fail_run_1.failRun)(toolbox);
|
|
133
137
|
return;
|
|
134
138
|
}
|
|
135
139
|
// Resolve api mode (CLI > experimental override > config > global > interactive/default).
|
|
@@ -177,6 +181,7 @@ const NewCommand = {
|
|
|
177
181
|
}
|
|
178
182
|
else if (cliFrameworkMode) {
|
|
179
183
|
error(`Invalid --framework-mode value "${cliFrameworkMode}". Use "npm" or "vendor".`);
|
|
184
|
+
(0, fail_run_1.failRun)(toolbox);
|
|
180
185
|
return;
|
|
181
186
|
}
|
|
182
187
|
else if (configFrameworkMode === 'npm' || configFrameworkMode === 'vendor') {
|
|
@@ -282,6 +287,7 @@ const NewCommand = {
|
|
|
282
287
|
});
|
|
283
288
|
if (!apiResult.success) {
|
|
284
289
|
apiSpinner.fail(`Failed to set up API: ${apiResult.path}`);
|
|
290
|
+
(0, fail_run_1.failRun)(toolbox);
|
|
285
291
|
return;
|
|
286
292
|
}
|
|
287
293
|
apiSpinner.succeed(`API integrated (${apiResult.method})`);
|
|
@@ -10,6 +10,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.help = void 0;
|
|
13
|
+
const fail_run_1 = require("../../lib/fail-run");
|
|
13
14
|
const workspace_integration_1 = require("../../lib/workspace-integration");
|
|
14
15
|
/**
|
|
15
16
|
* Add a frontend app (`projects/app/`) to a fullstack workspace that
|
|
@@ -57,6 +58,7 @@ const NewCommand = {
|
|
|
57
58
|
info('Add app to fullstack workspace');
|
|
58
59
|
toolbox.tools.nonInteractiveHint('lt fullstack add-app --frontend <nuxt|angular> [--frontend-branch <ref>] [--next] [--dry-run] --noConfirm');
|
|
59
60
|
if (!(yield git.gitInstalled())) {
|
|
61
|
+
(0, fail_run_1.failRun)(toolbox);
|
|
60
62
|
return;
|
|
61
63
|
}
|
|
62
64
|
const ltConfig = config.loadConfig();
|
|
@@ -110,10 +112,12 @@ const NewCommand = {
|
|
|
110
112
|
const layout = (0, workspace_integration_1.detectWorkspaceLayout)(workspaceDir, filesystem);
|
|
111
113
|
if (!layout.hasWorkspace) {
|
|
112
114
|
error(`No fullstack workspace detected at "${workspaceDir}". Expected pnpm-workspace.yaml, package.json#workspaces, or a projects/ directory. Use \`lt fullstack init\` for a fresh workspace.`);
|
|
115
|
+
(0, fail_run_1.failRun)(toolbox);
|
|
113
116
|
return;
|
|
114
117
|
}
|
|
115
118
|
if (layout.hasApp) {
|
|
116
119
|
error(`An app already exists at "${workspaceDir}/projects/app". Remove it first or use \`lt fullstack init\` in a fresh directory.`);
|
|
120
|
+
(0, fail_run_1.failRun)(toolbox);
|
|
117
121
|
return;
|
|
118
122
|
}
|
|
119
123
|
// Resolve frontend.
|
|
@@ -123,6 +127,7 @@ const NewCommand = {
|
|
|
123
127
|
}
|
|
124
128
|
else if (cliFrontend) {
|
|
125
129
|
error('Invalid --frontend option. Use "angular" or "nuxt".');
|
|
130
|
+
(0, fail_run_1.failRun)(toolbox);
|
|
126
131
|
return;
|
|
127
132
|
}
|
|
128
133
|
else if (configFrontend === 'angular' || configFrontend === 'nuxt') {
|
|
@@ -150,6 +155,7 @@ const NewCommand = {
|
|
|
150
155
|
}
|
|
151
156
|
else if (cliFrontendFrameworkMode) {
|
|
152
157
|
error(`Invalid --frontend-framework-mode value "${cliFrontendFrameworkMode}". Use "npm" or "vendor".`);
|
|
158
|
+
(0, fail_run_1.failRun)(toolbox);
|
|
153
159
|
return;
|
|
154
160
|
}
|
|
155
161
|
else if (configFrontendFrameworkMode === 'npm' || configFrontendFrameworkMode === 'vendor') {
|
|
@@ -225,6 +231,7 @@ const NewCommand = {
|
|
|
225
231
|
});
|
|
226
232
|
if (!result.success) {
|
|
227
233
|
appSpinner.fail(`Failed to set up ${frontend} frontend: ${result.path}`);
|
|
234
|
+
(0, fail_run_1.failRun)(toolbox);
|
|
228
235
|
return;
|
|
229
236
|
}
|
|
230
237
|
appSpinner.succeed(`${frontend} integrated (${result.method})`);
|
|
@@ -16,6 +16,7 @@ const gluegun_1 = require("gluegun");
|
|
|
16
16
|
const caddy_1 = require("../../lib/caddy");
|
|
17
17
|
const dev_migrate_helper_1 = require("../../lib/dev-migrate-helper");
|
|
18
18
|
const dev_project_1 = require("../../lib/dev-project");
|
|
19
|
+
const fail_run_1 = require("../../lib/fail-run");
|
|
19
20
|
const package_name_1 = require("../../lib/package-name");
|
|
20
21
|
const vendor_claude_md_1 = require("../../lib/vendor-claude-md");
|
|
21
22
|
const workspace_integration_1 = require("../../lib/workspace-integration");
|
|
@@ -38,9 +39,10 @@ const NewCommand = {
|
|
|
38
39
|
// Info
|
|
39
40
|
info('Create a new fullstack workspace');
|
|
40
41
|
// Hint for non-interactive callers (e.g. Claude Code)
|
|
41
|
-
toolbox.tools.nonInteractiveHint('lt fullstack init --name <name> --frontend <nuxt|angular> --api-mode <Rest|GraphQL|Both> --framework-mode <npm|vendor> [--framework-upstream-branch <ref>] [--next: implies nuxt-base-starter#next unless --frontend-branch overrides] [--dry-run] --noConfirm');
|
|
42
|
+
toolbox.tools.nonInteractiveHint('lt fullstack init --name <name> --frontend <nuxt|angular> --api-mode <Rest|GraphQL|Both> --framework-mode <npm|vendor: workspace-wide, applies to API and frontend> [--frontend-framework-mode <npm|vendor>: overrides --framework-mode for the frontend only] [--framework-upstream-branch <ref>] [--next: implies nuxt-base-starter#next unless --frontend-branch overrides] [--dry-run] --noConfirm');
|
|
42
43
|
// Check git
|
|
43
44
|
if (!(yield git.gitInstalled())) {
|
|
45
|
+
(0, fail_run_1.failRun)(toolbox);
|
|
44
46
|
return;
|
|
45
47
|
}
|
|
46
48
|
// Load configuration
|
|
@@ -92,6 +94,7 @@ const NewCommand = {
|
|
|
92
94
|
if (cwdLayout.hasApi && cwdLayout.hasApp) {
|
|
93
95
|
error('Workspace already has both projects/api and projects/app — nothing to add.');
|
|
94
96
|
info('Use `lt fullstack add-api --help-json` or `lt fullstack add-app --help-json` to inspect options.');
|
|
97
|
+
(0, fail_run_1.failRun)(toolbox);
|
|
95
98
|
return;
|
|
96
99
|
}
|
|
97
100
|
if (cwdLayout.hasApp && !cwdLayout.hasApi) {
|
|
@@ -124,6 +127,7 @@ const NewCommand = {
|
|
|
124
127
|
if (filesystem.exists(projectDir)) {
|
|
125
128
|
info('');
|
|
126
129
|
error(`There's already a folder named "${projectDir}" here.`);
|
|
130
|
+
(0, fail_run_1.failRun)(toolbox);
|
|
127
131
|
return;
|
|
128
132
|
}
|
|
129
133
|
// Determine frontend with priority: CLI > config > interactive
|
|
@@ -132,6 +136,7 @@ const NewCommand = {
|
|
|
132
136
|
frontend = cliFrontend === 'angular' ? 'angular' : cliFrontend === 'nuxt' ? 'nuxt' : null;
|
|
133
137
|
if (!frontend) {
|
|
134
138
|
error('Invalid frontend option. Use "angular" or "nuxt".');
|
|
139
|
+
(0, fail_run_1.failRun)(toolbox);
|
|
135
140
|
return;
|
|
136
141
|
}
|
|
137
142
|
}
|
|
@@ -220,6 +225,7 @@ const NewCommand = {
|
|
|
220
225
|
}
|
|
221
226
|
else if (cliFrameworkMode) {
|
|
222
227
|
error(`Invalid --framework-mode value "${cliFrameworkMode}". Use "npm" or "vendor".`);
|
|
228
|
+
(0, fail_run_1.failRun)(toolbox);
|
|
223
229
|
return;
|
|
224
230
|
}
|
|
225
231
|
else if (configFrameworkMode === 'npm' || configFrameworkMode === 'vendor') {
|
|
@@ -245,20 +251,34 @@ const NewCommand = {
|
|
|
245
251
|
}
|
|
246
252
|
// ── Frontend framework mode ─────────────────────────────────────────
|
|
247
253
|
const configFrontendFrameworkMode = (_0 = (_z = ltConfig === null || ltConfig === void 0 ? void 0 : ltConfig.commands) === null || _z === void 0 ? void 0 : _z.fullstack) === null || _0 === void 0 ? void 0 : _0.frontendFrameworkMode;
|
|
254
|
+
// Precedence: frontend-specific CLI flag > workspace-wide CLI flag >
|
|
255
|
+
// frontend-specific config > workspace-wide config > vendor default.
|
|
256
|
+
//
|
|
257
|
+
// `--framework-mode` is the workspace-wide default and MUST propagate here.
|
|
258
|
+
// Without that inheritance, `--framework-mode npm` silently produced an
|
|
259
|
+
// npm API next to a vendored frontend — visible only as the M1..M3 steps in
|
|
260
|
+
// a `--dry-run` plan, or afterwards as an unexpected `app/core/` tree.
|
|
261
|
+
// Consumers who genuinely want mixed modes pass `--frontend-framework-mode`.
|
|
248
262
|
let frontendFrameworkMode;
|
|
249
263
|
if (cliFrontendFrameworkMode === 'npm' || cliFrontendFrameworkMode === 'vendor') {
|
|
250
264
|
frontendFrameworkMode = cliFrontendFrameworkMode;
|
|
251
265
|
}
|
|
252
266
|
else if (cliFrontendFrameworkMode) {
|
|
253
267
|
error(`Invalid --frontend-framework-mode value "${cliFrontendFrameworkMode}". Use "npm" or "vendor".`);
|
|
268
|
+
(0, fail_run_1.failRun)(toolbox);
|
|
254
269
|
return;
|
|
255
270
|
}
|
|
271
|
+
else if (cliFrameworkMode === 'npm' || cliFrameworkMode === 'vendor') {
|
|
272
|
+
frontendFrameworkMode = cliFrameworkMode;
|
|
273
|
+
info(`Using frontend framework mode from --framework-mode: ${frontendFrameworkMode}`);
|
|
274
|
+
}
|
|
256
275
|
else if (configFrontendFrameworkMode === 'npm' || configFrontendFrameworkMode === 'vendor') {
|
|
257
276
|
frontendFrameworkMode = configFrontendFrameworkMode;
|
|
258
277
|
info(`Using frontend framework mode from lt.config: ${frontendFrameworkMode}`);
|
|
259
278
|
}
|
|
260
|
-
else if (
|
|
261
|
-
frontendFrameworkMode =
|
|
279
|
+
else if (configFrameworkMode === 'npm' || configFrameworkMode === 'vendor') {
|
|
280
|
+
frontendFrameworkMode = configFrameworkMode;
|
|
281
|
+
info(`Using frontend framework mode from lt.config frameworkMode: ${frontendFrameworkMode}`);
|
|
262
282
|
}
|
|
263
283
|
else {
|
|
264
284
|
// Default to vendor without asking (unless user sets it explicitly)
|
|
@@ -403,11 +423,13 @@ const NewCommand = {
|
|
|
403
423
|
}
|
|
404
424
|
catch (err) {
|
|
405
425
|
workspaceSpinner.fail(`Failed to clone monorepo: ${err.message}`);
|
|
426
|
+
(0, fail_run_1.failRun)(toolbox);
|
|
406
427
|
return;
|
|
407
428
|
}
|
|
408
429
|
// Check for directory
|
|
409
430
|
if (!filesystem.isDirectory(`./${projectDir}`)) {
|
|
410
431
|
workspaceSpinner.fail(`The directory "${projectDir}" could not be created.`);
|
|
432
|
+
(0, fail_run_1.failRun)(toolbox);
|
|
411
433
|
return;
|
|
412
434
|
}
|
|
413
435
|
workspaceSpinner.succeed(`Create fullstack workspace with ${frontend} in ${projectDir} for ${name} created`);
|
|
@@ -467,6 +489,7 @@ const NewCommand = {
|
|
|
467
489
|
}
|
|
468
490
|
catch (err) {
|
|
469
491
|
error(`Failed to initialize git: ${err.message}`);
|
|
492
|
+
(0, fail_run_1.failRun)(toolbox);
|
|
470
493
|
return;
|
|
471
494
|
}
|
|
472
495
|
// Add remote if push is configured
|
|
@@ -476,6 +499,7 @@ const NewCommand = {
|
|
|
476
499
|
}
|
|
477
500
|
catch (err) {
|
|
478
501
|
error(`Failed to add remote: ${err.message}`);
|
|
502
|
+
(0, fail_run_1.failRun)(toolbox);
|
|
479
503
|
return;
|
|
480
504
|
}
|
|
481
505
|
}
|
|
@@ -503,6 +527,7 @@ const NewCommand = {
|
|
|
503
527
|
}
|
|
504
528
|
if (!frontendResult.success) {
|
|
505
529
|
error(`Failed to set up ${frontend} frontend: ${frontendResult.path}`);
|
|
530
|
+
(0, fail_run_1.failRun)(toolbox);
|
|
506
531
|
return;
|
|
507
532
|
}
|
|
508
533
|
// Patch frontend .env with project-specific values (skip for linked templates)
|
|
@@ -546,6 +571,7 @@ const NewCommand = {
|
|
|
546
571
|
});
|
|
547
572
|
if (!apiResult.success) {
|
|
548
573
|
serverSpinner.fail(`Failed to set up API: ${apiResult.path}`);
|
|
574
|
+
(0, fail_run_1.failRun)(toolbox);
|
|
549
575
|
return;
|
|
550
576
|
}
|
|
551
577
|
// Auto-run `bun run rename <projectDir>` for the experimental nest-base
|
|
@@ -638,6 +664,7 @@ const NewCommand = {
|
|
|
638
664
|
}
|
|
639
665
|
catch (err) {
|
|
640
666
|
installSpinner.fail(`Failed to install packages: ${err.message}`);
|
|
667
|
+
(0, fail_run_1.failRun)(toolbox);
|
|
641
668
|
return;
|
|
642
669
|
}
|
|
643
670
|
}
|
|
@@ -673,6 +700,7 @@ const NewCommand = {
|
|
|
673
700
|
}
|
|
674
701
|
catch (err) {
|
|
675
702
|
error(`Failed to create initial commit: ${err.message}`);
|
|
703
|
+
(0, fail_run_1.failRun)(toolbox);
|
|
676
704
|
return;
|
|
677
705
|
}
|
|
678
706
|
// Push to remote if configured
|
|
@@ -682,6 +710,7 @@ const NewCommand = {
|
|
|
682
710
|
}
|
|
683
711
|
catch (err) {
|
|
684
712
|
error(`Failed to push to remote: ${err.message}`);
|
|
713
|
+
(0, fail_run_1.failRun)(toolbox);
|
|
685
714
|
return;
|
|
686
715
|
}
|
|
687
716
|
}
|
|
@@ -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('');
|
|
@@ -46,7 +46,7 @@ const NewCommand = {
|
|
|
46
46
|
return;
|
|
47
47
|
}
|
|
48
48
|
// Check remote (use short SSH timeout so ls-remote doesn't hang in offline environments)
|
|
49
|
-
const remoteBranch = yield system.run(`GIT_TERMINAL_PROMPT=0 GIT_SSH_COMMAND="ssh -o ConnectTimeout=5 -o BatchMode=yes" git ls-remote --heads origin ${branch} 2>/dev/null || true`);
|
|
49
|
+
const remoteBranch = yield system.run(`GIT_TERMINAL_PROMPT=0 GIT_SSH_COMMAND="\${GIT_SSH_COMMAND:-ssh -o ConnectTimeout=5 -o BatchMode=yes}" git ls-remote --heads origin ${branch} 2>/dev/null || true`);
|
|
50
50
|
if (!remoteBranch) {
|
|
51
51
|
error(`No remote branch ${branch} found!`);
|
|
52
52
|
return;
|
|
@@ -48,7 +48,7 @@ const NewCommand = {
|
|
|
48
48
|
info(`Current branch: ${branch}`);
|
|
49
49
|
info('');
|
|
50
50
|
// Fetch to see incoming changes (use short SSH timeout so it doesn't hang offline)
|
|
51
|
-
yield run('GIT_TERMINAL_PROMPT=0 GIT_SSH_COMMAND="ssh -o ConnectTimeout=5 -o BatchMode=yes" git fetch 2>/dev/null || true');
|
|
51
|
+
yield run('GIT_TERMINAL_PROMPT=0 GIT_SSH_COMMAND="${GIT_SSH_COMMAND:-ssh -o ConnectTimeout=5 -o BatchMode=yes}" git fetch 2>/dev/null || true');
|
|
52
52
|
// Check for incoming commits
|
|
53
53
|
const incomingCommits = yield run(`git log ${branch}..origin/${branch} --oneline 2>/dev/null || echo ""`);
|
|
54
54
|
const commits = (incomingCommits === null || incomingCommits === void 0 ? void 0 : incomingCommits.trim().split('\n').filter((c) => c)) || [];
|
|
@@ -78,7 +78,7 @@ const NewCommand = {
|
|
|
78
78
|
const timer = startTimer();
|
|
79
79
|
// Update
|
|
80
80
|
const updateSpin = spin(`Update branch ${branch}`);
|
|
81
|
-
yield run('GIT_TERMINAL_PROMPT=0 GIT_SSH_COMMAND="ssh -o ConnectTimeout=5 -o BatchMode=yes" git fetch 2>/dev/null || true && GIT_TERMINAL_PROMPT=0 GIT_SSH_COMMAND="ssh -o ConnectTimeout=5 -o BatchMode=yes" git pull --rebase');
|
|
81
|
+
yield run('GIT_TERMINAL_PROMPT=0 GIT_SSH_COMMAND="${GIT_SSH_COMMAND:-ssh -o ConnectTimeout=5 -o BatchMode=yes}" git fetch 2>/dev/null || true && GIT_TERMINAL_PROMPT=0 GIT_SSH_COMMAND="${GIT_SSH_COMMAND:-ssh -o ConnectTimeout=5 -o BatchMode=yes}" git pull --rebase');
|
|
82
82
|
updateSpin.succeed();
|
|
83
83
|
// Install packages (unless skipped) with correctly detected package manager (supports monorepo lockfiles)
|
|
84
84
|
if (!skipInstall) {
|
|
@@ -13,6 +13,7 @@ exports.FrontendHelper = void 0;
|
|
|
13
13
|
const check_freshness_hooks_1 = require("../lib/check-freshness-hooks");
|
|
14
14
|
const markdown_table_1 = require("../lib/markdown-table");
|
|
15
15
|
const strip_comments_1 = require("../lib/strip-comments");
|
|
16
|
+
const strip_vendor_schema_augmentation_1 = require("../lib/strip-vendor-schema-augmentation");
|
|
16
17
|
const vendor_claude_md_1 = require("../lib/vendor-claude-md");
|
|
17
18
|
/**
|
|
18
19
|
* Frontend helper functions for project scaffolding
|
|
@@ -520,6 +521,24 @@ class FrontendHelper {
|
|
|
520
521
|
this.rewriteConsumerImportsToVendor(dest);
|
|
521
522
|
// ── 4. Rewrite nuxt.config.ts module entry ──────────────────────────
|
|
522
523
|
this.rewriteNuxtConfig(dest, 'vendor');
|
|
524
|
+
// ── 4b. Drop the core's `nuxt/schema` runtime-config augmentation ────
|
|
525
|
+
//
|
|
526
|
+
// Harmless inside node_modules, poisonous as project source: it augments the
|
|
527
|
+
// same interface under both `nuxt/schema` and `@nuxt/schema` (the former
|
|
528
|
+
// re-exports the latter) and closes a cycle with Nuxt's generated
|
|
529
|
+
// runtime-config types. TS2310 — suppressed by `skipLibCheck`, so all a
|
|
530
|
+
// developer sees is every `config.public.*` typed `unknown`. See the lib for
|
|
531
|
+
// the measurement.
|
|
532
|
+
const strippedAugmentation = (0, strip_vendor_schema_augmentation_1.stripVendorSchemaAugmentation)({ coreDir, filesystem });
|
|
533
|
+
if (strippedAugmentation.touched.length > 0) {
|
|
534
|
+
this.toolbox.print.info(` vendored core: removed the nuxt/schema runtime-config augmentation from ${strippedAugmentation.touched.length} file(s) — keeps config.public.* typed`);
|
|
535
|
+
}
|
|
536
|
+
// A block the transform could not process is left in place, which means the
|
|
537
|
+
// TS2310 bug ships with the project. Silence there would be the worst of both
|
|
538
|
+
// worlds: the conversion reports success and the typing is broken anyway.
|
|
539
|
+
for (const warning of strippedAugmentation.warnings) {
|
|
540
|
+
this.toolbox.print.warning(` ⚠ vendored core: ${warning}`);
|
|
541
|
+
}
|
|
523
542
|
// ── 5. package.json: remove @lenne.tech/nuxt-extensions, merge deps ─
|
|
524
543
|
const pkgPath = path.join(dest, 'package.json');
|
|
525
544
|
if (filesystem.exists(pkgPath)) {
|
package/build/extensions/git.js
CHANGED
|
@@ -120,7 +120,7 @@ class Git {
|
|
|
120
120
|
// Toolbox features
|
|
121
121
|
const { system } = this.toolbox;
|
|
122
122
|
// Get branches (use short SSH timeout so fetch doesn't hang in offline environments)
|
|
123
|
-
const branches = yield system.run('GIT_TERMINAL_PROMPT=0 GIT_SSH_COMMAND="ssh -o ConnectTimeout=5 -o BatchMode=yes" git fetch 2>/dev/null; git show-branch --list');
|
|
123
|
+
const branches = yield system.run('GIT_TERMINAL_PROMPT=0 GIT_SSH_COMMAND="${GIT_SSH_COMMAND:-ssh -o ConnectTimeout=5 -o BatchMode=yes}" git fetch 2>/dev/null; git show-branch --list');
|
|
124
124
|
branches.split('\n').forEach((item) => {
|
|
125
125
|
const matches = item.match(/\[(.*?)]/);
|
|
126
126
|
if (matches) {
|
|
@@ -197,6 +197,27 @@ class Git {
|
|
|
197
197
|
/**
|
|
198
198
|
* Check if git is installed (cached for performance)
|
|
199
199
|
*/
|
|
200
|
+
/**
|
|
201
|
+
* Why the git calls in this repo spell `GIT_SSH_COMMAND="\${GIT_SSH_COMMAND:-…}"` (shell default)
|
|
202
|
+
* rather than assigning it outright.
|
|
203
|
+
*
|
|
204
|
+
* These commands do a best-effort `git fetch` to see whether the branch is
|
|
205
|
+
* behind. They set `BatchMode=yes` so ssh fails instead of PROMPTING — but an
|
|
206
|
+
* agent that stalls is not a prompt. On a 1Password-backed machine the agent is
|
|
207
|
+
* reachable and every signature needs an interactive approval; unattended it
|
|
208
|
+
* waits and then reports `communication with agent failed`. Measured: **61 s per
|
|
209
|
+
* fetch**, so `lt git update --dry-run` took 62 s and `lt git create --dry-run`
|
|
210
|
+
* 123 s (two fetches). `ConnectTimeout` does not bound it — that covers the TCP
|
|
211
|
+
* connect, not the agent.
|
|
212
|
+
*
|
|
213
|
+
* Waiting for a human to approve a key is legitimate for an interactive command,
|
|
214
|
+
* so the default is unchanged. What was wrong is that the assignment was
|
|
215
|
+
* UNCONDITIONAL: it overrode a caller who had deliberately configured ssh,
|
|
216
|
+
* including a test harness trying to make the behaviour deterministic. The
|
|
217
|
+
* `:-` default respects an existing value and keeps the old behaviour when there
|
|
218
|
+
* is none. `IdentityAgent=none` in the caller's env then drops the same fetch to
|
|
219
|
+
* ~1 s with a clean `Permission denied (publickey)`.
|
|
220
|
+
*/
|
|
200
221
|
gitInstalled() {
|
|
201
222
|
return __awaiter(this, void 0, void 0, function* () {
|
|
202
223
|
// Return cached result if available
|
|
@@ -252,7 +273,7 @@ class Git {
|
|
|
252
273
|
searchSpin = spin(opts.spinText);
|
|
253
274
|
}
|
|
254
275
|
// Update infos (use short SSH timeout so fetch doesn't hang in offline environments)
|
|
255
|
-
const fetch = yield system.run('GIT_TERMINAL_PROMPT=0 GIT_SSH_COMMAND="ssh -o ConnectTimeout=5 -o BatchMode=yes" git fetch 2>/dev/null || true');
|
|
276
|
+
const fetch = yield system.run('GIT_TERMINAL_PROMPT=0 GIT_SSH_COMMAND="${GIT_SSH_COMMAND:-ssh -o ConnectTimeout=5 -o BatchMode=yes}" git fetch 2>/dev/null || true');
|
|
256
277
|
if (fetch.length && !fetch.startsWith('remote')) {
|
|
257
278
|
info(`Could not update infos ${fetch.length}`);
|
|
258
279
|
}
|