create-basalt 1.10.0 → 1.12.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
@@ -93,7 +93,7 @@ Usage: npm create basalt <name> [options]
93
93
  | `--no-auth` | auth **on** | Removes authentication (`@basaltkit/auth`, `APP_SECRET`, `/auth/*` routes) |
94
94
  | `--billing` | off | Includes subscriptions/plans (`@basaltkit/subscriptions`, example `free` and `pro` plans) |
95
95
  | `--ui` | off | Generates the `web/` frontend (React + shadcn via `@basaltkit/admin-shadcn` + `@basaltkit/sdk`). **Forces pnpm** — see note below |
96
- | `--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`) |
97
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` |
98
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 |
99
99
  | `--install` | off | Installs dependencies at the end (with the detected/chosen manager) |
@@ -120,7 +120,7 @@ Behavior notes (faithful to the code):
120
120
  pnpm create basalt
121
121
 
122
122
  # Full project, no questions, with everything:
123
- pnpm create basalt my-app --billing --ui --cli --prisma --install --git
123
+ pnpm create basalt my-app --billing --ui --prisma --install --git
124
124
 
125
125
  # Minimal API (no tenancy or auth), in another folder:
126
126
  npm create basalt service-api --no-tenancy --no-auth --dir=./apps/service-api
@@ -138,10 +138,14 @@ Always:
138
138
 
139
139
  ```
140
140
  my-app/
141
- ├── package.json # scripts: dev, start, test, typecheck, basalt (+ dev:web with --ui); create-basalt as a devDependency
141
+ ├── package.json # scripts: dev, build, start (node dist/src/server.js), start:dev (tsx), test, typecheck, basalt (+ dev:web with --ui); create-basalt as a devDependency
142
142
  ├── .basalt/project.json # scaffold manifest: create-basalt version, options, a hash per generated file — commit it
143
143
  ├── tsconfig.json # strict TypeScript, ESM
144
- ├── .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
+ ├── tsconfig.build.json # what `pnpm build` compiles: src/ → dist/ (rootDir ".", so dist/src/server.js)
145
+ ├── Dockerfile # multi-stage: build with the dev toolchain, run dist/src/server.js on plain node as USER node
146
+ ├── .dockerignore # keeps .env, keys, node_modules and dist out of the build context
147
+ ├── .env.example # MY_APP_PORT, MY_APP_HOST, MY_APP_LOG_LEVEL, NODE_ENV (+ MY_APP_APP_SECRET with auth) + the precedence warning — committed
148
+ ├── .env # the same values for local development (+ a generated MY_APP_APP_SECRET with auth) — git-ignored, loaded by dev/basalt
145
149
  ├── .gitignore
146
150
  ├── README.md # instructions adapted to the chosen options
147
151
  ├── pnpm-workspace.yaml # esbuild allowBuilds, @basaltkit/* + create-basalt release-age exclusion (+ "web" member with --ui) + pnpm 11 notes
@@ -149,11 +153,12 @@ my-app/
149
153
  │ ├── env.ts # environment variables validated with Zod (@basaltkit/env), app-prefixed
150
154
  │ ├── app.ts # buildApp() with the chosen plugins
151
155
  │ ├── routes.ts # GET / (friendly index) and GET /health
152
- │ └── server.ts # startup + clean shutdown on SIGINT/SIGTERM
156
+ │ ├── server.ts # startup + clean shutdown on SIGINT/SIGTERM (`pnpm start` runs it compiled — loads no .env)
157
+ │ └── dev.ts # `pnpm dev`: loads .env, defaults NODE_ENV=development, starts server.ts
153
158
  └── tests/app.test.ts # smoke test adapted to the options
154
159
  ```
155
160
 
156
- 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` without `--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).
161
+ 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 client is generated into `generated/prisma` (outside `src/`, so the build never copies it) and imported as `#db/client.js` through a package.json `imports` alias; `@prisma/client-runtime-utils` is a direct dependency (the generated runtime requires it by name), and `pnpm-workspace.yaml` approves the `prisma` / `@prisma/engines` build 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).
157
162
 
158
163
  ### With `--ui`: running the API and frontend
159
164
 
@@ -185,11 +190,14 @@ the next restart.
185
190
  ```bash
186
191
  pnpm create basalt my-app --prisma
187
192
  cd my-app && pnpm install # postinstall runs `prisma generate`
188
- cp .env.example .env # point MY_APP_DATABASE_URL at your database
189
- pnpm db:migrate # prisma migrate dev — creates the tables and seeds `demo`
193
+ # .env already points MY_APP_DATABASE_URL at postgres://…@localhost:5432/my_app — start PostgreSQL or edit it
194
+ pnpm db:migrate # prisma migrate dev — creates the tables
195
+ pnpm db:seed # prisma db seed — the `demo` tenant
190
196
  pnpm dev
191
197
  ```
192
198
 
199
+ After a schema change: `pnpm db:migrate --name <change>` writes and applies the migration (without `--name` Prisma prompts for one), then `pnpm db:generate` — Prisma 7's `migrate dev` does not regenerate the client.
200
+
193
201
  **Migrations, never `prisma db push`.** The generated app boots with
