@lenne.tech/cli 1.13.0 → 1.15.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 CHANGED
@@ -94,6 +94,37 @@ $ lt fullstack init --name myapp --framework-mode vendor --dry-run --noConfirm
94
94
  $ lt server create --name myapp --framework-mode vendor
95
95
  ```
96
96
 
97
+ ### Experimental: `--next` (nest-base)
98
+
99
+ Both `lt fullstack init` and `lt server create` support an experimental
100
+ `--next` flag that swaps the API template from
101
+ [`nest-server-starter`](https://github.com/lenneTech/nest-server-starter)
102
+ (MongoDB) to [`nest-base`](https://github.com/lenneTech/nest-base) — a new
103
+ NestJS stack on **Bun + Prisma 7 + Postgres + Better-Auth** with a built-in
104
+ `/dev` cockpit.
105
+
106
+ ```bash
107
+ # experimental standalone api
108
+ $ lt server create my-next-api --next --noConfirm
109
+
110
+ # experimental fullstack (nuxt + nest-base)
111
+ $ lt fullstack init --name my-next-app --frontend nuxt --next --noConfirm
112
+ ```
113
+
114
+ When `--next` is set the CLI:
115
+
116
+ - clones `nest-base` instead of `nest-server-starter`,
117
+ - forces `--api-mode Rest` and `--framework-mode npm` (other modes are not
118
+ applicable to nest-base),
119
+ - skips `nest-server-starter`-specific patching (`config.env.ts`,
120
+ `main.ts` Swagger setup, `meta.json`, `lt.config.json`),
121
+ - skips the workspace install in fullstack mode — run `pnpm install` for
122
+ the frontend and `bun install` for the API yourself.
123
+
124
+ This option is **experimental** and may change. The downstream `lt server
125
+ module/object/addProp/test/permissions` commands target the classic
126
+ `nest-server` layout and are not yet compatible with `nest-base`.
127
+
97
128
  ### Working on an existing project
98
129
 
99
130
  All `lt server …` commands (module, object, addProp, test, permissions)
@@ -21,13 +21,13 @@ const NewCommand = {
21
21
  run: (toolbox) => __awaiter(void 0, void 0, void 0, function* () {
22
22
  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;
23
23
  // Retrieve the tools we need
24
- const { config, filesystem, frontendHelper, git, helper, parameters, patching, print: { error, info, spin, success }, prompt: { ask, confirm }, server, strings: { kebabCase }, system, } = toolbox;
24
+ const { config, filesystem, frontendHelper, git, helper, parameters, patching, print: { error, info, spin, success }, prompt: { ask, confirm }, server, strings: { kebabCase }, system, template, } = toolbox;
25
25
  // Start timer
26
26
  const timer = system.startTimer();
27
27
  // Info
28
28
  info('Create a new fullstack workspace');
29
29
  // Hint for non-interactive callers (e.g. Claude Code)
30
- 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
+ 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] [--dry-run] --noConfirm');
31
31
  // Check git
32
32
  if (!(yield git.gitInstalled())) {
33
33
  return;
@@ -46,8 +46,9 @@ const NewCommand = {
46
46
  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;
47
47
  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;
48
48
  // Parse CLI arguments
49
- 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 { '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, next: cliNext, } = parameters.options;
50
50
  const dryRun = cliDryRun === true || cliDryRun === 'true';
51
+ const experimental = cliNext === true || cliNext === 'true';
51
52
  const frameworkUpstreamBranch = typeof cliFrameworkUpstreamBranch === 'string' && cliFrameworkUpstreamBranch.length > 0
52
53
  ? cliFrameworkUpstreamBranch
53
54
  : undefined;
@@ -109,7 +110,11 @@ const NewCommand = {
109
110
  // Determine API mode with priority: CLI > config > global > interactive (default: Rest)
110
111
  const globalApiMode = config.getGlobalDefault(ltConfig, 'apiMode');
111
112
  let apiMode;
112
- if (cliApiMode) {
113
+ if (experimental) {
114
+ apiMode = 'Rest';
115
+ info('Using experimental nest-base template (Bun + Prisma + Postgres + Better-Auth)');
116
+ }
117
+ else if (cliApiMode) {
113
118
  apiMode = cliApiMode;
114
119
  }
115
120
  else if (configApiMode) {
@@ -154,7 +159,10 @@ const NewCommand = {
154
159
  //
155
160
  // Default is still 'npm' until the vendoring pilot is fully evaluated.
156
161
  let frameworkMode;
157
- if (cliFrameworkMode === 'npm' || cliFrameworkMode === 'vendor') {
162
+ if (experimental) {
163
+ frameworkMode = 'npm';
164
+ }
165
+ else if (cliFrameworkMode === 'npm' || cliFrameworkMode === 'vendor') {
158
166
  frameworkMode = cliFrameworkMode;
159
167
  }
160
168
  else if (cliFrameworkMode) {
@@ -291,7 +299,10 @@ const NewCommand = {
291
299
  info('Would execute:');
292
300
  info(` 1. git clone lt-monorepo → ${projectDir}/`);
293
301
  info(` 2. setup frontend (${frontend}) → ${projectDir}/projects/app`);
294
- if (frameworkMode === 'vendor') {
302
+ if (experimental) {
303
+ info(` 3. clone nest-base (experimental) → ${projectDir}/projects/api`);
304
+ }
305
+ else if (frameworkMode === 'vendor') {
295
306
  info(` 3. clone nest-server-starter → ${projectDir}/projects/api`);
296
307
  info(` 4. clone @lenne.tech/nest-server${frameworkUpstreamBranch ? ` (branch/tag: ${frameworkUpstreamBranch})` : ''} → /tmp`);
297
308
  info(` 5. vendor core/ + flatten-fix + codemod consumer imports`);
@@ -333,16 +344,46 @@ const NewCommand = {
333
344
  const ngBaseSpinner = spin(`Integrate example for ${frontend}`);
334
345
  // Remove git folder after clone
335
346
  filesystem.remove(`${projectDir}/.git`);
336
- // Patch CLAUDE.md with project-specific values
337
- const claudeMdPath = `${projectDir}/CLAUDE.md`;
338
- if (filesystem.exists(claudeMdPath)) {
339
- const frontendName = frontend === 'nuxt' ? 'Nuxt 4' : 'Angular';
340
- yield patching.update(claudeMdPath, (content) => content
341
- .replace(/\{\{PROJECT_NAME\}\}/g, () => name)
342
- .replace(/\{\{PROJECT_DIR\}\}/g, () => projectDir)
343
- .replace(/\{\{API_MODE\}\}/g, () => apiMode)
344
- .replace(/\{\{FRAMEWORK_MODE\}\}/g, () => frameworkMode)
345
- .replace(/\{\{FRONTEND_FRAMEWORK\}\}/g, () => frontendName));
347
+ // Patch root files for the project.
348
+ //
349
+ // For the classic flow we patch the cloned `lt-monorepo` CLAUDE.md with
350
+ // template variables. For `--next` (experimental) we replace the root
351
+ // README.md, CLAUDE.md, and create `.claude/QUICKSTART.md` outright,
352
+ // because `lt-monorepo`'s root files describe the legacy MongoDB +
353
+ // GraphQL stack which is explicitly out of scope for the nest-base
354
+ // template leaving them in place poisons every AI agent's context.
355
+ if (experimental) {
356
+ const nextTemplateProps = { name, projectDir };
357
+ // Render new root files. `template.generate({ target })` overwrites
358
+ // anything at `target`, which is what we want — the freshly cloned
359
+ // monorepo's stale README/CLAUDE.md must be replaced wholesale.
360
+ yield template.generate({
361
+ props: nextTemplateProps,
362
+ target: `${projectDir}/README.md`,
363
+ template: 'next-fullstack/README.md.ejs',
364
+ });
365
+ yield template.generate({
366
+ props: nextTemplateProps,
367
+ target: `${projectDir}/CLAUDE.md`,
368
+ template: 'next-fullstack/CLAUDE.md.ejs',
369
+ });
370
+ yield template.generate({
371
+ props: nextTemplateProps,
372
+ target: `${projectDir}/.claude/QUICKSTART.md`,
373
+ template: 'next-fullstack/.claude/QUICKSTART.md.ejs',
374
+ });
375
+ }
376
+ else {
377
+ const claudeMdPath = `${projectDir}/CLAUDE.md`;
378
+ if (filesystem.exists(claudeMdPath)) {
379
+ const frontendName = frontend === 'nuxt' ? 'Nuxt 4' : 'Angular';
380
+ yield patching.update(claudeMdPath, (content) => content
381
+ .replace(/\{\{PROJECT_NAME\}\}/g, () => name)
382
+ .replace(/\{\{PROJECT_DIR\}\}/g, () => projectDir)
383
+ .replace(/\{\{API_MODE\}\}/g, () => apiMode)
384
+ .replace(/\{\{FRAMEWORK_MODE\}\}/g, () => frameworkMode)
385
+ .replace(/\{\{FRONTEND_FRAMEWORK\}\}/g, () => frontendName));
386
+ }
346
387
  }
347
388
  // Always initialize git
348
389
  try {
@@ -420,6 +461,7 @@ const NewCommand = {
420
461
  apiMode,
421
462
  branch: apiBranch,
422
463
  copyPath: apiCopy,
464
+ experimental,
423
465
  frameworkMode,
424
466
  frameworkUpstreamBranch,
425
467
  linkPath: apiLink,
@@ -466,15 +508,20 @@ const NewCommand = {
466
508
  // instead.` and silently disables CVE overrides.
