create-basalt 1.9.0 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/templates.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { THIRD_PARTY_VERSIONS } from './latest-versions.js';
2
2
  import { SCAFFOLD_VERSIONS } from './versions.js';
3
+ export { basaltBin } from './bin-template.js';
3
4
  export { SCAFFOLD_VERSIONS } from './versions.js';
4
5
  export { THIRD_PARTY_VERSIONS } from './latest-versions.js';
5
6
  /**
@@ -28,6 +29,10 @@ export const thirdPartyVersionOf = (pkg) => {
28
29
  }
29
30
  return range;
30
31
  };
32
+ /** The root `basalt` script of an app without `--cli`: create-basalt's project commands. */
33
+ export const BASALT_PROJECT_SCRIPT = 'create-basalt --project';
34
+ /** The root `dev:web` script of a `--ui` app (web/ is a pnpm workspace member). */
35
+ export const webDevScript = (options) => `pnpm --filter ${options.name}-web dev`;
31
36
  /** True when the scaffold binds users to the tenant they act on (tenancy + auth). */
32
37
  export const enforcesMembership = (options) => options.tenancy && options.auth;
33
38
  export function packageJson(options) {
@@ -76,6 +81,9 @@ export function packageJson(options) {
76
81
  }
77
82
  const devDependencies = {
78
83
  '@basaltkit/testing': versionOf('@basaltkit/testing'),
84
+ // The project commands (\`pnpm basalt update | add | doctor | info\`) — dev
85
+ // tooling with no dependencies of its own, never imported by the app.
86
+ 'create-basalt': versionOf('create-basalt'),
79
87
  };
80
88
  for (const pkg of ['@types/node', 'pino-pretty', 'tsx', 'typescript', 'vitest']) {
81
89
  devDependencies[pkg] = thirdPartyVersionOf(pkg);
@@ -107,7 +115,11 @@ export function packageJson(options) {
107
115
  start: 'tsx src/server.ts',
108
116
  test: 'vitest run',
109
117
  typecheck: 'tsc --noEmit',
110
- ...(options.cli ? { basalt: 'tsx bin/basalt.ts' } : {}),
118
+ // With --cli, bin/basalt.ts serves the generators AND hands the project
119
+ // commands to create-basalt; without it, \`pnpm basalt update\` goes to
120
+ // create-basalt directly (\`create-basalt add cli\` swaps the script).
121
+ basalt: options.cli ? 'tsx bin/basalt.ts' : BASALT_PROJECT_SCRIPT,
122
+ ...(options.ui ? { 'dev:web': webDevScript(options) } : {}),
111
123
  ...(options.prisma
112
124
  ? {
113
125
  // Generate right after install so `pnpm typecheck` has the client
@@ -210,10 +222,19 @@ export function envPrefix(name) {
210
222
  base = base.slice(start, end);
211
223
  return base === '' || /^\d/.test(base) ? `APP_${base}` : base;
212
224
  }
225
+ /**
226
+ * The opening lines of `.env.example`. Up to create-basalt 1.10 they read
227
+ * {@link LEGACY_ENV_EXAMPLE_LEAD} ("nothing loads this file"); `update`
228
+ * swaps that for this one in existing apps.
229
+ */
230
+ export const ENV_EXAMPLE_LEAD = `# Development: copy this file to .env — \`pnpm dev\` and \`pnpm basalt\` load it
231
+ # automatically (a new app already has one). \`pnpm start\` (production) does
232
+ # NOT: there the variables come from the environment (or \`node --env-file\`).`;
233
+ export const LEGACY_ENV_EXAMPLE_LEAD = `# Nothing loads this file for you: copy it to .env and start with
234
+ # \`node --env-file=.env\` / \`tsx --env-file=.env\`, or export the variables.`;
213
235
  export function envExample(options) {
214
236
  const prefix = envPrefix(options.name);
215
- return `# Nothing loads this file for you: copy it to .env and start with
216
- # \`node --env-file=.env\` / \`tsx --env-file=.env\`, or export the variables.
237
+ return `${ENV_EXAMPLE_LEAD}
217
238
  # --env-file NEVER overrides a variable already exported in your shell: if
218
239
  # another project exported PORT or DATABASE_URL, an app reading the generic
219
240
  # name would silently use THAT one. That is why src/env.ts reads app-prefixed
@@ -240,6 +261,20 @@ ${prefix}_DATABASE_URL=postgres://postgres:postgres@localhost:5432/${databaseNam
240
261
  # ${prefix}_DATABASE_URL=postgres://user:pass@localhost:5432/${options.name.replace(/^@[^/]+\//, '')}
241
262
  `}`;
242
263
  }
264
+ /**
265
+ * The `.env` a new app starts with (git-ignored, never in the manifest): the
266
+ * values of `.env.example` for local development plus — with auth — a freshly
267
+ * generated APP_SECRET, so `pnpm dev` works without a copy step.
268
+ */
269
+ export function devEnvFile(options, appSecret) {
270
+ const prefix = envPrefix(options.name);
271
+ const example = envExample(options);
272
+ const body = example.slice(example.indexOf(`${prefix}_PORT=`));
273
+ return `# Local development settings, generated by create-basalt from .env.example.
274
+ # Git-ignored: never commit it. \`pnpm dev\` and \`pnpm basalt\` load it (a
275
+ # variable exported in your shell wins); \`pnpm start\` (production) does not.
276
+ ${body.replace(`# ${prefix}_APP_SECRET=\n`, `${prefix}_APP_SECRET=${appSecret}\n`)}`;
277
+ }
243
278
  /** A PostgreSQL-safe database name derived from the project name. */
244
279
  export function databaseName(name) {
245
280
  return name.replace(/^@[^/]+\//, '').replace(/[^a-zA-Z0-9]+/g, '_').toLowerCase();
@@ -514,45 +549,31 @@ for (const signal of ['SIGINT', 'SIGTERM'] as const) {
514
549
  `;
515
550
  }
516
551
  /**
517
- * `pnpm dev` entrypoint. Opts into NODE_ENV=development (dev secret defaults,
518
- * pretty logs) unless NODE_ENV is already set, then starts the server. Kept out
519
- * of server.ts on purpose: `pnpm start` runs server.ts directly, where an unset
520
- * NODE_ENV counts as production and secrets are required (fail-closed).
552
+ * `pnpm dev` entrypoint. Loads `.env` (when present) with `node --env-file`
553
+ * semantics — an exported variable wins, the file only fills in what is
554
+ * missing — and opts into NODE_ENV=development (dev secret defaults, pretty
555
+ * logs) unless NODE_ENV is already set, then starts the server. Kept out of
556
+ * server.ts on purpose: `pnpm start` runs server.ts directly, where nothing
557
+ * loads `.env` (production configuration comes from the real environment) and
558
+ * an unset NODE_ENV counts as production, so secrets are required (fail-closed).
521
559
  */
522
560
  export function devTs() {
523
561
  return `// Development entrypoint (\`pnpm dev\`). \`pnpm start\` runs server.ts directly,
524
562
  // where an unset NODE_ENV counts as production: secrets are then required.
563
+ import { existsSync } from 'node:fs'
564
+ import { fileURLToPath } from 'node:url'
565
+
566
+ // Development loads .env (when present) like \`node --env-file=.env\`: a variable
567
+ // already exported in your shell still wins, the file only fills in what is
568
+ // missing. \`pnpm start\` (production) never loads it — there the configuration
569
+ // comes from the real environment.
570
+ const envFile = fileURLToPath(new URL('../.env', import.meta.url))
571
+ if (existsSync(envFile)) process.loadEnvFile(envFile)
572
+
525
573
  process.env['NODE_ENV'] ??= 'development'
526
574
  await import('./server.js')
527
575
  `;
528
576
  }
529
- export function basaltBin() {
530
- return `#!/usr/bin/env node
531
- import { runCli } from '@basaltkit/cli'
532
- import { generatorCommands } from '@basaltkit/generator'
533
- import { prismaSyncCommand } from '@basaltkit/prisma'
534
-
535
- // Dev tooling: opt into development defaults unless NODE_ENV is already set
536
- // (imported dynamically below so env.ts is evaluated after this line).
537
- process.env['NODE_ENV'] ??= 'development'
538
- const { buildApp } = await import('../src/app.js')
539
-
540
- // The 'basalt' CLI: boots the app WITH the dev/CLI commands, runs one, shuts down.
541
- // The dev tools (@basaltkit/generator; add @basaltkit/ai for ai:*) are imported
542
- // ONLY here — the runtime server (src/server.ts) never loads them, so the SaaS
543
- // runs without the codegen/AI layer.
544
- // pnpm basalt dev — dev server: route table + watch (--worker for a queue worker)
545
- // pnpm basalt list — show available commands
546
- // pnpm basalt routes — list registered HTTP routes
547
- // pnpm basalt make:resource Project — generate a full resource vertical
548
- // pnpm basalt make:service Project — generate a single artifact (schema/service/…)
549
- const app = buildApp({
550
- logLevel: 'silent',
551
- commands: [...generatorCommands(), prismaSyncCommand()],
552
- })
553
- process.exit(await runCli({ app }))
554
- `;
555
- }
556
577
  /**
557
578
  * The models each `@basaltkit/<domain>-prisma` package needs, copied VERBATIM
558
579
  * from that package's reference `prisma/schema.prisma` — the same blocks
@@ -807,9 +828,9 @@ export function prismaConfigTs(options) {
807
828
  return `import { loadEnvFile } from 'node:process'
808
829
  import { defineConfig } from 'prisma/config'
809
830
 
810
- // Prisma loads no .env for you (neither does the app — see src/env.ts). This
811
- // mirrors \`node --env-file=.env\`: a variable already exported in your shell
812
- // still wins, the file only fills in what is missing.
831
+ // Prisma loads no .env for you. Like the app's dev entrypoints (src/dev.ts,
832
+ // bin/basalt.ts) this mirrors \`node --env-file=.env\`: a variable already
833
+ // exported in your shell still wins, the file only fills in what is missing.
813
834
  try {
814
835
  loadEnvFile('.env')
815
836
  } catch {
@@ -962,7 +983,7 @@ Included: ${features.join(' · ')}.
962
983
  \`\`\`bash
963
984
  pnpm install${options.prisma
964
985
  ? `
965
- # point ${envPrefix(options.name)}_DATABASE_URL at a PostgreSQL database (.env.example), then
986
+ # start PostgreSQL — .env points ${envPrefix(options.name)}_DATABASE_URL at localhost:5432 — then
966
987
  pnpm db:migrate # create the tables${options.tenancy ? ` (runs the seed too)` : ''}`
967
988
  : ''}
968
989
  pnpm dev # API on http://localhost:3000
@@ -1020,16 +1041,18 @@ scaffold with \`--cli\` — \`pnpm basalt prisma:sync\` does the merge for you.)
1020
1041
  : ''}
1021
1042
  ## Environment
1022
1043
 
1023
- \`src/env.ts\` validates \`process.env\` — nothing loads \`.env\` for you. Copy
1024
- \`.env.example\` to \`.env\` and launch with \`--env-file=.env\` (Node/tsx), or
1025
- export the variables.
1044
+ \`src/env.ts\` validates \`process.env\`. For development, ${options.cli ? '`pnpm dev` and `pnpm basalt` load' : '`pnpm dev` loads'} \`.env\`
1045
+ automatically — create-basalt wrote one for you: git-ignored, with local
1046
+ values${options.auth ? ' and a generated secret' : ''}. \`.env.example\` is its committed template.
1047
+ \`pnpm start\` (production, \`src/server.ts\`) loads **nothing**: there the
1048
+ variables come from the real environment (or \`node --env-file=.env\`).
1026
1049
 
1027
1050
  The variables are **app-prefixed**: \`${envPrefix(options.name)}_PORT\`,
1028
1051
  \`${envPrefix(options.name)}_HOST\`, \`${envPrefix(options.name)}_LOG_LEVEL\`${options.auth ? `, \`${envPrefix(options.name)}_APP_SECRET\`` : ''}${options.prisma ? `, \`${envPrefix(options.name)}_DATABASE_URL\`` : ''}. \`NODE_ENV\` is never prefixed.
1029
1052
 
1030
- Why: **\`--env-file\` never overrides a variable that is already exported.** In a
1031
- shell where another project exported \`DATABASE_URL\` (or \`PORT\`), an app reading
1032
- the generic name boots against THAT value and only fails on the first request
1053
+ Why: **loading \`.env\` (like \`--env-file\`) never overrides a variable that is
1054
+ already exported.** In a shell where another project exported \`DATABASE_URL\`
1055
+ (or \`PORT\`), an app reading the generic name boots against THAT value and only fails on the first request
1033
1056
  that touches it. \`src/env.ts\` therefore passes \`prefix: '${envPrefix(options.name)}'\` to
1034
1057
  \`defineEnv\`: each variable is read as \`${envPrefix(options.name)}_<NAME>\` first and
1035
1058
  **falls back** to the bare \`<NAME>\`, so a deployment that already exports the
@@ -1044,8 +1067,24 @@ database and \`DATABASE_URL\` is read as \`${envPrefix(options.name)}_DATABASE_U
1044
1067
  To drop the fallback entirely (prefixed names only), write
1045
1068
  \`prefix: { value: '${envPrefix(options.name)}', fallback: false }\`. When in doubt about what
1046
1069
  your shell exports: \`env | grep DATABASE_URL\`.
1047
- ${options.cli
1048
- ? `
1070
+
1071
+ ## Keeping it up to date
1072
+
1073
+ \`\`\`bash
1074
+ pnpm basalt update # dependencies → latest (framework always; third-party within its major)
1075
+ pnpm basalt update --dry # preview the table, write nothing
1076
+ pnpm basalt add ui # add a feature later: ui, cli, mcp
1077
+ pnpm basalt doctor # Node, package manager, versions, env and Prisma checks
1078
+ \`\`\`
1079
+
1080
+ \`.basalt/project.json\` records what create-basalt generated (options and file
1081
+ hashes) — commit it: \`add\` and \`update\` use it to tell untouched template
1082
+ files from the ones you edited.
1083
+ ${options.cli ? readmeCliSection(options) : ''}${options.ui ? readmeUiSection(options) : ''}${options.mcp ? readmeMcpSection(options) : ''}`;
1084
+ }
1085
+ /** README section for the \`basalt\` CLI (also appended by \`create-basalt add cli\`). */
1086
+ export function readmeCliSection(_options) {
1087
+ return `
1049
1088
  ## The \`basalt\` CLI
1050
1089
 
1051
1090
  \`\`\`bash
@@ -1069,21 +1108,25 @@ node_modules/.bin/tsx bin/basalt.ts make:resource Project
1069
1108
 
1070
1109
  or set \`verifyDepsBeforeRun: warn\` in \`pnpm-workspace.yaml\` (a conscious
1071
1110
  choice: you then run \`pnpm install\` yourself after dependency changes).
1072
- `
1073
- : ''}${options.ui
1074
- ? `
1111
+ `;
1112
+ }
1113
+ /** README section for the web/ frontend (also appended by \`create-basalt add ui\`). */
1114
+ export function readmeUiSection(options) {
1115
+ return `
1075
1116
  ## Web UI
1076
1117
 
1077
1118
  \`\`\`bash
1078
- pnpm dev # terminal 1 — API on :3000
1079
- pnpm --filter ${options.name}-web dev # terminal 2 — UI on http://localhost:5180
1119
+ pnpm dev # terminal 1 — API on :3000
1120
+ pnpm dev:web # terminal 2 — UI on http://localhost:5180 (pnpm --filter ${options.name}-web dev)
1080
1121
  \`\`\`
1081
1122
 
1082
1123
  Open <http://localhost:5180>. The Vite dev server proxies \`/api\` to the API,
1083
1124
  so there is no CORS to configure${options.auth ? '. Register, then sign in' : ''}.
1084
- `
1085
- : ''}${options.mcp
1086
- ? `
1125
+ `;
1126
+ }
1127
+ /** README section for the MCP server (also appended by \`create-basalt add mcp\`). */
1128
+ export function readmeMcpSection(_options) {
1129
+ return `
1087
1130
  ## MCP server
1088
1131
 
1089
1132
  Read-only routes marked with \`meta.mcp\` in \`src/routes.ts\` (the overview and
@@ -1109,8 +1152,7 @@ app's runtime) with MCP clients such as Claude Code and Claude Desktop. It expos
1109
1152
  \`basalt_make\` (preview by default, writes confined to this project) plus workflow
1110
1153
  prompts. Claude Code picks up \`.mcp.json\` automatically; for Claude Desktop add the
1111
1154
  same server to its config with \`--cwd=<absolute project path>\`.
1112
- `
1113
- : ''}`;
1155
+ `;
1114
1156
  }
1115
1157
  /**
1116
1158
  * A Claude Code / generic MCP client config that registers the dev-only
@@ -1183,10 +1225,12 @@ export function pnpmWorkspaceYaml(options) {
1183
1225
  : ''}allowBuilds:
1184
1226
  esbuild: true
1185
1227
  msgpackr-extract: false
1186
- # @basaltkit/* releases in lockstep, often within hours — exclude the scope from
1187
- # pnpm's minimumReleaseAge policy so \`pnpm up\` is never blocked on a fresh release.
1228
+ # @basaltkit/* releases in lockstep, often within hours — exclude the scope (and
1229
+ # create-basalt, which serves \`pnpm basalt update\`) from pnpm's minimumReleaseAge
1230
+ # policy so an update is never blocked on a fresh release.
1188
1231
  minimumReleaseAgeExclude:
1189
1232
  - '@basaltkit/*'
1233
+ - create-basalt
1190
1234
  # To let specific versions of ONE package bypass the policy, write ONE entry
1191
1235
  # with a \`||\` union. pnpm evaluates this list first match wins by package
1192
1236
  # name, so a second '<name>@<version>' entry for the same package is ignored:
package/dist/versions.js CHANGED
@@ -4,10 +4,10 @@
4
4
  export const SCAFFOLD_VERSIONS = {
5
5
  '@basaltkit/admin': '^2.0.0',
6
6
  '@basaltkit/admin-shadcn': '^1.1.0',
7
- '@basaltkit/ai-mcp': '^0.2.0',
8
- '@basaltkit/auth': '^4.0.0',
7
+ '@basaltkit/ai-mcp': '^0.3.0',
8
+ '@basaltkit/auth': '^4.1.0',
9
9
  '@basaltkit/auth-prisma': '^2.0.0',
10
- '@basaltkit/cli': '^1.2.0',
10
+ '@basaltkit/cli': '^1.3.0',
11
11
  '@basaltkit/config': '^1.1.0',
12
12
  '@basaltkit/core': '^1.5.0',
13
13
  '@basaltkit/env': '^3.1.0',
@@ -15,14 +15,15 @@ export const SCAFFOLD_VERSIONS = {
15
15
  '@basaltkit/fastify': '^2.5.0',
16
16
  '@basaltkit/generator': '^1.5.0',
17
17
  '@basaltkit/logger': '^1.3.0',
18
- '@basaltkit/mcp': '^4.0.0',
18
+ '@basaltkit/mcp': '^5.0.0',
19
19
  '@basaltkit/prisma': '^3.0.0',
20
20
  '@basaltkit/sdk': '^2.1.0',
21
21
  '@basaltkit/subscriptions': '^5.0.0',
22
22
  '@basaltkit/subscriptions-prisma': '^3.0.0',
23
23
  '@basaltkit/teams': '^4.0.0',
24
24
  '@basaltkit/teams-prisma': '^1.1.0',
25
- '@basaltkit/tenancy': '^3.0.0',
25
+ '@basaltkit/tenancy': '^3.1.0',
26
26
  '@basaltkit/tenancy-prisma': '^2.0.0',
27
27
  '@basaltkit/testing': '^2.0.0',
28
+ 'create-basalt': '^1.11.0',
28
29
  };
package/dist/wizard.js CHANGED
@@ -11,9 +11,9 @@ export const FEATURES = [
11
11
  ];
12
12
  export const PRESETS = [
13
13
  { value: 'saas', label: 'SaaS starter', hint: 'tenancy + auth + billing + database + CLI', features: ['tenancy', 'auth', 'billing', 'prisma', 'cli'] },
14
- { value: 'api', label: 'API only', hint: 'auth + MCP, no tenancy or UI', features: ['auth', 'mcp'] },
14
+ { value: 'api', label: 'API only', hint: 'auth + MCP + CLI, no tenancy or UI', features: ['auth', 'mcp', 'cli'] },
15
15
  { value: 'full', label: 'Full stack', hint: 'everything + web UI', features: ['tenancy', 'auth', 'billing', 'prisma', 'ui', 'cli', 'mcp'] },
16
- { value: 'minimal', label: 'Minimal', hint: 'no batteries, add them later', features: [] },
16
+ { value: 'minimal', label: 'Minimal', hint: 'just the CLI, add the rest later', features: ['cli'] },
17
17
  { value: 'custom', label: 'Custom', hint: 'pick features yourself', features: null },
18
18
  ];
19
19
  const PACKAGE_MANAGERS = [
@@ -59,7 +59,7 @@ export async function runWizard(prompter, options = {}) {
59
59
  (await prompter.multiselect({
60
60
  message: 'Select features',
61
61
  choices: FEATURES,
62
- initial: ['tenancy', 'auth'],
62
+ initial: ['tenancy', 'auth', 'cli'],
63
63
  })));
64
64
  let pm = await prompter.select({
65
65
  message: 'Package manager',
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "create-basalt",
3
- "version": "1.9.0",
3
+ "version": "1.11.0",
4
4
  "engines": {
5
5
  "node": ">=22.5.0"
6
6
  },
7
- "description": "Scaffold a production-shaped Basalt SaaS app: npm create basalt my-saas [--billing] [--ui] [--cli] [--install] [--git]",
7
+ "description": "Scaffold a production-shaped Basalt SaaS app (npm create basalt my-saas [--billing] [--ui] [--cli] [--install] [--git]) — then keep it current: create-basalt update | add | doctor | info",
8
8
  "license": "MIT",
9
9
  "type": "module",
10
10
  "sideEffects": false,
@@ -30,26 +30,26 @@
30
30
  "typescript": "^7.0.2",
31
31
  "vitest": "^5.0.1",
32
32
  "zod": "^4.6.5",
33
- "@basaltkit/auth": "^4.0.0",
33
+ "@basaltkit/auth": "^4.1.0",
34
34
  "@basaltkit/auth-prisma": "^2.0.0",
35
- "@basaltkit/cli": "^1.2.5",
36
35
  "@basaltkit/config": "^1.1.2",
36
+ "@basaltkit/cli": "^1.3.0",
37
37
  "@basaltkit/core": "^1.5.0",
38
38
  "@basaltkit/env": "^3.1.1",
39
39
  "@basaltkit/events": "^1.3.0",
40
40
  "@basaltkit/fastify": "^2.5.0",
41
41
  "@basaltkit/generator": "^1.5.0",
42
42
  "@basaltkit/logger": "^1.3.0",
43
- "@basaltkit/mcp": "^4.0.0",
43
+ "@basaltkit/mcp": "^5.0.0",
44
44
  "@basaltkit/prisma": "^3.0.0",
45
45
  "@basaltkit/subscriptions": "^5.0.0",
46
46
  "@basaltkit/subscriptions-prisma": "^3.0.0",
47
47
  "@basaltkit/teams": "^4.0.0",
48
48
  "@basaltkit/teams-prisma": "^1.1.0",
49
- "@basaltkit/tenancy": "^3.0.0",
50
- "@basaltkit/tenancy-prisma": "^2.0.0",
49
+ "@basaltkit/tenancy": "^3.1.0",
50
+ "@basaltkit/tsconfig": "^0.24.0",
51
51
  "@basaltkit/testing": "^2.0.3",
52
- "@basaltkit/tsconfig": "^0.24.0"
52
+ "@basaltkit/tenancy-prisma": "^2.0.0"
53
53
  },
54
54
  "publishConfig": {
55
55
  "access": "public"