194
202
  `assertMigrated: true`, which refuses to start unless the database it reached has
195
203
  the `_prisma_migrations` table — written by `prisma migrate dev` /
@@ -201,9 +209,9 @@ the database and host instead of a `500` on the first request. In production:
201
209
  The generated `tests/app.test.ts` skips itself when no database is configured, so
202
210
  `pnpm test` stays green on a machine without PostgreSQL. To add another Basalt
203
211
  domain later, install its `@basaltkit/<domain>-prisma` package, run
204
- `pnpm basalt prisma:sync` (with `--cli`) and then `pnpm db:migrate`.
212
+ `pnpm basalt prisma:sync` (unless `--no-cli`) and then `pnpm db:migrate`.
205
213
 
206
- ### With `--cli`: the `basalt` command line
214
+ ### The `basalt` command line (default; `--no-cli` skips it)
207
215
 
208
216
  ```bash
209
217
  pnpm basalt list # available commands
@@ -215,7 +223,7 @@ With pnpm 11, every `pnpm <script>` and `pnpm exec` first verifies dependencies
215
223
 
216
224
  ## Project commands: update, add, doctor, info
217
225
 
218
- Run inside an existing Basalt app — any app, created with or without `--cli`,
226
+ Run inside an existing Basalt app — any app, created with or without the CLI (`--no-cli`),
219
227
  by any create-basalt release:
220
228
 
221
229
  ```bash
@@ -235,7 +243,7 @@ field, then the lockfile), and output is colored unless `NO_COLOR` is set,
235
243
  `--no-color` is passed or stdout is not a terminal.
236
244
 
237
245
  **How `pnpm basalt <command>` reaches them.** New apps list `create-basalt` as a
238
- devDependency. Without `--cli` the `basalt` script is `create-basalt --project`;
246
+ devDependency. With `--no-cli` the `basalt` script is `create-basalt --project`;
239
247
  with `--cli`, `bin/basalt.ts` handles `update`/`add`/`doctor`/`info` **before**
240
248
  importing anything of the app (they must work while the app is broken
241
249
  mid-upgrade) and runs the installed create-basalt — or `pnpm dlx` /
@@ -255,7 +263,7 @@ pnpm basalt update # asks, writes, installs, runs the codemods
255
263
  | `--major` | Lets third-party packages cross a major too |
256
264
  | `--only=@basaltkit` | Only the framework packages (and `create-basalt`) |
257
265
  | `--no-install` | Writes `package.json`, skips the install and the codemods |
258
- | `--no-tooling` | Leaves `bin/basalt.ts` and the `create-basalt` devDependency alone |
266
+ | `--no-tooling` | Leaves `bin/basalt.ts`, `src/dev.ts`, `.env.example` and the `create-basalt` devDependency alone |
259
267
 
260
268
  The policy is the scaffold's own (the same registry code): `@basaltkit/*` and
261
269
  `create-basalt` go to `latest` across majors — each framework major prints a
