create-better-fullstack 2.1.4 → 2.1.6

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.
@@ -1,11 +1,13 @@
1
1
  #!/usr/bin/env node
2
- import { A as types_exports, D as getGraphSummary, E as getGraphPart, S as getUserPkgManager, T as getGraphBackendUrl, _ as writeBtsConfig, a as handleError, b as DEFAULT_UI_LIBRARY_BY_FRONTEND, c as isSilent, d as setLastPromptShownUI, f as buildBtsConfigForPersistence, h as readBtsConfigFromFile, i as exitWithError, k as hasGraphPart, l as runWithContextAsync, n as UserCancelledError, o as didLastPromptShowUI, r as exitCancelled, s as isFirstPrompt, t as CLIError, u as setIsFirstPrompt, v as getLatestCLIVersion, w as getGraphBackendDeployInstructions, x as getDefaultConfig, y as DEFAULT_CONFIG } from "./errors-Cyol8zbN.mjs";
2
+ import { c as DEFAULT_CONFIG, d as getUserPkgManager, g as getGraphSummary, h as getGraphPart, i as readBtsConfigFromFile, l as DEFAULT_UI_LIBRARY_BY_FRONTEND, m as getGraphBackendUrl, o as writeBtsConfig, p as getGraphBackendDeployInstructions, s as getLatestCLIVersion, t as buildBtsConfigForPersistence, u as getDefaultConfig, v as hasGraphPart, y as types_exports } from "./bts-config-BceXPcpI.mjs";
3
3
  import { t as renderTitle } from "./render-title-zvyKC1ej.mjs";
4
- import { i as canPromptInteractively, n as getPackageExecutionArgs, r as addPackageDependency, t as setupAddons } from "./addons-setup-CyrP1IV-.mjs";
5
- import { _ as CreateCommandInputSchema, a as allowedApisForFrontends$1, c as getCompatibleUILibraries$1, d as isExampleChatSdkAllowed$1, f as isFrontendAllowedWithBackend$1, h as splitFrontends$1, i as validateFullConfig, l as hasWebStyling$1, m as requiresChatSdkVercelAI, n as formatProject, p as isWebFrontend$1, r as validateConfigForProgrammaticUse, s as getCompatibleCSSFrameworks$1, u as isExampleAIAllowed$1, v as CreateCommandOptionsSchema } from "./file-formatter-B3dsev2l.mjs";
6
- import { _ as isGoBack, a as runMavenTests, c as applyDependencyVersionChannel, d as isCancel$1, f as navigableConfirm, g as GO_BACK_SYMBOL, h as setIsFirstPrompt$1, i as runGradleTests, l as getAddonsChoice, m as navigableSelect, n as runCargoBuild, o as runMixCompile, p as navigableMultiselect, r as runGoModTidy, s as runUvSync, t as installDependencies } from "./install-dependencies-CgNh-aOy.mjs";
7
- import { n as commandExists, t as runGeneratedChecks } from "./generated-checks-C8hn9w2i.mjs";
8
- import { n as getTemplateDescription, r as generateReproducibleCommand, t as getTemplateConfig } from "./templates-CnTOtKjm.mjs";
4
+ import { a as handleError, c as isSilent, d as setLastPromptShownUI, i as exitWithError, l as runWithContextAsync, n as UserCancelledError, o as didLastPromptShowUI, r as exitCancelled, s as isFirstPrompt, t as CLIError, u as setIsFirstPrompt } from "./errors-ns_o2OKg.mjs";
5
+ import { i as canPromptInteractively, n as getPackageExecutionArgs, r as addPackageDependency, t as setupAddons } from "./addons-setup-C_lxdJqU.mjs";
6
+ import { _ as CreateCommandInputSchema, a as allowedApisForFrontends$1, c as getCompatibleUILibraries$1, d as isExampleChatSdkAllowed$1, f as isFrontendAllowedWithBackend$1, h as splitFrontends$1, i as validateFullConfig, l as hasWebStyling$1, m as requiresChatSdkVercelAI, n as formatProject, p as isWebFrontend$1, r as validateConfigForProgrammaticUse, s as getCompatibleCSSFrameworks$1, u as isExampleAIAllowed$1, v as CreateCommandOptionsSchema } from "./file-formatter-XU6ti05V.mjs";
7
+ import { _ as isGoBack, a as runMavenTests, c as applyDependencyVersionChannel, d as isCancel$1, f as navigableConfirm, g as GO_BACK_SYMBOL, h as setIsFirstPrompt$1, i as runGradleTests, l as getAddonsChoice, m as navigableSelect, n as runCargoBuild, o as runMixCompile, p as navigableMultiselect, r as runGoModTidy, s as runUvSync, t as installDependencies } from "./install-dependencies-DHoYa3P-.mjs";
8
+ import { n as commandExists, t as runGeneratedChecks } from "./generated-checks-Dt4Xqp1x.mjs";
9
+ import { a as getTemplateConfig, i as validateArrayOptions, n as getProvidedFlags, o as getTemplateDescription, r as processFlags, s as generateReproducibleCommand, t as applyEffectBackendDefaults } from "./config-processing-B_1wTe3g.mjs";
10
+ import { r as recordScaffoldManifest } from "./scaffold-manifest-GV1fbhpD.mjs";
9
11
  import { cancel, confirm, intro, isCancel, log, outro, select, spinner, text } from "@clack/prompts";
10
12
  import { createRouterClient, os } from "@orpc/server";
11
13
  import pc from "picocolors";
@@ -17,7 +19,6 @@ import path from "node:path";
17
19
  import { formatStackPartSpec, getLocalWebDevPort } from "@better-fullstack/types";
18
20
  import consola, { consola as consola$1 } from "consola";
19
21
  import { $, execa } from "execa";
20
- import { cliInputToProjectConfigPartial } from "@better-fullstack/types/stack-translation";
21
22
  import os$1 from "node:os";
22
23
 
23
24
  //#region src/utils/project-history.ts
@@ -792,6 +793,11 @@ const BACKEND_PROMPT_OPTIONS = [
792
793
  label: "feTS",
793
794
  hint: "TypeScript HTTP Framework with e2e type-safety"
794
795
  },
