From 1e200cc998a74e334aa4c0a4ba32d7119e31d83d Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 21 Jul 2026 02:40:17 +0000 Subject: [PATCH 1/2] feat(create-bestax): scaffold .claude/launch.json with the AI skills opt-in When the user accepts the AI-skills prompt, also write a .claude/launch.json declaring the app's dev server (npm run dev on port 5173) so Claude Code's browser preview can preview_start the scaffold without rediscovering the dev command. npm keeps the config package-manager-neutral, and --strictPort makes a busy 5173 fail loudly instead of Vite silently drifting to another port. Users who decline the skills opt-in get nothing new. The success message and the scaffolded CLAUDE.md now mention the file, and the LLMs and AI-development docs guides describe it. Fixes #337 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Fh5j4Th9cdaEpZnVirseot --- create-bestax/src/__tests__/constants.test.ts | 21 +++++ .../src/__tests__/project-creator.test.ts | 80 +++++++++++++++++++ create-bestax/src/constants.ts | 30 ++++++- create-bestax/src/project-creator.ts | 5 ++ .../guides/getting-started/ai-development.md | 8 ++ docs/docs/guides/llms/index.md | 7 +- 6 files changed, 147 insertions(+), 4 deletions(-) diff --git a/create-bestax/src/__tests__/constants.test.ts b/create-bestax/src/__tests__/constants.test.ts index 7ad624241..9d7124f61 100644 --- a/create-bestax/src/__tests__/constants.test.ts +++ b/create-bestax/src/__tests__/constants.test.ts @@ -24,6 +24,7 @@ const { PROJECT_NAME_REGEX, MESSAGES, PROMPTS, + LAUNCH_JSON, } = await import('../constants.js'); describe('constants', () => { @@ -206,6 +207,26 @@ describe('constants', () => { }); }); + describe('LAUNCH_JSON', () => { + it('is valid JSON with a single dev configuration', () => { + const launch = JSON.parse(LAUNCH_JSON); + expect(launch.version).toBe('0.0.1'); + expect(launch.configurations).toHaveLength(1); + expect(launch.configurations[0].name).toBe('dev'); + }); + + it('uses package-manager-neutral npm and pins the Vite port with --strictPort', () => { + const config = JSON.parse(LAUNCH_JSON).configurations[0]; + expect(config.runtimeExecutable).toBe('npm'); + expect(config.runtimeArgs).toEqual(['run', 'dev', '--', '--strictPort']); + expect(config.port).toBe(5173); + }); + + it('is mentioned in the skills success message', () => { + expect(MESSAGES.SKILLS_ADDED).toContain('launch.json'); + }); + }); + describe('DEFAULT_PROJECT_NAME', () => { it('should be a valid project name', () => { expect(DEFAULT_PROJECT_NAME).toBeDefined(); diff --git a/create-bestax/src/__tests__/project-creator.test.ts b/create-bestax/src/__tests__/project-creator.test.ts index 56c148a5b..6c92ef67f 100644 --- a/create-bestax/src/__tests__/project-creator.test.ts +++ b/create-bestax/src/__tests__/project-creator.test.ts @@ -276,6 +276,37 @@ describe('ProjectCreator', () => { expect(written).not.toContain('classPrefix'); }); + it('writes .claude/launch.json for the browser preview alongside the skills', async () => { + ( + fileSystem.checkDirectoryExists as jest.MockedFunction< + typeof fileSystem.checkDirectoryExists + > + ).mockResolvedValue(true); + + await projectCreator.setupSkills('/target/path', 'my-app', { + bulmaFlavor: 'complete', + iconLibrary: 'none', + }); + + const written = ( + fs.default.writeFile as unknown as jest.Mock + ).mock.calls.find(([p]) => String(p).includes('launch.json'))?.[1]; + expect(written).toBeDefined(); + const launch = JSON.parse(String(written)); + expect(launch.configurations).toHaveLength(1); + expect(launch.configurations[0]).toEqual({ + name: 'dev', + runtimeExecutable: 'npm', + runtimeArgs: ['run', 'dev', '--', '--strictPort'], + port: 5173, + }); + // the scaffolded CLAUDE.md points agents at the launch config + const claudeMd = ( + fs.default.writeFile as unknown as jest.Mock + ).mock.calls.find(([p]) => String(p).includes('CLAUDE.md'))?.[1]; + expect(claudeMd).toContain('.claude/launch.json'); + }); + it('documents the prefix caveat and icon library for a prefixed scaffold', async () => { ( fileSystem.checkDirectoryExists as jest.MockedFunction< @@ -933,6 +964,55 @@ describe('ProjectCreator', () => { expect(fileSystem.updatePackageJson).toHaveBeenCalled(); }); + it('writes launch.json when the skills opt-in is accepted', async () => { + // target dir does not exist; the bundled skills dir does + ( + fileSystem.checkDirectoryExists as jest.MockedFunction< + typeof fileSystem.checkDirectoryExists + > + ).mockImplementation(async p => String(p).includes('skills')); + ( + fileSystem.isDirectoryEmpty as jest.MockedFunction< + typeof fileSystem.isDirectoryEmpty + > + ).mockResolvedValue(true); + ( + fs.default.existsSync as jest.MockedFunction + ).mockReturnValue(false); + + await projectCreator.create('test-project', { yes: true, skills: true }); + + expect( + (fs.default.writeFile as unknown as jest.Mock).mock.calls.some(([p]) => + String(p).includes('launch.json') + ) + ).toBe(true); + }); + + it('does not write launch.json when the skills opt-in is declined', async () => { + ( + fileSystem.checkDirectoryExists as jest.MockedFunction< + typeof fileSystem.checkDirectoryExists + > + ).mockImplementation(async p => String(p).includes('skills')); + ( + fileSystem.isDirectoryEmpty as jest.MockedFunction< + typeof fileSystem.isDirectoryEmpty + > + ).mockResolvedValue(true); + ( + fs.default.existsSync as jest.MockedFunction + ).mockReturnValue(false); + + await projectCreator.create('test-project', { yes: true, skills: false }); + + expect( + (fs.default.writeFile as unknown as jest.Mock).mock.calls.some(([p]) => + String(p).includes('launch.json') + ) + ).toBe(false); + }); + it('should prompt for missing template option', async () => { const options = { bulma: 'complete', diff --git a/create-bestax/src/constants.ts b/create-bestax/src/constants.ts index 46ae945d1..d649a10d3 100644 --- a/create-bestax/src/constants.ts +++ b/create-bestax/src/constants.ts @@ -38,9 +38,33 @@ export const MESSAGES = { NEXT_STEPS: 'Next steps:', HAPPY_CODING: 'Happy coding! 🎉', SKILLS_ADDED: - '✔ Installed bestax AI skills into .claude/skills/ (+ CLAUDE.md)', + '✔ Installed bestax AI skills into .claude/skills/ (+ CLAUDE.md, .claude/launch.json)', } as const; +// Dev-server manifest written into .claude/ with the skills opt-in so Claude +// Code's browser preview (`preview_start`) can boot the app by name instead of +// rediscovering the dev command from package.json. `npm` is deliberately +// package-manager-neutral (`npm run dev` works whichever PM installed the +// project), and `--strictPort` stops Vite from silently auto-incrementing to +// 5174 when 5173 is busy — a mismatch with the declared `port` must fail +// loudly, not point the preview at nothing. +export const LAUNCH_JSON = + JSON.stringify( + { + version: '0.0.1', + configurations: [ + { + name: 'dev', + runtimeExecutable: 'npm', + runtimeArgs: ['run', 'dev', '--', '--strictPort'], + port: 5173, + }, + ], + }, + null, + 2 + ) + '\n'; + export const PROMPTS = { PROJECT_NAME: 'Project name:', SELECT_FRAMEWORK: 'Select a framework:', @@ -131,6 +155,10 @@ automatically when the task matches: Prefer the library's components and these skills over hand-written Bulma markup or custom CSS. +\`.claude/launch.json\` declares this app's dev server for Claude Code's browser preview +(\`npm run dev\` on port 5173, \`--strictPort\`) — start it from there rather than rediscovering +the command. + ## Docs - Docs site: https://bestax.io diff --git a/create-bestax/src/project-creator.ts b/create-bestax/src/project-creator.ts index 9682e5ba3..d9e190045 100644 --- a/create-bestax/src/project-creator.ts +++ b/create-bestax/src/project-creator.ts @@ -30,6 +30,7 @@ import { ICON_LIBRARIES, BULMA_FLAVORS, CLAUDE_MD, + LAUNCH_JSON, CONFIG_PROVIDER_ICON_VALUES, type ClaudeMdOptions, } from './constants.js'; @@ -117,6 +118,10 @@ export class ProjectCreator { } await fs.copy(skillsSrc, path.join(targetPath, '.claude', 'skills')); + await fs.writeFile( + path.join(targetPath, '.claude', 'launch.json'), + LAUNCH_JSON + ); await fs.writeFile( path.join(targetPath, 'CLAUDE.md'), CLAUDE_MD(projectName, options) diff --git a/docs/docs/guides/getting-started/ai-development.md b/docs/docs/guides/getting-started/ai-development.md index 2d2462f4e..235b9303a 100644 --- a/docs/docs/guides/getting-started/ai-development.md +++ b/docs/docs/guides/getting-started/ai-development.md @@ -103,3 +103,11 @@ machine. reading it. - **Kill switches**: removing the `ai-loop` label stops one PR; a repository variable turns the whole system off. + +## AI-ready scaffolds + +New apps can start AI-ready too: accepting the AI-skills prompt in `npm create bestax@latest` +installs the bestax Agent Skills and a `CLAUDE.md`, plus a `.claude/launch.json` that tells +Claude Code's browser preview how to start the app's dev server (`npm run dev` on port 5173, +with `--strictPort` so a busy port fails loudly instead of silently drifting to another port). +See the [LLMs guide](/docs/guides/llms) for the full AI tooling story. diff --git a/docs/docs/guides/llms/index.md b/docs/docs/guides/llms/index.md index ecab1e3ed..cf8f5f5d2 100644 --- a/docs/docs/guides/llms/index.md +++ b/docs/docs/guides/llms/index.md @@ -35,9 +35,10 @@ npx skills add https://github.com/allxsmith/bestax --skill bestax-layout-scaffol ``` Starting a new app? `pnpm create bestax@latest` offers to **preinstall these skills** -into the generated app's `.claude/skills/` (alongside a `CLAUDE.md`), so a Claude Code -session picks them up automatically. See the [Skills overview](/docs/skills/intro) for -what each one does. +into the generated app's `.claude/skills/` (alongside a `CLAUDE.md` and a +`.claude/launch.json` that lets Claude Code's browser preview start the dev server by +name), so a Claude Code session picks them up automatically. See the +[Skills overview](/docs/skills/intro) for what each one does. ## MCP server (coming soon) From 093d239ac9d01681282aa8fdf58e826d6e3addd3 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 21 Jul 2026 02:52:56 +0000 Subject: [PATCH 2/2] test(create-bestax): assert launch.json presence in e2e scaffolds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The e2e matrix scaffolds with -y, which implies the AI-skills opt-in, so every real scaffold must contain .claude/launch.json — fail the scaffold step loudly if the write path regresses. Follow-up to a deep-review note on #343. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Fh5j4Th9cdaEpZnVirseot --- create-bestax/e2e/utils/scaffold.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/create-bestax/e2e/utils/scaffold.ts b/create-bestax/e2e/utils/scaffold.ts index 87a8599fe..9c04cf17c 100644 --- a/create-bestax/e2e/utils/scaffold.ts +++ b/create-bestax/e2e/utils/scaffold.ts @@ -64,6 +64,13 @@ export async function scaffoldApp(config: ScaffoldConfig): Promise { stdio: 'inherit', }); + // -y implies the AI-skills opt-in, so every real scaffold must contain the + // browser-preview manifest (#337) — fail loudly if the write path regresses. + const launchJsonPath = path.join(outputDir, '.claude', 'launch.json'); + if (!(await fs.pathExists(launchJsonPath))) { + throw new Error(`Expected scaffold to contain ${launchJsonPath}`); + } + // Install dependencies in the scaffolded app with a fresh pnpm resolve. // --ignore-workspace: the app may live inside the monorepo tree, so treat it // as a standalone project rather than a workspace member.