@@ -278,10 +286,20 @@ edits stay and the command says how to revert (`git checkout -- package.json …
278
286
 
279
287
  Project tooling rides along (skip with `--no-tooling`): an app without the
280
288
  `create-basalt` devDependency gets it plus a `basalt` script (and a
281
- `create-basalt` entry in `minimumReleaseAgeExclude`), and a `bin/basalt.ts`
282
- generated by an earlier release gains the project commands — **only** when it is
283
- byte-for-byte a known template (or the manifest records it untouched); a
284
- customised one is left alone and the exact snippet to paste is printed.
289
+ `create-basalt` entry in `minimumReleaseAgeExclude`); a `bin/basalt.ts`
290
+ generated by an earlier release gains the project commands, `.env` loading, a
291
+ pre-boot `upgrade` and the readable env error; a `src/dev.ts` from 1.9/1.10 gains
292
+ `.env` loading; and the `.env.example` header stops saying "nothing loads this
293
+ file". Files are patched **only** when they are byte-for-byte a known template
294
+ (or the manifest records them untouched); a customised one is left alone and the
295
+ exact snippet to paste is printed.
296
+
297
+ An app from before the production path is **offered** what it lacks:
298
+ `tsconfig.build.json`, a `build` script, the `Dockerfile` (pnpm apps) and
299
+ `.dockerignore`, and with Prisma `@prisma/client-runtime-utils`. Existing files
300
+ are never touched and an existing `start` script is never rewritten — the two
301
+ script lines to paste are printed instead. A Prisma client still generated under
302
+ `src/generated` gets the move instructions and no Dockerfile until it is moved.
285
303
 
286
304
  ### Adding features later
287
305
 
@@ -320,10 +338,20 @@ lockfiles; installed versus declared versions, duplicated `@basaltkit/*`
320
338
  versions and unmet peer ranges; framework packages behind `latest` (skip with
321
339
  `--offline`); the auth secret (`<PREFIX>_APP_SECRET`, from the environment or
322
340
  `.env`, against the `minLength` in `src/env.ts` and the placeholder rules of
323
- `secret()`) and `DATABASE_URL`; whether the Prisma client is generated and
341
+ `secret()`); every variable `src/env.ts` **requires** (declared without a
342
+ default — `DATABASE_URL` with `--prisma`) that is set neither in the environment
343
+ nor in `.env` — an error, with the fix (`cp .env.example .env`, set it, start
344
+ PostgreSQL); a missing `.env` next to a `.env.example` (warning); whether the Prisma client is generated and
324
345
  migrations exist (whether they are *applied* needs a database —
325
- `prisma migrate status`); `.mcp.json` when `@basaltkit/ai-mcp` is installed; dev
326
- tooling declared as a runtime dependency; and an outdated `bin/basalt.ts`.
346
+ `prisma migrate status`); the `db:seed` script of create-basalt 1.8–1.11
347
+ (`tsx prisma/seed.ts`, which loads no `.env` — a warning with the exact
348
+ replacement, `"db:seed": "prisma db seed"`; `update` prints the same line and
349
+ never rewrites the script); `.mcp.json` when `@basaltkit/ai-mcp` is installed; dev
350
+ tooling declared as a runtime dependency; an outdated `bin/basalt.ts` or
351
+ `src/dev.ts`; and, statically, the production path (a `start` running tsx while
352
+ tsx is a devDependency, no `build` script, `dist/src/server.js` older than
353
+ `src/`, a Prisma client generated under `src/`, a missing
354
+ `@prisma/client-runtime-utils`). It never builds or starts the app.
327
355
 
328
356
  `info` prints create-basalt, Node, OS, package manager, the app's features and
329
357
  the declared/installed versions of the framework and key tools — paste it into
@@ -345,7 +373,9 @@ yours; the first `add` creates the manifest.
345
373
 
346
374
  ### Environment variables and `--env-file`
347
375
 
348
- 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.
376
+ **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.
377
+
378
+ 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.
349
379
 
350
380
  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.
351
381
 
@@ -366,6 +396,8 @@ NODE_ENV=development # never prefixed — a Node-wide convention
366
396
  # MY_SAAS_APP_SECRET= # with auth
367
397
  ```
368
398
 
399
+ 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.
400
+
369
401
  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).
370
402
 
371
403
  ### Programmatic usage (Advanced)
@@ -418,7 +450,7 @@ Exported from `create-basalt` (in addition to the `create-basalt` executable):
418
450
  | Field | Type | Description |
419
451
  | --- | --- | --- |
420
452
  | `dir` | `string` | Absolute path of the created folder |
421
- | `files` | `string[]` | Files written (relative, sorted) |
453
+ | `files` | `string[]` | Files written (relative, sorted) — includes the development `.env` |
422
454
  | `options` | `ProjectOptions` | The options actually applied (with defaults resolved) |
423
455
  | `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' }`) |
424
456
 
@@ -469,9 +501,12 @@ Outside an interactive terminal (no TTY) the questions are skipped. Always pass
469
501
  Automatic installation failed (network, Node version, etc.). Go into the folder and run `pnpm install` (or the indicated manager) to see the real error.
470
502
 
471
503
  **I started the app and `GET /auth/login` gives a secret error.**
472
- 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.
504
+ 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`.
473
505
 
474
- **With `--prisma`, the app refuses to boot: `PRISMA_NOT_MIGRATED`.**
506
+ **`The app cannot start — the database did not answer`.**
507
+ 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.
508
+
509
+ **With `--prisma`, the app refuses to boot: `PRISMA_NOT_MIGRATED`** (from `pnpm basalt`: `The app cannot start — the database is not migrated`).
475
510
  `assertMigrated` reached a database without the `_prisma_migrations` table. The
476
511
  message names the database and host it actually reached (never the
477
512
  credentials): either it is not the database you meant — check
@@ -486,7 +521,10 @@ The Prisma client is generated code, not a checked-in file. `pnpm install` runs
486
521
  or after editing the schema, run `pnpm db:generate`.
487
522
 
488
523
  **My app connects to the wrong database / port.**
489
- 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`*.
524
+ 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`*.
525
+
526
+ **`The app cannot start — invalid environment variables` (or `EnvValidationError: … expected string, received undefined`).**
527
+ 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.
490
528
 
491
529
  **`pnpm basalt …` starts by running `pnpm install` (or fails offline).**
492
530
  pnpm 11's `verifyDepsBeforeRun` (default `install`). Run `node_modules/.bin/tsx bin/basalt.ts …` instead, or set `verifyDepsBeforeRun: warn` in `pnpm-workspace.yaml`.
@@ -515,6 +553,6 @@ The app's `bin/basalt.ts` predates the project commands. Run `npx create-basalt@
515
553
  - **`@basaltkit/auth`** — included by default (remove with `--no-auth`): `/auth/*` routes and `APP_SECRET` validated in `env.ts`.
516
554
  - **`@basaltkit/subscriptions`** — with `--billing`: example `free`/`pro` plans with trial and feature limits.
517
555
  - **`@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.
518
- - **`@basaltkit/cli` + `@basaltkit/generator`** — with `--cli`: `bin/basalt.ts` calls `runCli`, and `commandsPlugin(generatorCommands())` registers the `make:*` generators. `update` runs the installed `@basaltkit/cli` upgrade codemods after an install.
556
+ - **`@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.
519
557
  - **`@basaltkit/sdk` + `@basaltkit/admin-shadcn` + `@basaltkit/admin`** — with `--ui`: the `web/` frontend calls the API through a typed client and uses the shadcn components.
520
558
  - **`@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 --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 --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\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";
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
@@ -9,7 +9,8 @@ Options:
9
9
  --no-auth Skip authentication
10
10
  --billing Include subscriptions/billing
11
11
  --ui Scaffold a web/ frontend (React + shadcn + SDK)
12
- --cli Scaffold the 'basalt' CLI (code generators + commands)
12
+ --no-cli Skip the 'basalt' CLI (code generators + commands;
13
+ on by default)
13
14
  --mcp Expose read-only routes as MCP tools at /mcp
14
15
  --prisma, --db Back the app with PostgreSQL through Prisma: schema,
15
16
  migrations, prisma-backed stores and a boot-time check
@@ -41,7 +42,7 @@ export function parseArgs(argv) {
41
42
  auth: true,
42
43
  billing: false,
43
44
  ui: false,
44
- cli: false,
45
+ cli: true,
45
46
  mcp: false,
46
47
  prisma: false,
47
48
  yes: false,
@@ -58,6 +59,8 @@ export function parseArgs(argv) {
58
59
  flags.ui = true;
59
60
  else if (token === '--cli')
60
61
  flags.cli = true;
62
+ else if (token === '--no-cli')
63
+ flags.cli = false;
61
64
  else if (token === '--mcp')
62
65
  flags.mcp = true;
63
66
  // `--db` reads naturally in a sentence ("scaffold it with a db") and is the
@@ -1,5 +1,7 @@
1
1
  /** Marker proving a bin/basalt.ts already delegates the project commands. */
2
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)";
3
5
  export declare function basaltBin(): string;
