create-better-fullstack 2.0.3 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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 { c as DEFAULT_CONFIG, d as getUserPkgManager, g as getGraphSummary, h as getGraphPart, i as readBtsConfigFromFile, l as DEFAULT_UI_LIBRARY_BY_FRONTEND, m as getGraphBackendUrl, o as writeBtsConfig, p as getGraphBackendDeployInstructions, s as getLatestCLIVersion, t as buildBtsConfigForPersistence, u as getDefaultConfig, v as hasGraphPart, y as types_exports } from "./bts-config-InNcw1aJ.mjs";
3
+ import { t as renderTitle } from "./render-title-zvyKC1ej.mjs";
4
+ import { a as handleError, c as isSilent, d as setLastPromptShownUI, i as exitWithError, l as runWithContextAsync, n as UserCancelledError, o as didLastPromptShowUI, r as exitCancelled, s as isFirstPrompt, t as CLIError, u as setIsFirstPrompt } from "./errors-ns_o2OKg.mjs";
5
+ import { i as canPromptInteractively, n as getPackageExecutionArgs, r as addPackageDependency, t as setupAddons } from "./addons-setup-uSvqHagW.mjs";
6
+ 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, _ 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$1, 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-CDjTNvIV.mjs";
7
+ import { n as commandExists, t as runGeneratedChecks } from "./generated-checks-CUhFFfVo.mjs";
8
+ import { n as getTemplateDescription, r as generateReproducibleCommand, t as getTemplateConfig } from "./templates-B351F1yX.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);
@@ -133,10 +155,195 @@ async function historyHandler(input) {
133
155
  }
134
156
  }
135
157
 
158
+ //#endregion
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
+ }
340
+
136
341
  //#endregion
137
342
  //#region src/create-command-input.ts
