create-better-fullstack 2.1.5 → 2.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,11 +1,14 @@
1
1
  #!/usr/bin/env node
2
- import "./errors-Cyol8zbN.mjs";
3
- import { a as createBtsCli, c as history, d as telemetry, i as create, l as router, n as builder, o as docs, r as check, s as doctor, t as add, u as sponsors } from "./run-BYse4yJy.mjs";
2
+ import "./bts-config-BceXPcpI.mjs";
3
+ import { a as createBtsCli, c as history, d as telemetry, f as update, i as create, l as router, n as builder, o as docs, r as check, s as doctor, t as add, u as sponsors } from "./run-D80ZtSO8.mjs";
4
4
  import "./render-title-zvyKC1ej.mjs";
5
- import "./addons-setup-CyrP1IV-.mjs";
6
- import "./file-formatter-B3dsev2l.mjs";
7
- import "./install-dependencies-CgNh-aOy.mjs";
8
- import "./generated-checks-C8hn9w2i.mjs";
5
+ import "./errors-ns_o2OKg.mjs";
6
+ import "./addons-setup-C_lxdJqU.mjs";
7
+ import "./file-formatter-XU6ti05V.mjs";
8
+ import "./install-dependencies-DHoYa3P-.mjs";
9
+ import "./generated-checks-Dt4Xqp1x.mjs";
10
+ import { t as applyEffectBackendDefaults } from "./config-processing-B_1wTe3g.mjs";
11
+ import "./scaffold-manifest-GV1fbhpD.mjs";
9
12
  import { EMBEDDED_TEMPLATES, TEMPLATE_COUNT, VirtualFileSystem, generateVirtualProject } from "@better-fullstack/template-generator";
10
13
 
11
14
  //#region src/index.ts
