create-better-fullstack 2.0.3 → 2.1.1

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,7 +1,11 @@
1
1
  #!/usr/bin/env node
2
- import { _ as getUserPkgManager, c as getGraphPart, d as hasGraphPart, f as types_exports, g as getDefaultConfig, h as DEFAULT_UI_LIBRARY_BY_FRONTEND, i as writeBtsConfig, l as getGraphSummary, m as DEFAULT_CONFIG, o as getGraphBackendDeployInstructions, p as getLatestCLIVersion, s as getGraphBackendUrl, u as getPrimaryGraphPart } from "./bts-config-Bg1Qea9Y.mjs";
3
- import { A as validateAddonsAgainstFrontends, B as validateWorkersCompatibility, C as isExampleAIAllowed$1, D as requiresChatSdkVercelAI, E as isWebFrontend$1, F as validateServerDeployRequiresBackend, H as incompatibilityError, I as validateUILibraryCSSFrameworkCompatibility, L as validateUILibraryFrontendCompatibility, M as validateExamplesCompatibility, N as validatePaymentsCompatibility, O as splitFrontends$1, P as validateSelfBackendCompatibility, R as validateWebDeployFrontendTemplates, S as hasWebStyling$1, T as isFrontendAllowedWithBackend$1, U as missingRequirementError, V as constraintError, W as renderTitle, _ as isGoBack, a as runMavenTests, b as getCompatibleCSSFrameworks$1, c as applyDependencyVersionChannel, d as isCancel$1, f as navigableConfirm, g as GO_BACK_SYMBOL, h as setIsFirstPrompt, i as runGradleTests, j as validateApiFrontendCompatibility, k as validateAIFrontendCompatibility, l as getAddonsChoice, m as navigableSelect, n as runCargoBuild, o as runMixCompile, p as navigableMultiselect, r as runGoModTidy, s as runUvSync, t as installDependencies, v as allowedApisForFrontends$1, w as isExampleChatSdkAllowed$1, x as getCompatibleUILibraries$1, y as ensureSingleWebAndNative, z as validateWebDeployRequiresWebFrontend } from "./install-dependencies-D0Z1dZEx.mjs";
4
- import { a as CLIError, c as exitWithError, d as isFirstPrompt, f as isSilent, h as setLastPromptShownUI, i as canPromptInteractively, l as handleError, m as setIsFirstPrompt$1, n as getPackageExecutionArgs, o as UserCancelledError, p as runWithContextAsync, r as addPackageDependency, s as exitCancelled, t as setupAddons, u as didLastPromptShowUI } from "./addons-setup-HSghQS7c.mjs";
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-D9yiiGVq.mjs";
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-C_xrNtkL.mjs";
5
+ import { a as generateReproducibleCommand, i as getTemplateDescription, n as validateFullConfig, o as CreateCommandInputSchema, r as getTemplateConfig, t as validateConfigForProgrammaticUse } from "./config-validation-C4glouQh.mjs";
6
+ import { a as getCompatibleUILibraries$1, c as isExampleChatSdkAllowed$1, d as requiresChatSdkVercelAI, f as splitFrontends$1, i as getCompatibleCSSFrameworks$1, l as isFrontendAllowedWithBackend$1, o as hasWebStyling$1, s as isExampleAIAllowed$1, t as allowedApisForFrontends$1, u as isWebFrontend$1 } from "./compatibility-rules-D7zYNVjC.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-DDGF-zDG.mjs";
8
+ import { n as commandExists, t as runGeneratedChecks } from "./generated-checks-DUvVXWId.mjs";
5
9
  import { cancel, confirm, intro, isCancel, log, outro, select, spinner, text } from "@clack/prompts";
6
10
  import { createRouterClient, os } from "@orpc/server";
7
11
  import pc from "picocolors";
@@ -18,10 +22,10 @@ import os$1 from "node:os";
18
22
  import { format } from "oxfmt";
19
23
 
20
24
  //#region src/utils/project-history.ts
21
- const paths = envPaths("better-fullstack", { suffix: "" });
25
+ const paths$1 = envPaths("better-fullstack", { suffix: "" });
22
26
  const HISTORY_FILE = "history.json";
23
27
  function getHistoryPath() {
24
- return path.join(paths.data, HISTORY_FILE);
28
+ return path.join(paths$1.data, HISTORY_FILE);
25
29
  }
