@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.
Files changed (38) hide show
  1. package/README.md +88 -3
  2. package/build/commands/config/validate.js +2 -0
  3. package/build/commands/frontend/convert-mode.js +198 -0
  4. package/build/commands/fullstack/convert-mode.js +368 -0
  5. package/build/commands/fullstack/init.js +150 -4
  6. package/build/commands/fullstack/update.js +177 -0
  7. package/build/commands/server/add-property.js +29 -2
  8. package/build/commands/server/convert-mode.js +197 -0
  9. package/build/commands/server/create.js +41 -3
  10. package/build/commands/server/module.js +58 -25
  11. package/build/commands/server/object.js +26 -5
  12. package/build/commands/server/permissions.js +20 -6
  13. package/build/commands/server/test.js +7 -1
  14. package/build/commands/status.js +94 -3
  15. package/build/config/vendor-frontend-runtime-deps.json +4 -0
  16. package/build/config/vendor-runtime-deps.json +9 -0
  17. package/build/extensions/api-mode.js +19 -3
  18. package/build/extensions/frontend-helper.js +652 -0
  19. package/build/extensions/server.js +1475 -3
  20. package/build/lib/framework-detection.js +167 -0
  21. package/build/lib/frontend-framework-detection.js +129 -0
  22. package/build/templates/nest-server-module/inputs/template-create.input.ts.ejs +1 -1
  23. package/build/templates/nest-server-module/inputs/template.input.ts.ejs +1 -1
  24. package/build/templates/nest-server-module/outputs/template-fac-result.output.ts.ejs +1 -1
  25. package/build/templates/nest-server-module/template.controller.ts.ejs +1 -1
  26. package/build/templates/nest-server-module/template.model.ts.ejs +1 -1
  27. package/build/templates/nest-server-module/template.module.ts.ejs +1 -1
  28. package/build/templates/nest-server-module/template.resolver.ts.ejs +1 -1
  29. package/build/templates/nest-server-module/template.service.ts.ejs +1 -1
  30. package/build/templates/nest-server-object/template-create.input.ts.ejs +1 -1
  31. package/build/templates/nest-server-object/template.input.ts.ejs +1 -1
  32. package/build/templates/nest-server-object/template.object.ts.ejs +1 -1
  33. package/build/templates/nest-server-tests/tests.e2e-spec.ts.ejs +1 -1
  34. package/docs/LT-ECOSYSTEM-GUIDE.md +973 -0
  35. package/docs/VENDOR-MODE-WORKFLOW.md +471 -0
  36. package/docs/commands.md +196 -0
  37. package/docs/lt.config.md +9 -7
  38. 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: (_w = ltConfig === null || ltConfig === void 0 ? void 0 : ltConfig.commands) === null || _w === void 0 ? void 0 : _w.fullstack,
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 });