Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,37 @@ $ lt fullstack init --name myapp --framework-mode vendor --dry-run --noConfirm
$ lt server create --name myapp --framework-mode vendor
```

### Experimental: `--next` (nest-base)

Both `lt fullstack init` and `lt server create` support an experimental
`--next` flag that swaps the API template from
[`nest-server-starter`](https://github.com/lenneTech/nest-server-starter)
(MongoDB) to [`nest-base`](https://github.com/lenneTech/nest-base) — a new
NestJS stack on **Bun + Prisma 7 + Postgres + Better-Auth** with a built-in
`/dev` cockpit.

```bash
# experimental standalone api
$ lt server create my-next-api --next --noConfirm

# experimental fullstack (nuxt + nest-base)
$ lt fullstack init --name my-next-app --frontend nuxt --next --noConfirm
```

When `--next` is set the CLI:

- clones `nest-base` instead of `nest-server-starter`,
- forces `--api-mode Rest` and `--framework-mode npm` (other modes are not
applicable to nest-base),
- skips `nest-server-starter`-specific patching (`config.env.ts`,
`main.ts` Swagger setup, `meta.json`, `lt.config.json`),
- skips the workspace install in fullstack mode — run `pnpm install` for
the frontend and `bun install` for the API yourself.

This option is **experimental** and may change. The downstream `lt server
module/object/addProp/test/permissions` commands target the classic
`nest-server` layout and are not yet compatible with `nest-base`.

### Working on an existing project

All `lt server …` commands (module, object, addProp, test, permissions)
Expand Down
1 change: 1 addition & 0 deletions docs/LT-ECOSYSTEM-GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ Flags:
- `--frontend-framework-mode npm|vendor` — Frontend mode
- `--framework-upstream-branch <tag>` — Specific nest-server version for vendor
- `--dry-run` — Show plan without making changes
- `--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.

---

Expand Down
6 changes: 5 additions & 1 deletion docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,16 @@ lt server create [name] [options]
|--------|-------------|
| `--description <text>` | Project description |
| `--author <name>` | Author name |
| `--api-mode <Rest\|GraphQL\|Both>` | API mode (ignored with `--next`) |
| `--framework-mode <npm\|vendor>` | Framework consumption mode (ignored with `--next`) |
| `--branch <branch>` / `-b` | Branch of nest-server-starter to use as template |
| `--copy <path>` / `-c` | Copy from local template directory instead of cloning |
| `--link <path>` | Symlink to local template directory (fastest, changes affect original) |
| `--git` | Initialize git repository |
| `--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. |
| `--noConfirm` | Skip confirmation prompts |

**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.
**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.

**Configuration:** `commands.server.create.*`, `defaults.author`, `defaults.noConfirm`

Expand Down Expand Up @@ -532,6 +535,7 @@ lt fullstack init [options]
| `--framework-mode <mode>` | Backend framework consumption mode: `npm` (classic) or `vendor` (pilot, core copied to `src/core/`) |
| `--framework-upstream-branch <ref>` | Upstream `nest-server` branch/tag to vendor from (only with `--framework-mode vendor`) |
| `--frontend-framework-mode <mode>` | Frontend framework consumption mode: `npm` or `vendor` (nuxt-extensions copied to `app/core/`) |
| `--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). |
| `--dry-run` | Print the resolved plan without making any changes |
| `--git` | Push initial commit to remote repository (git is always initialized) |
| `--git-link <url>` | Git remote repository URL (required when `--git` is true) |
Expand Down
60 changes: 41 additions & 19 deletions src/commands/fullstack/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const NewCommand: GluegunCommand = {

// Hint for non-interactive callers (e.g. Claude Code)
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',
'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',
);

// Check git
Expand Down Expand Up @@ -75,9 +75,11 @@ const NewCommand: GluegunCommand = {
git: cliGit,
'git-link': cliGitLink,
name: cliName,
next: cliNext,
} = parameters.options;

