create-better-t-stack 2.33.8-canary.98a850ad → 2.33.9-canary.20c8e952

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/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { createBtsCli } from "./src-D0MPVT22.js";
2
+ import { createBtsCli } from "./src-CpB-qAME.js";
3
3
 
4
4
  //#region src/cli.ts
5
5
  createBtsCli().run();
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import { builder, createBtsCli, docs, init, router, sponsors } from "./src-D0MPVT22.js";
2
+ import { builder, createBtsCli, docs, init, router, sponsors } from "./src-CpB-qAME.js";
3
3
 
4
4
  export { builder, createBtsCli, docs, init, router, sponsors };
@@ -5,7 +5,6 @@ import { createCli, trpcServer } from "trpc-cli";
5
5
  import z from "zod";
6
6
  import path from "node:path";
7
7
  import consola, { consola as consola$1 } from "consola";
8
- import * as fs$1 from "fs-extra";
9
8
  import fs from "fs-extra";
10
9
  import { fileURLToPath } from "node:url";
11
10
  import gradient from "gradient-string";
@@ -30,9 +29,8 @@ const getUserPkgManager = () => {
30
29
  const __filename = fileURLToPath(import.meta.url);
31
30
  const distPath = path.dirname(__filename);
32
31
  const PKG_ROOT = path.join(distPath, "../");
33
- const DEFAULT_CONFIG = {
32
+ const DEFAULT_CONFIG_BASE = {
34
33
  projectName: "my-better-t-app",
35
- projectDir: path.resolve(process.cwd(), "my-better-t-app"),
36
34
  relativePath: "my-better-t-app",
37
35
  frontend: ["tanstack-router"],
38
36
  database: "sqlite",
@@ -41,7 +39,6 @@ const DEFAULT_CONFIG = {
41
39
  addons: ["turborepo"],
42
40
  examples: [],
43
41
  git: true,
44
- packageManager: getUserPkgManager(),
45
42
  install: true,
46
43
  dbSetup: "none",
47
44
  backend: "hono",
@@ -50,6 +47,17 @@ const DEFAULT_CONFIG = {
50
47
  webDeploy: "none",
51
48
  serverDeploy: "none"
52
49
  };
50
+ function getDefaultConfig() {
51
+ return {
52
+ ...DEFAULT_CONFIG_BASE,
53
+ projectDir: path.resolve(process.cwd(), DEFAULT_CONFIG_BASE.projectName),
54
+ packageManager: getUserPkgManager(),
55
+ frontend: [...DEFAULT_CONFIG_BASE.frontend],
56
+ addons: [...DEFAULT_CONFIG_BASE.addons],
57
+ examples: [...DEFAULT_CONFIG_BASE.examples]
58
+ };
59
+ }
60
+ const DEFAULT_CONFIG = getDefaultConfig();
53
61
  const dependencyVersionMap = {
54
62
  "better-auth": "^1.3.4",
55
63
  "@better-auth/expo": "^1.3.4",
@@ -137,7 +145,8 @@ const ADDON_COMPATIBILITY = {
137
145
  "react-router",
138
146
  "nuxt",
139
147
  "svelte",
140
- "solid"
148
+ "solid",
149
+ "next"
141
150
  ],
142
151
  biome: [],
143
152
  husky: [],
@@ -565,6 +574,20 @@ function validateWebDeployRequiresWebFrontend(webDeploy, hasWebFrontendFlag) {
565
574
  function validateServerDeployRequiresBackend(serverDeploy, backend) {
566
575
  if (serverDeploy && serverDeploy !== "none" && (!backend || backend === "none")) exitWithError("'--server-deploy' requires a backend. Please select a backend or set '--server-deploy none'.");
567
576
  }
577
+ function validateAddonsAgainstFrontends(addons = [], frontends = []) {
578
+ for (const addon of addons) {
579
+ if (addon === "none") continue;
580
+ const { isCompatible, reason } = validateAddonCompatibility(addon, frontends);
581
+ if (!isCompatible) exitWithError(`Incompatible addon/frontend combination: ${reason}`);
582
+ }
583
+ }
584
+ function validateExamplesCompatibility(examples, backend, database, frontend) {
585
+ const examplesArr = examples ?? [];
586
+ if (examplesArr.length === 0 || examplesArr.includes("none")) return;
587
+ if (examplesArr.includes("todo") && backend !== "convex" && backend !== "none" && database === "none") exitWithError("The 'todo' example requires a database if a backend (other than Convex) is present. Cannot use --examples todo when database is 'none' and a backend is selected.");
588
+ if (examplesArr.includes("ai") && backend === "elysia") exitWithError("The 'ai' example is not compatible with the Elysia backend.");
589
+ if (examplesArr.includes("ai") && (frontend ?? []).includes("solid")) exitWithError("The 'ai' example is not compatible with the Solid frontend.");
590
+ }
568
591
 
569
592
  //#endregion
570
593
  //#region src/prompts/api.ts
@@ -1298,9 +1321,9 @@ async function getProjectName(initialName) {
1298
1321
 
1299
1322
  //#endregion
1300
1323
  //#region src/utils/get-latest-cli-version.ts
1301
- const getLatestCLIVersion = async () => {
1324
+ const getLatestCLIVersion = () => {
1302
1325
  const packageJsonPath = path.join(PKG_ROOT, "package.json");
1303
- const packageJsonContent = await fs.readJSON(packageJsonPath);
1326
+ const packageJsonContent = fs.readJSONSync(packageJsonPath);
1304
1327
  return packageJsonContent.version ?? "1.0.0";
1305
1328
  };
1306
1329
 
@@ -1562,7 +1585,7 @@ const renderTitle = () => {
1562
1585
  };
1563
1586
 
1564
1587
  //#endregion
1565
- //#region src/validation.ts
1588
+ //#region src/utils/config-processing.ts
1566
1589
  function processArrayOption(options) {
1567
1590
  if (!options || options.length === 0) return [];
1568
1591
  if (options.includes("none")) return [];
@@ -1573,22 +1596,10 @@ function deriveProjectName(projectName, projectDirectory) {
1573
1596
  if (projectDirectory) return path.basename(path.resolve(process.cwd(), projectDirectory));
1574
1597
  return "";
1575
1598
  }
1576
- function validateProjectName(name) {
1577
- const result = ProjectNameSchema.safeParse(name);
1578
- if (!result.success) exitWithError(`Invalid project name: ${result.error.issues[0]?.message || "Invalid project name"}`);
1579
- }
1580
- function processAndValidateFlags(options, providedFlags, projectName) {
1599
+ function processFlags(options, projectName) {
1581
1600
  const config = {};
1582
- if (options.api) {
1583
- config.api = options.api;
1584
- if (options.api === "none") {
1585
- if (options.examples && !(options.examples.length === 1 && options.examples[0] === "none") && options.backend !== "convex") exitWithError("Cannot use '--examples' when '--api' is set to 'none'. Please remove the --examples flag or choose an API type.");
1586
- }
1587
- }
1601
+ if (options.api) config.api = options.api;
1588
1602
  if (options.backend) config.backend = options.backend;
1589
- if (providedFlags.has("backend") && config.backend && config.backend !== "convex" && config.backend !== "none") {
1590
- if (providedFlags.has("runtime") && options.runtime === "none") exitWithError(`'--runtime none' is only supported with '--backend convex' or '--backend none'. Please choose 'bun', 'node', or remove the --runtime flag.`);
1591
- }
1592
1603
  if (options.database) config.database = options.database;
1593
1604
  if (options.orm) config.orm = options.orm;
1594
1605
  if (options.auth !== void 0) config.auth = options.auth;
@@ -1600,70 +1611,180 @@ function processAndValidateFlags(options, providedFlags, projectName) {
1600
1611
  if (options.webDeploy) config.webDeploy = options.webDeploy;
1601
1612
  if (options.serverDeploy) config.serverDeploy = options.serverDeploy;
1602
1613
  const derivedName = deriveProjectName(projectName, options.projectDirectory);
1603
- if (derivedName) {
1604
- const nameToValidate = projectName ? path.basename(projectName) : derivedName;
1605
- validateProjectName(nameToValidate);
1606
- config.projectName = projectName || derivedName;
1607
- }
1608
- if (options.frontend && options.frontend.length > 0) if (options.frontend.includes("none")) {
1609
- if (options.frontend.length > 1) exitWithError(`Cannot combine 'none' with other frontend options.`);
1610
- config.frontend = [];
1611
- } else {
1612
- const validOptions = processArrayOption(options.frontend);
1613
- ensureSingleWebAndNative(validOptions);
1614
- config.frontend = validOptions;
1615
- }
1616
- if (providedFlags.has("api") && providedFlags.has("frontend") && config.api && config.frontend && config.frontend.length > 0) validateApiFrontendCompatibility(config.api, config.frontend);
1617
- if (options.addons && options.addons.length > 0) if (options.addons.includes("none")) {
1618
- if (options.addons.length > 1) exitWithError(`Cannot combine 'none' with other addons.`);
1619
- config.addons = [];
1620
- } else config.addons = processArrayOption(options.addons);
1621
- if (options.examples && options.examples.length > 0) if (options.examples.includes("none")) {
1622
- if (options.examples.length > 1) exitWithError("Cannot combine 'none' with other examples.");
1623
- config.examples = [];
1624
- } else {
1625
- config.examples = processArrayOption(options.examples);
1626
- if (options.examples.includes("none") && config.backend !== "convex") config.examples = [];
1614
+ if (derivedName) config.projectName = projectName || derivedName;
1615
+ if (options.frontend && options.frontend.length > 0) config.frontend = processArrayOption(options.frontend);
1616
+ if (options.addons && options.addons.length > 0) config.addons = processArrayOption(options.addons);
1617
+ if (options.examples && options.examples.length > 0) config.examples = processArrayOption(options.examples);
1618
+ return config;
1619
+ }
1620
+ function getProvidedFlags(options) {
1621
+ return new Set(Object.keys(options).filter((key) => options[key] !== void 0));
1622
+ }
1623
+ function validateNoneExclusivity(options, optionName) {
1624
+ if (!options || options.length === 0) return;
1625
+ if (options.includes("none") && options.length > 1) throw new Error(`Cannot combine 'none' with other ${optionName}.`);
1626
+ }
1627
+ function validateArrayOptions(options) {
1628
+ validateNoneExclusivity(options.frontend, "frontend options");
1629
+ validateNoneExclusivity(options.addons, "addons");
1630
+ validateNoneExclusivity(options.examples, "examples");
1631
+ }
1632
+
1633
+ //#endregion
1634
+ //#region src/utils/config-validation.ts
1635
+ function validateDatabaseOrmAuth(cfg, flags) {
1636
+ const db = cfg.database;
1637
+ const orm = cfg.orm;
1638
+ const has = (k) => flags ? flags.has(k) : true;
1639
+ if (has("orm") && has("database") && orm === "mongoose" && db !== "mongodb") exitWithError("Mongoose ORM requires MongoDB database. Please use '--database mongodb' or choose a different ORM.");
1640
+ if (has("orm") && has("database") && orm === "drizzle" && db === "mongodb") exitWithError("Drizzle ORM does not support MongoDB. Please use '--orm mongoose' or '--orm prisma' or choose a different database.");
1641
+ if (has("database") && has("orm") && db === "mongodb" && orm && orm !== "mongoose" && orm !== "prisma" && orm !== "none") exitWithError("MongoDB database requires Mongoose or Prisma ORM. Please use '--orm mongoose' or '--orm prisma' or choose a different database.");
1642
+ if (has("database") && has("orm") && db && db !== "none" && orm === "none") exitWithError("Database selection requires an ORM. Please choose '--orm drizzle', '--orm prisma', or '--orm mongoose'.");
1643
+ if (has("orm") && has("database") && orm && orm !== "none" && db === "none") exitWithError("ORM selection requires a database. Please choose a database or set '--orm none'.");
1644
+ if (has("auth") && has("database") && cfg.auth && db === "none") exitWithError("Authentication requires a database. Please choose a database or set '--no-auth'.");
1645
+ if (cfg.auth && db === "none") exitWithError("Authentication requires a database. Please choose a database or set '--no-auth'.");
1646
+ if (orm && orm !== "none" && db === "none") exitWithError("ORM selection requires a database. Please choose a database or set '--orm none'.");
1647
+ }
1648
+ function validateDatabaseSetup(config, providedFlags) {
1649
+ const { dbSetup, database, runtime } = config;
1650
+ if (providedFlags.has("dbSetup") && providedFlags.has("database") && dbSetup && dbSetup !== "none" && database === "none") exitWithError("Database setup requires a database. Please choose a database or set '--db-setup none'.");
1651
+ const setupValidations = {
1652
+ turso: {
1653
+ database: "sqlite",
1654
+ errorMessage: "Turso setup requires SQLite database. Please use '--database sqlite' or choose a different setup."
1655
+ },
1656
+ neon: {
1657
+ database: "postgres",
1658
+ errorMessage: "Neon setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup."
1659
+ },
1660
+ "prisma-postgres": {
1661
+ database: "postgres",
1662
+ errorMessage: "Prisma PostgreSQL setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup."
1663
+ },
1664
+ "mongodb-atlas": {
1665
+ database: "mongodb",
1666
+ errorMessage: "MongoDB Atlas setup requires MongoDB database. Please use '--database mongodb' or choose a different setup."
1667
+ },
1668
+ supabase: {
1669
+ database: "postgres",
1670
+ errorMessage: "Supabase setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup."
1671
+ },
1672
+ d1: {
1673
+ database: "sqlite",
1674
+ runtime: "workers",
1675
+ errorMessage: "Cloudflare D1 setup requires SQLite database and Cloudflare Workers runtime."
1676
+ },
1677
+ docker: { errorMessage: "Docker setup is not compatible with SQLite database or Cloudflare Workers runtime." },
1678
+ none: { errorMessage: "" }
1679
+ };
1680
+ if (dbSetup && dbSetup !== "none") {
1681
+ const validation = setupValidations[dbSetup];
1682
+ if (validation.database && database !== validation.database) exitWithError(validation.errorMessage);
1683
+ if (validation.runtime && runtime !== validation.runtime) exitWithError(validation.errorMessage);
1684
+ if (dbSetup === "docker") {
1685
+ if (database === "sqlite") exitWithError("Docker setup is not compatible with SQLite database. SQLite is file-based and doesn't require Docker. Please use '--database postgres', '--database mysql', '--database mongodb', or choose a different setup.");
1686
+ if (runtime === "workers") exitWithError("Docker setup is not compatible with Cloudflare Workers runtime. Workers runtime uses serverless databases (D1) and doesn't support local Docker containers. Please use '--db-setup d1' for SQLite or choose a different runtime.");
1687
+ }
1627
1688
  }
1628
- if (config.backend === "convex" || config.backend === "none") {
1629
- const incompatibleFlags = incompatibleFlagsForBackend(config.backend, providedFlags, options);
1630
- if (incompatibleFlags.length > 0) exitWithError(`The following flags are incompatible with '--backend ${config.backend}': ${incompatibleFlags.join(", ")}. Please remove them.`);
1631
- if (config.backend === "convex" && providedFlags.has("frontend") && options.frontend) {
1689
+ }
1690
+ function validateBackendConstraints(config, providedFlags, options) {
1691
+ const { backend } = config;
1692
+ if (providedFlags.has("backend") && backend && backend !== "convex" && backend !== "none") {
1693
+ if (providedFlags.has("runtime") && options.runtime === "none") exitWithError("'--runtime none' is only supported with '--backend convex' or '--backend none'. Please choose 'bun', 'node', or remove the --runtime flag.");
1694
+ }
1695
+ if (backend === "convex" || backend === "none") {
1696
+ const incompatibleFlags = incompatibleFlagsForBackend(backend, providedFlags, options);
1697
+ if (incompatibleFlags.length > 0) exitWithError(`The following flags are incompatible with '--backend ${backend}': ${incompatibleFlags.join(", ")}. Please remove them.`);
1698
+ if (backend === "convex" && providedFlags.has("frontend") && options.frontend) {
1632
1699
  const incompatibleFrontends = options.frontend.filter((f) => f === "solid");
1633
1700
  if (incompatibleFrontends.length > 0) exitWithError(`The following frontends are not compatible with '--backend convex': ${incompatibleFrontends.join(", ")}. Please choose a different frontend or backend.`);
1634
1701
  }
1635
1702
  coerceBackendPresets(config);
1636
1703
  }
1637
- if (providedFlags.has("orm") && providedFlags.has("database") && config.orm === "mongoose" && config.database !== "mongodb") exitWithError("Mongoose ORM requires MongoDB database. Please use '--database mongodb' or choose a different ORM.");
1638
- if (providedFlags.has("database") && providedFlags.has("orm") && config.database === "mongodb" && config.orm && config.orm !== "mongoose" && config.orm !== "prisma") exitWithError("MongoDB database requires Mongoose or Prisma ORM. Please use '--orm mongoose' or '--orm prisma' or choose a different database.");
1639
- if (providedFlags.has("orm") && providedFlags.has("database") && config.orm === "drizzle" && config.database === "mongodb") exitWithError("Drizzle ORM does not support MongoDB. Please use '--orm mongoose' or '--orm prisma' or choose a different database.");
1640
- if (providedFlags.has("database") && providedFlags.has("orm") && config.database && config.database !== "none" && config.orm === "none") exitWithError("Database selection requires an ORM. Please choose '--orm drizzle', '--orm prisma', or '--orm mongoose'.");
1641
- if (providedFlags.has("orm") && providedFlags.has("database") && config.orm && config.orm !== "none" && config.database === "none") exitWithError("ORM selection requires a database. Please choose a database or set '--orm none'.");
1642
- if (providedFlags.has("auth") && providedFlags.has("database") && config.auth && config.database === "none") exitWithError("Authentication requires a database. Please choose a database or set '--no-auth'.");
1643
- if (providedFlags.has("dbSetup") && providedFlags.has("database") && config.dbSetup && config.dbSetup !== "none" && config.database === "none") exitWithError("Database setup requires a database. Please choose a database or set '--db-setup none'.");
1644
- if (providedFlags.has("dbSetup") && (config.database ? providedFlags.has("database") : true) && config.dbSetup === "turso" && config.database !== "sqlite") exitWithError("Turso setup requires SQLite database. Please use '--database sqlite' or choose a different setup.");
1645
- if (providedFlags.has("dbSetup") && (config.database ? providedFlags.has("database") : true) && config.dbSetup === "neon" && config.database !== "postgres") exitWithError("Neon setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup.");
1646
- if (providedFlags.has("dbSetup") && (config.database ? providedFlags.has("database") : true) && config.dbSetup === "prisma-postgres" && config.database !== "postgres") exitWithError("Prisma PostgreSQL setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup.");
1647
- if (providedFlags.has("dbSetup") && (config.database ? providedFlags.has("database") : true) && config.dbSetup === "mongodb-atlas" && config.database !== "mongodb") exitWithError("MongoDB Atlas setup requires MongoDB database. Please use '--database mongodb' or choose a different setup.");
1648
- if (providedFlags.has("dbSetup") && (config.database ? providedFlags.has("database") : true) && config.dbSetup === "supabase" && config.database !== "postgres") exitWithError("Supabase setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup.");
1649
- if (config.dbSetup === "d1") {
1650
- if (providedFlags.has("dbSetup") && providedFlags.has("database") || providedFlags.has("dbSetup") && !config.database) {
1651
- if (config.database !== "sqlite") exitWithError("Cloudflare D1 setup requires SQLite database. Please use '--database sqlite' or choose a different setup.");
1652
- }
1653
- if (providedFlags.has("dbSetup") && providedFlags.has("runtime") || providedFlags.has("dbSetup") && !config.runtime) {
1654
- if (config.runtime !== "workers") exitWithError("Cloudflare D1 setup requires the Cloudflare Workers runtime. Please use '--runtime workers' or choose a different setup.");
1655
- }
1704
+ }
1705
+ function validateFrontendConstraints(config, providedFlags) {
1706
+ const { frontend } = config;
1707
+ if (frontend && frontend.length > 0) {
1708
+ ensureSingleWebAndNative(frontend);
1709
+ if (providedFlags.has("api") && providedFlags.has("frontend") && config.api) validateApiFrontendCompatibility(config.api, frontend);
1710
+ const hasWebFrontendFlag = frontend.some((f) => isWebFrontend(f));
1711
+ validateWebDeployRequiresWebFrontend(config.webDeploy, hasWebFrontendFlag);
1656
1712
  }
1657
- if (providedFlags.has("dbSetup") && providedFlags.has("database") && config.dbSetup === "docker" && config.database === "sqlite") exitWithError("Docker setup is not compatible with SQLite database. SQLite is file-based and doesn't require Docker. Please use '--database postgres', '--database mysql', '--database mongodb', or choose a different setup.");
1658
- if (providedFlags.has("dbSetup") && providedFlags.has("runtime") && config.dbSetup === "docker" && config.runtime === "workers") exitWithError("Docker setup is not compatible with Cloudflare Workers runtime. Workers runtime uses serverless databases (D1) and doesn't support local Docker containers. Please use '--db-setup d1' for SQLite or choose a different runtime.");
1659
- validateWorkersCompatibility(providedFlags, options, config);
1660
- const hasWebFrontendFlag = (config.frontend ?? []).some((f) => isWebFrontend(f));
1661
- validateWebDeployRequiresWebFrontend(config.webDeploy, hasWebFrontendFlag);
1713
+ }
1714
+ function validateApiConstraints(config, options) {
1715
+ if (config.api === "none") {
1716
+ if (options.examples && !(options.examples.length === 1 && options.examples[0] === "none") && options.backend !== "convex") exitWithError("Cannot use '--examples' when '--api' is set to 'none'. Please remove the --examples flag or choose an API type.");
1717
+ }
1718
+ }
1719
+ function validateFullConfig(config, providedFlags, options) {
1720
+ validateDatabaseOrmAuth(config, providedFlags);
1721
+ validateDatabaseSetup(config, providedFlags);
1722
+ validateBackendConstraints(config, providedFlags, options);
1723
+ validateFrontendConstraints(config, providedFlags);
1724
+ validateApiConstraints(config, options);
1662
1725
  validateServerDeployRequiresBackend(config.serverDeploy, config.backend);
1726
+ validateWorkersCompatibility(providedFlags, options, config);
1727
+ if (config.addons && config.addons.length > 0) {
1728
+ validateAddonsAgainstFrontends(config.addons, config.frontend);
1729
+ config.addons = [...new Set(config.addons)];
1730
+ }
1731
+ validateExamplesCompatibility(config.examples ?? [], config.backend, config.database, config.frontend ?? []);
1732
+ }
1733
+ function validateConfigForProgrammaticUse(config) {
1734
+ try {
1735
+ validateDatabaseOrmAuth(config);
1736
+ if (config.frontend && config.frontend.length > 0) ensureSingleWebAndNative(config.frontend);
1737
+ validateApiFrontendCompatibility(config.api, config.frontend);
1738
+ if (config.addons && config.addons.length > 0) validateAddonsAgainstFrontends(config.addons, config.frontend);
1739
+ validateExamplesCompatibility(config.examples ?? [], config.backend, config.database, config.frontend ?? []);
1740
+ } catch (error) {
1741
+ if (error instanceof Error) throw error;
1742
+ throw new Error(String(error));
1743
+ }
1744
+ }
1745
+
1746
+ //#endregion
1747
+ //#region src/utils/project-name-validation.ts
1748
+ function validateProjectName(name) {
1749
+ const result = ProjectNameSchema.safeParse(name);
1750
+ if (!result.success) exitWithError(`Invalid project name: ${result.error.issues[0]?.message || "Invalid project name"}`);
1751
+ }
1752
+ function validateProjectNameThrow(name) {
1753
+ const result = ProjectNameSchema.safeParse(name);
1754
+ if (!result.success) throw new Error(`Invalid project name: ${result.error.issues[0]?.message}`);
1755
+ }
1756
+ function extractAndValidateProjectName(projectName, projectDirectory, throwOnError = false) {
1757
+ const derivedName = projectName || (projectDirectory ? path.basename(path.resolve(process.cwd(), projectDirectory)) : "");
1758
+ if (!derivedName) return "";
1759
+ const nameToValidate = projectName ? path.basename(projectName) : derivedName;
1760
+ if (throwOnError) validateProjectNameThrow(nameToValidate);
1761
+ else validateProjectName(nameToValidate);
1762
+ return projectName || derivedName;
1763
+ }
1764
+
1765
+ //#endregion
1766
+ //#region src/validation.ts
1767
+ function processAndValidateFlags(options, providedFlags, projectName) {
1768
+ try {
1769
+ validateArrayOptions(options);
1770
+ } catch (error) {
1771
+ exitWithError(error instanceof Error ? error.message : String(error));
1772
+ }
1773
+ const config = processFlags(options, projectName);
1774
+ const validatedProjectName = extractAndValidateProjectName(projectName, options.projectDirectory, false);
1775
+ if (validatedProjectName) config.projectName = validatedProjectName;
1776
+ validateFullConfig(config, providedFlags, options);
1663
1777
  return config;
1664
1778
  }
1665
- function getProvidedFlags(options) {
1666
- return new Set(Object.keys(options).filter((key) => options[key] !== void 0));
1779
+ function processProvidedFlagsWithoutValidation(options, projectName) {
1780
+ const config = processFlags(options, projectName);
1781
+ const validatedProjectName = extractAndValidateProjectName(projectName, options.projectDirectory, true);
1782
+ if (validatedProjectName) config.projectName = validatedProjectName;
1783
+ return config;
1784
+ }
1785
+ function validateConfigCompatibility(config, providedFlags, options) {
1786
+ if (options && providedFlags) validateFullConfig(config, providedFlags, options);
1787
+ else validateConfigForProgrammaticUse(config);
1667
1788
  }
1668
1789
 
1669
1790
  //#endregion
@@ -2216,20 +2337,16 @@ async function setupDeploymentTemplates(projectDir, context) {
2216
2337
  }
2217
2338
  } else {
2218
2339
  if (context.webDeploy === "alchemy") {
2340
+ const alchemyTemplateSrc = path.join(PKG_ROOT, "templates/deploy/alchemy");
2219
2341
  const webAppDir = path.join(projectDir, "apps/web");
2220
- if (await fs.pathExists(webAppDir)) {
2221
- const alchemyTemplateSrc = path.join(PKG_ROOT, "templates/deploy/alchemy");
2222
- if (await fs.pathExists(alchemyTemplateSrc)) await processAndCopyFiles("**/*", alchemyTemplateSrc, webAppDir, context);
2223
- }
2342
+ if (await fs.pathExists(alchemyTemplateSrc) && await fs.pathExists(webAppDir)) await processAndCopyFiles("alchemy.run.ts.hbs", alchemyTemplateSrc, webAppDir, context);
2224
2343
  }
2225
2344
  if (context.serverDeploy === "alchemy") {
2345
+ const alchemyTemplateSrc = path.join(PKG_ROOT, "templates/deploy/alchemy");
2226
2346
  const serverAppDir = path.join(projectDir, "apps/server");
2227
- if (await fs.pathExists(serverAppDir)) {
2228
- const alchemyTemplateSrc = path.join(PKG_ROOT, "templates/deploy/alchemy");
2229
- if (await fs.pathExists(alchemyTemplateSrc)) {
2230
- await processAndCopyFiles("alchemy.run.ts.hbs", alchemyTemplateSrc, serverAppDir, context);
2231
- await processAndCopyFiles("env.d.ts.hbs", alchemyTemplateSrc, serverAppDir, context);
2232
- }
2347
+ if (await fs.pathExists(alchemyTemplateSrc) && await fs.pathExists(serverAppDir)) {
2348
+ await processAndCopyFiles("alchemy.run.ts.hbs", alchemyTemplateSrc, serverAppDir, context);
2349
+ await processAndCopyFiles("env.d.ts.hbs", alchemyTemplateSrc, serverAppDir, context);
2233
2350
  }
2234
2351
  }
2235
2352
  }
@@ -2501,7 +2618,7 @@ async function setupUltracite(config, hasHusky) {
2501
2618
  ];
2502
2619
  if (editors.length > 0) ultraciteArgs.push("--editors", ...editors);
2503
2620
  if (rules.length > 0) ultraciteArgs.push("--rules", ...rules);
2504
- if (hasHusky) ultraciteArgs.push("--features", "husky", "lint-staged");
2621
+ if (hasHusky) ultraciteArgs.push("--integrations", "husky", "lint-staged");
2505
2622
  const ultraciteArgsString = ultraciteArgs.join(" ");
2506
2623
  const commandWithArgs = `ultracite@latest ${ultraciteArgsString} --skip-install`;
2507
2624
  const ultraciteInitCommand = getPackageExecutionCommand(packageManager, commandWithArgs);
@@ -3342,15 +3459,15 @@ async function setupCombinedAlchemyDeploy(projectDir, packageManager, config) {
3342
3459
  //#region src/helpers/deployment/workers/workers-next-setup.ts
3343
3460
  async function setupNextWorkersDeploy(projectDir, _packageManager) {
3344
3461
  const webAppDir = path.join(projectDir, "apps/web");
3345
- if (!await fs$1.pathExists(webAppDir)) return;
3462
+ if (!await fs.pathExists(webAppDir)) return;
3346
3463
  await addPackageDependency({
3347
3464
  dependencies: ["@opennextjs/cloudflare"],
3348
3465
  devDependencies: ["wrangler"],
3349
3466
  projectDir: webAppDir
3350
3467
  });
3351
3468
  const packageJsonPath = path.join(webAppDir, "package.json");
3352
- if (await fs$1.pathExists(packageJsonPath)) {
3353
- const pkg = await fs$1.readJson(packageJsonPath);
3469
+ if (await fs.pathExists(packageJsonPath)) {
3470
+ const pkg = await fs.readJson(packageJsonPath);
3354
3471
  pkg.scripts = {
3355
3472
  ...pkg.scripts,
3356
3473
  preview: "opennextjs-cloudflare build && opennextjs-cloudflare preview",
@@ -3358,7 +3475,7 @@ async function setupNextWorkersDeploy(projectDir, _packageManager) {
3358
3475
  upload: "opennextjs-cloudflare build && opennextjs-cloudflare upload",
3359
3476
  "cf-typegen": "wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts"
3360
3477
  };
3361
- await fs$1.writeJson(packageJsonPath, pkg, { spaces: 2 });
3478
+ await fs.writeJson(packageJsonPath, pkg, { spaces: 2 });
3362
3479
  }
3363
3480
  }
3364
3481
 
@@ -5471,13 +5588,14 @@ async function displayPostInstallInstructions(config) {
5471
5588
  const runCmd = packageManager === "npm" ? "npm run" : packageManager;
5472
5589
  const cdCmd = `cd ${relativePath}`;
5473
5590
  const hasHuskyOrBiome = addons?.includes("husky") || addons?.includes("biome");
5474
- const databaseInstructions = !isConvex && database !== "none" ? await getDatabaseInstructions(database, orm, runCmd, runtime, dbSetup) : "";
5591
+ const databaseInstructions = !isConvex && database !== "none" ? await getDatabaseInstructions(database, orm, runCmd, runtime, dbSetup, serverDeploy) : "";
5475
5592
  const tauriInstructions = addons?.includes("tauri") ? getTauriInstructions(runCmd) : "";
5476
5593
  const lintingInstructions = hasHuskyOrBiome ? getLintingInstructions(runCmd) : "";
5477
5594
  const nativeInstructions = frontend?.includes("native-nativewind") || frontend?.includes("native-unistyles") ? getNativeInstructions(isConvex) : "";
5478
5595
  const pwaInstructions = addons?.includes("pwa") && frontend?.includes("react-router") ? getPwaInstructions() : "";
5479
5596
  const starlightInstructions = addons?.includes("starlight") ? getStarlightInstructions(runCmd) : "";
5480
- const workersDeployInstructions = webDeploy === "wrangler" ? getWorkersDeployInstructions(runCmd) : "";
5597
+ const wranglerDeployInstructions = getWranglerDeployInstructions(runCmd, webDeploy, serverDeploy);
5598
+ const alchemyDeployInstructions = getAlchemyDeployInstructions(runCmd, webDeploy, serverDeploy);
5481
5599
  const hasWeb = frontend?.some((f) => [
5482
5600
  "tanstack-router",
5483
5601
  "react-router",
@@ -5506,8 +5624,8 @@ async function displayPostInstallInstructions(config) {
5506
5624
  if (runtime === "workers") {
5507
5625
  if (dbSetup === "d1") output += `${pc.yellow("IMPORTANT:")} Complete D1 database setup first\n (see Database commands below)\n`;
5508
5626
  output += `${pc.cyan(`${stepCounter++}.`)} ${runCmd} dev\n`;
5509
- if (serverDeploy === "wrangler") output += `${pc.cyan(`${stepCounter++}.`)} cd apps/server && ${runCmd} cf-typegen\n\n`;
5510
- } else output += "\n";
5627
+ if (serverDeploy === "wrangler") output += `${pc.cyan(`${stepCounter++}.`)} cd apps/server && ${runCmd} cf-typegen\n`;
5628
+ }
5511
5629
  }
5512
5630
  output += `${pc.bold("Your project will be available at:")}\n`;
5513
5631
  if (hasWeb) output += `${pc.cyan("•")} Frontend: http://localhost:${webPort}\n`;
@@ -5520,7 +5638,8 @@ async function displayPostInstallInstructions(config) {
5520
5638
  if (tauriInstructions) output += `\n${tauriInstructions.trim()}\n`;
5521
5639
  if (lintingInstructions) output += `\n${lintingInstructions.trim()}\n`;
5522
5640
  if (pwaInstructions) output += `\n${pwaInstructions.trim()}\n`;
5523
- if (workersDeployInstructions) output += `\n${workersDeployInstructions.trim()}\n`;
5641
+ if (wranglerDeployInstructions) output += `\n${wranglerDeployInstructions.trim()}\n`;
5642
+ if (alchemyDeployInstructions) output += `\n${alchemyDeployInstructions.trim()}\n`;
5524
5643
  if (starlightInstructions) output += `\n${starlightInstructions.trim()}\n`;
5525
5644
  if (noOrmWarning) output += `\n${noOrmWarning.trim()}\n`;
5526
5645
  if (bunWebNativeWarning) output += `\n${bunWebNativeWarning.trim()}\n`;
@@ -5541,7 +5660,7 @@ function getNativeInstructions(isConvex) {
5541
5660
  function getLintingInstructions(runCmd) {
5542
5661
  return `${pc.bold("Linting and formatting:")}\n${pc.cyan("•")} Format and lint fix: ${`${runCmd} check`}\n`;
5543
5662
  }
5544
- async function getDatabaseInstructions(database, orm, runCmd, runtime, dbSetup) {
5663
+ async function getDatabaseInstructions(database, orm, runCmd, _runtime, dbSetup, serverDeploy) {
5545
5664
  const instructions = [];
5546
5665
  if (dbSetup === "docker") {
5547
5666
  const dockerStatus = await getDockerStatus(database);
@@ -5550,7 +5669,7 @@ async function getDatabaseInstructions(database, orm, runCmd, runtime, dbSetup)
5550
5669
  instructions.push("");
5551
5670
  }
5552
5671
  }
5553
- if (runtime === "workers" && dbSetup === "d1") {
5672
+ if (serverDeploy === "wrangler" && dbSetup === "d1") {
5554
5673
  const packageManager = runCmd === "npm run" ? "npm" : runCmd || "npm";
5555
5674
  instructions.push(`${pc.cyan("1.")} Login to Cloudflare: ${pc.white(`${packageManager} wrangler login`)}`);
5556
5675
  instructions.push(`${pc.cyan("2.")} Create D1 database: ${pc.white(`${packageManager} wrangler d1 create your-database-name`)}`);
@@ -5558,8 +5677,8 @@ async function getDatabaseInstructions(database, orm, runCmd, runtime, dbSetup)
5558
5677
  instructions.push(`${pc.cyan("4.")} Generate migrations: ${pc.white(`cd apps/server && ${packageManager} db:generate`)}`);
5559
5678
  instructions.push(`${pc.cyan("5.")} Apply migrations locally: ${pc.white(`${packageManager} wrangler d1 migrations apply YOUR_DB_NAME --local`)}`);
5560
5679
  instructions.push(`${pc.cyan("6.")} Apply migrations to production: ${pc.white(`${packageManager} wrangler d1 migrations apply YOUR_DB_NAME`)}`);
5561
- instructions.push("");
5562
5680
  }
5681
+ if (dbSetup === "d1" && serverDeploy === "alchemy") instructions.push(`${pc.cyan("•")} Generate migrations: ${pc.white(`${runCmd} db:generate`)}`);
5563
5682
  if (orm === "prisma") {
5564
5683
  if (dbSetup === "turso") instructions.push(`${pc.yellow("NOTE:")} Turso support with Prisma is in Early Access and requires\n additional setup. Learn more at:\n https://www.prisma.io/docs/orm/overview/databases/turso`);
5565
5684
  if (database === "mongodb" && dbSetup === "docker") instructions.push(`${pc.yellow("WARNING:")} Prisma + MongoDB + Docker combination\n may not work.`);
@@ -5568,7 +5687,7 @@ async function getDatabaseInstructions(database, orm, runCmd, runtime, dbSetup)
5568
5687
  instructions.push(`${pc.cyan("•")} Database UI: ${`${runCmd} db:studio`}`);
5569
5688
  } else if (orm === "drizzle") {
5570
5689
  if (dbSetup === "docker") instructions.push(`${pc.cyan("•")} Start docker container: ${`${runCmd} db:start`}`);
5571
- instructions.push(`${pc.cyan("•")} Apply schema: ${`${runCmd} db:push`}`);
5690
+ if (dbSetup !== "d1") instructions.push(`${pc.cyan("•")} Apply schema: ${`${runCmd} db:push`}`);
5572
5691
  instructions.push(`${pc.cyan("•")} Database UI: ${`${runCmd} db:studio`}`);
5573
5692
  if (database === "sqlite" && dbSetup !== "d1") instructions.push(`${pc.cyan("•")} Start local DB (if needed): ${`cd apps/server && ${runCmd} db:local`}`);
5574
5693
  } else if (orm === "mongoose") {
@@ -5591,8 +5710,18 @@ function getNoOrmWarning() {
5591
5710
  function getBunWebNativeWarning() {
5592
5711
  return `\n${pc.yellow("WARNING:")} 'bun' might cause issues with web + native apps in a monorepo.\n Use 'pnpm' if problems arise.`;
5593
5712
  }
5594
- function getWorkersDeployInstructions(runCmd) {
5595
- return `\n${pc.bold("Deploy frontend to Cloudflare Workers:")}\n${pc.cyan("•")} Deploy: ${`cd apps/web && ${runCmd} run deploy`}`;
5713
+ function getWranglerDeployInstructions(runCmd, webDeploy, serverDeploy) {
5714
+ const instructions = [];
5715
+ if (webDeploy === "wrangler") instructions.push(`${pc.bold("Deploy web to Cloudflare Workers:")}\n${pc.cyan("•")} Deploy: ${`cd apps/web && ${runCmd} run deploy`}`);
5716
+ if (serverDeploy === "wrangler") instructions.push(`${pc.bold("Deploy server to Cloudflare Workers:")}\n${pc.cyan("•")} Deploy: ${`cd apps/server && ${runCmd} run deploy`}`);
5717
+ return instructions.length ? `\n${instructions.join("\n")}` : "";
5718
+ }
5719
+ function getAlchemyDeployInstructions(runCmd, webDeploy, serverDeploy) {
5720
+ const instructions = [];
5721
+ if (webDeploy === "alchemy" && serverDeploy !== "alchemy") instructions.push(`${pc.bold("Deploy web to Alchemy:")}\n${pc.cyan("•")} Deploy: ${`cd apps/web && ${runCmd} deploy`}`);
5722
+ else if (serverDeploy === "alchemy" && webDeploy !== "alchemy") instructions.push(`${pc.bold("Deploy server to Alchemy:")}\n${pc.cyan("•")} Deploy: ${`cd apps/server && ${runCmd} deploy`}`);
5723
+ else if (webDeploy === "alchemy" && serverDeploy === "alchemy") instructions.push(`${pc.bold("Deploy to Alchemy:")}\n${pc.cyan("•")} Deploy: ${`${runCmd} deploy`}`);
5724
+ return instructions.length ? `\n${instructions.join("\n")}` : "";
5596
5725
  }
5597
5726
 
5598
5727
  //#endregion
@@ -5805,6 +5934,7 @@ async function createProject(options) {
5805
5934
  if (!isConvex && options.auth) await setupAuth(options);
5806
5935
  await handleExtras(projectDir, options);
5807
5936
  await setupWebDeploy(options);
5937
+ await setupServerDeploy(options);
5808
5938
  await setupEnvironmentVariables(options);
5809
5939
  await updatePackageConfigurations(projectDir, options);
5810
5940
  await createReadme(projectDir, options);
@@ -5844,10 +5974,11 @@ async function createProjectHandler(input) {
5844
5974
  let currentPathInput;
5845
5975
  if (input.yes && input.projectName) currentPathInput = input.projectName;
5846
5976
  else if (input.yes) {
5847
- let defaultName = DEFAULT_CONFIG.relativePath;
5977
+ const defaultConfig = getDefaultConfig();
5978
+ let defaultName = defaultConfig.relativePath;
5848
5979
  let counter = 1;
5849
5980
  while (await fs.pathExists(path.resolve(process.cwd(), defaultName)) && (await fs.readdir(path.resolve(process.cwd(), defaultName))).length > 0) {
5850
- defaultName = `${DEFAULT_CONFIG.projectName}-${counter}`;
5981
+ defaultName = `${defaultConfig.projectName}-${counter}`;
5851
5982
  counter++;
5852
5983
  }
5853
5984
  currentPathInput = defaultName;
@@ -5902,28 +6033,33 @@ async function createProjectHandler(input) {
5902
6033
  projectDirectory: input.projectName
5903
6034
  };
5904
6035
  const providedFlags = getProvidedFlags(cliInput);
5905
- const flagConfig = processAndValidateFlags(cliInput, providedFlags, finalBaseName);
5906
- const { projectName: _projectNameFromFlags,...otherFlags } = flagConfig;
5907
- if (!input.yes && Object.keys(otherFlags).length > 0) {
5908
- log.info(pc.yellow("Using these pre-selected options:"));
5909
- log.message(displayConfig(otherFlags));
5910
- log.message("");
5911
- }
5912
6036
  let config;
5913
6037
  if (input.yes) {
6038
+ const flagConfig = processProvidedFlagsWithoutValidation(cliInput, finalBaseName);
5914
6039
  config = {
5915
- ...DEFAULT_CONFIG,
6040
+ ...getDefaultConfig(),
5916
6041
  ...flagConfig,
5917
6042
  projectName: finalBaseName,
5918
6043
  projectDir: finalResolvedPath,
5919
6044
  relativePath: finalPathInput
5920
6045
  };
6046
+ coerceBackendPresets(config);
6047
+ validateConfigCompatibility(config, providedFlags, cliInput);
5921
6048
  if (config.backend === "convex") log.info("Due to '--backend convex' flag, the following options have been automatically set: auth=false, database=none, orm=none, api=none, runtime=none, dbSetup=none, examples=todo");
5922
6049
  else if (config.backend === "none") log.info("Due to '--backend none', the following options have been automatically set: --auth=false, --database=none, --orm=none, --api=none, --runtime=none, --db-setup=none, --examples=none");
5923
6050
  log.info(pc.yellow("Using default/flag options (config prompts skipped):"));
5924
6051
  log.message(displayConfig(config));
5925
6052
  log.message("");
5926
- } else config = await gatherConfig(flagConfig, finalBaseName, finalResolvedPath, finalPathInput);
6053
+ } else {
6054
+ const flagConfig = processAndValidateFlags(cliInput, providedFlags, finalBaseName);
6055
+ const { projectName: _projectNameFromFlags,...otherFlags } = flagConfig;
6056
+ if (Object.keys(otherFlags).length > 0) {
6057
+ log.info(pc.yellow("Using these pre-selected options:"));
6058
+ log.message(displayConfig(otherFlags));
6059
+ log.message("");
6060
+ }
6061
+ config = await gatherConfig(flagConfig, finalBaseName, finalResolvedPath, finalPathInput);
6062
+ }
5927
6063
  await createProject(config);
5928
6064
  const reproducibleCommand = generateReproducibleCommand(config);
5929
6065
  log.success(pc.blue(`You can reproduce this setup with the following command:\n${reproducibleCommand}`));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-better-t-stack",
3
- "version": "2.33.8-canary.98a850ad",
3
+ "version": "2.33.9-canary.20c8e952",
4
4
  "description": "A modern CLI tool for scaffolding end-to-end type-safe TypeScript projects with best practices and customizable configurations",
5
5
  "type": "module",
6
6
  "license": "MIT",