Skip to content

Commit ee3bd6f

Browse files
pascal-klesseclaude
andcommitted
feat(fullstack init --next): auto-run bun run rename after nest-base clone
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 <name>` 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) <noreply@anthropic.com>
1 parent 2e7ac1b commit ee3bd6f

2 files changed

Lines changed: 133 additions & 0 deletions

File tree

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
const fs = require('fs');
2+
const path = require('path');
3+
// `filesystem` / `patching` are required lazily inside the test that
4+
// uses them so this file doesn't collide with the global-scope
5+
// declarations in other test files (e.g.
6+
// `fullstack-claude-md-patching.test.ts`). ts-jest treats every test
7+
// file as part of one TypeScript program, so top-level `const
8+
// { filesystem } = require('gluegun')` here would trigger TS2451.
9+
10+
/**
11+
* Auto-rename behaviour for `lt fullstack init --next`.
12+
*
13+
* The experimental nest-base template ships with hard-coded `nest-base`
14+
* references in four files. After cloning, the CLI runs
15+
* `bun run rename <projectDir>` for the user.
16+
*
17+
* Two things need to hold for that to work end-to-end:
18+
*
19+
* 1. The init.ts code path actually invokes the rename script when
20+
* `--next` is set, and only then. We verify this by reading the
21+
* command source directly — the rename is a single, deterministic
22+
* `system.run` call inside the `experimental` block.
23+
*
24+
* 2. Before invoking the rename, init.ts restores `package.json`'s
25+
* `name` to `"nest-base"` so the planner has a coherent starting
26+
* state across all four files. Otherwise the planner would treat
27+
* `projectDir` as the "old" name, fail to match `# nest-base` in
28+
* the README, and leave the rename half-done. This regression is
29+
* easy to reintroduce, so we cover the package.json reset as a
30+
* black-box test against a fixture.
31+
*/
32+
describe('Fullstack init --next auto-rename', () => {
33+
const initSource = fs.readFileSync(
34+
path.join(__dirname, '..', 'src', 'commands', 'fullstack', 'init.ts'),
35+
'utf8',
36+
);
37+
38+
test('init.ts invokes `bun run rename` when --next is set', () => {
39+
expect(initSource).toMatch(/bun run rename \$\{projectDir\}/);
40+
});
41+
42+
test('rename invocation is gated on the experimental flag', () => {
43+
// Match the entire `if (experimental && apiResult.method !== 'link') {
44+
// ... }` block so a refactor that drops the gate without re-adding it
45+
// breaks the test.
46+
expect(initSource).toMatch(/if \(experimental && apiResult\.method !== 'link'\) \{[\s\S]*?bun run rename/);
47+
});
48+
49+
test('init.ts no longer prints a manual rename hint in the Next section', () => {
50+
// The Next: section for the experimental branch must not tell the
51+
// user to run rename themselves — the CLI does it now.
52+
const nextSection = initSource.match(/info\('Next:'\);[\s\S]*?info\(''\);/);
53+
expect(nextSection).not.toBeNull();
54+
expect(nextSection![0]).not.toMatch(/bun run rename/);
55+
});
56+
57+
describe('package.json name reset', () => {
58+
let tempDir: string;
59+
// Lazy require to avoid a top-level `filesystem` declaration that
60+
// would clash with other test files' globals (see header comment).
61+
const { filesystem, patching } = require('gluegun');
62+
63+
beforeEach(() => {
64+
tempDir = filesystem.path('__tests__', `temp-fullstack-rename-${Date.now()}`);
65+
filesystem.dir(tempDir);
66+
});
67+
68+
afterEach(() => {
69+
filesystem.remove(tempDir);
70+
});
71+
72+
test('reverts package.json name back to "nest-base" so the planner can detect the canonical old slug', async () => {
73+
const pkgPath = filesystem.path(tempDir, 'package.json');
74+
// Simulate state after setupServerForFullstack: the experimental
75+
// patch has already overwritten the template's `name` field with
76+
// the project's kebab-cased directory name.
77+
filesystem.write(pkgPath, {
78+
name: 'my-next-fs',
79+
description: 'API for my-next-fs app',
80+
version: '0.0.0',
81+
});
82+
83+
// This is the exact patch init.ts performs before invoking rename.
84+
await patching.update(pkgPath, (config: Record<string, unknown>) => {
85+
config.name = 'nest-base';
86+
return config;
87+
});
88+
89+
const result = filesystem.read(pkgPath, 'json');
90+
expect(result.name).toBe('nest-base');
91+
// Other fields must survive untouched so the rename is the only
92+
// thing that changes the surrounding state.
93+
expect(result.description).toBe('API for my-next-fs app');
94+
expect(result.version).toBe('0.0.0');
95+
});
96+
});
97+
});

src/commands/fullstack/init.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -525,6 +525,42 @@ const NewCommand: GluegunCommand = {
525525
return;
526526
}
527527

528+
// Auto-run `bun run rename <projectDir>` for the experimental nest-base
529+
// template. The template ships with hard-coded `nest-base` references in
530+
// four files (package.json, README.md, portless.yml, docker-compose.yml).
531+
// The rename script patches all four idempotently. Since the consumer
532+
// already gave us --name, doing this for them is strictly less
533+
// friction-prone than relying on a manual follow-up step (which agents
534+
// and humans both forget). Failure is non-fatal: the workspace is still
535+
// usable, the user can re-run `bun run rename <name>` manually.
536+
//
537+
// Note: setupServerForFullstack already patched projects/api/package.json
538+
// to set `name = projectDir`. The rename planner reads that name as the
539+
// "old" slug, which would short-circuit the README/portless/compose
540+
// rewrites because they still say `nest-base`. We restore the canonical
541+
// `name = "nest-base"` first so the planner has a coherent starting
542+
// state across all four files; the rename then writes the project name
543+
// into every spot consistently.
544+
if (experimental && apiResult.method !== 'link') {
545+
const apiPackageJsonPath = `${apiDest}/package.json`;
546+
if (filesystem.exists(apiPackageJsonPath)) {
547+
await patching.update(apiPackageJsonPath, (config: Record<string, unknown>) => {
548+
config.name = 'nest-base';
549+
return config;
550+
});
551+
}
552+
553+
const renameSpinner = spin(`Rename nest-base → ${projectDir}`);
554+
try {
555+
await system.run(`cd ${apiDest} && bun run rename ${projectDir}`);
556+
renameSpinner.succeed(`Renamed nest-base → ${projectDir} in projects/api`);
557+
} catch (err) {
558+
renameSpinner.warn(
559+
`Auto-rename failed (${(err as Error).message}). Run \`bun run rename ${projectDir}\` manually inside projects/api.`,
560+
);
561+
}
562+
}
563+
528564
// Create lt.config.json for API
529565
// Note: frameworkMode is persisted under meta so that subsequent `lt
530566
// server module` / `addProp` / `permissions` calls can detect the mode

0 commit comments

Comments
 (0)