create-better-fullstack 2.1.5 → 2.1.7

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.
Files changed (29) hide show
  1. package/dist/{add-handler-BNSL6HdM.mjs → add-handler-ztNjzoMN.mjs} +52 -6
  2. package/dist/addons-setup-DnLjAzTw.mjs +7 -0
  3. package/dist/{addons-setup-CyrP1IV-.mjs → addons-setup-LaAj43NP.mjs} +10 -24
  4. package/dist/analytics-DVltG11u.mjs +170 -0
  5. package/dist/{errors-Cyol8zbN.mjs → bts-config-DQVWvPDs.mjs} +72 -149
  6. package/dist/cli.mjs +2 -2
  7. package/dist/{templates-CnTOtKjm.mjs → config-processing-D9-F2Us9.mjs} +57 -2
  8. package/dist/{doctor-DBoq7bZ9.mjs → doctor-a4ca3SMd.mjs} +3 -2
  9. package/dist/errors-ns_o2OKg.mjs +86 -0
  10. package/dist/{file-formatter-B3dsev2l.mjs → file-formatter-gvmrpd-g.mjs} +78 -6
  11. package/dist/gen-CCClL7Ve.mjs +274 -0
  12. package/dist/{generated-checks-C8hn9w2i.mjs → generated-checks-BV9jol5h.mjs} +1 -1
  13. package/dist/index.d.mts +162 -64
  14. package/dist/index.mjs +17 -8
  15. package/dist/{install-dependencies-CgNh-aOy.mjs → install-dependencies-RoUyaE8o.mjs} +80 -21
  16. package/dist/mcp-492OkjcS.mjs +11 -0
  17. package/dist/mcp-entry.mjs +205 -30
  18. package/dist/prompt-environment-BR0Kkw2W.mjs +26 -0
  19. package/dist/registry-DSf2CEaU.mjs +408 -0
  20. package/dist/{run-BYse4yJy.mjs → run-D9I7NdES.mjs} +2081 -464
  21. package/dist/run-buK4aZS8.mjs +15 -0
  22. package/dist/scaffold-manifest-DGRyepdb.mjs +4 -0
  23. package/dist/scaffold-manifest-Dyi0voqE.mjs +135 -0
  24. package/dist/update-DJ8CI5KW.mjs +401 -0
  25. package/package.json +3 -3
  26. package/dist/addons-setup-DyMm42a5.mjs +0 -5
  27. package/dist/mcp-CsW3i66c.mjs +0 -6
  28. package/dist/run-_cf_sFwM.mjs +0 -10
  29. /package/dist/{update-deps-D5OG0KmJ.mjs → update-deps-aD-iQw4U.mjs} +0 -0
@@ -1,5 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { A as types_exports } from "./errors-Cyol8zbN.mjs";
2
+ import { b as types_exports } from "./bts-config-DQVWvPDs.mjs";
3
+ import path from "node:path";
4
+ import { cliInputToProjectConfigPartial } from "@better-fullstack/types/stack-translation";
3
5
 
4
6
  //#region src/utils/generate-reproducible-command.ts
