From ee3bd6ff1de86b75cf2ea6eeeb3be33e434aa0e2 Mon Sep 17 00:00:00 2001 From: Pascal Klesse <54418919+pascal-klesse@users.noreply.github.com> Date: Thu, 30 Apr 2026 14:18:48 +0200 Subject: [PATCH] feat(fullstack init --next): auto-run `bun run rename` after nest-base clone MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The experimental nest-base template hard-codes `nest-base` in four files (package.json, README.md, portless.yml, docker-compose.yml). The repo ships `bun run rename ` to patch them all idempotently, but until now consumers had to remember to invoke it themselves. Friction-log runs kept surfacing the gap: agents skip the manual step, half the workspace still says "nest-base". The CLI already knows the desired name from --name, so it now runs the rename automatically after the API clone. Failure is non-fatal — the workspace is still usable and the warning prints the exact command to retry. Restore the package.json `name` to "nest-base" first: setupServer- ForFullstack pre-patches it to projectDir, which would make the rename planner treat projectDir as the old slug and skip the README/portless/ compose rewrites. Resetting before the run gives the planner a coherent canonical starting state. Co-Authored-By: Claude Opus 4.7 (1M context) --- __tests__/fullstack-init-next-rename.test.ts | 97 ++++++++++++++++++++ src/commands/fullstack/init.ts | 36 ++++++++ 2 files changed, 133 insertions(+) create mode 100644 __tests__/fullstack-init-next-rename.test.ts diff --git a/__tests__/fullstack-init-next-rename.test.ts b/__tests__/fullstack-init-next-rename.test.ts new file mode 100644 index 0000000..a0af247 --- /dev/null +++ b/__tests__/fullstack-init-next-rename.test.ts @@ -0,0 +1,97 @@ +const fs = require('fs'); +const path = require('path'); +// `filesystem` / `patching` are required lazily inside the test that +// uses them so this file doesn't collide with the global-scope +// declarations in other test files (e.g. +// `fullstack-claude-md-patching.test.ts`). ts-jest treats every test +// file as part of one TypeScript program, so top-level `const +// { filesystem } = require('gluegun')` here would trigger TS2451. + +/** + * Auto-rename behaviour for `lt fullstack init --next`. + * + * The experimental nest-base template ships with hard-coded `nest-base` + * references in four files. After cloning, the CLI runs + * `bun run rename ` for the user. + * + * Two things need to hold for that to work end-to-end: + * + * 1. The init.ts code path actually invokes the rename script when + * `--next` is set, and only then. We verify this by reading the + * command source directly — the rename is a single, deterministic + * `system.run` call inside the `experimental` block. + * + * 2. Before invoking the rename, init.ts restores `package.json`'s + * `name` to `"nest-base"` so the planner has a coherent starting + * state across all four files. Otherwise the planner would treat + * `projectDir` as the "old" name, fail to match `# nest-base` in + * the README, and leave the rename half-done. This regression is + * easy to reintroduce, so we cover the package.json reset as a + * black-box test against a fixture. + */ +describe('Fullstack init --next auto-rename', () => { + const initSource = fs.readFileSync( + path.join(__dirname, '..', 'src', 'commands', 'fullstack', 'init.ts'), + 'utf8', + ); + + test('init.ts invokes `bun run rename` when --next is set', () => { + expect(initSource).toMatch(/bun run rename \$\{projectDir\}/); + }); + + test('rename invocation is gated on the experimental flag', () => { + // Match the entire `if (experimental && apiResult.method !== 'link') { + // ... }` block so a refactor that drops the gate without re-adding it + // breaks the test. + expect(initSource).toMatch(/if \(experimental && apiResult\.method !== 'link'\) \{[\s\S]*?bun run rename/); + }); + + test('init.ts no longer prints a manual rename hint in the Next section', () => { + // The Next: section for the experimental branch must not tell the + // user to run rename themselves — the CLI does it now. + const nextSection = initSource.match(/info\('Next:'\);[\s\S]*?info\(''\);/); + expect(nextSection).not.toBeNull(); + expect(nextSection![0]).not.toMatch(/bun run rename/); + }); + + describe('package.json name reset', () => { + let tempDir: string; + // Lazy require to avoid a top-level `filesystem` declaration that + // would clash with other test files' globals (see header comment). + const { filesystem, patching } = require('gluegun'); + + beforeEach(() => { + tempDir = filesystem.path('__tests__', `temp-fullstack-rename-${Date.now()}`); + filesystem.dir(tempDir); + }); + + afterEach(() => { + filesystem.remove(tempDir); + }); + + test('reverts package.json name back to "nest-base" so the planner can detect the canonical old slug', async () => { + const pkgPath = filesystem.path(tempDir, 'package.json'); + // Simulate state after setupServerForFullstack: the experimental + // patch has already overwritten the template's `name` field with + // the project's kebab-cased directory name. + filesystem.write(pkgPath, { + name: 'my-next-fs', + description: 'API for my-next-fs app', + version: '0.0.0', + }); + + // This is the exact patch init.ts performs before invoking rename. + await patching.update(pkgPath, (config: Record) => { + config.name = 'nest-base'; + return config; + }); + + const result = filesystem.read(pkgPath, 'json'); + expect(result.name).toBe('nest-base'); + // Other fields must survive untouched so the rename is the only + // thing that changes the surrounding state. + expect(result.description).toBe('API for my-next-fs app'); + expect(result.version).toBe('0.0.0'); + }); + }); +}); diff --git a/src/commands/fullstack/init.ts b/src/commands/fullstack/init.ts index b4aa8db..fb7f8ec 100644 --- a/src/commands/fullstack/init.ts +++ b/src/commands/fullstack/init.ts @@ -525,6 +525,42 @@ const NewCommand: GluegunCommand = { return; } + // Auto-run `bun run rename ` for the experimental nest-base + // template. The template ships with hard-coded `nest-base` references in + // four files (package.json, README.md, portless.yml, docker-compose.yml). + // The rename script patches all four idempotently. Since the consumer + // already gave us --name, doing this for them is strictly less + // friction-prone than relying on a manual follow-up step (which agents + // and humans both forget). Failure is non-fatal: the workspace is still + // usable, the user can re-run `bun run rename ` manually. + // + // Note: setupServerForFullstack already patched projects/api/package.json + // to set `name = projectDir`. The rename planner reads that name as the + // "old" slug, which would short-circuit the README/portless/compose + // rewrites because they still say `nest-base`. We restore the canonical + // `name = "nest-base"` first so the planner has a coherent starting + // state across all four files; the rename then writes the project name + // into every spot consistently. + if (experimental && apiResult.method !== 'link') { + const apiPackageJsonPath = `${apiDest}/package.json`; + if (filesystem.exists(apiPackageJsonPath)) { + await patching.update(apiPackageJsonPath, (config: Record) => { + config.name = 'nest-base'; + return config; + }); + } + + const renameSpinner = spin(`Rename nest-base → ${projectDir}`); + try { + await system.run(`cd ${apiDest} && bun run rename ${projectDir}`); + renameSpinner.succeed(`Renamed nest-base → ${projectDir} in projects/api`); + } catch (err) { + renameSpinner.warn( + `Auto-rename failed (${(err as Error).message}). Run \`bun run rename ${projectDir}\` manually inside projects/api.`, + ); + } + } + // Create lt.config.json for API // Note: frameworkMode is persisted under meta so that subsequent `lt // server module` / `addProp` / `permissions` calls can detect the mode