create-basalt 1.9.0 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -16,6 +16,8 @@ Starting a backend project from scratch involves dozens of decisions and files b
16
16
 
17
17
  It works in two ways: interactive mode (answers questions in the terminal) or direct mode with flags (ideal for scripts). It doesn't install dependencies or touch git unless you ask it to (`--install`, `--git`).
18
18
 
19
+ It also stays useful **after** the project exists: `create-basalt update` moves an app to the latest dependency versions, `create-basalt add ui|cli|mcp` adds a feature you didn't pick at creation time, and `create-basalt doctor` / `info` check the project — see [Project commands](#project-commands-update-add-doctor-info). In a scaffolded app they are simply `pnpm basalt update`, `pnpm basalt add ui`, …
20
+
19
21
  ## Installation
20
22
 
21
23
  You don't need to install anything — your package manager's `create` command downloads and runs the package on the spot:
@@ -84,13 +86,14 @@ Usage: npm create basalt <name> [options]
84
86
 
85
87
  | Flag | Default | What it does |
86
88
  | --- | --- | --- |
87
- | `<name>` | — (asked in interactive mode) | Project name (and folder name, unless `--dir`) |
89
+ | `<name>` | — (asked in interactive mode) | Project name (and folder name, unless `--dir`). `update`, `add`, `doctor` and `info` are reserved for the [project commands](#project-commands-update-add-doctor-info) |
90
+ | `--name=<name>` | — | Project name as a flag — the way to create a project literally called `update`/`add`/`doctor`/`info` |
88
91
  | `--dir=<path>` | `./<name>` | Destination folder |
89
92
  | `--no-tenancy` | tenancy **on** | Removes multi-tenancy (`@basaltkit/tenancy`) |
90
93
  | `--no-auth` | auth **on** | Removes authentication (`@basaltkit/auth`, `APP_SECRET`, `/auth/*` routes) |
91
94
  | `--billing` | off | Includes subscriptions/plans (`@basaltkit/subscriptions`, example `free` and `pro` plans) |
92
95
  | `--ui` | off | Generates the `web/` frontend (React + shadcn via `@basaltkit/admin-shadcn` + `@basaltkit/sdk`). **Forces pnpm** — see note below |
93
- | `--cli` | off | Generates the `basalt` CLI (`bin/basalt.ts`, `pnpm basalt` script, `make:*` generators from `@basaltkit/generator`) |
96
+ | `--no-cli` | CLI on | Skips the `basalt` CLI (by default every app gets it: `bin/basalt.ts`, the `pnpm basalt` script, `make:*` generators from `@basaltkit/generator`) |
94
97
  | `--mcp` | off | Exposes read-only routes as MCP tools (`@basaltkit/mcp`) over HTTP at `POST /mcp` — the overview and health endpoints are opted in via `meta.mcp` |
95
98
  | `--prisma` (alias `--db`) | off | Backs the app with PostgreSQL through Prisma: `prisma/schema.prisma`, `prisma.config.ts`, `src/db.ts`, the `@basaltkit/*-prisma` stores instead of the in-memory ones, a required `<APP>_DATABASE_URL`, the `db:*` scripts and `prismaPlugin({ assertMigrated: true })` — see *With `--prisma`* below |
96
99
  | `--install` | off | Installs dependencies at the end (with the detected/chosen manager) |
@@ -117,7 +120,7 @@ Behavior notes (faithful to the code):
117
120
  pnpm create basalt
118
121
 
119
122
  # Full project, no questions, with everything:
120
- pnpm create basalt my-app --billing --ui --cli --prisma --install --git
123
+ pnpm create basalt my-app --billing --ui --prisma --install --git
121
124
 
122
125
  # Minimal API (no tenancy or auth), in another folder:
123
126
  npm create basalt service-api --no-tenancy --no-auth --dir=./apps/service-api
@@ -135,28 +138,31 @@ Always:
135
138
 
136
139
  ```
137
140
  my-app/
138
- ├── package.json # scripts: dev, start, test, typecheck (+ basalt with --cli)
141
+ ├── package.json # scripts: dev, start, test, typecheck, basalt (+ dev:web with --ui); create-basalt as a devDependency
142
+ ├── .basalt/project.json # scaffold manifest: create-basalt version, options, a hash per generated file — commit it
139
143
  ├── tsconfig.json # strict TypeScript, ESM
140
- ├── .env.example # MY_APP_PORT, MY_APP_HOST, MY_APP_LOG_LEVEL, NODE_ENV (+ MY_APP_APP_SECRET with auth) + the --env-file precedence warning
144
+ ├── .env.example # MY_APP_PORT, MY_APP_HOST, MY_APP_LOG_LEVEL, NODE_ENV (+ MY_APP_APP_SECRET with auth) + the precedence warning — committed
145
+ ├── .env # the same values for local development (+ a generated MY_APP_APP_SECRET with auth) — git-ignored, loaded by dev/basalt
141
146
  ├── .gitignore
142
147
  ├── README.md # instructions adapted to the chosen options
143
- ├── pnpm-workspace.yaml # esbuild allowBuilds, @basaltkit/* release-age exclusion (+ "web" member with --ui) + pnpm 11 notes
148
+ ├── pnpm-workspace.yaml # esbuild allowBuilds, @basaltkit/* + create-basalt release-age exclusion (+ "web" member with --ui) + pnpm 11 notes
144
149
  ├── src/
145
150
  │ ├── env.ts # environment variables validated with Zod (@basaltkit/env), app-prefixed
146
151
  │ ├── app.ts # buildApp() with the chosen plugins
147
152
  │ ├── routes.ts # GET / (friendly index) and GET /health
148
- │ └── server.ts # startup + clean shutdown on SIGINT/SIGTERM
153
+ │ ├── server.ts # startup + clean shutdown on SIGINT/SIGTERM (`pnpm start` — loads no .env)
154
+ │ └── dev.ts # `pnpm dev`: loads .env, defaults NODE_ENV=development, starts server.ts
149
155
  └── tests/app.test.ts # smoke test adapted to the options
150
156
  ```
151
157
 
152
- With `--prisma`, adds `prisma/schema.prisma`, `prisma.config.ts`, `src/db.ts`, `prisma/seed.ts` (with tenancy) and the `db:generate` / `db:migrate` / `db:deploy` / `db:seed` scripts. With `--cli`, adds `bin/basalt.ts` and the `"basalt": "tsx bin/basalt.ts"` script. With `--ui`, adds the `web/` folder (Vite 8 + React 19 + Tailwind CSS 4 via `@tailwindcss/vite` + shadcn — Tailwind is configured in `web/src/index.css`, no `tailwind.config.js`/PostCSS — with `web/src/api.ts` built on top of `@basaltkit/sdk`; with auth on it includes a login/register screen).
158
+ With `--prisma`, adds `prisma/schema.prisma`, `prisma.config.ts`, `src/db.ts`, `prisma/seed.ts` (with tenancy) and the `db:generate` / `db:migrate` / `db:deploy` / `db:seed` scripts. The `basalt` script is `create-basalt --project` with `--no-cli` (so `pnpm basalt update|add|doctor|info` works in every app); with `--cli` it is `tsx bin/basalt.ts`, and `bin/basalt.ts` forwards those four commands to create-basalt before booting the app. With `--ui`, adds the `web/` folder (Vite 8 + React 19 + Tailwind CSS 4 via `@tailwindcss/vite` + shadcn — Tailwind is configured in `web/src/index.css`, no `tailwind.config.js`/PostCSS — with `web/src/api.ts` built on top of `@basaltkit/sdk`; with auth on it includes a login/register screen).
153
159
 
154
160
  ### With `--ui`: running the API and frontend
155
161
 
156
162
  ```bash
157
163
  pnpm install
158
- pnpm dev # terminal 1 — API on :3000
159
- pnpm --filter my-app-web dev # terminal 2 — UI at http://localhost:5180
164
+ pnpm dev # terminal 1 — API on :3000
165
+ pnpm dev:web # terminal 2 — UI at http://localhost:5180 (= pnpm --filter my-app-web dev)
160
166
  ```
161
167
 
162
168
  Vite's dev server proxies `/api` to the API — there's no CORS to configure.
@@ -181,7 +187,7 @@ the next restart.
181
187
  ```bash
182
188
  pnpm create basalt my-app --prisma
183
189
  cd my-app && pnpm install # postinstall runs `prisma generate`
184
- cp .env.example .env # point MY_APP_DATABASE_URL at your database
190
+ # .env already points MY_APP_DATABASE_URL at postgres://…@localhost:5432/my_app — start PostgreSQL or edit it
185
191
  pnpm db:migrate # prisma migrate dev — creates the tables and seeds `demo`
186
192
  pnpm dev
187
193
  ```
@@ -197,9 +203,9 @@ the database and host instead of a `500` on the first request. In production:
197
203
  The generated `tests/app.test.ts` skips itself when no database is configured, so
198
204
  `pnpm test` stays green on a machine without PostgreSQL. To add another Basalt
199
205
  domain later, install its `@basaltkit/<domain>-prisma` package, run
200
- `pnpm basalt prisma:sync` (with `--cli`) and then `pnpm db:migrate`.
206
+ `pnpm basalt prisma:sync` (unless `--no-cli`) and then `pnpm db:migrate`.
201
207
 
202
- ### With `--cli`: the `basalt` command line
208
+ ### The `basalt` command line (default; `--no-cli` skips it)
203
209
 
204
210
  ```bash
205
211
  pnpm basalt list # available commands
@@ -209,6 +215,138 @@ pnpm basalt make:resource Project # generates schema → repository → servic
209
215
 
210
216
  With pnpm 11, every `pnpm <script>` and `pnpm exec` first verifies dependencies (`verifyDepsBeforeRun`, default `install`) and runs `pnpm install` when any workspace project is out of sync — so `pnpm basalt …` can need the network. `node_modules/.bin/tsx bin/basalt.ts …` skips that check; `verifyDepsBeforeRun: warn` (commented out in the generated `pnpm-workspace.yaml`) turns it into a warning for the whole project.
211
217
 
218
+ ## Project commands: update, add, doctor, info
219
+
220
+ Run inside an existing Basalt app — any app, created with or without the CLI (`--no-cli`),
221
+ by any create-basalt release:
222
+
223
+ ```bash
224
+ npx create-basalt@latest update # or, in a scaffolded app: pnpm basalt update
225
+ npx create-basalt@latest add ui # pnpm basalt add ui
226
+ npx create-basalt@latest doctor # pnpm basalt doctor
227
+ npx create-basalt@latest info # pnpm basalt info
228
+ ```
229
+
230
+ The first positional argument picks the mode: `update`, `add`, `doctor` and
231
+ `info` are project commands; anything else is a new project's name (use
232
+ `--name=update` to create a project with one of those names). Outside a Basalt
233
+ app (no `package.json`, or no `@basaltkit/*` dependency) they stop with an
234
+ actionable error. Every command has `--help`; `--cwd=<dir>` targets another
235
+ directory, `--pm=` overrides the detected package manager (`packageManager`
236
+ field, then the lockfile), and output is colored unless `NO_COLOR` is set,
237
+ `--no-color` is passed or stdout is not a terminal.
238
+
239
+ **How `pnpm basalt <command>` reaches them.** New apps list `create-basalt` as a
240
+ devDependency. With `--no-cli` the `basalt` script is `create-basalt --project`;
241
+ with `--cli`, `bin/basalt.ts` handles `update`/`add`/`doctor`/`info` **before**
242
+ importing anything of the app (they must work while the app is broken
243
+ mid-upgrade) and runs the installed create-basalt — or `pnpm dlx` /
244
+ `bunx` / `npx create-basalt@latest` when it isn't installed.
245
+
246
+ ### Updating an app
247
+
248
+ ```bash
249
+ pnpm basalt update --dry # the table, nothing written
250
+ pnpm basalt update # asks, writes, installs, runs the codemods
251
+ ```
252
+
253
+ | Flag | What it does |
254
+ | --- | --- |
255
+ | `--dry` | Prints the plan; writes nothing |
256
+ | `-y`, `--yes` | Applies without asking (required when stdin is not a terminal) |
257
+ | `--major` | Lets third-party packages cross a major too |
258
+ | `--only=@basaltkit` | Only the framework packages (and `create-basalt`) |
259
+ | `--no-install` | Writes `package.json`, skips the install and the codemods |
260
+ | `--no-tooling` | Leaves `bin/basalt.ts`, `src/dev.ts`, `.env.example` and the `create-basalt` devDependency alone |
261
+
262
+ The policy is the scaffold's own (the same registry code): `@basaltkit/*` and
263
+ `create-basalt` go to `latest` across majors — each framework major prints a
264
+ link to its CHANGELOG and to the upgrade notes; third-party packages go to the
265
+ newest release **on the app's current major** (a newer major is listed as
266
+ "kept … pass --major"); a version younger than the release-age window (pnpm
267
+ `minimumReleaseAge`, read from the project's `pnpm-workspace.yaml` when set) is
268
+ left for later so the install cannot refuse it. When a framework release
269
+ declares a peer range the app would not meet (say `zod ^5` while zod is held on
270
+ 4), the plan warns before anything is written.
271
+
272
+ Both `package.json` and `web/package.json` are updated, **in place**: only the
273
+ version strings change — order, indentation and range style (`^`, `~`, exact)
274
+ are kept, and ranges `update` does not manage (`workspace:`, tags, git, `>=`) are
275
+ left alone. The lockfile is never edited by hand: the detected package manager
276
+ installs, then the installed `@basaltkit/cli` upgrade codemods run (no app
277
+ boot), then it suggests `pnpm typecheck && pnpm test`. If the install fails the
278
+ edits stay and the command says how to revert (`git checkout -- package.json …`).
279
+ `--offline` is refused: update needs the registry.
280
+
281
+ Project tooling rides along (skip with `--no-tooling`): an app without the
282
+ `create-basalt` devDependency gets it plus a `basalt` script (and a
283
+ `create-basalt` entry in `minimumReleaseAgeExclude`); a `bin/basalt.ts`
284
+ generated by an earlier release gains the project commands, `.env` loading, a
285
+ pre-boot `upgrade` and the readable env error; a `src/dev.ts` from 1.9/1.10 gains
286
+ `.env` loading; and the `.env.example` header stops saying "nothing loads this
287
+ file". Files are patched **only** when they are byte-for-byte a known template
288
+ (or the manifest records them untouched); a customised one is left alone and the
289
+ exact snippet to paste is printed.
290
+
291
+ ### Adding features later
292
+
293
+ ```bash
294
+ pnpm basalt add ui --dry # what would be created/merged/skipped
295
+ pnpm basalt add ui # web/ exactly as --ui scaffolds it
296
+ pnpm basalt add cli # bin/basalt.ts + @basaltkit/cli, generator, prisma
297
+ pnpm basalt add mcp # @basaltkit/mcp at POST /mcp + the dev-only ai-mcp bridge + .mcp.json
298
+ ```
299
+
300
+ `add` adapts the templates to the project as it is now (name, auth/tenancy from
301
+ its dependencies) and plans every change before applying any:
302
+
303
+ - **Existing files are never overwritten** — skipped with a notice; `--force`
304
+ overwrites generated files.
305
+ - **Merged, not replaced:** `package.json` (new dependencies in sorted
306
+ position, scripts such as `dev:web`; an existing entry is never changed),
307
+ `pnpm-workspace.yaml` (`web` added to `packages:`), `.gitignore`, `README.md`.
308
+ - **Your code** (`src/app.ts`, `src/routes.ts`) is regenerated only when
309
+ `.basalt/project.json` proves it untouched, patched where the template's
310
+ anchors are still unambiguous (works with `fastifyPlugin`, `expressPlugin` or
311
+ `honoPlugin`), and otherwise left alone with the exact manual steps printed.
312
+ - `add ui` needs a pnpm project (`web/` is a pnpm workspace member) and touches
313
+ no API code: the Vite dev server proxies `/api`, so there is no CORS to set up.
314
+ - Dependencies are installed afterwards unless `--no-install`; `--offline` uses
315
+ the ranges bundled with this create-basalt instead of the registry.
316
+
317
+ A project created without `--ui` followed by `add ui` ends up with the same
318
+ files as one created with `--ui` (the test suite checks exactly that).
319
+
320
+ ### doctor and info
321
+
322
+ `doctor` is read-only and exits non-zero only on errors (warnings exit 0). It
323
+ checks Node against `engines` and Basalt's `>=22.5.0`; the package manager and
324
+ lockfiles; installed versus declared versions, duplicated `@basaltkit/*`
325
+ versions and unmet peer ranges; framework packages behind `latest` (skip with
326
+ `--offline`); the auth secret (`<PREFIX>_APP_SECRET`, from the environment or
327
+ `.env`, against the `minLength` in `src/env.ts` and the placeholder rules of
328
+ `secret()`); every variable `src/env.ts` **requires** (declared without a
329
+ default — `DATABASE_URL` with `--prisma`) that is set neither in the environment
330
+ nor in `.env` — an error, with the fix (`cp .env.example .env`, set it, start
331
+ PostgreSQL); a missing `.env` next to a `.env.example` (warning); whether the Prisma client is generated and
332
+ migrations exist (whether they are *applied* needs a database —
333
+ `prisma migrate status`); `.mcp.json` when `@basaltkit/ai-mcp` is installed; dev
334
+ tooling declared as a runtime dependency; and an outdated `bin/basalt.ts` or
335
+ `src/dev.ts`.
336
+
337
+ `info` prints create-basalt, Node, OS, package manager, the app's features and
338
+ the declared/installed versions of the framework and key tools — paste it into
339
+ bug reports.
340
+
341
+ ### The scaffold manifest
342
+
343
+ `.basalt/project.json` records the create-basalt version, the options and a
344
+ `sha256` of every file the scaffold wrote (`package.json` excluded — installs
345
+ rewrite it). Commit it: it is how `add` and `update` tell an untouched template
346
+ file from one you edited. Projects created before it existed are handled too —
347
+ features are inferred from `package.json` and the tree, and every file counts as
348
+ yours; the first `add` creates the manifest.
349
+
212
350
  ### `pnpm-workspace.yaml` and pnpm 11
213
351
 
214
352
  - `minimumReleaseAgeExclude` is evaluated **first-match-wins by package name**. To exclude several versions of one package, write ONE entry with a union — `'@types/node@22.20.4 || 26.6.2'` — never two `@types/node@…` entries (only the first would apply). The generated file carries this example as a comment.
@@ -216,7 +354,9 @@ With pnpm 11, every `pnpm <script>` and `pnpm exec` first verifies dependencies
216
354
 
217
355
  ### Environment variables and `--env-file`
218
356
 
219
- Nothing loads `.env` for you. `node --env-file=.env` / `tsx --env-file=.env` **never override a variable already exported in the shell** — in a terminal where another project exported `DATABASE_URL` or `PORT`, an app reading the generic name boots against that value and only fails on the first request that touches it.
357
+ **Development loads `.env`; production does not.** `pnpm dev` (`src/dev.ts`) and `pnpm basalt` (`bin/basalt.ts`, scaffolded unless `--no-cli`) load `.env` from the project root when it exists, with `node --env-file` semantics. `pnpm start` (`src/server.ts`) loads **nothing**: in production the configuration comes from the real environment (or start it yourself with `node --env-file=…`). A new app gets a ready-made `.env` — a copy of `.env.example` with local values and, with auth, a generated `APP_SECRET` — git-ignored (`.gitignore` and `.dockerignore`) and written with mode `0600`; `.env.example` stays the committed template. `prisma.config.ts` loads the same file for the Prisma CLI.
358
+
359
+ Loading `.env` — like `node --env-file=.env` / `tsx --env-file=.env` — **never overrides a variable already exported in the shell**: in a terminal where another project exported `DATABASE_URL` or `PORT`, an app reading the generic name boots against that value and only fails on the first request that touches it.
220
360
 
221
361
  The scaffold closes that trap instead of only warning about it: `src/env.ts` passes an **app-specific prefix** derived from the project name (`my-saas` → `MY_SAAS`) to `defineEnv`, and `.env.example` uses the prefixed names.
222
362
 
@@ -237,6 +377,8 @@ NODE_ENV=development # never prefixed — a Node-wide convention
237
377
  # MY_SAAS_APP_SECRET= # with auth
238
378
  ```
239
379
 
380
+ When a variable is invalid or missing, `pnpm basalt <command>` does not print a stack trace: it lists the variables (`EnvValidationError.report`), says where they were read from (the environment, and `.env` when it exists) and how to fix it — `cp .env.example .env`, fill them in, and for `DATABASE_URL` start PostgreSQL — then exits 1. `BASALT_DEBUG=1` (or `--debug`) shows the stack. The database gets the same treatment: PostgreSQL not answering (`ECONNREFUSED`, Prisma `P1001`, or an `assertMigrated` that could not query it) and an unmigrated database (`PRISMA_NOT_MIGRATED`) print what failed, the database in use — `postgres://host:port/name` and the variable it came from, never the credentials — and the fix (start PostgreSQL, e.g. `docker compose up -d`; check `MY_SAAS_DATABASE_URL`; `pnpm db:migrate`). Any other boot error keeps its stack trace. `pnpm basalt upgrade` (the `@basaltkit/cli` codemods) never boots the app, so it runs even when the environment is incomplete.
381
+
240
382
  Each variable is read as `MY_SAAS_<NAME>` first and **falls back** to the bare `<NAME>`, so a deployment that already exports the generic names keeps booting — while a stray `PORT` in your shell no longer wins. The rest of the app is unchanged: the keys stay bare (`env.PORT`). To require the prefixed names only, edit the generated file to `prefix: { value: 'MY_SAAS', fallback: false }`. Full rules: [`@basaltkit/env`](https://github.com/basaltkit/basalt/tree/main/packages/env#app-specific-prefix-prefix).
241
383
 
242
384
  ### Programmatic usage (Advanced)
@@ -289,7 +431,7 @@ Exported from `create-basalt` (in addition to the `create-basalt` executable):
289
431
  | Field | Type | Description |
290
432
  | --- | --- | --- |
291
433
  | `dir` | `string` | Absolute path of the created folder |
292
- | `files` | `string[]` | Files written (relative, sorted) |
434
+ | `files` | `string[]` | Files written (relative, sorted) — includes the development `.env` |
293
435
  | `options` | `ProjectOptions` | The options actually applied (with defaults resolved) |
294
436
  | `versions` | `VersionResolution \| undefined` | With `resolveLatest`: `{ versions, resolved, failed, heldBack, tooFresh, registry }` — the final ranges, which packages got `^<latest>`, which kept the fallback after a registry failure, third-party packages held back because their latest is a new major, and third-party packages whose latest is not provably older than the release-age window (`{ name, latest, range, reason: 'recent' \| 'unknown' }`) |
295
437
 
@@ -299,6 +441,17 @@ Throws `TargetNotEmptyError` if the destination folder exists and isn't empty.
299
441
 
300
442
  Detects the manager that invoked the command from `npm_config_user_agent` (or the string passed in). Returns `'pnpm' | 'yarn' | 'bun'` when recognized; otherwise `'npm'`.
301
443
 
444
+ ### Project commands (programmatic)
445
+
446
+ | Export | What it does |
447
+ | --- | --- |
448
+ | `runProjectCommand(argv, deps)` | Runs `update`/`add`/`doctor`/`info` and returns the exit code; every side effect (output, prompts, package-manager runs, registry fetch, codemods) is injected through `deps` |
449
+ | `loadProject(dir, pm?)` | Reads an app: package.json (+ `web/`), manifest, detected features and package manager. Throws `NotABasaltAppError` |
450
+ | `planUpdate(ctx, { major?, only?, tooling?, registry? })` | The update plan — entries, the full before/after of every file, tooling, warnings, framework majors. Writes nothing |
451
+ | `planAdd(ctx, feature, { force?, resolveLatest?, registry? })` | The add plan — created/merged/skipped files and manual steps. Writes nothing |
452
+ | `runDoctor(ctx, options)` | Doctor findings (`{ level: 'ok' \| 'info' \| 'warn' \| 'error', area, message }`) |
453
+ | `MANIFEST_PATH`, `readManifest(dir)`, `hashContent(text)` | The scaffold manifest |
454
+
302
455
  ### `TargetNotEmptyError`
303
456
 
304
457
  Error (extends `Error`) with the message `Target directory "<dir>" already exists and is not empty.`
@@ -329,9 +482,12 @@ Outside an interactive terminal (no TTY) the questions are skipped. Always pass
329
482
  Automatic installation failed (network, Node version, etc.). Go into the folder and run `pnpm install` (or the indicated manager) to see the real error.
330
483
 
331
484
  **I started the app and `GET /auth/login` gives a secret error.**
332
- With auth on, `src/env.ts` requires `APP_SECRET` — read as `MY_SAAS_APP_SECRET`, with at least 32 characters (`secret()` supplies a development default only when `NODE_ENV` is explicitly `development`/`test`). Copy `.env.example` to `.env` and set your own secret before going to production.
485
+ With auth on, `src/env.ts` requires `APP_SECRET` — read as `MY_SAAS_APP_SECRET`, with at least 32 characters (`secret()` supplies a development default only when `NODE_ENV` is explicitly `development`/`test`). For development the scaffold's `.env` carries a generated one; in production export your own (`openssl rand -base64 48`) — `pnpm start` does not read `.env`.
486
+
487
+ **`The app cannot start — the database did not answer`.**
488
+ PostgreSQL is not running, or `MY_SAAS_DATABASE_URL` points at the wrong host/port (the message shows which, without credentials). Start it (`docker compose up -d`, or your local service) or fix the URL in `.env`; on a fresh database, `pnpm db:migrate`. `pnpm dev` still prints the raw error (the explainer lives in `bin/basalt.ts` only, so `src/dev.ts` stays a few lines); `pnpm basalt routes` gives the explained version.
333
489
 
334
- **With `--prisma`, the app refuses to boot: `PRISMA_NOT_MIGRATED`.**
490
+ **With `--prisma`, the app refuses to boot: `PRISMA_NOT_MIGRATED`** (from `pnpm basalt`: `The app cannot start — the database is not migrated`).
335
491
  `assertMigrated` reached a database without the `_prisma_migrations` table. The
336
492
  message names the database and host it actually reached (never the
337
493
  credentials): either it is not the database you meant — check
@@ -346,13 +502,28 @@ The Prisma client is generated code, not a checked-in file. `pnpm install` runs
346
502
  or after editing the schema, run `pnpm db:generate`.
347
503
 
348
504
  **My app connects to the wrong database / port.**
349
- A variable exported in your shell beats `--env-file`. The generated `src/env.ts` already reads app-prefixed names (`MY_SAAS_PORT`), so set those rather than the generic ones — the bare names are only a fallback. Check `env | grep DATABASE_URL`, and see *Environment variables and `--env-file`*.
505
+ A variable exported in your shell beats `.env` (as with `--env-file`). The generated `src/env.ts` already reads app-prefixed names (`MY_SAAS_PORT`), so set those rather than the generic ones — the bare names are only a fallback. Check `env | grep DATABASE_URL`, and see *Environment variables and `--env-file`*.
506
+
507
+ **`The app cannot start — invalid environment variables` (or `EnvValidationError: … expected string, received undefined`).**
508
+ A required variable is set neither in your shell nor in `.env`. Apps from create-basalt ≤ 1.10 did not load `.env` at all in `pnpm dev` / `pnpm basalt`: run `npx create-basalt@latest update` once — it patches an unmodified `bin/basalt.ts` and `src/dev.ts`. Then `cp .env.example .env` if there is no `.env`, fill the variables listed, and run `pnpm basalt doctor` to check. `pnpm start` never reads `.env`: export the variables there.
350
509
 
351
510
  **`pnpm basalt …` starts by running `pnpm install` (or fails offline).**
352
511
  pnpm 11's `verifyDepsBeforeRun` (default `install`). Run `node_modules/.bin/tsx bin/basalt.ts …` instead, or set `verifyDepsBeforeRun: warn` in `pnpm-workspace.yaml`.
353
512
 
354
- **I want to change my mind after generating (e.g. add billing).**
355
- There's no "re-scaffold" command. Either generate a new project with the right flags and compare, or add it by hand: install `@basaltkit/subscriptions` and add the `subscriptionsPlugin` to `src/app.ts` (the generated README and the templates serve as reference).
513
+ **I want to change my mind after generating (e.g. add the frontend).**
514
+ `pnpm basalt add ui` (or `npx create-basalt@latest add ui`) — likewise `cli` and `mcp`; see [Adding features later](#adding-features-later). Billing, tenancy and auth are not `add`-able (they reshape `src/app.ts` and the schema): install `@basaltkit/subscriptions` and add the `subscriptionsPlugin` to `src/app.ts` by hand (the generated README and the templates serve as reference).
515
+
516
+ **`npm create basalt update` did not create a project called "update".**
517
+ `update`, `add`, `doctor` and `info` are project commands. Use `npm create basalt -- --name=update`.
518
+
519
+ **`update cannot run with --offline` / `Could not reach https://registry.npmjs.org for any package`.**
520
+ `update` resolves versions from the registry. Drop `--offline`, check the connection, or point `npm_config_registry` at your mirror.
521
+
522
+ **`Not a terminal — nothing written. Re-run with --yes`.**
523
+ `update` and `add` only apply after a confirmation; in CI or a pipe, pass `--yes`.
524
+
525
+ **`pnpm basalt update` says `Unknown command "update"`.**
526
+ The app's `bin/basalt.ts` predates the project commands. Run `npx create-basalt@latest update` once — it patches an unmodified `bin/basalt.ts` (or prints the snippet for a customised one).
356
527
 
357
528
  ## How it connects to other modules
358
529
 
@@ -363,6 +534,6 @@ There's no "re-scaffold" command. Either generate a new project with the right f
363
534
  - **`@basaltkit/auth`** — included by default (remove with `--no-auth`): `/auth/*` routes and `APP_SECRET` validated in `env.ts`.
364
535
  - **`@basaltkit/subscriptions`** — with `--billing`: example `free`/`pro` plans with trial and feature limits.
365
536
  - **`@basaltkit/prisma` + `@basaltkit/{tenancy,auth,teams,subscriptions}-prisma`** — with `--prisma`: the PostgreSQL layer — `prismaPlugin` (tenant-scoped client, boot-time `assertMigrated`), the composed `prisma/schema.prisma` and the Prisma-backed stores for whichever domains are on.
366
- - **`@basaltkit/cli` + `@basaltkit/generator`** — with `--cli`: `bin/basalt.ts` calls `runCli`, and `commandsPlugin(generatorCommands())` registers the `make:*` generators.
537
+ - **`@basaltkit/cli` + `@basaltkit/generator`** — by default (not with `--no-cli`): `bin/basalt.ts` calls `runCli`, and `commandsPlugin(generatorCommands())` registers the `make:*` generators. `update` runs the installed `@basaltkit/cli` upgrade codemods after an install.
367
538
  - **`@basaltkit/sdk` + `@basaltkit/admin-shadcn` + `@basaltkit/admin`** — with `--ui`: the `web/` frontend calls the API through a typed client and uses the shadcn components.
368
539
  - **`@basaltkit/testing`** — always present in `devDependencies`, with a generated smoke test in `tests/app.test.ts`.
package/dist/args.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { PackageManager } from './index.js';
2
- export declare const USAGE = "Usage: npm create basalt <name> [options]\n\nOptions:\n --dir=<path> Target directory (default: ./<name>)\n --no-tenancy Skip multi-tenancy\n --no-auth Skip authentication\n --billing Include subscriptions/billing\n --ui Scaffold a web/ frontend (React + shadcn + SDK)\n --cli Scaffold the 'basalt' CLI (code generators + commands)\n --mcp Expose read-only routes as MCP tools at /mcp\n --prisma, --db Back the app with PostgreSQL through Prisma: schema,\n migrations, prisma-backed stores and a boot-time check\n that the database is the migrated one\n --install Install dependencies (default: yes in an interactive\n terminal, skipped in CI/non-TTY; --no-install to opt out)\n --no-install Never install dependencies\n --git Initialize a git repository with a first commit\n (same default rule as --install; --no-git to opt out)\n --no-git Never initialize a git repository\n --offline Don't query the npm registry for the latest versions;\n use the ranges bundled with this create-basalt release\n --pm=<manager> Package manager: pnpm | npm | yarn | bun (default: auto-detect)\n -y, --yes Skip prompts and accept defaults\n -h, --help Show this help\n\nRun with no name in a terminal to be prompted interactively.\n";
2
+ export declare const USAGE = "Usage: npm create basalt <name> [options]\n\nOptions:\n --name=<name> Project name (the way to create a project named like a\n project command: update, add, doctor, info)\n --dir=<path> Target directory (default: ./<name>)\n --no-tenancy Skip multi-tenancy\n --no-auth Skip authentication\n --billing Include subscriptions/billing\n --ui Scaffold a web/ frontend (React + shadcn + SDK)\n --no-cli Skip the 'basalt' CLI (code generators + commands;\n on by default)\n --mcp Expose read-only routes as MCP tools at /mcp\n --prisma, --db Back the app with PostgreSQL through Prisma: schema,\n migrations, prisma-backed stores and a boot-time check\n that the database is the migrated one\n --install Install dependencies (default: yes in an interactive\n terminal, skipped in CI/non-TTY; --no-install to opt out)\n --no-install Never install dependencies\n --git Initialize a git repository with a first commit\n (same default rule as --install; --no-git to opt out)\n --no-git Never initialize a git repository\n --offline Don't query the npm registry for the latest versions;\n use the ranges bundled with this create-basalt release\n --pm=<manager> Package manager: pnpm | npm | yarn | bun (default: auto-detect)\n -y, --yes Skip prompts and accept defaults\n -h, --help Show this help\n\nRun with no name in a terminal to be prompted interactively.\n\nProject commands \u2014 inside an existing app (in a scaffolded app: pnpm basalt <command>):\n create-basalt update Update dependencies to the latest versions (--dry, --major, \u2026)\n create-basalt add <feature> Add ui | cli | mcp to the app\n create-basalt doctor Check the project's health\n create-basalt info Versions summary for bug reports\n create-basalt <command> --help Options of one command\n";
3
3
  export interface Flags {
4
4
  name?: string;
5
5
  dir?: string;
package/dist/args.js CHANGED
@@ -2,12 +2,15 @@ import { stdout } from 'node:process';
2
2
  export const USAGE = `Usage: npm create basalt <name> [options]
3
3
 
4
4
  Options:
5
+ --name=<name> Project name (the way to create a project named like a
6
+ project command: update, add, doctor, info)
5
7
  --dir=<path> Target directory (default: ./<name>)
6
8
  --no-tenancy Skip multi-tenancy
7
9
  --no-auth Skip authentication
8
10
  --billing Include subscriptions/billing
9
11
  --ui Scaffold a web/ frontend (React + shadcn + SDK)
10
- --cli Scaffold the 'basalt' CLI (code generators + commands)
12
+ --no-cli Skip the 'basalt' CLI (code generators + commands;
13
+ on by default)
11
14
  --mcp Expose read-only routes as MCP tools at /mcp
12
15
  --prisma, --db Back the app with PostgreSQL through Prisma: schema,
13
16
  migrations, prisma-backed stores and a boot-time check
@@ -25,6 +28,13 @@ Options:
25
28
  -h, --help Show this help
26
29
 
27
30
  Run with no name in a terminal to be prompted interactively.
31
+
32
+ Project commands — inside an existing app (in a scaffolded app: pnpm basalt <command>):
33
+ create-basalt update Update dependencies to the latest versions (--dry, --major, …)
34
+ create-basalt add <feature> Add ui | cli | mcp to the app
35
+ create-basalt doctor Check the project's health
36
+ create-basalt info Versions summary for bug reports
37
+ create-basalt <command> --help Options of one command
28
38
  `;
29
39
  export function parseArgs(argv) {
30
40
  const flags = {
@@ -32,7 +42,7 @@ export function parseArgs(argv) {
32
42
  auth: true,
33
43
  billing: false,
34
44
  ui: false,
35
- cli: false,
45
+ cli: true,
36
46
  mcp: false,
37
47
  prisma: false,
38
48
  yes: false,
@@ -49,6 +59,8 @@ export function parseArgs(argv) {
49
59
  flags.ui = true;
50
60
  else if (token === '--cli')
51
61
  flags.cli = true;
62
+ else if (token === '--no-cli')
63
+ flags.cli = false;
52
64
  else if (token === '--mcp')
53
65
  flags.mcp = true;
54
66
  // `--db` reads naturally in a sentence ("scaffold it with a db") and is the
@@ -69,6 +81,8 @@ export function parseArgs(argv) {
69
81
  flags.offline = true;
70
82
  else if (token.startsWith('--dir='))
71
83
  flags.dir = token.slice('--dir='.length);
84
+ else if (token.startsWith('--name='))
85
+ flags.name = token.slice('--name='.length);
72
86
  else if (token.startsWith('--pm='))
73
87
  flags.pm = token.slice('--pm='.length);
74
88
  else if (token === '--help' || token === '-h') {
@@ -0,0 +1,28 @@
1
+ /** Marker proving a bin/basalt.ts already delegates the project commands. */
2
+ export declare const PROJECT_COMMANDS_MARKER = "['update', 'add', 'doctor', 'info'].includes(process.argv[2]";
3
+ /** Marker proving a bin/basalt.ts already has the dev prelude (.env loading, `upgrade`, env errors). */
4
+ export declare const DEV_PRELUDE_MARKER = "process.loadEnvFile(envFile)";
5
+ export declare function basaltBin(): string;
6
+ /**
7
+ * Hashes of every bin/basalt.ts an earlier create-basalt release generated
8
+ * (CRLF-insensitive). Only these — or a file the project's manifest records as
9
+ * untouched — are patched automatically.
10
+ */
11
+ export declare const LEGACY_BIN_HASHES: ReadonlySet<string>;
12
+ export declare const hasProjectCommands: (content: string) => boolean;
13
+ export declare const hasDevPrelude: (content: string) => boolean;
14
+ /**
15
+ * Upgrades a template-generated bin/basalt.ts to the current template, step by
16
+ * step: (1) without the project commands, inserts their prelude after the
17
+ * shebang and turns the remaining static imports into dynamic ones (so they no
18
+ * longer run before it); (2) without the dev prelude, inserts it right after
19
+ * the project commands and routes the app import through `explainBootFailure`.
20
+ * Deterministic for the known templates — for the latest ones (1.9, 1.10) the
21
+ * result is exactly {@link basaltBin}.
22
+ */
23
+ export declare function patchBasaltBin(content: string): string;
24
+ export type BinStatus = 'current' | 'patchable' | 'modified' | 'absent';
25
+ /** Where a bin/basalt.ts stands: already current, an untouched old template, or user code. */
26
+ export declare function binStatus(content: string | undefined, manifestHash?: string): BinStatus;
27
+ /** What to paste by hand into a customised bin/basalt.ts (only the parts it lacks). */
28
+ export declare function manualBinSnippet(content: string): string;