@@ -36,6 +39,7 @@ async function createVirtual(options) {
36
39
  const ecosystem = options.ecosystem || "typescript";
37
40
  const isReactNative = ecosystem === "react-native";
38
41
  const frontend = options.frontend || (isReactNative ? ["native-bare"] : ["tanstack-router"]);
42
+ const backend = options.backend || (isReactNative ? "none" : "hono");
39
43
  const hasNativeFrontend = frontend.some((item) => item === "native-bare" || item === "native-uniwind" || item === "native-unistyles");
40
44
  const config = {
41
45
  ecosystem,
@@ -44,7 +48,7 @@ async function createVirtual(options) {
44
48
  relativePath: "./virtual",
45
49
  database: options.database || "none",
46
50
  orm: options.orm || "none",
47
- backend: options.backend || (isReactNative ? "none" : "hono"),
51
+ backend,
48
52
  runtime: options.runtime || (isReactNative ? "none" : "bun"),
49
53
  frontend,
50
54
  addons: options.addons || [],
@@ -56,6 +60,7 @@ async function createVirtual(options) {
56
60
  effect: options.effect || "none",
57
61
  git: options.git ?? false,
58
62
  packageManager: options.packageManager || "bun",
63
+ workspaceShape: options.workspaceShape || "monorepo",
59
64
  versionChannel: options.versionChannel || "stable",
60
65
  install: false,
61
66
  dbSetup: options.dbSetup || "none",
@@ -139,6 +144,7 @@ async function createVirtual(options) {
139
144
  goConfig: options.goConfig || "none",
140
145
  goObservability: options.goObservability || "none",
141
146
  javaWebFramework: options.javaWebFramework || (options.ecosystem === "java" ? "spring-boot" : "none"),
147
+ javaLanguage: options.javaLanguage || "java",
142
148
  javaBuildTool: options.javaBuildTool || (options.ecosystem === "java" ? "maven" : "none"),
143
149
  javaOrm: options.javaOrm || "none",
144
150
  javaAuth: options.javaAuth || "none",
@@ -176,6 +182,7 @@ async function createVirtual(options) {
176
182
  aiDocs: options.aiDocs || ["claude-md", "agents-md"]
177
183
  };
178
184
  if (options.stackParts) config.stackParts = options.stackParts;
185
+ applyEffectBackendDefaults(config, new Set(Object.keys(options)));
179
186
  const { generateVirtualProject: generate, EMBEDDED_TEMPLATES: EMBEDDED_TEMPLATES$2 } = await import("@better-fullstack/template-generator");
180
187
  const result = await generate({
181
188
  config,
@@ -198,4 +205,4 @@ async function createVirtual(options) {
198
205
  }
199
206
 
200
207
  //#endregion
201
- export { EMBEDDED_TEMPLATES, TEMPLATE_COUNT, VirtualFileSystem, add, builder, check, create, createBtsCli, createVirtual, docs, doctor, generateVirtualProject, history, router, sponsors, telemetry };
208
+ export { EMBEDDED_TEMPLATES, TEMPLATE_COUNT, VirtualFileSystem, add, builder, check, create, createBtsCli, createVirtual, docs, doctor, generateVirtualProject, history, router, sponsors, telemetry, update };
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { A as types_exports, d as setLastPromptShownUI, r as exitCancelled, s as isFirstPrompt, u as setIsFirstPrompt$1, y as DEFAULT_CONFIG } from "./errors-Cyol8zbN.mjs";
3
- import { g as validateAddonCompatibility, o as getCompatibleAddons } from "./file-formatter-B3dsev2l.mjs";
2
+ import { c as DEFAULT_CONFIG, y as types_exports } from "./bts-config-BceXPcpI.mjs";
3
+ import { d as setLastPromptShownUI, r as exitCancelled, s as isFirstPrompt, u as setIsFirstPrompt$1 } from "./errors-ns_o2OKg.mjs";
4
+ import { g as validateAddonCompatibility, o as getCompatibleAddons } from "./file-formatter-XU6ti05V.mjs";
4
5
  import { log, spinner } from "@clack/prompts";
5
6
  import pc from "picocolors";
6
7
  import fs from "fs-extra";
@@ -496,6 +497,16 @@ const PRERELEASE_TAG_PRIORITY = [
496
497
  ];
497
498
  const REGISTRY_FETCH_TIMEOUT_MS = 1e4;
498
499
  const REGISTRY_CONCURRENCY = 10;
500
+ const SYNCHRONIZED_VERSION_FAMILIES = [{
501
+ name: "oRPC",
502
+ packages: [
503
+ "@orpc/server",
504
+ "@orpc/client",
505
+ "@orpc/openapi",
506
+ "@orpc/zod",
507
+ "@orpc/tanstack-query"
508
+ ]
509
+ }];
499
510
  function mapWithConcurrency(items, fn, concurrency) {
500
511
  const results = Array.from({ length: items.length });
501
512
  let index = 0;
@@ -507,6 +518,19 @@ function mapWithConcurrency(items, fn, concurrency) {
507
518
  }
508
519
  return Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, () => worker())).then(() => results);
509
520
  }
521
+ function getVersionSections(packageJson) {
522
+ const sections = [];
523
+ for (const sectionName of ["dependencies", "devDependencies"]) {
524
+ const section = packageJson[sectionName];
525
+ if (section && typeof section === "object" && !Array.isArray(section)) sections.push(section);
526
+ }
527
+ const workspaces = packageJson.workspaces;
528
+ if (workspaces && typeof workspaces === "object" && !Array.isArray(workspaces)) {
529
+ const catalog = workspaces.catalog;
530
+ if (catalog && typeof catalog === "object" && !Array.isArray(catalog)) sections.push(catalog);
531
+ }
532
+ return sections;
533
+ }
510
534
  function parseVersion(value) {
511
535
  const normalized = value.replace(/^[^\d]*/, "");
512
536
  const dashIdx = normalized.indexOf("-");
@@ -580,10 +604,40 @@ function selectRegistryVersionForChannel(packageInfo, channel) {
580
604
  if (prereleases.length > 0) return prereleases.sort((left, right) => compareVersions(right, left))[0] ?? null;
581
605
  return tags.latest ?? null;
582
606
  }
583
- async function resolveRegistryVersion(packageName, channel) {
584
- const version = selectRegistryVersionForChannel(await fetchPackageInfo(packageName), channel);
585
- if (!version) throw new Error(`No ${channel} version available for ${packageName}`);
586
- return version;
607
+ function getVersionsForChannel(packageInfo, channel) {
608
+ const versions = Object.keys(packageInfo.versions ?? {});
609
+ if (channel === "latest") return versions.filter((version) => !isPrerelease(version));
610
+ const prereleases = versions.filter(isPrerelease);
611
+ return prereleases.length > 0 ? prereleases : versions.filter((version) => !isPrerelease(version));
612
+ }
613
+ function resolveSharedFamilyVersion(packageInfos, channel) {
614
+ if (packageInfos.length === 0) return null;
615
+ const firstInfo = packageInfos[0];
616
+ if (!firstInfo) return null;
617
+ let commonVersions = new Set(getVersionsForChannel(firstInfo, channel));
618
+ const remainingInfos = packageInfos.slice(1);
619
+ for (const packageInfo of remainingInfos) {
620
+ const availableVersions = new Set(getVersionsForChannel(packageInfo, channel));
621
+ commonVersions = new Set([...commonVersions].filter((version) => availableVersions.has(version)));
622
+ }
623
+ return [...commonVersions].sort((left, right) => compareVersions(right, left))[0] ?? null;
624
+ }
625
+ function applySynchronizedFamilyVersions(resolvedVersions, packageInfos, channel) {
626
+ for (const family of SYNCHRONIZED_VERSION_FAMILIES) {
627
+ const selectedPackages = family.packages.filter((packageName) => resolvedVersions.has(packageName));
628
+ if (selectedPackages.length < 2) continue;
629
+ const selectedPackageInfos = selectedPackages.flatMap((packageName) => {
630
+ const packageInfo = packageInfos.get(packageName);
631
+ return packageInfo ? [packageInfo] : [];
632
+ });
633
+ if (selectedPackageInfos.length !== selectedPackages.length) continue;
634
+ const sharedVersion = resolveSharedFamilyVersion(selectedPackageInfos, channel);
635
+ if (!sharedVersion) {
636
+ log.warn(`Failed to resolve shared ${channel} version for ${family.name} packages`);
637
+ continue;
638
+ }
639
+ for (const packageName of selectedPackages) resolvedVersions.set(packageName, sharedVersion);
640
+ }
587
641
  }
588
642
  async function collectPackageJsonPaths(projectDir) {
589
643
  const results = [];
@@ -609,35 +663,35 @@ async function applyDependencyVersionChannel(projectDir, channel) {
609
663
  const packageNames = /* @__PURE__ */ new Set();
610
664
  for (const packageJsonPath of packageJsonPaths) {
611
665
  const packageJson = await fs.readJson(packageJsonPath);
612
- for (const [depName, depVersion] of Object.entries(packageJson.dependencies ?? {})) if (typeof depVersion === "string" && isRegistrySemverSpec(depVersion)) packageNames.add(depName);
613
- for (const [depName, depVersion] of Object.entries(packageJson.devDependencies ?? {})) if (typeof depVersion === "string" && isRegistrySemverSpec(depVersion)) packageNames.add(depName);
666
+ for (const section of getVersionSections(packageJson)) for (const [depName, depVersion] of Object.entries(section)) if (typeof depVersion === "string" && isRegistrySemverSpec(depVersion)) packageNames.add(depName);
614
667
  }
615
668
  if (packageNames.size === 0) return;
616
669
  const resolvedVersions = /* @__PURE__ */ new Map();
670
+ const packageInfos = /* @__PURE__ */ new Map();
617
671
  await mapWithConcurrency([...packageNames], async (packageName) => {
618
672
  try {
619
- const resolvedVersion = await resolveRegistryVersion(packageName, channel);
673
+ const packageInfo = await fetchPackageInfo(packageName);
674
+ const resolvedVersion = selectRegistryVersionForChannel(packageInfo, channel);
675
+ if (!resolvedVersion) throw new Error(`No ${channel} version available for ${packageName}`);
676
+ packageInfos.set(packageName, packageInfo);
620
677
  resolvedVersions.set(packageName, resolvedVersion);
621
678
  } catch (error) {
622
679
  log.warn(`Failed to resolve ${channel} version for ${packageName}: ${error instanceof Error ? error.message : String(error)}`);
623
680
  }
624
681
  }, REGISTRY_CONCURRENCY);
625
682
  if (resolvedVersions.size === 0) return;
683
+ applySynchronizedFamilyVersions(resolvedVersions, packageInfos, channel);
626
684
  for (const packageJsonPath of packageJsonPaths) {
627
685
  const packageJson = await fs.readJson(packageJsonPath);
628
686
  let changed = false;
629
- for (const sectionName of ["dependencies", "devDependencies"]) {
630
- const section = packageJson[sectionName];
631
- if (!section) continue;
632
- for (const [packageName, currentVersion] of Object.entries(section)) {
633
- if (!isRegistrySemverSpec(currentVersion)) continue;
634
- const resolvedVersion = resolvedVersions.get(packageName);
635
- if (!resolvedVersion) continue;
636
- const nextVersion = applyVersionPrefix(currentVersion, resolvedVersion);
637
- if (nextVersion !== currentVersion) {
638
- section[packageName] = nextVersion;
639
- changed = true;
640
- }
687
+ for (const section of getVersionSections(packageJson)) for (const [packageName, currentVersion] of Object.entries(section)) {
688
+ if (!isRegistrySemverSpec(currentVersion)) continue;
689
+ const resolvedVersion = resolvedVersions.get(packageName);
690
+ if (!resolvedVersion) continue;
691
+ const nextVersion = applyVersionPrefix(currentVersion, resolvedVersion);
692
+ if (nextVersion !== currentVersion) {
693
+ section[packageName] = nextVersion;
694
+ changed = true;
641
695
  }
642
696
  }
643
697
  if (changed) await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ import "./bts-config-BceXPcpI.mjs";
3
+ import "./errors-ns_o2OKg.mjs";
4
+ import "./file-formatter-XU6ti05V.mjs";
5
+ import "./config-processing-B_1wTe3g.mjs";
6
+ import { a as startMcpServer, i as recommendStackFromBrief, n as MCP_STACK_UPDATE_SCHEMA, r as getMcpGraphPreview, t as MCP_PLAN_CREATE_SCHEMA } from "./mcp-entry.mjs";
7
+
8
+ export { recommendStackFromBrief, startMcpServer };
@@ -1,11 +1,12 @@
1
1
  #!/usr/bin/env node
2
- import { A as types_exports, C as getEffectiveStack, D as getGraphSummary, _ as writeBtsConfig, f as buildBtsConfigForPersistence, m as readBtsConfig, p as previewBtsConfigUpdate, v as getLatestCLIVersion, x as getDefaultConfig } from "./errors-Cyol8zbN.mjs";
3
- import { r as validateConfigForProgrammaticUse, t as formatCode, v as CreateCommandOptionsSchema } from "./file-formatter-B3dsev2l.mjs";
4
- import { n as getTemplateDescription, r as generateReproducibleCommand, t as getTemplateConfig } from "./templates-CnTOtKjm.mjs";
2
+ import { f as getEffectiveStack, g as getGraphSummary, n as previewBtsConfigUpdate, o as writeBtsConfig, r as readBtsConfig, s as getLatestCLIVersion, t as buildBtsConfigForPersistence, u as getDefaultConfig, y as types_exports } from "./bts-config-BceXPcpI.mjs";
3
+ import "./errors-ns_o2OKg.mjs";
4
+ import { r as validateConfigForProgrammaticUse, t as formatCode, v as CreateCommandOptionsSchema } from "./file-formatter-XU6ti05V.mjs";
5
+ import { a as getTemplateConfig, o as getTemplateDescription, s as generateReproducibleCommand, t as applyEffectBackendDefaults } from "./config-processing-B_1wTe3g.mjs";
5
6
  import z from "zod";
6
7
  import fs from "fs-extra";
7
8
  import path from "node:path";
8
- import { AISchema, APISchema, AddonsSchema, AiDocsSchema, AnalyticsSchema, AnimationSchema, AstroIntegrationSchema, AuthSchema, BackendSchema, CATEGORY_ORDER, CMSSchema, CSSFrameworkSchema, CachingSchema, DatabaseSchema, DatabaseSetupSchema, DotnetApiSchema, DotnetAuthSchema, DotnetCachingSchema, DotnetDeploySchema, DotnetJobQueueSchema, DotnetObservabilitySchema, DotnetOrmSchema, DotnetRealtimeSchema, DotnetTestingSchema, DotnetValidationSchema, DotnetWebFrameworkSchema, EcosystemSchema, EffectSchema, ElixirApiSchema, ElixirAuthSchema, ElixirCachingSchema, ElixirDeploySchema, ElixirEmailSchema, ElixirHttpSchema, ElixirJobsSchema, ElixirJsonSchema, ElixirLibrariesSchema, ElixirObservabilitySchema, ElixirOrmSchema, ElixirQualitySchema, ElixirRealtimeSchema, ElixirTestingSchema, ElixirValidationSchema, ElixirWebFrameworkSchema, EmailSchema, ExamplesSchema, FeatureFlagsSchema, FileStorageSchema, FileUploadSchema, FormsSchema, FrontendSchema, GoApiSchema, GoAuthSchema, GoCachingSchema, GoCliSchema, GoConfigSchema, GoLoggingSchema, GoMessageQueueSchema, GoObservabilitySchema, GoOrmSchema, GoRealtimeSchema, GoTestingSchema, GoWebFrameworkSchema, I18nSchema, JavaApiSchema, JavaAuthSchema, JavaBuildToolSchema, JavaLibrariesSchema, JavaLoggingSchema, JavaOrmSchema, JavaTestingLibrariesSchema, JavaWebFrameworkSchema, JobQueueSchema, LoggingSchema, MobileDeepLinkingSchema, MobileNavigationSchema, MobileOTASchema, MobilePushSchema, MobileStorageSchema, MobileTestingSchema, MobileUISchema, OPTION_CATEGORY_METADATA, ORMSchema, ObservabilitySchema, PackageManagerSchema, PaymentsSchema, PythonAiSchema, PythonApiSchema, PythonAuthSchema, PythonCachingSchema, PythonCliSchema, PythonGraphqlSchema, PythonObservabilitySchema, PythonOrmSchema, PythonQualitySchema, PythonRealtimeSchema, PythonTaskQueueSchema, PythonTestingSchema, PythonValidationSchema, PythonWebFrameworkSchema, RateLimitSchema, RealtimeSchema, RuntimeSchema, RustApiSchema, RustAuthSchema, RustCachingSchema, RustCliSchema, RustErrorHandlingSchema, RustFrontendSchema, RustLibrariesSchema, RustLoggingSchema, RustMessageQueueSchema, RustObservabilitySchema, RustOrmSchema, RustRealtimeSchema, RustTemplatingSchema, RustWebFrameworkSchema, SearchSchema, ServerDeploySchema, ShadcnBaseColorSchema, ShadcnBaseSchema, ShadcnColorThemeSchema, ShadcnFontSchema, ShadcnIconLibrarySchema, ShadcnRadiusSchema, ShadcnStyleSchema, StateManagementSchema, TEMPLATE_VALUES, TestingSchema, UILibrarySchema, ValidationSchema, VectorDbSchema, VersionChannelSchema, WebDeploySchema, analyzeStackCompatibility, evaluateCompatibility, formatStackPartSpec, getCategoryOrderForEcosystem, legacyProjectConfigToStackParts, parseStackPartSpecs, stackPartsToLegacyProjectConfigPartial } from "@better-fullstack/types";
9
+ import { AISchema, APISchema, AddonsSchema, AiDocsSchema, AnalyticsSchema, AnimationSchema, AstroIntegrationSchema, AuthSchema, BackendSchema, CATEGORY_ORDER, CMSSchema, CSSFrameworkSchema, CachingSchema, DatabaseSchema, DatabaseSetupSchema, DotnetApiSchema, DotnetAuthSchema, DotnetCachingSchema, DotnetDeploySchema, DotnetJobQueueSchema, DotnetObservabilitySchema, DotnetOrmSchema, DotnetRealtimeSchema, DotnetTestingSchema, DotnetValidationSchema, DotnetWebFrameworkSchema, EcosystemSchema, EffectSchema, ElixirApiSchema, ElixirAuthSchema, ElixirCachingSchema, ElixirDeploySchema, ElixirEmailSchema, ElixirHttpSchema, ElixirJobsSchema, ElixirJsonSchema, ElixirLibrariesSchema, ElixirObservabilitySchema, ElixirOrmSchema, ElixirQualitySchema, ElixirRealtimeSchema, ElixirTestingSchema, ElixirValidationSchema, ElixirWebFrameworkSchema, EmailSchema, ExamplesSchema, FeatureFlagsSchema, FileStorageSchema, FileUploadSchema, FormsSchema, FrontendSchema, GoApiSchema, GoAuthSchema, GoCachingSchema, GoCliSchema, GoConfigSchema, GoLoggingSchema, GoMessageQueueSchema, GoObservabilitySchema, GoOrmSchema, GoRealtimeSchema, GoTestingSchema, GoWebFrameworkSchema, I18nSchema, JavaApiSchema, JavaAuthSchema, JavaBuildToolSchema, JavaLanguageSchema, JavaLibrariesSchema, JavaLoggingSchema, JavaOrmSchema, JavaTestingLibrariesSchema, JavaWebFrameworkSchema, JobQueueSchema, LoggingSchema, MobileDeepLinkingSchema, MobileNavigationSchema, MobileOTASchema, MobilePushSchema, MobileStorageSchema, MobileTestingSchema, MobileUISchema, OPTION_CATEGORY_METADATA, ORMSchema, ObservabilitySchema, PackageManagerSchema, PaymentsSchema, PythonAiSchema, PythonApiSchema, PythonAuthSchema, PythonCachingSchema, PythonCliSchema, PythonGraphqlSchema, PythonObservabilitySchema, PythonOrmSchema, PythonQualitySchema, PythonRealtimeSchema, PythonTaskQueueSchema, PythonTestingSchema, PythonValidationSchema, PythonWebFrameworkSchema, RateLimitSchema, RealtimeSchema, RuntimeSchema, RustApiSchema, RustAuthSchema, RustCachingSchema, RustCliSchema, RustErrorHandlingSchema, RustFrontendSchema, RustLibrariesSchema, RustLoggingSchema, RustMessageQueueSchema, RustObservabilitySchema, RustOrmSchema, RustRealtimeSchema, RustTemplatingSchema, RustWebFrameworkSchema, SearchSchema, ServerDeploySchema, ShadcnBaseColorSchema, ShadcnBaseSchema, ShadcnColorThemeSchema, ShadcnFontSchema, ShadcnIconLibrarySchema, ShadcnRadiusSchema, ShadcnStyleSchema, StateManagementSchema, TEMPLATE_VALUES, TestingSchema, UILibrarySchema, ValidationSchema, VectorDbSchema, VersionChannelSchema, WebDeploySchema, analyzeStackCompatibility, evaluateCompatibility, formatStackPartSpec, getCategoryOrderForEcosystem, legacyProjectConfigToStackParts, parseStackPartSpecs, stackPartsToLegacyProjectConfigPartial } from "@better-fullstack/types";
9
10
  import { tmpdir } from "node:os";
10
11
  import { EMBEDDED_TEMPLATES, generateVirtualProject } from "@better-fullstack/template-generator";
11
12
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -43,7 +44,8 @@ const NON_STACK_UPDATE_CREATE_KEYS = new Set([
43
44
  "directoryConflict",
44
45
  "renderTitle",
45
46
  "disableAnalytics",
46
- "manualDb"
47
+ "manualDb",
48
+ "workspaceShape"
47
49
  ]);
48
50
  const SUPPORTED_STACK_UPDATE_KEYS = Object.keys(CreateCommandOptionsSchema.shape).filter((key) => !NON_STACK_UPDATE_CREATE_KEYS.has(key)).sort();
49
51
  const SUPPORTED_STACK_UPDATE_KEY_SET = new Set(SUPPORTED_STACK_UPDATE_KEYS);
@@ -51,8 +53,17 @@ const IGNORED_REQUEST_KEYS = new Set([
51
53
  "projectDir",
52
54
  "projectName",
53
55
  "install",
54
- "git"
56
+ "git",
57
+ "acknowledgeArchitectureChange"
55
58
  ]);
59
+ const RISKY_ARCHITECTURE_KEYS = [
60
+ "database",
61
+ "orm",
62
+ "auth",
63
+ "api",
64
+ "backend",
65
+ "runtime"
66
+ ];
56
67
  const PACKAGE_JSON_SECTIONS = [
57
68
  "dependencies",
58
69
  "devDependencies",
@@ -193,6 +204,18 @@ function getUpdatedSpecForChangedPart(part, parts, proposedConfig, changedKeys)
193
204
  }, parts);
194
205
  }
195
206
  }
207
+ function pruneScopedSpecsWithoutOwners(specs) {
208
+ const primaryRoles = /* @__PURE__ */ new Set();
209
+ for (const spec of specs) {
210
+ const rolePath = spec.split(":")[0];
211
+ if (rolePath && !rolePath.includes(".")) primaryRoles.add(rolePath);
212
+ }
213
+ return specs.filter((spec) => {
214
+ const rolePath = spec.split(":")[0];
215
+ const ownerRole = rolePath?.split(".")[0];
216
+ return !rolePath?.includes(".") || ownerRole !== void 0 && primaryRoles.has(ownerRole);
217
+ });
218
+ }
196
219
  function mergeDerivedStackPartsWithExistingGraph(currentConfig, proposedConfig) {
197
220
  const currentStackParts = currentConfig.stackParts?.length ? currentConfig.stackParts : (0, types_exports.legacyProjectConfigToStackParts)(currentConfig);
198
221
  const derivedStackParts = (0, types_exports.legacyProjectConfigToStackParts)(proposedConfig);
@@ -210,21 +233,82 @@ function mergeDerivedStackPartsWithExistingGraph(currentConfig, proposedConfig)
210
233
  return spec ? [spec] : [];
211
234
  });
212
235
  const preservedSpecs = new Set(preservedParts.map((part) => (0, types_exports.formatStackPartSpec)(part, currentStackParts)));
213
- const coveredParts = (0, types_exports.parseStackPartSpecs)([...new Set([...preservedSpecs, ...updatedSpecs])], "selected");
236
+ const coveredSpecs = pruneScopedSpecsWithoutOwners([...new Set([...preservedSpecs, ...updatedSpecs])]);
237
+ const coveredParts = (0, types_exports.parseStackPartSpecs)(coveredSpecs, "selected");
214
238
  const preservedProjectedKeys = /* @__PURE__ */ new Set();
215
239
  for (const part of coveredParts) for (const key of getProjectedConfigKeys(part, coveredParts)) preservedProjectedKeys.add(key);
216
- const nextSpecs = [...new Set([...preservedSpecs, ...updatedSpecs])];
240
+ const nextSpecs = [...coveredSpecs];
217
241
  for (const part of derivedStackParts) {
218
242
  if (part.source === "provided") continue;
219
243
  const spec = (0, types_exports.formatStackPartSpec)(part, derivedStackParts);
220
244
  if (nextSpecs.includes(spec)) continue;
221
245
  if (![...getProjectedConfigKeys(part, derivedStackParts)].some((key) => preservedProjectedKeys.has(key))) nextSpecs.push(spec);
222
246
  }
223
- return (0, types_exports.parseStackPartSpecs)([...new Set(nextSpecs)], "selected");
247
+ return (0, types_exports.parseStackPartSpecs)(pruneScopedSpecsWithoutOwners([...new Set(nextSpecs)]), "selected");
224
248
  }
225
249
  function asString(value, fallback = "none") {
226
250
  return typeof value === "string" ? value : fallback;
227
251
  }
252
+ function computeArchitectureChanges(currentConfig, proposedConfig) {
253
+ const changes = [];
254
+ for (const key of RISKY_ARCHITECTURE_KEYS) {
255
+ const from = asString(currentConfig[key]);
256
+ const to = asString(proposedConfig[key]);
257
+ if (from !== "none" && from !== to) changes.push({
258
+ key,
259
+ from,
260
+ to
261
+ });
262
+ }
263
+ return changes;
264
+ }
265
+ function buildMigrationSteps(changes) {
266
+ const steps = [];
267
+ for (const { key, from, to } of changes) {
268
+ const label = `${key} (${from} -> ${to})`;
269
+ switch (key) {
270
+ case "database":
271
+ steps.push(`${label}: Back up all existing data from the ${from} database before making changes.`, `${label}: Provision a ${to} database and update DATABASE_URL in .env and .env.example.`, `${label}: Regenerate the schema for ${to} and create + run an initial migration.`, `${label}: Export rows from ${from} and import them into ${to} (data is NOT migrated automatically).`);
272
+ break;
273
+ case "orm":
274
+ steps.push(`${label}: Re-author the database schema/models using ${to} conventions.`, `${label}: Regenerate the ${to} client and create an initial ${to} migration.`, `${label}: Port existing ${from} queries and migration history to ${to}, then remove ${from} artifacts.`);
275
+ break;
276
+ case "auth":
277
+ steps.push(`${label}: Migrate existing user/account records into the ${to} schema.`, `${label}: Invalidate current sessions and update auth secrets/env vars for ${to}.`, `${label}: Update sign-in/sign-up flows and protected routes to use ${to}.`);
278
+ break;
279
+ case "api":
280
+ steps.push(`${label}: Port server routers/handlers from ${from} to ${to}.`, `${label}: Update client call sites and generated types to the ${to} client.`);
281
+ break;
282
+ case "backend":
283
+ steps.push(`${label}: Port the server entrypoint, routes, and middleware from ${from} to ${to}.`, `${label}: Reconcile runtime and deploy configuration for the ${to} server.`);
284
+ break;
285
+ case "runtime":
286
+ steps.push(`${label}: Update the runtime toolchain, scripts, and deploy target for ${to}.`, `${label}: Verify runtime-specific APIs and environment bindings behave correctly on ${to}.`);
287
+ break;
288
+ default: steps.push(`${label}: Review and migrate affected code manually.`);
289
+ }
290
+ }
291
+ return steps;
292
+ }
293
+ async function writeMigrationChecklist(projectDir, plan) {
294
+ if (plan.architectureChanges.length === 0 || plan.migrationSteps.length === 0) return;
295
+ const migrationPath = path.join(projectDir, "MIGRATION.md");
296
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
297
+ const swaps = plan.architectureChanges.map((change) => `\`${change.key}\`: \`${change.from}\` -> \`${change.to}\``).join(", ");
298
+ const section = [
299
+ `## Architecture change - ${timestamp}`,
300
+ "",
301
+ `Swapped: ${swaps}`,
302
+ "",
303
+ "Data and schema are NOT migrated automatically. Complete these steps manually:",
304
+ "",
305
+ ...plan.migrationSteps.map((step) => `- [ ] ${step}`)
306
+ ].join("\n");
307
+ if (await fs.pathExists(migrationPath)) {
308
+ const existing = (await fs.readFile(migrationPath, "utf-8")).trimEnd();
309
+ await fs.writeFile(migrationPath, `${existing}\n\n${section}\n`, "utf-8");
310
+ } else await fs.writeFile(migrationPath, `# Migration checklist\n\n${section}\n`, "utf-8");
311
+ }
228
312
  function asStringArray(value) {
229
313
  return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
230
314
  }
@@ -304,6 +388,13 @@ function getDefaultNativeFrontendForRequestedUpdate(requestedChanges) {
304
388
  if (requestedChanges.mobileUI === "uniwind" || requestedChanges.mobileUI === "unistyles") return;
305
389
  return hasRequestedNonNoneValue(requestedChanges, "mobileNavigation") || hasRequestedNonNoneValue(requestedChanges, "mobileUI") || hasRequestedNonNoneValue(requestedChanges, "mobileStorage") || hasRequestedNonNoneValue(requestedChanges, "mobileTesting") || hasRequestedNonNoneValue(requestedChanges, "mobilePush") || hasRequestedNonNoneValue(requestedChanges, "mobileOTA") || hasRequestedNonNoneValue(requestedChanges, "mobileDeepLinking") ? "native-bare" : void 0;
306
390
  }
391
+ function hasSelectedTypeScriptBackendPart(config) {
392
+ return config.stackParts?.some((part) => part.source !== "provided" && !part.ownerPartId && part.role === "backend" && part.ecosystem === "typescript" && part.toolId !== "none") ?? false;
393
+ }
394
+ function getCompatibilityEcosystem(config) {
395
+ if (config.ecosystem === "react-native" && hasSelectedTypeScriptBackendPart(config)) return "typescript";
396
+ return config.ecosystem;
397
+ }
307
398
  function buildCompatibilityInputFromConfig(config) {
308
399
  const frontend = asStringArray(config.frontend);
309
400
  const addons = asStringArray(config.addons);
@@ -319,7 +410,7 @@ function buildCompatibilityInputFromConfig(config) {
319
410
  else if (addon !== "none") appPlatforms.push(addon);
320
411
  }
321
412
  return {
322
- ecosystem: config.ecosystem,
413
+ ecosystem: getCompatibilityEcosystem(config),
323
414
  projectName: config.projectName ?? null,
324
415
  webFrontend,
325
416
  nativeFrontend,
@@ -372,6 +463,7 @@ function buildCompatibilityInputFromConfig(config) {
372
463
  documentation,
373
464
  appPlatforms,
374
465
  packageManager: asString(config.packageManager, "bun"),
466
+ workspaceShape: asString(config.workspaceShape, "monorepo"),
375
467
  versionChannel: asString(config.versionChannel, "stable"),
376
468
  examples: asStringArray(config.examples),
377
469
  aiSdk: asString(config.ai),
@@ -422,6 +514,7 @@ function buildCompatibilityInputFromConfig(config) {
422
514
  goCaching: asString(config.goCaching),
423
515
  goConfig: asString(config.goConfig),
424
516
  goObservability: asString(config.goObservability),
517
+ javaLanguage: asString(config.javaLanguage, "java"),
425
518
  javaWebFramework: asString(config.javaWebFramework),
426
519
  javaBuildTool: asString(config.javaBuildTool),
427
520
  javaOrm: asString(config.javaOrm),
@@ -599,6 +692,16 @@ function applyKnownDependencyExpansions(config, requestedChanges) {
599
692
  adjustments.push("examples: AI SDK set to 'vercel-ai' (Chat SDK profile requires Vercel AI)");
600
693
  }
601
694
  }
695
+ if (next.backend === "effect") {
696
+ if (next.effect !== "effect-full" && !requestedKeys.has("effect")) {
697
+ next.effect = "effect-full";
698
+ adjustments.push("effect: Effect services set to 'effect-full' (Effect backend requires Effect Platform + SQL)");
699
+ }
700
+ if (next.validation !== "effect-schema" && !requestedKeys.has("validation")) {
701
+ next.validation = "effect-schema";
702
+ adjustments.push("validation: Validation set to 'effect-schema' (Effect backend requires Effect Schema)");
703
+ }
704
+ }
602
705
  return {
603
706
  config: next,
604
707
  adjustments
@@ -1037,6 +1140,8 @@ async function planStackUpdate(projectDirInput, input) {
1037
1140
  proposedConfig: normalizedProposedConfig
1038
1141
  });
1039
1142
  const graphPreview = getGraphPreview(persistedProposedConfig);
1143
+ const architectureChanges = computeArchitectureChanges(currentConfig, proposedConfig);
1144
+ const migrationSteps = buildMigrationSteps(architectureChanges);
1040
1145
  return {
1041
1146
  success: true,
1042
1147
  projectDir,
@@ -1049,6 +1154,9 @@ async function planStackUpdate(projectDirInput, input) {
1049
1154
  scriptChanges,
1050
1155
  envChanges,
1051
1156
  manualReviewBlockers,
1157
+ architectureChanges,
1158
+ migrationSteps,
1159
+ requiresArchitectureAck: architectureChanges.length > 0,
1052
1160
  operations,
1053
1161
  installCommand: getInstallCommand$1(normalizedProposedConfig),
1054
1162
  compatibilityAdjustments,
@@ -1063,6 +1171,16 @@ async function applyStackUpdate(projectDirInput, input) {
1063
1171
  projectDir: plan.projectDir,
1064
1172
  error: `Manual review required before applying stack update: ${plan.manualReviewBlockers.join("; ")}`
1065
1173
  };
1174
+ const acknowledgeArchitectureChange = input.acknowledgeArchitectureChange === true;
1175
+ if (plan.requiresArchitectureAck && !acknowledgeArchitectureChange) {
1176
+ const swaps = plan.architectureChanges.map((change) => `${change.key}: ${change.from} -> ${change.to}`).join("; ");
1177
+ const checklist = plan.migrationSteps.map((step) => ` - ${step}`).join("\n");
1178
+ return {
1179
+ success: false,
1180
+ projectDir: plan.projectDir,
1181
+ error: `This architecture change requires acknowledgment before it can be applied. It replaces existing architecture-defining choices (${swaps}); data and schema are NOT migrated automatically. Re-run with acknowledgeArchitectureChange: true (MCP) or --acknowledge-architecture-change (CLI) after reviewing the migration checklist:\n${checklist}`
1182
+ };
1183
+ }
1066
1184
  const projectName = await inferProjectName(plan.projectDir);
1067
1185
  const proposedConfig = configFromBtsConfig(plan.proposedConfig, plan.projectDir, projectName);
1068
1186
  const proposedTree = await generateTree(proposedConfig);
@@ -1078,6 +1196,7 @@ async function applyStackUpdate(projectDirInput, input) {
1078
1196
  version: plan.proposedConfig.version,
1079
1197
  createdAt: plan.proposedConfig.createdAt
1080
1198
  });
1199
+ await writeMigrationChecklist(plan.projectDir, plan);
1081
1200
  return plan;
1082
1201
  }
1083
1202
 
@@ -1326,6 +1445,7 @@ const MCP_COMPATIBILITY_DEFAULTS = {
1326
1445
  mobilePush: "none",
1327
1446
  mobileOTA: "none",
1328
1447
  packageManager: "bun",
1448
+ workspaceShape: "monorepo",
1329
1449
  versionChannel: "stable",
1330
1450
  examples: [],
1331
1451
  aiSdk: "none",
@@ -1376,6 +1496,7 @@ const MCP_COMPATIBILITY_DEFAULTS = {
1376
1496
  goCaching: "none",
1377
1497
  goConfig: "none",
1378
1498
  goObservability: "none",
1499
+ javaLanguage: "java",
1379
1500
  javaWebFramework: "spring-boot",
1380
1501
  javaBuildTool: "maven",
1381
1502
  javaOrm: "none",
@@ -1465,6 +1586,7 @@ function buildProjectConfig(input, overrides) {
1465
1586
  const stackParts = parseStackPartSpecs(input.part.filter((part) => typeof part === "string"), "selected");
1466
1587
  Object.assign(config, stackPartsToLegacyProjectConfigPartial(stackParts), { stackParts });
1467
1588
  }
1589
+ applyEffectBackendDefaults(config, new Set(Object.keys(input)));
1468
1590
  return config;
1469
1591
  }
1470
1592
  function sanitizePath(input) {
@@ -1487,7 +1609,7 @@ function buildCompatibilityInput(input) {
1487
1609
  ...documentation,
1488
1610
  "none"
1489
1611
  ].includes(a));
1490
- return {
1612
+ const result = {
1491
1613
  ecosystem,
1492
1614
  projectName: input.projectName ?? null,
1493
1615
  webFrontend,
@@ -1500,6 +1622,11 @@ function buildCompatibilityInput(input) {
1500
1622
  appPlatforms,
1501
1623
  aiSdk: input.ai ?? defaults.aiSdk
1502
1624
  };
1625
+ if (result.backend === "effect") {
1626
+ if (input.effect === void 0) result.backendLibraries = "effect-full";
1627
+ if (input.validation === void 0) result.validation = "effect-schema";
1628
+ }
1629
+ return result;
1503
1630
  }
1504
1631
  function summarizeTree(tree) {
1505
1632
  const paths = [];
@@ -1534,6 +1661,7 @@ const COMPATIBILITY_RULES_MD = `# Better-Fullstack Compatibility Rules
1534
1661
 
1535
1662
  ## Backend Constraints
1536
1663
  - **Convex**: Forces runtime=none, database=none, orm=none, api=none, dbSetup=none, serverDeploy=none. Removes incompatible frontends (Solid, SolidStart, Astro).
1664
+ - **Effect backend**: Requires effect=effect-full and validation=effect-schema. Other compatible frontend/backend-adjacent tools can still be selected.
1537
1665
  - **No backend (none)**: Clears auth, payments, database, orm, api, serverDeploy, search, fileStorage.
1538
1666
  - **Fullstack (backend='self')**: Sets runtime=none, serverDeploy=none. Only works with: next, vinext, tanstack-start, astro, nuxt, svelte, solid-start.
1539
1667
 
@@ -1734,6 +1862,13 @@ const stackUpdateOutputSchema = {
1734
1862
  scriptChanges: z.record(z.string(), z.array(z.string())).optional(),
1735
1863
  envChanges: z.record(z.string(), z.array(z.string())).optional(),
1736
1864
  manualReviewBlockers: z.array(z.string()).optional(),
1865
+ architectureChanges: z.array(z.object({
1866
+ key: z.string(),
1867
+ from: z.string(),
1868
+ to: z.string()
1869
+ })).optional(),
1870
+ migrationSteps: z.array(z.string()).optional(),
1871
+ requiresArchitectureAck: z.boolean().optional(),
1737
1872
  compatibilityAdjustments: z.array(z.string()).optional(),
1738
1873
  compatibilityWarnings: z.array(z.string()).optional(),
1739
1874
  installCommand: z.string().optional(),
@@ -1989,6 +2124,7 @@ const crossEcosystemInputSchema = {
1989
2124
  goCaching: GoCachingSchema.optional().describe("Go caching library"),
1990
2125
  goConfig: GoConfigSchema.optional().describe("Go config management"),
1991
2126
  goObservability: GoObservabilitySchema.optional().describe("Go observability"),
2127
+ javaLanguage: JavaLanguageSchema.optional().describe("JVM language (java, kotlin)"),
1992
2128
  javaWebFramework: JavaWebFrameworkSchema.optional().describe("Java web framework"),
1993
2129
  javaBuildTool: JavaBuildToolSchema.optional().describe("Java build tool"),
1994
2130
  javaOrm: JavaOrmSchema.optional().describe("Java ORM"),
@@ -2071,7 +2207,7 @@ const MCP_PLAN_CREATE_SCHEMA = {
2071
2207
  ...mobileInputSchema,
2072
2208
  fileUpload: FileUploadSchema.optional().describe("File upload"),
2073
2209
  ...deploymentInputSchema,
2074
- effect: EffectSchema.optional().describe("Effect ecosystem (effect, effect-full)"),
2210
+ effect: EffectSchema.optional().describe("Effect services (effect, effect-full)"),
2075
2211
  analytics: AnalyticsSchema.optional().describe("Privacy-focused analytics provider"),
2076
2212
  astroIntegration: AstroIntegrationSchema.optional().describe("Astro UI framework integration (react, vue, svelte, solid)"),
2077
2213
  aiDocs: z.array(AiDocsSchema).optional().describe("AI documentation files (claude-md, agents-md, cursorrules)"),
@@ -2080,7 +2216,8 @@ const MCP_PLAN_CREATE_SCHEMA = {
2080
2216
  };
2081
2217
  const MCP_STACK_UPDATE_SCHEMA = {
2082
2218
  ...MCP_PLAN_CREATE_SCHEMA,
2083
- projectDir: z.string().describe("Absolute path to the existing Better-Fullstack project")
2219
+ projectDir: z.string().describe("Absolute path to the existing Better-Fullstack project"),
2220
+ acknowledgeArchitectureChange: z.boolean().optional().describe("Acknowledge that this update replaces an existing database/orm/auth/api/backend/runtime choice. Required to apply architecture-changing updates; data and schema are NOT migrated automatically.")
2084
2221
  };
2085
2222
  async function startMcpServer() {
2086
2223
  const server = new McpServer({
@@ -2393,7 +2530,7 @@ async function startMcpServer() {
2393
2530
  const graphPreview = getMcpGraphPreview(config);
2394
2531
  let addonWarnings = [];
2395
2532
  if (config.addons.length > 0 && config.addons[0] !== "none") {
2396
- const { setupAddons } = await import("./addons-setup-DyMm42a5.mjs");
2533
+ const { setupAddons } = await import("./addons-setup-DEPfsn6z.mjs");
2397
2534
  addonWarnings = await setupAddons(config);
2398
2535
  }
2399
2536
  const installCmd = getInstallCommand(input.ecosystem ?? "typescript", projectName, input.packageManager, input.javaBuildTool, input.javaWebFramework);
@@ -2455,7 +2592,7 @@ async function startMcpServer() {
2455
2592
  ...safePlan,
2456
2593
  ...plan.compatibilityAdjustments.length > 0 ? { compatibilityAdjustments: plan.compatibilityAdjustments } : {},
2457
2594
  ...compatibilityWarnings ? { compatibilityWarnings } : {},
2458
- message: plan.manualReviewBlockers.length > 0 ? "Plan created, but manual review is required before applying." : `Plan created. If approved, call bfs_apply_stack_update, then run: ${plan.installCommand}`
2595
+ message: plan.manualReviewBlockers.length > 0 ? "Plan created, but manual review is required before applying." : plan.requiresArchitectureAck ? `Plan created. This is an architecture change (${plan.architectureChanges.map((change) => `${change.key}: ${change.from} -> ${change.to}`).join("; ")}); data and schema are NOT migrated automatically. Review migrationSteps, then call bfs_apply_stack_update with acknowledgeArchitectureChange: true, then run: ${plan.installCommand}` : `Plan created. If approved, call bfs_apply_stack_update, then run: ${plan.installCommand}`
2459
2596
  };
2460
2597
  return {
2461
2598
  content: [{
@@ -2718,4 +2855,4 @@ async function startMcpServer() {
2718
2855
  startMcpServer();
2719
2856
 
2720
2857
  //#endregion
2721
- export { applyStackUpdate as a, startMcpServer as i, MCP_STACK_UPDATE_SCHEMA as n, planStackUpdate as o, getMcpGraphPreview as r, MCP_PLAN_CREATE_SCHEMA as t };
2858
+ export { startMcpServer as a, formatGeneratedTree as c, treeToFileMap as d, recommendStackFromBrief as i, generateTree as l, MCP_STACK_UPDATE_SCHEMA as n, applyStackUpdate as o, getMcpGraphPreview as r, configFromBtsConfig as s, MCP_PLAN_CREATE_SCHEMA as t, planStackUpdate as u };