4
6
  /**
5
7
  * Hashes of every bin/basalt.ts an earlier create-basalt release generated
@@ -8,16 +10,19 @@ export declare function basaltBin(): string;
8
10
  */
9
11
  export declare const LEGACY_BIN_HASHES: ReadonlySet<string>;
10
12
  export declare const hasProjectCommands: (content: string) => boolean;
13
+ export declare const hasDevPrelude: (content: string) => boolean;
11
14
  /**
12
- * Upgrades a template-generated bin/basalt.ts to delegate the project
13
- * commands: inserts the prelude after the shebang and turns the remaining
14
- * static imports into dynamic ones (so they no longer run before it).
15
- * Deterministic for the known templates — for the latest one the result is
16
- * exactly {@link basaltBin}.
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}.
17
22
  */
18
23
  export declare function patchBasaltBin(content: string): string;
19
24
  export type BinStatus = 'current' | 'patchable' | 'modified' | 'absent';
20
- /** Where a bin/basalt.ts stands: already delegating, an untouched old template, or user code. */
25
+ /** Where a bin/basalt.ts stands: already current, an untouched old template, or user code. */
21
26
  export declare function binStatus(content: string | undefined, manifestHash?: string): BinStatus;
22
- /** What to paste by hand into a customised bin/basalt.ts. */
23
- export declare const MANUAL_BIN_SNIPPET = "Paste this right after the shebang line of bin/basalt.ts, then turn its\nstatic imports of @basaltkit/* and ../src/app.js into dynamic ones\n(`import { runCli } from '@basaltkit/cli'` \u2192 `const { runCli } = await import('@basaltkit/cli')`)\nso they no longer run before it:\n\nimport { spawnSync } from 'node:child_process'\nimport { existsSync, readFileSync } from 'node:fs'\nimport { join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\n// Project commands run BEFORE anything of the app is imported, so they keep\n// working while the app is broken mid-upgrade. create-basalt provides them \u2014\n// the project's devDependency when installed, else its latest release:\n// pnpm basalt update \u2014 update dependencies to the latest versions (--dry to preview)\n// pnpm basalt add ui \u2014 add a feature later (ui, cli, mcp)\n// pnpm basalt doctor \u2014 check the project's health\n// pnpm basalt info \u2014 versions summary for bug reports\nconst root = fileURLToPath(new URL('..', import.meta.url))\nif (['update', 'add', 'doctor', 'info'].includes(process.argv[2] ?? '')) {\n process.exit(createBasalt(process.argv.slice(2)))\n}\n\n/** Runs create-basalt: the installed devDependency, else `<pm> dlx create-basalt@latest`. */\nfunction createBasalt(args: string[]): number {\n const local = join(root, 'node_modules', 'create-basalt', 'dist', 'cli.js')\n if (existsSync(local)) {\n return spawnSync(process.execPath, [local, ...args], { cwd: root, stdio: 'inherit' }).status ?? 1\n }\n let declared = ''\n try {\n declared = String(JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')).packageManager ?? '')\n } catch {\n // No readable package.json: the lockfiles decide.\n }\n const uses = (pm: string, ...lockfiles: string[]): boolean =>\n declared.startsWith(`${pm}@`) || lockfiles.some((file) => existsSync(join(root, file)))\n const [command, prefix]: [string, string[]] = uses('pnpm', 'pnpm-lock.yaml')\n ? ['pnpm', ['dlx']]\n : uses('bun', 'bun.lock', 'bun.lockb')\n ? ['bunx', []]\n : ['npx', ['--yes']]\n const result = spawnSync(command, [...prefix, 'create-basalt@latest', ...args], {\n cwd: root,\n stdio: 'inherit',\n shell: process.platform === 'win32',\n })\n return result.status ?? 1\n}\n";
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;
@@ -55,11 +55,136 @@ function createBasalt(args: string[]): number {
55
55
  return result.status ?? 1
56
56
  }
