create-basalt 1.9.0 → 1.10.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 +161 -9
- package/dist/args.d.ts +1 -1
- package/dist/args.js +11 -0
- package/dist/bin-template.d.ts +23 -0
- package/dist/bin-template.js +140 -0
- package/dist/cli.js +44 -5
- package/dist/index.d.ts +9 -1
- package/dist/index.js +32 -26
- package/dist/latest-versions.d.ts +31 -2
- package/dist/latest-versions.js +91 -51
- package/dist/project/add.d.ts +66 -0
- package/dist/project/add.js +322 -0
- package/dist/project/context.d.ts +53 -0
- package/dist/project/context.js +112 -0
- package/dist/project/doctor.d.ts +40 -0
- package/dist/project/doctor.js +350 -0
- package/dist/project/manifest.d.ts +36 -0
- package/dist/project/manifest.js +81 -0
- package/dist/project/package-json.d.ts +57 -0
- package/dist/project/package-json.js +161 -0
- package/dist/project/run.d.ts +55 -0
- package/dist/project/run.js +473 -0
- package/dist/project/semver.d.ts +29 -0
- package/dist/project/semver.js +132 -0
- package/dist/project/term.d.ts +24 -0
- package/dist/project/term.js +39 -0
- package/dist/project/update.d.ts +101 -0
- package/dist/project/update.js +302 -0
- package/dist/templates.d.ts +11 -1
- package/dist/templates.js +48 -42
- package/dist/versions.js +6 -5
- package/package.json +12 -12
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,7 +86,8 @@ 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) |
|
|
@@ -135,12 +138,13 @@ Always:
|
|
|
135
138
|
|
|
136
139
|
```
|
|
137
140
|
my-app/
|
|
138
|
-
├── package.json # scripts: dev, start, test, typecheck (+
|
|
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
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
|
|
141
145
|
├── .gitignore
|
|
142
146
|
├── 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
|
|
147
|
+
├── pnpm-workspace.yaml # esbuild allowBuilds, @basaltkit/* + create-basalt release-age exclusion (+ "web" member with --ui) + pnpm 11 notes
|
|
144
148
|
├── src/
|
|
145
149
|
│ ├── env.ts # environment variables validated with Zod (@basaltkit/env), app-prefixed
|
|
146
150
|
│ ├── app.ts # buildApp() with the chosen plugins
|
|
@@ -149,14 +153,14 @@ my-app/
|
|
|
149
153
|
└── tests/app.test.ts # smoke test adapted to the options
|
|
150
154
|
```
|
|
151
155
|
|
|
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.
|
|
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).
|
|
153
157
|
|
|
154
158
|
### With `--ui`: running the API and frontend
|
|
155
159
|
|
|
156
160
|
```bash
|
|
157
161
|
pnpm install
|
|
158
|
-
pnpm dev
|
|
159
|
-
pnpm
|
|
162
|
+
pnpm dev # terminal 1 — API on :3000
|
|
163
|
+
pnpm dev:web # terminal 2 — UI at http://localhost:5180 (= pnpm --filter my-app-web dev)
|
|
160
164
|
```
|
|
161
165
|
|
|
162
166
|
Vite's dev server proxies `/api` to the API — there's no CORS to configure.
|
|
@@ -209,6 +213,131 @@ pnpm basalt make:resource Project # generates schema → repository → servic
|
|
|
209
213
|
|
|
210
214
|
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
215
|
|
|
216
|
+
## Project commands: update, add, doctor, info
|
|
217
|
+
|
|
218
|
+
Run inside an existing Basalt app — any app, created with or without `--cli`,
|
|
219
|
+
by any create-basalt release:
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
npx create-basalt@latest update # or, in a scaffolded app: pnpm basalt update
|
|
223
|
+
npx create-basalt@latest add ui # pnpm basalt add ui
|
|
224
|
+
npx create-basalt@latest doctor # pnpm basalt doctor
|
|
225
|
+
npx create-basalt@latest info # pnpm basalt info
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
The first positional argument picks the mode: `update`, `add`, `doctor` and
|
|
229
|
+
`info` are project commands; anything else is a new project's name (use
|
|
230
|
+
`--name=update` to create a project with one of those names). Outside a Basalt
|
|
231
|
+
app (no `package.json`, or no `@basaltkit/*` dependency) they stop with an
|
|
232
|
+
actionable error. Every command has `--help`; `--cwd=<dir>` targets another
|
|
233
|
+
directory, `--pm=` overrides the detected package manager (`packageManager`
|
|
234
|
+
field, then the lockfile), and output is colored unless `NO_COLOR` is set,
|
|
235
|
+
`--no-color` is passed or stdout is not a terminal.
|
|
236
|
+
|
|
237
|
+
**How `pnpm basalt <command>` reaches them.** New apps list `create-basalt` as a
|
|
238
|
+
devDependency. Without `--cli` the `basalt` script is `create-basalt --project`;
|
|
239
|
+
with `--cli`, `bin/basalt.ts` handles `update`/`add`/`doctor`/`info` **before**
|
|
240
|
+
importing anything of the app (they must work while the app is broken
|
|
241
|
+
mid-upgrade) and runs the installed create-basalt — or `pnpm dlx` /
|
|
242
|
+
`bunx` / `npx create-basalt@latest` when it isn't installed.
|
|
243
|
+
|
|
244
|
+
### Updating an app
|
|
245
|
+
|
|
246
|
+
```bash
|
|
247
|
+
pnpm basalt update --dry # the table, nothing written
|
|
248
|
+
pnpm basalt update # asks, writes, installs, runs the codemods
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
| Flag | What it does |
|
|
252
|
+
| --- | --- |
|
|
253
|
+
| `--dry` | Prints the plan; writes nothing |
|
|
254
|
+
| `-y`, `--yes` | Applies without asking (required when stdin is not a terminal) |
|
|
255
|
+
| `--major` | Lets third-party packages cross a major too |
|
|
256
|
+
| `--only=@basaltkit` | Only the framework packages (and `create-basalt`) |
|
|
257
|
+
| `--no-install` | Writes `package.json`, skips the install and the codemods |
|
|
258
|
+
| `--no-tooling` | Leaves `bin/basalt.ts` and the `create-basalt` devDependency alone |
|
|
259
|
+
|
|
260
|
+
The policy is the scaffold's own (the same registry code): `@basaltkit/*` and
|
|
261
|
+
`create-basalt` go to `latest` across majors — each framework major prints a
|
|
262
|
+
link to its CHANGELOG and to the upgrade notes; third-party packages go to the
|
|
263
|
+
newest release **on the app's current major** (a newer major is listed as
|
|
264
|
+
"kept … pass --major"); a version younger than the release-age window (pnpm
|
|
265
|
+
`minimumReleaseAge`, read from the project's `pnpm-workspace.yaml` when set) is
|
|
266
|
+
left for later so the install cannot refuse it. When a framework release
|
|
267
|
+
declares a peer range the app would not meet (say `zod ^5` while zod is held on
|
|
268
|
+
4), the plan warns before anything is written.
|
|
269
|
+
|
|
270
|
+
Both `package.json` and `web/package.json` are updated, **in place**: only the
|
|
271
|
+
version strings change — order, indentation and range style (`^`, `~`, exact)
|
|
272
|
+
are kept, and ranges `update` does not manage (`workspace:`, tags, git, `>=`) are
|
|
273
|
+
left alone. The lockfile is never edited by hand: the detected package manager
|
|
274
|
+
installs, then the installed `@basaltkit/cli` upgrade codemods run (no app
|
|
275
|
+
boot), then it suggests `pnpm typecheck && pnpm test`. If the install fails the
|
|
276
|
+
edits stay and the command says how to revert (`git checkout -- package.json …`).
|
|
277
|
+
`--offline` is refused: update needs the registry.
|
|
278
|
+
|
|
279
|
+
Project tooling rides along (skip with `--no-tooling`): an app without the
|
|
280
|
+
`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.
|
|
285
|
+
|
|
286
|
+
### Adding features later
|
|
287
|
+
|
|
288
|
+
```bash
|
|
289
|
+
pnpm basalt add ui --dry # what would be created/merged/skipped
|
|
290
|
+
pnpm basalt add ui # web/ exactly as --ui scaffolds it
|
|
291
|
+
pnpm basalt add cli # bin/basalt.ts + @basaltkit/cli, generator, prisma
|
|
292
|
+
pnpm basalt add mcp # @basaltkit/mcp at POST /mcp + the dev-only ai-mcp bridge + .mcp.json
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
`add` adapts the templates to the project as it is now (name, auth/tenancy from
|
|
296
|
+
its dependencies) and plans every change before applying any:
|
|
297
|
+
|
|
298
|
+
- **Existing files are never overwritten** — skipped with a notice; `--force`
|
|
299
|
+
overwrites generated files.
|
|
300
|
+
- **Merged, not replaced:** `package.json` (new dependencies in sorted
|
|
301
|
+
position, scripts such as `dev:web`; an existing entry is never changed),
|
|
302
|
+
`pnpm-workspace.yaml` (`web` added to `packages:`), `.gitignore`, `README.md`.
|
|
303
|
+
- **Your code** (`src/app.ts`, `src/routes.ts`) is regenerated only when
|
|
304
|
+
`.basalt/project.json` proves it untouched, patched where the template's
|
|
305
|
+
anchors are still unambiguous (works with `fastifyPlugin`, `expressPlugin` or
|
|
306
|
+
`honoPlugin`), and otherwise left alone with the exact manual steps printed.
|
|
307
|
+
- `add ui` needs a pnpm project (`web/` is a pnpm workspace member) and touches
|
|
308
|
+
no API code: the Vite dev server proxies `/api`, so there is no CORS to set up.
|
|
309
|
+
- Dependencies are installed afterwards unless `--no-install`; `--offline` uses
|
|
310
|
+
the ranges bundled with this create-basalt instead of the registry.
|
|
311
|
+
|
|
312
|
+
A project created without `--ui` followed by `add ui` ends up with the same
|
|
313
|
+
files as one created with `--ui` (the test suite checks exactly that).
|
|
314
|
+
|
|
315
|
+
### doctor and info
|
|
316
|
+
|
|
317
|
+
`doctor` is read-only and exits non-zero only on errors (warnings exit 0). It
|
|
318
|
+
checks Node against `engines` and Basalt's `>=22.5.0`; the package manager and
|
|
319
|
+
lockfiles; installed versus declared versions, duplicated `@basaltkit/*`
|
|
320
|
+
versions and unmet peer ranges; framework packages behind `latest` (skip with
|
|
321
|
+
`--offline`); the auth secret (`<PREFIX>_APP_SECRET`, from the environment or
|
|
322
|
+
`.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
|
|
324
|
+
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`.
|
|
327
|
+
|
|
328
|
+
`info` prints create-basalt, Node, OS, package manager, the app's features and
|
|
329
|
+
the declared/installed versions of the framework and key tools — paste it into
|
|
330
|
+
bug reports.
|
|
331
|
+
|
|
332
|
+
### The scaffold manifest
|
|
333
|
+
|
|
334
|
+
`.basalt/project.json` records the create-basalt version, the options and a
|
|
335
|
+
`sha256` of every file the scaffold wrote (`package.json` excluded — installs
|
|
336
|
+
rewrite it). Commit it: it is how `add` and `update` tell an untouched template
|
|
337
|
+
file from one you edited. Projects created before it existed are handled too —
|
|
338
|
+
features are inferred from `package.json` and the tree, and every file counts as
|
|
339
|
+
yours; the first `add` creates the manifest.
|
|
340
|
+
|
|
212
341
|
### `pnpm-workspace.yaml` and pnpm 11
|
|
213
342
|
|
|
214
343
|
- `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.
|
|
@@ -299,6 +428,17 @@ Throws `TargetNotEmptyError` if the destination folder exists and isn't empty.
|
|
|
299
428
|
|
|
300
429
|
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
430
|
|
|
431
|
+
### Project commands (programmatic)
|
|
432
|
+
|
|
433
|
+
| Export | What it does |
|
|
434
|
+
| --- | --- |
|
|
435
|
+
| `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` |
|
|
436
|
+
| `loadProject(dir, pm?)` | Reads an app: package.json (+ `web/`), manifest, detected features and package manager. Throws `NotABasaltAppError` |
|
|
437
|
+
| `planUpdate(ctx, { major?, only?, tooling?, registry? })` | The update plan — entries, the full before/after of every file, tooling, warnings, framework majors. Writes nothing |
|
|
438
|
+
| `planAdd(ctx, feature, { force?, resolveLatest?, registry? })` | The add plan — created/merged/skipped files and manual steps. Writes nothing |
|
|
439
|
+
| `runDoctor(ctx, options)` | Doctor findings (`{ level: 'ok' \| 'info' \| 'warn' \| 'error', area, message }`) |
|
|
440
|
+
| `MANIFEST_PATH`, `readManifest(dir)`, `hashContent(text)` | The scaffold manifest |
|
|
441
|
+
|
|
302
442
|
### `TargetNotEmptyError`
|
|
303
443
|
|
|
304
444
|
Error (extends `Error`) with the message `Target directory "<dir>" already exists and is not empty.`
|
|
@@ -351,8 +491,20 @@ A variable exported in your shell beats `--env-file`. The generated `src/env.ts`
|
|
|
351
491
|
**`pnpm basalt …` starts by running `pnpm install` (or fails offline).**
|
|
352
492
|
pnpm 11's `verifyDepsBeforeRun` (default `install`). Run `node_modules/.bin/tsx bin/basalt.ts …` instead, or set `verifyDepsBeforeRun: warn` in `pnpm-workspace.yaml`.
|
|
353
493
|
|
|
354
|
-
**I want to change my mind after generating (e.g. add
|
|
355
|
-
|
|
494
|
+
**I want to change my mind after generating (e.g. add the frontend).**
|
|
495
|
+
`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).
|
|
496
|
+
|
|
497
|
+
**`npm create basalt update` did not create a project called "update".**
|
|
498
|
+
`update`, `add`, `doctor` and `info` are project commands. Use `npm create basalt -- --name=update`.
|
|
499
|
+
|
|
500
|
+
**`update cannot run with --offline` / `Could not reach https://registry.npmjs.org for any package`.**
|
|
501
|
+
`update` resolves versions from the registry. Drop `--offline`, check the connection, or point `npm_config_registry` at your mirror.
|
|
502
|
+
|
|
503
|
+
**`Not a terminal — nothing written. Re-run with --yes`.**
|
|
504
|
+
`update` and `add` only apply after a confirmation; in CI or a pipe, pass `--yes`.
|
|
505
|
+
|
|
506
|
+
**`pnpm basalt update` says `Unknown command "update"`.**
|
|
507
|
+
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
508
|
|
|
357
509
|
## How it connects to other modules
|
|
358
510
|
|
|
@@ -363,6 +515,6 @@ There's no "re-scaffold" command. Either generate a new project with the right f
|
|
|
363
515
|
- **`@basaltkit/auth`** — included by default (remove with `--no-auth`): `/auth/*` routes and `APP_SECRET` validated in `env.ts`.
|
|
364
516
|
- **`@basaltkit/subscriptions`** — with `--billing`: example `free`/`pro` plans with trial and feature limits.
|
|
365
517
|
- **`@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.
|
|
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.
|
|
367
519
|
- **`@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
520
|
- **`@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 --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";
|
|
3
3
|
export interface Flags {
|
|
4
4
|
name?: string;
|
|
5
5
|
dir?: string;
|
package/dist/args.js
CHANGED
|
@@ -2,6 +2,8 @@ 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
|
|
@@ -25,6 +27,13 @@ Options:
|
|
|
25
27
|
-h, --help Show this help
|
|
26
28
|
|
|
27
29
|
Run with no name in a terminal to be prompted interactively.
|
|
30
|
+
|
|
31
|
+
Project commands — inside an existing app (in a scaffolded app: pnpm basalt <command>):
|
|
32
|
+
create-basalt update Update dependencies to the latest versions (--dry, --major, …)
|
|
33
|
+
create-basalt add <feature> Add ui | cli | mcp to the app
|
|
34
|
+
create-basalt doctor Check the project's health
|
|
35
|
+
create-basalt info Versions summary for bug reports
|
|
36
|
+
create-basalt <command> --help Options of one command
|
|
28
37
|
`;
|
|
29
38
|
export function parseArgs(argv) {
|
|
30
39
|
const flags = {
|
|
@@ -69,6 +78,8 @@ export function parseArgs(argv) {
|
|
|
69
78
|
flags.offline = true;
|
|
70
79
|
else if (token.startsWith('--dir='))
|
|
71
80
|
flags.dir = token.slice('--dir='.length);
|
|
81
|
+
else if (token.startsWith('--name='))
|
|
82
|
+
flags.name = token.slice('--name='.length);
|
|
72
83
|
else if (token.startsWith('--pm='))
|
|
73
84
|
flags.pm = token.slice('--pm='.length);
|
|
74
85
|
else if (token === '--help' || token === '-h') {
|
|
@@ -0,0 +1,23 @@
|
|
|
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
|
+
export declare function basaltBin(): string;
|
|
4
|
+
/**
|
|
5
|
+
* Hashes of every bin/basalt.ts an earlier create-basalt release generated
|
|
6
|
+
* (CRLF-insensitive). Only these — or a file the project's manifest records as
|
|
7
|
+
* untouched — are patched automatically.
|
|
8
|
+
*/
|
|
9
|
+
export declare const LEGACY_BIN_HASHES: ReadonlySet<string>;
|
|
10
|
+
export declare const hasProjectCommands: (content: string) => boolean;
|
|
11
|
+
/**
|
|
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}.
|
|
17
|
+
*/
|
|
18
|
+
export declare function patchBasaltBin(content: string): string;
|
|
19
|
+
export type BinStatus = 'current' | 'patchable' | 'modified' | 'absent';
|
|
20
|
+
/** Where a bin/basalt.ts stands: already delegating, an untouched old template, or user code. */
|
|
21
|
+
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";
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { hashContent } from './project/manifest.js';
|
|
2
|
+
/**
|
|
3
|
+
* `bin/basalt.ts` — the app's `basalt` CLI (scaffolded with `--cli`, or added
|
|
4
|
+
* later with `create-basalt add cli`).
|
|
5
|
+
*
|
|
6
|
+
* The file opens with the PROJECT-COMMANDS PRELUDE: `update`, `add`, `doctor`
|
|
7
|
+
* and `info` are handed to create-basalt BEFORE anything of the app is
|
|
8
|
+
* imported — those commands exist precisely for the moments the app does not
|
|
9
|
+
* boot (mid-upgrade, a missing dependency). That is also why the dev tools
|
|
10
|
+
* below are imported dynamically: a static import is evaluated before the
|
|
11
|
+
* first statement, so a broken `@basaltkit/generator` would take `pnpm basalt
|
|
12
|
+
* update` down with it.
|
|
13
|
+
*/
|
|
14
|
+
const PRELUDE = `import { spawnSync } from 'node:child_process'
|
|
15
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
16
|
+
import { join } from 'node:path'
|
|
17
|
+
import { fileURLToPath } from 'node:url'
|
|
18
|
+
|
|
19
|
+
// Project commands run BEFORE anything of the app is imported, so they keep
|
|
20
|
+
// working while the app is broken mid-upgrade. create-basalt provides them —
|
|
21
|
+
// the project's devDependency when installed, else its latest release:
|
|
22
|
+
// pnpm basalt update — update dependencies to the latest versions (--dry to preview)
|
|
23
|
+
// pnpm basalt add ui — add a feature later (ui, cli, mcp)
|
|
24
|
+
// pnpm basalt doctor — check the project's health
|
|
25
|
+
// pnpm basalt info — versions summary for bug reports
|
|
26
|
+
const root = fileURLToPath(new URL('..', import.meta.url))
|
|
27
|
+
if (['update', 'add', 'doctor', 'info'].includes(process.argv[2] ?? '')) {
|
|
28
|
+
process.exit(createBasalt(process.argv.slice(2)))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Runs create-basalt: the installed devDependency, else \`<pm> dlx create-basalt@latest\`. */
|
|
32
|
+
function createBasalt(args: string[]): number {
|
|
33
|
+
const local = join(root, 'node_modules', 'create-basalt', 'dist', 'cli.js')
|
|
34
|
+
if (existsSync(local)) {
|
|
35
|
+
return spawnSync(process.execPath, [local, ...args], { cwd: root, stdio: 'inherit' }).status ?? 1
|
|
36
|
+
}
|
|
37
|
+
let declared = ''
|
|
38
|
+
try {
|
|
39
|
+
declared = String(JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')).packageManager ?? '')
|
|
40
|
+
} catch {
|
|
41
|
+
// No readable package.json: the lockfiles decide.
|
|
42
|
+
}
|
|
43
|
+
const uses = (pm: string, ...lockfiles: string[]): boolean =>
|
|
44
|
+
declared.startsWith(\`\${pm}@\`) || lockfiles.some((file) => existsSync(join(root, file)))
|
|
45
|
+
const [command, prefix]: [string, string[]] = uses('pnpm', 'pnpm-lock.yaml')
|
|
46
|
+
? ['pnpm', ['dlx']]
|
|
47
|
+
: uses('bun', 'bun.lock', 'bun.lockb')
|
|
48
|
+
? ['bunx', []]
|
|
49
|
+
: ['npx', ['--yes']]
|
|
50
|
+
const result = spawnSync(command, [...prefix, 'create-basalt@latest', ...args], {
|
|
51
|
+
cwd: root,
|
|
52
|
+
stdio: 'inherit',
|
|
53
|
+
shell: process.platform === 'win32',
|
|
54
|
+
})
|
|
55
|
+
return result.status ?? 1
|
|
56
|
+
}
|
|
57
|
+
`;
|
|
58
|
+
/** Marker proving a bin/basalt.ts already delegates the project commands. */
|
|
59
|
+
export const PROJECT_COMMANDS_MARKER = "['update', 'add', 'doctor', 'info'].includes(process.argv[2]";
|
|
60
|
+
export function basaltBin() {
|
|
61
|
+
return `#!/usr/bin/env node
|
|
62
|
+
${PRELUDE}
|
|
63
|
+
// The dev tools are imported dynamically, after the project commands above.
|
|
64
|
+
const { runCli } = await import('@basaltkit/cli')
|
|
65
|
+
const { generatorCommands } = await import('@basaltkit/generator')
|
|
66
|
+
const { prismaSyncCommand } = await import('@basaltkit/prisma')
|
|
67
|
+
|
|
68
|
+
// Dev tooling: opt into development defaults unless NODE_ENV is already set
|
|
69
|
+
// (imported dynamically below so env.ts is evaluated after this line).
|
|
70
|
+
process.env['NODE_ENV'] ??= 'development'
|
|
71
|
+
const { buildApp } = await import('../src/app.js')
|
|
72
|
+
|
|
73
|
+
// The 'basalt' CLI: boots the app WITH the dev/CLI commands, runs one, shuts down.
|
|
74
|
+
// The dev tools (@basaltkit/generator; add @basaltkit/ai for ai:*) are imported
|
|
75
|
+
// ONLY here — the runtime server (src/server.ts) never loads them, so the SaaS
|
|
76
|
+
// runs without the codegen/AI layer.
|
|
77
|
+
// pnpm basalt dev — dev server: route table + watch (--worker for a queue worker)
|
|
78
|
+
// pnpm basalt list — show available commands
|
|
79
|
+
// pnpm basalt routes — list registered HTTP routes
|
|
80
|
+
// pnpm basalt make:resource Project — generate a full resource vertical
|
|
81
|
+
// pnpm basalt make:service Project — generate a single artifact (schema/service/…)
|
|
82
|
+
const app = buildApp({
|
|
83
|
+
logLevel: 'silent',
|
|
84
|
+
commands: [...generatorCommands(), prismaSyncCommand()],
|
|
85
|
+
})
|
|
86
|
+
process.exit(await runCli({ app }))
|
|
87
|
+
`;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Hashes of every bin/basalt.ts an earlier create-basalt release generated
|
|
91
|
+
* (CRLF-insensitive). Only these — or a file the project's manifest records as
|
|
92
|
+
* untouched — are patched automatically.
|
|
93
|
+
*/
|
|
94
|
+
export const LEGACY_BIN_HASHES = new Set([
|
|
95
|
+
// create-basalt 0.x (rebrand, 2026-08-09): static imports, no dev commands.
|
|
96
|
+
'sha256-78537f4bf21ca44d78dc50f8841f39898b86300c4eb2c696f3d1a6e6c9921cbd',
|
|
97
|
+
// 2026-08-13: generator + prisma:sync passed via `commands`.
|
|
98
|
+
'sha256-0c898eb1faf32792fc9d2593070c8421905882ba8d8f2aba9a69c74ce2dfa5d8',
|
|
99
|
+
// 2026-08-22: `basalt dev` documented.
|
|
100
|
+
'sha256-d09d583f1a39bffcb72f302c5f6c3e2651badea358ba38019238a4aa4b842545',
|
|
101
|
+
// 2026-09-19 (create-basalt ≤ 1.9): NODE_ENV defaulted before the app import.
|
|
102
|
+
'sha256-a0df89dee04091a9470f67968ff72cfc3bf64076c1bf3d141ae657c7438a94c8',
|
|
103
|
+
]);
|
|
104
|
+
export const hasProjectCommands = (content) => content.includes(PROJECT_COMMANDS_MARKER);
|
|
105
|
+
/**
|
|
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}.
|
|
111
|
+
*/
|
|
112
|
+
export function patchBasaltBin(content) {
|
|
113
|
+
const lines = content.replace(/\r\n/g, '\n').split('\n');
|
|
114
|
+
const shebang = lines[0]?.startsWith('#!') ? `${lines.shift()}\n` : '';
|
|
115
|
+
const body = lines.map((line) => {
|
|
116
|
+
const match = /^import \{ ([^}]+) \} from '([^']+)'$/.exec(line);
|
|
117
|
+
return match ? `const { ${match[1]} } = await import('${match[2]}')` : line;
|
|
118
|
+
});
|
|
119
|
+
const firstDynamic = body.findIndex((line) => line.startsWith('const {') && line.includes('= await import('));
|
|
120
|
+
if (firstDynamic >= 0 && !(body[firstDynamic] ?? '').includes("'../src/app.js'")) {
|
|
121
|
+
body.splice(firstDynamic, 0, '// The dev tools are imported dynamically, after the project commands above.');
|
|
122
|
+
}
|
|
123
|
+
return `${shebang}${PRELUDE}\n${body.join('\n')}`;
|
|
124
|
+
}
|
|
125
|
+
/** Where a bin/basalt.ts stands: already delegating, an untouched old template, or user code. */
|
|
126
|
+
export function binStatus(content, manifestHash) {
|
|
127
|
+
if (content === undefined)
|
|
128
|
+
return 'absent';
|
|
129
|
+
if (hasProjectCommands(content))
|
|
130
|
+
return 'current';
|
|
131
|
+
const hash = hashContent(content);
|
|
132
|
+
return LEGACY_BIN_HASHES.has(hash) || hash === manifestHash ? 'patchable' : 'modified';
|
|
133
|
+
}
|
|
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
|
|
136
|
+
static imports of @basaltkit/* and ../src/app.js into dynamic ones
|
|
137
|
+
(\`import { runCli } from '@basaltkit/cli'\` → \`const { runCli } = await import('@basaltkit/cli')\`)
|
|
138
|
+
so they no longer run before it:
|
|
139
|
+
|
|
140
|
+
${PRELUDE}`;
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { spawn } from 'node:child_process';
|
|
2
|
+
import { execFile, spawn } from 'node:child_process';
|
|
3
3
|
import { stdin, stdout } from 'node:process';
|
|
4
4
|
import { createProject, describeResolution, detectPackageManager, resolveRunDefaults, runWizard, ttyPrompter, TargetNotEmptyError, WizardCancelledError, } from './index.js';
|
|
5
|
+
import { basaltCommand, isProjectCommand, runProjectCommand } from './project/run.js';
|
|
5
6
|
import { parseArgs, resolvesLatest, USAGE } from './args.js';
|
|
6
7
|
import { envPrefix } from './templates.js';
|
|
7
8
|
/** Runs a command inheriting stdio; resolves false on non-zero exit (never throws). */
|
|
@@ -16,12 +17,49 @@ function run(command, args, cwd) {
|
|
|
16
17
|
child.on('close', (code) => resolve(code === 0));
|
|
17
18
|
});
|
|
18
19
|
}
|
|
20
|
+
/** Runs a command and returns its trimmed stdout, or undefined (never throws). */
|
|
21
|
+
function capture(command, args, cwd) {
|
|
22
|
+
return new Promise((resolve) => {
|
|
23
|
+
execFile(command, args, { cwd, shell: process.platform === 'win32', timeout: 10_000 }, (error, out) => {
|
|
24
|
+
resolve(error ? undefined : String(out).trim() || undefined);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
}
|
|
19
28
|
/** Cleanly abort on Ctrl+C instead of dumping a Node AbortError stack trace. */
|
|
20
29
|
function cancel() {
|
|
21
30
|
stdout.write('\nCancelled.\n');
|
|
22
31
|
process.exit(130); // 128 + SIGINT
|
|
23
32
|
}
|
|
24
|
-
const
|
|
33
|
+
const argv = process.argv.slice(2);
|
|
34
|
+
// Project commands (update | add | doctor | info) operate on an EXISTING app.
|
|
35
|
+
// They are selected by the first positional argument — those four words are
|
|
36
|
+
// reserved, so a project literally named "update" is created with
|
|
37
|
+
// --name=update. `--project` (the `basalt` script of apps without --cli) forces
|
|
38
|
+
// project mode: `pnpm basalt` alone then prints the project-command help.
|
|
39
|
+
const firstPositional = argv.find((token) => !token.startsWith('-'));
|
|
40
|
+
if (argv.includes('--project') || isProjectCommand(firstPositional)) {
|
|
41
|
+
const code = await runProjectCommand(argv.filter((token) => token !== '--project'), {
|
|
42
|
+
cwd: process.cwd(),
|
|
43
|
+
out: { log: (line = '') => stdout.write(`${line}\n`), error: (line) => process.stderr.write(`${line}\n`) },
|
|
44
|
+
env: process.env,
|
|
45
|
+
interactive: Boolean(stdin.isTTY) && !process.env['CI'],
|
|
46
|
+
isTTY: Boolean(stdout.isTTY),
|
|
47
|
+
run,
|
|
48
|
+
capture,
|
|
49
|
+
confirm: async (message) => {
|
|
50
|
+
try {
|
|
51
|
+
return await ttyPrompter().confirm({ message, initial: true });
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
if (error instanceof WizardCancelledError)
|
|
55
|
+
cancel();
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
process.exit(code);
|
|
61
|
+
}
|
|
62
|
+
const flags = parseArgs(argv);
|
|
25
63
|
// Rich interactive wizard when nothing was specified in a terminal. CI, piped
|
|
26
64
|
// input, and `--yes` keep the flag-driven path.
|
|
27
65
|
if (!flags.yes && flags.name === undefined && stdin.isTTY) {
|
|
@@ -118,11 +156,12 @@ try {
|
|
|
118
156
|
steps.push(`${pm} run db:migrate${flags.tenancy ? ' # creates the tables and seeds the demo tenant' : ' # creates the tables'}`);
|
|
119
157
|
}
|
|
120
158
|
steps.push(`${pm} run dev${flags.ui ? ' # API on :3000' : ''}`);
|
|
121
|
-
// `web` is
|
|
122
|
-
// server
|
|
159
|
+
// `web` is a pnpm workspace member (pnpm-workspace.yaml); `dev:web` runs its
|
|
160
|
+
// dev server through a pnpm filter.
|
|
123
161
|
if (flags.ui)
|
|
124
|
-
steps.push(`pnpm
|
|
162
|
+
steps.push(`pnpm dev:web # UI on :5180`);
|
|
125
163
|
console.log(`\nNext steps:\n${steps.map((s) => ` ${s}`).join('\n')}\n`);
|
|
164
|
+
console.log(`Later: \`${basaltCommand(pm, 'update')}\` updates the dependencies, \`${basaltCommand(pm, 'add ui|cli|mcp')}\` adds a feature, \`${basaltCommand(pm, 'doctor')}\` checks the project.\n`);
|
|
126
165
|
}
|
|
127
166
|
catch (error) {
|
|
128
167
|
if (error instanceof TargetNotEmptyError) {
|
package/dist/index.d.ts
CHANGED
|
@@ -64,8 +64,16 @@ export declare function resolveRunDefaults(input: {
|
|
|
64
64
|
git: boolean;
|
|
65
65
|
interactive: boolean;
|
|
66
66
|
};
|
|
67
|
+
/** Every file a scaffold with `options` writes (dependency ranges = the bundled fallbacks). */
|
|
68
|
+
export declare function scaffoldFiles(options: ProjectOptions): Record<string, string>;
|
|
67
69
|
/** Generates a ready-to-run Basalt app. Does not install dependencies. */
|
|
68
70
|
export declare function createProject(input: CreateProjectInput): Promise<CreateProjectResult>;
|
|
69
|
-
export { THIRD_PARTY_VERSIONS, DEFAULT_REGISTRY, DEFAULT_MINIMUM_RELEASE_AGE_MINUTES, describeResolution, minimumReleaseAgeMinutes, resolveLatestVersions, registryUrl, type FreshVersion, type HeldBackVersion, type ResolveLatestOptions, type VersionResolution, } from './latest-versions.js';
|
|
71
|
+
export { THIRD_PARTY_VERSIONS, DEFAULT_REGISTRY, DEFAULT_MINIMUM_RELEASE_AGE_MINUTES, describeResolution, resolveFileVersions, minimumReleaseAgeMinutes, resolveLatestVersions, registryUrl, type FreshVersion, type HeldBackVersion, type ResolveLatestOptions, type VersionResolution, } from './latest-versions.js';
|
|
72
|
+
export { runProjectCommand, PROJECT_COMMANDS, isProjectCommand, type ProjectCommandDeps } from './project/run.js';
|
|
73
|
+
export { planUpdate, type UpdatePlan, type UpdateEntry } from './project/update.js';
|
|
74
|
+
export { planAdd, ADDABLE_FEATURES, type AddPlan, type AddableFeature } from './project/add.js';
|
|
75
|
+
export { runDoctor, type DoctorFinding } from './project/doctor.js';
|
|
76
|
+
export { loadProject, NotABasaltAppError, type ProjectContext } from './project/context.js';
|
|
77
|
+
export { MANIFEST_PATH, readManifest, hashContent, type ProjectManifest } from './project/manifest.js';
|
|
70
78
|
export { runWizard, validateProjectName, PRESETS, FEATURES, type WizardResult, type WizardOptions, type FeatureKey } from './wizard.js';
|
|
71
79
|
export { ttyPrompter, scriptedPrompter, WizardCancelledError, type Prompter, type Choice } from './prompt.js';
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,8 @@ import { mkdir, readdir, writeFile } from 'node:fs/promises';
|
|
|
2
2
|
import { dirname, join, resolve } from 'node:path';
|
|
3
3
|
import { appTest, appTs, dbTs, devTs, dockerignore, envExample, envTs, gitignore, mcpJson, basaltBin, packageJson, pnpmWorkspaceYaml, prismaConfigTs, prismaSchema, prismaSeedTs, readme, routesTs, serverTs, tsconfigJson, } from './templates.js';
|
|
4
4
|
import { uiFiles } from './templates-ui.js';
|
|
5
|
-
import {
|
|
5
|
+
import { createManifest, MANIFEST_PATH, serializeManifest } from './project/manifest.js';
|
|
6
|
+
import { resolveFileVersions } from './latest-versions.js';
|
|
6
7
|
export class TargetNotEmptyError extends Error {
|
|
7
8
|
constructor(dir) {
|
|
8
9
|
super(`Target directory "${dir}" already exists and is not empty.`);
|
|
@@ -35,23 +36,9 @@ export function resolveRunDefaults(input) {
|
|
|
35
36
|
git: input.git ?? interactive,
|
|
36
37
|
};
|
|
37
38
|
}
|
|
38
|
-
/**
|
|
39
|
-
export
|
|
40
|
-
|
|
41
|
-
name: input.name,
|
|
42
|
-
tenancy: input.tenancy ?? true,
|
|
43
|
-
auth: input.auth ?? true,
|
|
44
|
-
billing: input.billing ?? false,
|
|
45
|
-
ui: input.ui ?? false,
|
|
46
|
-
cli: input.cli ?? false,
|
|
47
|
-
mcp: input.mcp ?? false,
|
|
48
|
-
prisma: input.prisma ?? false,
|
|
49
|
-
};
|
|
50
|
-
const dir = resolve(input.dir ?? input.name);
|
|
51
|
-
const existing = await readdir(dir).catch(() => null);
|
|
52
|
-
if (existing && existing.length > 0)
|
|
53
|
-
throw new TargetNotEmptyError(dir);
|
|
54
|
-
const files = {
|
|
39
|
+
/** Every file a scaffold with `options` writes (dependency ranges = the bundled fallbacks). */
|
|
40
|
+
export function scaffoldFiles(options) {
|
|
41
|
+
return {
|
|
55
42
|
'package.json': packageJson(options),
|
|
56
43
|
'tsconfig.json': tsconfigJson(options),
|
|
57
44
|
'.env.example': envExample(options),
|
|
@@ -78,13 +65,26 @@ export async function createProject(input) {
|
|
|
78
65
|
...(options.mcp ? { '.mcp.json': mcpJson(options) } : {}),
|
|
79
66
|
...(options.ui ? uiFiles(options) : {}),
|
|
80
67
|
};
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
68
|
+
}
|
|
69
|
+
/** Generates a ready-to-run Basalt app. Does not install dependencies. */
|
|
70
|
+
export async function createProject(input) {
|
|
71
|
+
const options = {
|
|
72
|
+
name: input.name,
|
|
73
|
+
tenancy: input.tenancy ?? true,
|
|
74
|
+
auth: input.auth ?? true,
|
|
75
|
+
billing: input.billing ?? false,
|
|
76
|
+
ui: input.ui ?? false,
|
|
77
|
+
cli: input.cli ?? false,
|
|
78
|
+
mcp: input.mcp ?? false,
|
|
79
|
+
prisma: input.prisma ?? false,
|
|
80
|
+
};
|
|
81
|
+
const dir = resolve(input.dir ?? input.name);
|
|
82
|
+
const existing = await readdir(dir).catch(() => null);
|
|
83
|
+
if (existing && existing.length > 0)
|
|
84
|
+
throw new TargetNotEmptyError(dir);
|
|
85
|
+
const files = scaffoldFiles(options);
|
|
86
|
+
const versions = input.resolveLatest ? await resolveFileVersions(files, input.registry) : undefined;
|
|
87
|
+
files[MANIFEST_PATH] = serializeManifest(createManifest(options, files));
|
|
88
88
|
for (const [path, content] of Object.entries(files)) {
|
|
89
89
|
const target = join(dir, path);
|
|
90
90
|
await mkdir(dirname(target), { recursive: true });
|
|
@@ -92,6 +92,12 @@ export async function createProject(input) {
|
|
|
92
92
|
}
|
|
93
93
|
return { dir, files: Object.keys(files).sort(), options, ...(versions ? { versions } : {}) };
|
|
94
94
|
}
|
|
95
|
-
export { THIRD_PARTY_VERSIONS, DEFAULT_REGISTRY, DEFAULT_MINIMUM_RELEASE_AGE_MINUTES, describeResolution, minimumReleaseAgeMinutes, resolveLatestVersions, registryUrl, } from './latest-versions.js';
|
|
95
|
+
export { THIRD_PARTY_VERSIONS, DEFAULT_REGISTRY, DEFAULT_MINIMUM_RELEASE_AGE_MINUTES, describeResolution, resolveFileVersions, minimumReleaseAgeMinutes, resolveLatestVersions, registryUrl, } from './latest-versions.js';
|
|
96
|
+
export { runProjectCommand, PROJECT_COMMANDS, isProjectCommand } from './project/run.js';
|
|
97
|
+
export { planUpdate } from './project/update.js';
|
|
98
|
+
export { planAdd, ADDABLE_FEATURES } from './project/add.js';
|
|
99
|
+
export { runDoctor } from './project/doctor.js';
|
|
100
|
+
export { loadProject, NotABasaltAppError } from './project/context.js';
|
|
101
|
+
export { MANIFEST_PATH, readManifest, hashContent } from './project/manifest.js';
|
|
96
102
|
export { runWizard, validateProjectName, PRESETS, FEATURES } from './wizard.js';
|
|
97
103
|
export { ttyPrompter, scriptedPrompter, WizardCancelledError } from './prompt.js';
|