26
30
  function generateId() {
27
31
  return `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
@@ -44,11 +48,17 @@ async function readHistory() {
44
48
  }
45
49
  }
46
50
  async function writeHistory(history$1) {
47
- await fs.ensureDir(paths.data);
51
+ await fs.ensureDir(paths$1.data);
48
52
  await fs.writeJson(getHistoryPath(), history$1, { spaces: 2 });
49
53
  }
50
54
  async function addToHistory(config, reproducibleCommand) {
51
55
  const history$1 = await readHistory();
56
+ let configSnapshot;
57
+ try {
58
+ configSnapshot = buildBtsConfigForPersistence(config);
59
+ } catch {
60
+ configSnapshot = void 0;
61
+ }
52
62
  const entry = {
53
63
  id: generateId(),
54
64
  projectName: config.projectName,
@@ -69,7 +79,8 @@ async function addToHistory(config, reproducibleCommand) {
69
79
  packageManager: config.packageManager
70
80
  },
71
81
  cliVersion: getLatestCLIVersion(),
72
- reproducibleCommand
82
+ reproducibleCommand,
83
+ ...configSnapshot ? { config: configSnapshot } : {}
73
84
  };
74
85
  history$1.entries.unshift(entry);
75
86
  if (history$1.entries.length > 100) history$1.entries = history$1.entries.slice(0, 100);
@@ -78,6 +89,17 @@ async function addToHistory(config, reproducibleCommand) {
78
89
  async function getHistory(limit = 10) {
79
90
  return (await readHistory()).entries.slice(0, limit);
80
91
  }
92
+ /**
93
+ * Returns the entry at a 1-based position (1 = most recent), or null when the
94
+ * position is out of range.
95
+ */
96
+ async function getHistoryEntry(position) {
97
+ if (!Number.isInteger(position) || position < 1) return null;
98
+ return (await readHistory()).entries[position - 1] ?? null;
99
+ }
100
+ async function getHistoryCount() {
101
+ return (await readHistory()).entries.length;
102
+ }
81
103
  async function clearHistory() {
82
104
  const historyPath = getHistoryPath();
83
105
  if (await fs.pathExists(historyPath)) await fs.remove(historyPath);
@@ -134,153 +156,187 @@ async function historyHandler(input) {
134
156
  }
135
157
 
136
158
  //#endregion
137
- //#region src/create-command-input.ts
138
- const CreateCommandOptionsSchema = z.object({
139
- template: types_exports.TemplateSchema.optional().describe("Use a predefined template"),
140
- yes: z.boolean().optional().default(false).describe("Use default configuration"),
141
- yolo: z.boolean().optional().default(false).describe("(WARNING - NOT RECOMMENDED) Bypass validations and compatibility checks"),
142
- part: z.array(z.string()).optional().describe("Stack graph part binding, e.g. frontend:typescript:next or backend.orm:go:gorm"),
143
- verbose: z.boolean().optional().default(false).describe("Show detailed result information"),
144
- dryRun: z.boolean().optional().default(false).describe("Preview generated file tree without writing to disk"),
145
- verify: z.boolean().optional().default(false).describe("Run generated project checks after scaffolding without starting dev servers"),
146
- ecosystem: types_exports.EcosystemSchema.optional().describe("Language ecosystem (typescript, react-native, rust, python, go, java, or elixir)"),
147
- database: types_exports.DatabaseSchema.optional(),
148
- orm: types_exports.ORMSchema.optional(),
149
- auth: types_exports.AuthSchema.optional(),
150
- payments: types_exports.PaymentsSchema.optional(),
151
- email: types_exports.EmailSchema.optional(),
152
- fileUpload: types_exports.FileUploadSchema.optional(),
153
- effect: types_exports.EffectSchema.optional(),
154
- stateManagement: types_exports.StateManagementSchema.optional(),
155
- validation: types_exports.ValidationSchema.optional(),
156
- forms: types_exports.FormsSchema.optional(),
157
- testing: types_exports.TestingSchema.optional(),
158
- ai: types_exports.AISchema.optional(),
159
- realtime: types_exports.RealtimeSchema.optional(),
160
- jobQueue: types_exports.JobQueueSchema.optional(),
161
- animation: types_exports.AnimationSchema.optional(),
162
- logging: types_exports.LoggingSchema.optional(),
163
- observability: types_exports.ObservabilitySchema.optional(),
164
- featureFlags: types_exports.FeatureFlagsSchema.optional().describe("Feature flags provider"),
165
- analytics: types_exports.AnalyticsSchema.optional().describe("Privacy-focused analytics"),
166
- cms: types_exports.CMSSchema.optional().describe("Headless CMS solution"),
167
- caching: types_exports.CachingSchema.optional().describe("Caching solution"),
168
- rateLimit: types_exports.RateLimitSchema.optional().describe("Rate limiting solution"),
169
- i18n: types_exports.I18nSchema.optional().describe("Internationalization (i18n) library"),
170
- search: types_exports.SearchSchema.optional().describe("Search engine solution"),
171
- fileStorage: types_exports.FileStorageSchema.optional().describe("File storage solution (S3, R2)"),
172
- mobileNavigation: types_exports.MobileNavigationSchema.optional().describe("Mobile navigation (expo-router, react-navigation)"),
173
- mobileUI: types_exports.MobileUISchema.optional().describe("Mobile UI (tamagui, gluestack-ui, uniwind, unistyles)"),
174
- mobileStorage: types_exports.MobileStorageSchema.optional().describe("Mobile storage (mmkv)"),
175
- mobileTesting: types_exports.MobileTestingSchema.optional().describe("Mobile testing (maestro, react-native-testing-library)"),
176
- mobilePush: types_exports.MobilePushSchema.optional().describe("Mobile push notifications (expo-notifications)"),
177
- mobileOTA: types_exports.MobileOTASchema.optional().describe("Mobile OTA updates (expo-updates)"),
178
- mobileDeepLinking: types_exports.MobileDeepLinkingSchema.optional().describe("Mobile deep linking (expo-linking)"),
179
- frontend: z.array(types_exports.FrontendSchema).optional(),
180
- astroIntegration: types_exports.AstroIntegrationSchema.optional().describe("Astro UI framework integration (react, vue, svelte, solid)"),
181
- addons: z.array(types_exports.AddonsSchema).optional(),
182
- examples: z.array(types_exports.ExamplesSchema).optional(),
183
- git: z.boolean().optional(),
184
- packageManager: types_exports.PackageManagerSchema.optional(),
185
- install: z.boolean().optional(),
186
- versionChannel: types_exports.VersionChannelSchema.optional().describe("Dependency version channel (stable, latest, beta)"),
187
- dbSetup: types_exports.DatabaseSetupSchema.optional(),
188
- backend: types_exports.BackendSchema.optional(),
189
- runtime: types_exports.RuntimeSchema.optional(),
190
- api: types_exports.APISchema.optional(),
191
- cssFramework: types_exports.CSSFrameworkSchema.optional(),
192
- uiLibrary: types_exports.UILibrarySchema.optional(),
193
- shadcnBase: types_exports.ShadcnBaseSchema.optional().describe("shadcn/ui headless library (radix, base)"),
194
- shadcnStyle: types_exports.ShadcnStyleSchema.optional().describe("shadcn/ui visual style (vega, nova, maia, lyra, mira)"),
195
- shadcnIconLibrary: types_exports.ShadcnIconLibrarySchema.optional().describe("shadcn/ui icon library (lucide, tabler, hugeicons, phosphor, remixicon)"),
196
- shadcnColorTheme: types_exports.ShadcnColorThemeSchema.optional().describe("shadcn/ui color theme (neutral, blue, violet, etc.)"),
197
- shadcnBaseColor: types_exports.ShadcnBaseColorSchema.optional().describe("shadcn/ui base neutral color (neutral, stone, zinc, gray)"),
198
- shadcnFont: types_exports.ShadcnFontSchema.optional().describe("shadcn/ui font (inter, geist, figtree, etc.)"),
199
- shadcnRadius: types_exports.ShadcnRadiusSchema.optional().describe("shadcn/ui border radius (default, none, small, medium, large)"),
200
- webDeploy: types_exports.WebDeploySchema.optional(),
201
- serverDeploy: types_exports.ServerDeploySchema.optional(),
202
- directoryConflict: types_exports.DirectoryConflictSchema.optional(),
203
- renderTitle: z.boolean().optional(),
204
- disableAnalytics: z.boolean().optional().default(false).describe("Disable analytics"),
205
- manualDb: z.boolean().optional().default(false).describe("Skip automatic/manual database setup prompt and use manual setup"),
206
- rustWebFramework: types_exports.RustWebFrameworkSchema.optional().describe("Rust web framework (axum, actix-web)"),
207
- rustFrontend: types_exports.RustFrontendSchema.optional().describe("Rust WASM frontend (leptos, dioxus)"),
208
- rustOrm: types_exports.RustOrmSchema.optional().describe("Rust ORM/database (sea-orm, sqlx)"),
209
- rustApi: types_exports.RustApiSchema.optional().describe("Rust API layer (tonic, async-graphql)"),
210
- rustCli: types_exports.RustCliSchema.optional().describe("Rust CLI tools (clap, ratatui)"),
211
- rustLibraries: z.array(types_exports.RustLibrariesSchema).optional().describe("Rust core libraries"),
212
- rustLogging: types_exports.RustLoggingSchema.optional().describe("Rust logging (tracing, env-logger)"),
213
- rustErrorHandling: types_exports.RustErrorHandlingSchema.optional().describe("Rust error handling (anyhow-thiserror, eyre)"),
214
- rustCaching: types_exports.RustCachingSchema.optional().describe("Rust caching (moka, redis)"),
215
- rustAuth: types_exports.RustAuthSchema.optional().describe("Rust auth (oauth2)"),
216
- rustRealtime: types_exports.RustRealtimeSchema.optional().describe("Rust realtime (tokio-tungstenite)"),
217
- rustMessageQueue: types_exports.RustMessageQueueSchema.optional().describe("Rust message queue (lapin)"),
218
- rustObservability: types_exports.RustObservabilitySchema.optional().describe("Rust observability (opentelemetry)"),
219
- rustTemplating: types_exports.RustTemplatingSchema.optional().describe("Rust templating (askama, tera)"),
220
- pythonWebFramework: types_exports.PythonWebFrameworkSchema.optional().describe("Python web framework (fastapi, django)"),
221
- pythonOrm: types_exports.PythonOrmSchema.optional().describe("Python ORM/database (sqlalchemy, sqlmodel)"),
222
- pythonValidation: types_exports.PythonValidationSchema.optional().describe("Python validation (pydantic)"),
223
- pythonAi: z.array(types_exports.PythonAiSchema).optional().describe("Python AI/ML frameworks"),
224
- pythonAuth: types_exports.PythonAuthSchema.optional().describe("Python auth library (authlib, jwt)"),
225
- pythonApi: types_exports.PythonApiSchema.optional().describe("Python API framework (django-rest-framework, django-ninja)"),
226
- pythonTaskQueue: types_exports.PythonTaskQueueSchema.optional().describe("Python task queue (celery)"),
227
- pythonGraphql: types_exports.PythonGraphqlSchema.optional().describe("Python GraphQL framework (strawberry)"),
228
- pythonQuality: types_exports.PythonQualitySchema.optional().describe("Python code quality (ruff, mypy, pyright)"),
229
- pythonTesting: z.array(types_exports.PythonTestingSchema).optional().describe("Python testing libraries (pytest, hypothesis)"),
230
- pythonCaching: types_exports.PythonCachingSchema.optional().describe("Python caching (redis, aiocache)"),
231
- pythonRealtime: types_exports.PythonRealtimeSchema.optional().describe("Python realtime (python-socketio, websockets)"),
232
- pythonObservability: types_exports.PythonObservabilitySchema.optional().describe("Python observability (opentelemetry)"),
233
- pythonCli: z.array(types_exports.PythonCliSchema).optional().describe("Python CLI tooling (typer, click, rich)"),
234
- goWebFramework: types_exports.GoWebFrameworkSchema.optional().describe("Go web framework (gin, echo, fiber)"),
235
- goOrm: types_exports.GoOrmSchema.optional().describe("Go ORM/database (gorm, sqlc)"),
236
- goApi: types_exports.GoApiSchema.optional().describe("Go API layer (grpc-go)"),
237
- goCli: types_exports.GoCliSchema.optional().describe("Go CLI tools (cobra, bubbletea, urfave-cli)"),
238
- goLogging: types_exports.GoLoggingSchema.optional().describe("Go logging (zap, zerolog, slog)"),
239
- goAuth: types_exports.GoAuthSchema.optional().describe("Go auth (casbin, jwt, goth)"),
240
- goTesting: z.array(types_exports.GoTestingSchema).optional().describe("Go testing libraries (testify, gomock)"),
241
- goRealtime: types_exports.GoRealtimeSchema.optional().describe("Go realtime library (gorilla-websocket, centrifuge)"),
242
- goMessageQueue: types_exports.GoMessageQueueSchema.optional().describe("Go message queue (nats, watermill)"),
243
- goCaching: types_exports.GoCachingSchema.optional().describe("Go caching library (redis, ristretto)"),
244
- goConfig: types_exports.GoConfigSchema.optional().describe("Go config management (viper, koanf)"),
245
- goObservability: types_exports.GoObservabilitySchema.optional().describe("Go observability (opentelemetry)"),
246
- javaWebFramework: types_exports.JavaWebFrameworkSchema.optional().describe("Java web framework (spring-boot, quarkus, none)"),
247
- javaBuildTool: types_exports.JavaBuildToolSchema.optional().describe("Java build tool (maven, gradle, none)"),
248
- javaOrm: types_exports.JavaOrmSchema.optional().describe("Java ORM/database (spring-data-jpa)"),
249
- javaAuth: types_exports.JavaAuthSchema.optional().describe("Java auth (spring-security)"),
250
- javaApi: types_exports.JavaApiSchema.optional().describe("Java API layer (spring-graphql)"),
251
- javaLogging: types_exports.JavaLoggingSchema.optional().describe("Java logging (logback, log4j2)"),
252
- javaLibraries: z.array(types_exports.JavaLibrariesSchema).optional().describe("Java application libraries"),
253
- javaTestingLibraries: z.array(types_exports.JavaTestingLibrariesSchema).optional().describe("Java testing libraries"),
254
- dotnetWebFramework: types_exports.DotnetWebFrameworkSchema.optional().describe(".NET web framework (aspnet-minimal, aspnet-mvc, aspnet-blazor, none)"),
255
- dotnetOrm: types_exports.DotnetOrmSchema.optional().describe(".NET data access (ef-core, dapper, linq2db)"),
256
- dotnetAuth: types_exports.DotnetAuthSchema.optional().describe(".NET auth (aspnet-identity, duende-identityserver, auth0-aspnet, none)"),
257
- dotnetApi: types_exports.DotnetApiSchema.optional().describe(".NET API style (minimal-api, graphql-hotchocolate, grpc-dotnet, none)"),
258
- dotnetTesting: z.array(types_exports.DotnetTestingSchema).optional().describe(".NET testing libraries"),
259
- dotnetJobQueue: types_exports.DotnetJobQueueSchema.optional().describe(".NET jobs (hangfire, quartz-net, hosted-services, none)"),
260
- dotnetRealtime: types_exports.DotnetRealtimeSchema.optional().describe(".NET realtime (signalr, none)"),
261
- dotnetObservability: z.array(types_exports.DotnetObservabilitySchema).optional().describe(".NET observability/logging libraries"),
262
- dotnetValidation: types_exports.DotnetValidationSchema.optional().describe(".NET validation (fluentvalidation, data-annotations)"),
263
- dotnetCaching: types_exports.DotnetCachingSchema.optional().describe(".NET caching (redis, memory-cache, none)"),
264
- dotnetDeploy: types_exports.DotnetDeploySchema.optional().describe(".NET deploy target (docker, azure, aws, none)"),
265
- elixirWebFramework: types_exports.ElixirWebFrameworkSchema.optional().describe("Elixir web framework (phoenix, phoenix-live-view, none)"),
266
- elixirOrm: types_exports.ElixirOrmSchema.optional().describe("Elixir ORM/database (ecto, ecto-sql, none)"),
267
- elixirAuth: types_exports.ElixirAuthSchema.optional().describe("Elixir auth (phx-gen-auth, ueberauth, guardian, none)"),
268
- elixirApi: types_exports.ElixirApiSchema.optional().describe("Elixir API layer (rest, absinthe, none)"),
269
- elixirRealtime: types_exports.ElixirRealtimeSchema.optional().describe("Elixir realtime (channels, presence, pubsub, live-view-streams, none)"),
270
- elixirJobs: types_exports.ElixirJobsSchema.optional().describe("Elixir jobs (oban, quantum, none)"),
271
- elixirValidation: types_exports.ElixirValidationSchema.optional().describe("Elixir validation (ecto-changesets, nimble-options, none)"),
272
- elixirHttp: types_exports.ElixirHttpSchema.optional().describe("Elixir HTTP client (req, finch, none)"),
273
- elixirJson: types_exports.ElixirJsonSchema.optional().describe("Elixir JSON library (jason, none)"),
274
- elixirEmail: types_exports.ElixirEmailSchema.optional().describe("Elixir email library (swoosh, none)"),
275
- elixirCaching: types_exports.ElixirCachingSchema.optional().describe("Elixir caching (cachex, nebulex, none)"),
276
- elixirObservability: types_exports.ElixirObservabilitySchema.optional().describe("Elixir observability (telemetry, opentelemetry, prom_ex, none)"),
277
- elixirTesting: types_exports.ElixirTestingSchema.optional().describe("Elixir testing (ex_unit, mox, bypass, wallaby, none)"),
278
- elixirQuality: types_exports.ElixirQualitySchema.optional().describe("Elixir code quality (credo, dialyxir, sobelow, none)"),
279
- elixirDeploy: types_exports.ElixirDeploySchema.optional().describe("Elixir deploy target (docker, fly, gigalixir, mix-release, none)"),
280
- elixirLibraries: z.array(types_exports.ElixirLibrariesSchema).optional().describe("Elixir libraries (broadway, nx)"),
281
- aiDocs: z.array(types_exports.AiDocsSchema).optional().describe("AI documentation files (claude-md, agents-md, cursorrules)")
282
- });
283
- const CreateCommandInputSchema = z.tuple([types_exports.ProjectNameSchema.optional(), CreateCommandOptionsSchema]);
159
+ //#region src/utils/telemetry-settings.ts
160
+ const paths = envPaths("better-fullstack", { suffix: "" });
161
+ const SETTINGS_FILE = "telemetry.json";
162
+ function getSettingsPath() {
163
+ return path.join(paths.data, SETTINGS_FILE);
164
+ }
165
+ function emptySettings() {
166
+ return {
167
+ version: 1,
168
+ noticeShown: false
169
+ };
170
+ }
171
+ async function readTelemetrySettings() {
172
+ const settingsPath = getSettingsPath();
173
+ if (!await fs.pathExists(settingsPath)) return emptySettings();
174
+ try {
175
+ const data = await fs.readJson(settingsPath);
176
+ if (!data || typeof data !== "object") return emptySettings();
177
+ return {
178
+ version: typeof data.version === "number" ? data.version : 1,
179
+ enabled: typeof data.enabled === "boolean" ? data.enabled : void 0,
180
+ noticeShown: data.noticeShown === true
181
+ };
182
+ } catch {
183
+ return emptySettings();
184
+ }
185
+ }
186
+ async function writeTelemetrySettings(settings) {
187
+ await fs.ensureDir(paths.data);
188
+ await fs.writeJson(getSettingsPath(), settings, { spaces: 2 });
189
+ }
190
+ /**
191
+ * The user's explicit persisted preference, or `undefined` when they never chose.
192
+ */
193
+ async function getPersistedTelemetryPreference() {
194
+ return (await readTelemetrySettings()).enabled;
195
+ }
196
+ /**
197
+ * Persist an explicit opt-in/opt-out. Choosing a preference also marks the
198
+ * first-run notice as shown so it never appears afterwards.
199
+ */
200
+ async function setTelemetryPreference(enabled) {
201
+ const settings = await readTelemetrySettings();
202
+ settings.enabled = enabled;
203
+ settings.noticeShown = true;
204
+ await writeTelemetrySettings(settings);
205
+ }
206
+ async function hasTelemetryNoticeBeenShown() {
207
+ return (await readTelemetrySettings()).noticeShown;
208
+ }
209
+ async function markTelemetryNoticeShown() {
210
+ const settings = await readTelemetrySettings();
211
+ settings.noticeShown = true;
212
+ await writeTelemetrySettings(settings);
213
+ }
214
+
215
+ //#endregion
216
+ //#region src/utils/analytics.ts
217
+ const CONVEX_INGEST_URL = "https://curious-elephant-653.convex.site/api/analytics/ingest";
218
+ /**
219
+ * Whether telemetry is explicitly overridden at runtime.
220
+ *
221
+ * Only `BTS_TELEMETRY_DISABLED` is a runtime override: `BTS_TELEMETRY` is inlined
222
+ * by the bundler at build time (see tsdown.config.ts) and therefore acts as a
223
+ * build-time default, not a runtime switch.
224
+ */
225
+ function hasTelemetryEnvOverride() {
226
+ return process.env.BTS_TELEMETRY_DISABLED !== void 0;
227
+ }
228
+ /**
229
+ * Resolve whether telemetry is enabled.
230
+ *
231
+ * Precedence: runtime env override (`BTS_TELEMETRY_DISABLED`) > persisted
232
+ * preference > default. The default honors the build-time `BTS_TELEMETRY` flag
233
+ * (inlined by the bundler, "0" by default) and falls back to enabled when the
234
+ * flag is unset (e.g. running from source).
235
+ *
236
+ * `BTS_TELEMETRY` is intentionally evaluated last: the bundler replaces it with
237
+ * a literal, so an early `!== undefined` check would always short-circuit and
238
+ * make the persisted preference unreachable in the shipped CLI.
239
+ */
240
+ async function isTelemetryEnabled() {
241
+ const disabled = process.env.BTS_TELEMETRY_DISABLED;
242
+ if (disabled !== void 0) return disabled !== "1";
243
+ const persisted = await getPersistedTelemetryPreference();
244
+ if (persisted !== void 0) return persisted;
245
+ const buildDefault = "1";
246
+ return buildDefault === void 0 ? true : buildDefault === "1";
247
+ }
248
+ /**
249
+ * Print a one-time notice describing the anonymous telemetry the CLI collects
250
+ * and how to opt out, then remember that it was shown so it never repeats.
251
+ *
252
+ * No-ops when telemetry is explicitly configured via env var, when a persisted
253
+ * preference already exists, when the notice was already shown, when telemetry
254
+ * is disabled by the build default, or when the CLI is not running interactively
255
+ * (CI / silent / non-TTY).
256
+ */
257
+ async function maybeShowTelemetryNotice() {
258
+ if (hasTelemetryEnvOverride()) return;
259
+ if (!canPromptInteractively()) return;
260
+ if (await getPersistedTelemetryPreference() !== void 0) return;
261
+ if (await hasTelemetryNoticeBeenShown()) return;
262
+ if (!await isTelemetryEnabled()) return;
263
+ log.info(`${pc.bold("Anonymous usage telemetry is enabled.")}\n${pc.dim("We collect your selected stack options (e.g. frontend, backend, database),")}\n${pc.dim("plus CLI version, Node.js version, and OS platform — never project names,")}\n${pc.dim("file paths, or any personal data.")}\nOpt out anytime with ${pc.cyan("create-better-fullstack telemetry disable")} or ${pc.cyan("BTS_TELEMETRY_DISABLED=1")}.`);
264
+ try {
265
+ await markTelemetryNoticeShown();
266
+ } catch {}
267
+ }
268
+ async function sendConvexEvent(payload) {
269
+ try {
270
+ await fetch(CONVEX_INGEST_URL, {
271
+ method: "POST",
272
+ headers: { "Content-Type": "application/json" },
273
+ body: JSON.stringify(payload)
274
+ });
275
+ } catch {}
276
+ }
277
+ async function trackProjectCreation(config, disableAnalytics = false) {
278
+ if (disableAnalytics || !await isTelemetryEnabled()) return;
279
+ const { projectName: _projectName, projectDir: _projectDir, relativePath: _relativePath, ...safeConfig } = config;
280
+ try {
281
+ await sendConvexEvent({
282
+ ...safeConfig,
283
+ cli_version: getLatestCLIVersion(),
284
+ node_version: typeof process !== "undefined" ? process.version : "",
285
+ platform: typeof process !== "undefined" ? process.platform : ""
286
+ });
287
+ } catch {}
288
+ }
289
+
290
+ //#endregion
291
+ //#region src/commands/telemetry.ts
292
+ const COLLECTED = ["Selected stack options (frontend, backend, database, ORM, auth, API, etc.)", "CLI version, Node.js version, and OS platform"];
293
+ const NOT_COLLECTED = ["Project names, directory paths, or file contents", "Personal or otherwise identifying information"];
294
+ function resolveSource(persisted) {
295
+ if (hasTelemetryEnvOverride()) return "env";
296
+ if (persisted !== void 0) return "preference";
297
+ return "default";
298
+ }
299
+ function describeSource(source) {
300
+ switch (source) {
301
+ case "env": return "environment variable (BTS_TELEMETRY_DISABLED)";
302
+ case "preference": return "saved preference (create-better-fullstack telemetry enable/disable)";
303
+ default: return "default";
304
+ }
305
+ }
306
+ async function telemetryHandler(input) {
307
+ if (input.action === "enable") {
308
+ await setTelemetryPreference(true);
309
+ log.success(pc.green("Telemetry enabled. Thanks for helping improve Better Fullstack."));
310
+ return;
311
+ }
312
+ if (input.action === "disable") {
313
+ await setTelemetryPreference(false);
314
+ log.success(pc.green("Telemetry disabled. No anonymous usage data will be sent."));
315
+ return;
316
+ }
317
+ const enabled = await isTelemetryEnabled();
318
+ const persisted = await getPersistedTelemetryPreference();
319
+ const source = resolveSource(persisted);
320
+ if (input.json) {
321
+ console.log(JSON.stringify({
322
+ enabled,
323
+ source,
324
+ persisted: persisted ?? null,
325
+ envOverride: hasTelemetryEnvOverride()
326
+ }, null, 2));
327
+ return;
328
+ }
329
+ renderTitle();
330
+ intro(pc.magenta("Telemetry"));
331
+ log.message(`${pc.bold("Status:")} ${enabled ? pc.green("enabled") : pc.yellow("disabled")} ${pc.dim(`(${describeSource(source)})`)}`);
332
+ log.message("");
333
+ log.message(pc.bold("What is sent when enabled:"));
334
+ for (const item of COLLECTED) log.message(` ${pc.green("+")} ${item}`);
335
+ log.message(pc.bold("What is never sent:"));
336
+ for (const item of NOT_COLLECTED) log.message(` ${pc.red("-")} ${item}`);
337
+ log.message("");
338
+ log.message(pc.dim(`Change anytime: ${pc.cyan("create-better-fullstack telemetry enable")} / ${pc.cyan("create-better-fullstack telemetry disable")} (or set ${pc.cyan("BTS_TELEMETRY_DISABLED=1")}).`));
339
+ }
284
340
 
285
341
  //#endregion
286
342
  //#region src/prompts/prompt-contract.ts
@@ -400,7 +456,7 @@ const AI_DOCS_OPTIONS = [
400
456
  },
401
457
  {
402
458
  value: "agents-md",
403
- label: "Agents.md",
459
+ label: "AGENTS.md",
404
460
  hint: "Generic AI assistant documentation"
405
461
  },
406
462
  {
@@ -531,6 +587,11 @@ const API_PROMPT_OPTION_MAP = {
531
587
  label: "GraphQL Yoga",
532
588
  hint: "Batteries-included GraphQL server with Pothos schema builder"
533
589
  },
590
+ "apollo-server": {
591
+ value: "apollo-server",
592
+ label: "Apollo Server",
593
+ hint: "Spec-compliant GraphQL server with a schema-first starter"
594
+ },
534
595
  openapi: {
535
596
  value: "openapi",
536
597
  label: "OpenAPI",
@@ -899,6 +960,11 @@ const CMS_PROMPT_OPTIONS = [
899
960
  label: "Directus",
900
961
  hint: "Open data platform and headless CMS for SQL databases"
901
962
  },
963
+ {
964
+ value: "keystatic",
965
+ label: "Keystatic",
966
+ hint: "Git-backed CMS for Markdown, JSON, and YAML content"
967
+ },
902
968
  {
903
969
  value: "none",
904
970
  label: "None",
@@ -2361,7 +2427,7 @@ async function getFrontendChoice(frontendOptions, backend, auth) {
2361
2427
  });
2362
2428
  if (isGoBack(frontendTypes)) return GO_BACK_SYMBOL;
2363
2429
  if (isCancel$1(frontendTypes)) return exitCancelled("Operation cancelled");
2364
- setIsFirstPrompt(false);
2430
+ setIsFirstPrompt$1(false);
2365
2431
  const result = [];
2366
2432
  let shouldRestart = false;
2367
2433
  if (frontendTypes.includes("web")) {
@@ -2377,7 +2443,7 @@ async function getFrontendChoice(frontendOptions, backend, auth) {
2377
2443
  else result.push(webFramework);
2378
2444
  }
2379
2445
  if (shouldRestart) {
2380
- setIsFirstPrompt(wasFirstPrompt);
2446
+ setIsFirstPrompt$1(wasFirstPrompt);
2381
2447
  continue;
2382
2448
  }
2383
2449
  if (frontendTypes.includes("native")) {
@@ -2390,14 +2456,14 @@ async function getFrontendChoice(frontendOptions, backend, auth) {
2390
2456
  });
2391
2457
  if (isGoBack(nativeFramework)) if (frontendTypes.includes("web")) shouldRestart = true;
2392
2458
  else {
2393
- setIsFirstPrompt(wasFirstPrompt);
2459
+ setIsFirstPrompt$1(wasFirstPrompt);
2394
2460
  continue;
2395
2461
  }
2396
2462
  else if (isCancel$1(nativeFramework)) return exitCancelled("Operation cancelled");
2397
2463
  else result.push(nativeFramework);
2398
2464
  }
2399
2465
  if (shouldRestart) {
2400
- setIsFirstPrompt(wasFirstPrompt);
2466
+ setIsFirstPrompt$1(wasFirstPrompt);
2401
2467
  continue;
2402
2468
  }
2403
2469
  return result;
@@ -2832,17 +2898,36 @@ async function getGoObservabilityChoice(goObservability) {
2832
2898
 
2833
2899
  //#endregion
2834
2900
  //#region src/prompts/i18n.ts
2901
+ const PARAGLIDE_FRONTENDS = new Set([
2902
+ "next",
2903
+ "nuxt",
2904
+ "vinext",
2905
+ "tanstack-router",
2906
+ "tanstack-start",
2907
+ "react-router",
2908
+ "react-vite",
2909
+ "svelte",
2910
+ "solid",
2911
+ "solid-start",
2912
+ "astro"
2913
+ ]);
2835
2914
  async function getI18nChoice(i18n, frontend) {
2836
2915
  if (i18n !== void 0) return i18n;
2916
+ const hasNext = frontend?.includes("next") ?? false;
2837
2917
  const response = await navigableSelect({
2838
2918
  message: "Select internationalization (i18n) library",
2839
2919
  options: [
2920
+ ...frontend?.some((f) => PARAGLIDE_FRONTENDS.has(f)) ?? false ? [{
2921
+ value: "paraglide",
2922
+ label: "Paraglide",
2923
+ hint: "Type-safe, compiler-based i18n for modern web frontends"
2924
+ }] : [],
2840
2925
  {
2841
2926
  value: "i18next",
2842
2927
  label: "i18next",
2843
2928
  hint: "Full-featured i18n framework, works with all frontends"
2844
2929
  },
2845
- ...frontend?.includes("next") ?? false ? [{
2930
+ ...hasNext ? [{
2846
2931
  value: "next-intl",
2847
2932
  label: "next-intl",
2848
2933
  hint: "Lightweight i18n for Next.js with App Router support"
@@ -2859,17 +2944,6 @@ async function getI18nChoice(i18n, frontend) {
2859
2944
  return response;
2860
2945
  }
2861
2946
 
2862
- //#endregion
2863
- //#region src/utils/command-exists.ts
2864
- async function commandExists(command) {
2865
- try {
2866
- if (process.platform === "win32") return (await $({ reject: false })`where ${command}`).exitCode === 0;
2867
- return (await $({ reject: false })`which ${command}`).exitCode === 0;
2868
- } catch {
2869
- return false;
2870
- }
2871
- }
2872
-
2873
2947
  //#endregion
2874
2948
  //#region src/prompts/install.ts
2875
2949
  async function getinstallChoice(install, ecosystem, javaBuildTool) {
@@ -5618,7 +5692,7 @@ async function navigableGroup(prompts, opts) {
5618
5692
  while (currentIndex < promptNames.length) {
5619
5693
  const name = promptNames[currentIndex];
5620
5694
  const prompt = prompts[name];
5621
- setIsFirstPrompt$1(currentIndex === 0);
5695
+ setIsFirstPrompt(currentIndex === 0);
5622
5696
  setLastPromptShownUI(false);
5623
5697
  const result = await prompt({ results })?.catch((e) => {
5624
5698
  throw e;
@@ -5639,7 +5713,7 @@ async function navigableGroup(prompts, opts) {
5639
5713
  results[name] = "canceled";
5640
5714
  opts.onCancel({ results });
5641
5715
  }
5642
- setIsFirstPrompt$1(false);
5716
+ setIsFirstPrompt(false);
5643
5717
  return results;
5644
5718
  }
5645
5719
  if (goingBack && !didLastPromptShowUI()) {
@@ -5654,7 +5728,7 @@ async function navigableGroup(prompts, opts) {
5654
5728
  results[name] = result;
5655
5729
  currentIndex++;
5656
5730
  }
5657
- setIsFirstPrompt$1(false);
5731
+ setIsFirstPrompt(false);
5658
5732
  return results;
5659
5733
  }
5660
5734
 
@@ -6095,6 +6169,11 @@ const SEARCH_PROMPT_OPTIONS = [
6095
6169
  label: "Elasticsearch",
6096
6170
  hint: "Distributed search and analytics engine with local and cloud deployments"
6097
6171
  },
6172
+ {
6173
+ value: "opensearch",
6174
+ label: "OpenSearch",
6175
+ hint: "Open-source search and analytics suite compatible with Elasticsearch APIs"
6176
+ },
6098
6177
  {
6099
6178
  value: "algolia",
6100
6179
  label: "Algolia",
@@ -6149,6 +6228,77 @@ async function getSearchChoice(search, backend, ecosystem) {
6149
6228
  return response;
6150
6229
  }
6151
6230
 
6231
+ //#endregion
6232
+ //#region src/prompts/vector-db.ts
6233
+ const VECTOR_DB_PROMPT_OPTIONS = [
6234
+ {
6235
+ value: "pgvector",
6236
+ label: "pgvector",
6237
+ hint: "Self-hosted Postgres + pgvector extension for embeddings"
6238
+ },
6239
+ {
6240
+ value: "qdrant",
6241
+ label: "Qdrant",
6242
+ hint: "High-performance open-source vector database"
6243
+ },
6244
+ {
6245
+ value: "chroma",
6246
+ label: "Chroma",
6247
+ hint: "Lightweight open-source embedding database"
6248
+ },
6249
+ {
6250
+ value: "pinecone",
6251
+ label: "Pinecone",
6252
+ hint: "Fully managed serverless vector database"
6253
+ },
6254
+ {
6255
+ value: "none",
6256
+ label: "None",
6257
+ hint: "Skip vector database setup"
6258
+ }
6259
+ ];
6260
+ /**
6261
+ * Vector DB is a TypeScript-ecosystem feature backed by a standalone server.
6262
+ * Every provider (including pgvector via a dedicated Postgres instance) is a
6263
+ * separate service, so there is no dependency on the primary database choice.
6264
+ */
6265
+ function resolveVectorDbPrompt(context = {}) {
6266
+ const skip = () => ({
6267
+ shouldPrompt: false,
6268
+ mode: "single",
6269
+ options: [],
6270
+ autoValue: "none"
6271
+ });
6272
+ if (context.ecosystem && context.ecosystem !== "typescript") return skip();
6273
+ if (context.backend === "none" || context.backend === "convex") return skip();
6274
+ return context.vectorDb !== void 0 ? {
6275
+ shouldPrompt: false,
6276
+ mode: "single",
6277
+ options: VECTOR_DB_PROMPT_OPTIONS,
6278
+ autoValue: context.vectorDb
6279
+ } : {
6280
+ shouldPrompt: true,
6281
+ mode: "single",
6282
+ options: VECTOR_DB_PROMPT_OPTIONS,
6283
+ initialValue: "none"
6284
+ };
6285
+ }
6286
+ async function getVectorDbChoice(vectorDb, backend, ecosystem) {
6287
+ const resolution = resolveVectorDbPrompt({
6288
+ vectorDb,
6289
+ backend,
6290
+ ecosystem
6291
+ });
6292
+ if (!resolution.shouldPrompt) return resolution.autoValue ?? "none";
6293
+ const response = await navigableSelect({
6294
+ message: "Select vector database",
6295
+ options: resolution.options,
6296
+ initialValue: resolution.initialValue
6297
+ });
6298
+ if (isCancel$1(response)) return exitCancelled("Operation cancelled");
6299
+ return response;
6300
+ }
6301
+
6152
6302
  //#endregion
6153
6303
  //#region src/prompts/server-deploy.ts
6154
6304
  async function getServerDeploymentChoice(deployment, runtime, backend, _webDeploy) {
@@ -6442,7 +6592,7 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
6442
6592
  },
6443
6593
  addons: ({ results }) => {
6444
6594
  if (results.ecosystem !== "typescript") {
6445
- const nonTypeScriptAddons = (flags.addons ?? []).filter((addon) => addon === "docker-compose");
6595
+ const nonTypeScriptAddons = (flags.addons ?? []).filter((addon) => addon === "docker-compose" || addon === "devcontainer" || addon === "github-actions");
6446
6596
  return Promise.resolve(nonTypeScriptAddons);
6447
6597
  }
6448
6598
  return getAddonsChoice(flags.addons, results.frontend, results.auth, results.backend, results.runtime);
@@ -6536,6 +6686,10 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
6536
6686
  if (results.ecosystem === "react-native" || results.ecosystem === "elixir") return Promise.resolve("none");
6537
6687
  return getSearchChoice(flags.search, results.backend, results.ecosystem);
6538
6688
  },
6689
+ vectorDb: ({ results }) => {
6690
+ if (results.ecosystem !== "typescript") return Promise.resolve("none");
6691
+ return getVectorDbChoice(flags.vectorDb, results.backend, results.ecosystem);
6692
+ },
6539
6693
  fileStorage: ({ results }) => {
6540
6694
  if (results.ecosystem !== "typescript") return Promise.resolve("none");
6541
6695
  return getFileStorageChoice(flags.fileStorage, results.backend);
@@ -6943,6 +7097,7 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
6943
7097
  rateLimit: result.rateLimit,
6944
7098
  i18n: result.i18n,
6945
7099
  search: result.search,
7100
+ vectorDb: result.vectorDb,
6946
7101
  fileStorage: result.fileStorage,
6947
7102
  mobileNavigation: result.mobileNavigation,
6948
7103
  mobileUI: result.mobileUI,
@@ -7111,35 +7266,76 @@ async function getVersionChannelChoice(versionChannel) {
7111
7266
  }
7112
7267
 
7113
7268
  //#endregion
7114
- //#region src/utils/analytics.ts
7115
- const CONVEX_INGEST_URL = "https://curious-elephant-653.convex.site/api/analytics/ingest";
7116
- function isTelemetryEnabled() {
7117
- const disabled = process.env.BTS_TELEMETRY_DISABLED;
7118
- if (disabled !== void 0) return disabled !== "1";
7119
- const enabled = "1";
7120
- if (enabled !== void 0) return enabled === "1";
7121
- return true;
7269
+ //#region src/utils/config-source.ts
7270
+ /**
7271
+ * CreateInput flag keys that can be sourced from a stored config. `projectName`
7272
+ * is excluded so a replayed config never reuses the original project name.
7273
+ */
7274
+ const COPYABLE_CREATE_INPUT_KEYS = Object.keys(types_exports.CreateInputSchema.shape).filter((key) => key !== "projectName");
7275
+ /**
7276
+ * Projects a persisted Better-Fullstack config (bts.jsonc shape) onto the
7277
+ * subset of `create` flags it can drive. Only defined values are copied so the
7278
+ * result can be safely overlaid by explicitly-passed CLI flags.
7279
+ */
7280
+ function betterTStackConfigToCreateInput(config) {
7281
+ const source = config;
7282
+ const result = {};
7283
+ for (const key of COPYABLE_CREATE_INPUT_KEYS) {
7284
+ const value = source[key];
7285
+ if (value !== void 0 && value !== null) result[key] = value;
7286
+ }
7287
+ return result;
7122
7288
  }
7123
- async function sendConvexEvent(payload) {
7124
- try {
7125
- await fetch(CONVEX_INGEST_URL, {
7126
- method: "POST",
7127
- headers: { "Content-Type": "application/json" },
7128
- body: JSON.stringify(payload)
7129
- });
7130
- } catch {}
7289
+ /**
7290
+ * Maps a legacy history entry (written before the full config snapshot existed)
7291
+ * onto `create` flags using the limited stack summary it stored.
7292
+ */
7293
+ function historyStackToCreateInput(stack) {
7294
+ return {
7295
+ frontend: stack.frontend,
7296
+ backend: stack.backend,
7297
+ database: stack.database,
7298
+ orm: stack.orm,
7299
+ runtime: stack.runtime,
7300
+ auth: stack.auth,
7301
+ payments: stack.payments,
7302
+ api: stack.api,
7303
+ addons: stack.addons,
7304
+ examples: stack.examples,
7305
+ dbSetup: stack.dbSetup,
7306
+ packageManager: stack.packageManager
7307
+ };
7131
7308
  }
7132
- async function trackProjectCreation(config, disableAnalytics = false) {
7133
- if (!isTelemetryEnabled() || disableAnalytics) return;
7134
- const { projectName: _projectName, projectDir: _projectDir, relativePath: _relativePath, ...safeConfig } = config;
7135
- try {
7136
- await sendConvexEvent({
7137
- ...safeConfig,
7138
- cli_version: getLatestCLIVersion(),
7139
- node_version: typeof process !== "undefined" ? process.version : "",
7140
- platform: typeof process !== "undefined" ? process.platform : ""
7141
- });
7142
- } catch {}
7309
+ /**
7310
+ * Resolves the `--from-history` / `--config` flags into a base set of `create`
7311
+ * flags. Returns `undefined` when neither flag is used. The two flags are
7312
+ * mutually exclusive, and both are incompatible with `--yes`, `--template`, and
7313
+ * `--part` (each of those also wants to own the full stack). On any user error
7314
+ * this calls `exitWithError`, which exits the CLI (or throws a CLIError in
7315
+ * silent/programmatic mode).
7316
+ */
7317
+ async function resolveCreateConfigBase(input) {
7318
+ const hasHistory = input.fromHistory !== void 0;
7319
+ const hasConfig = input.config !== void 0 && input.config !== "";
7320
+ if (!hasHistory && !hasConfig) return;
7321
+ if (hasHistory && hasConfig) exitWithError("Cannot combine --from-history with --config. Pass only one config source.");
7322
+ const sourceFlag = hasHistory ? "--from-history" : "--config";
7323
+ if (input.yes) exitWithError(`Cannot combine --yes with ${sourceFlag}: the config already provides a complete stack. Remove --yes.`);
7324
+ if (input.template && input.template !== "none") exitWithError(`Cannot combine --template with ${sourceFlag}. Choose either a template or a saved config as the base.`);
7325
+ if (input.part && input.part.length > 0) exitWithError(`Cannot combine --part with ${sourceFlag}. Use stack parts or a saved config as the base, not both.`);
7326
+ if (hasConfig) {
7327
+ const loaded = await readBtsConfigFromFile(input.config);
7328
+ if (!loaded) exitWithError(`Could not load config file: ${input.config}. Ensure the path points to a valid bts.jsonc/JSON config.`);
7329
+ return betterTStackConfigToCreateInput(loaded);
7330
+ }
7331
+ const position = input.fromHistory;
7332
+ if (!Number.isInteger(position) || position < 1) exitWithError(`Invalid --from-history value: ${position}. Provide a positive integer (1 = most recent).`);
7333
+ const entry = await getHistoryEntry(position);
7334
+ if (!entry) {
7335
+ const count = await getHistoryCount();
7336
+ exitWithError(`No project history entry at position ${position}.${count === 0 ? " Project history is empty." : ` History has ${count} ${count === 1 ? "entry" : "entries"} (use 1-${count}).`}`);
7337
+ }
7338
+ return entry.config ? betterTStackConfigToCreateInput(entry.config) : historyStackToCreateInput(entry.stack);
7143
7339
  }
7144
7340
 
7145
7341
  //#endregion
@@ -7284,480 +7480,6 @@ function displayConfig(config) {
7284
7480
  return configDisplay.join("\n");
7285
7481
  }
7286
7482
 
7287
- //#endregion
7288
- //#region src/utils/generate-reproducible-command.ts
7289
- function getBaseCommand(packageManager) {
7290
- switch (packageManager) {
7291
- case "bun": return "bun create better-fullstack@latest";
7292
- case "pnpm": return "pnpm create better-fullstack@latest";
7293
- case "yarn": return "yarn create better-fullstack@latest";
7294
- case "npm":
7295
- default: return "npx create-better-fullstack@latest";
7296
- }
7297
- }
7298
- function formatArrayFlag(flag, values) {
7299
- const normalizedValues = values.filter((value) => value !== "none");
7300
- if (normalizedValues.length === 0) return `--${flag} none`;
7301
- return `--${flag} ${normalizedValues.join(" ")}`;
7302
- }
7303
- function appendCommonFlags(flags, config) {
7304
- if (config.aiDocs && config.aiDocs.length > 0) flags.push(formatArrayFlag("ai-docs", config.aiDocs));
7305
- else flags.push("--ai-docs none");
7306
- flags.push(config.git ? "--git" : "--no-git");
7307
- flags.push(`--package-manager ${config.packageManager}`);
7308
- if (config.versionChannel !== "stable") flags.push(`--version-channel ${config.versionChannel}`);
7309
- flags.push(config.install ? "--install" : "--no-install");
7310
- }
7311
- function hasGraphPrimaryPart(config, role, ecosystem) {
7312
- return config.stackParts?.some((part) => part.source !== "provided" && part.role === role && !part.ownerPartId && (!ecosystem || part.ecosystem === ecosystem));
7313
- }
7314
- function hasOwnedGraphPart(config, ownerRole, role, ecosystem, toolId) {
7315
- const owner = config.stackParts?.find((part) => part.source !== "provided" && part.role === ownerRole && !part.ownerPartId && (!ecosystem || part.ecosystem === ecosystem));
7316
- if (!owner) return false;
7317
- return Boolean(config.stackParts?.some((part) => part.source !== "provided" && part.role === role && part.ownerPartId === owner.id && (!ecosystem || part.ecosystem === ecosystem) && (!toolId || part.toolId === toolId)));
7318
- }
7319
- function hasGraphAddonPart(config, addon) {
7320
- const binding = (0, types_exports.getAddonStackPartBinding)(addon);
7321
- if (!binding) return false;
7322
- if (!binding.ownerRole) return Boolean(config.stackParts?.some((part) => part.source !== "provided" && part.role === binding.role && part.ecosystem === binding.ecosystem && part.toolId === addon && !part.ownerPartId));
7323
- const owner = config.stackParts?.find((part) => part.source !== "provided" && part.role === binding.ownerRole && !part.ownerPartId && part.ecosystem === binding.ecosystem);
7324
- if (!owner) return false;
7325
- return Boolean(config.stackParts?.some((part) => part.source !== "provided" && part.role === binding.role && part.ecosystem === binding.ecosystem && part.toolId === addon && part.ownerPartId === owner.id));
7326
- }
7327
- function hasGraphExamplePart(config, example) {
7328
- return Boolean(config.stackParts?.some((part) => part.source !== "provided" && part.role === "examples" && part.ecosystem === "universal" && part.toolId === example && !part.ownerPartId));
7329
- }
7330
- function hasGraphArrayParts(config, values, hasPart) {
7331
- const normalizedValues = values.filter((value) => value !== "none");
7332
- return normalizedValues.length > 0 && normalizedValues.every((value) => hasPart(config, value));
7333
- }
7334
- function appendChangedStringFlag(flags, flag, value, defaultValue) {
7335
- if (value !== defaultValue) flags.push(`--${flag} ${value}`);
7336
- }
7337
- function appendChangedGraphStringFlag(flags, config, role, ecosystem, flag, value, defaultValue) {
7338
- if (hasGraphPart(config, role, ecosystem)) return;
7339
- appendChangedStringFlag(flags, flag, value, defaultValue);
7340
- }
7341
- function appendChangedOwnedGraphStringFlag(flags, config, ownerRole, role, ecosystem, flag, value, defaultValue) {
7342
- if (hasOwnedGraphPart(config, ownerRole, role, ecosystem, value)) return;
7343
- appendChangedStringFlag(flags, flag, value, defaultValue);
7344
- }
7345
- function hasOwnedGraphArrayParts(config, ownerRole, role, ecosystem, values) {
7346
- const normalizedValues = values.filter((value) => value !== "none");
7347
- if (normalizedValues.length === 0) return false;
7348
- return normalizedValues.every((value) => hasOwnedGraphPart(config, ownerRole, role, ecosystem, value));
7349
- }
7350
- function appendChangedOwnedGraphArrayFlag(flags, config, ownerRole, role, ecosystem, flag, values, defaultValues) {
7351
- if (hasOwnedGraphArrayParts(config, ownerRole, role, ecosystem, values)) return;
7352
- appendChangedArrayFlag(flags, flag, values, defaultValues);
7353
- }
7354
- function appendChangedArrayFlag(flags, flag, values, defaultValues) {
7355
- if (values.length !== defaultValues.length || values.some((value, index) => value !== defaultValues[index])) flags.push(formatArrayFlag(flag, values));
7356
- }
7357
- function appendAstroIntegrationFlag(flags, config) {
7358
- if (config.frontend.includes("astro") && config.astroIntegration !== "none") flags.push(`--astro-integration ${config.astroIntegration}`);
7359
- }
7360
- function appendGraphExtraFlags(flags, config) {
7361
- if (!hasGraphArrayParts(config, config.addons, hasGraphAddonPart)) appendChangedArrayFlag(flags, "addons", config.addons, ["turborepo"]);
7362
- if (!hasGraphArrayParts(config, config.examples, hasGraphExamplePart)) appendChangedArrayFlag(flags, "examples", config.examples, []);
7363
- if (hasGraphPrimaryPart(config, "database")) appendChangedOwnedGraphStringFlag(flags, config, "database", "dbSetup", "universal", "db-setup", config.dbSetup, "none");
7364
- else appendChangedStringFlag(flags, "db-setup", config.dbSetup, "none");
7365
- if (hasGraphPrimaryPart(config, "frontend", "typescript")) {
7366
- appendAstroIntegrationFlag(flags, config);
7367
- appendChangedOwnedGraphStringFlag(flags, config, "frontend", "deploy", "typescript", "web-deploy", config.webDeploy, "none");
7368
- appendChangedGraphStringFlag(flags, config, "css", "typescript", "css-framework", config.cssFramework, "tailwind");
7369
- appendChangedGraphStringFlag(flags, config, "ui", "typescript", "ui-library", config.uiLibrary, "shadcn-ui");
7370
- if (config.uiLibrary === "shadcn-ui") {
7371
- appendChangedStringFlag(flags, "shadcn-base", config.shadcnBase ?? "radix", "radix");
7372
- appendChangedStringFlag(flags, "shadcn-style", config.shadcnStyle ?? "nova", "nova");
7373
- appendChangedStringFlag(flags, "shadcn-icon-library", config.shadcnIconLibrary ?? "lucide", "lucide");
7374
- appendChangedStringFlag(flags, "shadcn-color-theme", config.shadcnColorTheme ?? "neutral", "neutral");
7375
- appendChangedStringFlag(flags, "shadcn-base-color", config.shadcnBaseColor ?? "neutral", "neutral");
7376
- appendChangedStringFlag(flags, "shadcn-font", config.shadcnFont ?? "inter", "inter");
7377
- appendChangedStringFlag(flags, "shadcn-radius", config.shadcnRadius ?? "default", "default");
7378
- }
7379
- appendChangedGraphStringFlag(flags, config, "stateManagement", "typescript", "state-management", config.stateManagement, "none");
7380
- appendChangedGraphStringFlag(flags, config, "forms", "typescript", "forms", config.forms, "react-hook-form");
7381
- appendChangedStringFlag(flags, "validation", config.validation, "zod");
7382
- appendChangedStringFlag(flags, "testing", config.testing, "vitest");
7383
- appendChangedGraphStringFlag(flags, config, "animation", "typescript", "animation", config.animation, "none");
7384
- appendChangedGraphStringFlag(flags, config, "fileUpload", "typescript", "file-upload", config.fileUpload, "none");
7385
- appendChangedGraphStringFlag(flags, config, "i18n", "typescript", "i18n", config.i18n, "none");
7386
- appendChangedGraphStringFlag(flags, config, "analytics", "typescript", "analytics", config.analytics, "none");
7387
- }
7388
- if (hasGraphPrimaryPart(config, "frontend", "typescript") || hasGraphPrimaryPart(config, "backend", "typescript")) {
7389
- if (hasGraphPrimaryPart(config, "backend", "typescript")) {
7390
- appendChangedOwnedGraphStringFlag(flags, config, "backend", "runtime", "typescript", "runtime", config.runtime, "bun");
7391
- appendChangedOwnedGraphStringFlag(flags, config, "backend", "deploy", "typescript", "server-deploy", config.serverDeploy, "none");
7392
- } else appendChangedStringFlag(flags, "server-deploy", config.serverDeploy, "none");
7393
- appendChangedGraphStringFlag(flags, config, "payments", "typescript", "payments", config.payments, "none");
7394
- appendChangedGraphStringFlag(flags, config, "email", "typescript", "email", config.email, "none");
7395
- appendChangedStringFlag(flags, "effect", config.effect, "none");
7396
- appendChangedGraphStringFlag(flags, config, "ai", "typescript", "ai", config.ai, "none");
7397
- appendChangedGraphStringFlag(flags, config, "realtime", "typescript", "realtime", config.realtime, "none");
7398
- appendChangedGraphStringFlag(flags, config, "jobQueue", "typescript", "job-queue", config.jobQueue, "none");
7399
- appendChangedGraphStringFlag(flags, config, "logging", "typescript", "logging", config.logging, "none");
7400
- appendChangedGraphStringFlag(flags, config, "observability", "typescript", "observability", config.observability, "none");
7401
- appendChangedGraphStringFlag(flags, config, "featureFlags", "typescript", "feature-flags", config.featureFlags, "none");
7402
- appendChangedGraphStringFlag(flags, config, "caching", "typescript", "caching", config.caching, "none");
7403
- appendChangedGraphStringFlag(flags, config, "rateLimit", "typescript", "rate-limit", config.rateLimit, "none");
7404
- appendChangedGraphStringFlag(flags, config, "cms", "typescript", "cms", config.cms, "none");
7405
- appendChangedGraphStringFlag(flags, config, "search", "typescript", "search", config.search, "none");
7406
- appendChangedGraphStringFlag(flags, config, "fileStorage", "typescript", "file-storage", config.fileStorage, "none");
7407
- }
7408
- if (hasGraphPrimaryPart(config, "mobile")) {
7409
- appendChangedOwnedGraphStringFlag(flags, config, "mobile", "navigation", "react-native", "mobile-navigation", config.mobileNavigation, "expo-router");
7410
- appendChangedOwnedGraphStringFlag(flags, config, "mobile", "ui", "react-native", "mobile-ui", config.mobileUI, "none");
7411
- appendChangedOwnedGraphStringFlag(flags, config, "mobile", "storage", "react-native", "mobile-storage", config.mobileStorage, "none");
7412
- appendChangedOwnedGraphStringFlag(flags, config, "mobile", "testing", "react-native", "mobile-testing", config.mobileTesting, "none");
7413
- appendChangedStringFlag(flags, "mobile-push", config.mobilePush, "none");
7414
- appendChangedStringFlag(flags, "mobile-ota", config.mobileOTA, "none");
7415
- appendChangedStringFlag(flags, "mobile-deep-linking", config.mobileDeepLinking, "none");
7416
- }
7417
- if (hasGraphPrimaryPart(config, "frontend", "rust")) appendChangedStringFlag(flags, "rust-frontend", config.rustFrontend, "none");
7418
- if (hasGraphPrimaryPart(config, "backend", "rust")) {
7419
- appendChangedOwnedGraphStringFlag(flags, config, "backend", "cli", "rust", "rust-cli", config.rustCli, "none");
7420
- appendChangedOwnedGraphArrayFlag(flags, config, "backend", "libraries", "rust", "rust-libraries", config.rustLibraries, []);
7421
- appendChangedOwnedGraphStringFlag(flags, config, "backend", "logging", "rust", "rust-logging", config.rustLogging, "tracing");
7422
- appendChangedOwnedGraphStringFlag(flags, config, "backend", "errorHandling", "rust", "rust-error-handling", config.rustErrorHandling, "anyhow-thiserror");
7423
- appendChangedOwnedGraphStringFlag(flags, config, "backend", "caching", "rust", "rust-caching", config.rustCaching, "none");
7424
- }
7425
- if (hasGraphPrimaryPart(config, "backend", "python")) {
7426
- appendChangedOwnedGraphStringFlag(flags, config, "backend", "validation", "python", "python-validation", config.pythonValidation, "none");
7427
- appendChangedOwnedGraphArrayFlag(flags, config, "backend", "ai", "python", "python-ai", config.pythonAi, []);
7428
- appendChangedOwnedGraphStringFlag(flags, config, "backend", "jobQueue", "python", "python-task-queue", config.pythonTaskQueue, "none");
7429
- appendChangedOwnedGraphStringFlag(flags, config, "backend", "api", "python", "python-graphql", config.pythonGraphql, "none");
7430
- appendChangedOwnedGraphStringFlag(flags, config, "backend", "codeQuality", "python", "python-quality", config.pythonQuality, "none");
7431
- }
7432
- if (hasGraphPrimaryPart(config, "backend", "go")) {
7433
- appendChangedOwnedGraphStringFlag(flags, config, "backend", "cli", "go", "go-cli", config.goCli, "none");
7434
- appendChangedOwnedGraphStringFlag(flags, config, "backend", "logging", "go", "go-logging", config.goLogging, "none");
7435
- }
7436
- if (hasGraphPrimaryPart(config, "backend", "java")) {
7437
- appendChangedOwnedGraphStringFlag(flags, config, "backend", "buildTool", "java", "java-build-tool", config.javaBuildTool, "maven");
7438
- appendChangedOwnedGraphArrayFlag(flags, config, "backend", "libraries", "java", "java-libraries", config.javaLibraries, []);
7439
- appendChangedOwnedGraphArrayFlag(flags, config, "backend", "testing", "java", "java-testing-libraries", config.javaTestingLibraries, ["junit5"]);
7440
- }
7441
- if (hasGraphPrimaryPart(config, "backend", "elixir")) {
7442
- appendChangedGraphStringFlag(flags, config, "realtime", "elixir", "elixir-realtime", config.elixirRealtime, "channels");
7443
- appendChangedGraphStringFlag(flags, config, "jobQueue", "elixir", "elixir-jobs", config.elixirJobs, "none");
7444
- appendChangedGraphStringFlag(flags, config, "validation", "elixir", "elixir-validation", config.elixirValidation, "ecto-changesets");
7445
- appendChangedOwnedGraphStringFlag(flags, config, "backend", "httpClient", "elixir", "elixir-http", config.elixirHttp, "req");
7446
- appendChangedStringFlag(flags, "elixir-json", config.elixirJson, "jason");
7447
- appendChangedGraphStringFlag(flags, config, "email", "elixir", "elixir-email", config.elixirEmail, "none");
7448
- appendChangedGraphStringFlag(flags, config, "caching", "elixir", "elixir-caching", config.elixirCaching, "none");
7449
- appendChangedGraphStringFlag(flags, config, "observability", "elixir", "elixir-observability", config.elixirObservability, "telemetry");
7450
- appendChangedGraphStringFlag(flags, config, "testing", "elixir", "elixir-testing", config.elixirTesting, "ex_unit");
7451
- appendChangedOwnedGraphStringFlag(flags, config, "backend", "codeQuality", "elixir", "elixir-quality", config.elixirQuality, "credo");
7452
- appendChangedGraphStringFlag(flags, config, "deploy", "elixir", "elixir-deploy", config.elixirDeploy, "none");
7453
- }
7454
- }
7455
- function appendSharedNonTypeScriptFlags(flags, config) {
7456
- flags.push(`--email ${config.email}`);
7457
- flags.push(`--observability ${config.observability}`);
7458
- flags.push(`--caching ${config.caching}`);
7459
- flags.push(`--search ${config.search}`);
7460
- flags.push(formatArrayFlag("addons", config.addons));
7461
- flags.push(formatArrayFlag("examples", config.examples));
7462
- flags.push(`--db-setup ${config.dbSetup}`);
7463
- flags.push(`--web-deploy ${config.webDeploy}`);
7464
- flags.push(`--server-deploy ${config.serverDeploy}`);
7465
- }
7466
- function getTypeScriptFlags(config) {
7467
- const flags = [];
7468
- if (config.frontend && config.frontend.length > 0) flags.push(`--frontend ${config.frontend.join(" ")}`);
7469
- else flags.push("--frontend none");
7470
- appendAstroIntegrationFlag(flags, config);
7471
- flags.push(`--backend ${config.backend}`);
7472
- flags.push(`--runtime ${config.runtime}`);
7473
- flags.push(`--database ${config.database}`);
7474
- flags.push(`--orm ${config.orm}`);
7475
- flags.push(`--api ${config.api}`);
7476
- flags.push(`--auth ${config.auth}`);
7477
- flags.push(`--payments ${config.payments}`);
7478
- flags.push(`--email ${config.email}`);
7479
- flags.push(`--file-upload ${config.fileUpload}`);
7480
- flags.push(`--effect ${config.effect}`);
7481
- flags.push(`--css-framework ${config.cssFramework}`);
7482
- flags.push(`--ui-library ${config.uiLibrary}`);
7483
- if (config.uiLibrary === "shadcn-ui") {
7484
- flags.push(`--shadcn-base ${config.shadcnBase}`);
7485
- flags.push(`--shadcn-style ${config.shadcnStyle}`);
7486
- flags.push(`--shadcn-icon-library ${config.shadcnIconLibrary}`);
7487
- flags.push(`--shadcn-color-theme ${config.shadcnColorTheme}`);
7488
- flags.push(`--shadcn-base-color ${config.shadcnBaseColor}`);
7489
- flags.push(`--shadcn-font ${config.shadcnFont}`);
7490
- flags.push(`--shadcn-radius ${config.shadcnRadius}`);
7491
- }
7492
- flags.push(`--ai ${config.ai}`);
7493
- flags.push(`--state-management ${config.stateManagement}`);
7494
- flags.push(`--forms ${config.forms}`);
7495
- flags.push(`--validation ${config.validation}`);
7496
- flags.push(`--testing ${config.testing}`);
7497
- flags.push(`--animation ${config.animation}`);
7498
- flags.push(`--realtime ${config.realtime}`);
7499
- flags.push(`--job-queue ${config.jobQueue}`);
7500
- flags.push(`--logging ${config.logging}`);
7501
- flags.push(`--observability ${config.observability}`);
7502
- flags.push(`--feature-flags ${config.featureFlags}`);
7503
- flags.push(`--caching ${config.caching}`);
7504
- flags.push(`--rate-limit ${config.rateLimit}`);
7505
- flags.push(`--i18n ${config.i18n}`);
7506
- flags.push(`--cms ${config.cms}`);
7507
- flags.push(`--search ${config.search}`);
7508
- flags.push(`--file-storage ${config.fileStorage}`);
7509
- flags.push(`--mobile-navigation ${config.mobileNavigation}`);
7510
- flags.push(`--mobile-ui ${config.mobileUI}`);
7511
- flags.push(`--mobile-storage ${config.mobileStorage}`);
7512
- flags.push(`--mobile-testing ${config.mobileTesting}`);
7513
- flags.push(`--mobile-push ${config.mobilePush}`);
7514
- flags.push(`--mobile-ota ${config.mobileOTA}`);
7515
- flags.push(`--mobile-deep-linking ${config.mobileDeepLinking}`);
7516
- if (config.addons && config.addons.length > 0) flags.push(`--addons ${config.addons.join(" ")}`);
7517
- else flags.push("--addons none");
7518
- if (config.examples && config.examples.length > 0) flags.push(`--examples ${config.examples.join(" ")}`);
7519
- else flags.push("--examples none");
7520
- flags.push(`--db-setup ${config.dbSetup}`);
7521
- flags.push(`--web-deploy ${config.webDeploy}`);
7522
- flags.push(`--server-deploy ${config.serverDeploy}`);
7523
- appendCommonFlags(flags, config);
7524
- return flags;
7525
- }
7526
- function getReactNativeFlags(config) {
7527
- const flags = ["--ecosystem react-native"];
7528
- if (config.frontend && config.frontend.length > 0) flags.push(`--frontend ${config.frontend.join(" ")}`);
7529
- else flags.push("--frontend native-bare");
7530
- flags.push(`--auth ${config.auth}`);
7531
- flags.push(`--mobile-navigation ${config.mobileNavigation}`);
7532
- flags.push(`--mobile-ui ${config.mobileUI}`);
7533
- flags.push(`--mobile-storage ${config.mobileStorage}`);
7534
- flags.push(`--mobile-testing ${config.mobileTesting}`);
7535
- flags.push(`--mobile-push ${config.mobilePush}`);
7536
- flags.push(`--mobile-ota ${config.mobileOTA}`);
7537
- flags.push(`--mobile-deep-linking ${config.mobileDeepLinking}`);
7538
- appendCommonFlags(flags, config);
7539
- return flags;
7540
- }
7541
- function getRustFlags(config) {
7542
- const flags = ["--ecosystem rust"];
7543
- flags.push(`--rust-web-framework ${config.rustWebFramework}`);
7544
- flags.push(`--rust-frontend ${config.rustFrontend}`);
7545
- flags.push(`--rust-orm ${config.rustOrm}`);
7546
- flags.push(`--rust-api ${config.rustApi}`);
7547
- flags.push(`--rust-cli ${config.rustCli}`);
7548
- flags.push(formatArrayFlag("rust-libraries", config.rustLibraries));
7549
- flags.push(`--rust-logging ${config.rustLogging}`);
7550
- flags.push(`--rust-error-handling ${config.rustErrorHandling}`);
7551
- flags.push(`--rust-caching ${config.rustCaching}`);
7552
- flags.push(`--rust-auth ${config.rustAuth}`);
7553
- flags.push(`--rust-realtime ${config.rustRealtime}`);
7554
- flags.push(`--rust-message-queue ${config.rustMessageQueue}`);
7555
- flags.push(`--rust-observability ${config.rustObservability}`);
7556
- flags.push(`--rust-templating ${config.rustTemplating}`);
7557
- appendSharedNonTypeScriptFlags(flags, config);
7558
- appendCommonFlags(flags, config);
7559
- return flags;
7560
- }
7561
- function getPythonFlags(config) {
7562
- const flags = ["--ecosystem python"];
7563
- flags.push(`--python-web-framework ${config.pythonWebFramework}`);
7564
- flags.push(`--python-orm ${config.pythonOrm}`);
7565
- flags.push(`--python-validation ${config.pythonValidation}`);
7566
- flags.push(formatArrayFlag("python-ai", config.pythonAi));
7567
- flags.push(`--python-auth ${config.pythonAuth}`);
7568
- flags.push(`--python-api ${config.pythonApi}`);
7569
- flags.push(`--python-task-queue ${config.pythonTaskQueue}`);
7570
- flags.push(`--python-graphql ${config.pythonGraphql}`);
7571
- flags.push(`--python-quality ${config.pythonQuality}`);
7572
- flags.push(formatArrayFlag("python-testing", config.pythonTesting));
7573
- flags.push(`--python-caching ${config.pythonCaching}`);
7574
- flags.push(`--python-realtime ${config.pythonRealtime}`);
7575
- flags.push(`--python-observability ${config.pythonObservability}`);
7576
- flags.push(formatArrayFlag("python-cli", config.pythonCli));
7577
- appendSharedNonTypeScriptFlags(flags, config);
7578
- appendCommonFlags(flags, config);
7579
- return flags;
7580
- }
7581
- function getGoFlags(config) {
7582
- const flags = ["--ecosystem go"];
7583
- flags.push(`--go-web-framework ${config.goWebFramework}`);
7584
- flags.push(`--go-orm ${config.goOrm}`);
7585
- flags.push(`--go-api ${config.goApi}`);
7586
- flags.push(`--go-cli ${config.goCli}`);
7587
- flags.push(`--go-logging ${config.goLogging}`);
7588
- flags.push(`--go-auth ${config.goAuth}`);
7589
- flags.push(formatArrayFlag("go-testing", config.goTesting));
7590
- flags.push(`--go-realtime ${config.goRealtime}`);
7591
- flags.push(`--go-message-queue ${config.goMessageQueue}`);
7592
- flags.push(`--go-caching ${config.goCaching}`);
7593
- flags.push(`--go-config ${config.goConfig}`);
7594
- flags.push(`--go-observability ${config.goObservability}`);
7595
- flags.push(`--auth ${config.auth}`);
7596
- appendSharedNonTypeScriptFlags(flags, config);
7597
- appendCommonFlags(flags, config);
7598
- return flags;
7599
- }
7600
- function getJavaFlags(config) {
7601
- const flags = ["--ecosystem java"];
7602
- flags.push(`--java-web-framework ${config.javaWebFramework}`);
7603
- flags.push(`--java-build-tool ${config.javaBuildTool}`);
7604
- flags.push(`--java-orm ${config.javaOrm}`);
7605
- flags.push(`--java-auth ${config.javaAuth}`);
7606
- flags.push(`--java-api ${config.javaApi}`);
7607
- flags.push(`--java-logging ${config.javaLogging}`);
7608
- flags.push(formatArrayFlag("java-libraries", config.javaLibraries));
7609
- flags.push(formatArrayFlag("java-testing-libraries", config.javaTestingLibraries));
7610
- appendSharedNonTypeScriptFlags(flags, config);
7611
- appendCommonFlags(flags, config);
7612
- return flags;
7613
- }
7614
- function getDotnetFlags(config) {
7615
- const flags = ["--ecosystem dotnet"];
7616
- flags.push(`--dotnet-web-framework ${config.dotnetWebFramework}`);
7617
- flags.push(`--dotnet-orm ${config.dotnetOrm}`);
7618
- flags.push(`--dotnet-auth ${config.dotnetAuth}`);
7619
- flags.push(`--dotnet-api ${config.dotnetApi}`);
7620
- flags.push(formatArrayFlag("dotnet-testing", config.dotnetTesting));
7621
- flags.push(`--dotnet-job-queue ${config.dotnetJobQueue}`);
7622
- flags.push(`--dotnet-realtime ${config.dotnetRealtime}`);
7623
- flags.push(formatArrayFlag("dotnet-observability", config.dotnetObservability));
7624
- flags.push(`--dotnet-validation ${config.dotnetValidation}`);
7625
- flags.push(`--dotnet-caching ${config.dotnetCaching}`);
7626
- flags.push(`--dotnet-deploy ${config.dotnetDeploy}`);
7627
- appendCommonFlags(flags, config);
7628
- return flags;
7629
- }
7630
- function getElixirFlags(config) {
7631
- const flags = ["--ecosystem elixir"];
7632
- flags.push(`--elixir-web-framework ${config.elixirWebFramework}`);
7633
- flags.push(`--elixir-orm ${config.elixirOrm}`);
7634
- flags.push(`--elixir-auth ${config.elixirAuth}`);
7635
- flags.push(`--elixir-api ${config.elixirApi}`);
7636
- flags.push(`--elixir-realtime ${config.elixirRealtime}`);
7637
- flags.push(`--elixir-jobs ${config.elixirJobs}`);
7638
- flags.push(`--elixir-validation ${config.elixirValidation}`);
7639
- flags.push(`--elixir-http ${config.elixirHttp}`);
7640
- flags.push(`--elixir-json ${config.elixirJson}`);
7641
- flags.push(`--elixir-email ${config.elixirEmail}`);
7642
- flags.push(`--elixir-caching ${config.elixirCaching}`);
7643
- flags.push(`--elixir-observability ${config.elixirObservability}`);
7644
- flags.push(`--elixir-testing ${config.elixirTesting}`);
7645
- flags.push(`--elixir-quality ${config.elixirQuality}`);
7646
- flags.push(`--elixir-deploy ${config.elixirDeploy}`);
7647
- flags.push(formatArrayFlag("elixir-libraries", config.elixirLibraries));
7648
- appendCommonFlags(flags, config);
7649
- return flags;
7650
- }
7651
- function generateReproducibleCommand(config) {
7652
- let flags;
7653
- if (config.stackParts && config.stackParts.length > 0) {
7654
- flags = config.stackParts.filter((part) => part.source !== "provided").map((part) => `--part ${(0, types_exports.formatStackPartSpec)(part, config.stackParts ?? [])}`);
7655
- appendGraphExtraFlags(flags, config);
7656
- appendCommonFlags(flags, config);
7657
- return `${getBaseCommand(config.packageManager)}${config.relativePath ? ` ${config.relativePath}` : ""} ${flags.join(" ")}`;
7658
- }
7659
- switch (config.ecosystem) {
7660
- case "react-native":
7661
- flags = getReactNativeFlags(config);
7662
- break;
7663
- case "rust":
7664
- flags = getRustFlags(config);
7665
- break;
7666
- case "python":
7667
- flags = getPythonFlags(config);
7668
- break;
7669
- case "go":
7670
- flags = getGoFlags(config);
7671
- break;
7672
- case "java":
7673
- flags = getJavaFlags(config);
7674
- break;
7675
- case "dotnet":
7676
- flags = getDotnetFlags(config);
7677
- break;
7678
- case "elixir":
7679
- flags = getElixirFlags(config);
7680
- break;
7681
- case "typescript":
7682
- default:
7683
- flags = getTypeScriptFlags(config);
7684
- break;
7685
- }
7686
- return `${getBaseCommand(config.packageManager)}${config.relativePath ? ` ${config.relativePath}` : ""} ${flags.join(" ")}`;
7687
- }
7688
-
7689
- //#endregion
7690
- //#region src/utils/generated-checks.ts
7691
- function getGraphTarget(config) {
7692
- const backend = getPrimaryGraphPart(config, "backend");
7693
- if (!backend || backend.ecosystem === "typescript" || backend.ecosystem === "react-native" || backend.ecosystem === "universal") return null;
7694
- return {
7695
- ecosystem: backend.ecosystem,
7696
- projectDir: path.join(config.projectDir, backend.targetPath ?? "apps/server")
7697
- };
7698
- }
7699
- function getSingleEcosystemTarget(config) {
7700
- if (config.ecosystem === "typescript" || config.ecosystem === "react-native" || config.ecosystem === "java") return null;
7701
- return {
7702
- ecosystem: config.ecosystem,
7703
- projectDir: config.projectDir
7704
- };
7705
- }
7706
- async function runCommand(cwd, command, args) {
7707
- await $({
7708
- cwd,
7709
- stdout: "inherit",
7710
- stderr: "inherit"
7711
- })`${command} ${args}`;
7712
- }
7713
- async function verifyTarget(target) {
7714
- const s = spinner();
7715
- const cwd = target.projectDir;
7716
- switch (target.ecosystem) {
7717
- case "go":
7718
- s.start("Verifying generated Go server...");
7719
- await runCommand(cwd, "go", ["mod", "tidy"]);
7720
- await runCommand(cwd, "go", ["test", "./..."]);
7721
- s.stop("Generated Go server checks passed");
7722
- return;
7723
- case "rust":
7724
- s.start("Verifying generated Rust server...");
7725
- await runCommand(cwd, "cargo", ["check"]);
7726
- s.stop("Generated Rust server checks passed");
7727
- return;
7728
- case "python":
7729
- s.start("Verifying generated Python server...");
7730
- await runCommand(cwd, "uv", ["sync"]);
7731
- await runCommand(cwd, "uv", [
7732
- "run",
7733
- "ruff",
7734
- "check",
7735
- "."
7736
- ]);
7737
- s.stop("Generated Python server checks passed");
7738
- return;
7739
- case "elixir":
7740
- if (!await commandExists("mix")) {
7741
- log.warn(pc.yellow("Skipping Elixir verification because mix is not on PATH"));
7742
- return;
7743
- }
7744
- s.start("Verifying generated Elixir server...");
7745
- await runCommand(cwd, "mix", ["deps.get"]);
7746
- await runCommand(cwd, "mix", ["compile"]);
7747
- s.stop("Generated Elixir server checks passed");
7748
- return;
7749
- default: log.warn(pc.yellow(`No generated checks are configured for ${target.ecosystem}`));
7750
- }
7751
- }
7752
- async function runGeneratedChecks(config) {
7753
- const target = getGraphTarget(config) ?? getSingleEcosystemTarget(config);
7754
- if (!target) {
7755
- log.warn(pc.yellow("No generated checks are configured for this stack"));
7756
- return;
7757
- }
7758
- await verifyTarget(target);
7759
- }
7760
-
7761
7483
  //#endregion
7762
7484
  //#region src/utils/preflight-display.ts
7763
7485
  function displayPreflightWarnings({ warnings }) {
@@ -7861,87 +7583,6 @@ async function setupProjectDirectory(finalPathInput, shouldClearDirectory) {
7861
7583
  };
7862
7584
  }
7863
7585
 
7864
- //#endregion
7865
- //#region src/utils/templates.ts
7866
- const TEMPLATE_PRESETS = {
7867
- mern: {
7868
- database: "mongodb",
7869
- orm: "mongoose",
7870
- backend: "express",
7871
- runtime: "node",
7872
- frontend: ["react-router"],
7873
- api: "orpc",
7874
- auth: "better-auth",
7875
- payments: "none",
7876
- addons: ["turborepo"],
7877
- examples: ["none"],
7878
- dbSetup: "mongodb-atlas",
7879
- webDeploy: "none",
7880
- serverDeploy: "none"
7881
- },
7882
- pern: {
7883
- database: "postgres",
7884
- orm: "drizzle",
7885
- backend: "express",
7886
- runtime: "node",
7887
- frontend: ["tanstack-router"],
7888
- api: "trpc",
7889
- auth: "better-auth",
7890
- payments: "none",
7891
- addons: ["turborepo"],
7892
- examples: ["none"],
7893
- dbSetup: "none",
7894
- webDeploy: "none",
7895
- serverDeploy: "none"
7896
- },
7897
- t3: {
7898
- database: "postgres",
7899
- orm: "prisma",
7900
- backend: "self",
7901
- runtime: "none",
7902
- frontend: ["next"],
7903
- api: "trpc",
7904
- auth: "better-auth",
7905
- payments: "none",
7906
- addons: ["biome", "turborepo"],
7907
- examples: ["none"],
7908
- dbSetup: "none",
7909
- webDeploy: "none",
7910
- serverDeploy: "none"
7911
- },
7912
- uniwind: {
7913
- database: "none",
7914
- orm: "none",
7915
- backend: "none",
7916
- runtime: "none",
7917
- frontend: ["native-uniwind"],
7918
- api: "none",
7919
- auth: "none",
7920
- payments: "none",
7921
- addons: ["none"],
7922
- examples: ["none"],
7923
- dbSetup: "none",
7924
- webDeploy: "none",
7925
- serverDeploy: "none"
7926
- },
7927
- none: null
7928
- };
7929
- function getTemplateConfig(template) {
7930
- if (template === "none" || !template) return null;
7931
- const config = TEMPLATE_PRESETS[template];
7932
- if (!config) throw new Error(`Unknown template: ${template}`);
7933
- return config;
7934
- }
7935
- function getTemplateDescription(template) {
7936
- return {
7937
- mern: "MongoDB + Express + React + Node.js - Classic MERN stack",
7938
- pern: "PostgreSQL + Express + React + Node.js - Popular PERN stack",
7939
- t3: "T3 Stack - Next.js + tRPC + Prisma + PostgreSQL + Better Auth",
7940
- uniwind: "Expo + Uniwind native app with no backend services",
7941
- none: "No template - Full customization"
7942
- }[template] || "";
7943
- }
7944
-
7945
7586
  //#endregion
7946
7587
  //#region src/utils/config-processing.ts
7947
7588
  function deriveProjectName(projectName, projectDirectory) {
@@ -7971,925 +7612,6 @@ function validateArrayOptions(options) {
7971
7612
  validateNoneExclusivity(options.javaTestingLibraries, "java testing libraries");
7972
7613
  }
7973
7614
 
7974
- //#endregion
7975
- //#region src/utils/peer-dependency-conflicts.ts
7976
- /**
7977
- * Known peer dependency conflicts.
7978
- *
7979
- * Add new conflicts here as they are discovered.
7980
- * The validator will check these against the user's selected options.
7981
- */
7982
- const PEER_DEPENDENCY_CONFLICTS = [
7983
- {
7984
- id: "redux-toolkit-react19",
7985
- description: "redux-toolkit with React 19 may have peer dependency warnings",
7986
- packages: ["@reduxjs/toolkit", "react-redux"],
7987
- severity: "warning",
7988
- resolution: "Consider using zustand or jotai for React 19 projects, or ensure react-redux v9+",
7989
- triggeredBy: [{
7990
- optionKey: "stateManagement",
7991
- values: ["redux-toolkit"]
7992
- }],
7993
- conflictsWithOptions: [{
7994
- optionKey: "frontend",
7995
- values: ["next", "vinext"]
7996
- }]
7997
- },
7998
- {
7999
- id: "effect-schema-zod-overlap",
8000
- description: "effect/Schema (built into effect) and zod both provide validation - may cause confusion",
8001
- packages: ["effect", "zod"],
8002
- severity: "warning",
8003
- resolution: "Use --validation effect-schema with Effect, or --effect none with Zod",
8004
- triggeredBy: [{
8005
- optionKey: "effect",
8006
- values: ["effect-full"]
8007
- }],
8008
- conflictsWithOptions: [{
8009
- optionKey: "validation",
8010
- values: ["zod"]
8011
- }]
8012
- },
8013
- {
8014
- id: "prisma-d1-version",
8015
- description: "Prisma with D1 requires Prisma 5.x+ and specific adapter setup",
8016
- packages: ["prisma", "@prisma/adapter-d1"],
8017
- severity: "warning",
8018
- resolution: "Consider Drizzle ORM for simpler D1 integration",
8019
- triggeredBy: [{
8020
- optionKey: "orm",
8021
- values: ["prisma"]
8022
- }],
8023
- conflictsWithOptions: [{
8024
- optionKey: "dbSetup",
8025
- values: ["d1"]
8026
- }]
8027
- },
8028
- {
8029
- id: "biome-linting-overlap",
8030
- description: "Biome includes linting/formatting - other linting addons are redundant",
8031
- packages: [
8032
- "@biomejs/biome",
8033
- "eslint",
8034
- "prettier"
8035
- ],
8036
- severity: "warning",
8037
- resolution: "Choose either Biome (all-in-one) or other linting tools, not both",
8038
- triggeredBy: [{
8039
- optionKey: "addons",
8040
- values: ["biome"]
8041
- }],
8042
- conflictsWithOptions: [{
8043
- optionKey: "addons",
8044
- values: ["ultracite", "oxlint"]
8045
- }]
8046
- },
8047
- {
8048
- id: "framer-motion-react19",
8049
- description: "framer-motion versions <12 may have React 19 issues",
8050
- packages: ["framer-motion"],
8051
- severity: "warning",
8052
- resolution: "The CLI uses motion (framer-motion v12+) which supports React 19",
8053
- triggeredBy: [{
8054
- optionKey: "animation",
8055
- values: ["framer-motion"]
8056
- }],
8057
- conflictsWithOptions: [{
8058
- optionKey: "frontend",
8059
- values: ["next", "vinext"]
8060
- }]
8061
- }
8062
- ];
8063
-
8064
- //#endregion
8065
- //#region src/utils/peer-dependency-validator.ts
8066
- /**
8067
- * Peer dependency conflict validator.
8068
- *
8069
- * Checks project configuration against known conflicts and provides
8070
- * warnings or errors before project creation begins.
8071
- */
8072
- /**
8073
- * Checks if a config value matches any of the specified values.
8074
- */
8075
- function matchesValue(configValue, values) {
8076
- if (configValue === void 0 || configValue === null) return false;
8077
- if (Array.isArray(configValue)) return values.some((v) => configValue.includes(v));
8078
- return values.includes(configValue);
8079
- }
8080
- /**
8081
- * Checks the configuration against known peer dependency conflicts.
8082
- */
8083
- function checkPeerDependencyConflicts(config) {
8084
- const errors = [];
8085
- const warnings = [];
8086
- for (const conflict of PEER_DEPENDENCY_CONFLICTS) {
8087
- if (!conflict.triggeredBy.some((trigger) => {
8088
- const configValue = config[trigger.optionKey];
8089
- return matchesValue(configValue, trigger.values);
8090
- })) continue;
8091
- if (conflict.conflictsWithOptions.some((opt) => {
8092
- const configValue = config[opt.optionKey];
8093
- return matchesValue(configValue, opt.values);
8094
- })) if (conflict.severity === "error") errors.push(conflict);
8095
- else warnings.push(conflict);
8096
- }
8097
- return {
8098
- errors,
8099
- warnings
8100
- };
8101
- }
8102
- /**
8103
- * Prints a warning message for a dependency conflict.
8104
- */
8105
- function warnDependencyConflict(message, resolution) {
8106
- if (isSilent()) return;
8107
- consola.warn(pc.yellow(`Peer Dependency Warning: ${message}`));
8108
- consola.log(pc.dim(` → ${resolution}`));
8109
- }
8110
- /**
8111
- * Handles conflict results by printing warnings and exiting on errors.
8112
- */
8113
- function handleConflictResult(result) {
8114
- for (const warning of result.warnings) warnDependencyConflict(warning.description, warning.resolution);
8115
- if (result.errors.length > 0) exitWithError(`Peer Dependency Conflict:\n${result.errors.map((e) => `${e.description}\n → ${e.resolution}`).join("\n\n")}`);
8116
- }
8117
- /**
8118
- * Validates peer dependencies for the given configuration.
8119
- * Prints warnings and exits on errors.
8120
- */
8121
- function validatePeerDependencies(config) {
8122
- handleConflictResult(checkPeerDependencyConflicts(config));
8123
- }
8124
-
8125
- //#endregion
8126
- //#region src/utils/config-validation.ts
8127
- function validateDatabaseOrmAuth(cfg, flags) {
8128
- const db = cfg.database;
8129
- const orm = cfg.orm;
8130
- const has = (k) => flags ? flags.has(k) : true;
8131
- const hasGraphOrm = cfg.stackParts?.some((part) => part.role === "orm" && part.source !== "provided");
8132
- const ecosystemOrm = getEcosystemOrm(cfg);
8133
- const hasEcosystemOrm = ecosystemOrm !== void 0 && ecosystemOrm !== "none";
8134
- const isNonTypeScriptSqliteDefault = cfg.ecosystem !== void 0 && cfg.ecosystem !== "typescript" && cfg.ecosystem !== "react-native" && db === "sqlite" && !hasEcosystemOrm;
8135
- if (has("orm") && has("database") && orm === "mongoose" && db !== "mongodb") incompatibilityError({
8136
- message: "Mongoose ORM requires MongoDB database.",
8137
- provided: {
8138
- orm: "mongoose",
8139
- database: db || "none"
8140
- },
8141
- suggestions: ["Use --database mongodb", "Choose a different ORM (drizzle, prisma)"]
8142
- });
8143
- if (has("orm") && has("database") && orm === "drizzle" && db === "mongodb") incompatibilityError({
8144
- message: "Drizzle ORM does not support MongoDB.",
8145
- provided: {
8146
- orm: "drizzle",
8147
- database: "mongodb"
8148
- },
8149
- suggestions: ["Use --orm mongoose or --orm prisma for MongoDB", "Choose a different database (postgres, sqlite, mysql)"]
8150
- });
8151
- if (has("orm") && has("database") && orm === "typeorm" && db === "mongodb") incompatibilityError({
8152
- message: "TypeORM does not support MongoDB in Better Fullstack.",
8153
- provided: {
8154
- orm: "typeorm",
8155
- database: "mongodb"
8156
- },
8157
- suggestions: ["Use --orm mongoose or --orm prisma for MongoDB", "Choose a different database (postgres, sqlite, mysql)"]
8158
- });
8159
- if (has("orm") && has("database") && orm === "kysely" && db === "mongodb") incompatibilityError({
8160
- message: "Kysely does not support MongoDB.",
8161
- provided: {
8162
- orm: "kysely",
8163
- database: "mongodb"
8164
- },
8165
- suggestions: ["Use --orm mongoose or --orm prisma for MongoDB", "Choose a different database (postgres, sqlite, mysql)"]
8166
- });
8167
- if (has("orm") && has("database") && orm === "mikroorm" && db === "mongodb") incompatibilityError({
8168
- message: "MikroORM does not support MongoDB in Better Fullstack.",
8169
- provided: {
8170
- orm: "mikroorm",
8171
- database: "mongodb"
8172
- },
8173
- suggestions: ["Use --orm mongoose or --orm prisma for MongoDB", "Choose a different database (postgres, sqlite, mysql)"]
8174
- });
8175
- if (has("orm") && has("database") && orm === "sequelize" && db === "mongodb") incompatibilityError({
8176
- message: "Sequelize does not support MongoDB.",
8177
- provided: {
8178
- orm: "sequelize",
8179
- database: "mongodb"
8180
- },
8181
- suggestions: ["Use --orm mongoose or --orm prisma for MongoDB", "Choose a different database (postgres, sqlite, mysql)"]
8182
- });
8183
- if (has("database") && has("orm") && db === "mongodb" && orm && orm !== "mongoose" && orm !== "prisma" && orm !== "none") incompatibilityError({
8184
- message: "In Better-Fullstack, MongoDB is currently supported only with Mongoose or Prisma ORM.",
8185
- provided: {
8186
- database: "mongodb",
8187
- orm
8188
- },
8189
- suggestions: ["Use --orm mongoose", "Use --orm prisma"]
8190
- });
8191
- if (has("database") && has("orm") && db && db !== "none" && db !== "edgedb" && db !== "redis" && orm === "none" && !hasGraphOrm && !hasEcosystemOrm && !isNonTypeScriptSqliteDefault) missingRequirementError({
8192
- message: "Database selection requires an ORM.",
8193
- provided: {
8194
- database: db,
8195
- orm: "none"
8196
- },
8197
- suggestions: [
8198
- "Use --orm drizzle (recommended)",
8199
- "Use --orm prisma",
8200
- "Use --orm mongoose (MongoDB only)"
8201
- ]
8202
- });
8203
- if (has("database") && has("orm") && db === "edgedb" && orm && orm !== "none") incompatibilityError({
8204
- message: "EdgeDB has its own built-in query builder and does not require an ORM.",
8205
- provided: {
8206
- database: "edgedb",
8207
- orm
8208
- },
8209
- suggestions: ["Use --orm none with EdgeDB", "Choose a different database if you want to use an ORM"]
8210
- });
8211
- if (has("database") && has("orm") && db === "redis" && orm && orm !== "none") incompatibilityError({
8212
- message: "Redis is a key-value store and does not require an ORM.",
8213
- provided: {
8214
- database: "redis",
8215
- orm
8216
- },
8217
- suggestions: ["Use --orm none with Redis", "Choose a different database if you want to use an ORM"]
8218
- });
8219
- if (has("orm") && has("database") && orm && orm !== "none" && db === "none") missingRequirementError({
8220
- message: "ORM selection requires a database.",
8221
- provided: {
8222
- orm,
8223
- database: "none"
8224
- },
8225
- suggestions: [
8226
- "Use --database postgres",
8227
- "Use --database sqlite",
8228
- "Use --database mysql",
8229
- "Set --orm none"
8230
- ]
8231
- });
8232
- }
8233
- function getEcosystemOrm(cfg) {
8234
- switch (cfg.ecosystem) {
8235
- case "rust": return cfg.rustOrm;
8236
- case "python": return cfg.pythonOrm;
8237
- case "go": return cfg.goOrm;
8238
- case "java": return cfg.javaOrm;
8239
- case "dotnet": return cfg.dotnetOrm;
8240
- case "elixir": return cfg.elixirOrm;
8241
- default: return;
8242
- }
8243
- }
8244
- function getEcosystemBackend(cfg) {
8245
- switch (cfg.ecosystem) {
8246
- case "rust": return cfg.rustWebFramework && cfg.rustWebFramework !== "none" ? cfg.rustWebFramework : void 0;
8247
- case "python": return cfg.pythonWebFramework && cfg.pythonWebFramework !== "none" ? cfg.pythonWebFramework : void 0;
8248
- case "go": return cfg.goWebFramework && cfg.goWebFramework !== "none" ? cfg.goWebFramework : void 0;
8249
- case "java": return cfg.javaWebFramework && cfg.javaWebFramework !== "none" ? cfg.javaWebFramework : void 0;
8250
- case "dotnet": return cfg.dotnetWebFramework && cfg.dotnetWebFramework !== "none" ? cfg.dotnetWebFramework : void 0;
8251
- case "elixir": return cfg.elixirWebFramework && cfg.elixirWebFramework !== "none" ? cfg.elixirWebFramework : void 0;
8252
- default: return;
8253
- }
8254
- }
8255
- function validateDatabaseSetup(config, providedFlags) {
8256
- const { dbSetup, database, runtime } = config;
8257
- 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'.");
8258
- const setupValidations = {
8259
- turso: {
8260
- database: "sqlite",
8261
- errorMessage: "Turso setup requires SQLite database. Please use '--database sqlite' or choose a different setup."
8262
- },
8263
- neon: {
8264
- database: "postgres",
8265
- errorMessage: "Neon setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup."
8266
- },
8267
- "prisma-postgres": {
8268
- database: "postgres",
8269
- errorMessage: "Prisma PostgreSQL setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup."
8270
- },
8271
- planetscale: { errorMessage: "PlanetScale setup requires PostgreSQL or MySQL database. Please use '--database postgres' or '--database mysql' or choose a different setup." },
8272
- "mongodb-atlas": {
8273
- database: "mongodb",
8274
- errorMessage: "MongoDB Atlas setup requires MongoDB database. Please use '--database mongodb' or choose a different setup."
8275
- },
8276
- upstash: {
8277
- database: "redis",
8278
- errorMessage: "Upstash setup requires Redis database. Please use '--database redis' or choose a different setup."
8279
- },
8280
- supabase: {
8281
- database: "postgres",
8282
- errorMessage: "Supabase setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup."
8283
- },
8284
- d1: {
8285
- database: "sqlite",
8286
- runtime: "workers",
8287
- errorMessage: "Cloudflare D1 setup requires SQLite database and Cloudflare Workers runtime."
8288
- },
8289
- docker: { errorMessage: "In Better-Fullstack, Docker setup is currently not available with SQLite database or Cloudflare Workers runtime." },
8290
- none: { errorMessage: "" }
8291
- };
8292
- if (dbSetup && dbSetup !== "none") {
8293
- const validation = setupValidations[dbSetup];
8294
- if (dbSetup === "planetscale") {
8295
- if (database !== "postgres" && database !== "mysql") exitWithError(validation.errorMessage);
8296
- } else if (validation.database && database !== validation.database) exitWithError(validation.errorMessage);
8297
- if (validation.runtime && runtime !== validation.runtime) exitWithError(validation.errorMessage);
8298
- if (dbSetup === "docker") {
8299
- if (database === "sqlite") exitWithError("In Better-Fullstack, Docker setup is currently not available 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.");
8300
- if (runtime === "workers") exitWithError("In Better-Fullstack, Docker setup is currently not available 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.");
8301
- }
8302
- }
8303
- }
8304
- function validateEcosystemAuthCompatibility(config, providedFlags) {
8305
- const auth = config.auth;
8306
- if (!auth || auth === "none") return;
8307
- if ((auth === "better-auth" || auth === "better-auth-organizations") && config.orm && [
8308
- "typeorm",
8309
- "sequelize",
8310
- "mikroorm"
8311
- ].includes(config.orm)) {
8312
- config.auth = "none";
8313
- if (providedFlags?.has("auth") && !isSilent()) consola.warn(pc.yellow(`Unsupported auth selection '${auth}' with ${config.orm}: no Better Auth adapter exists. Falling back to '--auth none'.`));
8314
- return;
8315
- }
8316
- const normalized = (0, types_exports.normalizeCapabilitySelection)("auth", {
8317
- ecosystem: config.ecosystem,
8318
- backend: config.backend,
8319
- frontend: config.frontend
8320
- }, auth);
8321
- if (!normalized.normalized || normalized.value === auth) return;
8322
- config.auth = normalized.value;
8323
- if (providedFlags?.has("auth") && normalized.reason && !isSilent()) consola.warn(pc.yellow(`Unsupported auth selection '${auth}' for the current stack: ${normalized.reason}. Falling back to '--auth ${normalized.value}'.`));
8324
- }
8325
- function validateConvexConstraints(config, providedFlags) {
8326
- const { backend } = config;
8327
- if (backend !== "convex") return;
8328
- const has = (k) => providedFlags.has(k);
8329
- if (has("runtime") && config.runtime !== "none") constraintError({
8330
- message: "Convex backend manages its own runtime.",
8331
- provided: {
8332
- backend: "convex",
8333
- runtime: config.runtime || ""
8334
- },
8335
- suggestions: ["Remove --runtime flag", "Set --runtime none"]
8336
- });
8337
- if (has("database") && config.database !== "none") constraintError({
8338
- message: "Convex backend has its own built-in database.",
8339
- provided: {
8340
- backend: "convex",
8341
- database: config.database || ""
8342
- },
8343
- suggestions: ["Remove --database flag", "Set --database none"]
8344
- });
8345
- if (has("orm") && config.orm !== "none") constraintError({
8346
- message: "Convex backend has its own data layer (no ORM needed).",
8347
- provided: {
8348
- backend: "convex",
8349
- orm: config.orm || ""
8350
- },
8351
- suggestions: ["Remove --orm flag", "Set --orm none"]
8352
- });
8353
- if (has("api") && config.api !== "none") constraintError({
8354
- message: "Convex backend has its own built-in API layer.",
8355
- provided: {
8356
- backend: "convex",
8357
- api: config.api || ""
8358
- },
8359
- suggestions: ["Remove --api flag", "Set --api none"]
8360
- });
8361
- if (has("dbSetup") && config.dbSetup !== "none") constraintError({
8362
- message: "Convex backend manages its own database infrastructure.",
8363
- provided: {
8364
- backend: "convex",
8365
- "db-setup": config.dbSetup || ""
8366
- },
8367
- suggestions: ["Remove --db-setup flag", "Set --db-setup none"]
8368
- });
8369
- if (has("serverDeploy") && config.serverDeploy !== "none") constraintError({
8370
- message: "Convex backend has its own deployment platform.",
8371
- provided: {
8372
- backend: "convex",
8373
- "server-deploy": config.serverDeploy || ""
8374
- },
8375
- suggestions: ["Remove --server-deploy flag", "Set --server-deploy none"]
8376
- });
8377
- }
8378
- function validateBackendNoneConstraints(config, providedFlags) {
8379
- const { backend } = config;
8380
- const hasGraphBackend = config.stackParts?.some((part) => part.role === "backend" && !part.ownerPartId && part.source !== "provided" && part.ecosystem !== "typescript" && part.ecosystem !== "react-native" && part.ecosystem !== "universal");
8381
- const hasEcosystemBackend = getEcosystemBackend(config) !== void 0;
8382
- if (backend !== "none" || hasGraphBackend || hasEcosystemBackend) return;
8383
- const has = (k) => providedFlags.has(k);
8384
- if (has("runtime") && config.runtime !== "none") exitWithError("Backend 'none' requires '--runtime none'. Please remove the --runtime flag or set it to 'none'.");
8385
- if (has("database") && config.database !== "none") exitWithError("Backend 'none' requires '--database none'. Please remove the --database flag or set it to 'none'.");
8386
- if (has("orm") && config.orm !== "none") exitWithError("Backend 'none' requires '--orm none'. Please remove the --orm flag or set it to 'none'.");
8387
- if (has("api") && config.api !== "none") exitWithError("Backend 'none' requires '--api none'. Please remove the --api flag or set it to 'none'.");
8388
- if (has("payments") && config.payments !== "none") exitWithError("Backend 'none' requires '--payments none'. Please remove the --payments flag or set it to 'none'.");
8389
- if (has("dbSetup") && config.dbSetup !== "none") exitWithError("Backend 'none' requires '--db-setup none'. Please remove the --db-setup flag or set it to 'none'.");
8390
- if (has("serverDeploy") && config.serverDeploy !== "none") exitWithError("Backend 'none' requires '--server-deploy none'. Please remove the --server-deploy flag or set it to 'none'.");
8391
- }
8392
- function validateSelfBackendConstraints(config, providedFlags) {
8393
- const { backend } = config;
8394
- if (backend !== "self") return;
8395
- const has = (k) => providedFlags.has(k);
8396
- if (has("runtime") && config.runtime !== "none") exitWithError("Backend 'self' (fullstack) requires '--runtime none'. Please remove the --runtime flag or set it to 'none'.");
8397
- }
8398
- function validateEncoreConstraints(config, providedFlags) {
8399
- const { backend } = config;
8400
- if (backend !== "encore") return;
8401
- const has = (k) => providedFlags.has(k);
8402
- if (has("runtime") && config.runtime !== "none") exitWithError("Encore.ts backend requires '--runtime none'. Encore has its own runtime via the Encore CLI. Please remove the --runtime flag or set it to 'none'.");
8403
- if (has("api") && config.api !== "none") exitWithError("Encore.ts backend requires '--api none'. Encore has its own type-safe API system. Please remove the --api flag or set it to 'none'.");
8404
- if (has("database") && config.database !== "none") exitWithError("Encore.ts backend requires '--database none'. Encore manages databases through its infrastructure primitives. Please remove the --database flag or set it to 'none'.");
8405
- if (has("orm") && config.orm !== "none") exitWithError("Encore.ts backend requires '--orm none'. Encore has its own database abstractions. Please remove the --orm flag or set it to 'none'.");
8406
- if (has("dbSetup") && config.dbSetup !== "none") exitWithError("Encore.ts backend requires '--db-setup none'. Encore manages infrastructure automatically. Please remove the --db-setup flag or set it to 'none'.");
8407
- if (has("serverDeploy") && config.serverDeploy !== "none") exitWithError("Encore.ts backend requires '--server-deploy none'. Encore has its own deployment platform. Please remove the --server-deploy flag or set it to 'none'.");
8408
- }
8409
- function validateAdonisJSConstraints(config, providedFlags) {
8410
- const { backend } = config;
8411
- if (backend !== "adonisjs") return;
8412
- const has = (k) => providedFlags.has(k);
8413
- if (has("runtime") && config.runtime !== "node") exitWithError("AdonisJS backend requires '--runtime node'. AdonisJS currently only supports Node.js runtime. Please use '--runtime node' or remove the --runtime flag.");
8414
- }
8415
- function validateBackendConstraints(config, providedFlags, options) {
8416
- const { backend } = config;
8417
- if (providedFlags.has("backend") && backend && backend !== "convex" && backend !== "none" && backend !== "self" && backend !== "encore") {
8418
- if (providedFlags.has("runtime") && options.runtime === "none") exitWithError("'--runtime none' is only supported with '--backend convex', '--backend none', '--backend self', or '--backend encore'. Please choose 'bun', 'node', or remove the --runtime flag.");
8419
- }
8420
- if (backend === "convex" && providedFlags.has("frontend") && options.frontend) {
8421
- const incompatibleFrontends = options.frontend.filter((f) => ["solid", "astro"].includes(f));
8422
- if (incompatibleFrontends.length > 0) exitWithError(`The following frontends are not compatible with '--backend convex': ${incompatibleFrontends.join(", ")}. Please choose a different frontend or backend.`);
8423
- }
8424
- }
8425
- function validateFrontendConstraints(config, providedFlags) {
8426
- const { frontend } = config;
8427
- if (frontend && frontend.length > 0) {
8428
- if (config.ecosystem === "react-native" && frontend.some((item) => !item.startsWith("native-") && item !== "none")) incompatibilityError({
8429
- message: "React Native ecosystem only supports native Expo frontends.",
8430
- provided: {
8431
- ecosystem: "react-native",
8432
- frontend: frontend.join(" ")
8433
- },
8434
- suggestions: [
8435
- "Use --frontend native-bare",
8436
- "Use --frontend native-uniwind",
8437
- "Use --frontend native-unistyles"
8438
- ]
8439
- });
8440
- ensureSingleWebAndNative(frontend);
8441
- if (providedFlags.has("api") && providedFlags.has("frontend") && config.api) validateApiFrontendCompatibility(config.api, frontend, config.astroIntegration);
8442
- }
8443
- const hasWebFrontendFlag = (frontend ?? []).some((f) => isWebFrontend$1(f));
8444
- validateWebDeployRequiresWebFrontend(config.webDeploy, hasWebFrontendFlag);
8445
- validateWebDeployFrontendTemplates(config.webDeploy, frontend);
8446
- }
8447
- function validateApiConstraints(config, _options) {
8448
- if (config.api !== "openapi") return;
8449
- const frontend = config.frontend ?? [];
8450
- if (frontend.some((item) => [
8451
- "native-bare",
8452
- "native-uniwind",
8453
- "native-unistyles"
8454
- ].includes(item))) incompatibilityError({
8455
- message: "OpenAPI is currently available for web frontends, not React Native.",
8456
- provided: {
8457
- api: "openapi",
8458
- frontend
8459
- },
8460
- suggestions: [
8461
- "Use --api trpc",
8462
- "Use --api orpc",
8463
- "Use a web frontend"
8464
- ]
8465
- });
8466
- if (!config.backend || ![
8467
- "hono",
8468
- "express",
8469
- "fastify",
8470
- "elysia"
8471
- ].includes(config.backend)) incompatibilityError({
8472
- message: "OpenAPI currently supports Hono, Express, Fastify, and Elysia backends.",
8473
- provided: {
8474
- api: "openapi",
8475
- backend: config.backend ?? "none"
8476
- },
8477
- suggestions: [
8478
- "Use --backend hono",
8479
- "Use --backend express",
8480
- "Use --backend fastify",
8481
- "Use --backend elysia"
8482
- ]
8483
- });
8484
- }
8485
- function validateJavaConstraints(config, providedFlags = /* @__PURE__ */ new Set()) {
8486
- if (config.ecosystem !== "java") return;
8487
- const hasSpringBoot = config.javaWebFramework === "spring-boot";
8488
- const hasJavaWebFramework = config.javaWebFramework !== "none";
8489
- const hasNoBuildTool = config.javaBuildTool === "none";
8490
- const hasJavaLibraries = (config.javaLibraries ?? []).some((library) => library !== "none");
8491
- const hasJavaTestingLibraries = (config.javaTestingLibraries ?? []).some((library) => library !== "none");
8492
- const hasSpringOnlyFeatures = config.javaOrm !== "none" || config.javaAuth !== "none" || hasJavaLibraries;
8493
- if (hasNoBuildTool && hasJavaWebFramework) incompatibilityError({
8494
- message: "Java web frameworks require Maven or Gradle in the Java scaffold.",
8495
- provided: {
8496
- "java-web-framework": config.javaWebFramework ?? "none",
8497
- "java-build-tool": config.javaBuildTool ?? "none"
8498
- },
8499
- suggestions: ["Use --java-build-tool maven or --java-build-tool gradle with Java web frameworks", "Use --java-web-framework none for a plain Java source-only scaffold"]
8500
- });
8501
- if ((!hasSpringBoot || hasNoBuildTool) && hasSpringOnlyFeatures) incompatibilityError({
8502
- message: "Spring-only Java features require the Spring Boot scaffold with Maven or Gradle.",
8503
- provided: {
8504
- "java-web-framework": config.javaWebFramework ?? "none",
8505
- "java-build-tool": config.javaBuildTool ?? "none",
8506
- "java-orm": config.javaOrm ?? "none",
8507
- "java-auth": config.javaAuth ?? "none",
8508
- "java-libraries": (config.javaLibraries ?? []).join(" ") || "none"
8509
- },
8510
- 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"]
8511
- });
8512
- if (hasNoBuildTool && hasJavaTestingLibraries) incompatibilityError({
8513
- message: "Java testing libraries require Maven or Gradle to manage test dependencies.",
8514
- provided: {
8515
- "java-build-tool": config.javaBuildTool ?? "none",
8516
- "java-testing-libraries": (config.javaTestingLibraries ?? []).join(" ") || "none"
8517
- },
8518
- suggestions: ["Use --java-build-tool maven or --java-build-tool gradle to enable JUnit/Mockito/Testcontainers", "Set --java-testing-libraries none for a source-only plain Java scaffold"]
8519
- });
8520
- }
8521
- function validateElixirConstraints(config) {
8522
- if (config.ecosystem !== "elixir") return;
8523
- const hasPhoenix = config.elixirWebFramework !== "none";
8524
- const hasEcto = config.elixirOrm !== "none";
8525
- const unsupportedSelections = [
8526
- {
8527
- flag: "elixir-orm",
8528
- value: config.elixirOrm,
8529
- unsupported: ["ecto"],
8530
- message: "Plain Ecto without SQL Repo wiring is not generated yet.",
8531
- suggestions: ["Use --elixir-orm ecto-sql", "Use --elixir-orm none"]
8532
- },
8533
- {
8534
- flag: "elixir-auth",
8535
- value: config.elixirAuth,
8536
- unsupported: ["ueberauth", "guardian"],
8537
- message: "Only phx.gen.auth currently generates Phoenix auth files.",
8538
- suggestions: ["Use --elixir-auth phx-gen-auth", "Use --elixir-auth none"]
8539
- },
8540
- {
8541
- flag: "elixir-validation",
8542
- value: config.elixirValidation,
8543
- unsupported: ["nimble-options"],
8544
- message: "NimbleOptions is not generated yet.",
8545
- suggestions: ["Use --elixir-validation ecto-changesets", "Use --elixir-validation none"]
8546
- },
8547
- {
8548
- flag: "elixir-caching",
8549
- value: config.elixirCaching,
8550
- unsupported: ["nebulex"],
8551
- message: "Nebulex cache modules are not generated yet.",
8552
- suggestions: ["Use --elixir-caching cachex", "Use --elixir-caching none"]
8553
- },
8554
- {
8555
- flag: "elixir-observability",
8556
- value: config.elixirObservability,
8557
- unsupported: ["opentelemetry", "prom_ex"],
8558
- message: "OpenTelemetry and PromEx setup are not generated yet.",
8559
- suggestions: ["Use --elixir-observability telemetry", "Use --elixir-observability none"]
8560
- },
8561
- {
8562
- flag: "elixir-testing",
8563
- value: config.elixirTesting,
8564
- unsupported: [
8565
- "mox",
8566
- "bypass",
8567
- "wallaby"
8568
- ],
8569
- message: "Generated Phoenix projects currently include ExUnit tests only.",
8570
- suggestions: ["Use --elixir-testing ex_unit"]
8571
- },
8572
- {
8573
- flag: "elixir-deploy",
8574
- value: config.elixirDeploy,
8575
- unsupported: ["fly", "gigalixir"],
8576
- message: "Fly.io and Gigalixir config files are not generated yet.",
8577
- suggestions: ["Use --elixir-deploy docker", "Use --elixir-deploy mix-release"]
8578
- }
8579
- ];
8580
- for (const selection of unsupportedSelections) if (selection.value && selection.unsupported.includes(selection.value)) incompatibilityError({
8581
- message: selection.message,
8582
- provided: { [selection.flag]: selection.value },
8583
- suggestions: selection.suggestions
8584
- });
8585
- if (!hasPhoenix) {
8586
- const phoenixOnlySelections = [
8587
- {
8588
- flag: "elixir-auth",
8589
- value: config.elixirAuth,
8590
- message: "Elixir auth scaffolds require Phoenix."
8591
- },
8592
- {
8593
- flag: "elixir-api",
8594
- value: config.elixirApi,
8595
- message: "Elixir API scaffolds require Phoenix."
8596
- },
8597
- {
8598
- flag: "elixir-realtime",
8599
- value: config.elixirRealtime,
8600
- message: "Elixir realtime scaffolds require Phoenix."
8601
- }
8602
- ];
8603
- for (const selection of phoenixOnlySelections) {
8604
- if (!selection.value || selection.value === "none") continue;
8605
- incompatibilityError({
8606
- message: selection.message,
8607
- provided: {
8608
- "elixir-web-framework": config.elixirWebFramework ?? "none",
8609
- [selection.flag]: selection.value
8610
- },
8611
- suggestions: [
8612
- "Use --elixir-web-framework phoenix",
8613
- "Use --elixir-web-framework phoenix-live-view",
8614
- `Use --${selection.flag} none`
8615
- ]
8616
- });
8617
- }
8618
- }
8619
- if (hasPhoenix && config.elixirJson === "none") incompatibilityError({
8620
- message: "Phoenix JSON scaffolds require Jason.",
8621
- provided: { "elixir-json": "none" },
8622
- suggestions: ["Use --elixir-json jason"]
8623
- });
8624
- if (config.elixirAuth === "phx-gen-auth" && !hasEcto) incompatibilityError({
8625
- message: "phx.gen.auth requires Ecto in the generated Phoenix scaffold.",
8626
- provided: {
8627
- "elixir-auth": "phx-gen-auth",
8628
- "elixir-orm": config.elixirOrm ?? "none"
8629
- },
8630
- suggestions: ["Use --elixir-orm ecto-sql", "Use --elixir-auth none"]
8631
- });
8632
- if (config.elixirJobs === "oban" && config.elixirOrm !== "ecto-sql") incompatibilityError({
8633
- message: "Oban requires Ecto SQL with PostgreSQL in the generated Phoenix scaffold.",
8634
- provided: {
8635
- "elixir-jobs": "oban",
8636
- "elixir-orm": config.elixirOrm ?? "none"
8637
- },
8638
- suggestions: ["Use --elixir-orm ecto-sql", "Use --elixir-jobs none"]
8639
- });
8640
- if (config.elixirRealtime === "live-view-streams" && config.elixirWebFramework !== "phoenix-live-view") incompatibilityError({
8641
- message: "LiveView Streams require Phoenix LiveView.",
8642
- provided: {
8643
- "elixir-realtime": "live-view-streams",
8644
- "elixir-web-framework": config.elixirWebFramework ?? "none"
8645
- },
8646
- suggestions: ["Use --elixir-web-framework phoenix-live-view", "Use --elixir-realtime channels"]
8647
- });
8648
- if (config.elixirApi === "absinthe" && !hasEcto) incompatibilityError({
8649
- message: "Absinthe GraphQL requires Ecto in the current generated Phoenix scaffold.",
8650
- provided: {
8651
- "elixir-api": "absinthe",
8652
- "elixir-orm": config.elixirOrm ?? "none"
8653
- },
8654
- suggestions: ["Use --elixir-orm ecto-sql", "Use --elixir-api rest"]
8655
- });
8656
- }
8657
- function validateEmailConstraints(config) {
8658
- if (!config.email || config.email === "none") return;
8659
- if (config.ecosystem !== "typescript" && config.email !== "resend") incompatibilityError({
8660
- message: "Only Resend email is available for non-TypeScript ecosystems.",
8661
- provided: {
8662
- ecosystem: config.ecosystem ?? "typescript",
8663
- email: config.email
8664
- },
8665
- suggestions: ["Use --email resend", "Use --email none"]
8666
- });
8667
- if (config.ecosystem === "java" && config.email === "resend" && config.javaBuildTool === "none") incompatibilityError({
8668
- message: "Resend email for Java requires Maven or Gradle to manage the SDK dependency.",
8669
- provided: {
8670
- "java-build-tool": "none",
8671
- email: "resend"
8672
- },
8673
- suggestions: ["Use --java-build-tool maven", "Use --java-build-tool gradle"]
8674
- });
8675
- }
8676
- function validateObservabilityConstraints(config) {
8677
- if (!config.observability || config.observability === "none") return;
8678
- if (config.ecosystem !== "typescript" && config.observability !== "sentry") incompatibilityError({
8679
- message: "Only Sentry observability is available for non-TypeScript ecosystems.",
8680
- provided: {
8681
- ecosystem: config.ecosystem ?? "typescript",
8682
- observability: config.observability
8683
- },
8684
- suggestions: ["Use --observability sentry", "Use --observability none"]
8685
- });
8686
- if (config.ecosystem === "java" && config.observability === "sentry" && config.javaBuildTool === "none") incompatibilityError({
8687
- message: "Sentry observability for Java requires Maven or Gradle to manage the SDK dependency.",
8688
- provided: {
8689
- "java-build-tool": "none",
8690
- observability: "sentry"
8691
- },
8692
- suggestions: ["Use --java-build-tool maven", "Use --java-build-tool gradle"]
8693
- });
8694
- }
8695
- function validateCachingConstraints(config) {
8696
- if (!config.caching || config.caching === "none") return;
8697
- if (config.ecosystem !== "typescript" && config.caching !== "upstash-redis") incompatibilityError({
8698
- message: "Only Upstash Redis caching is available for non-TypeScript ecosystems.",
8699
- provided: {
8700
- ecosystem: config.ecosystem ?? "typescript",
8701
- caching: config.caching
8702
- },
8703
- suggestions: ["Use --caching upstash-redis", "Use --caching none"]
8704
- });
8705
- if (config.ecosystem === "java" && config.caching === "upstash-redis" && config.javaBuildTool === "none") incompatibilityError({
8706
- message: "Upstash Redis caching for Java requires Maven or Gradle to manage the Redis client dependency.",
8707
- provided: {
8708
- "java-build-tool": "none",
8709
- caching: "upstash-redis"
8710
- },
8711
- suggestions: ["Use --java-build-tool maven", "Use --java-build-tool gradle"]
8712
- });
8713
- }
8714
- function validateRateLimitConstraints(config) {
8715
- if (!config.rateLimit || config.rateLimit === "none") return;
8716
- if (config.ecosystem !== "typescript") incompatibilityError({
8717
- message: "Rate limiting helpers are currently available for TypeScript stacks only.",
8718
- provided: {
8719
- ecosystem: config.ecosystem ?? "typescript",
8720
- "rate-limit": config.rateLimit
8721
- },
8722
- suggestions: ["Use --rate-limit none"]
8723
- });
8724
- if (config.backend === "convex") incompatibilityError({
8725
- message: "Rate limiting helpers are not generated with Convex backend.",
8726
- provided: {
8727
- backend: "convex",
8728
- "rate-limit": config.rateLimit
8729
- },
8730
- suggestions: ["Use --rate-limit none"]
8731
- });
8732
- if (config.backend === "none") incompatibilityError({
8733
- message: "Rate limiting requires a backend.",
8734
- provided: {
8735
- backend: "none",
8736
- "rate-limit": config.rateLimit
8737
- },
8738
- suggestions: ["Use --backend hono", "Use --rate-limit none"]
8739
- });
8740
- }
8741
- function validateSearchConstraints(config) {
8742
- if (!config.search || config.search === "none") return;
8743
- if (config.ecosystem !== "typescript" && config.search !== "meilisearch") incompatibilityError({
8744
- message: "Only Meilisearch search is available for non-TypeScript ecosystems.",
8745
- provided: {
8746
- ecosystem: config.ecosystem ?? "typescript",
8747
- search: config.search
8748
- },
8749
- suggestions: ["Use --search meilisearch", "Use --search none"]
8750
- });
8751
- if (config.ecosystem === "java" && config.search === "meilisearch" && config.javaBuildTool === "none") incompatibilityError({
8752
- message: "Meilisearch search for Java requires Maven or Gradle to manage the SDK dependency.",
8753
- provided: {
8754
- "java-build-tool": "none",
8755
- search: "meilisearch"
8756
- },
8757
- suggestions: ["Use --java-build-tool maven", "Use --java-build-tool gradle"]
8758
- });
8759
- }
8760
- function validateShadcnConstraints(config, providedFlags) {
8761
- const shadcnFlagMap = {
8762
- shadcnBase: "--shadcn-base",
8763
- shadcnStyle: "--shadcn-style",
8764
- shadcnIconLibrary: "--shadcn-icon-library",
8765
- shadcnColorTheme: "--shadcn-color-theme",
8766
- shadcnBaseColor: "--shadcn-base-color",
8767
- shadcnFont: "--shadcn-font",
8768
- shadcnRadius: "--shadcn-radius"
8769
- };
8770
- const providedShadcnFlags = Object.keys(shadcnFlagMap).filter((f) => providedFlags.has(f));
8771
- if (providedShadcnFlags.length > 0 && config.uiLibrary !== "shadcn-ui") incompatibilityError({
8772
- message: "shadcn/ui customization flags require --ui-library shadcn-ui.",
8773
- provided: {
8774
- "ui-library": config.uiLibrary || "none",
8775
- ...Object.fromEntries(providedShadcnFlags.map((f) => [shadcnFlagMap[f], String(config[f] ?? "")]))
8776
- },
8777
- suggestions: ["Add --ui-library shadcn-ui to use shadcn customization flags", "Remove the --shadcn-* flags if not using shadcn/ui"]
8778
- });
8779
- }
8780
- function validatePythonApiConstraints(config) {
8781
- if (config.ecosystem === "python" && config.pythonApi && config.pythonApi !== "none" && config.pythonWebFramework !== "django") incompatibilityError({
8782
- message: "Python API frameworks require --python-web-framework django.",
8783
- provided: {
8784
- "python-web-framework": config.pythonWebFramework || "none",
8785
- "python-api": config.pythonApi
8786
- },
8787
- 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"]
8788
- });
8789
- }
8790
- function validateFullConfig(config, providedFlags, options) {
8791
- if (config.stackParts && !options.yolo) {
8792
- const graphValidation = (0, types_exports.validateStackParts)(config.stackParts);
8793
- if (graphValidation.issues.length > 0) exitWithError(graphValidation.issues.map((issue) => issue.message).join("\n"));
8794
- }
8795
- validateEcosystemAuthCompatibility(config, providedFlags);
8796
- validateDatabaseOrmAuth(config, providedFlags);
8797
- validateDatabaseSetup(config, providedFlags);
8798
- validateConvexConstraints(config, providedFlags);
8799
- validateBackendNoneConstraints(config, providedFlags);
8800
- validateSelfBackendConstraints(config, providedFlags);
8801
- validateEncoreConstraints(config, providedFlags);
8802
- validateAdonisJSConstraints(config, providedFlags);
8803
- validateBackendConstraints(config, providedFlags, options);
8804
- validateFrontendConstraints(config, providedFlags);
8805
- validateApiConstraints(config, options);
8806
- validatePythonApiConstraints(config);
8807
- validateEmailConstraints(config);
8808
- validateObservabilityConstraints(config);
8809
- validateCachingConstraints(config);
8810
- validateRateLimitConstraints(config);
8811
- validateSearchConstraints(config);
8812
- validateJavaConstraints(config, providedFlags);
8813
- validateElixirConstraints(config);
8814
- const hasGraphBackend = config.stackParts?.some((part) => part.role === "backend" && !part.ownerPartId && part.source !== "provided" && part.ecosystem !== "typescript" && part.ecosystem !== "react-native" && part.ecosystem !== "universal");
8815
- 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));
8816
- validateSelfBackendCompatibility(providedFlags, options, config);
8817
- validateWorkersCompatibility(providedFlags, options, config);
8818
- if (config.runtime === "workers" && config.serverDeploy === "none") exitWithError("Cloudflare Workers runtime requires a server deployment. Please choose 'alchemy' for --server-deploy.");
8819
- if (providedFlags.has("serverDeploy") && config.serverDeploy === "cloudflare" && config.runtime !== "workers") exitWithError(`Server deployment '${config.serverDeploy}' requires '--runtime workers'. Please use '--runtime workers' or choose a different server deployment.`);
8820
- if (config.serverDeploy === "vercel" && [
8821
- "nestjs",
8822
- "adonisjs",
8823
- "encore"
8824
- ].includes(config.backend)) incompatibilityError({
8825
- message: "Vercel serverless functions cannot host persistent-process backends",
8826
- provided: {
8827
- backend: config.backend,
8828
- serverDeploy: config.serverDeploy
8829
- },
8830
- suggestions: ["Use --server-deploy fly or --server-deploy railway for NestJS/AdonisJS", "Switch to a serverless-compatible backend like Hono or Express"]
8831
- });
8832
- if (config.serverDeploy === "netlify") {
8833
- if (config.backend !== "hono") incompatibilityError({
8834
- message: "Netlify Functions server deploy is currently supported only with Hono",
8835
- provided: {
8836
- backend: config.backend,
8837
- serverDeploy: config.serverDeploy
8838
- },
8839
- suggestions: ["Use --backend hono", "Use --server-deploy fly, railway, render, or docker for this backend"]
8840
- });
8841
- if (config.runtime !== "node") incompatibilityError({
8842
- message: "Netlify Functions server deploy requires Node.js runtime",
8843
- provided: {
8844
- runtime: config.runtime,
8845
- serverDeploy: config.serverDeploy
8846
- },
8847
- suggestions: ["Use --runtime node", "Choose a different server deployment target"]
8848
- });
8849
- }
8850
- if (config.addons && config.addons.length > 0) {
8851
- validateAddonsAgainstFrontends(config.addons, config.frontend, config.auth, config.backend, config.runtime, config.ecosystem, config.rustFrontend, config.javaWebFramework, config.database);
8852
- config.addons = [...new Set(config.addons)];
8853
- }
8854
- validateExamplesCompatibility(config.examples ?? [], config.backend, config.frontend ?? [], config.runtime, config.ai);
8855
- validatePaymentsCompatibility(config.payments, config.auth, config.backend, config.frontend ?? []);
8856
- validateAIFrontendCompatibility(config.ai, config.frontend ?? []);
8857
- validateUILibraryFrontendCompatibility(config.uiLibrary, config.frontend ?? [], config.astroIntegration);
8858
- validateUILibraryCSSFrameworkCompatibility(config.uiLibrary, config.cssFramework);
8859
- validateShadcnConstraints(config, providedFlags);
8860
- validatePeerDependencies(config);
8861
- }
8862
- function validateConfigForProgrammaticUse(config) {
8863
- try {
8864
- if (config.stackParts) {
8865
- const graphValidation = (0, types_exports.validateStackParts)(config.stackParts);
8866
- if (graphValidation.issues.length > 0) throw new Error(graphValidation.issues.map((issue) => issue.message).join("\n"));
8867
- }
8868
- validateEcosystemAuthCompatibility(config);
8869
- validateDatabaseOrmAuth(config);
8870
- if (config.frontend && config.frontend.length > 0) ensureSingleWebAndNative(config.frontend);
8871
- validateApiFrontendCompatibility(config.api, config.frontend, config.astroIntegration);
8872
- validatePythonApiConstraints(config);
8873
- validateEmailConstraints(config);
8874
- validateObservabilityConstraints(config);
8875
- validateCachingConstraints(config);
8876
- validateRateLimitConstraints(config);
8877
- validateSearchConstraints(config);
8878
- validateJavaConstraints(config);
8879
- validateElixirConstraints(config);
8880
- validatePaymentsCompatibility(config.payments, config.auth, config.backend, config.frontend);
8881
- 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);
8882
- validateExamplesCompatibility(config.examples ?? [], config.backend, config.frontend ?? [], config.runtime, config.ai);
8883
- validateAIFrontendCompatibility(config.ai, config.frontend ?? []);
8884
- validateUILibraryFrontendCompatibility(config.uiLibrary, config.frontend ?? [], config.astroIntegration);
8885
- validateUILibraryCSSFrameworkCompatibility(config.uiLibrary, config.cssFramework);
8886
- validatePeerDependencies(config);
8887
- } catch (error) {
8888
- if (error instanceof Error) throw error;
8889
- throw new Error(String(error), { cause: error });
8890
- }
8891
- }
8892
-
8893
7615
  //#endregion
8894
7616
  //#region src/validation.ts
8895
7617
  const CORE_STACK_FLAGS = new Set([
@@ -10084,10 +8806,10 @@ async function setupDatabase(config, cliInput) {
10084
8806
  const serverDbDir = path.join(projectDir, "apps/server/src/db");
10085
8807
  if (await fs.pathExists(serverDbDir)) await fs.remove(serverDbDir);
10086
8808
  }
10087
- return;
8809
+ return null;
10088
8810
  }
10089
8811
  const dbPackageDir = path.join(projectDir, "packages/db");
10090
- if (!await fs.pathExists(dbPackageDir)) return;
8812
+ if (!await fs.pathExists(dbPackageDir)) return null;
10091
8813
  try {
10092
8814
  if (dbSetup === "docker") await setupDockerCompose(config);
10093
8815
  else if (database === "sqlite" && dbSetup === "turso") await setupTurso(config, cliInput);
@@ -10100,8 +8822,18 @@ async function setupDatabase(config, cliInput) {
10100
8822
  } else if (database === "mysql" && dbSetup === "planetscale") await setupPlanetScale(config);
10101
8823
  else if (database === "mongodb" && dbSetup === "mongodb-atlas") await setupMongoDBAtlas(config, cliInput);
10102
8824
  else if (database === "redis" && dbSetup === "upstash") await setupUpstash(config, cliInput);
8825
+ return {
8826
+ step: "Database setup",
8827
+ success: true
8828
+ };
10103
8829
  } catch (error) {
10104
- if (error instanceof Error) consola.error(pc.red(error.message));
8830
+ const errorMessage = error instanceof Error ? error.message : String(error);
8831
+ consola.error(pc.red(errorMessage));
8832
+ return {
8833
+ step: "Database setup",
8834
+ success: false,
8835
+ errorMessage
8836
+ };
10105
8837
  }
10106
8838
  }
10107
8839
 
@@ -10701,14 +9433,14 @@ function displayJavaInstructions(config) {
10701
9433
  }
10702
9434
  const effectiveJavaTestingLibraries = javaBuildTool === "none" ? [] : javaTestingLibraries.filter((library) => library !== "none");
10703
9435
  const buildToolCommand = javaBuildTool === "none" ? null : javaBuildTool === "gradle" ? process.platform === "win32" ? "gradlew.bat" : "./gradlew" : process.platform === "win32" ? "mvnw.cmd" : "./mvnw";
10704
- const runCommand$1 = buildToolCommand ? isSpringBoot ? javaBuildTool === "gradle" ? `${buildToolCommand} bootRun` : `${buildToolCommand} spring-boot:run` : isQuarkus ? javaBuildTool === "gradle" ? `${buildToolCommand} quarkusDev` : `${buildToolCommand} quarkus:dev` : javaBuildTool === "gradle" ? `${buildToolCommand} run` : `${buildToolCommand} exec:java` : null;
9436
+ const runCommand = buildToolCommand ? isSpringBoot ? javaBuildTool === "gradle" ? `${buildToolCommand} bootRun` : `${buildToolCommand} spring-boot:run` : isQuarkus ? javaBuildTool === "gradle" ? `${buildToolCommand} quarkusDev` : `${buildToolCommand} quarkus:dev` : javaBuildTool === "gradle" ? `${buildToolCommand} run` : `${buildToolCommand} exec:java` : null;
10705
9437
  const packageCommand = buildToolCommand ? javaBuildTool === "gradle" ? `${buildToolCommand} build` : `${buildToolCommand} package` : null;
10706
9438
  const sourceCompileCommand = buildToolCommand ? null : `javac -d out ${getJavaMainSourcePath(projectName)}`;
10707
9439
  const sourceRunCommand = buildToolCommand ? null : `java -cp out ${getJavaMainClass(projectName)}`;
10708
9440
  let output = `${pc.bold("Next steps")}\n${pc.cyan("1.")} ${cdCmd}\n`;
10709
9441
  let stepCounter = 2;
10710
9442
  if (!depsInstalled && buildToolCommand && effectiveJavaTestingLibraries.length > 0) output += `${pc.cyan(`${stepCounter++}.`)} ${buildToolCommand} test\n`;
10711
- if (runCommand$1) output += `${pc.cyan(`${stepCounter++}.`)} ${runCommand$1}\n`;
9443
+ if (runCommand) output += `${pc.cyan(`${stepCounter++}.`)} ${runCommand}\n`;
10712
9444
  else if (sourceCompileCommand && sourceRunCommand) {
10713
9445
  output += `${pc.cyan(`${stepCounter++}.`)} ${sourceCompileCommand}\n`;
10714
9446
  output += `${pc.cyan(`${stepCounter++}.`)} ${sourceRunCommand}\n`;
@@ -10756,9 +9488,9 @@ function displayJavaInstructions(config) {
10756
9488
  output += `${pc.cyan("•")} Testing: ${testingList}\n`;
10757
9489
  }
10758
9490
  output += `\n${pc.bold("Common Java commands:")}\n`;
10759
- if (buildToolCommand && runCommand$1 && packageCommand) {
9491
+ if (buildToolCommand && runCommand && packageCommand) {
10760
9492
  if (effectiveJavaTestingLibraries.length > 0) output += `${pc.cyan("•")} Test: ${buildToolCommand} test\n`;
10761
- output += `${pc.cyan("•")} Run: ${runCommand$1}\n`;
9493
+ output += `${pc.cyan("•")} Run: ${runCommand}\n`;
10762
9494
  output += `${pc.cyan("•")} Package: ${packageCommand}\n`;
10763
9495
  } else if (sourceCompileCommand && sourceRunCommand) {
10764
9496
  output += `${pc.cyan("•")} Compile: ${sourceCompileCommand}\n`;
@@ -10776,14 +9508,14 @@ function displayJavaInstructions(config) {
10776
9508
  function displayPythonInstructions(config) {
10777
9509
  const { relativePath, depsInstalled, pythonWebFramework, pythonOrm, pythonValidation, pythonAi, pythonApi, pythonTaskQueue, pythonQuality } = config;
10778
9510
  const cdCmd = `cd ${relativePath}`;
10779
- let runCommand$1 = "uv run uvicorn app.main:app --reload";
10780
- if (pythonWebFramework === "django") runCommand$1 = "uv run python manage.py runserver";
10781
- else if (pythonWebFramework === "flask") runCommand$1 = "uv run flask --app app.main run --reload";
10782
- else if (pythonWebFramework === "litestar") runCommand$1 = "litestar --app src.app.main:app run --reload --port 3001";
9511
+ let runCommand = "uv run uvicorn app.main:app --reload";
9512
+ if (pythonWebFramework === "django") runCommand = "uv run python manage.py runserver";
9513
+ else if (pythonWebFramework === "flask") runCommand = "uv run flask --app app.main run --reload";
9514
+ else if (pythonWebFramework === "litestar") runCommand = "litestar --app src.app.main:app run --reload --port 3001";
10783
9515
  let output = `${pc.bold("Next steps")}\n${pc.cyan("1.")} ${cdCmd}\n`;
10784
9516
  let stepCounter = 2;
10785
9517
  if (!depsInstalled) output += `${pc.cyan(`${stepCounter++}.`)} uv sync\n`;
10786
- output += `${pc.cyan(`${stepCounter++}.`)} ${runCommand$1}\n`;
9518
+ output += `${pc.cyan(`${stepCounter++}.`)} ${runCommand}\n`;
10787
9519
  output += `\n${pc.bold("Your Python project includes:")}\n`;
10788
9520
  if (pythonWebFramework && pythonWebFramework !== "none") output += `${pc.cyan("•")} Web Framework: ${{
10789
9521
  fastapi: "FastAPI",
@@ -10821,7 +9553,7 @@ function displayPythonInstructions(config) {
10821
9553
  }[pythonQuality] || pythonQuality}\n`;
10822
9554
  output += `\n${pc.bold("Common Python commands:")}\n`;
10823
9555
  output += `${pc.cyan("•")} Install: uv sync\n`;
10824
- output += `${pc.cyan("•")} Run: ${runCommand$1}\n`;
9556
+ output += `${pc.cyan("•")} Run: ${runCommand}\n`;
10825
9557
  output += `${pc.cyan("•")} Test: uv run pytest\n`;
10826
9558
  if (pythonQuality === "ruff") {
10827
9559
  output += `${pc.cyan("•")} Format: uv run ruff format .\n`;
@@ -10871,6 +9603,8 @@ function displayElixirInstructions(config) {
10871
9603
  async function createProject(options, cliInput = {}) {
10872
9604
  const projectDir = options.projectDir;
10873
9605
  const isConvex = options.backend === "convex";
9606
+ const setupFailures = [];
9607
+ const dirHadContentBefore = await fs.pathExists(projectDir) && (await fs.readdir(projectDir)).length > 0;
10874
9608
  try {
10875
9609
  await fs.ensureDir(projectDir);
10876
9610
  const [{ generateVirtualProject, EMBEDDED_TEMPLATES }, { writeTreeToFilesystem }] = await Promise.all([import("@better-fullstack/template-generator"), import("@better-fullstack/template-generator/fs-writer")]);
@@ -10882,29 +9616,53 @@ async function createProject(options, cliInput = {}) {
10882
9616
  await writeTreeToFilesystem(result.tree, projectDir);
10883
9617
  await setPackageManagerVersion(projectDir, options.packageManager);
10884
9618
  await ensurePackageManagerProjectFiles(projectDir, options.packageManager);
10885
- if (!isConvex && options.database !== "none") await setupDatabase(options, cliInput);
9619
+ if (!isConvex && options.database !== "none") {
9620
+ const dbResult = await setupDatabase(options, cliInput);
9621
+ if (dbResult && !dbResult.success) setupFailures.push(dbResult);
9622
+ }
10886
9623
  if (options.addons.length > 0 && options.addons[0] !== "none") await setupAddons(options);
10887
9624
  await applyDependencyVersionChannel(projectDir, options.versionChannel);
10888
9625
  await writeBtsConfig(options);
10889
9626
  await formatProject(projectDir);
10890
9627
  if (!isSilent()) log.success("Project template successfully scaffolded!");
10891
- if (options.install && (options.ecosystem === "typescript" || options.ecosystem === "react-native")) await installDependencies({
10892
- projectDir,
10893
- packageManager: options.packageManager
10894
- });
10895
- if (options.install && options.ecosystem === "rust") await runCargoBuild({ projectDir });
10896
- if (options.install && options.ecosystem === "python") await runUvSync({ projectDir });
10897
- if (options.install && options.ecosystem === "go") await runGoModTidy({ projectDir });
10898
- if (options.install && options.ecosystem === "java" && options.javaBuildTool !== "none") if (options.javaBuildTool === "gradle") await runGradleTests({ projectDir });
10899
- else await runMavenTests({ projectDir });
10900
- if (options.install && options.ecosystem === "elixir") await runMixCompile({ projectDir });
9628
+ if (options.install && (options.ecosystem === "typescript" || options.ecosystem === "react-native")) {
9629
+ const result$1 = await installDependencies({
9630
+ projectDir,
9631
+ packageManager: options.packageManager
9632
+ });
9633
+ if (!result$1.success) setupFailures.push(result$1);
9634
+ }
9635
+ if (options.install && options.ecosystem === "rust") {
9636
+ const result$1 = await runCargoBuild({ projectDir });
9637
+ if (!result$1.success) setupFailures.push(result$1);
9638
+ }
9639
+ if (options.install && options.ecosystem === "python") {
9640
+ const result$1 = await runUvSync({ projectDir });
9641
+ if (!result$1.success) setupFailures.push(result$1);
9642
+ }
9643
+ if (options.install && options.ecosystem === "go") {
9644
+ const result$1 = await runGoModTidy({ projectDir });
9645
+ if (!result$1.success) setupFailures.push(result$1);
9646
+ }
9647
+ if (options.install && options.ecosystem === "java" && options.javaBuildTool !== "none") {
9648
+ const result$1 = options.javaBuildTool === "gradle" ? await runGradleTests({ projectDir }) : await runMavenTests({ projectDir });
9649
+ if (!result$1.success) setupFailures.push(result$1);
9650
+ }
9651
+ if (options.install && options.ecosystem === "elixir") {
9652
+ const result$1 = await runMixCompile({ projectDir });
9653
+ if (!result$1.success) setupFailures.push(result$1);
9654
+ }
10901
9655
  await initializeGit(projectDir, options.git);
10902
9656
  if (!isSilent()) await displayPostInstallInstructions({
10903
9657
  ...options,
10904
9658
  depsInstalled: options.install
10905
9659
  });
10906
- return projectDir;
9660
+ return {
9661
+ projectDir,
9662
+ setupFailures
9663
+ };
10907
9664
  } catch (error) {
9665
+ await rollbackPartialProject(projectDir, dirHadContentBefore);
10908
9666
  if (error instanceof Error) {
10909
9667
  if (!isSilent()) console.error(error.stack);
10910
9668
  exitWithError(`Error during project creation: ${error.message}`);
@@ -10914,6 +9672,23 @@ async function createProject(options, cliInput = {}) {
10914
9672
  }
10915
9673
  }
10916
9674
  }
9675
+ /**
9676
+ * Remove a half-written project directory after a fatal scaffolding error so the
9677
+ * user is not left with a broken, partially generated project. Only removes
9678
+ * directories we created ourselves (empty/new before scaffolding) — never one
9679
+ * that already had user content (merge mode). Set BTS_KEEP_FAILED_OUTPUT=1 to
9680
+ * keep the partial output for debugging template failures.
9681
+ */
9682
+ async function rollbackPartialProject(projectDir, dirHadContentBefore) {
9683
+ if (dirHadContentBefore || process.env.BTS_KEEP_FAILED_OUTPUT) {
9684
+ if (!isSilent() && dirHadContentBefore) log.warn(`Left partially created files in ${projectDir} (directory already existed). Review and clean up manually.`);
9685
+ return;
9686
+ }
9687
+ try {
9688
+ await fs.remove(projectDir);
9689
+ if (!isSilent()) log.warn(`Cleaned up partially created project at ${projectDir}. Set BTS_KEEP_FAILED_OUTPUT=1 to keep it for debugging.`);
9690
+ } catch {}
9691
+ }
10917
9692
  async function setPackageManagerVersion(projectDir, packageManager) {
10918
9693
  const pkgJsonPath = path.join(projectDir, "package.json");
10919
9694
  if (!await fs.pathExists(pkgJsonPath)) return;
@@ -10974,6 +9749,7 @@ function getYesBaseConfig(flagConfig) {
10974
9749
  rateLimit: "none",
10975
9750
  i18n: "none",
10976
9751
  search: "none",
9752
+ vectorDb: "none",
10977
9753
  fileStorage: "none",
10978
9754
  mobileNavigation: "expo-router",
10979
9755
  mobileUI: "none",
@@ -10984,8 +9760,8 @@ function getYesBaseConfig(flagConfig) {
10984
9760
  mobileDeepLinking: "none"
10985
9761
  };
10986
9762
  }
10987
- function shouldPromptForVersionChannel(input) {
10988
- if (input.yes || input.part?.length || input.versionChannel !== void 0 || isSilent()) return false;
9763
+ function shouldPromptForVersionChannel(input, hasConfigBase) {
9764
+ if (input.yes || input.part?.length || hasConfigBase || input.versionChannel !== void 0 || isSilent()) return false;
10989
9765
  return canPromptInteractively();
10990
9766
  }
10991
9767
  async function createProjectHandler(input, options = {}) {
@@ -10996,10 +9772,15 @@ async function createProjectHandler(input, options = {}) {
10996
9772
  try {
10997
9773
  if (!isSilent() && input.renderTitle !== false) renderTitle();
10998
9774
  if (!isSilent()) intro(pc.magenta("Creating a new Better Fullstack project"));
9775
+ await maybeShowTelemetryNotice();
10999
9776
  if (!isSilent() && input.yolo) consola.fatal("YOLO mode enabled - skipping checks. Things may break!");
9777
+ const configBase = await resolveCreateConfigBase(input);
9778
+ const hasConfigBase = configBase !== void 0;
9779
+ if (hasConfigBase && !isSilent()) log.info(pc.cyan(input.config ? `Using stack from config file: ${input.config}` : `Using stack from history entry #${input.fromHistory}`));
9780
+ const useDefaultsForName = Boolean(input.yes) || hasConfigBase;
11000
9781
  let currentPathInput;
11001
- if (input.yes && input.projectName) currentPathInput = input.projectName;
11002
- else if (input.yes) {
9782
+ if (useDefaultsForName && input.projectName) currentPathInput = input.projectName;
9783
+ else if (useDefaultsForName) {
11003
9784
  const defaultConfig = getDefaultConfig();
11004
9785
  let defaultName = defaultConfig.relativePath;
11005
9786
  let counter = 1;
@@ -11009,7 +9790,7 @@ async function createProjectHandler(input, options = {}) {
11009
9790
  }
11010
9791
  currentPathInput = defaultName;
11011
9792
  } else currentPathInput = await getProjectName(input.projectName);
11012
- const versionChannel = shouldPromptForVersionChannel(input) ? await getVersionChannelChoice() : input.versionChannel ?? "stable";
9793
+ const versionChannel = shouldPromptForVersionChannel(input, hasConfigBase) ? await getVersionChannelChoice() : input.versionChannel ?? configBase?.versionChannel ?? "stable";
11013
9794
  let finalResolvedPath;
11014
9795
  let finalBaseName;
11015
9796
  if (input.dryRun) {
@@ -11088,6 +9869,7 @@ async function createProjectHandler(input, options = {}) {
11088
9869
  rateLimit: "none",
11089
9870
  i18n: "none",
11090
9871
  search: "none",
9872
+ vectorDb: "none",
11091
9873
  featureFlags: "none",
11092
9874
  analytics: "none",
11093
9875
  fileStorage: "none",
@@ -11174,11 +9956,15 @@ async function createProjectHandler(input, options = {}) {
11174
9956
  finalBaseName = setupResult.finalBaseName;
11175
9957
  currentPathInput = finalPathInput;
11176
9958
  }
11177
- const originalInput = {
11178
- ...input,
9959
+ const explicitInput = {
9960
+ ...Object.fromEntries(Object.entries(input).filter(([, value]) => value !== void 0)),
11179
9961
  projectDirectory: input.projectName
11180
9962
  };
11181
- const providedFlags = getProvidedFlags(originalInput);
9963
+ const originalInput = {
9964
+ ...configBase,
9965
+ ...explicitInput
9966
+ };
9967
+ const providedFlags = getProvidedFlags(explicitInput);
11182
9968
  let cliInput = originalInput;
11183
9969
  if (input.template && input.template !== "none") {
11184
9970
  const templateConfig = getTemplateConfig(input.template);
@@ -11201,7 +9987,7 @@ async function createProjectHandler(input, options = {}) {
11201
9987
  }
11202
9988
  const { validatePreflightConfig } = await import("@better-fullstack/template-generator");
11203
9989
  let config;
11204
- if (cliInput.yes || cliInput.part?.length) {
9990
+ if (cliInput.yes || cliInput.part?.length || hasConfigBase) {
11205
9991
  const flagConfig = processProvidedFlagsWithoutValidation(cliInput, finalBaseName);
11206
9992
  config = {
11207
9993
  ...getYesBaseConfig(flagConfig),
@@ -11277,7 +10063,7 @@ async function createProjectHandler(input, options = {}) {
11277
10063
  files
11278
10064
  };
11279
10065
  }
11280
- await createProject(config, { manualDb: cliInput.manualDb ?? input.manualDb });
10066
+ const setupFailures = (await createProject(config, { manualDb: cliInput.manualDb ?? input.manualDb }))?.setupFailures ?? [];
11281
10067
  if (cliInput.verify ?? input.verify) await runGeneratedChecks(config);
11282
10068
  const reproducibleCommand = generateReproducibleCommand(config);
11283
10069
  if (!isSilent()) log.success(pc.blue(`You can reproduce this setup with the following command:\n${reproducibleCommand}`));
@@ -11290,7 +10076,12 @@ async function createProjectHandler(input, options = {}) {
11290
10076
  const elapsedTimeMs = Date.now() - startTime;
11291
10077
  if (!isSilent()) {
11292
10078
  const elapsedTimeInSeconds = (elapsedTimeMs / 1e3).toFixed(2);
11293
- outro(pc.magenta(`Project created successfully in ${pc.bold(elapsedTimeInSeconds)} seconds!`));
10079
+ if (setupFailures.length > 0) {
10080
+ const stepList = setupFailures.map((f) => f.step).join(", ");
10081
+ const installCmd = config.packageManager === "npm" ? "npm install" : `${config.packageManager} install`;
10082
+ log.warn(pc.yellow(`Project files were scaffolded in ${config.relativePath}, but ${setupFailures.length} setup step(s) did not complete: ${stepList}.\nReview the errors above, then finish setup manually (for example, run '${installCmd}' inside the project).`));
10083
+ outro(pc.yellow(`Project created with ${setupFailures.length} unfinished setup step(s) in ${pc.bold(elapsedTimeInSeconds)}s.`));
10084
+ } else outro(pc.magenta(`Project created successfully in ${pc.bold(elapsedTimeInSeconds)} seconds!`));
11294
10085
  }
11295
10086
  return {
11296
10087
  success: true,
@@ -11299,7 +10090,8 @@ async function createProjectHandler(input, options = {}) {
11299
10090
  timeScaffolded,
11300
10091
  elapsedTimeMs,
11301
10092
  projectDirectory: config.projectDir,
11302
- relativePath: config.relativePath
10093
+ relativePath: config.relativePath,
10094
+ setupFailures
11303
10095
  };
11304
10096
  } catch (error) {
11305
10097
  if (error instanceof UserCancelledError) {
@@ -11476,7 +10268,7 @@ const router = os.router({
11476
10268
  packageManager: types_exports.PackageManagerSchema.optional().describe("Package manager to use"),
11477
10269
  projectDir: z.string().optional().describe("Project directory (defaults to current)")
11478
10270
  })).handler(async ({ input }) => {
11479
- const { addHandler } = await import("./add-handler-BQN6pwGJ.mjs");
10271
+ const { addHandler } = await import("./add-handler-9F-AsGM-.mjs");
11480
10272
  await addHandler(input);
11481
10273
  }),
11482
10274
  history: os.meta({ description: "Show history of scaffolded projects with reproducible commands" }).input(z.object({
@@ -11486,6 +10278,17 @@ const router = os.router({
11486
10278
  })).handler(async ({ input }) => {
11487
10279
  await historyHandler(input);
11488
10280
  }),
10281
+ telemetry: os.meta({ description: "View or change anonymous usage telemetry collection (status | enable | disable)" }).input(z.tuple([z.enum([
10282
+ "status",
10283
+ "enable",
10284
+ "disable"
10285
+ ]).optional().default("status").describe("Action to perform: status (default), enable, or disable"), z.object({ json: z.boolean().optional().default(false).describe("Output status as JSON") })])).handler(async ({ input }) => {
10286
+ const [action, options] = input;
10287
+ await telemetryHandler({
10288
+ action,
10289
+ json: options.json
10290
+ });
10291
+ }),
11489
10292
  "update-deps": os.meta({ description: "Check and update dependency versions in add-deps.ts" }).input(z.object({
11490
10293
  check: z.boolean().default(false).describe("Report only, no changes"),
11491
10294
  patch: z.boolean().default(false).describe("Apply patch/minor updates only"),
@@ -11493,7 +10296,7 @@ const router = os.router({
11493
10296
  ecosystem: z.string().optional().describe("Filter by ecosystem (effect, tanstack, prisma, etc.)"),
11494
10297
  "list-ecosystems": z.boolean().default(false).describe("List available ecosystems")
11495
10298
  })).handler(async ({ input }) => {
11496
- const { updateDepsHandler, showEcosystems } = await import("./update-deps-CLebIM70.mjs");
10299
+ const { updateDepsHandler, showEcosystems } = await import("./update-deps-D5OG0KmJ.mjs");
11497
10300
  if (input["list-ecosystems"]) {
11498
10301
  showEcosystems();
11499
10302
  return;
@@ -11508,6 +10311,17 @@ const router = os.router({
11508
10311
  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 () => {
11509
10312
  log.message("MCP server is started via the 'mcp' subcommand intercepted in cli.ts.");
11510
10313
  log.message("Run: create-better-fullstack mcp");
10314
+ }),
10315
+ 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(z.tuple([z.string().optional().describe("Project directory to diagnose (defaults to current directory)"), z.object({
10316
+ skipChecks: z.boolean().optional().default(false).describe("Skip the ecosystem build/type checks (config + deps + env only)"),
10317
+ json: z.boolean().optional().default(false).describe("Output the diagnosis as JSON")
10318
+ })])).handler(async ({ input }) => {
10319
+ const [projectDir, options] = input;
10320
+ const { doctorCommand } = await import("./doctor-BFSSbS-U.mjs");
10321
+ await doctorCommand({
10322
+ projectDir,
10323
+ ...options
10324
+ });
11511
10325
  })
11512
10326
  });
11513
10327
  const caller = createRouterClient(router, { context: {} });
@@ -11573,7 +10387,7 @@ async function builder() {
11573
10387
  return caller.builder();
11574
10388
  }
11575
10389
  async function add(input) {
11576
- const { addHandler } = await import("./add-handler-BQN6pwGJ.mjs");
10390
+ const { addHandler } = await import("./add-handler-9F-AsGM-.mjs");
11577
10391
  return addHandler(input, { silent: true });
11578
10392
  }
11579
10393
  async function history(options) {
@@ -11583,6 +10397,9 @@ async function history(options) {
11583
10397
  json: options?.json ?? false
11584
10398
  });
11585
10399
  }
10400
+ async function telemetry(action = "status", options) {
10401
+ return caller.telemetry([action, { json: options?.json ?? false }]);
10402
+ }
11586
10403
 
11587
10404
  //#endregion
11588
- export { docs as a, sponsors as c, createBtsCli as i, builder as n, history as o, create as r, router as s, add as t };
10405
+ export { docs as a, sponsors as c, createBtsCli as i, telemetry as l, builder as n, history as o, create as r, router as s, add as t };