5
7
  function getBaseCommand(packageManager) {
@@ -21,6 +23,7 @@ function appendCommonFlags(flags, config) {
21
23
  else flags.push("--ai-docs none");
22
24
  flags.push(config.git ? "--git" : "--no-git");
23
25
  flags.push(`--package-manager ${config.packageManager}`);
26
+ if (config.workspaceShape && config.workspaceShape !== "monorepo") flags.push(`--workspace-shape ${config.workspaceShape}`);
24
27
  if (config.versionChannel !== "stable") flags.push(`--version-channel ${config.versionChannel}`);
25
28
  flags.push(config.install ? "--install" : "--no-install");
26
29
  }
@@ -326,6 +329,7 @@ function getGoFlags(config) {
326
329
  function getJavaFlags(config) {
327
330
  const flags = ["--ecosystem java"];
328
331
  flags.push(`--java-web-framework ${config.javaWebFramework}`);
332
+ if (config.javaLanguage === "kotlin") flags.push(`--java-language ${config.javaLanguage}`);
329
333
  flags.push(`--java-build-tool ${config.javaBuildTool}`);
330
334
  flags.push(`--java-orm ${config.javaOrm}`);
331
335
  flags.push(`--java-auth ${config.javaAuth}`);
@@ -460,6 +464,21 @@ const TEMPLATE_PRESETS = {
460
464
  webDeploy: "none",
461
465
  serverDeploy: "none"
462
466
  },
467
+ saas: {
468
+ database: "postgres",
469
+ orm: "drizzle",
470
+ backend: "self",
471
+ runtime: "none",
472
+ frontend: ["next"],
473
+ api: "trpc",
474
+ auth: "better-auth-organizations",
475
+ payments: "creem",
476
+ addons: ["turborepo"],
477
+ examples: ["none"],
478
+ dbSetup: "none",
479
+ webDeploy: "none",
480
+ serverDeploy: "none"
481
+ },
463
482
  uniwind: {
464
483
  database: "none",
465
484
  orm: "none",
@@ -488,10 +507,46 @@ function getTemplateDescription(template) {
488
507
  mern: "MongoDB + Express + React + Node.js - Classic MERN stack",
489
508
  pern: "PostgreSQL + Express + React + Node.js - Popular PERN stack",
490
509
  t3: "T3 Stack - Next.js + tRPC + Prisma + PostgreSQL + Better Auth",
510
+ saas: "SaaS Starter - Next.js + tRPC + Drizzle + PostgreSQL + Better Auth Organizations + Creem billing",
491
511
  uniwind: "Expo + Uniwind native app with no backend services",
492
512
  none: "No template - Full customization"
493
513
  }[template] || "";
494
514
  }
495
515
 
496
516
  //#endregion
497
- export { getTemplateDescription as n, generateReproducibleCommand as r, getTemplateConfig as t };
517
+ //#region src/utils/config-processing.ts
518
+ function deriveProjectName(projectName, projectDirectory) {
519
+ if (projectName) return projectName;
520
+ if (projectDirectory) return path.basename(path.resolve(process.cwd(), projectDirectory));
521
+ return "";
522
+ }
523
+ function processFlags(options, projectName) {
524
+ const derivedName = deriveProjectName(projectName, options.projectDirectory);
525
+ return cliInputToProjectConfigPartial(options, projectName || derivedName);
526
+ }
527
+ function applyEffectBackendDefaults(config, providedFlags = /* @__PURE__ */ new Set()) {
528
+ if (config.backend !== "effect") return config;
529
+ if (!providedFlags.has("effect")) config.effect = "effect-full";
530
+ if (!providedFlags.has("validation")) config.validation = "effect-schema";
531
+ return config;
532
+ }
533
+ function getProvidedFlags(options) {
534
+ return new Set(Object.keys(options).filter((key) => options[key] !== void 0));
535
+ }
536
+ function validateNoneExclusivity(options, optionName) {
537
+ if (!options || options.length === 0) return;
538
+ if (options.includes("none") && options.length > 1) throw new Error(`Cannot combine 'none' with other ${optionName}.`);
539
+ }
540
+ function validateArrayOptions(options) {
541
+ validateNoneExclusivity(options.frontend, "frontend options");
542
+ validateNoneExclusivity(options.addons, "addons");
543
+ validateNoneExclusivity(options.examples, "examples");
544
+ validateNoneExclusivity(options.aiDocs, "ai docs");
545
+ validateNoneExclusivity(options.rustLibraries, "rust libraries");
546
+ validateNoneExclusivity(options.pythonAi, "python ai libraries");
547
+ validateNoneExclusivity(options.javaLibraries, "java libraries");
548
+ validateNoneExclusivity(options.javaTestingLibraries, "java testing libraries");
549
+ }
550
+
551
+ //#endregion
552
+ export { getTemplateConfig as a, validateArrayOptions as i, getProvidedFlags as n, getTemplateDescription as o, processFlags as r, generateReproducibleCommand as s, applyEffectBackendDefaults as t };
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { a as handleError, m as readBtsConfig } from "./errors-Cyol8zbN.mjs";
2
+ import { r as readBtsConfig } from "./bts-config-DQVWvPDs.mjs";
3
3
  import { t as renderTitle } from "./render-title-zvyKC1ej.mjs";
4
- import { t as runGeneratedChecks } from "./generated-checks-C8hn9w2i.mjs";
4
+ import { a as handleError } from "./errors-ns_o2OKg.mjs";
5
+ import { t as runGeneratedChecks } from "./generated-checks-BV9jol5h.mjs";
5
6
  import { intro, log, spinner } from "@clack/prompts";
6
7
  import pc from "picocolors";
7
8
  import fs from "fs-extra";
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env node
2
+ import { cancel } from "@clack/prompts";
3
+ import pc from "picocolors";
4
+ import { AsyncLocalStorage } from "node:async_hooks";
5
+ import consola from "consola";
6
+
7
+ //#region src/utils/context.ts
8
+ const cliStorage = new AsyncLocalStorage();
9
+ function defaultContext() {
10
+ return {
11
+ navigation: {
12
+ isFirstPrompt: false,
13
+ lastPromptShownUI: false
14
+ },
15
+ silent: false
16
+ };
17
+ }
18
+ function getContext() {
19
+ const ctx = cliStorage.getStore();
20
+ if (!ctx) return defaultContext();
21
+ return ctx;
22
+ }
23
+ function tryGetContext() {
24
+ return cliStorage.getStore();
25
+ }
26
+ function isSilent() {
27
+ return getContext().silent;
28
+ }
29
+ function isFirstPrompt() {
30
+ return getContext().navigation.isFirstPrompt;
31
+ }
32
+ function didLastPromptShowUI() {
33
+ return getContext().navigation.lastPromptShownUI;
34
+ }
35
+ function setIsFirstPrompt(value) {
36
+ const ctx = tryGetContext();
37
+ if (ctx) ctx.navigation.isFirstPrompt = value;
38
+ }
39
+ function setLastPromptShownUI(value) {
40
+ const ctx = tryGetContext();
41
+ if (ctx) ctx.navigation.lastPromptShownUI = value;
42
+ }
43
+ async function runWithContextAsync(options, fn) {
44
+ const ctx = {
45
+ navigation: {
46
+ isFirstPrompt: false,
47
+ lastPromptShownUI: false
48
+ },
49
+ silent: options.silent ?? false
50
+ };
51
+ return cliStorage.run(ctx, fn);
52
+ }
53
+
54
+ //#endregion
55
+ //#region src/utils/errors.ts
56
+ var UserCancelledError = class extends Error {
57
+ constructor(message = "Operation cancelled") {
58
+ super(message);
59
+ this.name = "UserCancelledError";
60
+ }
61
+ };
62
+ var CLIError = class extends Error {
63
+ constructor(message) {
64
+ super(message);
65
+ this.name = "CLIError";
66
+ }
67
+ };
68
+ function exitWithError(message) {
69
+ if (isSilent()) throw new CLIError(message);
70
+ consola.error(pc.red(message));
71
+ process.exit(1);
72
+ }
73
+ function exitCancelled(message = "Operation cancelled") {
74
+ if (isSilent()) throw new UserCancelledError(message);
75
+ cancel(pc.red(message));
76
+ process.exit(1);
77
+ }
78
+ function handleError(error, fallbackMessage) {
79
+ const message = error instanceof Error ? error.message : fallbackMessage || String(error);
80
+ if (isSilent()) throw error instanceof Error ? error : new Error(message);
81
+ consola.error(pc.red(message));
82
+ process.exit(1);
83
+ }
84
+
85
+ //#endregion
86
+ export { handleError as a, isSilent as c, setLastPromptShownUI as d, exitWithError as i, runWithContextAsync as l, UserCancelledError as n, didLastPromptShowUI as o, exitCancelled as r, isFirstPrompt as s, CLIError as t, setIsFirstPrompt as u };
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { A as types_exports, c as isSilent, i as exitWithError, t as CLIError } from "./errors-Cyol8zbN.mjs";
2
+ import { b as types_exports } from "./bts-config-DQVWvPDs.mjs";
3
+ import { c as isSilent, i as exitWithError, t as CLIError } from "./errors-ns_o2OKg.mjs";
3
4
  import pc from "picocolors";
4
5
  import z from "zod";
5
6
  import fs from "fs-extra";
@@ -59,6 +60,7 @@ const CreateCommandOptionsSchema = z.object({
59
60
  examples: z.array(types_exports.ExamplesSchema).optional(),
60
61
  git: z.boolean().optional(),
61
62
  packageManager: types_exports.PackageManagerSchema.optional(),
63
+ workspaceShape: types_exports.WorkspaceShapeSchema.optional().describe("Workspace layout: monorepo (default) or single-app (flat root app; only for a thin self app)"),
62
64
  install: z.boolean().optional(),
63
65
  versionChannel: types_exports.VersionChannelSchema.optional().describe("Dependency version channel (stable, latest, beta)"),
64
66
  dbSetup: types_exports.DatabaseSetupSchema.optional(),
@@ -121,6 +123,7 @@ const CreateCommandOptionsSchema = z.object({
121
123
  goConfig: types_exports.GoConfigSchema.optional().describe("Go config management (viper, koanf)"),
122
124
  goObservability: types_exports.GoObservabilitySchema.optional().describe("Go observability (opentelemetry)"),
123
125
  javaWebFramework: types_exports.JavaWebFrameworkSchema.optional().describe("Java web framework (spring-boot, quarkus, none)"),
126
+ javaLanguage: types_exports.JavaLanguageSchema.optional().describe("JVM language (java, kotlin)"),
124
127
  javaBuildTool: types_exports.JavaBuildToolSchema.optional().describe("Java build tool (maven, gradle, none)"),
125
128
  javaOrm: types_exports.JavaOrmSchema.optional().describe("Java ORM/database (spring-data-jpa)"),
126
129
  javaAuth: types_exports.JavaAuthSchema.optional().describe("Java auth (spring-security)"),
@@ -426,6 +429,10 @@ function validatePaymentsCompatibility(payments, auth, _backend, frontends = [])
426
429
  const { web } = splitFrontends$1(frontends);
427
430
  if (web.length === 0 && frontends.length > 0) exitWithError("Polar payments requires a web frontend or no frontend. Please select a web frontend or choose a different payments provider.");
428
431
  }
432
+ if (payments === "revenuecat") {
433
+ const { native } = splitFrontends$1(frontends);
434
+ if (native.length === 0) exitWithError("RevenueCat payments requires a native frontend. Please select a native frontend or choose a different payments provider.");
435
+ }
429
436
  }
430
437
  function validateExamplesCompatibility(examples, backend, frontend, runtime, ai) {
431
438
  const examplesArr = examples ?? [];
@@ -677,6 +684,14 @@ function validatePeerDependencies(config) {
677
684
 
678
685
  //#endregion
679
686
  //#region src/utils/config-validation.ts
687
+ const INTLAYER_COMPATIBLE_FRONTENDS = new Set([
688
+ "next",
689
+ "vinext",
690
+ "tanstack-router",
691
+ "tanstack-start",
692
+ "react-router",
693
+ "react-vite"
694
+ ]);
680
695
  function validateDatabaseOrmAuth(cfg, flags) {
681
696
  const db = cfg.database;
682
697
  const orm = cfg.orm;
@@ -938,7 +953,8 @@ function validateBackendNoneConstraints(config, providedFlags) {
938
953
  if (has("database") && config.database !== "none") exitWithError("Backend 'none' requires '--database none'. Please remove the --database flag or set it to 'none'.");
939
954
  if (has("orm") && config.orm !== "none") exitWithError("Backend 'none' requires '--orm none'. Please remove the --orm flag or set it to 'none'.");
940
955
  if (has("api") && config.api !== "none") exitWithError("Backend 'none' requires '--api none'. Please remove the --api flag or set it to 'none'.");
941
- if (has("payments") && config.payments !== "none") exitWithError("Backend 'none' requires '--payments none'. Please remove the --payments flag or set it to 'none'.");
956
+ const isNativeRevenueCat = config.payments === "revenuecat" && splitFrontends$1(config.frontend ?? []).native.length > 0;
957
+ if (has("payments") && config.payments !== "none" && !isNativeRevenueCat) exitWithError("Backend 'none' requires '--payments none'. Please remove the --payments flag or set it to 'none'.");
942
958
  if (has("dbSetup") && config.dbSetup !== "none") exitWithError("Backend 'none' requires '--db-setup none'. Please remove the --db-setup flag or set it to 'none'.");
943
959
  if (has("serverDeploy") && config.serverDeploy !== "none") exitWithError("Backend 'none' requires '--server-deploy none'. Please remove the --server-deploy flag or set it to 'none'.");
944
960
  }
@@ -1019,23 +1035,44 @@ function validateApiConstraints(config, _options) {
1019
1035
  });
1020
1036
  if (!config.backend || ![
1021
1037
  "hono",
1038
+ "effect",
1022
1039
  "express",
1023
1040
  "fastify",
1024
1041
  "elysia"
1025
1042
  ].includes(config.backend)) incompatibilityError({
1026
- message: `${apiDisplayName} currently supports Hono, Express, Fastify, and Elysia backends.`,
1043
+ message: `${apiDisplayName} currently supports Hono, Effect, Express, Fastify, and Elysia backends.`,
1027
1044
  provided: {
1028
1045
  api: config.api,
1029
1046
  backend: config.backend ?? "none"
1030
1047
  },
1031
1048
  suggestions: [
1032
1049
  "Use --backend hono",
1050
+ "Use --backend effect",
1033
1051
  "Use --backend express",
1034
1052
  "Use --backend fastify",
1035
1053
  "Use --backend elysia"
1036
1054
  ]
1037
1055
  });
1038
1056
  }
1057
+ function validateEffectBackendConstraints(config) {
1058
+ if (config.backend !== "effect") return;
1059
+ if (config.effect !== "effect-full") missingRequirementError({
1060
+ message: "Effect backend requires Effect Platform + SQL services.",
1061
+ provided: {
1062
+ backend: "effect",
1063
+ effect: config.effect ?? "none"
1064
+ },
1065
+ suggestions: ["Use --effect effect-full"]
1066
+ });
1067
+ if (config.validation !== "effect-schema") missingRequirementError({
1068
+ message: "Effect backend requires Effect Schema validation.",
1069
+ provided: {
1070
+ backend: "effect",
1071
+ validation: config.validation ?? "none"
1072
+ },
1073
+ suggestions: ["Use --validation effect-schema"]
1074
+ });
1075
+ }
1039
1076
  function validateJavaConstraints(config, providedFlags = /* @__PURE__ */ new Set()) {
1040
1077
  if (config.ecosystem !== "java") return;
1041
1078
  const hasSpringBoot = config.javaWebFramework === "spring-boot";
@@ -1043,7 +1080,8 @@ function validateJavaConstraints(config, providedFlags = /* @__PURE__ */ new Set
1043
1080
  const hasNoBuildTool = config.javaBuildTool === "none";
1044
1081
  const hasJavaLibraries = (config.javaLibraries ?? []).some((library) => library !== "none");
1045
1082
  const hasJavaTestingLibraries = (config.javaTestingLibraries ?? []).some((library) => library !== "none");
1046
- const hasSpringOnlyFeatures = config.javaOrm !== "none" || config.javaAuth !== "none" || hasJavaLibraries;
1083
+ const hasJavaApi = (config.javaApi ?? "none") !== "none";
1084
+ const hasSpringOnlyFeatures = config.javaOrm !== "none" || config.javaAuth !== "none" || hasJavaLibraries || hasJavaApi;
1047
1085
  if (hasNoBuildTool && hasJavaWebFramework) incompatibilityError({
1048
1086
  message: "Java web frameworks require Maven or Gradle in the Java scaffold.",
1049
1087
  provided: {
@@ -1059,9 +1097,10 @@ function validateJavaConstraints(config, providedFlags = /* @__PURE__ */ new Set
1059
1097
  "java-build-tool": config.javaBuildTool ?? "none",
1060
1098
  "java-orm": config.javaOrm ?? "none",
1061
1099
  "java-auth": config.javaAuth ?? "none",
1100
+ "java-api": config.javaApi ?? "none",
1062
1101
  "java-libraries": (config.javaLibraries ?? []).join(" ") || "none"
1063
1102
  },
1064
- suggestions: ["Use --java-web-framework spring-boot and a real build tool for Spring features", "Clear --java-orm, --java-auth, and --java-libraries when using plain Java or Quarkus"]
1103
+ suggestions: ["Use --java-web-framework spring-boot and a real build tool for Spring features", "Clear --java-orm, --java-auth, --java-api, and --java-libraries when using plain Java or Quarkus"]
1065
1104
  });
1066
1105
  if (hasNoBuildTool && hasJavaTestingLibraries) incompatibilityError({
1067
1106
  message: "Java testing libraries require Maven or Gradle to manage test dependencies.",
@@ -1241,7 +1280,23 @@ function validateRateLimitConstraints(config) {
1241
1280
  function validateSearchConstraints(config) {
1242
1281
  if (!config.search || config.search === "none") return;
1243
1282
  const ecosystem = config.ecosystem ?? "typescript";
1244
- if (ecosystem !== "typescript" && config.search !== "meilisearch") incompatibilityError({
1283
+ if (config.search === "bleve" && ecosystem !== "go") incompatibilityError({
1284
+ message: "Bleve search is available for Go projects only.",
1285
+ provided: {
1286
+ ecosystem,
1287
+ search: config.search
1288
+ },
1289
+ suggestions: [
1290
+ "Use --ecosystem go",
1291
+ "Use --search meilisearch",
1292
+ "Use --search none"
1293
+ ]
1294
+ });
1295
+ const allowedSearch = {
1296
+ go: ["meilisearch", "bleve"],
1297
+ python: ["meilisearch", "elasticsearch"]
1298
+ }[ecosystem] ?? ["meilisearch"];
1299
+ if (ecosystem !== "typescript" && !allowedSearch.includes(config.search)) incompatibilityError({
1245
1300
  message: "Only Meilisearch search is available for non-TypeScript ecosystems.",
1246
1301
  provided: {
1247
1302
  ecosystem,
@@ -1288,6 +1343,19 @@ function validatePythonApiConstraints(config) {
1288
1343
  suggestions: ["Use --python-web-framework django with --python-api django-rest-framework or django-ninja", "Set --python-api none for FastAPI, Flask, Litestar, or no Python web framework"]
1289
1344
  });
1290
1345
  }
1346
+ function validateI18nConstraints(config) {
1347
+ if (config.i18n !== "intlayer") return;
1348
+ const { web } = splitFrontends$1(config.frontend ?? []);
1349
+ const unsupportedFrontend = web.find((frontend) => !INTLAYER_COMPATIBLE_FRONTENDS.has(frontend));
1350
+ if (web.length === 0 || unsupportedFrontend) incompatibilityError({
1351
+ message: "Intlayer i18n is currently wired only for Next.js, Vinext, TanStack Router/Start, React Router, and React + Vite frontends.",
1352
+ provided: {
1353
+ frontend: web.join(" ") || "none",
1354
+ i18n: "intlayer"
1355
+ },
1356
+ suggestions: ["Use --frontend next, vinext, tanstack-router, tanstack-start, react-router, or react-vite", "Set --i18n none or choose another i18n provider for this frontend"]
1357
+ });
1358
+ }
1291
1359
  function validateFullConfig(config, providedFlags, options) {
1292
1360
  if (config.stackParts && !options.yolo) {
1293
1361
  const graphValidation = (0, types_exports.validateStackParts)(config.stackParts);
@@ -1302,6 +1370,7 @@ function validateFullConfig(config, providedFlags, options) {
1302
1370
  validateEncoreConstraints(config, providedFlags);
1303
1371
  validateAdonisJSConstraints(config, providedFlags);
1304
1372
  validateBackendConstraints(config, providedFlags, options);
1373
+ validateEffectBackendConstraints(config);
1305
1374
  validateFrontendConstraints(config, providedFlags);
1306
1375
  validateApiConstraints(config, options);
1307
1376
  validatePythonApiConstraints(config);
@@ -1312,6 +1381,7 @@ function validateFullConfig(config, providedFlags, options) {
1312
1381
  validateSearchConstraints(config);
1313
1382
  validateJavaConstraints(config, providedFlags);
1314
1383
  validateElixirConstraints(config);
1384
+ validateI18nConstraints(config);
1315
1385
  const hasGraphBackend = config.stackParts?.some((part) => part.role === "backend" && !part.ownerPartId && part.source !== "provided" && part.ecosystem !== "typescript" && part.ecosystem !== "react-native" && part.ecosystem !== "universal");
1316
1386
  if (!(providedFlags.has("serverDeploy") && !options.yes && !options.part?.length && options.ecosystem === void 0 && options.backend === void 0 && config.stackParts === void 0)) validateServerDeployRequiresBackend(config.serverDeploy, config.backend, Boolean(hasGraphBackend));
1317
1387
  validateSelfBackendCompatibility(providedFlags, options, config);
@@ -1368,6 +1438,7 @@ function validateConfigForProgrammaticUse(config) {
1368
1438
  }
1369
1439
  validateEcosystemAuthCompatibility(config);
1370
1440
  validateDatabaseOrmAuth(config);
1441
+ validateEffectBackendConstraints(config);
1371
1442
  if (config.frontend && config.frontend.length > 0) ensureSingleWebAndNative(config.frontend);
1372
1443
  validateApiFrontendCompatibility(config.api, config.frontend, config.astroIntegration);
1373
1444
  validatePythonApiConstraints(config);
@@ -1378,6 +1449,7 @@ function validateConfigForProgrammaticUse(config) {
1378
1449
  validateSearchConstraints(config);
1379
1450
  validateJavaConstraints(config);
1380
1451
  validateElixirConstraints(config);
1452
+ validateI18nConstraints(config);
1381
1453
  validatePaymentsCompatibility(config.payments, config.auth, config.backend, config.frontend);
1382
1454
  if (config.addons && config.addons.length > 0) validateAddonsAgainstFrontends(config.addons, config.frontend, config.auth, config.backend, config.runtime, config.ecosystem, config.rustFrontend, config.javaWebFramework, config.database);
1383
1455
  validateExamplesCompatibility(config.examples ?? [], config.backend, config.frontend ?? [], config.runtime, config.ai);
@@ -0,0 +1,274 @@
1
+ #!/usr/bin/env node
2
+ import { r as readBtsConfig } from "./bts-config-DQVWvPDs.mjs";
3
+ import { log } from "@clack/prompts";
4
+ import pc from "picocolors";
5
+ import fs from "fs-extra";
6
+ import path from "node:path";
7
+ import { processTemplateString } from "@better-fullstack/template-generator";
8
+
9
+ //#region src/commands/gen.ts
10
+ /**
11
+ * Inline resource template (rendered with the project's ProjectConfig via the
12
+ * template-generator's Handlebars pipeline). Kept as a string constant so `gen`
13
+ * works in a published CLI with no template directory / EMBEDDED_TEMPLATES
14
+ * dependency. Branches on `api` (trpc | orpc) and `auth` (better-auth ->
15
+ * protectedProcedure).
16
+ */
17
+ const RESOURCE_TEMPLATE = `{{#if (eq api "trpc")}}
18
+ import { z } from "zod";
19
+
20
+ import { {{#if (isBetterAuth auth)}}protectedProcedure{{else}}publicProcedure{{/if}}, router } from "../index{{importExt}}";
21
+
22
+ export type {{ResourceName}} = {
23
+ id: string;
24
+ name: string;
25
+ createdAt: string;
26
+ };
27
+
28
+ const {{resourceName}}Store: {{ResourceName}}[] = [];
29
+ let {{resourceName}}NextId = 1;
30
+
31
+ const {{resourceName}}Procedure = {{#if (isBetterAuth auth)}}protectedProcedure{{else}}publicProcedure{{/if}};
32
+
33
+ export const {{resourceName}}Router = router({
34
+ list: {{resourceName}}Procedure.query(() => {
35
+ return {{resourceName}}Store;
36
+ }),
37
+ byId: {{resourceName}}Procedure.input(z.object({ id: z.string() })).query(({ input }) => {
38
+ return {{resourceName}}Store.find((item) => item.id === input.id) ?? null;
39
+ }),
40
+ create: {{resourceName}}Procedure
41
+ .input(z.object({ name: z.string().min(1) }))
42
+ .mutation(({ input }) => {
43
+ const item: {{ResourceName}} = {
44
+ id: String({{resourceName}}NextId++),
45
+ name: input.name,
46
+ createdAt: new Date().toISOString(),
47
+ };
48
+ {{resourceName}}Store.push(item);
49
+ return item;
50
+ }),
51
+ update: {{resourceName}}Procedure
52
+ .input(z.object({ id: z.string(), name: z.string().min(1) }))
53
+ .mutation(({ input }) => {
54
+ const item = {{resourceName}}Store.find((entry) => entry.id === input.id);
55
+ if (!item) return null;
56
+ item.name = input.name;
57
+ return item;
58
+ }),
59
+ remove: {{resourceName}}Procedure.input(z.object({ id: z.string() })).mutation(({ input }) => {
60
+ const index = {{resourceName}}Store.findIndex((entry) => entry.id === input.id);
61
+ if (index === -1) return { success: false };
62
+ {{resourceName}}Store.splice(index, 1);
63
+ return { success: true };
64
+ }),
65
+ });
66
+ {{else}}
67
+ import { z } from "zod";
68
+
69
+ import { {{#if (isBetterAuth auth)}}protectedProcedure{{else}}publicProcedure{{/if}} } from "../index{{importExt}}";
70
+
71
+ export type {{ResourceName}} = {
72
+ id: string;
73
+ name: string;
74
+ createdAt: string;
75
+ };
76
+
77
+ const {{resourceName}}Store: {{ResourceName}}[] = [];
78
+ let {{resourceName}}NextId = 1;
79
+
80
+ const {{resourceName}}Procedure = {{#if (isBetterAuth auth)}}protectedProcedure{{else}}publicProcedure{{/if}};
81
+
82
+ export const {{resourceName}}Router = {
83
+ list: {{resourceName}}Procedure.handler(() => {
84
+ return {{resourceName}}Store;
85
+ }),
86
+ byId: {{resourceName}}Procedure
87
+ .input(z.object({ id: z.string() }))
88
+ .handler(({ input }) => {
89
+ return {{resourceName}}Store.find((item) => item.id === input.id) ?? null;
90
+ }),
91
+ create: {{resourceName}}Procedure
92
+ .input(z.object({ name: z.string().min(1) }))
93
+ .handler(({ input }) => {
94
+ const item: {{ResourceName}} = {
95
+ id: String({{resourceName}}NextId++),
96
+ name: input.name,
97
+ createdAt: new Date().toISOString(),
98
+ };
99
+ {{resourceName}}Store.push(item);
100
+ return item;
101
+ }),
102
+ update: {{resourceName}}Procedure
103
+ .input(z.object({ id: z.string(), name: z.string().min(1) }))
104
+ .handler(({ input }) => {
105
+ const item = {{resourceName}}Store.find((entry) => entry.id === input.id);
106
+ if (!item) return null;
107
+ item.name = input.name;
108
+ return item;
109
+ }),
110
+ remove: {{resourceName}}Procedure
111
+ .input(z.object({ id: z.string() }))
112
+ .handler(({ input }) => {
113
+ const index = {{resourceName}}Store.findIndex((entry) => entry.id === input.id);
114
+ if (index === -1) return { success: false };
115
+ {{resourceName}}Store.splice(index, 1);
116
+ return { success: true };
117
+ }),
118
+ };
119
+ {{/if}}
120
+ `;
121
+ /** Candidate roots (relative to the project) that may hold a routers/index.ts. */
122
+ const ROUTER_INDEX_CANDIDATES = ["packages/api/src/routers/index.ts", "apps/server/src/routers/index.ts"];
123
+ function splitWords(raw) {
124
+ return raw.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/[^a-zA-Z0-9]+/).map((part) => part.trim()).filter(Boolean);
125
+ }
126
+ function toCamelCase(raw) {
127
+ return splitWords(raw).map((word, index) => index === 0 ? word.toLowerCase() : word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("");
128
+ }
129
+ function toPascalCase(raw) {
130
+ const camel = toCamelCase(raw);
131
+ return camel.charAt(0).toUpperCase() + camel.slice(1);
132
+ }
133
+ /**
134
+ * Scans for an existing routers/index.ts to register the new resource in.
135
+ * Checks the well-known locations first, then any `<root>/src/routers/index.ts`
136
+ * under apps/* and packages/* (robust across hono/express/elysia split backends
137
+ * and the packages/api layout).
138
+ */
139
+ async function findRouterIndex(projectDir) {
140
+ for (const candidate of ROUTER_INDEX_CANDIDATES) {
141
+ const full = path.join(projectDir, candidate);
142
+ if (await fs.pathExists(full)) return full;
143
+ }
144
+ for (const workspace of ["apps", "packages"]) {
145
+ const workspaceDir = path.join(projectDir, workspace);
146
+ if (!await fs.pathExists(workspaceDir)) continue;
147
+ let entries;
148
+ try {
149
+ entries = await fs.readdir(workspaceDir);
150
+ } catch {
151
+ continue;
152
+ }
153
+ for (const entry of entries) {
154
+ const full = path.join(workspaceDir, entry, "src", "routers", "index.ts");
155
+ if (await fs.pathExists(full)) return full;
156
+ }
157
+ }
158
+ return null;
159
+ }
160
+ /** Mirrors the relative-import extension used by the existing routers/index.ts. */
161
+ function detectImportExtension(indexContent) {
162
+ return /from\s+["']\.\.\/index\.js["']/.test(indexContent) ? ".js" : "";
163
+ }
164
+ function injectResource(indexContent, api, resourceName, importExt) {
165
+ const anchor = api === "trpc" ? "export const appRouter = router({" : "export const appRouter = {";
166
+ const lines = indexContent.split("\n");
167
+ const anchorIdx = lines.findIndex((line) => line.includes(anchor));
168
+ if (anchorIdx === -1) return {
169
+ ok: false,
170
+ reason: `Could not find the appRouter anchor (\`${anchor}\`)`
171
+ };
172
+ const registrationKey = `${resourceName}:`;
173
+ if (lines.some((line, idx) => idx > anchorIdx && line.trim().startsWith(registrationKey))) return {
174
+ ok: false,
175
+ reason: `\`${resourceName}\` is already registered in appRouter`
176
+ };
177
+ const importLine = `import { ${resourceName}Router } from "./${resourceName}${importExt}";`;
178
+ const registrationLine = ` ${resourceName}: ${resourceName}Router,`;
179
+ let lastImportIdx = -1;
180
+ for (let i = 0; i < anchorIdx; i += 1) {
181
+ const line = lines[i];
182
+ if (line !== void 0 && line.trimStart().startsWith("import ")) lastImportIdx = i;
183
+ }
184
+ lines.splice(lastImportIdx + 1, 0, importLine);
185
+ const newAnchorIdx = lines.findIndex((line) => line.includes(anchor));
186
+ lines.splice(newAnchorIdx + 1, 0, registrationLine);
187
+ return {
188
+ ok: true,
189
+ content: lines.join("\n")
190
+ };
191
+ }
192
+ async function genCommand(input) {
193
+ const projectDir = path.resolve(input.dir || process.cwd());
194
+ const dryRun = input.dryRun ?? false;
195
+ const btsConfig = await readBtsConfig(projectDir);
196
+ if (!btsConfig) throw new Error(`No Better Fullstack project found in ${projectDir}. Make sure bts.jsonc exists.`);
197
+ const resourceName = toCamelCase(input.name);
198
+ const ResourceName = toPascalCase(input.name);
199
+ if (!resourceName || !/^[a-z]/i.test(resourceName)) throw new Error(`Invalid resource name "${input.name}". Use a name that starts with a letter, e.g. "post".`);
200
+ const ecosystem = btsConfig.ecosystem;
201
+ const api = btsConfig.api;
202
+ if (ecosystem !== "typescript" || api !== "trpc" && api !== "orpc") {
203
+ const stack = `${ecosystem}${api && api !== "none" ? ` + ${api}` : ""}`;
204
+ const message$1 = `\`gen ${input.kind}\` is not yet supported for this stack (${stack}). Currently supported: TypeScript projects with a trpc or orpc API.`;
205
+ log.warn(pc.yellow(message$1));
206
+ return {
207
+ status: "unsupported",
208
+ message: message$1
209
+ };
210
+ }
211
+ const routerIndexPath = await findRouterIndex(projectDir);
212
+ if (!routerIndexPath) {
213
+ const message$1 = `Could not locate a \`routers/index.ts\` in this project. Is this a trpc/orpc project? No files were written.`;
214
+ log.warn(pc.yellow(message$1));
215
+ return {
216
+ status: "unsupported",
217
+ message: message$1
218
+ };
219
+ }
220
+ const routersDir = path.dirname(routerIndexPath);
221
+ const resourceFile = path.join(routersDir, `${resourceName}.ts`);
222
+ const relResourceFile = path.relative(projectDir, resourceFile);
223
+ if (await fs.pathExists(resourceFile)) throw new Error(`Resource "${resourceName}" already exists at ${relResourceFile}. Delete it first or choose another name.`);
224
+ const indexContent = await fs.readFile(routerIndexPath, "utf-8");
225
+ const importExt = detectImportExtension(indexContent);
226
+ const resourceContent = processTemplateString(RESOURCE_TEMPLATE, {
227
+ ...btsConfig,
228
+ resourceName,
229
+ ResourceName,
230
+ importExt
231
+ }).trimStart();
232
+ const injection = injectResource(indexContent, api, resourceName, importExt);
233
+ if (dryRun) {
234
+ log.info(pc.cyan(`[dry run] Would create ${relResourceFile}`));
235
+ log.message(resourceContent);
236
+ if (injection.ok) log.info(pc.cyan(`[dry run] Would register \`${resourceName}: ${resourceName}Router\` in ${path.relative(projectDir, routerIndexPath)}`));
237
+ else log.warn(pc.yellow(`[dry run] Manual wiring required: ${injection.reason}`));
238
+ return {
239
+ status: injection.ok ? "created" : "manual-wiring",
240
+ message: "dry run",
241
+ resourceFile,
242
+ registered: injection.ok
243
+ };
244
+ }
245
+ await fs.writeFile(resourceFile, resourceContent, "utf-8");
246
+ if (!injection.ok) {
247
+ const relIndex = path.relative(projectDir, routerIndexPath);
248
+ const message$1 = `Created ${relResourceFile}, but could not auto-register it: ${injection.reason}. Wire it manually.`;
249
+ log.warn(pc.yellow(message$1));
250
+ log.message([
251
+ `Add these two lines to ${relIndex}:`,
252
+ pc.dim(` import { ${resourceName}Router } from "./${resourceName}${importExt}";`),
253
+ pc.dim(` ${resourceName}: ${resourceName}Router, // inside appRouter`)
254
+ ].join("\n"));
255
+ return {
256
+ status: "manual-wiring",
257
+ message: message$1,
258
+ resourceFile,
259
+ registered: false
260
+ };
261
+ }
262
+ await fs.writeFile(routerIndexPath, injection.content, "utf-8");
263
+ const message = `Created ${relResourceFile} and registered \`${resourceName}: ${resourceName}Router\` in ${path.relative(projectDir, routerIndexPath)}.`;
264
+ log.success(pc.green(message));
265
+ return {
266
+ status: "created",
267
+ message,
268
+ resourceFile,
269
+ registered: true
270
+ };
271
+ }
272
+
273
+ //#endregion
274
+ export { genCommand };
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { O as getPrimaryGraphPart } from "./errors-Cyol8zbN.mjs";
2
+ import { f as getPrimaryGraphPart } from "./bts-config-DQVWvPDs.mjs";
3
3
  import { log, spinner } from "@clack/prompts";
4
4
  import pc from "picocolors";
5
5
  import path from "node:path";