467
509
  (0, hoist_workspace_pnpm_config_1.hoistWorkspacePnpmConfig)({ filesystem, projectDir, subProjects: ['projects/api', 'projects/app'] });
468
510
  // Install all packages
469
- const installSpinner = spin('Install all packages');
470
- try {
471
- const detectedPm = toolbox.pm.detect(projectDir);
472
- yield system.run(`cd ${projectDir} && ${toolbox.pm.install(detectedPm)} && ${toolbox.pm.run('init', detectedPm)}`);
473
- installSpinner.succeed('Successfully installed all packages');
511
+ if (!experimental) {
512
+ const installSpinner = spin('Install all packages');
513
+ try {
514
+ const detectedPm = toolbox.pm.detect(projectDir);
515
+ yield system.run(`cd ${projectDir} && ${toolbox.pm.install(detectedPm)} && ${toolbox.pm.run('init', detectedPm)}`);
516
+ installSpinner.succeed('Successfully installed all packages');
517
+ }
518
+ catch (err) {
519
+ installSpinner.fail(`Failed to install packages: ${err.message}`);
520
+ return;
521
+ }
474
522
  }
475
- catch (err) {
476
- installSpinner.fail(`Failed to install packages: ${err.message}`);
477
- return;
523
+ else {
524
+ info('Skipping workspace install — run `bun install` (api) and `pnpm install` (app) manually.');
478
525
  }
479
526
  // Post-install format pass. processApiMode (run earlier in
480
527
  // setupServerForFullstack) and convertAppCloneToVendored rewrite
@@ -482,10 +529,10 @@ const NewCommand = {
482
529
  // `pnpm run format:check` (multi-line arrays/imports after region
483
530
  // stripping, import-path rewrites that now fit single-line). The
484
531
  // formatter is only available after install, so we normalize here.
485
- if (apiMode && filesystem.isDirectory(`${projectDir}/projects/api`)) {
532
+ if (!experimental && apiMode && filesystem.isDirectory(`${projectDir}/projects/api`)) {
486
533
  yield toolbox.apiMode.formatProject(`${projectDir}/projects/api`);
487
534
  }
488
- if (isNuxt && filesystem.isDirectory(`${projectDir}/projects/app`)) {
535
+ if (!experimental && isNuxt && filesystem.isDirectory(`${projectDir}/projects/app`)) {
489
536
  yield toolbox.apiMode.formatProject(`${projectDir}/projects/app`);
490
537
  }
491
538
  // Create initial commit after everything is set up
@@ -511,9 +558,18 @@ const NewCommand = {
511
558
  success(`Generated fullstack workspace with ${frontend} in ${projectDir} with ${name} app in ${helper.msToMinutesAndSeconds(timer())}m.`);
512
559
  info('');
513
560
  info('Next:');
514
- info(` Run ${name}`);
515
- info(` $ cd ${projectDir}`);
516
- info(` $ ${toolbox.pm.run('start')}`);
561
+ if (experimental) {
562
+ info(` $ cd ${projectDir}`);
563
+ info(' Frontend: cd projects/app && pnpm install');
564
+ info(' API: cd projects/api && bun install');
565
+ info(' Configure projects/api/.env (see .env.example)');
566
+ info(' Start Postgres + run prisma generate / migrate');
567
+ }
568
+ else {
569
+ info(` Run ${name}`);
570
+ info(` $ cd ${projectDir}`);
571
+ info(` $ ${toolbox.pm.run('start')}`);
572
+ }
517
573
  info('');
518
574
  if (!toolbox.parameters.options.fromGluegunMenu) {
519
575
  process.exit();
@@ -42,6 +42,13 @@ const NewCommand = {
42
42
  { description: 'Git branch to clone from', flag: '--branch', required: false, type: 'string' },
43
43
  { description: 'Copy from local path instead of cloning', flag: '--copy', required: false, type: 'string' },
44
44
  { description: 'Symlink to local path instead of cloning', flag: '--link', required: false, type: 'string' },
45
+ {
46
+ default: false,
47
+ description: 'Use experimental nest-base template (Bun + Prisma + Postgres)',
48
+ flag: '--next',
49
+ required: false,
50
+ type: 'boolean',
51
+ },
45
52
  {
46
53
  default: false,
47
54
  description: 'Skip all interactive prompts',
@@ -77,6 +84,7 @@ const NewCommand = {
77
84
  const cliApiMode = parameters.options['api-mode'] || parameters.options.apiMode;
78
85
  const cliFrameworkMode = parameters.options['framework-mode'];
79
86
  const cliFrameworkUpstreamBranch = parameters.options['framework-upstream-branch'];
87
+ const experimental = parameters.options.next === true || parameters.options.next === 'true';
80
88
  // Determine noConfirm with priority: CLI > config > global > default (false)
81
89
  const noConfirm = config.getNoConfirm({
82
90
  cliValue: cliNoConfirm,
@@ -88,7 +96,7 @@ const NewCommand = {
88
96
  // Info
89
97
  info('Create a new server');
90
98
  // Hint for non-interactive callers (e.g. Claude Code)
91
- toolbox.tools.nonInteractiveHint('lt server create --name <name> --api-mode <Rest|GraphQL|Both> --noConfirm');
99
+ toolbox.tools.nonInteractiveHint('lt server create --name <name> --api-mode <Rest|GraphQL|Both> [--next] --noConfirm');
92
100
  // Check git
93
101
  if (!(yield git.gitInstalled())) {
94
102
  return;
@@ -150,7 +158,11 @@ const NewCommand = {
150
158
  }
151
159
  // Determine API mode with priority: CLI > config > global > interactive (default: Rest)
152
160
  let apiMode;
153
- if (cliApiMode) {
161
+ if (experimental) {
162
+ apiMode = 'Rest';
163
+ info('Using experimental nest-base template (Bun + Prisma + Postgres + Better-Auth)');
164
+ }
165
+ else if (cliApiMode) {
154
166
  apiMode = cliApiMode;
155
167
  }
156
168
  else if (configApiMode) {
@@ -184,7 +196,10 @@ const NewCommand = {
184
196
  // Determine framework consumption mode — same resolution cascade as
185
197
  // lt fullstack init: CLI flag > lt.config > interactive (default npm).
186
198
  let frameworkMode;
187
- if (cliFrameworkMode === 'npm' || cliFrameworkMode === 'vendor') {
199
+ if (experimental) {
200
+ frameworkMode = 'npm';
201
+ }
202
+ else if (cliFrameworkMode === 'npm' || cliFrameworkMode === 'vendor') {
188
203
  frameworkMode = cliFrameworkMode;
189
204
  }
190
205
  else if (cliFrameworkMode) {
@@ -222,6 +237,7 @@ const NewCommand = {
222
237
  branch,
223
238
  copyPath,
224
239
  description,
240
+ experimental,
225
241
  frameworkMode,
226
242
  frameworkUpstreamBranch,
227
243
  linkPath,
@@ -279,36 +295,47 @@ const NewCommand = {
279
295
  }
280
296
  // Derive controller type from API mode and save project config
281
297
  const controllerType = apiMode;
282
- // Create lt.config.json
283
- const projectConfig = {
284
- commands: {
285
- server: {
286
- module: {
287
- controller: controllerType,
298
+ if (!experimental) {
299
+ // Create lt.config.json
300
+ const projectConfig = {
301
+ commands: {
302
+ server: {
303
+ module: {
304
+ controller: controllerType,
305
+ },
288
306
  },
289
307
  },
290
- },
291
- meta: {
292
- apiMode,
293
- version: '1.0.0',
294
- },
295
- };
296
- const configPath = filesystem.path(projectDir, 'lt.config.json');
297
- filesystem.write(configPath, projectConfig, { jsonIndent: 2 });
298
- info('');
299
- success(`Configuration saved to ${projectDir}/lt.config.json`);
300
- info(` API mode: ${apiMode}`);
301
- info(` Default controller type: ${controllerType}`);
308
+ meta: {
309
+ apiMode,
310
+ version: '1.0.0',
311
+ },
312
+ };
313
+ const configPath = filesystem.path(projectDir, 'lt.config.json');
314
+ filesystem.write(configPath, projectConfig, { jsonIndent: 2 });
315
+ info('');
316
+ success(`Configuration saved to ${projectDir}/lt.config.json`);
317
+ info(` API mode: ${apiMode}`);
318
+ info(` Default controller type: ${controllerType}`);
319
+ }
302
320
  // We're done, so show what to do next
303
321
  info('');
304
322
  success(`Generated ${name} server with lenne.Tech CLI ${meta.version()} in ${helper.msToMinutesAndSeconds(timer())}m.`);
305
323
  info('');
306
324
  info('Next:');
307
- info(' Start database server (e.g. MongoDB)');
308
- info(` Check config: ${projectDir}/src/config.env.ts`);
309
- info(` Go to project directory: cd ${projectDir}`);
310
- info(` Run tests: ${toolbox.pm.run('test:e2e')}`);
311
- info(` Start server: ${toolbox.pm.run('start')}`);
325
+ if (experimental) {
326
+ info(` Go to project directory: cd ${projectDir}`);
327
+ info(' Install dependencies: bun install');
328
+ info(' Configure .env (see .env.example)');
329
+ info(' Start Postgres + run prisma generate / migrate');
330
+ info(' Start server: bun run dev');
331
+ }
332
+ else {
333
+ info(' Start database server (e.g. MongoDB)');
334
+ info(` Check config: ${projectDir}/src/config.env.ts`);
335
+ info(` Go to project directory: cd ${projectDir}`);
336
+ info(` Run tests: ${toolbox.pm.run('test:e2e')}`);
337
+ info(` Start server: ${toolbox.pm.run('start')}`);
338
+ }
312
339
  info('');
313
340
  if (!toolbox.parameters.options.fromGluegunMenu) {
314
341
  process.exit();
@@ -633,13 +633,16 @@ class Server {
633
633
  setupServer(dest, options) {
634
634
  return __awaiter(this, void 0, void 0, function* () {
635
635
  const { apiMode: apiModeHelper, patching, system, template, templateHelper } = this.toolbox;
636
- const { apiMode, author = '', branch, copyPath, description = '', frameworkMode = 'npm', frameworkUpstreamBranch, linkPath, name, projectDir, skipInstall = false, skipPatching = false, } = options;
636
+ const { apiMode, author = '', branch, copyPath, description = '', experimental = false, frameworkMode = 'npm', frameworkUpstreamBranch, linkPath, name, projectDir, skipInstall = false, skipPatching = false, } = options;
637
+ const repoUrl = experimental
638
+ ? 'https://github.com/lenneTech/nest-base.git'
639
+ : 'https://github.com/lenneTech/nest-server-starter.git';
637
640
  // Setup template
638
641
  const result = yield templateHelper.setup(dest, {
639
642
  branch,
640
643
  copyPath,
641
644
  linkPath,
642
- repoUrl: 'https://github.com/lenneTech/nest-server-starter.git',
645
+ repoUrl,
643
646
  });
644
647
  if (!result.success) {
645
648
  return { method: result.method, path: result.path, success: false };
@@ -651,18 +654,20 @@ class Server {
651
654
  // Apply patches (config.env.ts, package.json, main.ts, meta.json)
652
655
  if (!skipPatching) {
653
656
  try {
654
- // Generate README
655
- yield template.generate({
656
- props: { description, name },
657
- target: `${dest}/README.md`,
658
- template: 'nest-server-starter/README.md.ejs',
659
- });
660
- // Replace secret or private keys and update database names via AST
661
- this.patchConfigEnvTs(`${dest}/src/config.env.ts`, projectDir);
662
- // Update Swagger configuration in main.ts
663
- yield patching.update(`${dest}/src/main.ts`, (content) => content
664
- .replace(/\.setTitle\('.*?'\)/, `.setTitle('${name}')`)
665
- .replace(/\.setDescription\('.*?'\)/, `.setDescription('${description || name}')`));
657
+ if (!experimental) {
658
+ // Generate README
659
+ yield template.generate({
660
+ props: { description, name },
661
+ target: `${dest}/README.md`,
662
+ template: 'nest-server-starter/README.md.ejs',
663
+ });
664
+ // Replace secret or private keys and update database names via AST
665
+ this.patchConfigEnvTs(`${dest}/src/config.env.ts`, projectDir);
666
+ // Update Swagger configuration in main.ts
667
+ yield patching.update(`${dest}/src/main.ts`, (content) => content
668
+ .replace(/\.setTitle\('.*?'\)/, `.setTitle('${name}')`)
669
+ .replace(/\.setDescription\('.*?'\)/, `.setDescription('${description || name}')`));
670
+ }
666
671
  // Update package.json
667
672
  yield patching.update(`${dest}/package.json`, (config) => {
668
673
  config.author = author;
@@ -674,13 +679,15 @@ class Server {
674
679
  config.version = '0.0.1';
675
680
  return config;
676
681
  });
677
- // Update meta.json if exists
678
- if (this.filesystem.exists(`${dest}/src/meta`)) {
679
- yield patching.update(`${dest}/src/meta`, (config) => {
680
- config.name = name;
681
- config.description = description;
682
- return config;
683
- });
682
+ if (!experimental) {
683
+ // Update meta.json if exists
684
+ if (this.filesystem.exists(`${dest}/src/meta`)) {
685
+ yield patching.update(`${dest}/src/meta`, (config) => {
686
+ config.name = name;
687
+ config.description = description;
688
+ return config;
689
+ });
690
+ }
684
691
  }
685
692
  }
686
693
  catch (err) {
@@ -703,7 +710,7 @@ class Server {
703
710
  // manifest (same dance as in setupServerForFullstack).
704
711
  let standaloneVendorUpstreamDeps = {};
705
712
  let standaloneVendorCoreEssentials = [];
706
- if (frameworkMode === 'vendor') {
713
+ if (!experimental && frameworkMode === 'vendor') {
707
714
  try {
708
715
  const converted = yield this.convertCloneToVendored({
709
716
  dest,
@@ -718,7 +725,7 @@ class Server {
718
725
  standaloneVendorCoreEssentials = this.readApiModeGraphqlEssentials(dest);
719
726
  }
720
727
  // Process API mode (before install so package.json is correct)
721
- if (apiMode) {
728
+ if (!experimental && apiMode) {
722
729
  try {
723
730
  yield apiModeHelper.processApiMode(dest, apiMode);
724
731
  }
@@ -727,7 +734,7 @@ class Server {
727
734
  }
728
735
  }
729
736
  // Restore core essentials after processApiMode stripped them (vendor + REST only).
730
- if (frameworkMode === 'vendor' && apiMode === 'Rest') {
737
+ if (!experimental && frameworkMode === 'vendor' && apiMode === 'Rest') {
731
738
  try {
732
739
  this.restoreVendorCoreEssentials({
733
740
  dest,
@@ -740,9 +747,11 @@ class Server {
740
747
  }
741
748
  }
742
749
  // Patch CLAUDE.md with API mode info
743
- this.patchClaudeMdApiMode(dest, apiMode);
750
+ if (!experimental) {
751
+ this.patchClaudeMdApiMode(dest, apiMode);
752
+ }
744
753
  // Install packages
745
- if (!skipInstall) {
754
+ if (!skipInstall && !experimental) {
746
755
  try {
747
756
  const { pm } = this.toolbox;
748
757
  yield system.run(`cd "${dest}" && ${pm.install(pm.detect(dest))}`);
@@ -771,7 +780,10 @@ class Server {
771
780
  setupServerForFullstack(dest, options) {
772
781
  return __awaiter(this, void 0, void 0, function* () {
773
782
  const { apiMode: apiModeHelper, templateHelper } = this.toolbox;
774
- const { apiMode, branch, copyPath, frameworkMode = 'npm', frameworkUpstreamBranch, linkPath, name, projectDir, } = options;
783
+ const { apiMode, branch, copyPath, experimental = false, frameworkMode = 'npm', frameworkUpstreamBranch, linkPath, name, projectDir, } = options;
784
+ const repoUrl = experimental
785
+ ? 'https://github.com/lenneTech/nest-base.git'
786
+ : 'https://github.com/lenneTech/nest-server-starter';
775
787
  // Both npm and vendor mode clone nest-server-starter as the base. The
776
788
  // starter ships the minimal consumer conventions a project needs
777
789
  // (src/server/common/models/persistence.model.ts, src/server/modules/user/,
@@ -791,7 +803,7 @@ class Server {
791
803
  branch,
792
804
  copyPath,
793
805
  linkPath,
794
- repoUrl: 'https://github.com/lenneTech/nest-server-starter',
806
+ repoUrl,
795
807
  });
796
808
  if (!result.success) {
797
809
  return { method: result.method, path: result.path, success: false };
@@ -801,18 +813,33 @@ class Server {
801
813
  return { method: 'link', path: result.path, success: true };
802
814
  }
803
815
  // Apply minimal patches for fullstack
804
- try {
805
- // Write meta.json
806
- this.filesystem.write(`${dest}/src/meta.json`, {
807
- description: `API for ${name} app`,
808
- name: `${name}-api-server`,
809
- version: '0.0.0',
810
- });
811
- // Replace secret or private keys and update database names via AST
812
- this.patchConfigEnvTs(`${dest}/src/config.env.ts`, projectDir);
816
+ if (!experimental) {
817
+ try {
818
+ // Write meta.json
819
+ this.filesystem.write(`${dest}/src/meta.json`, {
820
+ description: `API for ${name} app`,
821
+ name: `${name}-api-server`,
822
+ version: '0.0.0',
823
+ });
824
+ // Replace secret or private keys and update database names via AST
825
+ this.patchConfigEnvTs(`${dest}/src/config.env.ts`, projectDir);
826
+ }
827
+ catch (err) {
828
+ return { method: result.method, path: dest, success: false };
829
+ }
813
830
  }
814
- catch (err) {
815
- return { method: result.method, path: dest, success: false };
831
+ else {
832
+ try {
833
+ yield this.toolbox.patching.update(`${dest}/package.json`, (config) => {
834
+ config.name = projectDir;
835
+ config.description = `API for ${name} app`;
836
+ config.version = '0.0.0';
837
+ return config;
838
+ });
839
+ }
840
+ catch (err) {
841
+ return { method: result.method, path: dest, success: false };
842
+ }
816
843
  }
817
844
  // Clean up copied template artifacts
818
845
  if (result.method === 'copy') {
@@ -833,7 +860,7 @@ class Server {
833
860
  // without hard-coding package lists.
834
861
  let vendorUpstreamDeps = {};
835
862
  let vendorCoreEssentials = [];
836
- if (frameworkMode === 'vendor') {
863
+ if (!experimental && frameworkMode === 'vendor') {
837
864
  try {
838
865
  const converted = yield this.convertCloneToVendored({
839
866
  dest,
@@ -856,7 +883,7 @@ class Server {
856
883
  vendorCoreEssentials = this.readApiModeGraphqlEssentials(dest);
857
884
  }
858
885
  // Process API mode (before install which happens at monorepo level)
859
- if (apiMode) {
886
+ if (!experimental && apiMode) {
860
887
  try {
861
888
  yield apiModeHelper.processApiMode(dest, apiMode);
862
889
  }
@@ -867,7 +894,7 @@ class Server {
867
894
  // In vendor mode + REST, re-add the graphql essentials that
868
895
  // processApiMode just stripped. Both and GraphQL keep all packages
869
896
  // by construction and don't need restoration.
870
- if (frameworkMode === 'vendor' && apiMode === 'Rest') {
897
+ if (!experimental && frameworkMode === 'vendor' && apiMode === 'Rest') {
871
898
  try {
872
899
  this.restoreVendorCoreEssentials({
873
900
  dest,
@@ -881,7 +908,9 @@ class Server {
881
908
  }
882
909
  }
883
910
  // Patch CLAUDE.md with API mode info
884
- this.patchClaudeMdApiMode(dest, apiMode);
911
+ if (!experimental) {
912
+ this.patchClaudeMdApiMode(dest, apiMode);
913
+ }
885
914
  return { method: result.method, path: dest, success: true };
886
915
  });
887
916
  }
@@ -43,14 +43,25 @@ class Tools {
43
43
  * Suggests using CLI parameters instead of interactive prompts.
44
44
  * Only shows once per session.
45
45
  *
46
+ * Skipped when the caller already passed `--noConfirm` — they're
47
+ * deliberately running headless and don't need a hint to repeat the
48
+ * command, which would just be confusing noise (the friction-log
49
+ * complaint that triggered this guard).
50
+ *
46
51
  * @param usage - Example command with parameters, e.g. "lt fullstack init --name <name> --frontend <nuxt|angular> --noConfirm"
47
52
  */
48
53
  nonInteractiveHint(usage) {
54
+ var _a;
49
55
  if (this.hintShown || process.stdin.isTTY) {
50
56
  return;
51
57
  }
58
+ const { parameters, print } = this.toolbox;
59
+ const noConfirm = (_a = parameters === null || parameters === void 0 ? void 0 : parameters.options) === null || _a === void 0 ? void 0 : _a.noConfirm;
60
+ if (noConfirm === true || noConfirm === 'true') {
61
+ this.hintShown = true;
62
+ return;
63
+ }
52
64
  this.hintShown = true;
53
- const { print } = this.toolbox;
54
65
  print.info(print.colors.yellow(`Hint: Non-interactive mode detected. Use parameters to skip prompts:`));
55
66
  print.info(print.colors.yellow(` ${usage}`));
56
67
  print.info('');
@@ -0,0 +1,61 @@
1
+ # QUICKSTART — `<%= props.name %>` workspace
2
+
3
+ A fresh AI agent that just opened this workspace. Read this once, then
4
+ switch to a subproject.
5
+
6
+ ## You almost certainly want a subproject
7
+
8
+ | If you are doing | Go to |
9
+ | --------------------------------------------------- | ----------------------------------------------------------------------- |
10
+ | Backend / API / DB / Prisma / auth / permissions | `cd projects/api` → [`projects/api/.claude/QUICKSTART.md`](../projects/api/.claude/QUICKSTART.md) |
11
+ | Frontend / Nuxt / pages / components | `cd projects/app` → its `README.md` and `CLAUDE.md` |
12
+ | Cross-project work (deploy, CI, monorepo tooling) | this root level |
13
+
14
+ The moment you're touching domain code or framework conventions, you
15
+ belong in a subproject. This root level is just the workspace shell.
16
+
17
+ ## Bring up the dev environment
18
+
19
+ ```bash
20
+ # API
21
+ cd projects/api
22
+ bun install
23
+ bun run setup # generates .env with strong random secrets
24
+ docker compose up -d postgres
25
+ bun run prepare:schema # only needed before first migrate / when feature flags change
26
+ bun run prisma:generate
27
+ bun run prisma:migrate
28
+ bun run dev # boots Postgres if needed, opens the Dev Hub at /dev
29
+
30
+ # Frontend (in another shell)
31
+ cd projects/app
32
+ pnpm install
33
+ pnpm dev
34
+ ```
35
+
36
+ If `bun run prisma:migrate` fails with a P1000 auth error after
37
+ re-running `bun run setup`, the Postgres volume still holds the old
38
+ password — run `docker compose down -v && docker compose up -d
39
+ postgres` to wipe it, then migrate again.
40
+
41
+ ## Stack
42
+
43
+ | | API | App |
44
+ | -------- | ------------------ | ------------------ |
45
+ | Runtime | Bun 1.x | Node 22 |
46
+ | Framework| NestJS 11 | Nuxt 4 |
47
+ | Database | Postgres 18 | — |
48
+ | ORM | Prisma 7 | — |
49
+ | Auth | Better-Auth | — |
50
+ | Lang | TypeScript strict | TypeScript strict |
51
+
52
+ For everything else (architecture, conventions, gates, debugging),
53
+ switch to the subproject.
54
+
55
+ ## Don'ts
56
+
57
+ - Don't add repo conventions in this root — they belong in the
58
+ subproject they apply to.
59
+ - Don't introduce GraphQL / MongoDB / Mongoose / vendor-mode
60
+ framework cores. Those are explicitly out of scope for `--next`
61
+ workspaces; see the API's `docs/architecture.md` for context.
@@ -0,0 +1,45 @@
1
+ # CLAUDE.md — `<%= props.name %>` workspace
2
+
3
+ This is a fullstack workspace with separate Claude Code contexts per
4
+ subproject. Pick the right one for your task and read its `CLAUDE.md`
5
+ first — that's where the conventions, architecture, and quickstart
6
+ live.
7
+
8
+ ## Routing
9
+
10
+ | Task | Where to read |
11
+ | ---------------------------------------------- | ------------------------------------------------------------------- |
12
+ | Backend / API / database / Prisma migrations | [`projects/api/CLAUDE.md`](./projects/api/CLAUDE.md) |
13
+ | Frontend / Nuxt / pages / components | [`projects/app/CLAUDE.md`](./projects/app/CLAUDE.md) |
14
+ | 60-second agent onboarding | [`.claude/QUICKSTART.md`](./.claude/QUICKSTART.md) |
15
+
16
+ ## Tech stack at a glance
17
+
18
+ - **API** — Bun 1.x · NestJS 11 · Prisma 7 · Postgres 18 · Better-Auth · REST + OpenAPI 3.1
19
+ - **App** — Nuxt 4 · Vue 3 · TypeScript
20
+
21
+ Detailed conventions live in the subproject `CLAUDE.md` files. Don't
22
+ add convention rules to this root file — they belong in the subproject
23
+ they apply to.
24
+
25
+ ## Out of scope (do not add to either subproject)
26
+
27
+ - GraphQL / Apollo
28
+ - Mongoose / MongoDB
29
+ - Vendor mode for the framework core (the `--next` template ships
30
+ `nest-base` directly, no `@lenne.tech/nest-server` npm dependency)
31
+ - The legacy `@Restricted` / `@Roles` decorators
32
+
33
+ These are explicitly removed in the `nest-base` template and a real
34
+ project should never reintroduce them. See
35
+ [`projects/api/docs/architecture.md`](./projects/api/docs/architecture.md)
36
+ "Out of scope" for the rationale.
37
+
38
+ ## When you open this repo cold
39
+
40
+ 1. Identify whether the task is backend, frontend, or cross-project.
41
+ 2. `cd projects/api` (or `projects/app`) and read its `CLAUDE.md`.
42
+ 3. Run the subproject's quality gates before committing.
43
+ 4. Cross-project changes (Docker compose, repo hygiene, deploy config)
44
+ stay at this root and follow each subproject's conventions when they
45
+ touch its files.
@@ -0,0 +1,48 @@
1
+ # <%= props.name %>
2
+
3
+ Fullstack workspace generated by `lt fullstack init --next`. The API is
4
+ the next-generation [`nest-base`](https://github.com/lenneTech/nest-base)
5
+ template (Bun + Prisma + Postgres + Better-Auth + REST). The app is a
6
+ Nuxt 4 frontend.
7
+
8
+ This root README intentionally stays minimal — each subproject has its
9
+ own `README.md` / `CLAUDE.md` / `.claude/` with the detail you actually
10
+ need while working in it.
11
+
12
+ ## Layout
13
+
14
+ - [`projects/api/`](./projects/api/) — NestJS backend (Bun · Prisma 7 · Postgres 18 · Better-Auth)
15
+ - [`projects/app/`](./projects/app/) — Nuxt 4 frontend
16
+ - [`.claude/QUICKSTART.md`](./.claude/QUICKSTART.md) — agent routing card
17
+
18
+ ## Quick start
19
+
20
+ ### API (Bun + Prisma + Postgres)
21
+
22
+ ```bash
23
+ cd projects/api
24
+ bun install
25
+ bun run setup # generate .env with strong random secrets
26
+ docker compose up -d postgres
27
+ bun run prepare:schema # concatenate active feature schemas
28
+ bun run prisma:generate
29
+ bun run prisma:migrate
30
+ bun run dev # boots Postgres if needed, opens the Dev Hub
31
+ ```
32
+
33
+ The Dev Hub is at `http://localhost:3000/dev`. See
34
+ [`projects/api/README.md`](./projects/api/README.md) for the full tour.
35
+
36
+ ### Frontend (Nuxt 4)
37
+
38
+ ```bash
39
+ cd projects/app
40
+ pnpm install
41
+ pnpm dev
42
+ ```
43
+
44
+ See [`projects/app/README.md`](./projects/app/README.md).
45
+
46
+ ## License
47
+
48
+ MIT
@@ -117,6 +117,7 @@ Flags:
117
117
  - `--frontend-framework-mode npm|vendor` — Frontend mode
118
118
  - `--framework-upstream-branch <tag>` — Specific nest-server version for vendor
119
119
  - `--dry-run` — Show plan without making changes
120
+ - `--next` — **Experimental:** clone [`nest-base`](https://github.com/lenneTech/nest-base) (Bun + Prisma 7 + Postgres + Better-Auth) for the API instead of `nest-server-starter`. Forces `--api-mode Rest`, `--framework-mode npm`, and skips workspace install (run `pnpm install` for app and `bun install` for api manually). Downstream `lt server module/object/addProp/test/permissions` are NOT compatible with the resulting layout.
120
121
 
121
122
  ---
122
123
 
package/docs/commands.md CHANGED
@@ -79,13 +79,16 @@ lt server create [name] [options]
79
79
  |--------|-------------|
80
80
  | `--description <text>` | Project description |
81
81
  | `--author <name>` | Author name |
82
+ | `--api-mode <Rest\|GraphQL\|Both>` | API mode (ignored with `--next`) |
83
+ | `--framework-mode <npm\|vendor>` | Framework consumption mode (ignored with `--next`) |
82
84
  | `--branch <branch>` / `-b` | Branch of nest-server-starter to use as template |
83
85
  | `--copy <path>` / `-c` | Copy from local template directory instead of cloning |
84
86
  | `--link <path>` | Symlink to local template directory (fastest, changes affect original) |
85
87
  | `--git` | Initialize git repository |
88
+ | `--next` | **Experimental:** clone [`nest-base`](https://github.com/lenneTech/nest-base) (Bun + Prisma 7 + Postgres + Better-Auth) instead of `nest-server-starter`. Skips API-mode / vendor-mode / install / lt.config.json processing. |
86
89
  | `--noConfirm` | Skip confirmation prompts |
87
90
 
88
- **CLAUDE.md Patching:** If the project contains a `CLAUDE.md`, the generic API mode description is replaced with the selected mode. In single-mode projects (`Rest` or `GraphQL`), the "API Mode System" documentation section is condensed to a brief note.
91
+ **CLAUDE.md Patching:** If the project contains a `CLAUDE.md`, the generic API mode description is replaced with the selected mode. In single-mode projects (`Rest` or `GraphQL`), the "API Mode System" documentation section is condensed to a brief note. Skipped when `--next` is used.
89
92
 
90
93
  **Configuration:** `commands.server.create.*`, `defaults.author`, `defaults.noConfirm`
91
94
 
@@ -532,6 +535,7 @@ lt fullstack init [options]
532
535
  | `--framework-mode <mode>` | Backend framework consumption mode: `npm` (classic) or `vendor` (pilot, core copied to `src/core/`) |
533
536
  | `--framework-upstream-branch <ref>` | Upstream `nest-server` branch/tag to vendor from (only with `--framework-mode vendor`) |
534
537
  | `--frontend-framework-mode <mode>` | Frontend framework consumption mode: `npm` or `vendor` (nuxt-extensions copied to `app/core/`) |
538
+ | `--next` | **Experimental:** clone [`nest-base`](https://github.com/lenneTech/nest-base) (Bun + Prisma 7 + Postgres + Better-Auth) for the API instead of `nest-server-starter`. Forces `--api-mode Rest` and `--framework-mode npm`, skips workspace install (run `pnpm install` for app and `bun install` for api manually). |
535
539
  | `--dry-run` | Print the resolved plan without making any changes |
536
540
  | `--git` | Push initial commit to remote repository (git is always initialized) |
537
541
  | `--git-link <url>` | Git remote repository URL (required when `--git` is true) |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lenne.tech/cli",
3
- "version": "1.13.0",
3
+ "version": "1.15.0",
4
4
  "description": "lenne.Tech CLI: lt",
5
5
  "keywords": [
6
6
  "lenne.Tech",