create-better-fullstack 2.0.2 → 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-D6-GO3BZ.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,16 @@ 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
+ },
747
+ openapi: {
748
+ value: "openapi",
749
+ label: "OpenAPI",
750
+ hint: "REST scaffolding with an OpenAPI 3.1 spec and Scalar docs"
751
+ },
534
752
  none: {
535
753
  value: "none",
536
754
  label: "None",
@@ -894,6 +1112,11 @@ const CMS_PROMPT_OPTIONS = [
894
1112
  label: "Directus",
895
1113
  hint: "Open data platform and headless CMS for SQL databases"
896
1114
  },
1115
+ {
1116
+ value: "keystatic",
1117
+ label: "Keystatic",
1118
+ hint: "Git-backed CMS for Markdown, JSON, and YAML content"
1119
+ },
897
1120
  {
898
1121
  value: "none",
899
1122
  label: "None",
@@ -2356,7 +2579,7 @@ async function getFrontendChoice(frontendOptions, backend, auth) {
2356
2579
  });
2357
2580
  if (isGoBack(frontendTypes)) return GO_BACK_SYMBOL;
2358
2581
  if (isCancel$1(frontendTypes)) return exitCancelled("Operation cancelled");
2359
- setIsFirstPrompt(false);
2582
+ setIsFirstPrompt$1(false);
2360
2583
  const result = [];
2361
2584
  let shouldRestart = false;
2362
2585
  if (frontendTypes.includes("web")) {
@@ -2372,7 +2595,7 @@ async function getFrontendChoice(frontendOptions, backend, auth) {
2372
2595
  else result.push(webFramework);
2373
2596
  }
2374
2597
  if (shouldRestart) {
2375
- setIsFirstPrompt(wasFirstPrompt);
2598
+ setIsFirstPrompt$1(wasFirstPrompt);
2376
2599
  continue;
2377
2600
  }
2378
2601
  if (frontendTypes.includes("native")) {
@@ -2385,14 +2608,14 @@ async function getFrontendChoice(frontendOptions, backend, auth) {
2385
2608
  });
2386
2609
  if (isGoBack(nativeFramework)) if (frontendTypes.includes("web")) shouldRestart = true;
2387
2610
  else {
2388
- setIsFirstPrompt(wasFirstPrompt);
2611
+ setIsFirstPrompt$1(wasFirstPrompt);
2389
2612
  continue;
2390
2613
  }
2391
2614
  else if (isCancel$1(nativeFramework)) return exitCancelled("Operation cancelled");
2392
2615
  else result.push(nativeFramework);
2393
2616
  }
2394
2617
  if (shouldRestart) {
2395
- setIsFirstPrompt(wasFirstPrompt);
2618
+ setIsFirstPrompt$1(wasFirstPrompt);
2396
2619
  continue;
2397
2620
  }
2398
2621
  return result;
@@ -2827,17 +3050,36 @@ async function getGoObservabilityChoice(goObservability) {
2827
3050
 
2828
3051
  //#endregion
2829
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
+ ]);
2830
3066
  async function getI18nChoice(i18n, frontend) {
2831
3067
  if (i18n !== void 0) return i18n;
3068
+ const hasNext = frontend?.includes("next") ?? false;
2832
3069
  const response = await navigableSelect({
2833
3070
  message: "Select internationalization (i18n) library",
2834
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
+ }] : [],
2835
3077
  {
2836
3078
  value: "i18next",
2837
3079
  label: "i18next",
2838
3080
  hint: "Full-featured i18n framework, works with all frontends"
2839
3081
  },
2840
- ...frontend?.includes("next") ?? false ? [{
3082
+ ...hasNext ? [{
2841
3083
  value: "next-intl",
2842
3084
  label: "next-intl",
2843
3085
  hint: "Lightweight i18n for Next.js with App Router support"
@@ -2854,17 +3096,6 @@ async function getI18nChoice(i18n, frontend) {
2854
3096
  return response;
2855
3097
  }
2856
3098
 
2857
- //#endregion
2858
- //#region src/utils/command-exists.ts
2859
- async function commandExists(command) {
2860
- try {
2861
- if (process.platform === "win32") return (await $({ reject: false })`where ${command}`).exitCode === 0;
2862
- return (await $({ reject: false })`which ${command}`).exitCode === 0;
2863
- } catch {
2864
- return false;
2865
- }
2866
- }
2867
-
2868
3099
  //#endregion
2869
3100
  //#region src/prompts/install.ts
2870
3101
  async function getinstallChoice(install, ecosystem, javaBuildTool) {
@@ -5613,7 +5844,7 @@ async function navigableGroup(prompts, opts) {
5613
5844
  while (currentIndex < promptNames.length) {
5614
5845
  const name = promptNames[currentIndex];
5615
5846
  const prompt = prompts[name];
5616
- setIsFirstPrompt$1(currentIndex === 0);
5847
+ setIsFirstPrompt(currentIndex === 0);
5617
5848
  setLastPromptShownUI(false);
5618
5849
  const result = await prompt({ results })?.catch((e) => {
5619
5850
  throw e;
@@ -5634,7 +5865,7 @@ async function navigableGroup(prompts, opts) {
5634
5865
  results[name] = "canceled";
5635
5866
  opts.onCancel({ results });
5636
5867
  }
5637
- setIsFirstPrompt$1(false);
5868
+ setIsFirstPrompt(false);
5638
5869
  return results;
5639
5870
  }
5640
5871
  if (goingBack && !didLastPromptShowUI()) {
@@ -5649,7 +5880,7 @@ async function navigableGroup(prompts, opts) {
5649
5880
  results[name] = result;
5650
5881
  currentIndex++;
5651
5882
  }
5652
- setIsFirstPrompt$1(false);
5883
+ setIsFirstPrompt(false);
5653
5884
  return results;
5654
5885
  }
5655
5886
 
@@ -6090,6 +6321,11 @@ const SEARCH_PROMPT_OPTIONS = [
6090
6321
  label: "Elasticsearch",
6091
6322
  hint: "Distributed search and analytics engine with local and cloud deployments"
6092
6323
  },
6324
+ {
6325
+ value: "opensearch",
6326
+ label: "OpenSearch",
6327
+ hint: "Open-source search and analytics suite compatible with Elasticsearch APIs"
6328
+ },
6093
6329
  {
6094
6330
  value: "algolia",
6095
6331
  label: "Algolia",
@@ -6144,6 +6380,77 @@ async function getSearchChoice(search, backend, ecosystem) {
6144
6380
  return response;
6145
6381
  }
6146
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
+
6147
6454
  //#endregion
6148
6455
  //#region src/prompts/server-deploy.ts
6149
6456
  async function getServerDeploymentChoice(deployment, runtime, backend, _webDeploy) {
@@ -6437,7 +6744,7 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
6437
6744
  },
6438
6745
  addons: ({ results }) => {
6439
6746
  if (results.ecosystem !== "typescript") {
6440
- const nonTypeScriptAddons = (flags.addons ?? []).filter((addon) => addon === "docker-compose");
6747
+ const nonTypeScriptAddons = (flags.addons ?? []).filter((addon) => addon === "docker-compose" || addon === "devcontainer" || addon === "github-actions");
6441
6748
  return Promise.resolve(nonTypeScriptAddons);
6442
6749
  }
6443
6750
  return getAddonsChoice(flags.addons, results.frontend, results.auth, results.backend, results.runtime);
@@ -6531,6 +6838,10 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
6531
6838
  if (results.ecosystem === "react-native" || results.ecosystem === "elixir") return Promise.resolve("none");
6532
6839
  return getSearchChoice(flags.search, results.backend, results.ecosystem);
6533
6840
  },
6841
+ vectorDb: ({ results }) => {
6842
+ if (results.ecosystem !== "typescript") return Promise.resolve("none");
6843
+ return getVectorDbChoice(flags.vectorDb, results.backend, results.ecosystem);
6844
+ },
6534
6845
  fileStorage: ({ results }) => {
6535
6846
  if (results.ecosystem !== "typescript") return Promise.resolve("none");
6536
6847
  return getFileStorageChoice(flags.fileStorage, results.backend);
@@ -6938,6 +7249,7 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
6938
7249
  rateLimit: result.rateLimit,
6939
7250
  i18n: result.i18n,
6940
7251
  search: result.search,
7252
+ vectorDb: result.vectorDb,
6941
7253
  fileStorage: result.fileStorage,
6942
7254
  mobileNavigation: result.mobileNavigation,
6943
7255
  mobileUI: result.mobileUI,
@@ -7106,35 +7418,76 @@ async function getVersionChannelChoice(versionChannel) {
7106
7418
  }
7107
7419
 
7108
7420
  //#endregion
7109
- //#region src/utils/analytics.ts
7110
- const CONVEX_INGEST_URL = "https://curious-elephant-653.convex.site/api/analytics/ingest";
7111
- function isTelemetryEnabled() {
7112
- const disabled = process.env.BTS_TELEMETRY_DISABLED;
7113
- if (disabled !== void 0) return disabled !== "1";
7114
- const enabled = "1";
7115
- if (enabled !== void 0) return enabled === "1";
7116
- 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;
7117
7440
  }
7118
- async function sendConvexEvent(payload) {
7119
- try {
7120
- await fetch(CONVEX_INGEST_URL, {
7121
- method: "POST",
7122
- headers: { "Content-Type": "application/json" },
7123
- body: JSON.stringify(payload)
7124
- });
7125
- } 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
+ };
7126
7460
  }
7127
- async function trackProjectCreation(config, disableAnalytics = false) {
7128
- if (!isTelemetryEnabled() || disableAnalytics) return;
7129
- const { projectName: _projectName, projectDir: _projectDir, relativePath: _relativePath, ...safeConfig } = config;
7130
- try {
7131
- await sendConvexEvent({
7132
- ...safeConfig,
7133
- cli_version: getLatestCLIVersion(),
7134
- node_version: typeof process !== "undefined" ? process.version : "",
7135
- platform: typeof process !== "undefined" ? process.platform : ""
7136
- });
7137
- } 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);
7138
7491
  }
7139
7492
 
7140
7493
  //#endregion
@@ -7279,480 +7632,6 @@ function displayConfig(config) {
7279
7632
  return configDisplay.join("\n");
7280
7633
  }
7281
7634
 
7282
- //#endregion
7283
- //#region src/utils/generate-reproducible-command.ts
7284
- function getBaseCommand(packageManager) {
7285
- switch (packageManager) {
7286
- case "bun": return "bun create better-fullstack@latest";
7287
- case "pnpm": return "pnpm create better-fullstack@latest";
7288
- case "yarn": return "yarn create better-fullstack@latest";
7289
- case "npm":
7290
- default: return "npx create-better-fullstack@latest";
7291
- }
7292
- }
7293
- function formatArrayFlag(flag, values) {
7294
- const normalizedValues = values.filter((value) => value !== "none");
7295
- if (normalizedValues.length === 0) return `--${flag} none`;
7296
- return `--${flag} ${normalizedValues.join(" ")}`;
7297
- }
7298
- function appendCommonFlags(flags, config) {
7299
- if (config.aiDocs && config.aiDocs.length > 0) flags.push(formatArrayFlag("ai-docs", config.aiDocs));
7300
- else flags.push("--ai-docs none");
7301
- flags.push(config.git ? "--git" : "--no-git");
7302
- flags.push(`--package-manager ${config.packageManager}`);
7303
- if (config.versionChannel !== "stable") flags.push(`--version-channel ${config.versionChannel}`);
7304
- flags.push(config.install ? "--install" : "--no-install");
7305
- }
7306
- function hasGraphPrimaryPart(config, role, ecosystem) {
7307
- return config.stackParts?.some((part) => part.source !== "provided" && part.role === role && !part.ownerPartId && (!ecosystem || part.ecosystem === ecosystem));
7308
- }
7309
- function hasOwnedGraphPart(config, ownerRole, role, ecosystem, toolId) {
7310
- const owner = config.stackParts?.find((part) => part.source !== "provided" && part.role === ownerRole && !part.ownerPartId && (!ecosystem || part.ecosystem === ecosystem));
7311
- if (!owner) return false;
7312
- return Boolean(config.stackParts?.some((part) => part.source !== "provided" && part.role === role && part.ownerPartId === owner.id && (!ecosystem || part.ecosystem === ecosystem) && (!toolId || part.toolId === toolId)));
7313
- }
7314
- function hasGraphAddonPart(config, addon) {
7315
- const binding = (0, types_exports.getAddonStackPartBinding)(addon);
7316
- if (!binding) return false;
7317
- 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));
7318
- const owner = config.stackParts?.find((part) => part.source !== "provided" && part.role === binding.ownerRole && !part.ownerPartId && part.ecosystem === binding.ecosystem);
7319
- if (!owner) return false;
7320
- return Boolean(config.stackParts?.some((part) => part.source !== "provided" && part.role === binding.role && part.ecosystem === binding.ecosystem && part.toolId === addon && part.ownerPartId === owner.id));
7321
- }
7322
- function hasGraphExamplePart(config, example) {
7323
- return Boolean(config.stackParts?.some((part) => part.source !== "provided" && part.role === "examples" && part.ecosystem === "universal" && part.toolId === example && !part.ownerPartId));
7324
- }
7325
- function hasGraphArrayParts(config, values, hasPart) {
7326
- const normalizedValues = values.filter((value) => value !== "none");
7327
- return normalizedValues.length > 0 && normalizedValues.every((value) => hasPart(config, value));
7328
- }
7329
- function appendChangedStringFlag(flags, flag, value, defaultValue) {
7330
- if (value !== defaultValue) flags.push(`--${flag} ${value}`);
7331
- }
7332
- function appendChangedGraphStringFlag(flags, config, role, ecosystem, flag, value, defaultValue) {
7333
- if (hasGraphPart(config, role, ecosystem)) return;
7334
- appendChangedStringFlag(flags, flag, value, defaultValue);
7335
- }
7336
- function appendChangedOwnedGraphStringFlag(flags, config, ownerRole, role, ecosystem, flag, value, defaultValue) {
7337
- if (hasOwnedGraphPart(config, ownerRole, role, ecosystem, value)) return;
7338
- appendChangedStringFlag(flags, flag, value, defaultValue);
7339
- }
7340
- function hasOwnedGraphArrayParts(config, ownerRole, role, ecosystem, values) {
7341
- const normalizedValues = values.filter((value) => value !== "none");
7342
- if (normalizedValues.length === 0) return false;
7343
- return normalizedValues.every((value) => hasOwnedGraphPart(config, ownerRole, role, ecosystem, value));
7344
- }
7345
- function appendChangedOwnedGraphArrayFlag(flags, config, ownerRole, role, ecosystem, flag, values, defaultValues) {
7346
- if (hasOwnedGraphArrayParts(config, ownerRole, role, ecosystem, values)) return;
7347
- appendChangedArrayFlag(flags, flag, values, defaultValues);
7348
- }
7349
- function appendChangedArrayFlag(flags, flag, values, defaultValues) {
7350
- if (values.length !== defaultValues.length || values.some((value, index) => value !== defaultValues[index])) flags.push(formatArrayFlag(flag, values));
7351
- }
7352
- function appendAstroIntegrationFlag(flags, config) {
7353
- if (config.frontend.includes("astro") && config.astroIntegration !== "none") flags.push(`--astro-integration ${config.astroIntegration}`);
7354
- }
7355
- function appendGraphExtraFlags(flags, config) {
7356
- if (!hasGraphArrayParts(config, config.addons, hasGraphAddonPart)) appendChangedArrayFlag(flags, "addons", config.addons, ["turborepo"]);
7357
- if (!hasGraphArrayParts(config, config.examples, hasGraphExamplePart)) appendChangedArrayFlag(flags, "examples", config.examples, []);
7358
- if (hasGraphPrimaryPart(config, "database")) appendChangedOwnedGraphStringFlag(flags, config, "database", "dbSetup", "universal", "db-setup", config.dbSetup, "none");
7359
- else appendChangedStringFlag(flags, "db-setup", config.dbSetup, "none");
7360
- if (hasGraphPrimaryPart(config, "frontend", "typescript")) {
7361
- appendAstroIntegrationFlag(flags, config);
7362
- appendChangedOwnedGraphStringFlag(flags, config, "frontend", "deploy", "typescript", "web-deploy", config.webDeploy, "none");
7363
- appendChangedGraphStringFlag(flags, config, "css", "typescript", "css-framework", config.cssFramework, "tailwind");
7364
- appendChangedGraphStringFlag(flags, config, "ui", "typescript", "ui-library", config.uiLibrary, "shadcn-ui");
7365
- if (config.uiLibrary === "shadcn-ui") {
7366
- appendChangedStringFlag(flags, "shadcn-base", config.shadcnBase ?? "radix", "radix");
7367
- appendChangedStringFlag(flags, "shadcn-style", config.shadcnStyle ?? "nova", "nova");
7368
- appendChangedStringFlag(flags, "shadcn-icon-library", config.shadcnIconLibrary ?? "lucide", "lucide");
7369
- appendChangedStringFlag(flags, "shadcn-color-theme", config.shadcnColorTheme ?? "neutral", "neutral");
7370
- appendChangedStringFlag(flags, "shadcn-base-color", config.shadcnBaseColor ?? "neutral", "neutral");
7371
- appendChangedStringFlag(flags, "shadcn-font", config.shadcnFont ?? "inter", "inter");
7372
- appendChangedStringFlag(flags, "shadcn-radius", config.shadcnRadius ?? "default", "default");
7373
- }
7374
- appendChangedGraphStringFlag(flags, config, "stateManagement", "typescript", "state-management", config.stateManagement, "none");
7375
- appendChangedGraphStringFlag(flags, config, "forms", "typescript", "forms", config.forms, "react-hook-form");
7376
- appendChangedStringFlag(flags, "validation", config.validation, "zod");
7377
- appendChangedStringFlag(flags, "testing", config.testing, "vitest");
7378
- appendChangedGraphStringFlag(flags, config, "animation", "typescript", "animation", config.animation, "none");
7379
- appendChangedGraphStringFlag(flags, config, "fileUpload", "typescript", "file-upload", config.fileUpload, "none");
7380
- appendChangedGraphStringFlag(flags, config, "i18n", "typescript", "i18n", config.i18n, "none");
7381
- appendChangedGraphStringFlag(flags, config, "analytics", "typescript", "analytics", config.analytics, "none");
7382
- }
7383
- if (hasGraphPrimaryPart(config, "frontend", "typescript") || hasGraphPrimaryPart(config, "backend", "typescript")) {
7384
- if (hasGraphPrimaryPart(config, "backend", "typescript")) {
7385
- appendChangedOwnedGraphStringFlag(flags, config, "backend", "runtime", "typescript", "runtime", config.runtime, "bun");
7386
- appendChangedOwnedGraphStringFlag(flags, config, "backend", "deploy", "typescript", "server-deploy", config.serverDeploy, "none");
7387
- } else appendChangedStringFlag(flags, "server-deploy", config.serverDeploy, "none");
7388
- appendChangedGraphStringFlag(flags, config, "payments", "typescript", "payments", config.payments, "none");
7389
- appendChangedGraphStringFlag(flags, config, "email", "typescript", "email", config.email, "none");
7390
- appendChangedStringFlag(flags, "effect", config.effect, "none");
7391
- appendChangedGraphStringFlag(flags, config, "ai", "typescript", "ai", config.ai, "none");
7392
- appendChangedGraphStringFlag(flags, config, "realtime", "typescript", "realtime", config.realtime, "none");
7393
- appendChangedGraphStringFlag(flags, config, "jobQueue", "typescript", "job-queue", config.jobQueue, "none");
7394
- appendChangedGraphStringFlag(flags, config, "logging", "typescript", "logging", config.logging, "none");
7395
- appendChangedGraphStringFlag(flags, config, "observability", "typescript", "observability", config.observability, "none");
7396
- appendChangedGraphStringFlag(flags, config, "featureFlags", "typescript", "feature-flags", config.featureFlags, "none");
7397
- appendChangedGraphStringFlag(flags, config, "caching", "typescript", "caching", config.caching, "none");
7398
- appendChangedGraphStringFlag(flags, config, "rateLimit", "typescript", "rate-limit", config.rateLimit, "none");
7399
- appendChangedGraphStringFlag(flags, config, "cms", "typescript", "cms", config.cms, "none");
7400
- appendChangedGraphStringFlag(flags, config, "search", "typescript", "search", config.search, "none");
7401
- appendChangedGraphStringFlag(flags, config, "fileStorage", "typescript", "file-storage", config.fileStorage, "none");
7402
- }
7403
- if (hasGraphPrimaryPart(config, "mobile")) {
7404
- appendChangedOwnedGraphStringFlag(flags, config, "mobile", "navigation", "react-native", "mobile-navigation", config.mobileNavigation, "expo-router");
7405
- appendChangedOwnedGraphStringFlag(flags, config, "mobile", "ui", "react-native", "mobile-ui", config.mobileUI, "none");
7406
- appendChangedOwnedGraphStringFlag(flags, config, "mobile", "storage", "react-native", "mobile-storage", config.mobileStorage, "none");
7407
- appendChangedOwnedGraphStringFlag(flags, config, "mobile", "testing", "react-native", "mobile-testing", config.mobileTesting, "none");
7408
- appendChangedStringFlag(flags, "mobile-push", config.mobilePush, "none");
7409
- appendChangedStringFlag(flags, "mobile-ota", config.mobileOTA, "none");
7410
- appendChangedStringFlag(flags, "mobile-deep-linking", config.mobileDeepLinking, "none");
7411
- }
7412
- if (hasGraphPrimaryPart(config, "frontend", "rust")) appendChangedStringFlag(flags, "rust-frontend", config.rustFrontend, "none");
7413
- if (hasGraphPrimaryPart(config, "backend", "rust")) {
7414
- appendChangedOwnedGraphStringFlag(flags, config, "backend", "cli", "rust", "rust-cli", config.rustCli, "none");
7415
- appendChangedOwnedGraphArrayFlag(flags, config, "backend", "libraries", "rust", "rust-libraries", config.rustLibraries, []);
7416
- appendChangedOwnedGraphStringFlag(flags, config, "backend", "logging", "rust", "rust-logging", config.rustLogging, "tracing");
7417
- appendChangedOwnedGraphStringFlag(flags, config, "backend", "errorHandling", "rust", "rust-error-handling", config.rustErrorHandling, "anyhow-thiserror");
7418
- appendChangedOwnedGraphStringFlag(flags, config, "backend", "caching", "rust", "rust-caching", config.rustCaching, "none");
7419
- }
7420
- if (hasGraphPrimaryPart(config, "backend", "python")) {
7421
- appendChangedOwnedGraphStringFlag(flags, config, "backend", "validation", "python", "python-validation", config.pythonValidation, "none");
7422
- appendChangedOwnedGraphArrayFlag(flags, config, "backend", "ai", "python", "python-ai", config.pythonAi, []);
7423
- appendChangedOwnedGraphStringFlag(flags, config, "backend", "jobQueue", "python", "python-task-queue", config.pythonTaskQueue, "none");
7424
- appendChangedOwnedGraphStringFlag(flags, config, "backend", "api", "python", "python-graphql", config.pythonGraphql, "none");
7425
- appendChangedOwnedGraphStringFlag(flags, config, "backend", "codeQuality", "python", "python-quality", config.pythonQuality, "none");
7426
- }
7427
- if (hasGraphPrimaryPart(config, "backend", "go")) {
7428
- appendChangedOwnedGraphStringFlag(flags, config, "backend", "cli", "go", "go-cli", config.goCli, "none");
7429
- appendChangedOwnedGraphStringFlag(flags, config, "backend", "logging", "go", "go-logging", config.goLogging, "none");
7430
- }
7431
- if (hasGraphPrimaryPart(config, "backend", "java")) {
7432
- appendChangedOwnedGraphStringFlag(flags, config, "backend", "buildTool", "java", "java-build-tool", config.javaBuildTool, "maven");
7433
- appendChangedOwnedGraphArrayFlag(flags, config, "backend", "libraries", "java", "java-libraries", config.javaLibraries, []);
7434
- appendChangedOwnedGraphArrayFlag(flags, config, "backend", "testing", "java", "java-testing-libraries", config.javaTestingLibraries, ["junit5"]);
7435
- }
7436
- if (hasGraphPrimaryPart(config, "backend", "elixir")) {
7437
- appendChangedGraphStringFlag(flags, config, "realtime", "elixir", "elixir-realtime", config.elixirRealtime, "channels");
7438
- appendChangedGraphStringFlag(flags, config, "jobQueue", "elixir", "elixir-jobs", config.elixirJobs, "none");
7439
- appendChangedGraphStringFlag(flags, config, "validation", "elixir", "elixir-validation", config.elixirValidation, "ecto-changesets");
7440
- appendChangedOwnedGraphStringFlag(flags, config, "backend", "httpClient", "elixir", "elixir-http", config.elixirHttp, "req");
7441
- appendChangedStringFlag(flags, "elixir-json", config.elixirJson, "jason");
7442
- appendChangedGraphStringFlag(flags, config, "email", "elixir", "elixir-email", config.elixirEmail, "none");
7443
- appendChangedGraphStringFlag(flags, config, "caching", "elixir", "elixir-caching", config.elixirCaching, "none");
7444
- appendChangedGraphStringFlag(flags, config, "observability", "elixir", "elixir-observability", config.elixirObservability, "telemetry");
7445
- appendChangedGraphStringFlag(flags, config, "testing", "elixir", "elixir-testing", config.elixirTesting, "ex_unit");
7446
- appendChangedOwnedGraphStringFlag(flags, config, "backend", "codeQuality", "elixir", "elixir-quality", config.elixirQuality, "credo");
7447
- appendChangedGraphStringFlag(flags, config, "deploy", "elixir", "elixir-deploy", config.elixirDeploy, "none");
7448
- }
7449
- }
7450
- function appendSharedNonTypeScriptFlags(flags, config) {
7451
- flags.push(`--email ${config.email}`);
7452
- flags.push(`--observability ${config.observability}`);
7453
- flags.push(`--caching ${config.caching}`);
7454
- flags.push(`--search ${config.search}`);
7455
- flags.push(formatArrayFlag("addons", config.addons));
7456
- flags.push(formatArrayFlag("examples", config.examples));
7457
- flags.push(`--db-setup ${config.dbSetup}`);
7458
- flags.push(`--web-deploy ${config.webDeploy}`);
7459
- flags.push(`--server-deploy ${config.serverDeploy}`);
7460
- }
7461
- function getTypeScriptFlags(config) {
7462
- const flags = [];
7463
- if (config.frontend && config.frontend.length > 0) flags.push(`--frontend ${config.frontend.join(" ")}`);
7464
- else flags.push("--frontend none");
7465
- appendAstroIntegrationFlag(flags, config);
7466
- flags.push(`--backend ${config.backend}`);
7467
- flags.push(`--runtime ${config.runtime}`);
7468
- flags.push(`--database ${config.database}`);
7469
- flags.push(`--orm ${config.orm}`);
7470
- flags.push(`--api ${config.api}`);
7471
- flags.push(`--auth ${config.auth}`);
7472
- flags.push(`--payments ${config.payments}`);
7473
- flags.push(`--email ${config.email}`);
7474
- flags.push(`--file-upload ${config.fileUpload}`);
7475
- flags.push(`--effect ${config.effect}`);
7476
- flags.push(`--css-framework ${config.cssFramework}`);
7477
- flags.push(`--ui-library ${config.uiLibrary}`);
7478
- if (config.uiLibrary === "shadcn-ui") {
7479
- flags.push(`--shadcn-base ${config.shadcnBase}`);
7480
- flags.push(`--shadcn-style ${config.shadcnStyle}`);
7481
- flags.push(`--shadcn-icon-library ${config.shadcnIconLibrary}`);
7482
- flags.push(`--shadcn-color-theme ${config.shadcnColorTheme}`);
7483
- flags.push(`--shadcn-base-color ${config.shadcnBaseColor}`);
7484
- flags.push(`--shadcn-font ${config.shadcnFont}`);
7485
- flags.push(`--shadcn-radius ${config.shadcnRadius}`);
7486
- }
7487
- flags.push(`--ai ${config.ai}`);
7488
- flags.push(`--state-management ${config.stateManagement}`);
7489
- flags.push(`--forms ${config.forms}`);
7490
- flags.push(`--validation ${config.validation}`);
7491
- flags.push(`--testing ${config.testing}`);
7492
- flags.push(`--animation ${config.animation}`);
7493
- flags.push(`--realtime ${config.realtime}`);
7494
- flags.push(`--job-queue ${config.jobQueue}`);
7495
- flags.push(`--logging ${config.logging}`);
7496
- flags.push(`--observability ${config.observability}`);
7497
- flags.push(`--feature-flags ${config.featureFlags}`);
7498
- flags.push(`--caching ${config.caching}`);
7499
- flags.push(`--rate-limit ${config.rateLimit}`);
7500
- flags.push(`--i18n ${config.i18n}`);
7501
- flags.push(`--cms ${config.cms}`);
7502
- flags.push(`--search ${config.search}`);
7503
- flags.push(`--file-storage ${config.fileStorage}`);
7504
- flags.push(`--mobile-navigation ${config.mobileNavigation}`);
7505
- flags.push(`--mobile-ui ${config.mobileUI}`);
7506
- flags.push(`--mobile-storage ${config.mobileStorage}`);
7507
- flags.push(`--mobile-testing ${config.mobileTesting}`);
7508
- flags.push(`--mobile-push ${config.mobilePush}`);
7509
- flags.push(`--mobile-ota ${config.mobileOTA}`);
7510
- flags.push(`--mobile-deep-linking ${config.mobileDeepLinking}`);
7511
- if (config.addons && config.addons.length > 0) flags.push(`--addons ${config.addons.join(" ")}`);
7512
- else flags.push("--addons none");
7513
- if (config.examples && config.examples.length > 0) flags.push(`--examples ${config.examples.join(" ")}`);
7514
- else flags.push("--examples none");
7515
- flags.push(`--db-setup ${config.dbSetup}`);
7516
- flags.push(`--web-deploy ${config.webDeploy}`);
7517
- flags.push(`--server-deploy ${config.serverDeploy}`);
7518
- appendCommonFlags(flags, config);
7519
- return flags;
7520
- }
7521
- function getReactNativeFlags(config) {
7522
- const flags = ["--ecosystem react-native"];
7523
- if (config.frontend && config.frontend.length > 0) flags.push(`--frontend ${config.frontend.join(" ")}`);
7524
- else flags.push("--frontend native-bare");
7525
- flags.push(`--auth ${config.auth}`);
7526
- flags.push(`--mobile-navigation ${config.mobileNavigation}`);
7527
- flags.push(`--mobile-ui ${config.mobileUI}`);
7528
- flags.push(`--mobile-storage ${config.mobileStorage}`);
7529
- flags.push(`--mobile-testing ${config.mobileTesting}`);
7530
- flags.push(`--mobile-push ${config.mobilePush}`);
7531
- flags.push(`--mobile-ota ${config.mobileOTA}`);
7532
- flags.push(`--mobile-deep-linking ${config.mobileDeepLinking}`);
7533
- appendCommonFlags(flags, config);
7534
- return flags;
7535
- }
7536
- function getRustFlags(config) {
7537
- const flags = ["--ecosystem rust"];
7538
- flags.push(`--rust-web-framework ${config.rustWebFramework}`);
7539
- flags.push(`--rust-frontend ${config.rustFrontend}`);
7540
- flags.push(`--rust-orm ${config.rustOrm}`);
7541
- flags.push(`--rust-api ${config.rustApi}`);
7542
- flags.push(`--rust-cli ${config.rustCli}`);
7543
- flags.push(formatArrayFlag("rust-libraries", config.rustLibraries));
7544
- flags.push(`--rust-logging ${config.rustLogging}`);
7545
- flags.push(`--rust-error-handling ${config.rustErrorHandling}`);
7546
- flags.push(`--rust-caching ${config.rustCaching}`);
7547
- flags.push(`--rust-auth ${config.rustAuth}`);
7548
- flags.push(`--rust-realtime ${config.rustRealtime}`);
7549
- flags.push(`--rust-message-queue ${config.rustMessageQueue}`);
7550
- flags.push(`--rust-observability ${config.rustObservability}`);
7551
- flags.push(`--rust-templating ${config.rustTemplating}`);
7552
- appendSharedNonTypeScriptFlags(flags, config);
7553
- appendCommonFlags(flags, config);
7554
- return flags;
7555
- }
7556
- function getPythonFlags(config) {
7557
- const flags = ["--ecosystem python"];
7558
- flags.push(`--python-web-framework ${config.pythonWebFramework}`);
7559
- flags.push(`--python-orm ${config.pythonOrm}`);
7560
- flags.push(`--python-validation ${config.pythonValidation}`);
7561
- flags.push(formatArrayFlag("python-ai", config.pythonAi));
7562
- flags.push(`--python-auth ${config.pythonAuth}`);
7563
- flags.push(`--python-api ${config.pythonApi}`);
7564
- flags.push(`--python-task-queue ${config.pythonTaskQueue}`);
7565
- flags.push(`--python-graphql ${config.pythonGraphql}`);
7566
- flags.push(`--python-quality ${config.pythonQuality}`);
7567
- flags.push(formatArrayFlag("python-testing", config.pythonTesting));
7568
- flags.push(`--python-caching ${config.pythonCaching}`);
7569
- flags.push(`--python-realtime ${config.pythonRealtime}`);
7570
- flags.push(`--python-observability ${config.pythonObservability}`);
7571
- flags.push(formatArrayFlag("python-cli", config.pythonCli));
7572
- appendSharedNonTypeScriptFlags(flags, config);
7573
- appendCommonFlags(flags, config);
7574
- return flags;
7575
- }
7576
- function getGoFlags(config) {
7577
- const flags = ["--ecosystem go"];
7578
- flags.push(`--go-web-framework ${config.goWebFramework}`);
7579
- flags.push(`--go-orm ${config.goOrm}`);
7580
- flags.push(`--go-api ${config.goApi}`);
7581
- flags.push(`--go-cli ${config.goCli}`);
7582
- flags.push(`--go-logging ${config.goLogging}`);
7583
- flags.push(`--go-auth ${config.goAuth}`);
7584
- flags.push(formatArrayFlag("go-testing", config.goTesting));
7585
- flags.push(`--go-realtime ${config.goRealtime}`);
7586
- flags.push(`--go-message-queue ${config.goMessageQueue}`);
7587
- flags.push(`--go-caching ${config.goCaching}`);
7588
- flags.push(`--go-config ${config.goConfig}`);
7589
- flags.push(`--go-observability ${config.goObservability}`);
7590
- flags.push(`--auth ${config.auth}`);
7591
- appendSharedNonTypeScriptFlags(flags, config);
7592
- appendCommonFlags(flags, config);
7593
- return flags;
7594
- }
7595
- function getJavaFlags(config) {
7596
- const flags = ["--ecosystem java"];
7597
- flags.push(`--java-web-framework ${config.javaWebFramework}`);
7598
- flags.push(`--java-build-tool ${config.javaBuildTool}`);
7599
- flags.push(`--java-orm ${config.javaOrm}`);
7600
- flags.push(`--java-auth ${config.javaAuth}`);
7601
- flags.push(`--java-api ${config.javaApi}`);
7602
- flags.push(`--java-logging ${config.javaLogging}`);
7603
- flags.push(formatArrayFlag("java-libraries", config.javaLibraries));
7604
- flags.push(formatArrayFlag("java-testing-libraries", config.javaTestingLibraries));
7605
- appendSharedNonTypeScriptFlags(flags, config);
7606
- appendCommonFlags(flags, config);
7607
- return flags;
7608
- }
7609
- function getDotnetFlags(config) {
7610
- const flags = ["--ecosystem dotnet"];
7611
- flags.push(`--dotnet-web-framework ${config.dotnetWebFramework}`);
7612
- flags.push(`--dotnet-orm ${config.dotnetOrm}`);
7613
- flags.push(`--dotnet-auth ${config.dotnetAuth}`);
7614
- flags.push(`--dotnet-api ${config.dotnetApi}`);
7615
- flags.push(formatArrayFlag("dotnet-testing", config.dotnetTesting));
7616
- flags.push(`--dotnet-job-queue ${config.dotnetJobQueue}`);
7617
- flags.push(`--dotnet-realtime ${config.dotnetRealtime}`);
7618
- flags.push(formatArrayFlag("dotnet-observability", config.dotnetObservability));
7619
- flags.push(`--dotnet-validation ${config.dotnetValidation}`);
7620
- flags.push(`--dotnet-caching ${config.dotnetCaching}`);
7621
- flags.push(`--dotnet-deploy ${config.dotnetDeploy}`);
7622
- appendCommonFlags(flags, config);
7623
- return flags;
7624
- }
7625
- function getElixirFlags(config) {
7626
- const flags = ["--ecosystem elixir"];
7627
- flags.push(`--elixir-web-framework ${config.elixirWebFramework}`);
7628
- flags.push(`--elixir-orm ${config.elixirOrm}`);
7629
- flags.push(`--elixir-auth ${config.elixirAuth}`);
7630
- flags.push(`--elixir-api ${config.elixirApi}`);
7631
- flags.push(`--elixir-realtime ${config.elixirRealtime}`);
7632
- flags.push(`--elixir-jobs ${config.elixirJobs}`);
7633
- flags.push(`--elixir-validation ${config.elixirValidation}`);
7634
- flags.push(`--elixir-http ${config.elixirHttp}`);
7635
- flags.push(`--elixir-json ${config.elixirJson}`);
7636
- flags.push(`--elixir-email ${config.elixirEmail}`);
7637
- flags.push(`--elixir-caching ${config.elixirCaching}`);
7638
- flags.push(`--elixir-observability ${config.elixirObservability}`);
7639
- flags.push(`--elixir-testing ${config.elixirTesting}`);
7640
- flags.push(`--elixir-quality ${config.elixirQuality}`);
7641
- flags.push(`--elixir-deploy ${config.elixirDeploy}`);
7642
- flags.push(formatArrayFlag("elixir-libraries", config.elixirLibraries));
7643
- appendCommonFlags(flags, config);
7644
- return flags;
7645
- }
7646
- function generateReproducibleCommand(config) {
7647
- let flags;
7648
- if (config.stackParts && config.stackParts.length > 0) {
7649
- flags = config.stackParts.filter((part) => part.source !== "provided").map((part) => `--part ${(0, types_exports.formatStackPartSpec)(part, config.stackParts ?? [])}`);
7650
- appendGraphExtraFlags(flags, config);
7651
- appendCommonFlags(flags, config);
7652
- return `${getBaseCommand(config.packageManager)}${config.relativePath ? ` ${config.relativePath}` : ""} ${flags.join(" ")}`;
7653
- }
7654
- switch (config.ecosystem) {
7655
- case "react-native":
7656
- flags = getReactNativeFlags(config);
7657
- break;
7658
- case "rust":
7659
- flags = getRustFlags(config);
7660
- break;
7661
- case "python":
7662
- flags = getPythonFlags(config);
7663
- break;
7664
- case "go":
7665
- flags = getGoFlags(config);
7666
- break;
7667
- case "java":
7668
- flags = getJavaFlags(config);
7669
- break;
7670
- case "dotnet":
7671
- flags = getDotnetFlags(config);
7672
- break;
7673
- case "elixir":
7674
- flags = getElixirFlags(config);
7675
- break;
7676
- case "typescript":
7677
- default:
7678
- flags = getTypeScriptFlags(config);
7679
- break;
7680
- }
7681
- return `${getBaseCommand(config.packageManager)}${config.relativePath ? ` ${config.relativePath}` : ""} ${flags.join(" ")}`;
7682
- }
7683
-
7684
- //#endregion
7685
- //#region src/utils/generated-checks.ts
7686
- function getGraphTarget(config) {
7687
- const backend = getPrimaryGraphPart(config, "backend");
7688
- if (!backend || backend.ecosystem === "typescript" || backend.ecosystem === "react-native" || backend.ecosystem === "universal") return null;
7689
- return {
7690
- ecosystem: backend.ecosystem,
7691
- projectDir: path.join(config.projectDir, backend.targetPath ?? "apps/server")
7692
- };
7693
- }
7694
- function getSingleEcosystemTarget(config) {
7695
- if (config.ecosystem === "typescript" || config.ecosystem === "react-native" || config.ecosystem === "java") return null;
7696
- return {
7697
- ecosystem: config.ecosystem,
7698
- projectDir: config.projectDir
7699
- };
7700
- }
7701
- async function runCommand(cwd, command, args) {
7702
- await $({
7703
- cwd,
7704
- stdout: "inherit",
7705
- stderr: "inherit"
7706
- })`${command} ${args}`;
7707
- }
7708
- async function verifyTarget(target) {
7709
- const s = spinner();
7710
- const cwd = target.projectDir;
7711
- switch (target.ecosystem) {
7712
- case "go":
7713
- s.start("Verifying generated Go server...");
7714
- await runCommand(cwd, "go", ["mod", "tidy"]);
7715
- await runCommand(cwd, "go", ["test", "./..."]);
7716
- s.stop("Generated Go server checks passed");
7717
- return;
7718
- case "rust":
7719
- s.start("Verifying generated Rust server...");
7720
- await runCommand(cwd, "cargo", ["check"]);
7721
- s.stop("Generated Rust server checks passed");
7722
- return;
7723
- case "python":
7724
- s.start("Verifying generated Python server...");
7725
- await runCommand(cwd, "uv", ["sync"]);
7726
- await runCommand(cwd, "uv", [
7727
- "run",
7728
- "ruff",
7729
- "check",
7730
- "."
7731
- ]);
7732
- s.stop("Generated Python server checks passed");
7733
- return;
7734
- case "elixir":
7735
- if (!await commandExists("mix")) {
7736
- log.warn(pc.yellow("Skipping Elixir verification because mix is not on PATH"));
7737
- return;
7738
- }
7739
- s.start("Verifying generated Elixir server...");
7740
- await runCommand(cwd, "mix", ["deps.get"]);
7741
- await runCommand(cwd, "mix", ["compile"]);
7742
- s.stop("Generated Elixir server checks passed");
7743
- return;
7744
- default: log.warn(pc.yellow(`No generated checks are configured for ${target.ecosystem}`));
7745
- }
7746
- }
7747
- async function runGeneratedChecks(config) {
7748
- const target = getGraphTarget(config) ?? getSingleEcosystemTarget(config);
7749
- if (!target) {
7750
- log.warn(pc.yellow("No generated checks are configured for this stack"));
7751
- return;
7752
- }
7753
- await verifyTarget(target);
7754
- }
7755
-
7756
7635
  //#endregion
7757
7636
  //#region src/utils/preflight-display.ts
7758
7637
  function displayPreflightWarnings({ warnings }) {
@@ -7856,87 +7735,6 @@ async function setupProjectDirectory(finalPathInput, shouldClearDirectory) {
7856
7735
  };
7857
7736
  }
7858
7737
 
7859
- //#endregion
7860
- //#region src/utils/templates.ts
7861
- const TEMPLATE_PRESETS = {
7862
- mern: {
7863
- database: "mongodb",
7864
- orm: "mongoose",
7865
- backend: "express",
7866
- runtime: "node",
7867
- frontend: ["react-router"],
7868
- api: "orpc",
7869
- auth: "better-auth",
7870
- payments: "none",
7871
- addons: ["turborepo"],
7872
- examples: ["none"],
7873
- dbSetup: "mongodb-atlas",
7874
- webDeploy: "none",
7875
- serverDeploy: "none"
7876
- },
7877
- pern: {
7878
- database: "postgres",
7879
- orm: "drizzle",
7880
- backend: "express",
7881
- runtime: "node",
7882
- frontend: ["tanstack-router"],
7883
- api: "trpc",
7884
- auth: "better-auth",
7885
- payments: "none",
7886
- addons: ["turborepo"],
7887
- examples: ["none"],
7888
- dbSetup: "none",
7889
- webDeploy: "none",
7890
- serverDeploy: "none"
7891
- },
7892
- t3: {
7893
- database: "postgres",
7894
- orm: "prisma",
7895
- backend: "self",
7896
- runtime: "none",
7897
- frontend: ["next"],
7898
- api: "trpc",
7899
- auth: "better-auth",
7900
- payments: "none",
7901
- addons: ["biome", "turborepo"],
7902
- examples: ["none"],
7903
- dbSetup: "none",
7904
- webDeploy: "none",
7905
- serverDeploy: "none"
7906
- },
7907
- uniwind: {
7908
- database: "none",
7909
- orm: "none",
7910
- backend: "none",
7911
- runtime: "none",
7912
- frontend: ["native-uniwind"],
7913
- api: "none",
7914
- auth: "none",
7915
- payments: "none",
7916
- addons: ["none"],
7917
- examples: ["none"],
7918
- dbSetup: "none",
7919
- webDeploy: "none",
7920
- serverDeploy: "none"
7921
- },
7922
- none: null
7923
- };
7924
- function getTemplateConfig(template) {
7925
- if (template === "none" || !template) return null;
7926
- const config = TEMPLATE_PRESETS[template];
7927
- if (!config) throw new Error(`Unknown template: ${template}`);
7928
- return config;
7929
- }
7930
- function getTemplateDescription(template) {
7931
- return {
7932
- mern: "MongoDB + Express + React + Node.js - Classic MERN stack",
7933
- pern: "PostgreSQL + Express + React + Node.js - Popular PERN stack",
7934
- t3: "T3 Stack - Next.js + tRPC + Prisma + PostgreSQL + Better Auth",
7935
- uniwind: "Expo + Uniwind native app with no backend services",
7936
- none: "No template - Full customization"
7937
- }[template] || "";
7938
- }
7939
-
7940
7738
  //#endregion
7941
7739
  //#region src/utils/config-processing.ts
7942
7740
  function deriveProjectName(projectName, projectDirectory) {
@@ -8439,7 +8237,45 @@ function validateFrontendConstraints(config, providedFlags) {
8439
8237
  validateWebDeployRequiresWebFrontend(config.webDeploy, hasWebFrontendFlag);
8440
8238
  validateWebDeployFrontendTemplates(config.webDeploy, frontend);
8441
8239
  }
8442
- function validateApiConstraints(_config, _options) {}
8240
+ function validateApiConstraints(config, _options) {
8241
+ if (config.api !== "openapi" && config.api !== "apollo-server") return;
8242
+ const apiDisplayName = config.api === "apollo-server" ? "Apollo Server" : "OpenAPI";
8243
+ const frontend = config.frontend ?? [];
8244
+ if (frontend.some((item) => [
8245
+ "native-bare",
8246
+ "native-uniwind",
8247
+ "native-unistyles"
8248
+ ].includes(item))) incompatibilityError({
8249
+ message: `${apiDisplayName} is currently available for web frontends, not React Native.`,
8250
+ provided: {
8251
+ api: config.api,
8252
+ frontend
8253
+ },
8254
+ suggestions: [
8255
+ "Use --api trpc",
8256
+ "Use --api orpc",
8257
+ "Use a web frontend"
8258
+ ]
8259
+ });
8260
+ if (!config.backend || ![
8261
+ "hono",
8262
+ "express",
8263
+ "fastify",
8264
+ "elysia"
8265
+ ].includes(config.backend)) incompatibilityError({
8266
+ message: `${apiDisplayName} currently supports Hono, Express, Fastify, and Elysia backends.`,
8267
+ provided: {
8268
+ api: config.api,
8269
+ backend: config.backend ?? "none"
8270
+ },
8271
+ suggestions: [
8272
+ "Use --backend hono",
8273
+ "Use --backend express",
8274
+ "Use --backend fastify",
8275
+ "Use --backend elysia"
8276
+ ]
8277
+ });
8278
+ }
8443
8279
  function validateJavaConstraints(config, providedFlags = /* @__PURE__ */ new Set()) {
8444
8280
  if (config.ecosystem !== "java") return;
8445
8281
  const hasSpringBoot = config.javaWebFramework === "spring-boot";
@@ -8698,10 +8534,11 @@ function validateRateLimitConstraints(config) {
8698
8534
  }
8699
8535
  function validateSearchConstraints(config) {
8700
8536
  if (!config.search || config.search === "none") return;
8701
- if (config.ecosystem !== "typescript" && config.search !== "meilisearch") incompatibilityError({
8537
+ const ecosystem = config.ecosystem ?? "typescript";
8538
+ if (ecosystem !== "typescript" && config.search !== "meilisearch") incompatibilityError({
8702
8539
  message: "Only Meilisearch search is available for non-TypeScript ecosystems.",
8703
8540
  provided: {
8704
- ecosystem: config.ecosystem ?? "typescript",
8541
+ ecosystem,
8705
8542
  search: config.search
8706
8543
  },
8707
8544
  suggestions: ["Use --search meilisearch", "Use --search none"]
@@ -10042,10 +9879,10 @@ async function setupDatabase(config, cliInput) {
10042
9879
  const serverDbDir = path.join(projectDir, "apps/server/src/db");
10043
9880
  if (await fs.pathExists(serverDbDir)) await fs.remove(serverDbDir);
10044
9881
  }
10045
- return;
9882
+ return null;
10046
9883
  }
10047
9884
  const dbPackageDir = path.join(projectDir, "packages/db");
10048
- if (!await fs.pathExists(dbPackageDir)) return;
9885
+ if (!await fs.pathExists(dbPackageDir)) return null;
10049
9886
  try {
10050
9887
  if (dbSetup === "docker") await setupDockerCompose(config);
10051
9888
  else if (database === "sqlite" && dbSetup === "turso") await setupTurso(config, cliInput);
@@ -10058,8 +9895,18 @@ async function setupDatabase(config, cliInput) {
10058
9895
  } else if (database === "mysql" && dbSetup === "planetscale") await setupPlanetScale(config);
10059
9896
  else if (database === "mongodb" && dbSetup === "mongodb-atlas") await setupMongoDBAtlas(config, cliInput);
10060
9897
  else if (database === "redis" && dbSetup === "upstash") await setupUpstash(config, cliInput);
9898
+ return {
9899
+ step: "Database setup",
9900
+ success: true
9901
+ };
10061
9902
  } catch (error) {
10062
- 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
+ };
10063
9910
  }
10064
9911
  }
10065
9912
 
@@ -10659,14 +10506,14 @@ function displayJavaInstructions(config) {
10659
10506
  }
10660
10507
  const effectiveJavaTestingLibraries = javaBuildTool === "none" ? [] : javaTestingLibraries.filter((library) => library !== "none");
10661
10508
  const buildToolCommand = javaBuildTool === "none" ? null : javaBuildTool === "gradle" ? process.platform === "win32" ? "gradlew.bat" : "./gradlew" : process.platform === "win32" ? "mvnw.cmd" : "./mvnw";
10662
- 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;
10663
10510
  const packageCommand = buildToolCommand ? javaBuildTool === "gradle" ? `${buildToolCommand} build` : `${buildToolCommand} package` : null;
10664
10511
  const sourceCompileCommand = buildToolCommand ? null : `javac -d out ${getJavaMainSourcePath(projectName)}`;
10665
10512
  const sourceRunCommand = buildToolCommand ? null : `java -cp out ${getJavaMainClass(projectName)}`;
10666
10513
  let output = `${pc.bold("Next steps")}\n${pc.cyan("1.")} ${cdCmd}\n`;
10667
10514
  let stepCounter = 2;
10668
10515
  if (!depsInstalled && buildToolCommand && effectiveJavaTestingLibraries.length > 0) output += `${pc.cyan(`${stepCounter++}.`)} ${buildToolCommand} test\n`;
10669
- if (runCommand$1) output += `${pc.cyan(`${stepCounter++}.`)} ${runCommand$1}\n`;
10516
+ if (runCommand) output += `${pc.cyan(`${stepCounter++}.`)} ${runCommand}\n`;
10670
10517
  else if (sourceCompileCommand && sourceRunCommand) {
10671
10518
  output += `${pc.cyan(`${stepCounter++}.`)} ${sourceCompileCommand}\n`;
10672
10519
  output += `${pc.cyan(`${stepCounter++}.`)} ${sourceRunCommand}\n`;
@@ -10714,9 +10561,9 @@ function displayJavaInstructions(config) {
10714
10561
  output += `${pc.cyan("•")} Testing: ${testingList}\n`;
10715
10562
  }
10716
10563
  output += `\n${pc.bold("Common Java commands:")}\n`;
10717
- if (buildToolCommand && runCommand$1 && packageCommand) {
10564
+ if (buildToolCommand && runCommand && packageCommand) {
10718
10565
  if (effectiveJavaTestingLibraries.length > 0) output += `${pc.cyan("•")} Test: ${buildToolCommand} test\n`;
10719
- output += `${pc.cyan("•")} Run: ${runCommand$1}\n`;
10566
+ output += `${pc.cyan("•")} Run: ${runCommand}\n`;
10720
10567
  output += `${pc.cyan("•")} Package: ${packageCommand}\n`;
10721
10568
  } else if (sourceCompileCommand && sourceRunCommand) {
10722
10569
  output += `${pc.cyan("•")} Compile: ${sourceCompileCommand}\n`;
@@ -10734,14 +10581,14 @@ function displayJavaInstructions(config) {
10734
10581
  function displayPythonInstructions(config) {
10735
10582
  const { relativePath, depsInstalled, pythonWebFramework, pythonOrm, pythonValidation, pythonAi, pythonApi, pythonTaskQueue, pythonQuality } = config;
10736
10583
  const cdCmd = `cd ${relativePath}`;
10737
- let runCommand$1 = "uv run uvicorn app.main:app --reload";
10738
- if (pythonWebFramework === "django") runCommand$1 = "uv run python manage.py runserver";
10739
- else if (pythonWebFramework === "flask") runCommand$1 = "uv run flask --app app.main run --reload";
10740
- 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";
10741
10588
  let output = `${pc.bold("Next steps")}\n${pc.cyan("1.")} ${cdCmd}\n`;
10742
10589
  let stepCounter = 2;
10743
10590
  if (!depsInstalled) output += `${pc.cyan(`${stepCounter++}.`)} uv sync\n`;
10744
- output += `${pc.cyan(`${stepCounter++}.`)} ${runCommand$1}\n`;
10591
+ output += `${pc.cyan(`${stepCounter++}.`)} ${runCommand}\n`;
10745
10592
  output += `\n${pc.bold("Your Python project includes:")}\n`;
10746
10593
  if (pythonWebFramework && pythonWebFramework !== "none") output += `${pc.cyan("•")} Web Framework: ${{
10747
10594
  fastapi: "FastAPI",
@@ -10779,7 +10626,7 @@ function displayPythonInstructions(config) {
10779
10626
  }[pythonQuality] || pythonQuality}\n`;
10780
10627
  output += `\n${pc.bold("Common Python commands:")}\n`;
10781
10628
  output += `${pc.cyan("•")} Install: uv sync\n`;
10782
- output += `${pc.cyan("•")} Run: ${runCommand$1}\n`;
10629
+ output += `${pc.cyan("•")} Run: ${runCommand}\n`;
10783
10630
  output += `${pc.cyan("•")} Test: uv run pytest\n`;
10784
10631
  if (pythonQuality === "ruff") {
10785
10632
  output += `${pc.cyan("•")} Format: uv run ruff format .\n`;
@@ -10829,6 +10676,8 @@ function displayElixirInstructions(config) {
10829
10676
  async function createProject(options, cliInput = {}) {
10830
10677
  const projectDir = options.projectDir;
10831
10678
  const isConvex = options.backend === "convex";
10679
+ const setupFailures = [];
10680
+ const dirHadContentBefore = await fs.pathExists(projectDir) && (await fs.readdir(projectDir)).length > 0;
10832
10681
  try {
10833
10682
  await fs.ensureDir(projectDir);
10834
10683
  const [{ generateVirtualProject, EMBEDDED_TEMPLATES }, { writeTreeToFilesystem }] = await Promise.all([import("@better-fullstack/template-generator"), import("@better-fullstack/template-generator/fs-writer")]);
@@ -10840,29 +10689,53 @@ async function createProject(options, cliInput = {}) {
10840
10689
  await writeTreeToFilesystem(result.tree, projectDir);
10841
10690
  await setPackageManagerVersion(projectDir, options.packageManager);
10842
10691
  await ensurePackageManagerProjectFiles(projectDir, options.packageManager);
10843
- 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
+ }
10844
10696
  if (options.addons.length > 0 && options.addons[0] !== "none") await setupAddons(options);
10845
10697
  await applyDependencyVersionChannel(projectDir, options.versionChannel);
10846
10698
  await writeBtsConfig(options);
10847
10699
  await formatProject(projectDir);
10848
10700
  if (!isSilent()) log.success("Project template successfully scaffolded!");
10849
- if (options.install && (options.ecosystem === "typescript" || options.ecosystem === "react-native")) await installDependencies({
10850
- projectDir,
10851
- packageManager: options.packageManager
10852
- });
10853
- if (options.install && options.ecosystem === "rust") await runCargoBuild({ projectDir });
10854
- if (options.install && options.ecosystem === "python") await runUvSync({ projectDir });
10855
- if (options.install && options.ecosystem === "go") await runGoModTidy({ projectDir });
10856
- if (options.install && options.ecosystem === "java" && options.javaBuildTool !== "none") if (options.javaBuildTool === "gradle") await runGradleTests({ projectDir });
10857
- else await runMavenTests({ projectDir });
10858
- 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
+ }
10859
10728
  await initializeGit(projectDir, options.git);
10860
10729
  if (!isSilent()) await displayPostInstallInstructions({
10861
10730
  ...options,
10862
10731
  depsInstalled: options.install
10863
10732
  });
10864
- return projectDir;
10733
+ return {
10734
+ projectDir,
10735
+ setupFailures
10736
+ };
10865
10737
  } catch (error) {
10738
+ await rollbackPartialProject(projectDir, dirHadContentBefore);
10866
10739
  if (error instanceof Error) {
10867
10740
  if (!isSilent()) console.error(error.stack);
10868
10741
  exitWithError(`Error during project creation: ${error.message}`);
@@ -10872,6 +10745,23 @@ async function createProject(options, cliInput = {}) {
10872
10745
  }
10873
10746
  }
10874
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
+ }
10875
10765
  async function setPackageManagerVersion(projectDir, packageManager) {
10876
10766
  const pkgJsonPath = path.join(projectDir, "package.json");
10877
10767
  if (!await fs.pathExists(pkgJsonPath)) return;
@@ -10932,6 +10822,7 @@ function getYesBaseConfig(flagConfig) {
10932
10822
  rateLimit: "none",
10933
10823
  i18n: "none",
10934
10824
  search: "none",
10825
+ vectorDb: "none",
10935
10826
  fileStorage: "none",
10936
10827
  mobileNavigation: "expo-router",
10937
10828
  mobileUI: "none",
@@ -10942,8 +10833,8 @@ function getYesBaseConfig(flagConfig) {
10942
10833
  mobileDeepLinking: "none"
10943
10834
  };
10944
10835
  }
10945
- function shouldPromptForVersionChannel(input) {
10946
- 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;
10947
10838
  return canPromptInteractively();
10948
10839
  }
10949
10840
  async function createProjectHandler(input, options = {}) {
@@ -10954,10 +10845,15 @@ async function createProjectHandler(input, options = {}) {
10954
10845
  try {
10955
10846
  if (!isSilent() && input.renderTitle !== false) renderTitle();
10956
10847
  if (!isSilent()) intro(pc.magenta("Creating a new Better Fullstack project"));
10848
+ await maybeShowTelemetryNotice();
10957
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;
10958
10854
  let currentPathInput;
10959
- if (input.yes && input.projectName) currentPathInput = input.projectName;
10960
- else if (input.yes) {
10855
+ if (useDefaultsForName && input.projectName) currentPathInput = input.projectName;
10856
+ else if (useDefaultsForName) {
10961
10857
  const defaultConfig = getDefaultConfig();
10962
10858
  let defaultName = defaultConfig.relativePath;
10963
10859
  let counter = 1;
@@ -10967,7 +10863,7 @@ async function createProjectHandler(input, options = {}) {
10967
10863
  }
10968
10864
  currentPathInput = defaultName;
10969
10865
  } else currentPathInput = await getProjectName(input.projectName);
10970
- const versionChannel = shouldPromptForVersionChannel(input) ? await getVersionChannelChoice() : input.versionChannel ?? "stable";
10866
+ const versionChannel = shouldPromptForVersionChannel(input, hasConfigBase) ? await getVersionChannelChoice() : input.versionChannel ?? configBase?.versionChannel ?? "stable";
10971
10867
  let finalResolvedPath;
10972
10868
  let finalBaseName;
10973
10869
  if (input.dryRun) {
@@ -11046,6 +10942,7 @@ async function createProjectHandler(input, options = {}) {
11046
10942
  rateLimit: "none",
11047
10943
  i18n: "none",
11048
10944
  search: "none",
10945
+ vectorDb: "none",
11049
10946
  featureFlags: "none",
11050
10947
  analytics: "none",
11051
10948
  fileStorage: "none",
@@ -11132,11 +11029,15 @@ async function createProjectHandler(input, options = {}) {
11132
11029
  finalBaseName = setupResult.finalBaseName;
11133
11030
  currentPathInput = finalPathInput;
11134
11031
  }
11135
- const originalInput = {
11136
- ...input,
11032
+ const explicitInput = {
11033
+ ...Object.fromEntries(Object.entries(input).filter(([, value]) => value !== void 0)),
11137
11034
  projectDirectory: input.projectName
11138
11035
  };
11139
- const providedFlags = getProvidedFlags(originalInput);
11036
+ const originalInput = {
11037
+ ...configBase,
11038
+ ...explicitInput
11039
+ };
11040
+ const providedFlags = getProvidedFlags(explicitInput);
11140
11041
  let cliInput = originalInput;
11141
11042
  if (input.template && input.template !== "none") {
11142
11043
  const templateConfig = getTemplateConfig(input.template);
@@ -11159,7 +11060,7 @@ async function createProjectHandler(input, options = {}) {
11159
11060
  }
11160
11061
  const { validatePreflightConfig } = await import("@better-fullstack/template-generator");
11161
11062
  let config;
11162
- if (cliInput.yes || cliInput.part?.length) {
11063
+ if (cliInput.yes || cliInput.part?.length || hasConfigBase) {
11163
11064
  const flagConfig = processProvidedFlagsWithoutValidation(cliInput, finalBaseName);
11164
11065
  config = {
11165
11066
  ...getYesBaseConfig(flagConfig),
@@ -11235,7 +11136,7 @@ async function createProjectHandler(input, options = {}) {
11235
11136
  files
11236
11137
  };
11237
11138
  }
11238
- await createProject(config, { manualDb: cliInput.manualDb ?? input.manualDb });
11139
+ const setupFailures = (await createProject(config, { manualDb: cliInput.manualDb ?? input.manualDb }))?.setupFailures ?? [];
11239
11140
  if (cliInput.verify ?? input.verify) await runGeneratedChecks(config);
11240
11141
  const reproducibleCommand = generateReproducibleCommand(config);
11241
11142
  if (!isSilent()) log.success(pc.blue(`You can reproduce this setup with the following command:\n${reproducibleCommand}`));
@@ -11248,7 +11149,12 @@ async function createProjectHandler(input, options = {}) {
11248
11149
  const elapsedTimeMs = Date.now() - startTime;
11249
11150
  if (!isSilent()) {
11250
11151
  const elapsedTimeInSeconds = (elapsedTimeMs / 1e3).toFixed(2);
11251
- 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!`));
11252
11158
  }
11253
11159
  return {
11254
11160
  success: true,
@@ -11257,7 +11163,8 @@ async function createProjectHandler(input, options = {}) {
11257
11163
  timeScaffolded,
11258
11164
  elapsedTimeMs,
11259
11165
  projectDirectory: config.projectDir,
11260
- relativePath: config.relativePath
11166
+ relativePath: config.relativePath,
11167
+ setupFailures
11261
11168
  };
11262
11169
  } catch (error) {
11263
11170
  if (error instanceof UserCancelledError) {
@@ -11434,7 +11341,7 @@ const router = os.router({
11434
11341
  packageManager: types_exports.PackageManagerSchema.optional().describe("Package manager to use"),
11435
11342
  projectDir: z.string().optional().describe("Project directory (defaults to current)")
11436
11343
  })).handler(async ({ input }) => {
11437
- const { addHandler } = await import("./add-handler-BPAZM8Zo.mjs");
11344
+ const { addHandler } = await import("./add-handler-BjTmOwEN.mjs");
11438
11345
  await addHandler(input);
11439
11346
  }),
11440
11347
  history: os.meta({ description: "Show history of scaffolded projects with reproducible commands" }).input(z.object({
@@ -11444,6 +11351,17 @@ const router = os.router({
11444
11351
  })).handler(async ({ input }) => {
11445
11352
  await historyHandler(input);
11446
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
+ }),
11447
11365
  "update-deps": os.meta({ description: "Check and update dependency versions in add-deps.ts" }).input(z.object({
11448
11366
  check: z.boolean().default(false).describe("Report only, no changes"),
11449
11367
  patch: z.boolean().default(false).describe("Apply patch/minor updates only"),
@@ -11451,7 +11369,7 @@ const router = os.router({
11451
11369
  ecosystem: z.string().optional().describe("Filter by ecosystem (effect, tanstack, prisma, etc.)"),
11452
11370
  "list-ecosystems": z.boolean().default(false).describe("List available ecosystems")
11453
11371
  })).handler(async ({ input }) => {
11454
- const { updateDepsHandler, showEcosystems } = await import("./update-deps-CLebIM70.mjs");
11372
+ const { updateDepsHandler, showEcosystems } = await import("./update-deps-D5OG0KmJ.mjs");
11455
11373
  if (input["list-ecosystems"]) {
11456
11374
  showEcosystems();
11457
11375
  return;
@@ -11466,6 +11384,17 @@ const router = os.router({
11466
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 () => {
11467
11385
  log.message("MCP server is started via the 'mcp' subcommand intercepted in cli.ts.");
11468
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
+ });
11469
11398
  })
11470
11399
  });
11471
11400
  const caller = createRouterClient(router, { context: {} });
@@ -11531,7 +11460,7 @@ async function builder() {
11531
11460
  return caller.builder();
11532
11461
  }
11533
11462
  async function add(input) {
11534
- const { addHandler } = await import("./add-handler-BPAZM8Zo.mjs");
11463
+ const { addHandler } = await import("./add-handler-BjTmOwEN.mjs");
11535
11464
  return addHandler(input, { silent: true });
11536
11465
  }
11537
11466
  async function history(options) {
@@ -11541,6 +11470,9 @@ async function history(options) {
11541
11470
  json: options?.json ?? false
11542
11471
  });
11543
11472
  }
11473
+ async function telemetry(action = "status", options) {
11474
+ return caller.telemetry([action, { json: options?.json ?? false }]);
11475
+ }
11544
11476
 
11545
11477
  //#endregion
11546
- 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 };