create-better-t-stack 3.32.0 → 3.33.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
@@ -68,8 +68,8 @@ Options:
68
68
  --install Install dependencies
69
69
  --no-install Skip installing dependencies
70
70
  --db-setup <setup> Database setup (turso, d1, neon, supabase, prisma-postgres, planetscale, mongodb-atlas, docker, none)
71
- --web-deploy <setup> Web deployment (cloudflare, none)
72
- --server-deploy <setup> Server deployment (cloudflare, none)
71
+ --web-deploy <setup> Web deployment (cloudflare, docker, none)
72
+ --server-deploy <setup> Server deployment (cloudflare, docker, none)
73
73
  --backend <framework> Backend framework (hono, express, fastify, elysia, convex, self, none)
74
74
  --runtime <runtime> Runtime (bun, node, workers, none)
75
75
  --api <type> API type (trpc, orpc, none)
@@ -223,6 +223,12 @@ Create a self-hosted fullstack project on Cloudflare with D1:
223
223
  npx create-better-t-stack --backend self --frontend next --api trpc --database sqlite --orm drizzle --db-setup d1 --web-deploy cloudflare
224
224
  ```
225
225
 
226
+ Create a self-hosted project that ships as Docker containers (web + server + database via Docker Compose):
227
+
228
+ ```bash
229
+ npx create-better-t-stack --frontend tanstack-router --backend hono --runtime bun --database postgres --orm drizzle --db-setup docker --web-deploy docker --server-deploy docker
230
+ ```
231
+
226
232
  Create a minimal API-only project:
227
233
 
228
234
  ```bash
package/dist/cli.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { _ as types_exports, i as SchemaNameSchema, l as create, m as getSchemaResult, s as add, u as createBtsCli, v as getLatestCLIVersion } from "./src-BZV4gWGl.mjs";
2
+ import { _ as types_exports, i as SchemaNameSchema, l as create, m as getSchemaResult, s as add, u as createBtsCli, v as getLatestCLIVersion } from "./src-_FzI_58B.mjs";
3
3
  import z from "zod";
