diff --git a/__tests__/git-commands.test.ts b/__tests__/git-commands.test.ts index 9dd501f..e360e56 100644 --- a/__tests__/git-commands.test.ts +++ b/__tests__/git-commands.test.ts @@ -25,27 +25,46 @@ const branchExists = async (branch: string): Promise => { } }; -// 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 => { 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 => { + 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', () => { @@ -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(); }); diff --git a/src/commands/fullstack/init.ts b/src/commands/fullstack/init.ts index 70ee877..b4aa8db 100644 --- a/src/commands/fullstack/init.ts +++ b/src/commands/fullstack/init.ts @@ -26,6 +26,7 @@ const NewCommand: GluegunCommand = { server, strings: { kebabCase }, system, + template, } = toolbox; // Start timer @@ -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 diff --git a/src/extensions/tools.ts b/src/extensions/tools.ts index 7da08c4..8b8ae8e 100644 --- a/src/extensions/tools.ts +++ b/src/extensions/tools.ts @@ -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 --frontend --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(''); diff --git a/src/templates/next-fullstack/.claude/QUICKSTART.md.ejs b/src/templates/next-fullstack/.claude/QUICKSTART.md.ejs new file mode 100644 index 0000000..1adbf3d --- /dev/null +++ b/src/templates/next-fullstack/.claude/QUICKSTART.md.ejs @@ -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. diff --git a/src/templates/next-fullstack/CLAUDE.md.ejs b/src/templates/next-fullstack/CLAUDE.md.ejs new file mode 100644 index 0000000..74ff86c --- /dev/null +++ b/src/templates/next-fullstack/CLAUDE.md.ejs @@ -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. diff --git a/src/templates/next-fullstack/README.md.ejs b/src/templates/next-fullstack/README.md.ejs new file mode 100644 index 0000000..fc2220c --- /dev/null +++ b/src/templates/next-fullstack/README.md.ejs @@ -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