const dryRun = cliDryRun === true || cliDryRun === 'true';
const experimental = cliNext === true || cliNext === 'true';
const frameworkUpstreamBranch =
typeof cliFrameworkUpstreamBranch === 'string' && cliFrameworkUpstreamBranch.length > 0
? cliFrameworkUpstreamBranch
Expand Down Expand Up @@ -147,7 +149,10 @@ const NewCommand: GluegunCommand = {
// Determine API mode with priority: CLI > config > global > interactive (default: Rest)
const globalApiMode = config.getGlobalDefault<'Both' | 'GraphQL' | 'Rest'>(ltConfig, 'apiMode');
let apiMode: 'Both' | 'GraphQL' | 'Rest';
if (cliApiMode) {
if (experimental) {
apiMode = 'Rest';
info('Using experimental nest-base template (Bun + Prisma + Postgres + Better-Auth)');
} else if (cliApiMode) {
apiMode = cliApiMode as 'Both' | 'GraphQL' | 'Rest';
} else if (configApiMode) {
apiMode = configApiMode;
Expand Down Expand Up @@ -189,7 +194,9 @@ const NewCommand: GluegunCommand = {
//
// Default is still 'npm' until the vendoring pilot is fully evaluated.
let frameworkMode: 'npm' | 'vendor';
if (cliFrameworkMode === 'npm' || cliFrameworkMode === 'vendor') {
if (experimental) {
frameworkMode = 'npm';
} else if (cliFrameworkMode === 'npm' || cliFrameworkMode === 'vendor') {
frameworkMode = cliFrameworkMode;
} else if (cliFrameworkMode) {
error(`Invalid --framework-mode value "${cliFrameworkMode}". Use "npm" or "vendor".`);
Expand Down Expand Up @@ -325,7 +332,9 @@ const NewCommand: GluegunCommand = {
info('Would execute:');
info(` 1. git clone lt-monorepo → ${projectDir}/`);
info(` 2. setup frontend (${frontend}) → ${projectDir}/projects/app`);
if (frameworkMode === 'vendor') {
if (experimental) {
info(` 3. clone nest-base (experimental) → ${projectDir}/projects/api`);
} else if (frameworkMode === 'vendor') {
info(` 3. clone nest-server-starter → ${projectDir}/projects/api`);
info(
` 4. clone @lenne.tech/nest-server${frameworkUpstreamBranch ? ` (branch/tag: ${frameworkUpstreamBranch})` : ''} → /tmp`,
Expand Down Expand Up @@ -472,6 +481,7 @@ const NewCommand: GluegunCommand = {
apiMode,
branch: apiBranch,
copyPath: apiCopy,
experimental,
frameworkMode,
frameworkUpstreamBranch,
linkPath: apiLink,
Expand Down Expand Up @@ -526,16 +536,20 @@ const NewCommand: GluegunCommand = {
hoistWorkspacePnpmConfig({ filesystem, projectDir, subProjects: ['projects/api', 'projects/app'] });

// Install all packages
const installSpinner = spin('Install all packages');
try {
const detectedPm = toolbox.pm.detect(projectDir);
await system.run(
`cd ${projectDir} && ${toolbox.pm.install(detectedPm)} && ${toolbox.pm.run('init', detectedPm)}`,
);
installSpinner.succeed('Successfully installed all packages');
} catch (err) {
installSpinner.fail(`Failed to install packages: ${err.message}`);
return;
if (!experimental) {
const installSpinner = spin('Install all packages');
try {
const detectedPm = toolbox.pm.detect(projectDir);
await system.run(
`cd ${projectDir} && ${toolbox.pm.install(detectedPm)} && ${toolbox.pm.run('init', detectedPm)}`,
);
installSpinner.succeed('Successfully installed all packages');
} catch (err) {
installSpinner.fail(`Failed to install packages: ${err.message}`);
return;
}
} else {
info('Skipping workspace install — run `bun install` (api) and `pnpm install` (app) manually.');
}

// Post-install format pass. processApiMode (run earlier in
Expand All @@ -544,10 +558,10 @@ const NewCommand: GluegunCommand = {
// `pnpm run format:check` (multi-line arrays/imports after region
// stripping, import-path rewrites that now fit single-line). The
// formatter is only available after install, so we normalize here.
if (apiMode && filesystem.isDirectory(`${projectDir}/projects/api`)) {
if (!experimental && apiMode && filesystem.isDirectory(`${projectDir}/projects/api`)) {
await toolbox.apiMode.formatProject(`${projectDir}/projects/api`);
}
if (isNuxt && filesystem.isDirectory(`${projectDir}/projects/app`)) {
if (!experimental && isNuxt && filesystem.isDirectory(`${projectDir}/projects/app`)) {
await toolbox.apiMode.formatProject(`${projectDir}/projects/app`);
}

Expand Down Expand Up @@ -578,9 +592,17 @@ const NewCommand: GluegunCommand = {
);
info('');
info('Next:');
info(` Run ${name}`);
info(` $ cd ${projectDir}`);
info(` $ ${toolbox.pm.run('start')}`);
if (experimental) {
info(` $ cd ${projectDir}`);
info(' Frontend: cd projects/app && pnpm install');
info(' API: cd projects/api && bun install');
info(' Configure projects/api/.env (see .env.example)');
info(' Start Postgres + run prisma generate / migrate');
} else {
info(` Run ${name}`);
info(` $ cd ${projectDir}`);
info(` $ ${toolbox.pm.run('start')}`);
}
info('');

if (!toolbox.parameters.options.fromGluegunMenu) {
Expand Down
78 changes: 52 additions & 26 deletions src/commands/server/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ const NewCommand: GluegunCommand = {
{ description: 'Git branch to clone from', flag: '--branch', required: false, type: 'string' },
{ description: 'Copy from local path instead of cloning', flag: '--copy', required: false, type: 'string' },
{ description: 'Symlink to local path instead of cloning', flag: '--link', required: false, type: 'string' },
{
default: false,
description: 'Use experimental nest-base template (Bun + Prisma + Postgres)',
flag: '--next',
required: false,
type: 'boolean',
},
{
default: false,
description: 'Skip all interactive prompts',
Expand Down Expand Up @@ -87,6 +94,7 @@ const NewCommand: GluegunCommand = {
const cliApiMode = parameters.options['api-mode'] || parameters.options.apiMode;
const cliFrameworkMode = parameters.options['framework-mode'] as 'npm' | 'vendor' | undefined;
const cliFrameworkUpstreamBranch = parameters.options['framework-upstream-branch'] as string | undefined;
const experimental = parameters.options.next === true || parameters.options.next === 'true';

// Determine noConfirm with priority: CLI > config > global > default (false)
const noConfirm = config.getNoConfirm({
Expand All @@ -102,7 +110,9 @@ const NewCommand: GluegunCommand = {
info('Create a new server');

// Hint for non-interactive callers (e.g. Claude Code)
toolbox.tools.nonInteractiveHint('lt server create --name <name> --api-mode <Rest|GraphQL|Both> --noConfirm');
toolbox.tools.nonInteractiveHint(
'lt server create --name <name> --api-mode <Rest|GraphQL|Both> [--next] --noConfirm',
);

// Check git
if (!(await git.gitInstalled())) {
Expand Down Expand Up @@ -168,7 +178,10 @@ const NewCommand: GluegunCommand = {

// Determine API mode with priority: CLI > config > global > interactive (default: Rest)
let apiMode: 'Both' | 'GraphQL' | 'Rest';
if (cliApiMode) {
if (experimental) {
apiMode = 'Rest';
info('Using experimental nest-base template (Bun + Prisma + Postgres + Better-Auth)');
} else if (cliApiMode) {
apiMode = cliApiMode as 'Both' | 'GraphQL' | 'Rest';
} else if (configApiMode) {
apiMode = configApiMode;
Expand Down Expand Up @@ -199,7 +212,9 @@ const NewCommand: GluegunCommand = {
// Determine framework consumption mode — same resolution cascade as
// lt fullstack init: CLI flag > lt.config > interactive (default npm).
let frameworkMode: 'npm' | 'vendor';
if (cliFrameworkMode === 'npm' || cliFrameworkMode === 'vendor') {
if (experimental) {
frameworkMode = 'npm';
} else if (cliFrameworkMode === 'npm' || cliFrameworkMode === 'vendor') {
frameworkMode = cliFrameworkMode;
} else if (cliFrameworkMode) {
error(`Invalid --framework-mode value "${cliFrameworkMode}". Use "npm" or "vendor".`);
Expand Down Expand Up @@ -239,6 +254,7 @@ const NewCommand: GluegunCommand = {
branch,
copyPath,
description,
experimental,
frameworkMode,
frameworkUpstreamBranch,
linkPath,
Expand Down Expand Up @@ -302,28 +318,30 @@ const NewCommand: GluegunCommand = {
// Derive controller type from API mode and save project config
const controllerType: 'Both' | 'GraphQL' | 'Rest' = apiMode;

// Create lt.config.json
const projectConfig = {
commands: {
server: {
module: {
controller: controllerType,
if (!experimental) {
// Create lt.config.json
const projectConfig = {
commands: {
server: {
module: {
controller: controllerType,
},
},
},
},
meta: {
apiMode,
version: '1.0.0',
},
};
meta: {
apiMode,
version: '1.0.0',
},
};

const configPath = filesystem.path(projectDir, 'lt.config.json');
filesystem.write(configPath, projectConfig, { jsonIndent: 2 });
const configPath = filesystem.path(projectDir, 'lt.config.json');
filesystem.write(configPath, projectConfig, { jsonIndent: 2 });

info('');
success(`Configuration saved to ${projectDir}/lt.config.json`);
info(` API mode: ${apiMode}`);
info(` Default controller type: ${controllerType}`);
info('');
success(`Configuration saved to ${projectDir}/lt.config.json`);
info(` API mode: ${apiMode}`);
info(` Default controller type: ${controllerType}`);
}

// We're done, so show what to do next
info('');
Expand All @@ -332,11 +350,19 @@ const NewCommand: GluegunCommand = {
);
info('');
info('Next:');
info(' Start database server (e.g. MongoDB)');
info(` Check config: ${projectDir}/src/config.env.ts`);
info(` Go to project directory: cd ${projectDir}`);
info(` Run tests: ${toolbox.pm.run('test:e2e')}`);
info(` Start server: ${toolbox.pm.run('start')}`);
if (experimental) {
info(` Go to project directory: cd ${projectDir}`);
info(' Install dependencies: bun install');
info(' Configure .env (see .env.example)');
info(' Start Postgres + run prisma generate / migrate');
info(' Start server: bun run dev');
} else {
info(' Start database server (e.g. MongoDB)');
info(` Check config: ${projectDir}/src/config.env.ts`);
info(` Go to project directory: cd ${projectDir}`);
info(` Run tests: ${toolbox.pm.run('test:e2e')}`);
info(` Start server: ${toolbox.pm.run('start')}`);
}
info('');

if (!toolbox.parameters.options.fromGluegunMenu) {
Expand Down
Loading
Loading