4
4
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
package/dist/index.d.mts CHANGED
@@ -184,8 +184,8 @@ declare const router: _$_trpc_server0.TRPCBuiltRouter<{
184
184
  backend?: "none" | "hono" | "express" | "fastify" | "elysia" | "convex" | "self" | undefined;
185
185
  runtime?: "none" | "bun" | "node" | "workers" | undefined;
186
186
  api?: "none" | "trpc" | "orpc" | undefined;
187
- webDeploy?: "none" | "cloudflare" | undefined;
188
- serverDeploy?: "none" | "cloudflare" | undefined;
187
+ webDeploy?: "none" | "docker" | "cloudflare" | undefined;
188
+ serverDeploy?: "none" | "docker" | "cloudflare" | undefined;
189
189
  directoryConflict?: "merge" | "overwrite" | "increment" | "error" | undefined;
190
190
  renderTitle?: boolean | undefined;
191
191
  disableAnalytics?: boolean | undefined;
@@ -283,8 +283,8 @@ declare const router: _$_trpc_server0.TRPCBuiltRouter<{
283
283
  backend?: "none" | "hono" | "express" | "fastify" | "elysia" | "convex" | "self" | undefined;
284
284
  runtime?: "none" | "bun" | "node" | "workers" | undefined;
285
285
  api?: "none" | "trpc" | "orpc" | undefined;
286
- webDeploy?: "none" | "cloudflare" | undefined;
287
- serverDeploy?: "none" | "cloudflare" | undefined;
286
+ webDeploy?: "none" | "docker" | "cloudflare" | undefined;
287
+ serverDeploy?: "none" | "docker" | "cloudflare" | undefined;
288
288
  directoryConflict?: "merge" | "overwrite" | "increment" | "error" | undefined;
289
289
  renderTitle?: boolean | undefined;
290
290
  disableAnalytics?: boolean | undefined;
@@ -363,8 +363,8 @@ declare const router: _$_trpc_server0.TRPCBuiltRouter<{
363
363
  hooks?: ("cursor" | "windsurf" | "codebuddy" | "claude" | "copilot")[] | undefined;
364
364
  } | undefined;
365
365
  } | undefined;
366
- webDeploy?: "none" | "cloudflare" | undefined;
367
- serverDeploy?: "none" | "cloudflare" | undefined;
366
+ webDeploy?: "none" | "docker" | "cloudflare" | undefined;
367
+ serverDeploy?: "none" | "docker" | "cloudflare" | undefined;
368
368
  projectDir?: string | undefined;
369
369
  install?: boolean | undefined;
370
370
  packageManager?: "bun" | "npm" | "pnpm" | undefined;
package/dist/index.mjs CHANGED
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env node
2
- import { C as ProjectCreationError, S as DirectoryConflictError, T as ValidationError, a as TEMPLATE_COUNT, b as CompatibilityError, c as builder, d as createVirtual, f as docs, g as sponsors, h as router, i as SchemaNameSchema, l as create, m as getSchemaResult, n as GeneratorError, o as VirtualFileSystem, p as generate, r as Result, s as add, t as EMBEDDED_TEMPLATES, u as createBtsCli, w as UserCancelledError, x as DatabaseSetupError, y as CLIError } from "./src-BZV4gWGl.mjs";
2
+ import { C as ProjectCreationError, S as DirectoryConflictError, T as ValidationError, a as TEMPLATE_COUNT, b as CompatibilityError, c as builder, d as createVirtual, f as docs, g as sponsors, h as router, i as SchemaNameSchema, l as create, m as getSchemaResult, n as GeneratorError, o as VirtualFileSystem, p as generate, r as Result, s as add, t as EMBEDDED_TEMPLATES, u as createBtsCli, w as UserCancelledError, x as DatabaseSetupError, y as CLIError } from "./src-_FzI_58B.mjs";
3
3
  export { CLIError, CompatibilityError, DatabaseSetupError, DirectoryConflictError, EMBEDDED_TEMPLATES, GeneratorError, ProjectCreationError, Result, SchemaNameSchema, TEMPLATE_COUNT, UserCancelledError, ValidationError, VirtualFileSystem, add, builder, create, createBtsCli, createVirtual, docs, generate, getSchemaResult, router, sponsors };
@@ -875,6 +875,12 @@ function validateServerDeployRequiresBackend(serverDeploy, backend) {
875
875
  if (serverDeploy && serverDeploy !== "none" && (!backend || backend === "none")) return validationErr$1("'--server-deploy' requires a backend. Please select a backend or set '--server-deploy none'.");
876
876
  return Result.ok(void 0);
877
877
  }
878
+ function validateDockerServerDeploy(serverDeploy, backend, runtime) {
879
+ if (serverDeploy !== "docker") return Result.ok(void 0);
880
+ if (backend === "convex" || backend === "self") return validationErr$1("'--server-deploy docker' requires a separate server backend (hono, express, fastify, elysia). For a fullstack 'self' backend, use '--web-deploy docker' instead.");
881
+ if (runtime === "workers") return validationErr$1("'--server-deploy docker' is not compatible with '--runtime workers'. Use '--runtime bun' or '--runtime node', or choose '--server-deploy cloudflare'.");
882
+ return Result.ok(void 0);
883
+ }
878
884
  function validateAddonCompatibility(addon, frontend, _auth, backend, runtime) {
879
885
  if (addon === "evlog" && !supportsEvlogAddon(frontend, backend, runtime)) return {
880
886
  isCompatible: false,
@@ -985,7 +991,7 @@ function normalizeValidationMessage(validationMessage) {
985
991
  async function runWithNavigation(prompt) {
986
992
  let goBack = false;
987
993
  prompt.on("key", (char) => {
988
- if (char === "b" && !isFirstPrompt()) {
994
+ if ((char === "b" || char === "B") && !isFirstPrompt()) {
989
995
  goBack = true;
990
996
  prompt.state = "cancel";
991
997
  }
@@ -1171,6 +1177,10 @@ async function navigableGroupMultiselect(opts) {
1171
1177
  }
1172
1178
  }));
1173
1179
  }
1180
+ /** Use the remembered answer as the initial value only while it is still selectable. */
1181
+ function preferValidInitial(options, previous, fallback) {
1182
+ return previous !== void 0 && options.some((o) => o.value === previous) ? previous : fallback;
1183
+ }
1174
1184
  //#endregion
1175
1185
  //#region src/prompts/addons.ts
1176
1186
  function getAddonDisplay(addon) {
@@ -1310,7 +1320,7 @@ function validateAddonSelection(selected) {
1310
1320
  "vite-plus"
1311
1321
  ].includes(addon)) ?? []).length > 1) return "Choose Turborepo, Nx, or Vite+ as your task runner, not more than one.";
1312
1322
  }
1313
- async function getAddonsChoice(addons, frontends, auth, backend, runtime) {
1323
+ async function getAddonsChoice(addons, frontends, auth, backend, runtime, previousValue) {
1314
1324
  if (addons !== void 0) return addons;
1315
1325
  const allAddons = types_exports.AddonsSchema.options.filter((addon) => addon !== "none");
1316
1326
  const groupedOptions = createGroupedOptions();
@@ -1329,7 +1339,7 @@ async function getAddonsChoice(addons, frontends, auth, backend, runtime) {
1329
1339
  const response = await navigableGroupMultiselect({
1330
1340
  message: "Select addons",
1331
1341
  options: groupedOptions,
1332
- initialValues: DEFAULT_CONFIG.addons.filter((addonValue) => Object.values(groupedOptions).some((options) => options.some((opt) => opt.value === addonValue))),
1342
+ initialValues: (previousValue ?? DEFAULT_CONFIG.addons).filter((addonValue) => Object.values(groupedOptions).some((options) => options.some((opt) => opt.value === addonValue))),
1333
1343
  required: false,
1334
1344
  validate: validateAddonSelection
1335
1345
  });
@@ -1974,26 +1984,36 @@ async function setupEvlog(config) {
1974
1984
  /**
1975
1985
  * Define a group of prompts that supports going back to previous prompts.
1976
1986
  * Returns a result object with all the values, or handles cancel/go-back navigation.
1987
+ * When navigating back, the discarded answers are replayed to the prompts as
1988
+ * `previousAnswer` so they can preselect what the user chose before.
1977
1989
  */
1978
1990
  async function navigableGroup(prompts, opts) {
1979
1991
  const results = {};
1992
+ const previousAnswers = {};
1980
1993
  const promptNames = Object.keys(prompts);
1981
1994
  let currentIndex = 0;
1982
1995
  let goingBack = false;
1996
+ const stepBack = () => {
1997
+ const prevName = promptNames[currentIndex - 1];
1998
+ previousAnswers[prevName] = results[prevName];
1999
+ delete results[prevName];
2000
+ currentIndex--;
2001
+ };
1983
2002
  while (currentIndex < promptNames.length) {
1984
2003
  const name = promptNames[currentIndex];
1985
2004
  const prompt = prompts[name];
1986
2005
  setIsFirstPrompt$1(currentIndex === 0);
1987
2006
  setLastPromptShownUI(false);
1988
- const result = await prompt({ results })?.catch((e) => {
2007
+ const result = await prompt({
2008
+ results,
2009
+ previousAnswer: previousAnswers[name]
2010
+ })?.catch((e) => {
1989
2011
  throw e;
1990
2012
  });
1991
2013
  if (isGoBack(result)) {
1992
2014
  goingBack = true;
1993
2015
  if (currentIndex > 0) {
1994
- const prevName = promptNames[currentIndex - 1];
1995
- delete results[prevName];
1996
- currentIndex--;
2016
+ stepBack();
1997
2017
  continue;
1998
2018
  }
1999
2019
  goingBack = false;
@@ -2009,9 +2029,7 @@ async function navigableGroup(prompts, opts) {
2009
2029
  }
2010
2030
  if (goingBack && !didLastPromptShowUI()) {
2011
2031
  if (currentIndex > 0) {
2012
- const prevName = promptNames[currentIndex - 1];
2013
- delete results[prevName];
2014
- currentIndex--;
2032
+ stepBack();
2015
2033
  continue;
2016
2034
  }
2017
2035
  }
@@ -4045,7 +4063,7 @@ async function addHandlerInternal(input) {
4045
4063
  }
4046
4064
  //#endregion
4047
4065
  //#region src/prompts/api.ts
4048
- async function getApiChoice(Api, frontend, backend) {
4066
+ async function getApiChoice(Api, frontend, backend, previousValue) {
4049
4067
  if (backend === "convex" || backend === "none") return "none";
4050
4068
  const allowed = allowedApisForFrontends(frontend ?? []);
4051
4069
  if (Api) {
@@ -4069,7 +4087,7 @@ async function getApiChoice(Api, frontend, backend) {
4069
4087
  const apiType = await navigableSelect({
4070
4088
  message: "Select API type",
4071
4089
  options: apiOptions,
4072
- initialValue: apiOptions[0].value
4090
+ initialValue: preferValidInitial(apiOptions, previousValue, apiOptions[0].value)
4073
4091
  });
4074
4092
  if (isCancel$1(apiType)) throw new UserCancelledError({ message: "Operation cancelled" });
4075
4093
  return apiType;
@@ -4095,7 +4113,7 @@ function getAvailableAuthProviders(backend, frontend = []) {
4095
4113
  if (options.length === 0) return ["none"];
4096
4114
  return [...options, "none"];
4097
4115
  }
4098
- async function getAuthChoice(auth, backend, frontend = []) {
4116
+ async function getAuthChoice(auth, backend, frontend = [], previousValue) {
4099
4117
  if (auth !== void 0) return auth;
4100
4118
  const availableProviders = getAvailableAuthProviders(backend, frontend);
4101
4119
  if (availableProviders.length === 1 && availableProviders[0] === "none") return "none";
@@ -4121,7 +4139,7 @@ async function getAuthChoice(auth, backend, frontend = []) {
4121
4139
  const response = await navigableSelect({
4122
4140
  message: "Select authentication provider",
4123
4141
  options,
4124
- initialValue: options.some((option) => option.value === DEFAULT_CONFIG.auth) ? DEFAULT_CONFIG.auth : "none"
4142
+ initialValue: preferValidInitial(options, previousValue, options.some((option) => option.value === DEFAULT_CONFIG.auth) ? DEFAULT_CONFIG.auth : "none")
4125
4143
  });
4126
4144
  if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4127
4145
  return response;
@@ -4135,7 +4153,7 @@ const FULLSTACK_FRONTENDS = [
4135
4153
  "svelte",
4136
4154
  "astro"
4137
4155
  ];
4138
- async function getBackendFrameworkChoice(backendFramework, frontends) {
4156
+ async function getBackendFrameworkChoice(backendFramework, frontends, previousValue) {
4139
4157
  if (backendFramework !== void 0) return backendFramework;
4140
4158
  const hasIncompatibleFrontend = frontends?.some((f) => f === "solid" || f === "astro");
4141
4159
  const hasFullstackFrontend = frontends?.some((f) => FULLSTACK_FRONTENDS.includes(f));
@@ -4175,14 +4193,14 @@ async function getBackendFrameworkChoice(backendFramework, frontends) {
4175
4193
  const response = await navigableSelect({
4176
4194
  message: "Select backend",
4177
4195
  options: backendOptions,
4178
- initialValue: hasFullstackFrontend ? "self" : DEFAULT_CONFIG.backend
4196
+ initialValue: preferValidInitial(backendOptions, previousValue, hasFullstackFrontend ? "self" : DEFAULT_CONFIG.backend)
4179
4197
  });
4180
4198
  if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4181
4199
  return response;
4182
4200
  }
4183
4201
  //#endregion
4184
4202
  //#region src/prompts/database.ts
4185
- async function getDatabaseChoice(database, backend, runtime) {
4203
+ async function getDatabaseChoice(database, backend, runtime, previousValue) {
4186
4204
  if (backend === "convex" || backend === "none") return "none";
4187
4205
  if (database !== void 0) return database;
4188
4206
  const databaseOptions = [
@@ -4215,14 +4233,14 @@ async function getDatabaseChoice(database, backend, runtime) {
4215
4233
  const response = await navigableSelect({
4216
4234
  message: "Select database",
4217
4235
  options: databaseOptions,
4218
- initialValue: DEFAULT_CONFIG.database
4236
+ initialValue: preferValidInitial(databaseOptions, previousValue, DEFAULT_CONFIG.database)
4219
4237
  });
4220
4238
  if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4221
4239
  return response;
4222
4240
  }
4223
4241
  //#endregion
4224
4242
  //#region src/prompts/database-setup.ts
4225
- async function getDBSetupChoice(databaseType, dbSetup, _orm, backend, runtime) {
4243
+ async function getDBSetupChoice(databaseType, dbSetup, _orm, backend, runtime, previousValue) {
4226
4244
  if (backend === "convex") return "none";
4227
4245
  if (dbSetup !== void 0) return dbSetup;
4228
4246
  if (databaseType === "none") return "none";
@@ -4314,14 +4332,14 @@ async function getDBSetupChoice(databaseType, dbSetup, _orm, backend, runtime) {
4314
4332
  const response = await navigableSelect({
4315
4333
  message: `Select ${databaseType} setup option`,
4316
4334
  options,
4317
- initialValue: "none"
4335
+ initialValue: preferValidInitial(options, previousValue, "none")
4318
4336
  });
4319
4337
  if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4320
4338
  return response;
4321
4339
  }
4322
4340
  //#endregion
4323
4341
  //#region src/prompts/examples.ts
4324
- async function getExamplesChoice(examples, database, frontends, backend, api) {
4342
+ async function getExamplesChoice(examples, database, frontends, backend, api, previousValue) {
4325
4343
  if (examples !== void 0) return examples;
4326
4344
  if (backend === "none") return [];
4327
4345
  let response = [];
@@ -4341,15 +4359,27 @@ async function getExamplesChoice(examples, database, frontends, backend, api) {
4341
4359
  message: "Include examples",
4342
4360
  options,
4343
4361
  required: false,
4344
- initialValues: DEFAULT_CONFIG.examples?.filter((ex) => options.some((o) => o.value === ex))
4362
+ initialValues: (previousValue ?? DEFAULT_CONFIG.examples)?.filter((ex) => options.some((o) => o.value === ex))
4345
4363
  });
4346
4364
  if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4347
4365
  return response;
4348
4366
  }
4349
4367
  //#endregion
4350
4368
  //#region src/prompts/frontend.ts
4351
- async function getFrontendChoice(frontendOptions, backend, auth) {
4369
+ const WEB_FRONTEND_VALUES = [
4370
+ "tanstack-router",
4371
+ "react-router",
4372
+ "next",
4373
+ "nuxt",
4374
+ "svelte",
4375
+ "solid",
4376
+ "astro",
4377
+ "tanstack-start"
4378
+ ];
4379
+ async function getFrontendChoice(frontendOptions, backend, auth, previousValue) {
4352
4380
  if (frontendOptions !== void 0) return frontendOptions;
4381
+ const previousWeb = previousValue?.find((f) => WEB_FRONTEND_VALUES.includes(f));
4382
+ const previousNative = previousValue?.find((f) => f.startsWith("native-"));
4353
4383
  while (true) {
4354
4384
  const wasFirstPrompt = isFirstPrompt();
4355
4385
  const frontendTypes = await navigableMultiselect({
@@ -4364,7 +4394,7 @@ async function getFrontendChoice(frontendOptions, backend, auth) {
4364
4394
  hint: "Create a React Native/Expo app"
4365
4395
  }],
4366
4396
  required: false,
4367
- initialValues: ["web"]
4397
+ initialValues: previousValue ? [...previousWeb ? ["web"] : [], ...previousNative ? ["native"] : []] : ["web"]
4368
4398
  });
4369
4399
  if (isGoBack(frontendTypes)) return GO_BACK_SYMBOL;
4370
4400
  if (isCancel$1(frontendTypes)) throw new UserCancelledError({ message: "Operation cancelled" });
@@ -4372,51 +4402,52 @@ async function getFrontendChoice(frontendOptions, backend, auth) {
4372
4402
  const result = [];
4373
4403
  let shouldRestart = false;
4374
4404
  if (frontendTypes.includes("web")) {
4405
+ const webOptions = [
4406
+ {
4407
+ value: "tanstack-router",
4408
+ label: "TanStack Router",
4409
+ hint: "Modern and scalable routing for React Applications"
4410
+ },
4411
+ {
4412
+ value: "react-router",
4413
+ label: "React Router",
4414
+ hint: "A user‑obsessed, standards‑focused, multi‑strategy router"
4415
+ },
4416
+ {
4417
+ value: "next",
4418
+ label: "Next.js",
4419
+ hint: "The React Framework for the Web"
4420
+ },
4421
+ {
4422
+ value: "nuxt",
4423
+ label: "Nuxt",
4424
+ hint: "The Progressive Web Framework for Vue.js"
4425
+ },
4426
+ {
4427
+ value: "svelte",
4428
+ label: "Svelte",
4429
+ hint: "web development for the rest of us"
4430
+ },
4431
+ {
4432
+ value: "solid",
4433
+ label: "Solid",
4434
+ hint: "Simple and performant reactivity for building user interfaces"
4435
+ },
4436
+ {
4437
+ value: "astro",
4438
+ label: "Astro",
4439
+ hint: "The web framework for content-driven websites"
4440
+ },
4441
+ {
4442
+ value: "tanstack-start",
4443
+ label: "TanStack Start",
4444
+ hint: "SSR, Server Functions, API Routes and more with TanStack Router"
4445
+ }
4446
+ ].filter((option) => isFrontendAllowedWithBackend(option.value, backend, auth));
4375
4447
  const webFramework = await navigableSelect({
4376
4448
  message: "Choose web",
4377
- options: [
4378
- {
4379
- value: "tanstack-router",
4380
- label: "TanStack Router",
4381
- hint: "Modern and scalable routing for React Applications"
4382
- },
4383
- {
4384
- value: "react-router",
4385
- label: "React Router",
4386
- hint: "A user‑obsessed, standards‑focused, multi‑strategy router"
4387
- },
4388
- {
4389
- value: "next",
4390
- label: "Next.js",
4391
- hint: "The React Framework for the Web"
4392
- },
4393
- {
4394
- value: "nuxt",
4395
- label: "Nuxt",
4396
- hint: "The Progressive Web Framework for Vue.js"
4397
- },
4398
- {
4399
- value: "svelte",
4400
- label: "Svelte",
4401
- hint: "web development for the rest of us"
4402
- },
4403
- {
4404
- value: "solid",
4405
- label: "Solid",
4406
- hint: "Simple and performant reactivity for building user interfaces"
4407
- },
4408
- {
4409
- value: "astro",
4410
- label: "Astro",
4411
- hint: "The web framework for content-driven websites"
4412
- },
4413
- {
4414
- value: "tanstack-start",
4415
- label: "TanStack Start",
4416
- hint: "SSR, Server Functions, API Routes and more with TanStack Router"
4417
- }
4418
- ].filter((option) => isFrontendAllowedWithBackend(option.value, backend, auth)),
4419
- initialValue: DEFAULT_CONFIG.frontend[0]
4449
+ options: webOptions,
4450
+ initialValue: preferValidInitial(webOptions, previousWeb, DEFAULT_CONFIG.frontend[0])
4420
4451
  });
4421
4452
  if (isGoBack(webFramework)) shouldRestart = true;
4422
4453
  else if (isCancel$1(webFramework)) throw new UserCancelledError({ message: "Operation cancelled" });
@@ -4446,7 +4477,7 @@ async function getFrontendChoice(frontendOptions, backend, auth) {
4446
4477
  hint: "Consistent styling for React Native"
4447
4478
  }
4448
4479
  ],
4449
- initialValue: "native-bare"
4480
+ initialValue: previousNative ?? "native-bare"
4450
4481
  });
4451
4482
  if (isGoBack(nativeFramework)) if (frontendTypes.includes("web")) shouldRestart = true;
4452
4483
  else {
@@ -4465,22 +4496,22 @@ async function getFrontendChoice(frontendOptions, backend, auth) {
4465
4496
  }
4466
4497
  //#endregion
4467
4498
  //#region src/prompts/git.ts
4468
- async function getGitChoice(git) {
4499
+ async function getGitChoice(git, previousValue) {
4469
4500
  if (git !== void 0) return git;
4470
4501
  const response = await navigableConfirm({
4471
4502
  message: "Initialize git repository?",
4472
- initialValue: DEFAULT_CONFIG.git
4503
+ initialValue: previousValue ?? DEFAULT_CONFIG.git
4473
4504
  });
4474
4505
  if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4475
4506
  return response;
4476
4507
  }
4477
4508
  //#endregion
4478
4509
  //#region src/prompts/install.ts
4479
- async function getinstallChoice(install) {
4510
+ async function getinstallChoice(install, previousValue) {
4480
4511
  if (install !== void 0) return install;
4481
4512
  const response = await navigableConfirm({
4482
4513
  message: "Install dependencies?",
4483
- initialValue: DEFAULT_CONFIG.install
4514
+ initialValue: previousValue ?? DEFAULT_CONFIG.install
4484
4515
  });
4485
4516
  if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4486
4517
  return response;
@@ -4664,6 +4695,7 @@ function validateFullConfig(config, providedFlags, options) {
4664
4695
  yield* validateFrontendConstraints(config, providedFlags);
4665
4696
  yield* validateApiConstraints(config, options);
4666
4697
  yield* validateServerDeployRequiresBackend(config.serverDeploy, config.backend);
4698
+ yield* validateDockerServerDeploy(config.serverDeploy, config.backend, config.runtime);
4667
4699
  yield* validateSelfBackendCompatibility(providedFlags, options, config);
4668
4700
  yield* validateWorkersCompatibility(providedFlags, options, config);
4669
4701
  if (config.runtime === "workers" && config.serverDeploy === "none") yield* validationErr("Cloudflare Workers runtime requires a server deployment. Please choose 'cloudflare' for --server-deploy.");
@@ -4707,7 +4739,7 @@ const ormOptions = {
4707
4739
  hint: "Lightweight and performant TypeScript ORM"
4708
4740
  }
4709
4741
  };
4710
- async function getORMChoice(orm, hasDatabase, database, backend, runtime) {
4742
+ async function getORMChoice(orm, hasDatabase, database, backend, runtime, previousValue) {
4711
4743
  if (backend === "convex") return "none";
4712
4744
  if (!hasDatabase) return "none";
4713
4745
  if (orm !== void 0) {
@@ -4715,18 +4747,20 @@ async function getORMChoice(orm, hasDatabase, database, backend, runtime) {
4715
4747
  if (compat.isErr()) throw compat.error;
4716
4748
  return orm;
4717
4749
  }
4750
+ const options = database === "mongodb" ? [ormOptions.prisma, ormOptions.mongoose] : [ormOptions.drizzle, ormOptions.prisma];
4718
4751
  const response = await navigableSelect({
4719
4752
  message: "Select ORM",
4720
- options: database === "mongodb" ? [ormOptions.prisma, ormOptions.mongoose] : [ormOptions.drizzle, ormOptions.prisma],
4721
- initialValue: database === "mongodb" ? "prisma" : runtime === "workers" ? "drizzle" : DEFAULT_CONFIG.orm
4753
+ options,
4754
+ initialValue: preferValidInitial(options, previousValue, database === "mongodb" ? "prisma" : runtime === "workers" ? "drizzle" : DEFAULT_CONFIG.orm)
4722
4755
  });
4723
4756
  if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4724
4757
  return response;
4725
4758
  }
4726
4759
  //#endregion
4727
4760
  //#region src/prompts/package-manager.ts
4728
- async function getPackageManagerChoice(packageManager) {
4761
+ async function getPackageManagerChoice(packageManager, previousValue) {
4729
4762
  if (packageManager !== void 0) return packageManager;
4763
+ const detectedPackageManager = getUserPkgManager();
4730
4764
  const response = await navigableSelect({
4731
4765
  message: "Choose package manager",
4732
4766
  options: [
@@ -4746,36 +4780,37 @@ async function getPackageManagerChoice(packageManager) {
4746
4780
  hint: "All-in-one JavaScript runtime & toolkit"
4747
4781
  }
4748
4782
  ],
4749
- initialValue: getUserPkgManager()
4783
+ initialValue: previousValue ?? detectedPackageManager
4750
4784
  });
4751
4785
  if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4752
4786
  return response;
4753
4787
  }
4754
4788
  //#endregion
4755
4789
  //#region src/prompts/payments.ts
4756
- async function getPaymentsChoice(payments, auth, backend, _frontends) {
4790
+ async function getPaymentsChoice(payments, auth, backend, _frontends, previousValue) {
4757
4791
  if (payments !== void 0) return payments;
4758
4792
  if (backend === "none") return "none";
4759
4793
  if (!(auth === "better-auth")) return "none";
4794
+ const options = [{
4795
+ value: "polar",
4796
+ label: "Polar",
4797
+ hint: "Turn your software into a business. 6 lines of code."
4798
+ }, {
4799
+ value: "none",
4800
+ label: "None",
4801
+ hint: "No payments integration"
4802
+ }];
4760
4803
  const response = await navigableSelect({
4761
4804
  message: "Select payments provider",
4762
- options: [{
4763
- value: "polar",
4764
- label: "Polar",
4765
- hint: "Turn your software into a business. 6 lines of code."
4766
- }, {
4767
- value: "none",
4768
- label: "None",
4769
- hint: "No payments integration"
4770
- }],
4771
- initialValue: DEFAULT_CONFIG.payments
4805
+ options,
4806
+ initialValue: preferValidInitial(options, previousValue, DEFAULT_CONFIG.payments)
4772
4807
  });
4773
4808
  if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4774
4809
  return response;
4775
4810
  }
4776
4811
  //#endregion
4777
4812
  //#region src/prompts/runtime.ts
4778
- async function getRuntimeChoice(runtime, backend) {
4813
+ async function getRuntimeChoice(runtime, backend, previousValue) {
4779
4814
  if (backend === "convex" || backend === "none" || backend === "self") return "none";
4780
4815
  if (runtime !== void 0) return runtime;
4781
4816
  const runtimeOptions = [{
@@ -4795,19 +4830,56 @@ async function getRuntimeChoice(runtime, backend) {
4795
4830
  const response = await navigableSelect({
4796
4831
  message: "Select runtime",
4797
4832
  options: runtimeOptions,
4798
- initialValue: DEFAULT_CONFIG.runtime
4833
+ initialValue: preferValidInitial(runtimeOptions, previousValue, DEFAULT_CONFIG.runtime)
4799
4834
  });
4800
4835
  if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4801
4836
  return response;
4802
4837
  }
4803
4838
  //#endregion
4804
4839
  //#region src/prompts/server-deploy.ts
4805
- async function getServerDeploymentChoice(deployment, runtime, backend, _webDeploy) {
4840
+ const SERVER_APP_BACKENDS = [
4841
+ "hono",
4842
+ "express",
4843
+ "fastify",
4844
+ "elysia"
4845
+ ];
4846
+ function getDeploymentDisplay$1(deployment) {
4847
+ if (deployment === "cloudflare") return {
4848
+ label: "Cloudflare",
4849
+ hint: "Deploy to Cloudflare Workers using Alchemy"
4850
+ };
4851
+ if (deployment === "docker") return {
4852
+ label: "Docker",
4853
+ hint: "Self-host with a Dockerfile and docker-compose.yml"
4854
+ };
4855
+ return {
4856
+ label: deployment,
4857
+ hint: `Add ${deployment} deployment`
4858
+ };
4859
+ }
4860
+ async function getServerDeploymentChoice(deployment, runtime, backend, _webDeploy, previousValue) {
4806
4861
  if (deployment !== void 0) return deployment;
4807
- if (backend === "none" || backend === "convex") return "none";
4808
- if (backend !== "hono") return "none";
4862
+ if (!backend || !SERVER_APP_BACKENDS.includes(backend)) return "none";
4809
4863
  if (runtime === "workers") return "cloudflare";
4810
- return "none";
4864
+ if (runtime !== "bun" && runtime !== "node") return "none";
4865
+ const options = ["docker", "none"].map((deploy) => {
4866
+ const { label, hint } = deploy === "none" ? {
4867
+ label: "None",
4868
+ hint: "Skip deployment setup"
4869
+ } : getDeploymentDisplay$1(deploy);
4870
+ return {
4871
+ value: deploy,
4872
+ label,
4873
+ hint
4874
+ };
4875
+ });
4876
+ const response = await navigableSelect({
4877
+ message: "Select server deployment",
4878
+ options,
4879
+ initialValue: preferValidInitial(options, previousValue, DEFAULT_CONFIG.serverDeploy)
4880
+ });
4881
+ if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4882
+ return response;
4811
4883
  }
4812
4884
  //#endregion
4813
4885
  //#region src/prompts/web-deploy.ts
@@ -4819,26 +4891,35 @@ function getDeploymentDisplay(deployment) {
4819
4891
  label: "Cloudflare",
4820
4892
  hint: "Deploy to Cloudflare Workers using Alchemy"
4821
4893
  };
4894
+ if (deployment === "docker") return {
4895
+ label: "Docker",
4896
+ hint: "Self-host with a Dockerfile and docker-compose.yml"
4897
+ };
4822
4898
  return {
4823
4899
  label: deployment,
4824
4900
  hint: `Add ${deployment} deployment`
4825
4901
  };
4826
4902
  }
4827
- async function getDeploymentChoice(deployment, _runtime, backend, frontend = [], dbSetup) {
4903
+ async function getDeploymentChoice(deployment, _runtime, backend, frontend = [], dbSetup, previousValue) {
4828
4904
  if (deployment !== void 0) return deployment;
4829
4905
  if (!hasWebFrontend(frontend)) return "none";
4830
4906
  if (backend === "self" && dbSetup === "d1") return "cloudflare";
4907
+ const options = [
4908
+ "cloudflare",
4909
+ "docker",
4910
+ "none"
4911
+ ].map((deploy) => {
4912
+ const { label, hint } = getDeploymentDisplay(deploy);
4913
+ return {
4914
+ value: deploy,
4915
+ label,
4916
+ hint
4917
+ };
4918
+ });
4831
4919
  const response = await navigableSelect({
4832
4920
  message: "Select web deployment",
4833
- options: ["cloudflare", "none"].map((deploy) => {
4834
- const { label, hint } = getDeploymentDisplay(deploy);
4835
- return {
4836
- value: deploy,
4837
- label,
4838
- hint
4839
- };
4840
- }),
4841
- initialValue: DEFAULT_CONFIG.webDeploy
4921
+ options,
4922
+ initialValue: preferValidInitial(options, previousValue, DEFAULT_CONFIG.webDeploy)
4842
4923
  });
4843
4924
  if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4844
4925
  return response;
@@ -4870,22 +4951,22 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
4870
4951
  serverDeploy: flags.serverDeploy ?? DEFAULT_CONFIG.serverDeploy
4871
4952
  };
4872
4953
  const result = await navigableGroup({
4873
- frontend: () => getFrontendChoice(flags.frontend, flags.backend, flags.auth),
4874
- backend: ({ results }) => getBackendFrameworkChoice(flags.backend, results.frontend),
4875
- runtime: ({ results }) => getRuntimeChoice(flags.runtime, results.backend),
4876
- database: ({ results }) => getDatabaseChoice(flags.database, results.backend, results.runtime),
4877
- orm: ({ results }) => getORMChoice(flags.orm, results.database !== "none", results.database, results.backend, results.runtime),
4878
- api: ({ results }) => getApiChoice(flags.api, results.frontend, results.backend),
4879
- auth: ({ results }) => getAuthChoice(flags.auth, results.backend, results.frontend),
4880
- payments: ({ results }) => getPaymentsChoice(flags.payments, results.auth, results.backend, results.frontend),
4881
- addons: ({ results }) => getAddonsChoice(flags.addons, results.frontend, results.auth, results.backend, results.runtime),
4882
- examples: ({ results }) => getExamplesChoice(flags.examples, results.database, results.frontend, results.backend, results.api),
4883
- dbSetup: ({ results }) => getDBSetupChoice(results.database ?? "none", flags.dbSetup, results.orm, results.backend, results.runtime),
4884
- webDeploy: ({ results }) => getDeploymentChoice(flags.webDeploy, results.runtime, results.backend, results.frontend, results.dbSetup),
4885
- serverDeploy: ({ results }) => getServerDeploymentChoice(flags.serverDeploy, results.runtime, results.backend, results.webDeploy),
4886
- git: () => getGitChoice(flags.git),
4887
- packageManager: () => getPackageManagerChoice(flags.packageManager),
4888
- install: () => getinstallChoice(flags.install)
4954
+ frontend: ({ previousAnswer }) => getFrontendChoice(flags.frontend, flags.backend, flags.auth, previousAnswer),
4955
+ backend: ({ results, previousAnswer }) => getBackendFrameworkChoice(flags.backend, results.frontend, previousAnswer),
4956
+ runtime: ({ results, previousAnswer }) => getRuntimeChoice(flags.runtime, results.backend, previousAnswer),
4957
+ database: ({ results, previousAnswer }) => getDatabaseChoice(flags.database, results.backend, results.runtime, previousAnswer),
4958
+ orm: ({ results, previousAnswer }) => getORMChoice(flags.orm, results.database !== "none", results.database, results.backend, results.runtime, previousAnswer),
4959
+ api: ({ results, previousAnswer }) => getApiChoice(flags.api, results.frontend, results.backend, previousAnswer),
4960
+ auth: ({ results, previousAnswer }) => getAuthChoice(flags.auth, results.backend, results.frontend, previousAnswer),
4961
+ payments: ({ results, previousAnswer }) => getPaymentsChoice(flags.payments, results.auth, results.backend, results.frontend, previousAnswer),
4962
+ addons: ({ results, previousAnswer }) => getAddonsChoice(flags.addons, results.frontend, results.auth, results.backend, results.runtime, previousAnswer),
4963
+ examples: ({ results, previousAnswer }) => getExamplesChoice(flags.examples, results.database, results.frontend, results.backend, results.api, previousAnswer),
4964
+ dbSetup: ({ results, previousAnswer }) => getDBSetupChoice(results.database ?? "none", flags.dbSetup, results.orm, results.backend, results.runtime, previousAnswer),
4965
+ webDeploy: ({ results, previousAnswer }) => getDeploymentChoice(flags.webDeploy, results.runtime, results.backend, results.frontend, results.dbSetup, previousAnswer),
4966
+ serverDeploy: ({ results, previousAnswer }) => getServerDeploymentChoice(flags.serverDeploy, results.runtime, results.backend, results.webDeploy, previousAnswer),
4967
+ git: ({ previousAnswer }) => getGitChoice(flags.git, previousAnswer),
4968
+ packageManager: ({ previousAnswer }) => getPackageManagerChoice(flags.packageManager, previousAnswer),
4969
+ install: ({ previousAnswer }) => getinstallChoice(flags.install, previousAnswer)
4889
4970
  }, { onCancel: () => {
4890
4971
  throw new UserCancelledError({ message: "Operation cancelled" });
4891
4972
  } });
@@ -7057,6 +7138,10 @@ function getAlchemyDeployInstructions(runCmd, webDeploy, serverDeploy, backend)
7057
7138
  if (webDeploy === "cloudflare" && serverDeploy !== "cloudflare" && !isBackendSelf) instructions.push(`${pc.bold("Deploy web with Cloudflare (Alchemy):")}\n${pc.cyan("•")} Dev: ${`${runCmd} dev`}\n${pc.cyan("•")} Deploy: ${`${runCmd} deploy`}\n${pc.cyan("•")} Destroy: ${`${runCmd} destroy`}`);
7058
7139
  else if (serverDeploy === "cloudflare" && webDeploy !== "cloudflare" && !isBackendSelf) instructions.push(`${pc.bold("Deploy server with Cloudflare (Alchemy):")}\n${pc.cyan("•")} Dev: ${`${runCmd} dev`}\n${pc.cyan("•")} Deploy: ${`${runCmd} deploy`}\n${pc.cyan("•")} Destroy: ${`${runCmd} destroy`}`);
7059
7140
  else if (webDeploy === "cloudflare" && (serverDeploy === "cloudflare" || isBackendSelf)) instructions.push(`${pc.bold("Deploy with Cloudflare (Alchemy):")}\n${pc.cyan("•")} Dev: ${`${runCmd} dev`}\n${pc.cyan("•")} Deploy: ${`${runCmd} deploy`}\n${pc.cyan("•")} Destroy: ${`${runCmd} destroy`}`);
7141
+ if (webDeploy === "docker" || serverDeploy === "docker") {
7142
+ const dockerTargets = webDeploy === "docker" && serverDeploy === "docker" ? "web + server" : webDeploy === "docker" ? "web" : "server";
7143
+ instructions.push(`${pc.bold(`Deploy ${dockerTargets} with Docker Compose:`)}\n${pc.cyan("•")} Start: ${`${runCmd} docker:up`}\n${pc.cyan("•")} Logs: ${`${runCmd} docker:logs`}\n${pc.cyan("•")} Stop: ${`${runCmd} docker:down`}\n${pc.cyan("•")} Config: docker-compose.yml`);
7144
+ }
7060
7145
  return instructions.length ? `\n${instructions.join("\n")}` : "";
7061
7146
  }
7062
7147
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-better-t-stack",
3
- "version": "3.32.0",
3
+ "version": "3.33.0",
4
4
  "description": "A modern CLI tool for scaffolding end-to-end type-safe TypeScript projects with best practices and customizable configurations",
5
5
  "keywords": [
6
6
  "better-auth",
@@ -69,10 +69,10 @@
69
69
  "prepublishOnly": "npm run build"
70
70
  },
71
71
  "dependencies": {
72
- "@better-t-stack/template-generator": "^3.32.0",
73
- "@better-t-stack/types": "^3.32.0",
74
- "@clack/core": "^1.3.1",
75
- "@clack/prompts": "^1.4.0",
72
+ "@better-t-stack/template-generator": "^3.33.0",
73
+ "@better-t-stack/types": "^3.33.0",
74
+ "@clack/core": "^1.4.1",
75
+ "@clack/prompts": "^1.5.1",
76
76
  "@modelcontextprotocol/sdk": "1.29.0",
77
77
  "@trpc/server": "^11.17.0",
78
78
  "better-result": "^2.9.2",