@lenne.tech/cli 1.9.6 → 1.11.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/README.md +88 -3
- package/build/commands/config/validate.js +2 -0
- package/build/commands/frontend/convert-mode.js +198 -0
- package/build/commands/fullstack/convert-mode.js +368 -0
- package/build/commands/fullstack/init.js +150 -4
- package/build/commands/fullstack/update.js +177 -0
- package/build/commands/server/add-property.js +29 -2
- package/build/commands/server/convert-mode.js +197 -0
- package/build/commands/server/create.js +41 -3
- package/build/commands/server/module.js +58 -25
- package/build/commands/server/object.js +26 -5
- package/build/commands/server/permissions.js +20 -6
- package/build/commands/server/test.js +7 -1
- package/build/commands/status.js +94 -3
- package/build/config/vendor-frontend-runtime-deps.json +4 -0
- package/build/config/vendor-runtime-deps.json +9 -0
- package/build/extensions/api-mode.js +19 -3
- package/build/extensions/frontend-helper.js +652 -0
- package/build/extensions/server.js +1475 -3
- package/build/lib/framework-detection.js +167 -0
- package/build/lib/frontend-framework-detection.js +129 -0
- package/build/templates/nest-server-module/inputs/template-create.input.ts.ejs +1 -1
- package/build/templates/nest-server-module/inputs/template.input.ts.ejs +1 -1
- package/build/templates/nest-server-module/outputs/template-fac-result.output.ts.ejs +1 -1
- package/build/templates/nest-server-module/template.controller.ts.ejs +1 -1
- package/build/templates/nest-server-module/template.model.ts.ejs +1 -1
- package/build/templates/nest-server-module/template.module.ts.ejs +1 -1
- package/build/templates/nest-server-module/template.resolver.ts.ejs +1 -1
- package/build/templates/nest-server-module/template.service.ts.ejs +1 -1
- package/build/templates/nest-server-object/template-create.input.ts.ejs +1 -1
- package/build/templates/nest-server-object/template.input.ts.ejs +1 -1
- package/build/templates/nest-server-object/template.object.ts.ejs +1 -1
- package/build/templates/nest-server-tests/tests.e2e-spec.ts.ejs +1 -1
- package/docs/LT-ECOSYSTEM-GUIDE.md +973 -0
- package/docs/VENDOR-MODE-WORKFLOW.md +471 -0
- package/docs/commands.md +196 -0
- package/docs/lt.config.md +9 -7
- package/package.json +17 -8
|
@@ -0,0 +1,368 @@
|
|
|
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
|
+
const path_1 = require("path");
|
|
13
|
+
const framework_detection_1 = require("../../lib/framework-detection");
|
|
14
|
+
const frontend_framework_detection_1 = require("../../lib/frontend-framework-detection");
|
|
15
|
+
/**
|
|
16
|
+
* Convert both backend and frontend of a fullstack monorepo between
|
|
17
|
+
* npm mode and vendor mode in a single command.
|
|
18
|
+
*
|
|
19
|
+
* Usage:
|
|
20
|
+
* lt fullstack convert-mode --to vendor [--framework-upstream-branch 11.24.3] [--frontend-framework-upstream-branch 1.5.3]
|
|
21
|
+
* lt fullstack convert-mode --to npm
|
|
22
|
+
* lt fullstack convert-mode --to vendor --skip-frontend
|
|
23
|
+
* lt fullstack convert-mode --to vendor --skip-backend
|
|
24
|
+
* lt fullstack convert-mode --to vendor --dry-run
|
|
25
|
+
*
|
|
26
|
+
* Orchestrates `lt server convert-mode` + `lt frontend convert-mode` with
|
|
27
|
+
* auto-detection of `projects/api/` and `projects/app/` subdirectories.
|
|
28
|
+
*/
|
|
29
|
+
const ConvertModeCommand = {
|
|
30
|
+
description: 'Convert fullstack monorepo between npm and vendor modes',
|
|
31
|
+
hidden: false,
|
|
32
|
+
name: 'convert-mode',
|
|
33
|
+
run: (toolbox) => __awaiter(void 0, void 0, void 0, function* () {
|
|
34
|
+
const { filesystem, frontendHelper, parameters, print: { colors, error, info, spin, success, warning }, prompt: { confirm }, server, } = toolbox;
|
|
35
|
+
// Handle --help-json flag
|
|
36
|
+
if (toolbox.tools.helpJson({
|
|
37
|
+
description: 'Convert fullstack monorepo (backend + frontend) between npm and vendor modes',
|
|
38
|
+
name: 'convert-mode',
|
|
39
|
+
options: [
|
|
40
|
+
{
|
|
41
|
+
description: 'Target mode',
|
|
42
|
+
flag: '--to',
|
|
43
|
+
required: true,
|
|
44
|
+
type: 'string',
|
|
45
|
+
values: ['vendor', 'npm'],
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
description: 'Backend upstream branch/tag (only with --to vendor, e.g. "11.24.3")',
|
|
49
|
+
flag: '--framework-upstream-branch',
|
|
50
|
+
required: false,
|
|
51
|
+
type: 'string',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
description: 'Frontend upstream branch/tag (only with --to vendor, e.g. "1.5.3")',
|
|
55
|
+
flag: '--frontend-framework-upstream-branch',
|
|
56
|
+
required: false,
|
|
57
|
+
type: 'string',
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
description: 'Backend version to install (only with --to npm, default: from VENDOR.md baseline)',
|
|
61
|
+
flag: '--framework-version',
|
|
62
|
+
required: false,
|
|
63
|
+
type: 'string',
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
description: 'Frontend version to install (only with --to npm, default: from VENDOR.md baseline)',
|
|
67
|
+
flag: '--frontend-framework-version',
|
|
68
|
+
required: false,
|
|
69
|
+
type: 'string',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
default: false,
|
|
73
|
+
description: 'Skip backend conversion',
|
|
74
|
+
flag: '--skip-backend',
|
|
75
|
+
required: false,
|
|
76
|
+
type: 'boolean',
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
default: false,
|
|
80
|
+
description: 'Skip frontend conversion',
|
|
81
|
+
flag: '--skip-frontend',
|
|
82
|
+
required: false,
|
|
83
|
+
type: 'boolean',
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
default: false,
|
|
87
|
+
description: 'Show the resolved plan without making any changes',
|
|
88
|
+
flag: '--dry-run',
|
|
89
|
+
required: false,
|
|
90
|
+
type: 'boolean',
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
default: false,
|
|
94
|
+
description: 'Skip confirmation prompt',
|
|
95
|
+
flag: '--noConfirm',
|
|
96
|
+
required: false,
|
|
97
|
+
type: 'boolean',
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
})) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const targetMode = parameters.options.to;
|
|
104
|
+
if (!targetMode || !['npm', 'vendor'].includes(targetMode)) {
|
|
105
|
+
error('Missing or invalid --to flag. Use: --to vendor or --to npm');
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const skipBackend = parameters.options['skip-backend'] === true || parameters.options['skip-backend'] === 'true';
|
|
109
|
+
const skipFrontend = parameters.options['skip-frontend'] === true || parameters.options['skip-frontend'] === 'true';
|
|
110
|
+
const dryRun = parameters.options['dry-run'] === true || parameters.options['dry-run'] === 'true';
|
|
111
|
+
const noConfirm = parameters.options.noConfirm === true || parameters.options.noConfirm === 'true';
|
|
112
|
+
if (skipBackend && skipFrontend) {
|
|
113
|
+
error('Cannot skip both backend and frontend — nothing would happen.');
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
// Locate the subprojects. Typical monorepo layouts: projects/{api,app} or packages/{api,app}.
|
|
117
|
+
const cwd = filesystem.cwd();
|
|
118
|
+
const backendCandidates = [(0, path_1.join)(cwd, 'projects', 'api'), (0, path_1.join)(cwd, 'packages', 'api')];
|
|
119
|
+
const frontendCandidates = [(0, path_1.join)(cwd, 'projects', 'app'), (0, path_1.join)(cwd, 'packages', 'app')];
|
|
120
|
+
let backendDir;
|
|
121
|
+
for (const candidate of backendCandidates) {
|
|
122
|
+
if (filesystem.exists((0, path_1.join)(candidate, 'package.json'))) {
|
|
123
|
+
backendDir = candidate;
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
let frontendDir;
|
|
128
|
+
for (const candidate of frontendCandidates) {
|
|
129
|
+
if (filesystem.exists((0, path_1.join)(candidate, 'package.json'))) {
|
|
130
|
+
frontendDir = candidate;
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (!backendDir && !frontendDir) {
|
|
135
|
+
error('Could not find any api or app subproject. Expected projects/api, projects/app, packages/api, or packages/app.');
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
// Detect current modes
|
|
139
|
+
const backendCurrentMode = backendDir ? (0, framework_detection_1.detectFrameworkMode)(backendDir) : null;
|
|
140
|
+
const frontendCurrentMode = frontendDir ? (0, frontend_framework_detection_1.detectFrontendFrameworkMode)(frontendDir) : null;
|
|
141
|
+
// Decide what will actually happen
|
|
142
|
+
const willConvertBackend = !skipBackend && backendDir && backendCurrentMode && backendCurrentMode !== targetMode;
|
|
143
|
+
const willConvertFrontend = !skipFrontend && frontendDir && frontendCurrentMode && frontendCurrentMode !== targetMode;
|
|
144
|
+
// ── Plan output ─────────────────────────────────────────────────────
|
|
145
|
+
info('');
|
|
146
|
+
info(colors.bold('Fullstack convert-mode plan:'));
|
|
147
|
+
info(colors.dim('─'.repeat(60)));
|
|
148
|
+
if (backendDir) {
|
|
149
|
+
const backendLabel = skipBackend
|
|
150
|
+
? colors.yellow('(skipped via --skip-backend)')
|
|
151
|
+
: backendCurrentMode === targetMode
|
|
152
|
+
? colors.dim(`(already in ${targetMode} mode — nothing to do)`)
|
|
153
|
+
: colors.green(`${backendCurrentMode} → ${targetMode}`);
|
|
154
|
+
info(` Backend: ${backendDir}`);
|
|
155
|
+
info(` ${backendLabel}`);
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
info(` Backend: ${colors.dim('(not found)')}`);
|
|
159
|
+
}
|
|
160
|
+
if (frontendDir) {
|
|
161
|
+
const frontendLabel = skipFrontend
|
|
162
|
+
? colors.yellow('(skipped via --skip-frontend)')
|
|
163
|
+
: frontendCurrentMode === targetMode
|
|
164
|
+
? colors.dim(`(already in ${targetMode} mode — nothing to do)`)
|
|
165
|
+
: colors.green(`${frontendCurrentMode} → ${targetMode}`);
|
|
166
|
+
info(` Frontend: ${frontendDir}`);
|
|
167
|
+
info(` ${frontendLabel}`);
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
info(` Frontend: ${colors.dim('(not found)')}`);
|
|
171
|
+
}
|
|
172
|
+
info(colors.dim('─'.repeat(60)));
|
|
173
|
+
if (!willConvertBackend && !willConvertFrontend) {
|
|
174
|
+
info('');
|
|
175
|
+
warning('Nothing to do: both subprojects are already in the target mode (or skipped).');
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (dryRun) {
|
|
179
|
+
info('');
|
|
180
|
+
info(colors.bold('Would execute:'));
|
|
181
|
+
if (willConvertBackend) {
|
|
182
|
+
if (targetMode === 'vendor') {
|
|
183
|
+
const branch = parameters.options['framework-upstream-branch'] || '(auto-detect from package.json)';
|
|
184
|
+
info(` [Backend] lt server convert-mode --to vendor --upstream-branch ${branch} --noConfirm`);
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
const version = parameters.options['framework-version'] || '(from VENDOR.md baseline)';
|
|
188
|
+
info(` [Backend] lt server convert-mode --to npm --version ${version} --noConfirm`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (willConvertFrontend) {
|
|
192
|
+
if (targetMode === 'vendor') {
|
|
193
|
+
const branch = parameters.options['frontend-framework-upstream-branch'] || '(auto-detect from package.json)';
|
|
194
|
+
info(` [Frontend] lt frontend convert-mode --to vendor --upstream-branch ${branch} --noConfirm`);
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
const version = parameters.options['frontend-framework-version'] || '(from VENDOR.md baseline)';
|
|
198
|
+
info(` [Frontend] lt frontend convert-mode --to npm --version ${version} --noConfirm`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
info('');
|
|
202
|
+
return `fullstack convert-mode dry-run (target: ${targetMode})`;
|
|
203
|
+
}
|
|
204
|
+
// ── Confirmation ────────────────────────────────────────────────────
|
|
205
|
+
if (!noConfirm) {
|
|
206
|
+
info('');
|
|
207
|
+
const proceed = yield confirm(`Convert ${[willConvertBackend && 'backend', willConvertFrontend && 'frontend'].filter(Boolean).join(' + ')} to ${targetMode} mode?`);
|
|
208
|
+
if (!proceed) {
|
|
209
|
+
info('Aborted.');
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// ── Execute ─────────────────────────────────────────────────────────
|
|
214
|
+
const results = [];
|
|
215
|
+
// 1. Backend conversion
|
|
216
|
+
if (willConvertBackend && backendDir) {
|
|
217
|
+
info('');
|
|
218
|
+
info(colors.bold(`[1/2] Backend: ${backendCurrentMode} → ${targetMode}`));
|
|
219
|
+
if (targetMode === 'vendor') {
|
|
220
|
+
let branch = parameters.options['framework-upstream-branch'];
|
|
221
|
+
// Auto-detect version from package.json if not provided
|
|
222
|
+
if (!branch) {
|
|
223
|
+
try {
|
|
224
|
+
const pkg = filesystem.read(`${backendDir}/package.json`, 'json');
|
|
225
|
+
const deps = Object.assign(Object.assign({}, ((pkg === null || pkg === void 0 ? void 0 : pkg.dependencies) || {})), ((pkg === null || pkg === void 0 ? void 0 : pkg.devDependencies) || {}));
|
|
226
|
+
const version = deps['@lenne.tech/nest-server'];
|
|
227
|
+
if (version) {
|
|
228
|
+
branch = version.replace(/^[^0-9]*/, '');
|
|
229
|
+
info(` Auto-detected @lenne.tech/nest-server version: ${branch}`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
catch (_a) {
|
|
233
|
+
// Will use HEAD
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
const spinner = spin(' Converting backend to vendor mode...');
|
|
237
|
+
try {
|
|
238
|
+
yield server.convertToVendorMode({
|
|
239
|
+
dest: backendDir,
|
|
240
|
+
upstreamBranch: branch,
|
|
241
|
+
});
|
|
242
|
+
spinner.succeed(' Backend converted to vendor mode');
|
|
243
|
+
results.push({ part: 'backend', status: 'ok' });
|
|
244
|
+
}
|
|
245
|
+
catch (err) {
|
|
246
|
+
spinner.fail(` Backend conversion failed: ${err.message}`);
|
|
247
|
+
results.push({ message: err.message, part: 'backend', status: 'failed' });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
const targetVersion = parameters.options['framework-version'];
|
|
252
|
+
const spinner = spin(' Converting backend to npm mode...');
|
|
253
|
+
try {
|
|
254
|
+
yield server.convertToNpmMode({
|
|
255
|
+
dest: backendDir,
|
|
256
|
+
targetVersion,
|
|
257
|
+
});
|
|
258
|
+
spinner.succeed(' Backend converted to npm mode');
|
|
259
|
+
results.push({ part: 'backend', status: 'ok' });
|
|
260
|
+
}
|
|
261
|
+
catch (err) {
|
|
262
|
+
spinner.fail(` Backend conversion failed: ${err.message}`);
|
|
263
|
+
results.push({ message: err.message, part: 'backend', status: 'failed' });
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
else if (backendDir) {
|
|
268
|
+
const reason = skipBackend ? 'skipped via flag' : `already in ${targetMode} mode`;
|
|
269
|
+
info('');
|
|
270
|
+
info(colors.dim(`[1/2] Backend: ${reason}`));
|
|
271
|
+
results.push({ message: reason, part: 'backend', status: 'skipped' });
|
|
272
|
+
}
|
|
273
|
+
// 2. Frontend conversion
|
|
274
|
+
if (willConvertFrontend && frontendDir) {
|
|
275
|
+
info('');
|
|
276
|
+
info(colors.bold(`[2/2] Frontend: ${frontendCurrentMode} → ${targetMode}`));
|
|
277
|
+
if (targetMode === 'vendor') {
|
|
278
|
+
let branch = parameters.options['frontend-framework-upstream-branch'];
|
|
279
|
+
if (!branch) {
|
|
280
|
+
try {
|
|
281
|
+
const pkg = filesystem.read(`${frontendDir}/package.json`, 'json');
|
|
282
|
+
const deps = Object.assign(Object.assign({}, ((pkg === null || pkg === void 0 ? void 0 : pkg.dependencies) || {})), ((pkg === null || pkg === void 0 ? void 0 : pkg.devDependencies) || {}));
|
|
283
|
+
const version = deps['@lenne.tech/nuxt-extensions'];
|
|
284
|
+
if (version) {
|
|
285
|
+
branch = version.replace(/^[^0-9]*/, '');
|
|
286
|
+
info(` Auto-detected @lenne.tech/nuxt-extensions version: ${branch}`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
catch (_b) {
|
|
290
|
+
// Will use HEAD
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
const spinner = spin(' Converting frontend to vendor mode...');
|
|
294
|
+
try {
|
|
295
|
+
yield frontendHelper.convertAppToVendorMode({
|
|
296
|
+
dest: frontendDir,
|
|
297
|
+
upstreamBranch: branch,
|
|
298
|
+
});
|
|
299
|
+
spinner.succeed(' Frontend converted to vendor mode');
|
|
300
|
+
results.push({ part: 'frontend', status: 'ok' });
|
|
301
|
+
}
|
|
302
|
+
catch (err) {
|
|
303
|
+
spinner.fail(` Frontend conversion failed: ${err.message}`);
|
|
304
|
+
results.push({ message: err.message, part: 'frontend', status: 'failed' });
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
const targetVersion = parameters.options['frontend-framework-version'];
|
|
309
|
+
const spinner = spin(' Converting frontend to npm mode...');
|
|
310
|
+
try {
|
|
311
|
+
yield frontendHelper.convertAppToNpmMode({
|
|
312
|
+
dest: frontendDir,
|
|
313
|
+
targetVersion,
|
|
314
|
+
});
|
|
315
|
+
spinner.succeed(' Frontend converted to npm mode');
|
|
316
|
+
results.push({ part: 'frontend', status: 'ok' });
|
|
317
|
+
}
|
|
318
|
+
catch (err) {
|
|
319
|
+
spinner.fail(` Frontend conversion failed: ${err.message}`);
|
|
320
|
+
results.push({ message: err.message, part: 'frontend', status: 'failed' });
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
else if (frontendDir) {
|
|
325
|
+
const reason = skipFrontend ? 'skipped via flag' : `already in ${targetMode} mode`;
|
|
326
|
+
info('');
|
|
327
|
+
info(colors.dim(`[2/2] Frontend: ${reason}`));
|
|
328
|
+
results.push({ message: reason, part: 'frontend', status: 'skipped' });
|
|
329
|
+
}
|
|
330
|
+
// ── Summary ─────────────────────────────────────────────────────────
|
|
331
|
+
info('');
|
|
332
|
+
info(colors.bold('Summary:'));
|
|
333
|
+
info(colors.dim('─'.repeat(60)));
|
|
334
|
+
for (const result of results) {
|
|
335
|
+
const icon = result.status === 'ok' ? colors.green('✓') : result.status === 'skipped' ? colors.dim('–') : colors.red('✗');
|
|
336
|
+
const label = result.part.padEnd(10);
|
|
337
|
+
info(` ${icon} ${label} ${result.message || result.status}`);
|
|
338
|
+
}
|
|
339
|
+
info(colors.dim('─'.repeat(60)));
|
|
340
|
+
const failed = results.filter((r) => r.status === 'failed');
|
|
341
|
+
if (failed.length > 0) {
|
|
342
|
+
info('');
|
|
343
|
+
error(`${failed.length} conversion(s) failed. See messages above.`);
|
|
344
|
+
if (!parameters.options.fromGluegunMenu) {
|
|
345
|
+
process.exit(1);
|
|
346
|
+
}
|
|
347
|
+
return `fullstack convert-mode failed (${failed.length} error(s))`;
|
|
348
|
+
}
|
|
349
|
+
info('');
|
|
350
|
+
success('All conversions completed successfully.');
|
|
351
|
+
info('');
|
|
352
|
+
info(colors.bold('Next steps:'));
|
|
353
|
+
info(' 1. Run: pnpm install (from monorepo root)');
|
|
354
|
+
if (backendDir && results.find((r) => r.part === 'backend' && r.status === 'ok')) {
|
|
355
|
+
info(' 2. Verify backend: cd projects/api && pnpm exec tsc --noEmit && pnpm test');
|
|
356
|
+
}
|
|
357
|
+
if (frontendDir && results.find((r) => r.part === 'frontend' && r.status === 'ok')) {
|
|
358
|
+
info(' 3. Verify frontend: cd projects/app && pnpm run build');
|
|
359
|
+
}
|
|
360
|
+
info(' 4. Commit the changes');
|
|
361
|
+
info('');
|
|
362
|
+
if (!parameters.options.fromGluegunMenu) {
|
|
363
|
+
process.exit();
|
|
364
|
+
}
|
|
365
|
+
return `fullstack convert-mode completed (target: ${targetMode})`;
|
|
366
|
+
}),
|
|
367
|
+
};
|
|
368
|
+
exports.default = ConvertModeCommand;
|
|
@@ -18,7 +18,7 @@ const NewCommand = {
|
|
|
18
18
|
hidden: false,
|
|
19
19
|
name: 'init',
|
|
20
20
|
run: (toolbox) => __awaiter(void 0, void 0, void 0, function* () {
|
|
21
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w;
|
|
21
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0;
|
|
22
22
|
// Retrieve the tools we need
|
|
23
23
|
const { config, filesystem, frontendHelper, git, helper, parameters, patching, print: { error, info, spin, success }, prompt: { ask, confirm }, server, strings: { kebabCase }, system, } = toolbox;
|
|
24
24
|
// Start timer
|
|
@@ -26,7 +26,7 @@ const NewCommand = {
|
|
|
26
26
|
// Info
|
|
27
27
|
info('Create a new fullstack workspace');
|
|
28
28
|
// Hint for non-interactive callers (e.g. Claude Code)
|
|
29
|
-
toolbox.tools.nonInteractiveHint('lt fullstack init --name <name> --frontend <nuxt|angular> --api-mode <Rest|GraphQL|Both> --noConfirm');
|
|
29
|
+
toolbox.tools.nonInteractiveHint('lt fullstack init --name <name> --frontend <nuxt|angular> --api-mode <Rest|GraphQL|Both> --framework-mode <npm|vendor> [--framework-upstream-branch <ref>] [--dry-run] --noConfirm');
|
|
30
30
|
// Check git
|
|
31
31
|
if (!(yield git.gitInstalled())) {
|
|
32
32
|
return;
|
|
@@ -43,12 +43,17 @@ const NewCommand = {
|
|
|
43
43
|
const configFrontendCopy = (_r = (_q = ltConfig === null || ltConfig === void 0 ? void 0 : ltConfig.commands) === null || _q === void 0 ? void 0 : _q.fullstack) === null || _r === void 0 ? void 0 : _r.frontendCopy;
|
|
44
44
|
const configApiLink = (_t = (_s = ltConfig === null || ltConfig === void 0 ? void 0 : ltConfig.commands) === null || _s === void 0 ? void 0 : _s.fullstack) === null || _t === void 0 ? void 0 : _t.apiLink;
|
|
45
45
|
const configFrontendLink = (_v = (_u = ltConfig === null || ltConfig === void 0 ? void 0 : ltConfig.commands) === null || _u === void 0 ? void 0 : _u.fullstack) === null || _v === void 0 ? void 0 : _v.frontendLink;
|
|
46
|
+
const configFrameworkMode = (_x = (_w = ltConfig === null || ltConfig === void 0 ? void 0 : ltConfig.commands) === null || _w === void 0 ? void 0 : _w.fullstack) === null || _x === void 0 ? void 0 : _x.frameworkMode;
|
|
46
47
|
// Parse CLI arguments
|
|
47
|
-
const { 'api-branch': cliApiBranch, 'api-copy': cliApiCopy, 'api-link': cliApiLink, 'api-mode': cliApiMode, frontend: cliFrontend, 'frontend-branch': cliFrontendBranch, 'frontend-copy': cliFrontendCopy, 'frontend-link': cliFrontendLink, git: cliGit, 'git-link': cliGitLink, name: cliName, } = parameters.options;
|
|
48
|
+
const { 'api-branch': cliApiBranch, 'api-copy': cliApiCopy, 'api-link': cliApiLink, 'api-mode': cliApiMode, 'dry-run': cliDryRun, 'framework-mode': cliFrameworkMode, 'framework-upstream-branch': cliFrameworkUpstreamBranch, frontend: cliFrontend, 'frontend-branch': cliFrontendBranch, 'frontend-copy': cliFrontendCopy, 'frontend-framework-mode': cliFrontendFrameworkMode, 'frontend-link': cliFrontendLink, git: cliGit, 'git-link': cliGitLink, name: cliName, } = parameters.options;
|
|
49
|
+
const dryRun = cliDryRun === true || cliDryRun === 'true';
|
|
50
|
+
const frameworkUpstreamBranch = typeof cliFrameworkUpstreamBranch === 'string' && cliFrameworkUpstreamBranch.length > 0
|
|
51
|
+
? cliFrameworkUpstreamBranch
|
|
52
|
+
: undefined;
|
|
48
53
|
// Determine noConfirm with priority: CLI > command > parent > global > default
|
|
49
54
|
const noConfirm = config.getNoConfirm({
|
|
50
55
|
cliValue: parameters.options.noConfirm,
|
|
51
|
-
commandConfig: (
|
|
56
|
+
commandConfig: (_y = ltConfig === null || ltConfig === void 0 ? void 0 : ltConfig.commands) === null || _y === void 0 ? void 0 : _y.fullstack,
|
|
52
57
|
config: ltConfig,
|
|
53
58
|
});
|
|
54
59
|
// Get name of the workspace
|
|
@@ -132,6 +137,71 @@ const NewCommand = {
|
|
|
132
137
|
});
|
|
133
138
|
apiMode = apiModeChoice.apiMode.split(' - ')[0];
|
|
134
139
|
}
|
|
140
|
+
// Determine framework-consumption mode (npm vs vendored)
|
|
141
|
+
//
|
|
142
|
+
// npm — classic: @lenne.tech/nest-server is an npm dependency. Framework
|
|
143
|
+
// source lives in node_modules/@lenne.tech/nest-server. Backend is
|
|
144
|
+
// cloned from nest-server-starter. Updates via
|
|
145
|
+
// `/lt-dev:backend:update-nest-server`.
|
|
146
|
+
//
|
|
147
|
+
// vendor — pilot: the framework's core/ directory is copied directly into
|
|
148
|
+
// projects/api/src/core/ as first-class project code. No npm
|
|
149
|
+
// dependency. Backend is cloned from the nest-server framework repo
|
|
150
|
+
// itself and stripped of framework-internal content. Updates via
|
|
151
|
+
// `/lt-dev:backend:update-nest-server-core`; local patches are logged
|
|
152
|
+
// in src/core/VENDOR.md.
|
|
153
|
+
//
|
|
154
|
+
// Default is still 'npm' until the vendoring pilot is fully evaluated.
|
|
155
|
+
let frameworkMode;
|
|
156
|
+
if (cliFrameworkMode === 'npm' || cliFrameworkMode === 'vendor') {
|
|
157
|
+
frameworkMode = cliFrameworkMode;
|
|
158
|
+
}
|
|
159
|
+
else if (cliFrameworkMode) {
|
|
160
|
+
error(`Invalid --framework-mode value "${cliFrameworkMode}". Use "npm" or "vendor".`);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
else if (configFrameworkMode === 'npm' || configFrameworkMode === 'vendor') {
|
|
164
|
+
frameworkMode = configFrameworkMode;
|
|
165
|
+
info(`Using framework mode from lt.config: ${frameworkMode}`);
|
|
166
|
+
}
|
|
167
|
+
else if (noConfirm) {
|
|
168
|
+
frameworkMode = 'npm';
|
|
169
|
+
info('Using default framework mode: npm (noConfirm mode)');
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
const frameworkModeChoice = yield ask({
|
|
173
|
+
choices: [
|
|
174
|
+
'npm - @lenne.tech/nest-server as npm dependency (classic, stable)',
|
|
175
|
+
'vendor - framework core vendored into projects/api/src/core/ (pilot, allows local patches)',
|
|
176
|
+
],
|
|
177
|
+
initial: 0,
|
|
178
|
+
message: 'Framework consumption mode?',
|
|
179
|
+
name: 'frameworkMode',
|
|
180
|
+
type: 'select',
|
|
181
|
+
});
|
|
182
|
+
frameworkMode = frameworkModeChoice.frameworkMode.startsWith('vendor') ? 'vendor' : 'npm';
|
|
183
|
+
}
|
|
184
|
+
// ── Frontend framework mode ─────────────────────────────────────────
|
|
185
|
+
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;
|
|
186
|
+
let frontendFrameworkMode;
|
|
187
|
+
if (cliFrontendFrameworkMode === 'npm' || cliFrontendFrameworkMode === 'vendor') {
|
|
188
|
+
frontendFrameworkMode = cliFrontendFrameworkMode;
|
|
189
|
+
}
|
|
190
|
+
else if (cliFrontendFrameworkMode) {
|
|
191
|
+
error(`Invalid --frontend-framework-mode value "${cliFrontendFrameworkMode}". Use "npm" or "vendor".`);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
else if (configFrontendFrameworkMode === 'npm' || configFrontendFrameworkMode === 'vendor') {
|
|
195
|
+
frontendFrameworkMode = configFrontendFrameworkMode;
|
|
196
|
+
info(`Using frontend framework mode from lt.config: ${frontendFrameworkMode}`);
|
|
197
|
+
}
|
|
198
|
+
else if (noConfirm) {
|
|
199
|
+
frontendFrameworkMode = 'npm';
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
// Default to npm without asking (unless user sets it explicitly)
|
|
203
|
+
frontendFrameworkMode = 'npm';
|
|
204
|
+
}
|
|
135
205
|
// Determine remote push settings with priority: CLI > config > interactive
|
|
136
206
|
// Git is always initialized; the question is whether to push to a remote
|
|
137
207
|
let pushToRemote = false;
|
|
@@ -190,6 +260,59 @@ const NewCommand = {
|
|
|
190
260
|
const apiLink = cliApiLink || configApiLink;
|
|
191
261
|
const frontendCopy = cliFrontendCopy || configFrontendCopy;
|
|
192
262
|
const frontendLink = cliFrontendLink || configFrontendLink;
|
|
263
|
+
// Dry-run mode: print the resolved plan and exit without any disk
|
|
264
|
+
// changes. Useful for CI previews, for Claude Code confirmation
|
|
265
|
+
// steps, and for debugging the mode-detection logic without
|
|
266
|
+
// committing to a multi-minute init flow.
|
|
267
|
+
if (dryRun) {
|
|
268
|
+
info('');
|
|
269
|
+
info('Dry-run plan:');
|
|
270
|
+
info(` name: ${name}`);
|
|
271
|
+
info(` projectDir: ${projectDir}`);
|
|
272
|
+
info(` frontend: ${frontend}`);
|
|
273
|
+
info(` apiMode: ${apiMode}`);
|
|
274
|
+
info(` frameworkMode: ${frameworkMode}`);
|
|
275
|
+
info(` frontendFrameworkMode: ${frontendFrameworkMode}`);
|
|
276
|
+
if (frameworkUpstreamBranch) {
|
|
277
|
+
info(` frameworkUpstreamBranch: ${frameworkUpstreamBranch}`);
|
|
278
|
+
}
|
|
279
|
+
info(` apiBranch: ${apiBranch || '(default)'}`);
|
|
280
|
+
info(` frontendBranch: ${frontendBranch || '(default)'}`);
|
|
281
|
+
info(` apiCopy: ${apiCopy || '(none)'}`);
|
|
282
|
+
info(` apiLink: ${apiLink || '(none)'}`);
|
|
283
|
+
info(` frontendCopy: ${frontendCopy || '(none)'}`);
|
|
284
|
+
info(` frontendLink: ${frontendLink || '(none)'}`);
|
|
285
|
+
info(` pushToRemote: ${pushToRemote}`);
|
|
286
|
+
if (pushToRemote) {
|
|
287
|
+
info(` gitLink: ${gitLink || '(unset — would abort at run-time)'}`);
|
|
288
|
+
}
|
|
289
|
+
info('');
|
|
290
|
+
info('Would execute:');
|
|
291
|
+
info(` 1. git clone lt-monorepo → ${projectDir}/`);
|
|
292
|
+
info(` 2. setup frontend (${frontend}) → ${projectDir}/projects/app`);
|
|
293
|
+
if (frameworkMode === 'vendor') {
|
|
294
|
+
info(` 3. clone nest-server-starter → ${projectDir}/projects/api`);
|
|
295
|
+
info(` 4. clone @lenne.tech/nest-server${frameworkUpstreamBranch ? ` (branch/tag: ${frameworkUpstreamBranch})` : ''} → /tmp`);
|
|
296
|
+
info(` 5. vendor core/ + flatten-fix + codemod consumer imports`);
|
|
297
|
+
info(` 6. merge upstream deps (dynamic, no hard-coded list)`);
|
|
298
|
+
info(` 7. run processApiMode(${apiMode})`);
|
|
299
|
+
if (apiMode === 'Rest') {
|
|
300
|
+
info(` 8. restore vendored core essentials (graphql-*)`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
info(` 3. clone nest-server-starter → ${projectDir}/projects/api`);
|
|
305
|
+
info(` 4. run processApiMode(${apiMode})`);
|
|
306
|
+
}
|
|
307
|
+
if (frontendFrameworkMode === 'vendor') {
|
|
308
|
+
info(` M1. clone @lenne.tech/nuxt-extensions → /tmp`);
|
|
309
|
+
info(` M2. vendor app/core/ (module.ts + runtime/) + codemod consumer imports`);
|
|
310
|
+
info(` M3. rewrite nuxt.config.ts module entry`);
|
|
311
|
+
}
|
|
312
|
+
info(' N. pnpm install + initial git commit');
|
|
313
|
+
info('');
|
|
314
|
+
return `fullstack init dry-run (${frameworkMode} / ${apiMode})`;
|
|
315
|
+
}
|
|
193
316
|
const workspaceSpinner = spin(`Create fullstack workspace with ${frontend} in ${projectDir} with ${name} app`);
|
|
194
317
|
// Clone monorepo
|
|
195
318
|
try {
|
|
@@ -217,6 +340,7 @@ const NewCommand = {
|
|
|
217
340
|
.replace(/\{\{PROJECT_NAME\}\}/g, () => name)
|
|
218
341
|
.replace(/\{\{PROJECT_DIR\}\}/g, () => projectDir)
|
|
219
342
|
.replace(/\{\{API_MODE\}\}/g, () => apiMode)
|
|
343
|
+
.replace(/\{\{FRAMEWORK_MODE\}\}/g, () => frameworkMode)
|
|
220
344
|
.replace(/\{\{FRONTEND_FRAMEWORK\}\}/g, () => frontendName));
|
|
221
345
|
}
|
|
222
346
|
// Always initialize git
|
|
@@ -267,6 +391,21 @@ const NewCommand = {
|
|
|
267
391
|
if (frontendResult.method !== 'link') {
|
|
268
392
|
frontendHelper.patchFrontendEnv(frontendDest, projectDir);
|
|
269
393
|
}
|
|
394
|
+
// ── Frontend vendoring (if requested) ───────────────────────────────
|
|
395
|
+
if (isNuxt && frontendFrameworkMode === 'vendor' && frontendResult.method !== 'link') {
|
|
396
|
+
const vendorSpinner = spin('Converting frontend to vendor mode...');
|
|
397
|
+
try {
|
|
398
|
+
yield frontendHelper.convertAppCloneToVendored({
|
|
399
|
+
dest: frontendDest,
|
|
400
|
+
projectName: name,
|
|
401
|
+
});
|
|
402
|
+
vendorSpinner.succeed('Frontend converted to vendor mode (app/core/)');
|
|
403
|
+
}
|
|
404
|
+
catch (err) {
|
|
405
|
+
vendorSpinner.fail(`Frontend vendor conversion failed: ${err.message}`);
|
|
406
|
+
toolbox.print.warning('Continuing with npm mode for frontend.');
|
|
407
|
+
}
|
|
408
|
+
}
|
|
270
409
|
// Remove gitkeep file
|
|
271
410
|
filesystem.remove(`${projectDir}/projects/.gitkeep`);
|
|
272
411
|
// Integrate files
|
|
@@ -280,6 +419,8 @@ const NewCommand = {
|
|
|
280
419
|
apiMode,
|
|
281
420
|
branch: apiBranch,
|
|
282
421
|
copyPath: apiCopy,
|
|
422
|
+
frameworkMode,
|
|
423
|
+
frameworkUpstreamBranch,
|
|
283
424
|
linkPath: apiLink,
|
|
284
425
|
name,
|
|
285
426
|
projectDir,
|
|
@@ -289,6 +430,10 @@ const NewCommand = {
|
|
|
289
430
|
return;
|
|
290
431
|
}
|
|
291
432
|
// Create lt.config.json for API
|
|
433
|
+
// Note: frameworkMode is persisted under meta so that subsequent `lt
|
|
434
|
+
// server module` / `addProp` / `permissions` calls can detect the mode
|
|
435
|
+
// without re-probing src/core/VENDOR.md each time (the VENDOR.md check
|
|
436
|
+
// still works; this is just an explicit marker).
|
|
292
437
|
const apiConfigPath = filesystem.path(apiDest, 'lt.config.json');
|
|
293
438
|
filesystem.write(apiConfigPath, {
|
|
294
439
|
commands: {
|
|
@@ -300,6 +445,7 @@ const NewCommand = {
|
|
|
300
445
|
},
|
|
301
446
|
meta: {
|
|
302
447
|
apiMode,
|
|
448
|
+
frameworkMode,
|
|
303
449
|
version: '1.0.0',
|
|
304
450
|
},
|
|
305
451
|
}, { jsonIndent: 2 });
|