CLI scaffolder for MigraCode bootcamp final projects.
Scaffolds a fullstack monorepo with a Vite frontend and an Express backend, with optional TypeScript, React, TanStack Router, CSS Modules or Tailwind CSS, Vitest, PostgreSQL, and GitHub Actions — all wired up and ready to run.
- Using the CLI (students)
- Repository structure
- Local development
- How to make changes
- Releasing a new version
- Troubleshooting
Students run the tool with a single command — no cloning, no setup:
npx github:Migracode-Barcelona/create-migracode-appThe wizard asks a series of questions one at a time:
| # | Question | Effect |
|---|---|---|
| 1 | Project name | Creates a new folder with this name |
| 2 | TypeScript? | All files use .ts/.tsx; tsconfig.json added to both apps |
| 3 | React? | Adds React 19 and @vitejs/plugin-react |
| 4 | CSS approach? | (only shown if React = yes) CSS Modules (scoped .module.css files) or Tailwind CSS (utility classes + @theme design tokens) |
| 5 | TanStack Router? | (only shown if React = yes) File-based routing with __root, index, about, users routes |
| 6 | Vitest? | Test setup and example tests for both frontend and backend |
| 7 | PostgreSQL? | docker-compose.yml, connection pool, migration script, seeded demo data |
| 8 | GitHub Actions? | .github/workflows/ci.yml that installs, lints, tests, and builds |
After answering, a summary is shown and the student confirms before anything is written to disk.
create-migracode-app/
├── src/
│ ├── index.js ← CLI entry point: asks questions, shows summary
│ ├── generator.js ← Orchestrator: decides which files to write and where
│ ├── utils.js ← write() helper, log() helper, version constants (V)
│ └── templates/
│ ├── packageJsons.js ← root / frontend / backend package.json generators
│ ├── configs.js ← vite.config, tsconfig (frontend + backend)
│ ├── frontend.js ← all frontend source files (main, App, routes, components, tests)
│ ├── backend.js ← all backend source files (index, routes, services, db, tests)
│ ├── infra.js ← docker-compose, .env.example, oxfmtrc, oxlintrc, .gitignore, CI workflow
│ └── readme.js ← the README that gets written into generated projects
├── scripts/
│ └── test-generate.js ← generates all combos non-interactively into test-output/
├── test-output/ ← gitignored; created by npm run test:generate
├── .gitignore
├── package.json
└── README.md ← this file
Data flow:
index.js → prompts user → builds opts object
↓
generator.js → decides which files to create based on opts
↓
templates/*.js → return file content as strings
↓
utils.write() → writes string to disk (creates dirs automatically)
git clone https://github.com/Migracode-Barcelona/create-migracode-app.git
cd create-migracode-app
npm install
npm run devnpm run dev runs node src/index.js directly — no build step, no packing, no linking needed. The interactive wizard starts immediately. cd somewhere sensible before running so the generated project lands in the right place:
cd /tmp
node ~/code/create-migracode-app/src/index.js
# → project gets created at /tmp/<your-project-name>For rapid iteration — when you've changed a template and want to see the result immediately without answering questions — use the test script:
npm run test:generateThis generates 7 representative combinations into test-output/ (which is gitignored) and reports pass/fail for each:
🧪 create-migracode-app — test:generate
full-ts-react-router-vitest-postgres-ci ✔ 31 files
ts-react-vitest-ci ✔ 23 files
ts-react-router-postgres ✔ 25 files
ts-react-router-tailwind-vitest ✔ 27 files
js-react-vitest ✔ 20 files
js-minimal ✔ 12 files
ts-minimal ✔ 17 files
Results: 7 passed
Add --files to also print every file path generated by each combo:
npm run test:generate -- --filesThe combos are defined at the top of scripts/test-generate.js — add new ones there when you add new features that need coverage.
After npm run test:generate, the output sits in test-output/. You can open any file directly:
cat test-output/full-ts-react-router-vitest-postgres-ci/frontend/vite.config.ts
cat test-output/js-minimal/backend/src/services/users.jsOr install and actually run a generated project to confirm it boots:
cd test-output/ts-react-vitest-ci
npm run dev
# frontend → http://localhost:5173
# backend → http://localhost:3000/api/health(The generator already runs npm install in all workspaces during generation, so you can start immediately.)
Run the tests if Vitest was included:
cd frontend && npm test
cd ../backend && npm testEvery generated file is a template function in one of the src/templates/ files. Find the relevant function, edit the returned string, then run npm run test:generate to see the result.
Example: change something in the backend entry point
Open src/templates/backend.js, find backendIndex, edit the string it returns, then verify:
npm run test:generate
cat test-output/ts-react-vitest-ci/backend/src/index.tsStep 1 — add the prompts call in src/index.js after the existing questions. Use select type (not confirm) so the student must make an active choice:
const { useEslint } = await prompts(
{
type: 'select',
name: 'useEslint',
message: 'Add ESLint?',
choices: [
{ title: 'Yes', value: true },
{ title: 'No', value: false },
],
},
{ onCancel }
)Step 2 — add it to the opts object (still in src/index.js):
const opts = {
// ...existing fields
useEslint,
}Step 3 — add a line to the summary block so it appears in the confirmation screen:
` ${kleur.gray('ESLint:')} ${flag(opts.useEslint)}`Step 4 — use opts.useEslint in src/generator.js and relevant templates to conditionally write files or add dependencies.
Step 5 — add at least one combo that tests the new flag to scripts/test-generate.js, then confirm it passes:
npm run test:generateStep 1 — write a template function in the appropriate src/templates/ file:
export function myNewUtil(opts) {
const { useTypeScript } = opts
return `// src/utils/logger.${useTypeScript ? 'ts' : 'js'}
export function log(msg${useTypeScript ? ': string' : ''}) {
console.log('[app]', msg)
}
`
}Step 2 — import it in src/generator.js and call write() inside generateProject():
import { myNewUtil } from './templates/backend.js'
// inside generateProject():
const ext = ts ? 'ts' : 'js'
write(path.join(targetDir, 'backend', `src/utils/logger.${ext}`), myNewUtil(opts))
log(`backend/src/utils/logger.${ext}`)write() creates parent directories automatically.
Step 3 — run npm run test:generate -- --files and confirm the file appears in the output.
All version strings live in one place: the V object in src/utils.js:
export const V = {
vite: '^8.0.16',
react: '^19.2.7',
// ...
}Change the string here and it propagates to every package.json that gets generated.
To find what's outdated, check inside a generated project:
npm run test:generate
cd test-output/full-ts-react-router-vitest-postgres-ci/frontend
npm outdatedThis package is installed directly from GitHub — no npm registry involved. Students always get the latest version of main when they run npx github:Migracode-Barcelona/create-migracode-app.
To release:
-
Make your changes and verify them:
npm run test:generate
-
Commit and push to
main:git add -A git commit -m "your change description" git push
That's it. The next time anyone runs npx github:Migracode-Barcelona/create-migracode-app, they get the updated version.
Note:
npxcaches packages locally. If a student gets stale output after an update, they can force a fresh fetch:npx --yes github:Migracode-Barcelona/create-migracode-appOr clear the npx cache entirely:
rm -rf ~/.npm/_npx
The CLI runs but creates files in the wrong directory
src/index.js resolves the target dir relative to process.cwd(). Make sure you cd to where you want the project created before running.
npm run test:generate fails with "Directory already exists"
The script wipes test-output/ at the start of every run. If it still fails, delete the folder manually:
rm -rf test-output
npm run test:generateGenerated project fails to start — "Cannot find module"
The generator runs npm install in all workspaces automatically. If something went wrong, run it manually:
npm install
cd frontend && npm install && cd ..
cd backend && npm install && cd ..TanStack Router: routeTree.gen.ts missing
This file is auto-generated the first time vite dev runs. It doesn't exist in the scaffold — that's expected. Running npm run dev creates it automatically.