57
57
  `;
58
+ /**
59
+ * The DEV PRELUDE, right after the project commands and still before anything
60
+ * of the app is imported:
61
+ *
62
+ * - `upgrade` (the @basaltkit/cli codemods) only rewrites source files, so it
63
+ * runs without booting the app — it must keep working while the app cannot
64
+ * start (that is usually why you are upgrading);
65
+ * - `.env` is loaded for development with `node --env-file` semantics: a
66
+ * variable already exported in the shell wins, the file only fills in what
67
+ * is missing (the app-prefixed names in src/env.ts exist because of exactly
68
+ * that rule). `pnpm start` / src/server.ts never loads it;
69
+ * - a boot-time `EnvValidationError` becomes a fix (which variables, where they
70
+ * are read from, what to do) instead of a stack trace.
71
+ */
72
+ const DEV_PRELUDE = `// \`upgrade\` (framework codemods) only rewrites source files: it runs without
73
+ // booting the app, so it works even while the app cannot start.
74
+ if (process.argv[2] === 'upgrade') {
75
+ const { consoleIo, parseArgv, upgradeCommand } = await import('@basaltkit/cli')
76
+ const { args, flags } = parseArgv(process.argv.slice(2))
77
+ // upgradeCommand reads only io/args/flags — there is no app to hand it.
78
+ const context = { io: consoleIo(), args, flags } as unknown as Parameters<typeof upgradeCommand.handle>[0]
79
+ process.exit((await upgradeCommand.handle(context)) ?? 0)
80
+ }
81
+
82
+ // Development loads .env (when present) like \`node --env-file=.env\`: a variable
83
+ // already exported in your shell still wins, the file only fills in what is
84
+ // missing. \`pnpm start\` (production) never loads it — there the configuration
85
+ // comes from the real environment.
86
+ const envFile = join(root, '.env')
87
+ if (existsSync(envFile)) process.loadEnvFile(envFile)
88
+
89
+ /**
90
+ * A boot failure as a fix instead of a stack trace (stack: BASALT_DEBUG=1 or --debug):
91
+ * invalid environment variables, a database that does not answer, one that is not
92
+ * migrated. Anything else is rethrown untouched.
93
+ */
94
+ async function explainBootFailure(error: unknown): Promise<never> {
95
+ const failure = (error ?? {}) as {
96
+ code?: unknown; report?: unknown; message?: unknown; cause?: unknown; errors?: unknown; details?: unknown
97
+ }
98
+ // @basaltkit/prisma's describeDbError diagnosis (assertMigrated attaches it): the cause and its one-line fix.
99
+ const diagnosis = (failure.details as { diagnosis?: { code?: unknown; cause?: unknown; fix?: unknown } } | undefined)?.diagnosis
100
+ const message = String(failure.message ?? '')
101
+ const codes = [failure, failure.cause, ...(Array.isArray(failure.errors) ? failure.errors : [])].map((item) =>
102
+ String((item as { code?: unknown } | null | undefined)?.code ?? ''),
103
+ )
104
+ const unreachable =
105
+ codes.some((code) => /^(ECONNREFUSED|ENOTFOUND|EHOSTUNREACH|ETIMEDOUT|EAI_AGAIN|P1001|P1002)$/.test(code)) ||
106
+ /ECONNREFUSED|ENOTFOUND|EHOSTUNREACH|Can't reach database server/.test(message) ||
107
+ // assertMigrated could not even query the database (down, wrong host/port or credentials).
108
+ (failure.code === 'PRISMA_NOT_MIGRATED' && message.startsWith('Could not verify'))
109
+ const debugHint = 'BASALT_DEBUG=1 shows the stack trace.'
110
+ let explanation: string | undefined
111
+ if (failure.code === 'ENV_INVALID' && Array.isArray(failure.report)) {
112
+ const hasEnvFile = existsSync(envFile)
113
+ explanation = [
114
+ 'The app cannot start — invalid environment variables:',
115
+ ...failure.report.map((line) => ' - ' + String(line)),
116
+ '',
117
+ "Read from: your shell's environment" +
118
+ (hasEnvFile ? ' and ' + envFile + ' (exported variables win).' : ' only — there is no ' + envFile + '.'),
119
+ 'Fix: ' + (hasEnvFile ? 'set them in .env' : 'cp .env.example .env, then fill them in') + ' (or export them)' +
120
+ (existsSync(join(root, 'prisma')) ? ' — for DATABASE_URL, start PostgreSQL and point the URL at it' : '') +
121
+ '. \`pnpm basalt doctor\` checks the environment; ' + debugHint,
122
+ ].join('\\n')
123
+ } else if (diagnosis && (diagnosis.code === 'DB_PERMISSION_DENIED' || diagnosis.code === 'DB_NOT_EMPTY_BASELINE')) {
124
+ const database = await databaseInUse()
125
+ explanation = [
126
+ diagnosis.code === 'DB_PERMISSION_DENIED'
127
+ ? 'The app cannot start — the database refused the app database role (permission denied):'
128
+ : 'The app cannot start — the database has tables but no migration history:',
129
+ ' ' + String(diagnosis.cause),
130
+ 'Database: ' + database.target + ' (from ' + database.name + '; credentials never shown).',
131
+ 'Fix: ' + String(diagnosis.fix) + ' ' + debugHint,
132
+ ].join('\\n')
133
+ } else if (unreachable || failure.code === 'PRISMA_NOT_MIGRATED') {
134
+ const database = await databaseInUse()
135
+ const where = 'Database: ' + database.target + ' (from ' + database.name + '; credentials never shown).'
136
+ explanation = unreachable
137
+ ? [
138
+ 'The app cannot start — the database did not answer.',
139
+ where,
140
+ 'Fix: start PostgreSQL (e.g. \`docker compose up -d\`, or your local service) and check that ' +
141
+ database.name + ' in .env points at it (host, port, user, password); on a new database, then run \`pnpm db:migrate\`. ' + debugHint,
142
+ ].join('\\n')
143
+ : [
144
+ 'The app cannot start — the database is not migrated:',
145
+ ' ' + message,
146
+ where,
147
+ 'Fix: run \`pnpm db:migrate\` (development; \`pnpm db:deploy\` in production) — or, if this is not the database you meant, check ' +
148
+ database.name + ' (a shell may have exported another project\\'s). ' + debugHint,
149
+ ].join('\\n')
150
+ }
151
+ if (explanation === undefined || process.env['BASALT_DEBUG'] === '1' || process.argv.includes('--debug')) throw error
152
+ console.error(explanation)
153
+ process.exit(1)
154
+ }
155
+
156
+ /** The database URL the app booted with — protocol, host, port and database only, never the credentials. */
157
+ async function databaseInUse(): Promise<{ name: string; target: string }> {
158
+ let url: unknown
159
+ try {
160
+ url = ((await import('../src/env.js')).env as unknown as Record<string, unknown>)['DATABASE_URL']
161
+ } catch {
162
+ // env.ts itself failed: there is no URL to show.
163
+ }
164
+ // The variable it came from: the app-prefixed name first (it wins in src/env.ts).
165
+ const name =
166
+ Object.keys(process.env)
167
+ .filter((key) => key.endsWith('DATABASE_URL') && process.env[key] === url)
168
+ .sort((a, b) => b.length - a.length)[0] ?? 'DATABASE_URL'
169
+ try {
170
+ const parsed = new URL(String(url))
171
+ return { name, target: parsed.protocol + '//' + parsed.hostname + ':' + (parsed.port || '5432') + parsed.pathname }
172
+ } catch {
173
+ return { name, target: '(no valid URL)' }
174
+ }
175
+ }
176
+ `;
58
177
  /** Marker proving a bin/basalt.ts already delegates the project commands. */
59
178
  export const PROJECT_COMMANDS_MARKER = "['update', 'add', 'doctor', 'info'].includes(process.argv[2]";
179
+ /** Marker proving a bin/basalt.ts already has the dev prelude (.env loading, `upgrade`, env errors). */
180
+ export const DEV_PRELUDE_MARKER = 'process.loadEnvFile(envFile)';
181
+ const APP_IMPORT = "await import('../src/app.js')";
182
+ const APP_IMPORT_EXPLAINED = `${APP_IMPORT}.catch(explainBootFailure)`;
183
+ const RUN_CLI = 'process.exit(await runCli({ app })';
60
184
  export function basaltBin() {
61
185
  return `#!/usr/bin/env node