796
+ {
797
+ value: "effect",
798
+ label: "Effect",
799
+ hint: "Effect-first service backend with layers and schema"
800
+ },
795
801
  {
796
802
  value: "nestjs",
797
803
  label: "NestJS",
@@ -833,6 +839,7 @@ function resolveBackendPrompt(context = {}) {
833
839
  "fastify",
834
840
  "elysia",
835
841
  "fets",
842
+ "effect",
836
843
  "nestjs",
837
844
  "adonisjs",
838
845
  "nitro",
@@ -870,15 +877,24 @@ async function getBackendFrameworkChoice(backendFramework, frontends) {
870
877
 
871
878
  //#endregion
872
879
  //#region src/prompts/caching.ts
873
- const CACHING_PROMPT_OPTIONS = [{
874
- value: "upstash-redis",
875
- label: "Upstash Redis",
876
- hint: "Serverless Redis with REST API for edge and serverless"
877
- }, {
878
- value: "none",
879
- label: "None",
880
- hint: "Skip caching layer setup"
881
- }];
880
+ const CACHING_PROMPT_OPTIONS = [
881
+ {
882
+ value: "upstash-redis",
883
+ label: "Upstash Redis",
884
+ hint: "Serverless Redis with REST API for edge and serverless"
885
+ },
886
+ {
887
+ value: "redis",
888
+ label: "Redis",
889
+ hint: "Self-hosted Redis via ioredis (TCP) — full command surface"
890
+ },
891
+ {
892
+ value: "none",
893
+ label: "None",
894
+ hint: "Skip caching layer setup"
895
+ }
896
+ ];
897
+ const NON_TS_CACHING_PROMPT_OPTIONS = CACHING_PROMPT_OPTIONS.filter((option) => option.value !== "redis");
882
898
  function resolveCachingPrompt(context = {}) {
883
899
  if (context.ecosystem === "react-native" || context.ecosystem === "elixir") return {
884
900
  shouldPrompt: false,
@@ -889,12 +905,12 @@ function resolveCachingPrompt(context = {}) {
889
905
  if (context.ecosystem && context.ecosystem !== "typescript") return context.caching !== void 0 ? {
890
906
  shouldPrompt: false,
891
907
  mode: "single",
892
- options: CACHING_PROMPT_OPTIONS,
908
+ options: NON_TS_CACHING_PROMPT_OPTIONS,
893
909
  autoValue: context.caching
894
910
  } : {
895
911
  shouldPrompt: true,
896
912
  mode: "single",
897
- options: CACHING_PROMPT_OPTIONS,
913
+ options: NON_TS_CACHING_PROMPT_OPTIONS,
898
914
  initialValue: "none"
899
915
  };
900
916
  if (context.backend === "none" || context.backend === "convex") return {
@@ -1968,22 +1984,22 @@ async function getEcosystemChoice(ecosystem) {
1968
1984
  async function getEffectChoice(effect) {
1969
1985
  if (effect !== void 0) return effect;
1970
1986
  const response = await navigableSelect({
1971
- message: "Select Effect ecosystem",
1987
+ message: "Select Effect services",
1972
1988
  options: [
1973
1989
  {
1974
1990
  value: "effect",
1975
- label: "Effect (core)",
1976
- hint: "Powerful effect system for TypeScript"
1991
+ label: "Effect Core",
1992
+ hint: "Add Effect services and layers to the selected stack"
1977
1993
  },
1978
1994
  {
1979
1995
  value: "effect-full",
1980
- label: "Effect Full",
1981
- hint: "Full Effect ecosystem with schema validation, platform abstractions, and SQL"
1996
+ label: "Effect Platform + SQL",
1997
+ hint: "Core plus Platform, browser/server adapters, SQL, and Vitest integration"
1982
1998
  },
1983
1999
  {
1984
2000
  value: "none",
1985
2001
  label: "None",
1986
- hint: "No Effect ecosystem"
2002
+ hint: "No additional Effect services"
1987
2003
  }
1988
2004
  ],
1989
2005
  initialValue: "none"
@@ -2135,6 +2151,11 @@ async function getFileStorageChoice(fileStorage, backend) {
2135
2151
  label: "Cloudinary",
2136
2152
  hint: "Image and media storage with transformations"
2137
2153
  },
2154
+ {
2155
+ value: "supabase-storage",
2156
+ label: "Supabase Storage",
2157
+ hint: "S3-compatible object storage with signed URLs and RLS"
2158
+ },
2138
2159
  {
2139
2160
  value: "none",
2140
2161
  label: "None",
@@ -2515,6 +2536,11 @@ const GO_WEB_FRAMEWORK_PROMPT_OPTIONS = [
2515
2536
  label: "Chi",
2516
2537
  hint: "Lightweight, zero-dependency router built on net/http"
2517
2538
  },
2539
+ {
2540
+ value: "stdlib",
2541
+ label: "net/http",
2542
+ hint: "Go 1.22+ standard-library routing (ServeMux), zero dependencies"
2543
+ },
2518
2544
  {
2519
2545
  value: "none",
2520
2546
  label: "None",
@@ -2537,6 +2563,11 @@ const GO_ORM_PROMPT_OPTIONS = [
2537
2563
  label: "Ent",
2538
2564
  hint: "Code-first ORM by Meta with graph traversal API, 15k+ stars"
2539
2565
  },
2566
+ {
2567
+ value: "bun",
2568
+ label: "Bun",
2569
+ hint: "SQL-first Go ORM by uptrace with a lightweight query builder"
2570
+ },
2540
2571
  {
2541
2572
  value: "none",
2542
2573
  label: "None",
@@ -2913,10 +2944,19 @@ const PARAGLIDE_FRONTENDS = new Set([
2913
2944
  async function getI18nChoice(i18n, frontend) {
2914
2945
  if (i18n !== void 0) return i18n;
2915
2946
  const hasNext = frontend?.includes("next") ?? false;
2947
+ const hasParaglideFrontend = frontend?.some((f) => PARAGLIDE_FRONTENDS.has(f)) ?? false;
2948
+ const hasIntlayerFrontend = frontend?.some((f) => [
2949
+ "next",
2950
+ "vinext",
2951
+ "tanstack-router",
2952
+ "tanstack-start",
2953
+ "react-router",
2954
+ "react-vite"
2955
+ ].includes(f)) ?? false;
2916
2956
  const response = await navigableSelect({
2917
2957
  message: "Select internationalization (i18n) library",
2918
2958
  options: [
2919
- ...frontend?.some((f) => PARAGLIDE_FRONTENDS.has(f)) ?? false ? [{
2959
+ ...hasParaglideFrontend ? [{
2920
2960
  value: "paraglide",
2921
2961
  label: "Paraglide",
2922
2962
  hint: "Type-safe, compiler-based i18n for modern web frontends"
@@ -2931,6 +2971,11 @@ async function getI18nChoice(i18n, frontend) {
2931
2971
  label: "next-intl",
2932
2972
  hint: "Lightweight i18n for Next.js with App Router support"
2933
2973
  }] : [],
2974
+ ...hasIntlayerFrontend ? [{
2975
+ value: "intlayer",
2976
+ label: "Intlayer",
2977
+ hint: "Component-level, type-safe i18n for React, Next.js, and Vite"
2978
+ }] : [],
2934
2979
  {
2935
2980
  value: "none",
2936
2981
  label: "None",
@@ -3032,12 +3077,26 @@ const JAVA_WEB_FRAMEWORK_PROMPT_OPTIONS = [
3032
3077
  label: "Quarkus",
3033
3078
  hint: "Cloud-native Java framework optimized for fast startup and lower memory use"
3034
3079
  },
3080
+ {
3081
+ value: "micronaut",
3082
+ label: "Micronaut",
3083
+ hint: "Modern JVM framework with compile-time DI/AOP and fast startup, low memory"
3084
+ },
3035
3085
  {
3036
3086
  value: "none",
3037
3087
  label: "None",
3038
3088
  hint: "No Java web framework"
3039
3089
  }
3040
3090
  ];
3091
+ const JAVA_LANGUAGE_PROMPT_OPTIONS = [{
3092
+ value: "java",
3093
+ label: "Java",
3094
+ hint: "Classic Java sources (default)"
3095
+ }, {
3096
+ value: "kotlin",
3097
+ label: "Kotlin",
3098
+ hint: "Idiomatic Kotlin sources (Spring Boot only)"
3099
+ }];
3041
3100
  const JAVA_BUILD_TOOL_PROMPT_OPTIONS = [
3042
3101
  {
3043
3102
  value: "maven",
@@ -3257,6 +3316,20 @@ async function getJavaWebFrameworkChoice(javaWebFramework) {
3257
3316
  if (isCancel$1(response)) return exitCancelled("Operation cancelled");
3258
3317
  return response;
3259
3318
  }
3319
+ function resolveJavaLanguagePrompt(javaLanguage) {
3320
+ return createStaticSinglePromptResolution(JAVA_LANGUAGE_PROMPT_OPTIONS, "java", javaLanguage);
3321
+ }
3322
+ async function getJavaLanguageChoice(javaLanguage) {
3323
+ const resolution = resolveJavaLanguagePrompt(javaLanguage);
3324
+ if (!resolution.shouldPrompt) return resolution.autoValue ?? "java";
3325
+ const response = await navigableSelect({
3326
+ message: "Select JVM language",
3327
+ options: resolution.options,
3328
+ initialValue: resolution.initialValue
3329
+ });
3330
+ if (isCancel$1(response)) return exitCancelled("Operation cancelled");
3331
+ return response;
3332
+ }
3260
3333
  function resolveJavaBuildToolPrompt(javaBuildTool) {
3261
3334
  return createStaticSinglePromptResolution(JAVA_BUILD_TOOL_PROMPT_OPTIONS, "maven", javaBuildTool);
3262
3335
  }
@@ -3331,24 +3404,45 @@ async function getJavaTestingLibrariesChoice(javaTestingLibraries) {
3331
3404
  if (response.includes("none")) return [];
3332
3405
  return response;
3333
3406
  }
3334
- const JAVA_API_PROMPT_OPTIONS = [{
3335
- value: "spring-graphql",
3336
- label: "Spring for GraphQL",
3337
- hint: "GraphQL schema + annotated controllers on Spring Boot"
3338
- }, {
3339
- value: "none",
3340
- label: "None",
3341
- hint: "REST controllers only"
3342
- }];
3343
- const JAVA_LOGGING_PROMPT_OPTIONS = [{
3344
- value: "logback",
3345
- label: "Logback",
3346
- hint: "Spring Boot default logger with explicit logback-spring.xml"
3347
- }, {
3348
- value: "none",
3349
- label: "None",
3350
- hint: "Spring Boot logging defaults"
3351
- }];
3407
+ const JAVA_API_PROMPT_OPTIONS = [
3408
+ {
3409
+ value: "spring-graphql",
3410
+ label: "Spring for GraphQL",
3411
+ hint: "GraphQL schema + annotated controllers on Spring Boot"
3412
+ },
3413
+ {
3414
+ value: "openapi-generator",
3415
+ label: "OpenAPI Generator",
3416
+ hint: "Generate Spring API interfaces + models from an OpenAPI spec at build time"
3417
+ },
3418
+ {
3419
+ value: "grpc",
3420
+ label: "gRPC",
3421
+ hint: "grpc-java service (protoc codegen) started alongside Spring Boot"
3422
+ },
3423
+ {
3424
+ value: "none",
3425
+ label: "None",
3426
+ hint: "REST controllers only"
3427
+ }
3428
+ ];
3429
+ const JAVA_LOGGING_PROMPT_OPTIONS = [
3430
+ {
3431
+ value: "logback",
3432
+ label: "Logback",
3433
+ hint: "Spring Boot default logger with explicit logback-spring.xml"
3434
+ },
3435
+ {
3436
+ value: "log4j2",
3437
+ label: "Log4j2",
3438
+ hint: "High-performance Apache logging with async loggers (Spring Boot, Micronaut, plain Java)"
3439
+ },
3440
+ {
3441
+ value: "none",
3442
+ label: "None",
3443
+ hint: "Spring Boot logging defaults"
3444
+ }
3445
+ ];
3352
3446
  function resolveJavaApiPrompt(javaApi) {
3353
3447
  return createStaticSinglePromptResolution(JAVA_API_PROMPT_OPTIONS, "none", javaApi);
3354
3448
  }
@@ -4211,6 +4305,16 @@ const RUST_WEB_FRAMEWORK_PROMPT_OPTIONS = [
4211
4305
  label: "Rocket",
4212
4306
  hint: "Convention-over-configuration web framework, 25k+ stars"
4213
4307
  },
4308
+ {
4309
+ value: "poem",
4310
+ label: "Poem",
4311
+ hint: "Full-featured, fast web framework built on Tokio and hyper"
4312
+ },
4313
+ {
4314
+ value: "loco",
4315
+ label: "Loco",
4316
+ hint: "The Rails of Rust: opinionated full-stack framework (runs DB-free too)"
4317
+ },
4214
4318
  {
4215
4319
  value: "none",
4216
4320
  label: "None",
@@ -5903,6 +6007,42 @@ async function getORMChoice(orm, hasDatabase, database, backend, runtime) {
5903
6007
  return response;
5904
6008
  }
5905
6009
 
6010
+ //#endregion
6011
+ //#region src/prompts/workspace-shape.ts
6012
+ const SINGLE_APP_WEB_FRONTENDS = new Set(["next", "tanstack-start"]);
6013
+ /**
6014
+ * The flat single-app layout only makes sense for a "thin self" app: backend
6015
+ * `self` with exactly one Next.js or TanStack Start web frontend. For anything
6016
+ * else we never prompt and resolve to `monorepo` (the compatibility engine and
6017
+ * generator also normalize non-qualifying stacks back to monorepo).
6018
+ */
6019
+ function stackCanUseSingleApp(backend, frontend) {
6020
+ if (backend !== "self") return false;
6021
+ const webFrontends = frontend.filter((f) => f && f !== "none");
6022
+ if (webFrontends.length !== 1) return false;
6023
+ return SINGLE_APP_WEB_FRONTENDS.has(webFrontends[0]);
6024
+ }
6025
+ async function getWorkspaceShapeChoice(workspaceShape, backend, frontend) {
6026
+ if (!stackCanUseSingleApp(backend, frontend ?? [])) return "monorepo";
6027
+ if (workspaceShape !== void 0) return workspaceShape;
6028
+ if (!canPromptInteractively()) return "monorepo";
6029
+ const response = await navigableSelect({
6030
+ message: "Choose workspace layout",
6031
+ options: [{
6032
+ value: "monorepo",
6033
+ label: "Monorepo",
6034
+ hint: "apps/* + packages/* workspaces (recommended)"
6035
+ }, {
6036
+ value: "single-app",
6037
+ label: "Single app",
6038
+ hint: "Flat app at the repo root (no workspaces)"
6039
+ }],
6040
+ initialValue: "monorepo"
6041
+ });
6042
+ if (isCancel$1(response)) return exitCancelled("Operation cancelled");
6043
+ return response;
6044
+ }
6045
+
5906
6046
  //#endregion
5907
6047
  //#region src/prompts/payments.ts
5908
6048
  function resolvePaymentsPrompt(context = {}) {
@@ -5912,13 +6052,30 @@ function resolvePaymentsPrompt(context = {}) {
5912
6052
  options: [],
5913
6053
  autoValue: context.payments
5914
6054
  };
5915
- if (context.backend === "none") return {
5916
- shouldPrompt: false,
5917
- mode: "single",
5918
- options: [],
5919
- autoValue: "none"
5920
- };
5921
6055
  const isPolarCompatible = (context.auth === "better-auth" || context.auth === "better-auth-organizations") && (context.frontends?.length === 0 || splitFrontends$1(context.frontends).web.length > 0);
6056
+ const isRevenueCatCompatible = (context.frontends ?? []).some((frontend) => frontend === "native-bare" || frontend === "native-uniwind" || frontend === "native-unistyles");
6057
+ if (context.backend === "none") {
6058
+ if (isRevenueCatCompatible) return {
6059
+ shouldPrompt: true,
6060
+ mode: "single",
6061
+ options: [{
6062
+ value: "revenuecat",
6063
+ label: "RevenueCat",
6064
+ hint: "In-app subscriptions and cross-platform monetization for mobile."
6065
+ }, {
6066
+ value: "none",
6067
+ label: "None",
6068
+ hint: "No payments integration"
6069
+ }],
6070
+ initialValue: DEFAULT_CONFIG.payments
6071
+ };
6072
+ return {
6073
+ shouldPrompt: false,
6074
+ mode: "single",
6075
+ options: [],
6076
+ autoValue: "none"
6077
+ };
6078
+ }
5922
6079
  const options = [];
5923
6080
  if (isPolarCompatible) options.push({
5924
6081
  value: "polar",
@@ -5941,11 +6098,28 @@ function resolvePaymentsPrompt(context = {}) {
5941
6098
  value: "dodo",
5942
6099
  label: "Dodo Payments",
5943
6100
  hint: "Simple payment infrastructure for developers."
6101
+ }, {
6102
+ value: "creem",
6103
+ label: "Creem",
6104
+ hint: "Cheapest merchant-of-record payments & billing, with a Better Auth plugin."
6105
+ }, {
6106
+ value: "autumn",
6107
+ label: "Autumn",
6108
+ hint: "Usage-based pricing & billing for SaaS and AI apps."
6109
+ }, {
6110
+ value: "commet",
6111
+ label: "Commet",
6112
+ hint: "All-in-one plan-first billing for SaaS and AI products."
5944
6113
  }, {
5945
6114
  value: "none",
5946
6115
  label: "None",
5947
6116
  hint: "No payments integration"
5948
6117
  });
6118
+ if (isRevenueCatCompatible) options.push({
6119
+ value: "revenuecat",
6120
+ label: "RevenueCat",
6121
+ hint: "In-app subscriptions and cross-platform monetization for mobile."
6122
+ });
5949
6123
  return {
5950
6124
  shouldPrompt: true,
5951
6125
  mode: "single",
@@ -6185,6 +6359,24 @@ const SEARCH_PROMPT_OPTIONS = [
6185
6359
  }
6186
6360
  ];
6187
6361
  const NON_TYPESCRIPT_SEARCH_PROMPT_OPTIONS = SEARCH_PROMPT_OPTIONS.filter((option) => option.value === "meilisearch" || option.value === "none");
6362
+ const PYTHON_SEARCH_PROMPT_OPTIONS = SEARCH_PROMPT_OPTIONS.filter((option) => option.value === "meilisearch" || option.value === "elasticsearch" || option.value === "none");
6363
+ const GO_SEARCH_PROMPT_OPTIONS = [
6364
+ {
6365
+ value: "meilisearch",
6366
+ label: "Meilisearch",
6367
+ hint: "Lightning-fast search engine with typo tolerance"
6368
+ },
6369
+ {
6370
+ value: "bleve",
6371
+ label: "Bleve",
6372
+ hint: "Embedded, server-less full-text search for Go (no external service)"
6373
+ },
6374
+ {
6375
+ value: "none",
6376
+ label: "None",
6377
+ hint: "Skip search engine setup"
6378
+ }
6379
+ ];
6188
6380
  function resolveSearchPrompt(context = {}) {
6189
6381
  if (context.ecosystem === "react-native" || context.ecosystem === "elixir") return {
6190
6382
  shouldPrompt: false,
@@ -6192,7 +6384,7 @@ function resolveSearchPrompt(context = {}) {
6192
6384
  options: [],
6193
6385
  autoValue: "none"
6194
6386
  };
6195
- const options = context.ecosystem && context.ecosystem !== "typescript" ? NON_TYPESCRIPT_SEARCH_PROMPT_OPTIONS : SEARCH_PROMPT_OPTIONS;
6387
+ const options = context.ecosystem === "go" ? GO_SEARCH_PROMPT_OPTIONS : context.ecosystem === "python" ? PYTHON_SEARCH_PROMPT_OPTIONS : context.ecosystem && context.ecosystem !== "typescript" ? NON_TYPESCRIPT_SEARCH_PROMPT_OPTIONS : SEARCH_PROMPT_OPTIONS;
6196
6388
  if ((!context.ecosystem || context.ecosystem === "typescript") && (context.backend === "none" || context.backend === "convex")) return {
6197
6389
  shouldPrompt: false,
6198
6390
  mode: "single",
@@ -6578,7 +6770,7 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
6578
6770
  return Promise.resolve("none");
6579
6771
  },
6580
6772
  payments: ({ results }) => {
6581
- if (results.ecosystem !== "typescript") return Promise.resolve("none");
6773
+ if (results.ecosystem !== "typescript" && results.ecosystem !== "react-native") return Promise.resolve("none");
6582
6774
  return getPaymentsChoice(flags.payments, results.auth, results.backend, results.frontend);
6583
6775
  },
6584
6776
  email: ({ results }) => {
@@ -6587,6 +6779,7 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
6587
6779
  },
6588
6780
  effect: ({ results }) => {
6589
6781
  if (results.ecosystem !== "typescript") return Promise.resolve("none");
6782
+ if (results.backend === "effect" && flags.effect === void 0) return Promise.resolve("effect-full");
6590
6783
  return getEffectChoice(flags.effect);
6591
6784
  },
6592
6785
  addons: ({ results }) => {
@@ -6619,6 +6812,7 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
6619
6812
  },
6620
6813
  validation: ({ results }) => {
6621
6814
  if (results.ecosystem !== "typescript") return Promise.resolve("none");
6815
+ if (results.backend === "effect" && flags.validation === void 0) return Promise.resolve("effect-schema");
6622
6816
  return getValidationChoice(flags.validation);
6623
6817
  },
6624
6818
  forms: ({ results }) => {
@@ -6895,6 +7089,13 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
6895
7089
  if (results.ecosystem !== "java") return Promise.resolve("none");
6896
7090
  return getJavaWebFrameworkChoice(flags.javaWebFramework);
6897
7091
  },
7092
+ javaLanguage: ({ results }) => {
7093
+ if (results.ecosystem !== "java") return Promise.resolve("java");
7094
+ if (results.javaWebFramework !== "spring-boot") return Promise.resolve("java");
7095
+ if (flags.javaLanguage !== void 0) return getJavaLanguageChoice(flags.javaLanguage);
7096
+ if (flags.javaWebFramework !== void 0) return Promise.resolve("java");
7097
+ return getJavaLanguageChoice(flags.javaLanguage);
7098
+ },
6898
7099
  javaBuildTool: ({ results }) => {
6899
7100
  if (results.ecosystem !== "java") return Promise.resolve("none");
6900
7101
  return getJavaBuildToolChoice(flags.javaBuildTool);
@@ -7046,6 +7247,7 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
7046
7247
  },
7047
7248
  aiDocs: () => getAiDocsChoice(flags.aiDocs),
7048
7249
  git: () => getGitChoice(flags.git),
7250
+ workspaceShape: ({ results }) => getWorkspaceShapeChoice(flags.workspaceShape, results.backend, results.frontend),
7049
7251
  packageManager: ({ results }) => {
7050
7252
  if (results.ecosystem === "rust" || results.ecosystem === "python" || results.ecosystem === "go" || results.ecosystem === "java" || results.ecosystem === "dotnet" || results.ecosystem === "elixir") return Promise.resolve(flags.packageManager ?? getUserPkgManager());
7051
7253
  return getPackageManagerChoice(flags.packageManager);
@@ -7073,6 +7275,7 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
7073
7275
  examples: result.examples,
7074
7276
  git: result.git,
7075
7277
  packageManager: result.packageManager,
7278
+ workspaceShape: result.workspaceShape,
7076
7279
  install: result.install,
7077
7280
  dbSetup: result.dbSetup,
7078
7281
  api: result.api,
@@ -7147,6 +7350,7 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
7147
7350
  goConfig: result.goConfig,
7148
7351
  goObservability: result.goObservability,
7149
7352
  javaWebFramework: result.javaWebFramework,
7353
+ javaLanguage: result.javaLanguage,
7150
7354
  javaBuildTool: result.javaBuildTool,
7151
7355
  javaOrm: result.javaOrm,
7152
7356
  javaAuth: result.javaAuth,
@@ -7585,35 +7789,6 @@ async function setupProjectDirectory(finalPathInput, shouldClearDirectory) {
7585
7789
  };
7586
7790
  }
7587
7791
 
7588
- //#endregion
7589
- //#region src/utils/config-processing.ts
7590
- function deriveProjectName(projectName, projectDirectory) {
7591
- if (projectName) return projectName;
7592
- if (projectDirectory) return path.basename(path.resolve(process.cwd(), projectDirectory));
7593
- return "";
7594
- }
7595
- function processFlags(options, projectName) {
7596
- const derivedName = deriveProjectName(projectName, options.projectDirectory);
7597
- return cliInputToProjectConfigPartial(options, projectName || derivedName);
7598
- }
7599
- function getProvidedFlags(options) {
7600
- return new Set(Object.keys(options).filter((key) => options[key] !== void 0));
7601
- }
7602
- function validateNoneExclusivity(options, optionName) {
7603
- if (!options || options.length === 0) return;
7604
- if (options.includes("none") && options.length > 1) throw new Error(`Cannot combine 'none' with other ${optionName}.`);
7605
- }
7606
- function validateArrayOptions(options) {
7607
- validateNoneExclusivity(options.frontend, "frontend options");
7608
- validateNoneExclusivity(options.addons, "addons");
7609
- validateNoneExclusivity(options.examples, "examples");
7610
- validateNoneExclusivity(options.aiDocs, "ai docs");
7611
- validateNoneExclusivity(options.rustLibraries, "rust libraries");
7612
- validateNoneExclusivity(options.pythonAi, "python ai libraries");
7613
- validateNoneExclusivity(options.javaLibraries, "java libraries");
7614
- validateNoneExclusivity(options.javaTestingLibraries, "java testing libraries");
7615
- }
7616
-
7617
7792
  //#endregion
7618
7793
  //#region src/validation.ts
7619
7794
  const CORE_STACK_FLAGS = new Set([
@@ -7658,6 +7833,7 @@ function extractAndValidateProjectName(projectName, projectDirectory, throwOnErr
7658
7833
  function processAndValidateFlags(options, providedFlags, projectName) {
7659
7834
  if (options.yolo) {
7660
7835
  const cfg = processFlags(options, projectName);
7836
+ applyEffectBackendDefaults(cfg, getProvidedFlags(options));
7661
7837
  const validatedProjectName$1 = extractAndValidateProjectName(projectName, options.projectDirectory, true);
7662
7838
  if (validatedProjectName$1) cfg.projectName = validatedProjectName$1;
7663
7839
  return cfg;
@@ -7669,6 +7845,7 @@ function processAndValidateFlags(options, providedFlags, projectName) {
7669
7845
  exitWithError(error instanceof Error ? error.message : String(error));
7670
7846
  }
7671
7847
  const config = processFlags(options, projectName);
7848
+ applyEffectBackendDefaults(config, providedFlags);
7672
7849
  const validatedProjectName = extractAndValidateProjectName(projectName, options.projectDirectory, false);
7673
7850
  if (validatedProjectName) config.projectName = validatedProjectName;
7674
7851
  validateFullConfig(config, providedFlags, options);
@@ -7683,7 +7860,9 @@ function processProvidedFlagsWithoutValidation(options, projectName) {
7683
7860
  exitWithError(error instanceof Error ? error.message : String(error));
7684
7861
  }
7685
7862
  }
7863
+ const providedFlags = getProvidedFlags(options);
7686
7864
  const config = processFlags(options, projectName);
7865
+ applyEffectBackendDefaults(config, providedFlags);
7687
7866
  const validatedProjectName = extractAndValidateProjectName(projectName, options.projectDirectory, true);
7688
7867
  if (validatedProjectName) config.projectName = validatedProjectName;
7689
7868
  return config;
@@ -8955,6 +9134,7 @@ async function displayPostInstallInstructions(config) {
8955
9134
  const clerkInstructions = config.auth === "clerk" ? getClerkInstructions(config.backend, config.frontend ?? []) : "";
8956
9135
  const authSetupInstructions = getAuthSetupInstructions(config.auth, backend, config.frontend ?? []);
8957
9136
  const polarInstructions = config.payments === "polar" ? getPolarInstructions(backend, packageManager) : "";
9137
+ const revenueCatInstructions = config.payments === "revenuecat" ? getRevenueCatInstructions(backend, packageManager) : "";
8958
9138
  const paymentSetupInstructions = getPaymentSetupInstructions(config.payments, backend);
8959
9139
  const alchemyDeployInstructions = getAlchemyDeployInstructions(runCmd, webDeploy, serverDeploy, backend);
8960
9140
  const vercelDeployInstructions = getVercelDeployInstructions(webDeploy, serverDeploy, backend);
@@ -9032,6 +9212,7 @@ async function displayPostInstallInstructions(config) {
9032
9212
  if (authSetupInstructions) output += `\n${authSetupInstructions.trim()}\n`;
9033
9213
  if (betterAuthConvexInstructions) output += `\n${betterAuthConvexInstructions.trim()}\n`;
9034
9214
  if (polarInstructions) output += `\n${polarInstructions.trim()}\n`;
9215
+ if (revenueCatInstructions) output += `\n${revenueCatInstructions.trim()}\n`;
9035
9216
  if (paymentSetupInstructions) output += `\n${paymentSetupInstructions.trim()}\n`;
9036
9217
  if (noOrmWarning) output += `\n${noOrmWarning.trim()}\n`;
9037
9218
  if (bunWebNativeWarning) output += `\n${bunWebNativeWarning.trim()}\n`;
@@ -9152,6 +9333,9 @@ function getPaymentSetupInstructions(payments, backend) {
9152
9333
  if (payments === "lemon-squeezy") return `${pc.bold("Lemon Squeezy Setup:")}\n${pc.cyan("•")} Get API key from ${pc.underline("https://app.lemonsqueezy.com/settings/api")}\n${pc.cyan("•")} Set ${pc.white("LEMONSQUEEZY_API_KEY")} and ${pc.white("LEMONSQUEEZY_STORE_ID")} in ${pc.white(envPath)}`;
9153
9334
  if (payments === "paddle") return `${pc.bold("Paddle Setup:")}\n${pc.cyan("•")} Get API keys from ${pc.underline("https://vendors.paddle.com")}\n${pc.cyan("•")} Set ${pc.white("PADDLE_API_KEY")} and ${pc.white("PADDLE_WEBHOOK_SECRET")} in ${pc.white(envPath)}`;
9154
9335
  if (payments === "dodo") return `${pc.bold("Dodo Payments Setup:")}\n${pc.cyan("•")} Get API key from ${pc.underline("https://app.dodopayments.com")}\n${pc.cyan("•")} Set ${pc.white("DODO_PAYMENTS_API_KEY")} and ${pc.white("DODO_PAYMENTS_WEBHOOK_SECRET")} in ${pc.white(envPath)}`;
9336
+ if (payments === "creem") return `${pc.bold("Creem Setup:")}\n${pc.cyan("•")} Get your API key from ${pc.underline("https://docs.creem.io")} (Developers > API Keys)\n${pc.cyan("•")} Set ${pc.white("CREEM_API_KEY")}, ${pc.white("CREEM_WEBHOOK_SECRET")}, and ${pc.white("CREEM_ENVIRONMENT")} (test|prod) in ${pc.white(envPath)}`;
9337
+ if (payments === "autumn") return `${pc.bold("Autumn Setup:")}\n${pc.cyan("•")} Sign up at ${pc.underline("https://useautumn.com")} and connect Stripe\n${pc.cyan("•")} Set ${pc.white("AUTUMN_SECRET_KEY")} in ${pc.white(envPath)} and mount handleAutumnRequest at ${pc.white("/api/autumn/*")}\n${pc.cyan("•")} Define plans/features with ${pc.white("npx atmn@latest")} (Autumn is webhookless)`;
9338
+ if (payments === "commet") return `${pc.bold("Commet Setup:")}\n${pc.cyan("•")} Get your API key from ${pc.underline("https://commet.co")} (Settings > API Keys)\n${pc.cyan("•")} Set ${pc.white("COMMET_API_KEY")} and ${pc.white("COMMET_WEBHOOK_SECRET")} in ${pc.white(envPath)}`;
9155
9339
  return "";
9156
9340
  }
9157
9341
  function getBetterAuthConvexInstructions(hasWeb, webPort, packageManager) {
@@ -9166,6 +9350,14 @@ function getPolarInstructions(backend, packageManager) {
9166
9350
  const envPath = backend === "self" ? "apps/web/.env" : "apps/server/.env";
9167
9351
  return `${pc.bold("Polar Payments Setup:")}\n${pc.cyan("•")} Get access token & product ID from ${pc.underline("https://sandbox.polar.sh/")}\n${pc.cyan("•")} Set POLAR_ACCESS_TOKEN in ${envPath}`;
9168
9352
  }
9353
+ function getRevenueCatInstructions(backend, packageManager) {
9354
+ const base = `${pc.bold("RevenueCat Payments Setup:")}\n${pc.cyan("•")} Create a project, entitlement, and offering in ${pc.underline("https://app.revenuecat.com/")}\n${pc.cyan("•")} Set the public SDK keys in ${pc.white("apps/native/.env")}:\n${pc.white(" EXPO_PUBLIC_REVENUECAT_IOS_KEY=appl_your_ios_key")}\n${pc.white(" EXPO_PUBLIC_REVENUECAT_ANDROID_KEY=goog_your_android_key")}\n${pc.white(" EXPO_PUBLIC_REVENUECAT_ENTITLEMENT_ID=pro")}`;
9355
+ if (backend === "convex") {
9356
+ const cmd = packageManager === "npm" ? "npx" : packageManager;
9357
+ return `${base}\n${pc.cyan("•")} Set the webhook secret (min 32 chars) from ${pc.white("packages/backend")}:\n${pc.white(" cd packages/backend")}\n${pc.white(` ${cmd} convex env set REVENUECAT_WEBHOOK_AUTH your_webhook_secret`)}\n${pc.cyan("•")} Configure a RevenueCat webhook to ${pc.white("https://<your-convex-site-url>/webhooks/revenuecat")} using the same value as the Authorization header`;
9358
+ }
9359
+ return base;
9360
+ }
9169
9361
  function getAlchemyDeployInstructions(runCmd, webDeploy, serverDeploy, backend) {
9170
9362
  const instructions = [];
9171
9363
  const isBackendSelf = backend === "self";
@@ -9596,6 +9788,7 @@ async function createProject(options, cliInput = {}) {
9596
9788
  await applyDependencyVersionChannel(projectDir, options.versionChannel);
9597
9789
  await writeBtsConfig(options);
9598
9790
  await formatProject(projectDir);
9791
+ await recordScaffoldManifest(projectDir);
9599
9792
  if (!isSilent()) log.success("Project template successfully scaffolded!");
9600
9793
  if (options.install && (options.ecosystem === "typescript" || options.ecosystem === "react-native")) {
9601
9794
  const result$1 = await installDependencies({
@@ -10204,8 +10397,12 @@ const AddCommandInputSchema = CreateCommandOptionsSchema.omit({
10204
10397
  directoryConflict: true,
10205
10398
  renderTitle: true,
10206
10399
  disableAnalytics: true,
10207
- manualDb: true
10208
- }).extend({ projectDir: z.string().optional().describe("Project directory (defaults to current)") });
10400
+ manualDb: true,
10401
+ workspaceShape: true
10402
+ }).extend({
10403
+ projectDir: z.string().optional().describe("Project directory (defaults to current)"),
10404
+ acknowledgeArchitectureChange: z.boolean().optional().describe("Acknowledge that this update replaces an existing database/orm/auth/api/backend/runtime choice (data/schema migration required)")
10405
+ });
10209
10406
  const ProjectCheckInputSchema = z.tuple([z.string().optional().describe("Project directory to diagnose (defaults to current directory)"), z.object({
10210
10407
  skipChecks: z.boolean().optional().default(false).describe("Skip the ecosystem build/type checks (config + deps + env only)"),
10211
10408
  json: z.boolean().optional().default(false).describe("Output the diagnosis as JSON")
@@ -10251,7 +10448,7 @@ const router = os.router({
10251
10448
  }
10252
10449
  }),
10253
10450
  add: os.meta({ description: "Add addons, deploy targets, or stack capabilities to an existing Better Fullstack project using its bts.jsonc config" }).input(AddCommandInputSchema).handler(async ({ input }) => {
10254
- const { addHandler } = await import("./add-handler-BNSL6HdM.mjs");
10451
+ const { addHandler } = await import("./add-handler-CuPmSJRM.mjs");
10255
10452
  await addHandler(input);
10256
10453
  }),
10257
10454
  history: os.meta({ description: "Show history of scaffolded projects with reproducible commands" }).input(z.object({
@@ -10279,7 +10476,7 @@ const router = os.router({
10279
10476
  ecosystem: z.string().optional().describe("Filter by ecosystem (effect, tanstack, prisma, etc.)"),
10280
10477
  "list-ecosystems": z.boolean().default(false).describe("List available ecosystems")
10281
10478
  })).handler(async ({ input }) => {
10282
- const { updateDepsHandler, showEcosystems } = await import("./update-deps-D5OG0KmJ.mjs");
10479
+ const { updateDepsHandler, showEcosystems } = await import("./update-deps-DLZAuT3V.mjs");
10283
10480
  if (input["list-ecosystems"]) {
10284
10481
  showEcosystems();
10285
10482
  return;
@@ -10291,13 +10488,63 @@ const router = os.router({
10291
10488
  ecosystem: input.ecosystem
10292
10489
  });
10293
10490
  }),
10491
+ gen: os.meta({ description: "Generate in-project code for an existing Better Fullstack project (e.g. `gen resource <name>` / `gen route <name>` for a new trpc/orpc API resource router)" }).input(z.tuple([
10492
+ z.enum(["resource", "route"]).describe("What to generate: resource (alias: route)"),
10493
+ z.string().describe("Name of the resource/route (e.g. post)"),
10494
+ z.object({
10495
+ dir: z.string().optional().describe("Project directory (defaults to current)"),
10496
+ dryRun: z.boolean().default(false).describe("Print the planned changes without writing any files")
10497
+ })
10498
+ ])).handler(async ({ input }) => {
10499
+ const [kind, name, options] = input;
10500
+ const { genCommand } = await import("./gen-DWx3Xu_K.mjs");
10501
+ await genCommand({
10502
+ kind,
10503
+ name,
10504
+ dir: options.dir,
10505
+ dryRun: options.dryRun
10506
+ });
10507
+ }),
10508
+ registry: os.meta({ description: "Manage community/private capability packs for an existing Better Fullstack project (`registry add <source>` installs a pack from a local path or file:// URL; `registry list` shows installed packs)" }).input(z.tuple([
10509
+ z.enum(["add", "list"]).optional().default("list").describe("Action to perform: add (install a pack) or list (default)"),
10510
+ z.string().optional().describe("Pack source: a local path or file:// URL (required for `add`)"),
10511
+ z.object({
10512
+ projectDir: z.string().optional().describe("Project directory (defaults to current)"),
10513
+ json: z.boolean().optional().default(false).describe("Output the result as JSON"),
10514
+ dryRun: z.boolean().optional().default(false).describe("Preview the install without writing any files")
10515
+ })
10516
+ ])).handler(async ({ input }) => {
10517
+ const [action, source, options] = input;
10518
+ const { registryHandler } = await import("./registry-CxeEOPot.mjs");
10519
+ await registryHandler({
10520
+ action,
10521
+ source,
10522
+ projectDir: options.projectDir,
10523
+ json: options.json,
10524
+ dryRun: options.dryRun
10525
+ });
10526
+ }),
10527
+ update: os.meta({ description: "Re-apply the current bundled templates to an existing Better Fullstack project, classifying template drift vs. your local edits from the bts.lock.json scaffold baseline. Default is a dry-run plan; `--apply` writes safe drift patches + new files. Distinct from the maintainer `update-deps` command." }).input(z.tuple([z.string().optional().describe("Project directory to update (defaults to current directory)"), z.object({
10528
+ dryRun: z.boolean().optional().default(false).describe("Preview the plan without writing (default behavior)"),
10529
+ apply: z.boolean().optional().default(false).describe("Write safe template-drift patches and new files, refreshing the baseline"),
10530
+ check: z.boolean().optional().default(false).describe("Exit non-zero when actionable template drift exists (CI gate)"),
10531
+ json: z.boolean().optional().default(false).describe("Output the plan as JSON"),
10532
+ recordBaseline: z.boolean().optional().default(false).describe("Adopt the current on-disk state as the scaffold baseline (for projects created before the update engine)")
10533
+ })])).handler(async ({ input }) => {
10534
+ const [projectDir, options] = input;
10535
+ const { updateCommand } = await import("./update-C9_x2yBF.mjs");
10536
+ await updateCommand({
10537
+ projectDir,
10538
+ ...options
10539
+ });
10540
+ }),
10294
10541
  mcp: os.meta({ description: "Start the Better Fullstack MCP server so AI agents can inspect the schema, plan stacks, and scaffold projects over stdio" }).handler(async () => {
10295
10542
  log.message("MCP server is started via the 'mcp' subcommand intercepted in cli.ts.");
10296
10543
  log.message("Run: create-better-fullstack mcp");
10297
10544
  }),
10298
10545
  doctor: os.meta({ description: "Diagnose a scaffolded Better Fullstack project: verify its bts.jsonc, installed dependencies, required env vars, and run ecosystem build/type checks" }).input(ProjectCheckInputSchema).handler(async ({ input }) => {
10299
10546
  const [projectDir, options] = input;
10300
- const { doctorCommand } = await import("./doctor-DBoq7bZ9.mjs");
10547
+ const { doctorCommand } = await import("./doctor-DucDyWfl.mjs");
10301
10548
  await doctorCommand({
10302
10549
  projectDir,
10303
10550
  ...options
@@ -10305,11 +10552,28 @@ const router = os.router({
10305
10552
  }),
10306
10553
  check: os.meta({ description: "Check a scaffolded Better Fullstack project for config, dependency, env, and generated build/type drift" }).input(ProjectCheckInputSchema).handler(async ({ input }) => {
10307
10554
  const [projectDir, options] = input;
10308
- const { doctorCommand } = await import("./doctor-DBoq7bZ9.mjs");
10555
+ const { doctorCommand } = await import("./doctor-DucDyWfl.mjs");
10309
10556
  await doctorCommand({
10310
10557
  projectDir,
10311
10558
  ...options
10312
10559
  });
10560
+ }),
10561
+ recommend: os.meta({ description: "Recommend a stack from a natural-language brief (prompt-to-stack): prints the suggested config, the rationale, and a ready-to-run create command" }).input(z.object({
10562
+ brief: z.string().min(1).describe("Natural-language description, e.g. \"a SaaS with Postgres, auth and payments\""),
10563
+ ecosystem: z.string().optional().describe("Force an ecosystem (typescript, react-native, rust, go, python, java, ...)"),
10564
+ json: z.boolean().default(false).describe("Output the recommendation as JSON")
10565
+ })).handler(async ({ input }) => {
10566
+ const { recommendStackFromBrief } = await import("./mcp-D9O5zgAA.mjs");
10567
+ const result = recommendStackFromBrief(input.brief, input.ecosystem);
10568
+ if (input.json) {
10569
+ log.message(JSON.stringify(result, null, 2));
10570
+ return;
10571
+ }
10572
+ log.message("Recommended stack:");
10573
+ for (const line of result.rationale) log.message(` • ${line}`);
10574
+ log.message(`\nConfig: ${JSON.stringify(result.input)}`);
10575
+ if (result.matchedPreset) log.message(`Nearest preset: ${result.matchedPreset}`);
10576
+ log.message("\nReview, then scaffold with: create-better-fullstack create <name> [flags]");
10313
10577
  })
10314
10578
  });
10315
10579
  const caller = createRouterClient(router, { context: {} });
@@ -10375,7 +10639,7 @@ async function builder() {
10375
10639
  return caller.builder();
10376
10640
  }
10377
10641
  async function add(input) {
10378
- const { addHandler } = await import("./add-handler-BNSL6HdM.mjs");
10642
+ const { addHandler } = await import("./add-handler-CuPmSJRM.mjs");
10379
10643
  return addHandler(input, { silent: true });
10380
10644
  }
10381
10645
  async function history(options) {
@@ -10400,6 +10664,15 @@ async function check(projectDir, options) {
10400
10664
  json: options?.json ?? false
10401
10665
  }]);
10402
10666
  }
10667
+ async function update(projectDir, options) {
10668
+ return caller.update([projectDir, {
10669
+ dryRun: options?.dryRun ?? false,
10670
+ apply: options?.apply ?? false,
10671
+ check: options?.check ?? false,
10672
+ json: options?.json ?? false,
10673
+ recordBaseline: options?.recordBaseline ?? false
10674
+ }]);
10675
+ }
10403
10676
 
10404
10677
  //#endregion
10405
- export { createBtsCli as a, history as c, telemetry as d, create as i, router as l, builder as n, docs as o, check as r, doctor as s, add as t, sponsors as u };
10678
+ export { createBtsCli as a, history as c, telemetry as d, update as f, create as i, router as l, builder as n, docs as o, check as r, doctor as s, add as t, sponsors as u };