138
343
  const CreateCommandOptionsSchema = z.object({
139
344
  template: types_exports.TemplateSchema.optional().describe("Use a predefined template"),
345
+ fromHistory: z.number().optional().describe("Replay the stack of the Nth most-recent project from history (1 = most recent)"),
346
+ config: z.string().optional().describe("Path to a bts.jsonc/JSON config file to use as the base stack"),
140
347
  yes: z.boolean().optional().default(false).describe("Use default configuration"),
141
348
  yolo: z.boolean().optional().default(false).describe("(WARNING - NOT RECOMMENDED) Bypass validations and compatibility checks"),
142
349
  part: z.array(z.string()).optional().describe("Stack graph part binding, e.g. frontend:typescript:next or backend.orm:go:gorm"),
@@ -168,6 +375,7 @@ const CreateCommandOptionsSchema = z.object({
168
375
  rateLimit: types_exports.RateLimitSchema.optional().describe("Rate limiting solution"),
169
376
  i18n: types_exports.I18nSchema.optional().describe("Internationalization (i18n) library"),
170
377
  search: types_exports.SearchSchema.optional().describe("Search engine solution"),
378
+ vectorDb: types_exports.VectorDbSchema.optional().describe("Vector database for AI embeddings (pgvector, qdrant, chroma, pinecone)"),
171
379
  fileStorage: types_exports.FileStorageSchema.optional().describe("File storage solution (S3, R2)"),
172
380
  mobileNavigation: types_exports.MobileNavigationSchema.optional().describe("Mobile navigation (expo-router, react-navigation)"),
173
381
  mobileUI: types_exports.MobileUISchema.optional().describe("Mobile UI (tamagui, gluestack-ui, uniwind, unistyles)"),
@@ -400,7 +608,7 @@ const AI_DOCS_OPTIONS = [
400
608
  },
401
609
  {
402
610
  value: "agents-md",
403
- label: "Agents.md",
611
+ label: "AGENTS.md",
404
612
  hint: "Generic AI assistant documentation"
405
613
  },
406
614
  {
@@ -531,6 +739,11 @@ const API_PROMPT_OPTION_MAP = {
531
739
  label: "GraphQL Yoga",
532
740
  hint: "Batteries-included GraphQL server with Pothos schema builder"
533
741
  },
742
+ "apollo-server": {
743
+ value: "apollo-server",
744
+ label: "Apollo Server",
745
+ hint: "Spec-compliant GraphQL server with a schema-first starter"
746
+ },
534
747
  openapi: {
535
748
  value: "openapi",
536
749
  label: "OpenAPI",
@@ -899,6 +1112,11 @@ const CMS_PROMPT_OPTIONS = [
899
1112
  label: "Directus",
900
1113
  hint: "Open data platform and headless CMS for SQL databases"
901
1114
  },
1115
+ {
1116
+ value: "keystatic",
1117
+ label: "Keystatic",
1118
+ hint: "Git-backed CMS for Markdown, JSON, and YAML content"
1119
+ },
902
1120
  {
903
1121
  value: "none",
904
1122
  label: "None",
@@ -2361,7 +2579,7 @@ async function getFrontendChoice(frontendOptions, backend, auth) {
2361
2579
  });
2362
2580
  if (isGoBack(frontendTypes)) return GO_BACK_SYMBOL;
2363
2581
  if (isCancel$1(frontendTypes)) return exitCancelled("Operation cancelled");
2364
- setIsFirstPrompt(false);
2582
+ setIsFirstPrompt$1(false);
2365
2583
  const result = [];
2366
2584
  let shouldRestart = false;
2367
2585
  if (frontendTypes.includes("web")) {
@@ -2377,7 +2595,7 @@ async function getFrontendChoice(frontendOptions, backend, auth) {
2377
2595
  else result.push(webFramework);
2378
2596
  }
2379
2597
  if (shouldRestart) {
2380
- setIsFirstPrompt(wasFirstPrompt);
2598
+ setIsFirstPrompt$1(wasFirstPrompt);
2381
2599
  continue;
2382
2600
  }
2383
2601
  if (frontendTypes.includes("native")) {
@@ -2390,14 +2608,14 @@ async function getFrontendChoice(frontendOptions, backend, auth) {
2390
2608
  });
2391
2609
  if (isGoBack(nativeFramework)) if (frontendTypes.includes("web")) shouldRestart = true;
2392
2610
  else {
2393
- setIsFirstPrompt(wasFirstPrompt);
2611
+ setIsFirstPrompt$1(wasFirstPrompt);
2394
2612
  continue;
2395
2613
  }
2396
2614
  else if (isCancel$1(nativeFramework)) return exitCancelled("Operation cancelled");
2397
2615
  else result.push(nativeFramework);
2398
2616
  }
2399
2617
  if (shouldRestart) {
2400
- setIsFirstPrompt(wasFirstPrompt);
2618
+ setIsFirstPrompt$1(wasFirstPrompt);
2401
2619
  continue;
2402
2620
  }
2403
2621
  return result;
@@ -2832,17 +3050,36 @@ async function getGoObservabilityChoice(goObservability) {
2832
3050
 
2833
3051
  //#endregion
2834
3052
  //#region src/prompts/i18n.ts
3053
+ const PARAGLIDE_FRONTENDS = new Set([
3054
+ "next",
3055
+ "nuxt",
3056
+ "vinext",
3057
+ "tanstack-router",
3058
+ "tanstack-start",
3059
+ "react-router",
3060
+ "react-vite",
3061
+ "svelte",
3062
+ "solid",
3063
+ "solid-start",
3064
+ "astro"
3065
+ ]);
2835
3066
  async function getI18nChoice(i18n, frontend) {
2836
3067
  if (i18n !== void 0) return i18n;
3068
+ const hasNext = frontend?.includes("next") ?? false;
2837
3069
  const response = await navigableSelect({
2838
3070
  message: "Select internationalization (i18n) library",
2839
3071
  options: [
3072
+ ...frontend?.some((f) => PARAGLIDE_FRONTENDS.has(f)) ?? false ? [{
3073
+ value: "paraglide",
3074
+ label: "Paraglide",
3075
+ hint: "Type-safe, compiler-based i18n for modern web frontends"
3076
+ }] : [],
2840
3077
  {
2841
3078
  value: "i18next",
2842
3079
  label: "i18next",
2843
3080
  hint: "Full-featured i18n framework, works with all frontends"
2844
3081
  },
2845
- ...frontend?.includes("next") ?? false ? [{
3082
+ ...hasNext ? [{
2846
3083
  value: "next-intl",
2847
3084
  label: "next-intl",
2848
3085
  hint: "Lightweight i18n for Next.js with App Router support"
@@ -2859,17 +3096,6 @@ async function getI18nChoice(i18n, frontend) {
2859
3096
  return response;
2860
3097
  }
2861
3098
 
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
3099
  //#endregion
2874
3100
  //#region src/prompts/install.ts
2875
3101
  async function getinstallChoice(install, ecosystem, javaBuildTool) {
@@ -5618,7 +5844,7 @@ async function navigableGroup(prompts, opts) {
5618
5844
  while (currentIndex < promptNames.length) {
5619
5845
  const name = promptNames[currentIndex];
5620
5846
  const prompt = prompts[name];
5621
- setIsFirstPrompt$1(currentIndex === 0);
5847
+ setIsFirstPrompt(currentIndex === 0);
5622
5848
  setLastPromptShownUI(false);
5623
5849
  const result = await prompt({ results })?.catch((e) => {
5624
5850
  throw e;
@@ -5639,7 +5865,7 @@ async function navigableGroup(prompts, opts) {
5639
5865
  results[name] = "canceled";
5640
5866
  opts.onCancel({ results });
5641
5867
  }
5642
- setIsFirstPrompt$1(false);
5868
+ setIsFirstPrompt(false);
5643
5869
  return results;
5644
5870
  }
5645
5871
  if (goingBack && !didLastPromptShowUI()) {
@@ -5654,7 +5880,7 @@ async function navigableGroup(prompts, opts) {
5654
5880
  results[name] = result;
5655
5881
  currentIndex++;
5656
5882
  }
5657
- setIsFirstPrompt$1(false);
5883
+ setIsFirstPrompt(false);
5658
5884
  return results;
5659
5885
  }
5660
5886
 
@@ -6095,6 +6321,11 @@ const SEARCH_PROMPT_OPTIONS = [
6095
6321
  label: "Elasticsearch",
6096
6322
  hint: "Distributed search and analytics engine with local and cloud deployments"
6097
6323
  },
6324
+ {
6325
+ value: "opensearch",
6326
+ label: "OpenSearch",
6327
+ hint: "Open-source search and analytics suite compatible with Elasticsearch APIs"
6328
+ },
6098
6329
  {
6099
6330
  value: "algolia",
6100
6331
  label: "Algolia",
@@ -6149,6 +6380,77 @@ async function getSearchChoice(search, backend, ecosystem) {
6149
6380
  return response;
6150
6381
  }
6151
6382
 
6383
+ //#endregion
6384
+ //#region src/prompts/vector-db.ts
6385
+ const VECTOR_DB_PROMPT_OPTIONS = [
6386
+ {
6387
+ value: "pgvector",
6388
+ label: "pgvector",
6389
+ hint: "Self-hosted Postgres + pgvector extension for embeddings"
6390
+ },
6391
+ {
6392
+ value: "qdrant",
6393
+ label: "Qdrant",
6394
+ hint: "High-performance open-source vector database"
6395
+ },
6396
+ {
6397
+ value: "chroma",
6398
+ label: "Chroma",
6399
+ hint: "Lightweight open-source embedding database"
6400
+ },
6401
+ {
6402
+ value: "pinecone",
6403
+ label: "Pinecone",
6404
+ hint: "Fully managed serverless vector database"
6405
+ },
6406
+ {
6407
+ value: "none",
6408
+ label: "None",
6409
+ hint: "Skip vector database setup"
6410
+ }
6411
+ ];
6412
+ /**
6413
+ * Vector DB is a TypeScript-ecosystem feature backed by a standalone server.
6414
+ * Every provider (including pgvector via a dedicated Postgres instance) is a
6415
+ * separate service, so there is no dependency on the primary database choice.
6416
+ */
6417
+ function resolveVectorDbPrompt(context = {}) {
6418
+ const skip = () => ({
6419
+ shouldPrompt: false,
6420
+ mode: "single",
6421
+ options: [],
6422
+ autoValue: "none"
6423
+ });
6424
+ if (context.ecosystem && context.ecosystem !== "typescript") return skip();
6425
+ if (context.backend === "none" || context.backend === "convex") return skip();
6426
+ return context.vectorDb !== void 0 ? {
6427
+ shouldPrompt: false,
6428
+ mode: "single",
6429
+ options: VECTOR_DB_PROMPT_OPTIONS,
6430
+ autoValue: context.vectorDb
6431
+ } : {
6432
+ shouldPrompt: true,
6433
+ mode: "single",
6434
+ options: VECTOR_DB_PROMPT_OPTIONS,
6435
+ initialValue: "none"
6436
+ };
6437
+ }
6438
+ async function getVectorDbChoice(vectorDb, backend, ecosystem) {
6439
+ const resolution = resolveVectorDbPrompt({
6440
+ vectorDb,
6441
+ backend,
6442
+ ecosystem
6443
+ });
6444
+ if (!resolution.shouldPrompt) return resolution.autoValue ?? "none";
6445
+ const response = await navigableSelect({
6446
+ message: "Select vector database",
6447
+ options: resolution.options,
6448
+ initialValue: resolution.initialValue
6449
+ });
6450
+ if (isCancel$1(response)) return exitCancelled("Operation cancelled");
6451
+ return response;
6452
+ }
6453
+
6152
6454
  //#endregion
6153
6455
  //#region src/prompts/server-deploy.ts
6154
6456
  async function getServerDeploymentChoice(deployment, runtime, backend, _webDeploy) {
@@ -6442,7 +6744,7 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
6442
6744
  },
6443
6745
  addons: ({ results }) => {
6444
6746
  if (results.ecosystem !== "typescript") {
6445
- const nonTypeScriptAddons = (flags.addons ?? []).filter((addon) => addon === "docker-compose");
6747
+ const nonTypeScriptAddons = (flags.addons ?? []).filter((addon) => addon === "docker-compose" || addon === "devcontainer" || addon === "github-actions");
6446
6748
  return Promise.resolve(nonTypeScriptAddons);
6447
6749
  }
6448
6750
  return getAddonsChoice(flags.addons, results.frontend, results.auth, results.backend, results.runtime);
@@ -6536,6 +6838,10 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
6536
6838
  if (results.ecosystem === "react-native" || results.ecosystem === "elixir") return Promise.resolve("none");
6537
6839
  return getSearchChoice(flags.search, results.backend, results.ecosystem);
6538
6840
  },
6841
+ vectorDb: ({ results }) => {
6842
+ if (results.ecosystem !== "typescript") return Promise.resolve("none");
6843
+ return getVectorDbChoice(flags.vectorDb, results.backend, results.ecosystem);
6844
+ },
6539
6845
  fileStorage: ({ results }) => {
6540
6846
  if (results.ecosystem !== "typescript") return Promise.resolve("none");
6541
6847
  return getFileStorageChoice(flags.fileStorage, results.backend);
@@ -6943,6 +7249,7 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
6943
7249
  rateLimit: result.rateLimit,
6944
7250
  i18n: result.i18n,
6945
7251
  search: result.search,
7252
+ vectorDb: result.vectorDb,
6946
7253
  fileStorage: result.fileStorage,
6947
7254
  mobileNavigation: result.mobileNavigation,
6948
7255
  mobileUI: result.mobileUI,
@@ -7111,35 +7418,76 @@ async function getVersionChannelChoice(versionChannel) {
7111
7418
  }
7112
7419
 
7113
7420
  //#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;
7421
+ //#region src/utils/config-source.ts
7422
+ /**
7423
+ * CreateInput flag keys that can be sourced from a stored config. `projectName`
7424
+ * is excluded so a replayed config never reuses the original project name.
7425
+ */
7426
+ const COPYABLE_CREATE_INPUT_KEYS = Object.keys(types_exports.CreateInputSchema.shape).filter((key) => key !== "projectName");
7427
+ /**
7428
+ * Projects a persisted Better-Fullstack config (bts.jsonc shape) onto the
7429
+ * subset of `create` flags it can drive. Only defined values are copied so the
7430
+ * result can be safely overlaid by explicitly-passed CLI flags.
7431
+ */
7432
+ function betterTStackConfigToCreateInput(config) {
7433
+ const source = config;
7434
+ const result = {};
7435
+ for (const key of COPYABLE_CREATE_INPUT_KEYS) {
7436
+ const value = source[key];
7437
+ if (value !== void 0 && value !== null) result[key] = value;
7438
+ }
7439
+ return result;
7122
7440
  }
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 {}
7441
+ /**
7442
+ * Maps a legacy history entry (written before the full config snapshot existed)
7443
+ * onto `create` flags using the limited stack summary it stored.
7444
+ */
7445
+ function historyStackToCreateInput(stack) {
7446
+ return {
7447
+ frontend: stack.frontend,
7448
+ backend: stack.backend,
7449
+ database: stack.database,
7450
+ orm: stack.orm,
7451
+ runtime: stack.runtime,
7452
+ auth: stack.auth,
7453
+ payments: stack.payments,
7454
+ api: stack.api,
7455
+ addons: stack.addons,
7456
+ examples: stack.examples,
7457
+ dbSetup: stack.dbSetup,
7458
+ packageManager: stack.packageManager
7459
+ };
7131
7460
  }
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 {}
7461
+ /**
7462
+ * Resolves the `--from-history` / `--config` flags into a base set of `create`
7463
+ * flags. Returns `undefined` when neither flag is used. The two flags are
7464
+ * mutually exclusive, and both are incompatible with `--yes`, `--template`, and
7465
+ * `--part` (each of those also wants to own the full stack). On any user error
7466
+ * this calls `exitWithError`, which exits the CLI (or throws a CLIError in
7467
+ * silent/programmatic mode).
7468
+ */
7469
+ async function resolveCreateConfigBase(input) {
7470
+ const hasHistory = input.fromHistory !== void 0;
7471
+ const hasConfig = input.config !== void 0 && input.config !== "";
7472
+ if (!hasHistory && !hasConfig) return;
7473
+ if (hasHistory && hasConfig) exitWithError("Cannot combine --from-history with --config. Pass only one config source.");
7474
+ const sourceFlag = hasHistory ? "--from-history" : "--config";
7475
+ if (input.yes) exitWithError(`Cannot combine --yes with ${sourceFlag}: the config already provides a complete stack. Remove --yes.`);
7476
+ if (input.template && input.template !== "none") exitWithError(`Cannot combine --template with ${sourceFlag}. Choose either a template or a saved config as the base.`);
7477
+ 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.`);
7478
+ if (hasConfig) {
7479
+ const loaded = await readBtsConfigFromFile(input.config);
7480
+ if (!loaded) exitWithError(`Could not load config file: ${input.config}. Ensure the path points to a valid bts.jsonc/JSON config.`);
7481
+ return betterTStackConfigToCreateInput(loaded);
7482
+ }
7483
+ const position = input.fromHistory;
7484
+ if (!Number.isInteger(position) || position < 1) exitWithError(`Invalid --from-history value: ${position}. Provide a positive integer (1 = most recent).`);
7485
+ const entry = await getHistoryEntry(position);
7486
+ if (!entry) {
7487
+ const count = await getHistoryCount();
7488
+ exitWithError(`No project history entry at position ${position}.${count === 0 ? " Project history is empty." : ` History has ${count} ${count === 1 ? "entry" : "entries"} (use 1-${count}).`}`);
7489
+ }
7490
+ return entry.config ? betterTStackConfigToCreateInput(entry.config) : historyStackToCreateInput(entry.stack);
7143
7491
  }
7144
7492
 
7145
7493
  //#endregion
@@ -7284,480 +7632,6 @@ function displayConfig(config) {
7284
7632
  return configDisplay.join("\n");
7285
7633
  }
7286
7634
 
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
7635
  //#endregion
7762
7636
  //#region src/utils/preflight-display.ts
7763
7637
  function displayPreflightWarnings({ warnings }) {
@@ -7861,87 +7735,6 @@ async function setupProjectDirectory(finalPathInput, shouldClearDirectory) {
7861
7735
  };
7862
7736
  }
7863
7737
 
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
7738
  //#endregion
7946
7739
  //#region src/utils/config-processing.ts
7947
7740
  function deriveProjectName(projectName, projectDirectory) {
@@ -8445,16 +8238,17 @@ function validateFrontendConstraints(config, providedFlags) {
8445
8238
  validateWebDeployFrontendTemplates(config.webDeploy, frontend);
8446
8239
  }
8447
8240
  function validateApiConstraints(config, _options) {
8448
- if (config.api !== "openapi") return;
8241
+ if (config.api !== "openapi" && config.api !== "apollo-server") return;
8242
+ const apiDisplayName = config.api === "apollo-server" ? "Apollo Server" : "OpenAPI";
8449
8243
  const frontend = config.frontend ?? [];
8450
8244
  if (frontend.some((item) => [
8451
8245
  "native-bare",
8452
8246
  "native-uniwind",
8453
8247
  "native-unistyles"
8454
8248
  ].includes(item))) incompatibilityError({
8455
- message: "OpenAPI is currently available for web frontends, not React Native.",
8249
+ message: `${apiDisplayName} is currently available for web frontends, not React Native.`,
8456
8250
  provided: {
8457
- api: "openapi",
8251
+ api: config.api,
8458
8252
  frontend
8459
8253
  },
8460
8254
  suggestions: [
@@ -8469,9 +8263,9 @@ function validateApiConstraints(config, _options) {
8469
8263
  "fastify",
8470
8264
  "elysia"
8471
8265
  ].includes(config.backend)) incompatibilityError({
8472
- message: "OpenAPI currently supports Hono, Express, Fastify, and Elysia backends.",
8266
+ message: `${apiDisplayName} currently supports Hono, Express, Fastify, and Elysia backends.`,
8473
8267
  provided: {
8474
- api: "openapi",
8268
+ api: config.api,
8475
8269
  backend: config.backend ?? "none"
8476
8270
  },
8477
8271
  suggestions: [
@@ -8740,10 +8534,11 @@ function validateRateLimitConstraints(config) {
8740
8534
  }
8741
8535
  function validateSearchConstraints(config) {
8742
8536
  if (!config.search || config.search === "none") return;
8743
- if (config.ecosystem !== "typescript" && config.search !== "meilisearch") incompatibilityError({
8537
+ const ecosystem = config.ecosystem ?? "typescript";
8538
+ if (ecosystem !== "typescript" && config.search !== "meilisearch") incompatibilityError({
8744
8539
  message: "Only Meilisearch search is available for non-TypeScript ecosystems.",
8745
8540
  provided: {
8746
- ecosystem: config.ecosystem ?? "typescript",
8541
+ ecosystem,
8747
8542
  search: config.search
8748
8543
  },
8749
8544
  suggestions: ["Use --search meilisearch", "Use --search none"]
@@ -10084,10 +9879,10 @@ async function setupDatabase(config, cliInput) {
10084
9879
  const serverDbDir = path.join(projectDir, "apps/server/src/db");
10085
9880
  if (await fs.pathExists(serverDbDir)) await fs.remove(serverDbDir);
10086
9881
  }
10087
- return;
9882
+ return null;
10088
9883
  }
10089
9884
  const dbPackageDir = path.join(projectDir, "packages/db");
10090
- if (!await fs.pathExists(dbPackageDir)) return;
9885
+ if (!await fs.pathExists(dbPackageDir)) return null;
10091
9886
  try {
10092
9887
  if (dbSetup === "docker") await setupDockerCompose(config);
10093
9888
  else if (database === "sqlite" && dbSetup === "turso") await setupTurso(config, cliInput);
@@ -10100,8 +9895,18 @@ async function setupDatabase(config, cliInput) {
10100
9895
  } else if (database === "mysql" && dbSetup === "planetscale") await setupPlanetScale(config);
10101
9896
  else if (database === "mongodb" && dbSetup === "mongodb-atlas") await setupMongoDBAtlas(config, cliInput);
10102
9897
  else if (database === "redis" && dbSetup === "upstash") await setupUpstash(config, cliInput);
9898
+ return {
9899
+ step: "Database setup",
9900
+ success: true
9901
+ };
10103
9902
  } catch (error) {
10104
- if (error instanceof Error) consola.error(pc.red(error.message));
9903
+ const errorMessage = error instanceof Error ? error.message : String(error);
9904
+ consola.error(pc.red(errorMessage));
9905
+ return {
9906
+ step: "Database setup",
9907
+ success: false,
9908
+ errorMessage
9909
+ };
10105
9910
  }
10106
9911
  }
10107
9912
 
@@ -10701,14 +10506,14 @@ function displayJavaInstructions(config) {
10701
10506
  }
10702
10507
  const effectiveJavaTestingLibraries = javaBuildTool === "none" ? [] : javaTestingLibraries.filter((library) => library !== "none");
10703
10508
  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;
10509
+ 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
10510
  const packageCommand = buildToolCommand ? javaBuildTool === "gradle" ? `${buildToolCommand} build` : `${buildToolCommand} package` : null;
10706
10511
  const sourceCompileCommand = buildToolCommand ? null : `javac -d out ${getJavaMainSourcePath(projectName)}`;
10707
10512
  const sourceRunCommand = buildToolCommand ? null : `java -cp out ${getJavaMainClass(projectName)}`;
10708
10513
  let output = `${pc.bold("Next steps")}\n${pc.cyan("1.")} ${cdCmd}\n`;
10709
10514
  let stepCounter = 2;
10710
10515
  if (!depsInstalled && buildToolCommand && effectiveJavaTestingLibraries.length > 0) output += `${pc.cyan(`${stepCounter++}.`)} ${buildToolCommand} test\n`;
10711
- if (runCommand$1) output += `${pc.cyan(`${stepCounter++}.`)} ${runCommand$1}\n`;
10516
+ if (runCommand) output += `${pc.cyan(`${stepCounter++}.`)} ${runCommand}\n`;
10712
10517
  else if (sourceCompileCommand && sourceRunCommand) {
10713
10518
  output += `${pc.cyan(`${stepCounter++}.`)} ${sourceCompileCommand}\n`;
10714
10519
  output += `${pc.cyan(`${stepCounter++}.`)} ${sourceRunCommand}\n`;
@@ -10756,9 +10561,9 @@ function displayJavaInstructions(config) {
10756
10561
  output += `${pc.cyan("•")} Testing: ${testingList}\n`;
10757
10562
  }
10758
10563
  output += `\n${pc.bold("Common Java commands:")}\n`;
10759
- if (buildToolCommand && runCommand$1 && packageCommand) {
10564
+ if (buildToolCommand && runCommand && packageCommand) {
10760
10565
  if (effectiveJavaTestingLibraries.length > 0) output += `${pc.cyan("•")} Test: ${buildToolCommand} test\n`;
10761
- output += `${pc.cyan("•")} Run: ${runCommand$1}\n`;
10566
+ output += `${pc.cyan("•")} Run: ${runCommand}\n`;
10762
10567
  output += `${pc.cyan("•")} Package: ${packageCommand}\n`;
10763
10568
  } else if (sourceCompileCommand && sourceRunCommand) {
10764
10569
  output += `${pc.cyan("•")} Compile: ${sourceCompileCommand}\n`;
@@ -10776,14 +10581,14 @@ function displayJavaInstructions(config) {
10776
10581
  function displayPythonInstructions(config) {
10777
10582
  const { relativePath, depsInstalled, pythonWebFramework, pythonOrm, pythonValidation, pythonAi, pythonApi, pythonTaskQueue, pythonQuality } = config;
10778
10583
  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";
10584
+ let runCommand = "uv run uvicorn app.main:app --reload";
10585
+ if (pythonWebFramework === "django") runCommand = "uv run python manage.py runserver";
10586
+ else if (pythonWebFramework === "flask") runCommand = "uv run flask --app app.main run --reload";
10587
+ else if (pythonWebFramework === "litestar") runCommand = "litestar --app src.app.main:app run --reload --port 3001";
10783
10588
  let output = `${pc.bold("Next steps")}\n${pc.cyan("1.")} ${cdCmd}\n`;
10784
10589
  let stepCounter = 2;
10785
10590
  if (!depsInstalled) output += `${pc.cyan(`${stepCounter++}.`)} uv sync\n`;
10786
- output += `${pc.cyan(`${stepCounter++}.`)} ${runCommand$1}\n`;
10591
+ output += `${pc.cyan(`${stepCounter++}.`)} ${runCommand}\n`;
10787
10592
  output += `\n${pc.bold("Your Python project includes:")}\n`;
10788
10593
  if (pythonWebFramework && pythonWebFramework !== "none") output += `${pc.cyan("•")} Web Framework: ${{
10789
10594
  fastapi: "FastAPI",
@@ -10821,7 +10626,7 @@ function displayPythonInstructions(config) {
10821
10626
  }[pythonQuality] || pythonQuality}\n`;
10822
10627
  output += `\n${pc.bold("Common Python commands:")}\n`;
10823
10628
  output += `${pc.cyan("•")} Install: uv sync\n`;
10824
- output += `${pc.cyan("•")} Run: ${runCommand$1}\n`;
10629
+ output += `${pc.cyan("•")} Run: ${runCommand}\n`;
10825
10630
  output += `${pc.cyan("•")} Test: uv run pytest\n`;
10826
10631
  if (pythonQuality === "ruff") {
10827
10632
  output += `${pc.cyan("•")} Format: uv run ruff format .\n`;
@@ -10871,6 +10676,8 @@ function displayElixirInstructions(config) {
10871
10676
  async function createProject(options, cliInput = {}) {
10872
10677
  const projectDir = options.projectDir;
10873
10678
  const isConvex = options.backend === "convex";
10679
+ const setupFailures = [];
10680
+ const dirHadContentBefore = await fs.pathExists(projectDir) && (await fs.readdir(projectDir)).length > 0;
10874
10681
  try {
10875
10682
  await fs.ensureDir(projectDir);
10876
10683
  const [{ generateVirtualProject, EMBEDDED_TEMPLATES }, { writeTreeToFilesystem }] = await Promise.all([import("@better-fullstack/template-generator"), import("@better-fullstack/template-generator/fs-writer")]);
@@ -10882,29 +10689,53 @@ async function createProject(options, cliInput = {}) {
10882
10689
  await writeTreeToFilesystem(result.tree, projectDir);
10883
10690
  await setPackageManagerVersion(projectDir, options.packageManager);
10884
10691
  await ensurePackageManagerProjectFiles(projectDir, options.packageManager);
10885
- if (!isConvex && options.database !== "none") await setupDatabase(options, cliInput);
10692
+ if (!isConvex && options.database !== "none") {
10693
+ const dbResult = await setupDatabase(options, cliInput);
10694
+ if (dbResult && !dbResult.success) setupFailures.push(dbResult);
10695
+ }
10886
10696
  if (options.addons.length > 0 && options.addons[0] !== "none") await setupAddons(options);
10887
10697
  await applyDependencyVersionChannel(projectDir, options.versionChannel);
10888
10698
  await writeBtsConfig(options);
10889
10699
  await formatProject(projectDir);
10890
10700
  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 });
10701
+ if (options.install && (options.ecosystem === "typescript" || options.ecosystem === "react-native")) {
10702
+ const result$1 = await installDependencies({
10703
+ projectDir,
10704
+ packageManager: options.packageManager
10705
+ });
10706
+ if (!result$1.success) setupFailures.push(result$1);
10707
+ }
10708
+ if (options.install && options.ecosystem === "rust") {
10709
+ const result$1 = await runCargoBuild({ projectDir });
10710
+ if (!result$1.success) setupFailures.push(result$1);
10711
+ }
10712
+ if (options.install && options.ecosystem === "python") {
10713
+ const result$1 = await runUvSync({ projectDir });
10714
+ if (!result$1.success) setupFailures.push(result$1);
10715
+ }
10716
+ if (options.install && options.ecosystem === "go") {
10717
+ const result$1 = await runGoModTidy({ projectDir });
10718
+ if (!result$1.success) setupFailures.push(result$1);
10719
+ }
10720
+ if (options.install && options.ecosystem === "java" && options.javaBuildTool !== "none") {
10721
+ const result$1 = options.javaBuildTool === "gradle" ? await runGradleTests({ projectDir }) : await runMavenTests({ projectDir });
10722
+ if (!result$1.success) setupFailures.push(result$1);
10723
+ }
10724
+ if (options.install && options.ecosystem === "elixir") {
10725
+ const result$1 = await runMixCompile({ projectDir });
10726
+ if (!result$1.success) setupFailures.push(result$1);
10727
+ }
10901
10728
  await initializeGit(projectDir, options.git);
10902
10729
  if (!isSilent()) await displayPostInstallInstructions({
10903
10730
  ...options,
10904
10731
  depsInstalled: options.install
10905
10732
  });
10906
- return projectDir;
10733
+ return {
10734
+ projectDir,
10735
+ setupFailures
10736
+ };
10907
10737
  } catch (error) {
10738
+ await rollbackPartialProject(projectDir, dirHadContentBefore);
10908
10739
  if (error instanceof Error) {
10909
10740
  if (!isSilent()) console.error(error.stack);
10910
10741
  exitWithError(`Error during project creation: ${error.message}`);
@@ -10914,6 +10745,23 @@ async function createProject(options, cliInput = {}) {
10914
10745
  }
10915
10746
  }
10916
10747
  }
10748
+ /**
10749
+ * Remove a half-written project directory after a fatal scaffolding error so the
10750
+ * user is not left with a broken, partially generated project. Only removes
10751
+ * directories we created ourselves (empty/new before scaffolding) — never one
10752
+ * that already had user content (merge mode). Set BTS_KEEP_FAILED_OUTPUT=1 to
10753
+ * keep the partial output for debugging template failures.
10754
+ */
10755
+ async function rollbackPartialProject(projectDir, dirHadContentBefore) {
10756
+ if (dirHadContentBefore || process.env.BTS_KEEP_FAILED_OUTPUT) {
10757
+ if (!isSilent() && dirHadContentBefore) log.warn(`Left partially created files in ${projectDir} (directory already existed). Review and clean up manually.`);
10758
+ return;
10759
+ }
10760
+ try {
10761
+ await fs.remove(projectDir);
10762
+ if (!isSilent()) log.warn(`Cleaned up partially created project at ${projectDir}. Set BTS_KEEP_FAILED_OUTPUT=1 to keep it for debugging.`);
10763
+ } catch {}
10764
+ }
10917
10765
  async function setPackageManagerVersion(projectDir, packageManager) {
10918
10766
  const pkgJsonPath = path.join(projectDir, "package.json");
10919
10767
  if (!await fs.pathExists(pkgJsonPath)) return;
@@ -10974,6 +10822,7 @@ function getYesBaseConfig(flagConfig) {
10974
10822
  rateLimit: "none",
10975
10823
  i18n: "none",
10976
10824
  search: "none",
10825
+ vectorDb: "none",
10977
10826
  fileStorage: "none",
10978
10827
  mobileNavigation: "expo-router",
10979
10828
  mobileUI: "none",
@@ -10984,8 +10833,8 @@ function getYesBaseConfig(flagConfig) {
10984
10833
  mobileDeepLinking: "none"
10985
10834
  };
10986
10835
  }
10987
- function shouldPromptForVersionChannel(input) {
10988
- if (input.yes || input.part?.length || input.versionChannel !== void 0 || isSilent()) return false;
10836
+ function shouldPromptForVersionChannel(input, hasConfigBase) {
10837
+ if (input.yes || input.part?.length || hasConfigBase || input.versionChannel !== void 0 || isSilent()) return false;
10989
10838
  return canPromptInteractively();
10990
10839
  }
10991
10840
  async function createProjectHandler(input, options = {}) {
@@ -10996,10 +10845,15 @@ async function createProjectHandler(input, options = {}) {
10996
10845
  try {
10997
10846
  if (!isSilent() && input.renderTitle !== false) renderTitle();
10998
10847
  if (!isSilent()) intro(pc.magenta("Creating a new Better Fullstack project"));
10848
+ await maybeShowTelemetryNotice();
10999
10849
  if (!isSilent() && input.yolo) consola.fatal("YOLO mode enabled - skipping checks. Things may break!");
10850
+ const configBase = await resolveCreateConfigBase(input);
10851
+ const hasConfigBase = configBase !== void 0;
10852
+ if (hasConfigBase && !isSilent()) log.info(pc.cyan(input.config ? `Using stack from config file: ${input.config}` : `Using stack from history entry #${input.fromHistory}`));
10853
+ const useDefaultsForName = Boolean(input.yes) || hasConfigBase;
11000
10854
  let currentPathInput;
11001
- if (input.yes && input.projectName) currentPathInput = input.projectName;
11002
- else if (input.yes) {
10855
+ if (useDefaultsForName && input.projectName) currentPathInput = input.projectName;
10856
+ else if (useDefaultsForName) {
11003
10857
  const defaultConfig = getDefaultConfig();
11004
10858
  let defaultName = defaultConfig.relativePath;
11005
10859
  let counter = 1;
@@ -11009,7 +10863,7 @@ async function createProjectHandler(input, options = {}) {
11009
10863
  }
11010
10864
  currentPathInput = defaultName;
11011
10865
  } else currentPathInput = await getProjectName(input.projectName);
11012
- const versionChannel = shouldPromptForVersionChannel(input) ? await getVersionChannelChoice() : input.versionChannel ?? "stable";
10866
+ const versionChannel = shouldPromptForVersionChannel(input, hasConfigBase) ? await getVersionChannelChoice() : input.versionChannel ?? configBase?.versionChannel ?? "stable";
11013
10867
  let finalResolvedPath;
11014
10868
  let finalBaseName;
11015
10869
  if (input.dryRun) {
@@ -11088,6 +10942,7 @@ async function createProjectHandler(input, options = {}) {
11088
10942
  rateLimit: "none",
11089
10943
  i18n: "none",
11090
10944
  search: "none",
10945
+ vectorDb: "none",
11091
10946
  featureFlags: "none",
11092
10947
  analytics: "none",
11093
10948
  fileStorage: "none",
@@ -11174,11 +11029,15 @@ async function createProjectHandler(input, options = {}) {
11174
11029
  finalBaseName = setupResult.finalBaseName;
11175
11030
  currentPathInput = finalPathInput;
11176
11031
  }
11177
- const originalInput = {
11178
- ...input,
11032
+ const explicitInput = {
11033
+ ...Object.fromEntries(Object.entries(input).filter(([, value]) => value !== void 0)),
11179
11034
  projectDirectory: input.projectName
11180
11035
  };
11181
- const providedFlags = getProvidedFlags(originalInput);
11036
+ const originalInput = {
11037
+ ...configBase,
11038
+ ...explicitInput
11039
+ };
11040
+ const providedFlags = getProvidedFlags(explicitInput);
11182
11041
  let cliInput = originalInput;
11183
11042
  if (input.template && input.template !== "none") {
11184
11043
  const templateConfig = getTemplateConfig(input.template);
@@ -11201,7 +11060,7 @@ async function createProjectHandler(input, options = {}) {
11201
11060
  }
11202
11061
  const { validatePreflightConfig } = await import("@better-fullstack/template-generator");
11203
11062
  let config;
11204
- if (cliInput.yes || cliInput.part?.length) {
11063
+ if (cliInput.yes || cliInput.part?.length || hasConfigBase) {
11205
11064
  const flagConfig = processProvidedFlagsWithoutValidation(cliInput, finalBaseName);
11206
11065
  config = {
11207
11066
  ...getYesBaseConfig(flagConfig),
@@ -11277,7 +11136,7 @@ async function createProjectHandler(input, options = {}) {
11277
11136
  files
11278
11137
  };
11279
11138
  }
11280
- await createProject(config, { manualDb: cliInput.manualDb ?? input.manualDb });
11139
+ const setupFailures = (await createProject(config, { manualDb: cliInput.manualDb ?? input.manualDb }))?.setupFailures ?? [];
11281
11140
  if (cliInput.verify ?? input.verify) await runGeneratedChecks(config);
11282
11141
  const reproducibleCommand = generateReproducibleCommand(config);
11283
11142
  if (!isSilent()) log.success(pc.blue(`You can reproduce this setup with the following command:\n${reproducibleCommand}`));
@@ -11290,7 +11149,12 @@ async function createProjectHandler(input, options = {}) {
11290
11149
  const elapsedTimeMs = Date.now() - startTime;
11291
11150
  if (!isSilent()) {
11292
11151
  const elapsedTimeInSeconds = (elapsedTimeMs / 1e3).toFixed(2);
11293
- outro(pc.magenta(`Project created successfully in ${pc.bold(elapsedTimeInSeconds)} seconds!`));
11152
+ if (setupFailures.length > 0) {
11153
+ const stepList = setupFailures.map((f) => f.step).join(", ");
11154
+ const installCmd = config.packageManager === "npm" ? "npm install" : `${config.packageManager} install`;
11155
+ 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).`));
11156
+ outro(pc.yellow(`Project created with ${setupFailures.length} unfinished setup step(s) in ${pc.bold(elapsedTimeInSeconds)}s.`));
11157
+ } else outro(pc.magenta(`Project created successfully in ${pc.bold(elapsedTimeInSeconds)} seconds!`));
11294
11158
  }
11295
11159
  return {
11296
11160
  success: true,
@@ -11299,7 +11163,8 @@ async function createProjectHandler(input, options = {}) {
11299
11163
  timeScaffolded,
11300
11164
  elapsedTimeMs,
11301
11165
  projectDirectory: config.projectDir,
11302
- relativePath: config.relativePath
11166
+ relativePath: config.relativePath,
11167
+ setupFailures
11303
11168
  };
11304
11169
  } catch (error) {
11305
11170
  if (error instanceof UserCancelledError) {
@@ -11476,7 +11341,7 @@ const router = os.router({
11476
11341
  packageManager: types_exports.PackageManagerSchema.optional().describe("Package manager to use"),
11477
11342
  projectDir: z.string().optional().describe("Project directory (defaults to current)")
11478
11343
  })).handler(async ({ input }) => {
11479
- const { addHandler } = await import("./add-handler-BQN6pwGJ.mjs");
11344
+ const { addHandler } = await import("./add-handler-BjTmOwEN.mjs");
11480
11345
  await addHandler(input);
11481
11346
  }),
11482
11347
  history: os.meta({ description: "Show history of scaffolded projects with reproducible commands" }).input(z.object({
@@ -11486,6 +11351,17 @@ const router = os.router({
11486
11351
  })).handler(async ({ input }) => {
11487
11352
  await historyHandler(input);
11488
11353
  }),
11354
+ telemetry: os.meta({ description: "View or change anonymous usage telemetry collection (status | enable | disable)" }).input(z.tuple([z.enum([
11355
+ "status",
11356
+ "enable",
11357
+ "disable"
11358
+ ]).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 }) => {
11359
+ const [action, options] = input;
11360
+ await telemetryHandler({
11361
+ action,
11362
+ json: options.json
11363
+ });
11364
+ }),
11489
11365
  "update-deps": os.meta({ description: "Check and update dependency versions in add-deps.ts" }).input(z.object({
11490
11366
  check: z.boolean().default(false).describe("Report only, no changes"),
11491
11367
  patch: z.boolean().default(false).describe("Apply patch/minor updates only"),
@@ -11493,7 +11369,7 @@ const router = os.router({
11493
11369
  ecosystem: z.string().optional().describe("Filter by ecosystem (effect, tanstack, prisma, etc.)"),
11494
11370
  "list-ecosystems": z.boolean().default(false).describe("List available ecosystems")
11495
11371
  })).handler(async ({ input }) => {
11496
- const { updateDepsHandler, showEcosystems } = await import("./update-deps-CLebIM70.mjs");
11372
+ const { updateDepsHandler, showEcosystems } = await import("./update-deps-D5OG0KmJ.mjs");
11497
11373
  if (input["list-ecosystems"]) {
11498
11374
  showEcosystems();
11499
11375
  return;
@@ -11508,6 +11384,17 @@ const router = os.router({
11508
11384
  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
11385
  log.message("MCP server is started via the 'mcp' subcommand intercepted in cli.ts.");
11510
11386
  log.message("Run: create-better-fullstack mcp");
11387
+ }),
11388
+ 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({
11389
+ skipChecks: z.boolean().optional().default(false).describe("Skip the ecosystem build/type checks (config + deps + env only)"),
11390
+ json: z.boolean().optional().default(false).describe("Output the diagnosis as JSON")
11391
+ })])).handler(async ({ input }) => {
11392
+ const [projectDir, options] = input;
11393
+ const { doctorCommand } = await import("./doctor-Bm9qvkya.mjs");
11394
+ await doctorCommand({
11395
+ projectDir,
11396
+ ...options
11397
+ });
11511
11398
  })
11512
11399
  });
11513
11400
  const caller = createRouterClient(router, { context: {} });
@@ -11573,7 +11460,7 @@ async function builder() {
11573
11460
  return caller.builder();
11574
11461
  }
11575
11462
  async function add(input) {
11576
- const { addHandler } = await import("./add-handler-BQN6pwGJ.mjs");
11463
+ const { addHandler } = await import("./add-handler-BjTmOwEN.mjs");
11577
11464
  return addHandler(input, { silent: true });
11578
11465
  }
11579
11466
  async function history(options) {
@@ -11583,6 +11470,9 @@ async function history(options) {
11583
11470
  json: options?.json ?? false
11584
11471
  });
11585
11472
  }
11473
+ async function telemetry(action = "status", options) {
11474
+ return caller.telemetry([action, { json: options?.json ?? false }]);
11475
+ }
11586
11476
 
11587
11477
  //#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 };
11478
+ 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 };