62
186
  ${PRELUDE}
187
+ ${DEV_PRELUDE}
63
188
  // The dev tools are imported dynamically, after the project commands above.
64
189
  const { runCli } = await import('@basaltkit/cli')
65
190
  const { generatorCommands } = await import('@basaltkit/generator')
@@ -68,7 +193,7 @@ const { prismaSyncCommand } = await import('@basaltkit/prisma')
68
193
  // Dev tooling: opt into development defaults unless NODE_ENV is already set
69
194
  // (imported dynamically below so env.ts is evaluated after this line).
70
195
  process.env['NODE_ENV'] ??= 'development'
71
- const { buildApp } = await import('../src/app.js')
196
+ const { buildApp } = ${APP_IMPORT_EXPLAINED}
72
197
 
73
198
  // The 'basalt' CLI: boots the app WITH the dev/CLI commands, runs one, shuts down.
74
199
  // The dev tools (@basaltkit/generator; add @basaltkit/ai for ai:*) are imported
@@ -83,7 +208,7 @@ const app = buildApp({
83
208
  logLevel: 'silent',
84
209
  commands: [...generatorCommands(), prismaSyncCommand()],
85
210
  })
86
- process.exit(await runCli({ app }))
211
+ process.exit(await runCli({ app }).catch(explainBootFailure))
87
212
  `;
88
213
  }
89
214
  /**
@@ -100,17 +225,31 @@ export const LEGACY_BIN_HASHES = new Set([
100
225
  'sha256-d09d583f1a39bffcb72f302c5f6c3e2651badea358ba38019238a4aa4b842545',
101
226
  // 2026-09-19 (create-basalt ≤ 1.9): NODE_ENV defaulted before the app import.
102
227
  'sha256-a0df89dee04091a9470f67968ff72cfc3bf64076c1bf3d141ae657c7438a94c8',
228
+ // 2026-10-01 (create-basalt 1.10): project commands (update/add/doctor/info)
229
+ // delegated before boot — but no .env loading and `upgrade` booted the app.
230
+ 'sha256-9091e137a5de39929d0e70b2fd973386b9cc1c2f996e130989a22c795b9d0893',
103
231
  ]);
104
232
  export const hasProjectCommands = (content) => content.includes(PROJECT_COMMANDS_MARKER);
233
+ export const hasDevPrelude = (content) => content.includes(DEV_PRELUDE_MARKER);
105
234
  /**
106
- * Upgrades a template-generated bin/basalt.ts to delegate the project
107
- * commands: inserts the prelude after the shebang and turns the remaining
108
- * static imports into dynamic ones (so they no longer run before it).
109
- * Deterministic for the known templates — for the latest one the result is
110
- * exactly {@link basaltBin}.
235
+ * Upgrades a template-generated bin/basalt.ts to the current template, step by
236
+ * step: (1) without the project commands, inserts their prelude after the
237
+ * shebang and turns the remaining static imports into dynamic ones (so they no
238
+ * longer run before it); (2) without the dev prelude, inserts it right after
239
+ * the project commands and routes the app import through `explainBootFailure`.
240
+ * Deterministic for the known templates — for the latest ones (1.9, 1.10) the
241
+ * result is exactly {@link basaltBin}.
111
242
  */
112
243
  export function patchBasaltBin(content) {
113
- const lines = content.replace(/\r\n/g, '\n').split('\n');
244
+ let text = content.replace(/\r\n/g, '\n');
245
+ if (!hasProjectCommands(text))
246
+ text = addProjectCommands(text);
247
+ if (!hasDevPrelude(text))
248
+ text = addDevPrelude(text);
249
+ return text;
250
+ }
251
+ function addProjectCommands(content) {
252
+ const lines = content.split('\n');
114
253
  const shebang = lines[0]?.startsWith('#!') ? `${lines.shift()}\n` : '';
115
254
  const body = lines.map((line) => {
116
255
  const match = /^import \{ ([^}]+) \} from '([^']+)'$/.exec(line);
@@ -122,19 +261,45 @@ export function patchBasaltBin(content) {
122
261
  }
123
262
  return `${shebang}${PRELUDE}\n${body.join('\n')}`;
124
263
  }
125
- /** Where a bin/basalt.ts stands: already delegating, an untouched old template, or user code. */
264
+ function addDevPrelude(content) {
265
+ const at = content.indexOf(PRELUDE);
266
+ // A file without the template's prelude block is not ours to rearrange.
267
+ if (at < 0)
268
+ return content;
269
+ const end = at + PRELUDE.length + (content.startsWith('\n', at + PRELUDE.length) ? 1 : 0);
270
+ const rest = content
271
+ .slice(end)
272
+ .replace(`= ${APP_IMPORT}\n`, `= ${APP_IMPORT_EXPLAINED}\n`)
273
+ .replace(`${RUN_CLI})\n`, `${RUN_CLI}.catch(explainBootFailure))\n`);
274
+ return `${content.slice(0, end)}${DEV_PRELUDE}\n${rest}`;
275
+ }
276
+ /** Where a bin/basalt.ts stands: already current, an untouched old template, or user code. */
126
277
  export function binStatus(content, manifestHash) {
127
278
  if (content === undefined)
128
279
  return 'absent';
129
- if (hasProjectCommands(content))
280
+ if (hasProjectCommands(content) && hasDevPrelude(content))
130
281
  return 'current';
131
282
  const hash = hashContent(content);
132
283
  return LEGACY_BIN_HASHES.has(hash) || hash === manifestHash ? 'patchable' : 'modified';
133
284
  }
134
- /** What to paste by hand into a customised bin/basalt.ts. */
135
- export const MANUAL_BIN_SNIPPET = `Paste this right after the shebang line of bin/basalt.ts, then turn its
285
+ /** What to paste by hand into a customised bin/basalt.ts (only the parts it lacks). */
286
+ export function manualBinSnippet(content) {
287
+ const steps = [];
288
+ if (!hasProjectCommands(content)) {
289
+ steps.push(`Paste this right after the shebang line of bin/basalt.ts, then turn its
136
290
  static imports of @basaltkit/* and ../src/app.js into dynamic ones
137
291
  (\`import { runCli } from '@basaltkit/cli'\` → \`const { runCli } = await import('@basaltkit/cli')\`)
138
292
  so they no longer run before it:
139
293
 
140
- ${PRELUDE}`;
294
+ ${PRELUDE}`);
295
+ }
296
+ if (!hasDevPrelude(content)) {
297
+ steps.push(`${steps.length > 0 ? 'Then paste' : 'Paste'} this after the project-commands block (before anything of the app is
298
+ imported) — it runs \`upgrade\` without booting the app, loads .env for development
299
+ and explains env errors — and change the app import to
300
+ \`const { buildApp } = ${APP_IMPORT_EXPLAINED}\`:
301
+
302
+ ${DEV_PRELUDE}`);
303
+ }
304
+ return steps.join('\n');
305
+ }
package/dist/cli.js CHANGED
@@ -152,8 +152,12 @@ try {
152
152
  if (flags.prisma) {
153
153
  // Nothing boots before the database exists: env.DATABASE_URL is required and
154
154
  // the app asserts at boot that it reached the MIGRATED database.
155
- steps.push(`cp .env.example .env # then point ${envPrefix(result.options.name)}_DATABASE_URL at your database`);
156
- steps.push(`${pm} run db:migrate${flags.tenancy ? ' # creates the tables and seeds the demo tenant' : ' # creates the tables'}`);
155
+ // .env is already written with a local URL (pnpm dev / pnpm basalt load it).
156
+ steps.push(`# start PostgreSQL, or edit ${envPrefix(result.options.name)}_DATABASE_URL in .env`);
157
+ steps.push(`${pm} run db:migrate # creates the tables`);
158
+ // Prisma 7's `migrate dev` no longer seeds: the demo tenant is its own step.
159
+ if (flags.tenancy)
160
+ steps.push(`${pm} run db:seed # the demo tenant the resolvers expect`);
157
161
  }
158
162
  steps.push(`${pm} run dev${flags.ui ? ' # API on :3000' : ''}`);
159
163
  // `web` is a pnpm workspace member (pnpm-workspace.yaml); `dev:web` runs its
@@ -0,0 +1,23 @@
1
+ /**
2
+ * `src/dev.ts` (the `pnpm dev` entrypoint) and the `.env.example` header across
3
+ * create-basalt releases. Up to 1.10, nothing loaded `.env`: `pnpm dev` booted
4
+ * without the variables the example told you to set. `update` brings untouched
5
+ * copies to the current templates; customised ones get the snippet instead.
6
+ */
7
+ /** Hashes of every src/dev.ts an earlier create-basalt release generated (CRLF-insensitive). */
8
+ export declare const LEGACY_DEV_HASHES: ReadonlySet<string>;
9
+ /** Marker proving a src/dev.ts already loads .env. */
10
+ export declare const DEV_ENV_MARKER = "process.loadEnvFile(";
11
+ export type DevEntryStatus = 'current' | 'patchable' | 'modified' | 'absent';
12
+ /** Where a src/dev.ts stands: loads .env already, an untouched old template, or user code. */
13
+ export declare function devEntryStatus(content: string | undefined, manifestHash?: string): DevEntryStatus;
14
+ /** The current src/dev.ts — what a patchable one becomes. */
15
+ export declare const patchDevEntry: () => string;
16
+ /** What to paste by hand into a customised src/dev.ts. */
17
+ export declare const MANUAL_DEV_SNIPPET = "Paste this at the top of src/dev.ts, BEFORE the server is imported (keep that\nimport dynamic \u2014 `await import('./server.js')` \u2014 so the variables are set first):\n\nimport { existsSync } from 'node:fs'\nimport { fileURLToPath } from 'node:url'\n\n// Development loads .env (when present) like `node --env-file=.env`: a variable\n// already exported in your shell still wins, the file only fills in what is missing.\nconst envFile = fileURLToPath(new URL('../.env', import.meta.url))\nif (existsSync(envFile)) process.loadEnvFile(envFile)\n";
18
+ /**
19
+ * `.env.example` with the old "nothing loads this file" lead swapped for the
20
+ * current one; undefined when the file does not start with the old lead (a
21
+ * newer or hand-written file is left alone — it is only comments).
22
+ */
23
+ export declare function patchEnvExample(content: string): string | undefined;