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
30 changes: 28 additions & 2 deletions __tests__/git-commands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,27 +25,46 @@ const branchExists = async (branch: string): Promise<boolean> => {
}
};

// Check if working directory is clean (no uncommitted changes)
// Check if working directory is clean — only modifications to tracked
// files count, since untracked files do not block `git pull --rebase`
// and parallel jest tests in this suite scatter `temp-*` directories
// across the working tree (see __tests__/temp-api-mode-*).
const isWorkingDirectoryClean = async (): Promise<boolean> => {
try {
const status = await system.run('git status --porcelain');
const status = await system.run('git status --porcelain --untracked-files=no');
return !status?.trim();
} catch {
return false;
}
};

// Check if the current branch has an upstream tracking branch (so
// `git pull --rebase` has somewhere to pull from). Local-only branches
// — e.g. a freshly created feature branch before its first push — fail
// `lt git update` with "no tracking information" through no fault of
// the command, so the test must skip rather than treat it as a bug.
const hasUpstreamBranch = async (): Promise<boolean> => {
try {
const upstream = await system.run('git rev-parse --abbrev-ref --symbolic-full-name @{u} 2>/dev/null');
return !!upstream?.trim();
} catch {
return false;
}
};

export {};

describe('Git Commands', () => {
let onBranch: boolean;
let hasMainBranch: boolean;
let cleanWorkingDir: boolean;
let hasUpstream: boolean;

beforeAll(async () => {
onBranch = await isOnBranch();
hasMainBranch = await branchExists('main');
cleanWorkingDir = await isWorkingDirectoryClean();
hasUpstream = await hasUpstreamBranch();
});

describe('lt git update', () => {
Expand All @@ -61,6 +80,13 @@ describe('Git Commands', () => {
await expect(cli('git update')).rejects.toThrow(/unstaged changes|uncommitted/i);
return;
}
if (!hasUpstream) {
// Fresh feature branch without `git push -u` yet: `git pull --rebase`
// exits 1 with "no tracking information". That is a configuration
// gap in the local checkout, not a regression in `lt git update`.
await expect(cli('git update')).rejects.toThrow();
return;
}
const output = await cli('git update');
expect(output).toBeDefined();
});
Expand Down
55 changes: 43 additions & 12 deletions src/commands/fullstack/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const NewCommand: GluegunCommand = {
server,
strings: { kebabCase },
system,
template,
} = toolbox;

// Start timer
Expand Down Expand Up @@ -383,18 +384,48 @@ const NewCommand: GluegunCommand = {
// Remove git folder after clone
filesystem.remove(`${projectDir}/.git`);

// Patch CLAUDE.md with project-specific values
const claudeMdPath = `${projectDir}/CLAUDE.md`;
if (filesystem.exists(claudeMdPath)) {
const frontendName = frontend === 'nuxt' ? 'Nuxt 4' : 'Angular';
await patching.update(claudeMdPath, (content: string) =>
content
.replace(/\{\{PROJECT_NAME\}\}/g, () => name)
.replace(/\{\{PROJECT_DIR\}\}/g, () => projectDir)
.replace(/\{\{API_MODE\}\}/g, () => apiMode)
.replace(/\{\{FRAMEWORK_MODE\}\}/g, () => frameworkMode)
.replace(/\{\{FRONTEND_FRAMEWORK\}\}/g, () => frontendName),
);
// Patch root files for the project.
//
// For the classic flow we patch the cloned `lt-monorepo` CLAUDE.md with
// template variables. For `--next` (experimental) we replace the root
// README.md, CLAUDE.md, and create `.claude/QUICKSTART.md` outright,
// because `lt-monorepo`'s root files describe the legacy MongoDB +
// GraphQL stack which is explicitly out of scope for the nest-base
// template — leaving them in place poisons every AI agent's context.
if (experimental) {
const nextTemplateProps = { name, projectDir };

// Render new root files. `template.generate({ target })` overwrites
// anything at `target`, which is what we want — the freshly cloned
// monorepo's stale README/CLAUDE.md must be replaced wholesale.
await template.generate({
props: nextTemplateProps,
target: `${projectDir}/README.md`,
template: 'next-fullstack/README.md.ejs',
});
await template.generate({
props: nextTemplateProps,
target: `${projectDir}/CLAUDE.md`,
template: 'next-fullstack/CLAUDE.md.ejs',
});
await template.generate({
props: nextTemplateProps,
target: `${projectDir}/.claude/QUICKSTART.md`,
template: 'next-fullstack/.claude/QUICKSTART.md.ejs',
});
} else {
const claudeMdPath = `${projectDir}/CLAUDE.md`;
if (filesystem.exists(claudeMdPath)) {
const frontendName = frontend === 'nuxt' ? 'Nuxt 4' : 'Angular';
await patching.update(claudeMdPath, (content: string) =>
content
.replace(/\{\{PROJECT_NAME\}\}/g, () => name)
.replace(/\{\{PROJECT_DIR\}\}/g, () => projectDir)
.replace(/\{\{API_MODE\}\}/g, () => apiMode)
.replace(/\{\{FRAMEWORK_MODE\}\}/g, () => frameworkMode)
.replace(/\{\{FRONTEND_FRAMEWORK\}\}/g, () => frontendName),
);
}
}

// Always initialize git
Expand Down
12 changes: 11 additions & 1 deletion src/extensions/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,24 @@ export class Tools {
* Suggests using CLI parameters instead of interactive prompts.
* Only shows once per session.
*
* Skipped when the caller already passed `--noConfirm` — they're
* deliberately running headless and don't need a hint to repeat the
* command, which would just be confusing noise (the friction-log
* complaint that triggered this guard).
*
* @param usage - Example command with parameters, e.g. "lt fullstack init --name <name> --frontend <nuxt|angular> --noConfirm"
*/
nonInteractiveHint(usage: string): void {
if (this.hintShown || process.stdin.isTTY) {
return;
}
const { parameters, print } = this.toolbox;
const noConfirm = parameters?.options?.noConfirm;
if (noConfirm === true || noConfirm === 'true') {
this.hintShown = true;
return;
}
this.hintShown = true;
const { print } = this.toolbox;
print.info(print.colors.yellow(`Hint: Non-interactive mode detected. Use parameters to skip prompts:`));
print.info(print.colors.yellow(` ${usage}`));
print.info('');
Expand Down
61 changes: 61 additions & 0 deletions src/templates/next-fullstack/.claude/QUICKSTART.md.ejs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# QUICKSTART — `<%= props.name %>` workspace

A fresh AI agent that just opened this workspace. Read this once, then
switch to a subproject.

## You almost certainly want a subproject

| If you are doing | Go to |
| --------------------------------------------------- | ----------------------------------------------------------------------- |
| Backend / API / DB / Prisma / auth / permissions | `cd projects/api` → [`projects/api/.claude/QUICKSTART.md`](../projects/api/.claude/QUICKSTART.md) |
| Frontend / Nuxt / pages / components | `cd projects/app` → its `README.md` and `CLAUDE.md` |
| Cross-project work (deploy, CI, monorepo tooling) | this root level |

The moment you're touching domain code or framework conventions, you
belong in a subproject. This root level is just the workspace shell.

## Bring up the dev environment

```bash
# API
cd projects/api
bun install
bun run setup # generates .env with strong random secrets
docker compose up -d postgres
bun run prepare:schema # only needed before first migrate / when feature flags change
bun run prisma:generate
bun run prisma:migrate
bun run dev # boots Postgres if needed, opens the Dev Hub at /dev

# Frontend (in another shell)
cd projects/app
pnpm install
pnpm dev
```

If `bun run prisma:migrate` fails with a P1000 auth error after
re-running `bun run setup`, the Postgres volume still holds the old
password — run `docker compose down -v && docker compose up -d
postgres` to wipe it, then migrate again.

## Stack

| | API | App |
| -------- | ------------------ | ------------------ |
| Runtime | Bun 1.x | Node 22 |
| Framework| NestJS 11 | Nuxt 4 |
| Database | Postgres 18 | — |
| ORM | Prisma 7 | — |
| Auth | Better-Auth | — |
| Lang | TypeScript strict | TypeScript strict |

For everything else (architecture, conventions, gates, debugging),
switch to the subproject.

## Don'ts

- Don't add repo conventions in this root — they belong in the
subproject they apply to.
- Don't introduce GraphQL / MongoDB / Mongoose / vendor-mode
framework cores. Those are explicitly out of scope for `--next`
workspaces; see the API's `docs/architecture.md` for context.
45 changes: 45 additions & 0 deletions src/templates/next-fullstack/CLAUDE.md.ejs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# CLAUDE.md — `<%= props.name %>` workspace

This is a fullstack workspace with separate Claude Code contexts per
subproject. Pick the right one for your task and read its `CLAUDE.md`
first — that's where the conventions, architecture, and quickstart
live.

## Routing

| Task | Where to read |
| ---------------------------------------------- | ------------------------------------------------------------------- |
| Backend / API / database / Prisma migrations | [`projects/api/CLAUDE.md`](./projects/api/CLAUDE.md) |
| Frontend / Nuxt / pages / components | [`projects/app/CLAUDE.md`](./projects/app/CLAUDE.md) |
| 60-second agent onboarding | [`.claude/QUICKSTART.md`](./.claude/QUICKSTART.md) |

## Tech stack at a glance

- **API** — Bun 1.x · NestJS 11 · Prisma 7 · Postgres 18 · Better-Auth · REST + OpenAPI 3.1
- **App** — Nuxt 4 · Vue 3 · TypeScript

Detailed conventions live in the subproject `CLAUDE.md` files. Don't
add convention rules to this root file — they belong in the subproject
they apply to.

## Out of scope (do not add to either subproject)

- GraphQL / Apollo
- Mongoose / MongoDB
- Vendor mode for the framework core (the `--next` template ships
`nest-base` directly, no `@lenne.tech/nest-server` npm dependency)
- The legacy `@Restricted` / `@Roles` decorators

These are explicitly removed in the `nest-base` template and a real
project should never reintroduce them. See
[`projects/api/docs/architecture.md`](./projects/api/docs/architecture.md)
"Out of scope" for the rationale.

## When you open this repo cold

1. Identify whether the task is backend, frontend, or cross-project.
2. `cd projects/api` (or `projects/app`) and read its `CLAUDE.md`.
3. Run the subproject's quality gates before committing.
4. Cross-project changes (Docker compose, repo hygiene, deploy config)
stay at this root and follow each subproject's conventions when they
touch its files.
48 changes: 48 additions & 0 deletions src/templates/next-fullstack/README.md.ejs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# <%= props.name %>

Fullstack workspace generated by `lt fullstack init --next`. The API is
the next-generation [`nest-base`](https://github.com/lenneTech/nest-base)
template (Bun + Prisma + Postgres + Better-Auth + REST). The app is a
Nuxt 4 frontend.

This root README intentionally stays minimal — each subproject has its
own `README.md` / `CLAUDE.md` / `.claude/` with the detail you actually
need while working in it.

## Layout

- [`projects/api/`](./projects/api/) — NestJS backend (Bun · Prisma 7 · Postgres 18 · Better-Auth)
- [`projects/app/`](./projects/app/) — Nuxt 4 frontend
- [`.claude/QUICKSTART.md`](./.claude/QUICKSTART.md) — agent routing card

## Quick start

### API (Bun + Prisma + Postgres)

```bash
cd projects/api
bun install
bun run setup # generate .env with strong random secrets
docker compose up -d postgres
bun run prepare:schema # concatenate active feature schemas
bun run prisma:generate
bun run prisma:migrate
bun run dev # boots Postgres if needed, opens the Dev Hub
```

The Dev Hub is at `http://localhost:3000/dev`. See
[`projects/api/README.md`](./projects/api/README.md) for the full tour.

### Frontend (Nuxt 4)

```bash
cd projects/app
pnpm install
pnpm dev
```

See [`projects/app/README.md`](./projects/app/README.md).

## License

MIT
Loading