create-better-fullstack 2.0.3 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,10 +1,902 @@
1
1
  #!/usr/bin/env node
2
- import { a as getEffectiveStack, i as writeBtsConfig, l as getGraphSummary, n as readBtsConfig, p as getLatestCLIVersion, t as previewBtsConfigUpdate } from "./bts-config-Bg1Qea9Y.mjs";
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-D9yiiGVq.mjs";
3
+ import { a as generateReproducibleCommand, i as getTemplateDescription, r as getTemplateConfig, s as CreateCommandOptionsSchema, t as validateConfigForProgrammaticUse } from "./config-validation-C4glouQh.mjs";
4
+ import "./compatibility-rules-D7zYNVjC.mjs";
3
5
  import z from "zod";
4
- import { AISchema, APISchema, AddonsSchema, 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, StateManagementSchema, TestingSchema, UILibrarySchema, ValidationSchema, WebDeploySchema, analyzeStackCompatibility, formatStackPartSpec, getCategoryOrderForEcosystem, legacyProjectConfigToStackParts } from "@better-fullstack/types";
6
+ import fs from "fs-extra";
7
+ 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 { tmpdir } from "node:os";
10
+ import { EMBEDDED_TEMPLATES, generateVirtualProject } from "@better-fullstack/template-generator";
5
11
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
12
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
13
+ import { writeSelectedFiles } from "@better-fullstack/template-generator/fs-writer";
7
14
 
15
+ //#region src/helpers/core/stack-update.ts
16
+ const ARRAY_UPDATE_KEYS = new Set([
17
+ "frontend",
18
+ "addons",
19
+ "examples",
20
+ "aiDocs",
21
+ "rustLibraries",
22
+ "pythonAi",
23
+ "pythonTesting",
24
+ "pythonCli",
25
+ "goTesting",
26
+ "javaLibraries",
27
+ "javaTestingLibraries",
28
+ "dotnetTesting",
29
+ "dotnetObservability",
30
+ "elixirLibraries"
31
+ ]);
32
+ const NON_STACK_UPDATE_CREATE_KEYS = new Set([
33
+ "template",
34
+ "fromHistory",
35
+ "config",
36
+ "yes",
37
+ "yolo",
38
+ "verbose",
39
+ "dryRun",
40
+ "verify",
41
+ "git",
42
+ "install",
43
+ "directoryConflict",
44
+ "renderTitle",
45
+ "disableAnalytics",
46
+ "manualDb"
47
+ ]);
48
+ const SUPPORTED_STACK_UPDATE_KEYS = Object.keys(CreateCommandOptionsSchema.shape).filter((key) => !NON_STACK_UPDATE_CREATE_KEYS.has(key)).sort();
49
+ const SUPPORTED_STACK_UPDATE_KEY_SET = new Set(SUPPORTED_STACK_UPDATE_KEYS);
50
+ const IGNORED_REQUEST_KEYS = new Set([
51
+ "projectDir",
52
+ "projectName",
53
+ "install",
54
+ "git"
55
+ ]);
56
+ const PACKAGE_JSON_SECTIONS = [
57
+ "dependencies",
58
+ "devDependencies",
59
+ "peerDependencies",
60
+ "scripts"
61
+ ];
62
+ const BINARY_FILE_MARKER = "[Binary file]";
63
+ function isEnvFilePath(filePath) {
64
+ const name = path.basename(filePath);
65
+ return name === ".env" || name.endsWith(".env.example");
66
+ }
67
+ function isSkippableGeneratedDoc(filePath) {
68
+ return path.basename(filePath).toLowerCase() === "readme.md";
69
+ }
70
+ function isGeneratedBinaryFile(file) {
71
+ return file?.content === BINARY_FILE_MARKER && typeof file.sourcePath === "string";
72
+ }
73
+ async function readGeneratedFileBytes(tree, filePath, file) {
74
+ if (!isGeneratedBinaryFile(file)) return void 0;
75
+ const tempDir = await fs.mkdtemp(path.join(tmpdir(), "bfs-stack-update-binary-"));
76
+ try {
77
+ if (!(await writeSelectedFiles(tree, tempDir, (candidatePath) => candidatePath === filePath)).includes(filePath)) return void 0;
78
+ return await fs.readFile(path.join(tempDir, filePath));
79
+ } finally {
80
+ await fs.rm(tempDir, {
81
+ recursive: true,
82
+ force: true
83
+ });
84
+ }
85
+ }
86
+ function buffersEqual(left, right) {
87
+ return Boolean(left && right && left.equals(right));
88
+ }
89
+ async function inferProjectName(projectDir) {
90
+ const packageJsonPath = path.join(projectDir, "package.json");
91
+ const packageJson = await fs.readJson(packageJsonPath).catch(() => null);
92
+ if (packageJson && typeof packageJson.name === "string" && packageJson.name.trim()) return packageJson.name.trim();
93
+ return path.basename(projectDir);
94
+ }
95
+ function configFromBtsConfig(config, projectDir, projectName) {
96
+ return {
97
+ ...getDefaultConfig(),
98
+ ...config,
99
+ projectName,
100
+ projectDir,
101
+ relativePath: ".",
102
+ git: false,
103
+ install: false
104
+ };
105
+ }
106
+ function buildRequestedChanges(input) {
107
+ const changes = {};
108
+ const stackPartSpecs = [];
109
+ const unsupportedKeys = [];
110
+ for (const [key, value] of Object.entries(input)) {
111
+ if (value === void 0 || IGNORED_REQUEST_KEYS.has(key)) continue;
112
+ if (!SUPPORTED_STACK_UPDATE_KEY_SET.has(key)) {
113
+ unsupportedKeys.push(key);
114
+ continue;
115
+ }
116
+ if (key === "part") {
117
+ if (Array.isArray(value)) stackPartSpecs.push(...value.filter((item) => typeof item === "string"));
118
+ continue;
119
+ }
120
+ changes[key] = value;
121
+ }
122
+ return {
123
+ changes,
124
+ stackPartSpecs,
125
+ unsupportedKeys
126
+ };
127
+ }
128
+ function mergeProjectConfig(currentConfig, requestedChanges) {
129
+ const next = { ...currentConfig };
130
+ for (const [key, value] of Object.entries(requestedChanges)) {
131
+ if (value === void 0) continue;
132
+ if (ARRAY_UPDATE_KEYS.has(key) && Array.isArray(value)) {
133
+ const arrayValue = value;
134
+ const requested = arrayValue.filter((item) => typeof item === "string" && item !== "none");
135
+ if (requested.length === 0 && arrayValue.includes("none")) next[key] = [];
136
+ else {
137
+ const existing = Array.isArray(next[key]) ? next[key].filter((item) => item !== "none") : [];
138
+ next[key] = [...new Set([...existing, ...requested])];
139
+ }
140
+ continue;
141
+ }
142
+ next[key] = value;
143
+ }
144
+ return next;
145
+ }
146
+ function mergeStackPartSpecs(currentConfig, specs) {
147
+ if (specs.length === 0) return {};
148
+ const currentStackParts = currentConfig.stackParts?.length ? currentConfig.stackParts : (0, types_exports.legacyProjectConfigToStackParts)(currentConfig);
149
+ const currentSpecs = currentStackParts.filter((part) => part.source !== "provided").map((part) => (0, types_exports.formatStackPartSpec)(part, currentStackParts));
150
+ const stackParts = (0, types_exports.parseStackPartSpecs)([...new Set([...currentSpecs, ...specs])], "selected");
151
+ return {
152
+ ...(0, types_exports.stackPartsToLegacyProjectConfigPartial)(stackParts),
153
+ stackParts
154
+ };
155
+ }
156
+ function asString(value, fallback = "none") {
157
+ return typeof value === "string" ? value : fallback;
158
+ }
159
+ function asStringArray(value) {
160
+ return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
161
+ }
162
+ function getCompatibilityBackend(config, webFrontend) {
163
+ if (config.backend !== "self") return asString(config.backend, "hono");
164
+ if (webFrontend.includes("next")) return "self-next";
165
+ if (webFrontend.includes("vinext")) return "self-vinext";
166
+ if (webFrontend.includes("tanstack-start")) return "self-tanstack-start";
167
+ if (webFrontend.includes("astro")) return "self-astro";
168
+ if (webFrontend.includes("nuxt")) return "self-nuxt";
169
+ if (webFrontend.includes("svelte")) return "self-svelte";
170
+ if (webFrontend.includes("solid-start")) return "self-solid-start";
171
+ return "self";
172
+ }
173
+ function getProjectBackendFromCompatibility(backend) {
174
+ return backend.startsWith("self-") ? "self" : backend;
175
+ }
176
+ function getDefaultDatabaseForDbSetup(dbSetup) {
177
+ switch (dbSetup) {
178
+ case "turso":
179
+ case "d1": return "sqlite";
180
+ case "neon":
181
+ case "prisma-postgres":
182
+ case "planetscale":
183
+ case "supabase":
184
+ case "docker": return "postgres";
185
+ case "mongodb-atlas": return "mongodb";
186
+ case "upstash": return "redis";
187
+ default: return;
188
+ }
189
+ }
190
+ function getDefaultOrmForDatabase(database) {
191
+ switch (database) {
192
+ case "sqlite":
193
+ case "postgres":
194
+ case "mysql": return "drizzle";
195
+ case "mongodb": return "prisma";
196
+ default: return;
197
+ }
198
+ }
199
+ function getRequiredCssFrameworkForUiLibrary(uiLibrary) {
200
+ switch (uiLibrary) {
201
+ case "shadcn-ui":
202
+ case "shadcn-svelte":
203
+ case "daisyui":
204
+ case "nextui":
205
+ case "park-ui": return "tailwind";
206
+ default: return;
207
+ }
208
+ }
209
+ function isBetterAuth(auth) {
210
+ return auth === "better-auth" || auth === "better-auth-organizations";
211
+ }
212
+ function withNativeFrontendVariant(frontend, nativeFrontend) {
213
+ return [...(Array.isArray(frontend) ? frontend : []).filter((item) => !item.startsWith("native-") && item !== "none"), nativeFrontend];
214
+ }
215
+ function withWebFrontendVariant(frontend, webFrontend) {
216
+ return [webFrontend, ...(Array.isArray(frontend) ? frontend : []).filter((item) => item.startsWith("native-"))];
217
+ }
218
+ function hasRequestedNonNoneValue(requestedChanges, key) {
219
+ const value = requestedChanges[key];
220
+ if (Array.isArray(value)) return value.some((item) => item !== "none");
221
+ return value !== void 0 && value !== "none";
222
+ }
223
+ function needsStandaloneBackendForRequestedUpdate(requestedChanges) {
224
+ return hasRequestedNonNoneValue(requestedChanges, "database") || hasRequestedNonNoneValue(requestedChanges, "orm") || hasRequestedNonNoneValue(requestedChanges, "dbSetup") || hasRequestedNonNoneValue(requestedChanges, "api") || isBetterAuth(requestedChanges.auth) || hasRequestedNonNoneValue(requestedChanges, "payments") || hasRequestedNonNoneValue(requestedChanges, "email") || hasRequestedNonNoneValue(requestedChanges, "logging") || hasRequestedNonNoneValue(requestedChanges, "observability") || hasRequestedNonNoneValue(requestedChanges, "caching") || hasRequestedNonNoneValue(requestedChanges, "rateLimit") || hasRequestedNonNoneValue(requestedChanges, "jobQueue") || hasRequestedNonNoneValue(requestedChanges, "realtime") || hasRequestedNonNoneValue(requestedChanges, "search") || hasRequestedNonNoneValue(requestedChanges, "vectorDb") || hasRequestedNonNoneValue(requestedChanges, "fileStorage") || hasRequestedNonNoneValue(requestedChanges, "serverDeploy");
225
+ }
226
+ function getDefaultWebFrontendForRequestedUpdate(requestedChanges) {
227
+ if (hasRequestedNonNoneValue(requestedChanges, "payments")) return "next";
228
+ if (requestedChanges.i18n === "next-intl") return "next";
229
+ if (requestedChanges.cms === "payload" || requestedChanges.cms === "keystatic") return "next";
230
+ if (requestedChanges.uiLibrary === "shadcn-svelte") return "svelte";
231
+ if (asStringArray(requestedChanges.addons).some((addon) => addon === "pwa" || addon === "tauri")) return "react-vite";
232
+ return hasRequestedNonNoneValue(requestedChanges, "cssFramework") || hasRequestedNonNoneValue(requestedChanges, "uiLibrary") || hasRequestedNonNoneValue(requestedChanges, "forms") || hasRequestedNonNoneValue(requestedChanges, "stateManagement") || hasRequestedNonNoneValue(requestedChanges, "animation") || hasRequestedNonNoneValue(requestedChanges, "fileUpload") || hasRequestedNonNoneValue(requestedChanges, "i18n") || hasRequestedNonNoneValue(requestedChanges, "cms") || hasRequestedNonNoneValue(requestedChanges, "analytics") || hasRequestedNonNoneValue(requestedChanges, "webDeploy") || requestedChanges.ai === "tanstack-ai" ? "react-vite" : void 0;
233
+ }
234
+ function getDefaultNativeFrontendForRequestedUpdate(requestedChanges) {
235
+ if (requestedChanges.mobileUI === "uniwind" || requestedChanges.mobileUI === "unistyles") return;
236
+ 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;
237
+ }
238
+ function buildCompatibilityInputFromConfig(config) {
239
+ const frontend = asStringArray(config.frontend);
240
+ const addons = asStringArray(config.addons);
241
+ const webFrontend = frontend.filter((item) => !item.startsWith("native-") && item !== "none");
242
+ const nativeFrontend = frontend.filter((item) => item.startsWith("native-"));
243
+ const codeQuality = [];
244
+ const documentation = [];
245
+ const appPlatforms = [];
246
+ for (const addon of addons) {
247
+ const binding = (0, types_exports.getAddonStackPartBinding)(addon);
248
+ if (binding?.role === "codeQuality") codeQuality.push(addon);
249
+ else if (binding?.role === "documentation") documentation.push(addon);
250
+ else if (addon !== "none") appPlatforms.push(addon);
251
+ }
252
+ return {
253
+ ecosystem: config.ecosystem,
254
+ projectName: config.projectName ?? null,
255
+ webFrontend,
256
+ nativeFrontend,
257
+ astroIntegration: asString(config.astroIntegration),
258
+ runtime: asString(config.runtime, "bun"),
259
+ backend: getCompatibilityBackend(config, webFrontend),
260
+ database: asString(config.database),
261
+ orm: asString(config.orm),
262
+ dbSetup: asString(config.dbSetup),
263
+ auth: asString(config.auth),
264
+ payments: asString(config.payments),
265
+ email: asString(config.email),
266
+ fileUpload: asString(config.fileUpload),
267
+ logging: asString(config.logging),
268
+ observability: asString(config.observability),
269
+ featureFlags: asString(config.featureFlags),
270
+ analytics: asString(config.analytics),
271
+ backendLibraries: "none",
272
+ stateManagement: asString(config.stateManagement),
273
+ forms: asString(config.forms),
274
+ validation: asString(config.validation),
275
+ testing: asString(config.testing),
276
+ realtime: asString(config.realtime),
277
+ jobQueue: asString(config.jobQueue),
278
+ caching: asString(config.caching),
279
+ rateLimit: asString(config.rateLimit),
280
+ animation: asString(config.animation),
281
+ cssFramework: asString(config.cssFramework),
282
+ uiLibrary: asString(config.uiLibrary),
283
+ shadcnBase: asString(config.shadcnBase, "radix"),
284
+ shadcnStyle: asString(config.shadcnStyle, "nova"),
285
+ shadcnIconLibrary: asString(config.shadcnIconLibrary, "lucide"),
286
+ shadcnColorTheme: asString(config.shadcnColorTheme, "neutral"),
287
+ shadcnBaseColor: asString(config.shadcnBaseColor, "neutral"),
288
+ shadcnFont: asString(config.shadcnFont, "inter"),
289
+ shadcnRadius: asString(config.shadcnRadius, "default"),
290
+ cms: asString(config.cms),
291
+ i18n: asString(config.i18n),
292
+ search: asString(config.search),
293
+ vectorDb: asString(config.vectorDb),
294
+ fileStorage: asString(config.fileStorage),
295
+ mobileNavigation: asString(config.mobileNavigation),
296
+ mobileUI: asString(config.mobileUI),
297
+ mobileStorage: asString(config.mobileStorage),
298
+ mobileTesting: asString(config.mobileTesting),
299
+ mobilePush: asString(config.mobilePush),
300
+ mobileOTA: asString(config.mobileOTA),
301
+ mobileDeepLinking: asString(config.mobileDeepLinking),
302
+ codeQuality,
303
+ documentation,
304
+ appPlatforms,
305
+ packageManager: asString(config.packageManager, "bun"),
306
+ versionChannel: asString(config.versionChannel, "stable"),
307
+ examples: asStringArray(config.examples),
308
+ aiSdk: asString(config.ai),
309
+ aiDocs: asStringArray(config.aiDocs),
310
+ git: "false",
311
+ install: "false",
312
+ api: asString(config.api),
313
+ webDeploy: asString(config.webDeploy),
314
+ serverDeploy: asString(config.serverDeploy),
315
+ yolo: "false",
316
+ rustWebFramework: asString(config.rustWebFramework),
317
+ rustFrontend: asString(config.rustFrontend),
318
+ rustOrm: asString(config.rustOrm),
319
+ rustApi: asString(config.rustApi),
320
+ rustCli: asString(config.rustCli),
321
+ rustLibraries: asStringArray(config.rustLibraries),
322
+ rustLogging: asString(config.rustLogging),
323
+ rustErrorHandling: asString(config.rustErrorHandling),
324
+ rustCaching: asString(config.rustCaching),
325
+ rustAuth: asString(config.rustAuth),
326
+ rustRealtime: asString(config.rustRealtime),
327
+ rustMessageQueue: asString(config.rustMessageQueue),
328
+ rustObservability: asString(config.rustObservability),
329
+ rustTemplating: asString(config.rustTemplating),
330
+ pythonWebFramework: asString(config.pythonWebFramework),
331
+ pythonOrm: asString(config.pythonOrm),
332
+ pythonValidation: asString(config.pythonValidation),
333
+ pythonAi: asStringArray(config.pythonAi),
334
+ pythonAuth: asString(config.pythonAuth),
335
+ pythonApi: asString(config.pythonApi),
336
+ pythonTaskQueue: asString(config.pythonTaskQueue),
337
+ pythonGraphql: asString(config.pythonGraphql),
338
+ pythonQuality: asString(config.pythonQuality),
339
+ pythonTesting: asStringArray(config.pythonTesting),
340
+ pythonCaching: asString(config.pythonCaching),
341
+ pythonRealtime: asString(config.pythonRealtime),
342
+ pythonObservability: asString(config.pythonObservability),
343
+ pythonCli: asStringArray(config.pythonCli),
344
+ goWebFramework: asString(config.goWebFramework),
345
+ goOrm: asString(config.goOrm),
346
+ goApi: asString(config.goApi),
347
+ goCli: asString(config.goCli),
348
+ goLogging: asString(config.goLogging),
349
+ goAuth: asString(config.goAuth),
350
+ goTesting: asStringArray(config.goTesting),
351
+ goRealtime: asString(config.goRealtime),
352
+ goMessageQueue: asString(config.goMessageQueue),
353
+ goCaching: asString(config.goCaching),
354
+ goConfig: asString(config.goConfig),
355
+ goObservability: asString(config.goObservability),
356
+ javaWebFramework: asString(config.javaWebFramework),
357
+ javaBuildTool: asString(config.javaBuildTool),
358
+ javaOrm: asString(config.javaOrm),
359
+ javaAuth: asString(config.javaAuth),
360
+ javaApi: asString(config.javaApi),
361
+ javaLogging: asString(config.javaLogging),
362
+ javaLibraries: asStringArray(config.javaLibraries),
363
+ javaTestingLibraries: asStringArray(config.javaTestingLibraries),
364
+ dotnetWebFramework: asString(config.dotnetWebFramework),
365
+ dotnetOrm: asString(config.dotnetOrm),
366
+ dotnetAuth: asString(config.dotnetAuth),
367
+ dotnetApi: asString(config.dotnetApi),
368
+ dotnetTesting: asStringArray(config.dotnetTesting),
369
+ dotnetJobQueue: asString(config.dotnetJobQueue),
370
+ dotnetRealtime: asString(config.dotnetRealtime),
371
+ dotnetObservability: asStringArray(config.dotnetObservability),
372
+ dotnetValidation: asString(config.dotnetValidation),
373
+ dotnetCaching: asString(config.dotnetCaching),
374
+ dotnetDeploy: asString(config.dotnetDeploy),
375
+ elixirWebFramework: asString(config.elixirWebFramework),
376
+ elixirOrm: asString(config.elixirOrm),
377
+ elixirAuth: asString(config.elixirAuth),
378
+ elixirApi: asString(config.elixirApi),
379
+ elixirRealtime: asString(config.elixirRealtime),
380
+ elixirJobs: asString(config.elixirJobs),
381
+ elixirValidation: asString(config.elixirValidation),
382
+ elixirHttp: asString(config.elixirHttp),
383
+ elixirJson: asString(config.elixirJson),
384
+ elixirEmail: asString(config.elixirEmail),
385
+ elixirCaching: asString(config.elixirCaching),
386
+ elixirObservability: asString(config.elixirObservability),
387
+ elixirTesting: asString(config.elixirTesting),
388
+ elixirQuality: asString(config.elixirQuality),
389
+ elixirDeploy: asString(config.elixirDeploy),
390
+ elixirLibraries: asStringArray(config.elixirLibraries)
391
+ };
392
+ }
393
+ function compatibilityChangesToProjectConfig(adjusted, baseConfig) {
394
+ const frontend = [...adjusted.webFrontend, ...adjusted.nativeFrontend];
395
+ const ignoredCompatibilityKeys = new Set([
396
+ "webFrontend",
397
+ "nativeFrontend",
398
+ "codeQuality",
399
+ "documentation",
400
+ "appPlatforms",
401
+ "aiSdk",
402
+ "backendLibraries",
403
+ "projectName",
404
+ "git",
405
+ "install",
406
+ "yolo"
407
+ ]);
408
+ const changes = {};
409
+ for (const [key, value] of Object.entries(adjusted)) if (!ignoredCompatibilityKeys.has(key)) changes[key] = value;
410
+ changes.frontend = frontend.length > 0 ? frontend : baseConfig.frontend;
411
+ changes.addons = [
412
+ ...adjusted.codeQuality,
413
+ ...adjusted.documentation,
414
+ ...adjusted.appPlatforms
415
+ ];
416
+ changes.ai = adjusted.aiSdk;
417
+ changes.backend = getProjectBackendFromCompatibility(adjusted.backend);
418
+ return changes;
419
+ }
420
+ function applyKnownDependencyExpansions(config, requestedChanges) {
421
+ const next = { ...config };
422
+ const adjustments = [];
423
+ const requestedKeys = new Set(Object.keys(requestedChanges));
424
+ const hasWebFrontend = asStringArray(next.frontend).some((item) => !item.startsWith("native-") && item !== "none");
425
+ if (next.ecosystem === "typescript" && !hasWebFrontend && !requestedKeys.has("frontend")) {
426
+ const defaultWebFrontend = getDefaultWebFrontendForRequestedUpdate(requestedChanges);
427
+ if (defaultWebFrontend) {
428
+ next.frontend = withWebFrontendVariant(next.frontend, defaultWebFrontend);
429
+ adjustments.push(`frontend: Web frontend set to '${defaultWebFrontend}' (requested feature requires a web app)`);
430
+ }
431
+ }
432
+ if (next.ecosystem === "typescript" && !requestedKeys.has("frontend")) {
433
+ const hasNativeFrontend = asStringArray(next.frontend).some((item) => item.startsWith("native-"));
434
+ const defaultNativeFrontend = getDefaultNativeFrontendForRequestedUpdate(requestedChanges);
435
+ if (defaultNativeFrontend && !hasNativeFrontend) {
436
+ next.frontend = withNativeFrontendVariant(next.frontend, defaultNativeFrontend);
437
+ adjustments.push(`mobile: Native frontend set to '${defaultNativeFrontend}' (requested feature requires a native app)`);
438
+ }
439
+ }
440
+ if (next.ecosystem === "typescript" && next.backend === "none" && needsStandaloneBackendForRequestedUpdate(requestedChanges)) {
441
+ if (!requestedKeys.has("backend")) {
442
+ next.backend = "hono";
443
+ adjustments.push("backend: Backend set to 'hono' (requested feature requires a server)");
444
+ }
445
+ if (next.backend !== "none" && next.runtime === "none" && !requestedKeys.has("runtime")) {
446
+ next.runtime = "bun";
447
+ adjustments.push("backend: Runtime set to 'bun' (Hono server default)");
448
+ }
449
+ }
450
+ if (hasRequestedNonNoneValue(requestedChanges, "orm") && next.database === "none" && !requestedKeys.has("database")) {
451
+ next.database = "sqlite";
452
+ adjustments.push("orm: Database set to 'sqlite' (ORM requires a database)");
453
+ }
454
+ if (isBetterAuth(requestedChanges.auth) || requestedChanges.payments === "polar") {
455
+ if (requestedChanges.payments === "polar" && !isBetterAuth(next.auth) && !requestedKeys.has("auth")) {
456
+ next.auth = "better-auth";
457
+ adjustments.push("payments: Auth set to 'better-auth' (Polar requires Better Auth)");
458
+ }
459
+ if (next.database === "none" && !requestedKeys.has("database")) {
460
+ next.database = "sqlite";
461
+ const reason = requestedChanges.payments === "polar" ? "payments: Database set to 'sqlite' (Better Auth requires a SQL database)" : "auth: Database set to 'sqlite' (Better Auth requires a SQL database)";
462
+ adjustments.push(reason);
463
+ }
464
+ if (next.orm === "none" && !requestedKeys.has("orm")) {
465
+ next.orm = "drizzle";
466
+ const reason = requestedChanges.payments === "polar" ? "payments: ORM set to 'drizzle' (Better Auth requires an adapter)" : "auth: ORM set to 'drizzle' (Better Auth requires an adapter)";
467
+ adjustments.push(reason);
468
+ }
469
+ }
470
+ if (requestedChanges.uiLibrary && requestedChanges.uiLibrary !== "none") {
471
+ const requiredCssFramework = getRequiredCssFrameworkForUiLibrary(requestedChanges.uiLibrary);
472
+ if (requiredCssFramework && next.cssFramework !== requiredCssFramework && !requestedKeys.has("cssFramework")) {
473
+ next.cssFramework = requiredCssFramework;
474
+ adjustments.push(`uiLibrary: CSS framework set to '${requiredCssFramework}' (${requestedChanges.uiLibrary} requires Tailwind CSS)`);
475
+ }
476
+ }
477
+ if (requestedChanges.mobileUI === "uniwind" && !requestedKeys.has("frontend")) {
478
+ if (!asStringArray(next.frontend).includes("native-uniwind")) {
479
+ next.frontend = withNativeFrontendVariant(next.frontend, "native-uniwind");
480
+ adjustments.push("mobileUI: Native frontend set to 'native-uniwind' (Uniwind mobile UI requires Expo + Uniwind)");
481
+ }
482
+ }
483
+ if (requestedChanges.mobileUI === "unistyles" && !requestedKeys.has("frontend")) {
484
+ if (!asStringArray(next.frontend).includes("native-unistyles")) {
485
+ next.frontend = withNativeFrontendVariant(next.frontend, "native-unistyles");
486
+ adjustments.push("mobileUI: Native frontend set to 'native-unistyles' (Unistyles mobile UI requires Expo + Unistyles)");
487
+ }
488
+ }
489
+ if (requestedChanges.dbSetup && requestedChanges.dbSetup !== "none") {
490
+ const defaultDatabase = getDefaultDatabaseForDbSetup(requestedChanges.dbSetup);
491
+ if (defaultDatabase && next.database !== defaultDatabase && !requestedKeys.has("database")) {
492
+ next.database = defaultDatabase;
493
+ adjustments.push(`dbSetup: Database set to '${defaultDatabase}' (${requestedChanges.dbSetup} requires ${defaultDatabase})`);
494
+ }
495
+ const defaultOrm = getDefaultOrmForDatabase(next.database);
496
+ if (defaultOrm && next.orm === "none" && !requestedKeys.has("orm")) {
497
+ next.orm = defaultOrm;
498
+ adjustments.push(`dbSetup: ORM set to '${defaultOrm}' (${requestedChanges.dbSetup} requires a database adapter)`);
499
+ }
500
+ }
501
+ if (requestedChanges.dbSetup === "d1") {
502
+ if (next.runtime !== "workers" && !requestedKeys.has("runtime")) {
503
+ next.runtime = "workers";
504
+ adjustments.push("dbSetup: Runtime set to 'workers' (D1 requires Workers)");
505
+ }
506
+ if (next.backend !== "hono" && !requestedKeys.has("backend")) {
507
+ next.backend = "hono";
508
+ adjustments.push("dbSetup: Backend set to 'hono' (Workers requires Hono)");
509
+ }
510
+ }
511
+ if (requestedChanges.serverDeploy === "cloudflare") {
512
+ if (next.backend === "hono" && next.runtime !== "workers" && !requestedKeys.has("runtime")) {
513
+ next.runtime = "workers";
514
+ adjustments.push("serverDeploy: Runtime set to 'workers' (Cloudflare requires Workers)");
515
+ }
516
+ }
517
+ if (requestedChanges.serverDeploy === "netlify") {
518
+ if (next.backend === "hono" && next.runtime !== "node" && !requestedKeys.has("runtime")) {
519
+ next.runtime = "node";
520
+ adjustments.push("serverDeploy: Runtime set to 'node' (Netlify Functions requires Node)");
521
+ }
522
+ }
523
+ if (asStringArray(next.examples).includes("chat-sdk")) {
524
+ if (getCompatibilityBackend(next, asStringArray(next.frontend).filter((item) => !item.startsWith("native-") && item !== "none")) === "hono" && next.runtime !== "node" && !requestedKeys.has("runtime")) {
525
+ next.runtime = "node";
526
+ adjustments.push("examples: Runtime set to 'node' (Chat SDK Hono profile requires Node)");
527
+ }
528
+ if ((0, types_exports.requiresChatSdkVercelAI)(buildCompatibilityInputFromConfig(next)) && next.ai !== "vercel-ai" && !requestedKeys.has("ai")) {
529
+ next.ai = "vercel-ai";
530
+ adjustments.push("examples: AI SDK set to 'vercel-ai' (Chat SDK profile requires Vercel AI)");
531
+ }
532
+ }
533
+ return {
534
+ config: next,
535
+ adjustments
536
+ };
537
+ }
538
+ async function generateTree(config) {
539
+ const result = await generateVirtualProject({
540
+ config,
541
+ templates: EMBEDDED_TEMPLATES
542
+ });
543
+ if (!result.success || !result.tree) throw new Error(result.error ?? "Failed to generate virtual project");
544
+ return result.tree;
545
+ }
546
+ function treeToFileMap(tree) {
547
+ const files = /* @__PURE__ */ new Map();
548
+ function walk(nodes) {
549
+ for (const node of nodes) if (node.type === "file") files.set(node.path, node);
550
+ else walk(node.children);
551
+ }
552
+ walk(tree.root.children);
553
+ return files;
554
+ }
555
+ function parseJson(content) {
556
+ if (!content) return null;
557
+ try {
558
+ const parsed = JSON.parse(content);
559
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
560
+ } catch {
561
+ return null;
562
+ }
563
+ }
564
+ function stringifyJson(data) {
565
+ return `${JSON.stringify(data, null, 2)}\n`;
566
+ }
567
+ function isPlainObject(value) {
568
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
569
+ }
570
+ function diffJsonSection(current, previous, proposed, section) {
571
+ const previousSection = isPlainObject(previous[section]) ? previous[section] : {};
572
+ const proposedSection = isPlainObject(proposed[section]) ? proposed[section] : {};
573
+ const currentSection = isPlainObject(current[section]) ? current[section] : {};
574
+ const values = {};
575
+ const blockers = [];
576
+ for (const [name, proposedValue] of Object.entries(proposedSection)) {
577
+ if (previousSection[name] === proposedValue) continue;
578
+ const currentValue = currentSection[name];
579
+ if (currentValue !== void 0 && currentValue !== previousSection[name]) {
580
+ blockers.push(`${section}.${name}`);
581
+ continue;
582
+ }
583
+ values[name] = String(proposedValue);
584
+ }
585
+ return {
586
+ values,
587
+ blockers
588
+ };
589
+ }
590
+ function mergePackageJson(existingContent, previousContent, proposedContent) {
591
+ const existing = parseJson(existingContent);
592
+ const previous = parseJson(previousContent);
593
+ const proposed = parseJson(proposedContent);
594
+ if (!existing || !previous || !proposed) return {
595
+ summary: [],
596
+ blockers: ["package.json is not valid JSON or has no generated baseline"],
597
+ dependencyChanges: {},
598
+ scriptChanges: []
599
+ };
600
+ const next = structuredClone(existing);
601
+ const summary = [];
602
+ const blockers = [];
603
+ const dependencyChanges = {};
604
+ const scriptChanges = [];
605
+ for (const section of PACKAGE_JSON_SECTIONS) {
606
+ const diff = diffJsonSection(existing, previous, proposed, section);
607
+ blockers.push(...diff.blockers);
608
+ if (Object.keys(diff.values).length === 0) continue;
609
+ const target = isPlainObject(next[section]) ? { ...next[section] } : {};
610
+ for (const [name, value] of Object.entries(diff.values)) target[name] = value;
611
+ next[section] = Object.fromEntries(Object.entries(target).sort(([a], [b]) => a.localeCompare(b)));
612
+ summary.push(`${section}: ${Object.keys(diff.values).join(", ")}`);
613
+ if (section === "scripts") scriptChanges.push(...Object.keys(diff.values));
614
+ else dependencyChanges[section] = diff.values;
615
+ }
616
+ return {
617
+ content: blockers.length === 0 && summary.length > 0 ? stringifyJson(next) : void 0,
618
+ summary,
619
+ blockers,
620
+ dependencyChanges,
621
+ scriptChanges
622
+ };
623
+ }
624
+ function parseEnvKeys(content) {
625
+ const keys = /* @__PURE__ */ new Set();
626
+ for (const rawLine of content.split("\n")) {
627
+ const line = rawLine.trim();
628
+ if (!line || line.startsWith("#")) continue;
629
+ const eq = line.indexOf("=");
630
+ if (eq === -1) continue;
631
+ const key = line.slice(0, eq).trim();
632
+ if (key) keys.add(key);
633
+ }
634
+ return keys;
635
+ }
636
+ function mergeEnvExample(existingContent, previousContent, proposedContent) {
637
+ const existingKeys = parseEnvKeys(existingContent);
638
+ const previousKeys = previousContent ? parseEnvKeys(previousContent) : /* @__PURE__ */ new Set();
639
+ const nextLines = [];
640
+ const keys = [];
641
+ for (const line of proposedContent.split("\n")) {
642
+ const trimmed = line.trim();
643
+ const eq = trimmed.indexOf("=");
644
+ if (!trimmed || trimmed.startsWith("#") || eq === -1) continue;
645
+ const key = trimmed.slice(0, eq).trim();
646
+ if (!key || previousKeys.has(key) || existingKeys.has(key)) continue;
647
+ nextLines.push(line);
648
+ keys.push(key);
649
+ }
650
+ if (nextLines.length === 0) return { keys };
651
+ return {
652
+ content: `${existingContent}${existingContent.endsWith("\n") ? "" : "\n"}${nextLines.join("\n")}\n`,
653
+ keys
654
+ };
655
+ }
656
+ function collectEnvReferences(content) {
657
+ const keys = /* @__PURE__ */ new Set();
658
+ if (!content) return keys;
659
+ for (const pattern of [
660
+ /\bprocess\.env\.([A-Z][A-Z0-9_]*)/g,
661
+ /\bDeno\.env\.get\(\s*["']([A-Z][A-Z0-9_]*)["']/g,
662
+ /\bSystem\.getenv\(\s*["']([A-Z][A-Z0-9_]*)["']/g,
663
+ /\bstd::env::var\(\s*["']([A-Z][A-Z0-9_]*)["']/g,
664
+ /\bos\.Getenv\(\s*["']([A-Z][A-Z0-9_]*)["']/g,
665
+ /\bos\.environ\[\s*["']([A-Z][A-Z0-9_]*)["']\s*\]/g,
666
+ /\bos\.getenv\(\s*["']([A-Z][A-Z0-9_]*)["']/g,
667
+ /^([A-Z][A-Z0-9_]*)=/gm
668
+ ]) for (const match of content.matchAll(pattern)) if (match[1]) keys.add(match[1]);
669
+ return keys;
670
+ }
671
+ function recordEnvReferenceChanges(envChanges, filePath, previousContent, proposedContent) {
672
+ const previous = collectEnvReferences(previousContent);
673
+ const added = [...collectEnvReferences(proposedContent)].filter((key) => !previous.has(key)).sort();
674
+ if (added.length > 0) envChanges[filePath] = [...new Set([...envChanges[filePath] ?? [], ...added])].sort();
675
+ }
676
+ function getInstallCommand$1(config) {
677
+ switch (config.ecosystem) {
678
+ case "rust": return "cargo build";
679
+ case "python": return "uv sync";
680
+ case "go": return "go mod tidy";
681
+ case "java": return config.javaBuildTool === "gradle" ? "./gradlew test" : "./mvnw test";
682
+ case "elixir": return "mix deps.get && mix compile";
683
+ default: return config.packageManager === "npm" ? "npm install" : `${config.packageManager} install`;
684
+ }
685
+ }
686
+ function getGraphPreview(config) {
687
+ const stackParts = config.stackParts?.length ? config.stackParts : (0, types_exports.legacyProjectConfigToStackParts)(config);
688
+ const graphSummary = stackParts.length > 0 ? getGraphSummary({ stackParts }) : void 0;
689
+ const effectiveStack = stackParts.length > 0 ? getEffectiveStack({ stackParts }) : void 0;
690
+ const stackPartSpecs = stackParts.filter((part) => part.source !== "provided").map((part) => (0, types_exports.formatStackPartSpec)(part, stackParts));
691
+ return {
692
+ ...graphSummary ? {
693
+ graphSummary,
694
+ effectiveStack
695
+ } : {},
696
+ stackPartSpecs
697
+ };
698
+ }
699
+ async function planStackUpdate(projectDirInput, input) {
700
+ const projectDir = path.resolve(projectDirInput);
701
+ const currentBtsConfig = await readBtsConfig(projectDir);
702
+ if (!currentBtsConfig) return {
703
+ success: false,
704
+ projectDir,
705
+ error: `No bts.jsonc found in ${projectDir}. Is this a Better-Fullstack project?`
706
+ };
707
+ const projectName = await inferProjectName(projectDir);
708
+ const currentConfig = configFromBtsConfig(currentBtsConfig, projectDir, projectName);
709
+ const { changes: requestedChanges, stackPartSpecs, unsupportedKeys } = buildRequestedChanges(input);
710
+ if (unsupportedKeys.length > 0) return {
711
+ success: false,
712
+ projectDir,
713
+ error: `Unsupported stack update field(s): ${unsupportedKeys.sort().join(", ")}`
714
+ };
715
+ let proposedConfig = mergeProjectConfig(currentConfig, requestedChanges);
716
+ const dependencyExpansion = applyKnownDependencyExpansions(proposedConfig, requestedChanges);
717
+ proposedConfig = dependencyExpansion.config;
718
+ const compatibilityResult = proposedConfig.ecosystem === "typescript" || proposedConfig.ecosystem === "react-native" ? (0, types_exports.analyzeStackCompatibility)(buildCompatibilityInputFromConfig(proposedConfig)) : {
719
+ adjustedStack: null,
720
+ changes: []
721
+ };
722
+ const compatibilityAdjustments = [...dependencyExpansion.adjustments, ...compatibilityResult.changes.map((change) => `${change.category}: ${change.message}`)];
723
+ if (compatibilityResult.adjustedStack) proposedConfig = mergeProjectConfig(proposedConfig, compatibilityChangesToProjectConfig(compatibilityResult.adjustedStack, proposedConfig));
724
+ proposedConfig.stackParts = (0, types_exports.legacyProjectConfigToStackParts)(proposedConfig);
725
+ Object.assign(proposedConfig, mergeStackPartSpecs(proposedConfig, stackPartSpecs));
726
+ try {
727
+ validateConfigForProgrammaticUse(proposedConfig);
728
+ } catch (error) {
729
+ return {
730
+ success: false,
731
+ projectDir,
732
+ error: `Invalid stack update: ${error instanceof Error ? error.message : String(error)}`
733
+ };
734
+ }
735
+ const persistedProposedConfig = buildBtsConfigForPersistence(proposedConfig, {
736
+ version: currentBtsConfig.version,
737
+ createdAt: currentBtsConfig.createdAt
738
+ });
739
+ const normalizedProposedConfig = configFromBtsConfig(persistedProposedConfig, projectDir, projectName);
740
+ let currentTree;
741
+ let proposedTree;
742
+ try {
743
+ [currentTree, proposedTree] = await Promise.all([generateTree(currentConfig), generateTree(normalizedProposedConfig)]);
744
+ } catch (error) {
745
+ return {
746
+ success: false,
747
+ projectDir,
748
+ error: `Failed to generate stack update plan: ${error instanceof Error ? error.message : String(error)}`
749
+ };
750
+ }
751
+ const currentGeneratedFiles = treeToFileMap(currentTree);
752
+ const proposedGeneratedFiles = treeToFileMap(proposedTree);
753
+ const operations = [];
754
+ const filesToAdd = [];
755
+ const filesToPatch = [];
756
+ const filesUnchanged = [];
757
+ const manualReviewBlockers = [];
758
+ const dependencyChanges = {};
759
+ const scriptChanges = {};
760
+ const envChanges = {};
761
+ const proposedFileEntries = await Promise.all([...proposedGeneratedFiles].map(async ([filePath, proposedFile]) => {
762
+ const existingPath = path.join(projectDir, filePath);
763
+ const exists = await fs.pathExists(existingPath);
764
+ return {
765
+ filePath,
766
+ proposedFile,
767
+ exists,
768
+ existingBuffer: exists ? await fs.readFile(existingPath).catch(() => void 0) : void 0
769
+ };
770
+ }));
771
+ for (const { filePath, proposedFile, exists, existingBuffer } of proposedFileEntries) {
772
+ const previousFile = currentGeneratedFiles.get(filePath);
773
+ const proposedContent = proposedFile.content;
774
+ const previousContent = previousFile?.content;
775
+ const isBinaryFile = isGeneratedBinaryFile(proposedFile) || isGeneratedBinaryFile(previousFile);
776
+ const existingContent = exists && existingBuffer && !isBinaryFile ? existingBuffer.toString("utf-8") : void 0;
777
+ if (!exists) {
778
+ filesToAdd.push(filePath);
779
+ operations.push({
780
+ kind: "add",
781
+ path: filePath,
782
+ writeMode: "generated"
783
+ });
784
+ if (isEnvFilePath(filePath)) envChanges[filePath] = [...parseEnvKeys(proposedContent)].sort();
785
+ recordEnvReferenceChanges(envChanges, filePath, void 0, proposedContent);
786
+ continue;
787
+ }
788
+ if (isBinaryFile) {
789
+ const [previousBinaryContent, proposedBinaryContent] = await Promise.all([readGeneratedFileBytes(currentTree, filePath, previousFile), readGeneratedFileBytes(proposedTree, filePath, proposedFile)]);
790
+ if (buffersEqual(existingBuffer, proposedBinaryContent)) {
791
+ filesUnchanged.push(filePath);
792
+ continue;
793
+ }
794
+ if (buffersEqual(existingBuffer, previousBinaryContent)) {
795
+ filesToPatch.push(filePath);
796
+ operations.push({
797
+ kind: "replace",
798
+ path: filePath,
799
+ writeMode: "generated"
800
+ });
801
+ continue;
802
+ }
803
+ manualReviewBlockers.push(`${filePath}: existing binary file differs from the generated baseline`);
804
+ continue;
805
+ }
806
+ if (existingContent === void 0 || existingContent === proposedContent) {
807
+ filesUnchanged.push(filePath);
808
+ continue;
809
+ }
810
+ if (filePath.endsWith("package.json")) {
811
+ const merged = mergePackageJson(existingContent, previousContent, proposedContent);
812
+ for (const blocker of merged.blockers) manualReviewBlockers.push(`${filePath}: ${blocker}`);
813
+ if (merged.content) {
814
+ filesToPatch.push(filePath);
815
+ operations.push({
816
+ kind: "merge",
817
+ path: filePath,
818
+ writeMode: "content",
819
+ content: merged.content,
820
+ summary: merged.summary
821
+ });
822
+ for (const [section, values] of Object.entries(merged.dependencyChanges)) dependencyChanges[`${filePath}:${section}`] = values;
823
+ if (merged.scriptChanges.length > 0) scriptChanges[filePath] = merged.scriptChanges;
824
+ }
825
+ continue;
826
+ }
827
+ if (isEnvFilePath(filePath)) {
828
+ const merged = mergeEnvExample(existingContent, previousContent, proposedContent);
829
+ if (merged.content) {
830
+ filesToPatch.push(filePath);
831
+ operations.push({
832
+ kind: "merge",
833
+ path: filePath,
834
+ writeMode: "content",
835
+ content: merged.content,
836
+ summary: [`env: ${merged.keys.join(", ")}`]
837
+ });
838
+ envChanges[filePath] = merged.keys;
839
+ }
840
+ continue;
841
+ }
842
+ if (previousContent !== void 0 && existingContent === previousContent) {
843
+ filesToPatch.push(filePath);
844
+ operations.push({
845
+ kind: "replace",
846
+ path: filePath,
847
+ writeMode: "generated"
848
+ });
849
+ recordEnvReferenceChanges(envChanges, filePath, previousContent, proposedContent);
850
+ continue;
851
+ }
852
+ if (isSkippableGeneratedDoc(filePath)) continue;
853
+ manualReviewBlockers.push(`${filePath}: existing file differs from the generated baseline`);
854
+ }
855
+ const graphPreview = getGraphPreview(persistedProposedConfig);
856
+ return {
857
+ success: true,
858
+ projectDir,
859
+ requestedChanges,
860
+ proposedConfig: persistedProposedConfig,
861
+ filesToAdd: [...new Set(filesToAdd)].sort(),
862
+ filesToPatch: [...new Set(filesToPatch)].sort(),
863
+ filesUnchanged: [...new Set(filesUnchanged)].sort(),
864
+ dependencyChanges,
865
+ scriptChanges,
866
+ envChanges,
867
+ manualReviewBlockers,
868
+ operations,
869
+ installCommand: getInstallCommand$1(normalizedProposedConfig),
870
+ compatibilityAdjustments,
871
+ ...graphPreview
872
+ };
873
+ }
874
+ async function applyStackUpdate(projectDirInput, input) {
875
+ const plan = await planStackUpdate(projectDirInput, input);
876
+ if (!plan.success) return plan;
877
+ if (plan.manualReviewBlockers.length > 0) return {
878
+ success: false,
879
+ projectDir: plan.projectDir,
880
+ error: `Manual review required before applying stack update: ${plan.manualReviewBlockers.join("; ")}`
881
+ };
882
+ const projectName = await inferProjectName(plan.projectDir);
883
+ const proposedConfig = configFromBtsConfig(plan.proposedConfig, plan.projectDir, projectName);
884
+ const proposedTree = await generateTree(proposedConfig);
885
+ const generatedPaths = new Set(plan.operations.filter((operation) => operation.writeMode === "generated").map((operation) => operation.path));
886
+ if (generatedPaths.size > 0) await writeSelectedFiles(proposedTree, plan.projectDir, (filePath) => generatedPaths.has(filePath));
887
+ await Promise.all(plan.operations.filter((operation) => operation.writeMode === "content").map(async (operation) => {
888
+ const targetPath = path.join(plan.projectDir, operation.path);
889
+ await fs.ensureDir(path.dirname(targetPath));
890
+ await fs.writeFile(targetPath, operation.content, "utf-8");
891
+ }));
892
+ await writeBtsConfig(proposedConfig, {
893
+ version: plan.proposedConfig.version,
894
+ createdAt: plan.proposedConfig.createdAt
895
+ });
896
+ return plan;
897
+ }
898
+
899
+ //#endregion
8
900
  //#region src/mcp.ts
9
901
  const INSTRUCTIONS = `Better-Fullstack scaffolds fullstack projects across TypeScript, React Native, Rust, Go, Python, Java, .NET, and Elixir ecosystems with ${Object.values(OPTION_CATEGORY_METADATA).reduce((sum, metadata) => sum + metadata.options.length, 0)} configurable options.
10
902
 
@@ -16,8 +908,9 @@ RECOMMENDED WORKFLOW:
16
908
  5. Call bfs_create_project to scaffold the project on disk.
17
909
 
18
910
  For existing projects:
19
- 1. Call bfs_plan_addition to validate proposed changes.
20
- 2. Call bfs_add_feature to apply changes.
911
+ 1. Call bfs_plan_stack_update to preview scaffold-time capability changes.
912
+ 2. Call bfs_apply_stack_update to safely apply supported stack changes.
913
+ 3. Use bfs_plan_addition / bfs_add_feature only for legacy addon/deploy-only flows.
21
914
 
22
915
  CRITICAL RULES:
23
916
  - Dependency installation is ALWAYS skipped in MCP mode (timeout risk). After scaffolding, tell the user to run install manually.
@@ -34,7 +927,8 @@ function getGuidance() {
34
927
  "Call bfs_check_compatibility to validate your planned stack before creation.",
35
928
  "Call bfs_plan_project to preview the generated project (dry-run, no files written).",
36
929
  "Call bfs_create_project to scaffold the project on disk.",
37
- "For existing projects: call bfs_plan_addition, then bfs_add_feature."
930
+ "For existing projects: call bfs_plan_stack_update, then bfs_apply_stack_update.",
931
+ "Use bfs_plan_addition / bfs_add_feature only for legacy addon/deploy-only flows."
38
932
  ],
39
933
  ecosystems: {
40
934
  typescript: "Full-featured web: frontend + backend + database + ORM + auth + payments + 20+ feature categories.",
@@ -56,7 +950,8 @@ function getGuidance() {
56
950
  addons: "ARRAY of strings. Monorepo tools, code quality, desktop (tauri), browser extensions (wxt), etc.",
57
951
  email: "String. TypeScript supports multiple providers; Rust, Python, Go, and Java currently support resend or none.",
58
952
  observability: "String. TypeScript supports multiple providers; Rust, Python, Go, and Java currently support sentry or none.",
59
- search: "String. TypeScript supports multiple providers; Rust, Python, Go, and Java currently support meilisearch or none."
953
+ search: "String. TypeScript supports multiple providers; Rust, Python, Go, and Java currently support meilisearch or none.",
954
+ vectorDb: "String. TypeScript-only vector database for AI embeddings: pgvector, qdrant, chroma, pinecone, or none. Each provider is a standalone service (pgvector connects to a dedicated Postgres+pgvector instance via PGVECTOR_DATABASE_URL). Requires a standalone backend (not convex/none)."
60
955
  },
61
956
  ambiguityRules: [
62
957
  "If the user request leaves major stack choices unspecified, ASK the user before proceeding. Do not guess.",
@@ -118,14 +1013,7 @@ const MCP_SCHEMA_EXCLUDED_CATEGORIES = new Set([
118
1013
  "aiDocs",
119
1014
  "git",
120
1015
  "install",
121
- "versionChannel",
122
- "shadcnBase",
123
- "shadcnStyle",
124
- "shadcnIconLibrary",
125
- "shadcnColorTheme",
126
- "shadcnBaseColor",
127
- "shadcnFont",
128
- "shadcnRadius"
1016
+ "versionChannel"
129
1017
  ]);
130
1018
  const MCP_SCHEMA_OPTION_OVERRIDES = {
131
1019
  ecosystem: EcosystemSchema.options,
@@ -245,6 +1133,7 @@ const MCP_COMPATIBILITY_DEFAULTS = {
245
1133
  shadcnRadius: "default",
246
1134
  cms: "none",
247
1135
  search: "none",
1136
+ vectorDb: "none",
248
1137
  fileStorage: "none",
249
1138
  mobileUI: "none",
250
1139
  mobileStorage: "none",
@@ -255,7 +1144,7 @@ const MCP_COMPATIBILITY_DEFAULTS = {
255
1144
  versionChannel: "stable",
256
1145
  examples: [],
257
1146
  aiSdk: "none",
258
- aiDocs: ["claude-md"],
1147
+ aiDocs: ["claude-md", "agents-md"],
259
1148
  git: "true",
260
1149
  install: "false",
261
1150
  api: "none",
@@ -364,7 +1253,7 @@ function buildProjectConfig(input, overrides) {
364
1253
  const hasNativeFrontend = frontend.some((item) => item.startsWith("native-"));
365
1254
  const hasMobileProject = ecosystem === "react-native" || hasNativeFrontend;
366
1255
  const defaults = getMcpProjectConfigDefaults(input);
367
- return {
1256
+ const config = {
368
1257
  projectName,
369
1258
  projectDir: overrides?.projectDir ?? "/virtual",
370
1259
  relativePath: overrides ? `./${projectName}` : "./virtual",
@@ -376,19 +1265,22 @@ function buildProjectConfig(input, overrides) {
376
1265
  cssFramework: input.cssFramework ?? (ecosystem === "react-native" ? "none" : defaults.cssFramework),
377
1266
  mobileNavigation: input.mobileNavigation ?? (hasMobileProject ? "expo-router" : "none"),
378
1267
  mobileDeepLinking: input.mobileDeepLinking ?? (hasMobileProject ? "expo-linking" : "none"),
379
- astroIntegration: "none",
380
- versionChannel: "stable",
381
- shadcnBase: "radix",
382
- shadcnStyle: "nova",
383
- shadcnIconLibrary: "lucide",
384
- shadcnColorTheme: "neutral",
385
- shadcnBaseColor: "neutral",
386
- shadcnFont: "inter",
387
- shadcnRadius: "default",
388
- aiDocs: ["claude-md"],
1268
+ shadcnBase: input.shadcnBase ?? "radix",
1269
+ shadcnStyle: input.shadcnStyle ?? "nova",
1270
+ shadcnIconLibrary: input.shadcnIconLibrary ?? "lucide",
1271
+ shadcnColorTheme: input.shadcnColorTheme ?? "neutral",
1272
+ shadcnBaseColor: input.shadcnBaseColor ?? "neutral",
1273
+ shadcnFont: input.shadcnFont ?? "inter",
1274
+ shadcnRadius: input.shadcnRadius ?? "default",
1275
+ aiDocs: input.aiDocs ?? ["claude-md", "agents-md"],
389
1276
  git: !!overrides,
390
1277
  install: false
391
1278
  };
1279
+ if (Array.isArray(input.part) && input.part.length > 0) {
1280
+ const stackParts = parseStackPartSpecs(input.part.filter((part) => typeof part === "string"), "selected");
1281
+ Object.assign(config, stackPartsToLegacyProjectConfigPartial(stackParts), { stackParts });
1282
+ }
1283
+ return config;
392
1284
  }
393
1285
  function sanitizePath(input) {
394
1286
  for (const ch of input) if (ch.charCodeAt(0) < 32) throw new Error("Path contains control characters");
@@ -561,10 +1453,450 @@ const GETTING_STARTED_MD = `# Getting Started with Better-Fullstack MCP
561
1453
  2. Tell the user to run: cd my-elixir-app && mix deps.get && mix phx.server
562
1454
 
563
1455
  ## Adding Features to Existing Projects
564
- 1. Call bfs_add_feature with projectDir pointing to the project root.
565
- 2. Provide addons array with features to add (e.g., ["biome", "turborepo"]).
566
- 3. Service categories such as email and observability are scaffold-time options. To add those to an existing app, inspect the generated templates from bfs_plan_project and apply the equivalent dependency, env var, and initialization changes manually.
1456
+ 1. Call bfs_plan_stack_update with projectDir and any stack fields to add or change.
1457
+ 2. Review filesToAdd, filesToPatch, dependencyChanges, envChanges, and manualReviewBlockers.
1458
+ 3. If there are no blockers, call bfs_apply_stack_update with the same arguments.
1459
+ 4. Use bfs_add_feature only for older addon/deploy-only workflows.
567
1460
  `;
1461
+ const guidanceOutputSchema = {
1462
+ workflow: z.array(z.string()),
1463
+ ecosystems: z.record(z.string(), z.string()),
1464
+ fieldRules: z.record(z.string(), z.string()),
1465
+ ambiguityRules: z.array(z.string()),
1466
+ criticalConstraints: z.array(z.string())
1467
+ };
1468
+ const schemaOutputSchema = {
1469
+ category: z.string().optional(),
1470
+ options: z.array(z.string()).optional(),
1471
+ categories: z.record(z.string(), z.array(z.string())).optional(),
1472
+ error: z.string().optional()
1473
+ };
1474
+ const compatibilityIssueOutputSchema = z.object({
1475
+ code: z.string(),
1476
+ message: z.string(),
1477
+ category: z.string().optional(),
1478
+ optionId: z.string().optional(),
1479
+ provided: z.record(z.string(), z.union([z.string(), z.array(z.string())])).optional(),
1480
+ suggestions: z.array(z.string()).optional()
1481
+ });
1482
+ const compatibilityOutputSchema = {
1483
+ adjustedStack: z.record(z.string(), z.unknown()).nullable(),
1484
+ changes: z.array(z.object({
1485
+ category: z.string(),
1486
+ message: z.string()
1487
+ })),
1488
+ issues: z.array(compatibilityIssueOutputSchema),
1489
+ hasIssues: z.boolean()
1490
+ };
1491
+ const graphPreviewOutputShape = {
1492
+ graphSummary: z.string().optional(),
1493
+ effectiveStack: z.record(z.string(), z.string()).optional(),
1494
+ stackPartSpecs: z.array(z.string()).optional()
1495
+ };
1496
+ const planProjectOutputSchema = {
1497
+ success: z.boolean(),
1498
+ fileCount: z.number().optional(),
1499
+ directoryCount: z.number().optional(),
1500
+ files: z.array(z.string()).optional(),
1501
+ ...graphPreviewOutputShape
1502
+ };
1503
+ const createProjectOutputSchema = {
1504
+ success: z.boolean(),
1505
+ projectDirectory: z.string().optional(),
1506
+ fileCount: z.number().optional(),
1507
+ addonWarnings: z.array(z.string()).optional(),
1508
+ message: z.string().optional(),
1509
+ ...graphPreviewOutputShape
1510
+ };
1511
+ const planAdditionOutputSchema = {
1512
+ success: z.boolean(),
1513
+ existingConfig: z.object({
1514
+ ecosystem: z.string(),
1515
+ frontend: z.array(z.string()).optional(),
1516
+ backend: z.string().optional(),
1517
+ addons: z.array(z.string()).optional(),
1518
+ graphSummary: z.string().optional(),
1519
+ effectiveStack: z.record(z.string(), z.string()).optional(),
1520
+ stackPartSpecs: z.array(z.string())
1521
+ }).optional(),
1522
+ proposedAdditions: z.object({
1523
+ newAddons: z.array(z.string()),
1524
+ webDeploy: z.string().nullable(),
1525
+ serverDeploy: z.string().nullable(),
1526
+ graphSummary: z.string().optional(),
1527
+ effectiveStack: z.record(z.string(), z.string()).optional(),
1528
+ stackPartSpecs: z.array(z.string())
1529
+ }).optional(),
1530
+ alreadyPresent: z.array(z.string()).optional(),
1531
+ compatibilityWarnings: z.array(z.string()).optional()
1532
+ };
1533
+ const addFeatureOutputSchema = {
1534
+ success: z.boolean(),
1535
+ addedAddons: z.array(z.string()).optional(),
1536
+ projectDir: z.string().optional(),
1537
+ message: z.string().optional(),
1538
+ ...graphPreviewOutputShape
1539
+ };
1540
+ const stackUpdateOutputSchema = {
1541
+ success: z.boolean(),
1542
+ projectDir: z.string().optional(),
1543
+ error: z.string().optional(),
1544
+ requestedChanges: z.record(z.string(), z.unknown()).optional(),
1545
+ proposedConfig: z.record(z.string(), z.unknown()).optional(),
1546
+ filesToAdd: z.array(z.string()).optional(),
1547
+ filesToPatch: z.array(z.string()).optional(),
1548
+ dependencyChanges: z.record(z.string(), z.record(z.string(), z.string())).optional(),
1549
+ scriptChanges: z.record(z.string(), z.array(z.string())).optional(),
1550
+ envChanges: z.record(z.string(), z.array(z.string())).optional(),
1551
+ manualReviewBlockers: z.array(z.string()).optional(),
1552
+ compatibilityAdjustments: z.array(z.string()).optional(),
1553
+ compatibilityWarnings: z.array(z.string()).optional(),
1554
+ installCommand: z.string().optional(),
1555
+ message: z.string().optional(),
1556
+ ...graphPreviewOutputShape
1557
+ };
1558
+ function buildPresetStackSummary(config) {
1559
+ const parts = [];
1560
+ const frontend = (config.frontend ?? []).filter((item) => item !== "none");
1561
+ if (frontend.length > 0) parts.push(`frontend: ${frontend.join("+")}`);
1562
+ if (config.backend && config.backend !== "none") parts.push(`backend: ${config.backend}`);
1563
+ if (config.runtime && config.runtime !== "none") parts.push(`runtime: ${config.runtime}`);
1564
+ if (config.database && config.database !== "none") parts.push(`database: ${config.database}`);
1565
+ if (config.orm && config.orm !== "none") parts.push(`orm: ${config.orm}`);
1566
+ if (config.api && config.api !== "none") parts.push(`api: ${config.api}`);
1567
+ if (config.auth && config.auth !== "none") parts.push(`auth: ${config.auth}`);
1568
+ if (config.payments && config.payments !== "none") parts.push(`payments: ${config.payments}`);
1569
+ const addons = (config.addons ?? []).filter((item) => item !== "none");
1570
+ if (addons.length > 0) parts.push(`addons: ${addons.join("+")}`);
1571
+ return parts.join(", ");
1572
+ }
1573
+ function listMcpPresets() {
1574
+ const presets = [];
1575
+ for (const id of TEMPLATE_VALUES) {
1576
+ if (id === "none") continue;
1577
+ const config = getTemplateConfig(id);
1578
+ if (!config) continue;
1579
+ const ecosystem = (config.frontend ?? []).some((item) => item.startsWith("native-")) ? "react-native" : "typescript";
1580
+ presets.push({
1581
+ id,
1582
+ name: id.toUpperCase(),
1583
+ description: getTemplateDescription(id),
1584
+ ecosystem,
1585
+ stackSummary: buildPresetStackSummary(config),
1586
+ stack: config
1587
+ });
1588
+ }
1589
+ return presets;
1590
+ }
1591
+ function briefMatches(text, keywords) {
1592
+ const tokens = new Set(text.split(/[^a-z0-9]+/i).filter(Boolean));
1593
+ return keywords.some((keyword) => keyword.includes(" ") ? text.includes(keyword) : tokens.has(keyword));
1594
+ }
1595
+ function matchNearestPreset(input) {
1596
+ const signatureKeys = [
1597
+ "database",
1598
+ "backend",
1599
+ "api",
1600
+ "auth"
1601
+ ];
1602
+ const inputFrontend = input.frontend?.[0];
1603
+ let best = null;
1604
+ for (const id of TEMPLATE_VALUES) {
1605
+ if (id === "none") continue;
1606
+ const config = getTemplateConfig(id);
1607
+ if (!config) continue;
1608
+ let score = 0;
1609
+ for (const key of signatureKeys) {
1610
+ const value = config[key];
1611
+ if (value !== void 0 && value === input[key]) score += 1;
1612
+ }
1613
+ const presetFrontend = (config.frontend ?? [])[0];
1614
+ if (presetFrontend && presetFrontend === inputFrontend) score += 1;
1615
+ if (!best || score > best.score) best = {
1616
+ id,
1617
+ score
1618
+ };
1619
+ }
1620
+ return best && best.score >= 3 ? best.id : null;
1621
+ }
1622
+ function recommendStackFromBrief(brief, ecosystemHint) {
1623
+ const text = brief.toLowerCase();
1624
+ const has = (...keywords) => briefMatches(text, keywords);
1625
+ const rationale = [];
1626
+ const input = {};
1627
+ if (ecosystemHint && ecosystemHint !== "typescript") {
1628
+ input.ecosystem = ecosystemHint;
1629
+ rationale.push(`Ecosystem forced to ${ecosystemHint} from the provided hint; using ${ecosystemHint} defaults.`);
1630
+ rationale.push(`Brief keyword analysis (database/auth/payments/AI feature detection) currently applies to the TypeScript ecosystem only — configure those features explicitly for ${ecosystemHint} via bfs_check_compatibility.`);
1631
+ return {
1632
+ input,
1633
+ rationale,
1634
+ matchedPreset: null
1635
+ };
1636
+ }
1637
+ if (has("mobile", "ios", "android", "expo") || text.includes("react native") || text.includes("react-native") || text.includes("app store") || text.includes("play store")) {
1638
+ input.ecosystem = "react-native";
1639
+ input.frontend = ["native-uniwind"];
1640
+ input.backend = "none";
1641
+ input.runtime = "none";
1642
+ input.api = "none";
1643
+ input.database = "none";
1644
+ input.orm = "none";
1645
+ rationale.push("Mobile app detected: React Native (Expo) with the native-uniwind styling preset and no bundled backend.");
1646
+ return {
1647
+ input,
1648
+ rationale,
1649
+ matchedPreset: "uniwind"
1650
+ };
1651
+ }
1652
+ input.ecosystem = "typescript";
1653
+ input.frontend = ["tanstack-router"];
1654
+ input.backend = "hono";
1655
+ input.runtime = "bun";
1656
+ input.database = "sqlite";
1657
+ input.orm = "drizzle";
1658
+ input.api = "trpc";
1659
+ rationale.push("Default TypeScript fullstack baseline: TanStack Router + Hono + tRPC on SQLite/Drizzle (Bun).");
1660
+ if (has("postgres", "postgresql", "supabase", "neon")) {
1661
+ input.database = "postgres";
1662
+ input.orm = "drizzle";
1663
+ rationale.push("Postgres requested: database=postgres, orm=drizzle.");
1664
+ } else if (has("mysql", "planetscale")) {
1665
+ input.database = "mysql";
1666
+ input.orm = "drizzle";
1667
+ rationale.push("MySQL requested: database=mysql, orm=drizzle.");
1668
+ } else if (has("mongo", "mongodb")) {
1669
+ input.database = "mongodb";
1670
+ input.orm = "mongoose";
1671
+ rationale.push("MongoDB requested: database=mongodb, orm=mongoose.");
1672
+ }
1673
+ if (has("saas", "payment", "payments", "billing", "subscription", "subscriptions", "checkout", "stripe", "ecommerce")) {
1674
+ input.payments = "stripe";
1675
+ input.auth = "better-auth";
1676
+ if (input.database === "sqlite") {
1677
+ input.database = "postgres";
1678
+ input.orm = "drizzle";
1679
+ }
1680
+ rationale.push("SaaS/payments detected: Stripe + better-auth, upgraded to Postgres/Drizzle for production data.");
1681
+ } else if (has("auth", "login", "signin", "signup", "account", "accounts", "user", "users", "authentication", "admin", "dashboard", "portal", "members", "rbac", "permissions", "roles")) {
1682
+ input.auth = "better-auth";
1683
+ rationale.push("Authentication requested: auth=better-auth.");
1684
+ }
1685
+ if (has("ai", "chatbot", "llm", "gpt", "rag", "agent", "agents", "openai", "assistant", "copilot")) {
1686
+ input.ai = "vercel-ai";
1687
+ input.examples = ["ai"];
1688
+ rationale.push("AI/chatbot detected: Vercel AI SDK with the bundled AI example.");
1689
+ }
1690
+ if (has("blog", "content", "cms", "marketing", "landing", "publishing")) {
1691
+ input.cms = "sanity";
1692
+ rationale.push("Content/marketing site detected: Sanity CMS.");
1693
+ }
1694
+ if (has("realtime", "collaborative", "collaboration", "multiplayer", "presence") || text.includes("real-time") || text.includes("real time")) {
1695
+ input.realtime = "socket-io";
1696
+ rationale.push("Realtime/collaboration detected: Socket.IO.");
1697
+ }
1698
+ let matchedPreset = null;
1699
+ if (has("t3")) matchedPreset = "t3";
1700
+ else if (has("mern")) matchedPreset = "mern";
1701
+ else if (has("pern")) matchedPreset = "pern";
1702
+ else matchedPreset = matchNearestPreset(input);
1703
+ if (matchedPreset) rationale.push(`Closest ready-made preset: ${matchedPreset}.`);
1704
+ return {
1705
+ input,
1706
+ rationale,
1707
+ matchedPreset
1708
+ };
1709
+ }
1710
+ function normalizeAdjustedToInput(adjusted, base) {
1711
+ const webFrontend = adjusted.webFrontend ?? [];
1712
+ const nativeFrontend = adjusted.nativeFrontend ?? [];
1713
+ const frontend = [...webFrontend, ...nativeFrontend];
1714
+ const codeQuality = adjusted.codeQuality ?? [];
1715
+ const documentation = adjusted.documentation ?? [];
1716
+ const appPlatforms = adjusted.appPlatforms ?? [];
1717
+ return {
1718
+ ...adjusted,
1719
+ projectName: base.projectName,
1720
+ ecosystem: adjusted.ecosystem ?? base.ecosystem,
1721
+ frontend: frontend.length > 0 ? frontend : base.frontend,
1722
+ addons: [
1723
+ ...codeQuality,
1724
+ ...documentation,
1725
+ ...appPlatforms
1726
+ ],
1727
+ ai: adjusted.aiSdk ?? base.ai
1728
+ };
1729
+ }
1730
+ function summarizeRecommendedConfig(config) {
1731
+ const isTsWeb = config.ecosystem === "typescript" || config.ecosystem === "react-native";
1732
+ return {
1733
+ projectName: config.projectName,
1734
+ ecosystem: config.ecosystem,
1735
+ ...isTsWeb ? {
1736
+ frontend: config.frontend,
1737
+ backend: config.backend,
1738
+ runtime: config.runtime,
1739
+ api: config.api
1740
+ } : {},
1741
+ database: config.database,
1742
+ orm: config.orm,
1743
+ auth: config.auth,
1744
+ payments: config.payments,
1745
+ ai: config.ai,
1746
+ cms: config.cms,
1747
+ realtime: config.realtime,
1748
+ examples: config.examples,
1749
+ addons: config.addons
1750
+ };
1751
+ }
1752
+ const mobileInputSchema = {
1753
+ mobileNavigation: MobileNavigationSchema.optional().describe("Mobile navigation"),
1754
+ mobileUI: MobileUISchema.optional().describe("Mobile UI"),
1755
+ mobileStorage: MobileStorageSchema.optional().describe("Mobile storage"),
1756
+ mobileTesting: MobileTestingSchema.optional().describe("Mobile testing"),
1757
+ mobilePush: MobilePushSchema.optional().describe("Mobile push notifications"),
1758
+ mobileOTA: MobileOTASchema.optional().describe("Mobile OTA updates"),
1759
+ mobileDeepLinking: MobileDeepLinkingSchema.optional().describe("Mobile deep linking")
1760
+ };
1761
+ const deploymentInputSchema = {
1762
+ dbSetup: DatabaseSetupSchema.optional().describe("Database hosting provider"),
1763
+ webDeploy: WebDeploySchema.optional().describe("Web deployment target"),
1764
+ serverDeploy: ServerDeploySchema.optional().describe("Server deployment target")
1765
+ };
1766
+ const crossEcosystemInputSchema = {
1767
+ rustWebFramework: RustWebFrameworkSchema.optional().describe("Rust web framework"),
1768
+ rustFrontend: RustFrontendSchema.optional().describe("Rust frontend (WASM)"),
1769
+ rustOrm: RustOrmSchema.optional().describe("Rust ORM"),
1770
+ rustApi: RustApiSchema.optional().describe("Rust API layer"),
1771
+ rustCli: RustCliSchema.optional().describe("Rust CLI framework"),
1772
+ rustLibraries: z.array(RustLibrariesSchema).optional().describe("Rust libraries"),
1773
+ rustLogging: RustLoggingSchema.optional().describe("Rust logging library"),
1774
+ rustErrorHandling: RustErrorHandlingSchema.optional().describe("Rust error handling library"),
1775
+ rustCaching: RustCachingSchema.optional().describe("Rust caching library"),
1776
+ rustAuth: RustAuthSchema.optional().describe("Rust authentication library"),
1777
+ rustRealtime: RustRealtimeSchema.optional().describe("Rust realtime library"),
1778
+ rustMessageQueue: RustMessageQueueSchema.optional().describe("Rust message queue"),
1779
+ rustObservability: RustObservabilitySchema.optional().describe("Rust observability"),
1780
+ rustTemplating: RustTemplatingSchema.optional().describe("Rust template engine"),
1781
+ pythonWebFramework: PythonWebFrameworkSchema.optional().describe("Python web framework"),
1782
+ pythonOrm: PythonOrmSchema.optional().describe("Python ORM"),
1783
+ pythonValidation: PythonValidationSchema.optional().describe("Python validation"),
1784
+ pythonAi: z.array(PythonAiSchema).optional().describe("Python AI libraries"),
1785
+ pythonAuth: PythonAuthSchema.optional().describe("Python auth library"),
1786
+ pythonApi: PythonApiSchema.optional().describe("Python API framework"),
1787
+ pythonTaskQueue: PythonTaskQueueSchema.optional().describe("Python task queue"),
1788
+ pythonGraphql: PythonGraphqlSchema.optional().describe("Python GraphQL framework"),
1789
+ pythonQuality: PythonQualitySchema.optional().describe("Python code quality"),
1790
+ pythonTesting: z.array(PythonTestingSchema).optional().describe("Python testing libraries"),
1791
+ pythonCaching: PythonCachingSchema.optional().describe("Python caching library"),
1792
+ pythonRealtime: PythonRealtimeSchema.optional().describe("Python realtime library"),
1793
+ pythonObservability: PythonObservabilitySchema.optional().describe("Python observability"),
1794
+ pythonCli: z.array(PythonCliSchema).optional().describe("Python CLI tooling"),
1795
+ goWebFramework: GoWebFrameworkSchema.optional().describe("Go web framework"),
1796
+ goOrm: GoOrmSchema.optional().describe("Go ORM"),
1797
+ goApi: GoApiSchema.optional().describe("Go API layer"),
1798
+ goCli: GoCliSchema.optional().describe("Go CLI framework"),
1799
+ goLogging: GoLoggingSchema.optional().describe("Go logging library"),
1800
+ goAuth: GoAuthSchema.optional().describe("Go authentication library"),
1801
+ goTesting: z.array(GoTestingSchema).optional().describe("Go testing libraries"),
1802
+ goRealtime: GoRealtimeSchema.optional().describe("Go realtime/WebSocket library"),
1803
+ goMessageQueue: GoMessageQueueSchema.optional().describe("Go message queue"),
1804
+ goCaching: GoCachingSchema.optional().describe("Go caching library"),
1805
+ goConfig: GoConfigSchema.optional().describe("Go config management"),
1806
+ goObservability: GoObservabilitySchema.optional().describe("Go observability"),
1807
+ javaWebFramework: JavaWebFrameworkSchema.optional().describe("Java web framework"),
1808
+ javaBuildTool: JavaBuildToolSchema.optional().describe("Java build tool"),
1809
+ javaOrm: JavaOrmSchema.optional().describe("Java ORM"),
1810
+ javaAuth: JavaAuthSchema.optional().describe("Java authentication library"),
1811
+ javaApi: JavaApiSchema.optional().describe("Java API layer"),
1812
+ javaLogging: JavaLoggingSchema.optional().describe("Java logging configuration"),
1813
+ javaLibraries: z.array(JavaLibrariesSchema).optional().describe("Java application libraries"),
1814
+ javaTestingLibraries: z.array(JavaTestingLibrariesSchema).optional().describe("Java testing libraries"),
1815
+ dotnetWebFramework: DotnetWebFrameworkSchema.optional().describe(".NET web framework"),
1816
+ dotnetOrm: DotnetOrmSchema.optional().describe(".NET ORM/data access"),
1817
+ dotnetAuth: DotnetAuthSchema.optional().describe(".NET authentication library"),
1818
+ dotnetApi: DotnetApiSchema.optional().describe(".NET API style"),
1819
+ dotnetTesting: z.array(DotnetTestingSchema).optional().describe(".NET testing libraries"),
1820
+ dotnetJobQueue: DotnetJobQueueSchema.optional().describe(".NET jobs and scheduling"),
1821
+ dotnetRealtime: DotnetRealtimeSchema.optional().describe(".NET realtime feature"),
1822
+ dotnetObservability: z.array(DotnetObservabilitySchema).optional().describe(".NET observability/logging libraries"),
1823
+ dotnetValidation: DotnetValidationSchema.optional().describe(".NET validation"),
1824
+ dotnetCaching: DotnetCachingSchema.optional().describe(".NET caching library"),
1825
+ dotnetDeploy: DotnetDeploySchema.optional().describe(".NET deployment target"),
1826
+ elixirWebFramework: ElixirWebFrameworkSchema.optional().describe("Elixir web framework"),
1827
+ elixirOrm: ElixirOrmSchema.optional().describe("Elixir persistence layer"),
1828
+ elixirAuth: ElixirAuthSchema.optional().describe("Elixir authentication"),
1829
+ elixirApi: ElixirApiSchema.optional().describe("Elixir API layer"),
1830
+ elixirRealtime: ElixirRealtimeSchema.optional().describe("Elixir realtime feature"),
1831
+ elixirJobs: ElixirJobsSchema.optional().describe("Elixir jobs and scheduling"),
1832
+ elixirValidation: ElixirValidationSchema.optional().describe("Elixir validation/data"),
1833
+ elixirHttp: ElixirHttpSchema.optional().describe("Elixir HTTP client"),
1834
+ elixirJson: ElixirJsonSchema.optional().describe("Elixir JSON library"),
1835
+ elixirEmail: ElixirEmailSchema.optional().describe("Elixir email library"),
1836
+ elixirCaching: ElixirCachingSchema.optional().describe("Elixir caching library"),
1837
+ elixirObservability: ElixirObservabilitySchema.optional().describe("Elixir observability"),
1838
+ elixirTesting: ElixirTestingSchema.optional().describe("Elixir testing library"),
1839
+ elixirQuality: ElixirQualitySchema.optional().describe("Elixir code quality/security"),
1840
+ elixirDeploy: ElixirDeploySchema.optional().describe("Elixir deployment target"),
1841
+ elixirLibraries: z.array(ElixirLibrariesSchema).optional().describe("Elixir libraries")
1842
+ };
1843
+ const MCP_PLAN_CREATE_SCHEMA = {
1844
+ projectName: z.string().optional().describe("Project name (kebab-case)"),
1845
+ part: z.array(z.string()).optional().describe("Stack graph part binding, e.g. frontend:typescript:next or backend.orm:go:gorm"),
1846
+ ecosystem: EcosystemSchema.optional().describe("Language ecosystem (default: typescript)"),
1847
+ frontend: z.array(FrontendSchema).optional().describe("Frontend frameworks (TypeScript only)"),
1848
+ backend: BackendSchema.optional().describe("Backend framework"),
1849
+ runtime: RuntimeSchema.optional().describe("JavaScript runtime"),
1850
+ database: DatabaseSchema.optional().describe("Database type"),
1851
+ orm: ORMSchema.optional().describe("ORM"),
1852
+ api: APISchema.optional().describe("API layer"),
1853
+ auth: AuthSchema.optional().describe("Auth provider"),
1854
+ payments: PaymentsSchema.optional().describe("Payments provider"),
1855
+ email: EmailSchema.optional().describe("Email provider"),
1856
+ addons: z.array(AddonsSchema).optional().describe("Addons"),
1857
+ examples: z.array(ExamplesSchema).optional().describe("Example templates"),
1858
+ packageManager: PackageManagerSchema.optional().describe("Package manager (default: bun)"),
1859
+ cssFramework: CSSFrameworkSchema.optional().describe("CSS framework"),
1860
+ uiLibrary: UILibrarySchema.optional().describe("UI component library"),
1861
+ shadcnBase: ShadcnBaseSchema.optional().describe("shadcn/ui headless library"),
1862
+ shadcnStyle: ShadcnStyleSchema.optional().describe("shadcn/ui visual style"),
1863
+ shadcnIconLibrary: ShadcnIconLibrarySchema.optional().describe("shadcn/ui icon library"),
1864
+ shadcnColorTheme: ShadcnColorThemeSchema.optional().describe("shadcn/ui color theme"),
1865
+ shadcnBaseColor: ShadcnBaseColorSchema.optional().describe("shadcn/ui base neutral color"),
1866
+ shadcnFont: ShadcnFontSchema.optional().describe("shadcn/ui font"),
1867
+ shadcnRadius: ShadcnRadiusSchema.optional().describe("shadcn/ui border radius"),
1868
+ ai: AISchema.optional().describe("AI SDK"),
1869
+ stateManagement: StateManagementSchema.optional().describe("State management"),
1870
+ forms: FormsSchema.optional().describe("Forms library"),
1871
+ validation: ValidationSchema.optional().describe("Validation library"),
1872
+ testing: TestingSchema.optional().describe("Testing framework"),
1873
+ realtime: RealtimeSchema.optional().describe("Realtime library"),
1874
+ jobQueue: JobQueueSchema.optional().describe("Job queue"),
1875
+ animation: AnimationSchema.optional().describe("Animation library"),
1876
+ logging: LoggingSchema.optional().describe("Logging library"),
1877
+ observability: ObservabilitySchema.optional().describe("Observability"),
1878
+ featureFlags: FeatureFlagsSchema.optional().describe("Feature flag provider"),
1879
+ search: SearchSchema.optional().describe("Search engine"),
1880
+ vectorDb: VectorDbSchema.optional().describe("Vector database (TypeScript only)"),
1881
+ caching: CachingSchema.optional().describe("Caching solution"),
1882
+ rateLimit: RateLimitSchema.optional().describe("Rate limiting solution"),
1883
+ i18n: I18nSchema.optional().describe("Internationalization (i18n) library"),
1884
+ cms: CMSSchema.optional().describe("CMS"),
1885
+ fileStorage: FileStorageSchema.optional().describe("File storage"),
1886
+ ...mobileInputSchema,
1887
+ fileUpload: FileUploadSchema.optional().describe("File upload"),
1888
+ ...deploymentInputSchema,
1889
+ effect: EffectSchema.optional().describe("Effect ecosystem (effect, effect-full)"),
1890
+ analytics: AnalyticsSchema.optional().describe("Privacy-focused analytics provider"),
1891
+ astroIntegration: AstroIntegrationSchema.optional().describe("Astro UI framework integration (react, vue, svelte, solid)"),
1892
+ aiDocs: z.array(AiDocsSchema).optional().describe("AI documentation files (claude-md, agents-md, cursorrules)"),
1893
+ versionChannel: VersionChannelSchema.optional().describe("Dependency version channel (stable, latest, beta)"),
1894
+ ...crossEcosystemInputSchema
1895
+ };
1896
+ const MCP_STACK_UPDATE_SCHEMA = {
1897
+ ...MCP_PLAN_CREATE_SCHEMA,
1898
+ projectDir: z.string().describe("Absolute path to the existing Better-Fullstack project")
1899
+ };
568
1900
  async function startMcpServer() {
569
1901
  const server = new McpServer({
570
1902
  name: "better-fullstack",
@@ -573,161 +1905,201 @@ async function startMcpServer() {
573
1905
  instructions: INSTRUCTIONS,
574
1906
  capabilities: { logging: {} }
575
1907
  });
576
- const registerTool = server.tool.bind(server);
577
- registerTool("bfs_get_guidance", "Returns workflow rules, field semantics, ambiguity rules, and critical constraints. Call this FIRST before using other tools.", mcpInputSchema({}), async () => {
1908
+ const registerTool = (name, config, cb) => {
1909
+ server.registerTool(name, config, cb);
1910
+ };
1911
+ registerTool("bfs_get_guidance", {
1912
+ description: "Returns workflow rules, field semantics, ambiguity rules, and critical constraints. Call this FIRST before using other tools.",
1913
+ inputSchema: mcpInputSchema({}),
1914
+ outputSchema: guidanceOutputSchema,
1915
+ annotations: {
1916
+ title: "Get guidance",
1917
+ readOnlyHint: true,
1918
+ idempotentHint: true,
1919
+ openWorldHint: false
1920
+ }
1921
+ }, async () => {
578
1922
  const guidance = getGuidance();
579
- return { content: [{
580
- type: "text",
581
- text: JSON.stringify(guidance, null, 2)
582
- }] };
1923
+ return {
1924
+ content: [{
1925
+ type: "text",
1926
+ text: JSON.stringify(guidance, null, 2)
1927
+ }],
1928
+ structuredContent: guidance
1929
+ };
583
1930
  });
584
- registerTool("bfs_get_schema", "Returns valid options for a specific category (e.g., 'database', 'frontend', 'backend') or ALL categories. Use ecosystem to filter to relevant categories only.", mcpInputSchema({
585
- category: z.string().optional().describe("Category name (e.g., 'database', 'orm', 'frontend'). Omit for all categories."),
586
- ecosystem: EcosystemSchema.optional().describe("Filter categories to this ecosystem (e.g., 'rust' returns only Rust + shared categories).")
587
- }), async ({ category, ecosystem }) => {
1931
+ registerTool("bfs_get_schema", {
1932
+ description: "Returns valid options for a specific category (e.g., 'database', 'frontend', 'backend') or ALL categories. Use ecosystem to filter to relevant categories only.",
1933
+ inputSchema: mcpInputSchema({
1934
+ category: z.string().optional().describe("Category name (e.g., 'database', 'orm', 'frontend'). Omit for all categories."),
1935
+ ecosystem: EcosystemSchema.optional().describe("Filter categories to this ecosystem (e.g., 'rust' returns only Rust + shared categories).")
1936
+ }),
1937
+ outputSchema: schemaOutputSchema,
1938
+ annotations: {
1939
+ title: "Get schema options",
1940
+ readOnlyHint: true,
1941
+ idempotentHint: true,
1942
+ openWorldHint: false
1943
+ }
1944
+ }, async ({ category, ecosystem }) => {
588
1945
  const result = getSchemaOptions(category, ecosystem);
1946
+ const structuredContent = "error" in result ? { error: result.error } : "category" in result ? {
1947
+ category: result.category,
1948
+ options: result.options
1949
+ } : { categories: result };
1950
+ return {
1951
+ content: [{
1952
+ type: "text",
1953
+ text: JSON.stringify(result, null, 2)
1954
+ }],
1955
+ structuredContent
1956
+ };
1957
+ });
1958
+ registerTool("bfs_list_presets", {
1959
+ description: "Lists the ready-made stack presets available to the CLI (mern, pern, t3, uniwind) with id, name, description, ecosystem, and a stack summary. Use to discover a starting point before bfs_recommend_stack, bfs_plan_project, or bfs_create_project.",
1960
+ inputSchema: mcpInputSchema({}),
1961
+ annotations: {
1962
+ title: "List presets",
1963
+ readOnlyHint: true,
1964
+ idempotentHint: true,
1965
+ openWorldHint: false
1966
+ }
1967
+ }, async () => {
1968
+ const presets = listMcpPresets();
589
1969
  return { content: [{
590
1970
  type: "text",
591
- text: JSON.stringify(result, null, 2)
1971
+ text: JSON.stringify({ presets }, null, 2)
592
1972
  }] };
593
1973
  });
594
- const mobileInputSchema = {
595
- mobileNavigation: MobileNavigationSchema.optional().describe("Mobile navigation"),
596
- mobileUI: MobileUISchema.optional().describe("Mobile UI"),
597
- mobileStorage: MobileStorageSchema.optional().describe("Mobile storage"),
598
- mobileTesting: MobileTestingSchema.optional().describe("Mobile testing"),
599
- mobilePush: MobilePushSchema.optional().describe("Mobile push notifications"),
600
- mobileOTA: MobileOTASchema.optional().describe("Mobile OTA updates"),
601
- mobileDeepLinking: MobileDeepLinkingSchema.optional().describe("Mobile deep linking")
602
- };
603
- const deploymentInputSchema = {
604
- dbSetup: DatabaseSetupSchema.optional().describe("Database hosting provider"),
605
- webDeploy: WebDeploySchema.optional().describe("Web deployment target"),
606
- serverDeploy: ServerDeploySchema.optional().describe("Server deployment target")
607
- };
608
- const crossEcosystemInputSchema = {
609
- rustWebFramework: RustWebFrameworkSchema.optional().describe("Rust web framework"),
610
- rustFrontend: RustFrontendSchema.optional().describe("Rust frontend (WASM)"),
611
- rustOrm: RustOrmSchema.optional().describe("Rust ORM"),
612
- rustApi: RustApiSchema.optional().describe("Rust API layer"),
613
- rustCli: RustCliSchema.optional().describe("Rust CLI framework"),
614
- rustLibraries: z.array(RustLibrariesSchema).optional().describe("Rust libraries"),
615
- rustLogging: RustLoggingSchema.optional().describe("Rust logging library"),
616
- rustErrorHandling: RustErrorHandlingSchema.optional().describe("Rust error handling library"),
617
- rustCaching: RustCachingSchema.optional().describe("Rust caching library"),
618
- rustAuth: RustAuthSchema.optional().describe("Rust authentication library"),
619
- rustRealtime: RustRealtimeSchema.optional().describe("Rust realtime library"),
620
- rustMessageQueue: RustMessageQueueSchema.optional().describe("Rust message queue"),
621
- rustObservability: RustObservabilitySchema.optional().describe("Rust observability"),
622
- rustTemplating: RustTemplatingSchema.optional().describe("Rust template engine"),
623
- pythonWebFramework: PythonWebFrameworkSchema.optional().describe("Python web framework"),
624
- pythonOrm: PythonOrmSchema.optional().describe("Python ORM"),
625
- pythonValidation: PythonValidationSchema.optional().describe("Python validation"),
626
- pythonAi: z.array(PythonAiSchema).optional().describe("Python AI libraries"),
627
- pythonAuth: PythonAuthSchema.optional().describe("Python auth library"),
628
- pythonApi: PythonApiSchema.optional().describe("Python API framework"),
629
- pythonTaskQueue: PythonTaskQueueSchema.optional().describe("Python task queue"),
630
- pythonGraphql: PythonGraphqlSchema.optional().describe("Python GraphQL framework"),
631
- pythonQuality: PythonQualitySchema.optional().describe("Python code quality"),
632
- pythonTesting: z.array(PythonTestingSchema).optional().describe("Python testing libraries"),
633
- pythonCaching: PythonCachingSchema.optional().describe("Python caching library"),
634
- pythonRealtime: PythonRealtimeSchema.optional().describe("Python realtime library"),
635
- pythonObservability: PythonObservabilitySchema.optional().describe("Python observability"),
636
- pythonCli: z.array(PythonCliSchema).optional().describe("Python CLI tooling"),
637
- goWebFramework: GoWebFrameworkSchema.optional().describe("Go web framework"),
638
- goOrm: GoOrmSchema.optional().describe("Go ORM"),
639
- goApi: GoApiSchema.optional().describe("Go API layer"),
640
- goCli: GoCliSchema.optional().describe("Go CLI framework"),
641
- goLogging: GoLoggingSchema.optional().describe("Go logging library"),
642
- goAuth: GoAuthSchema.optional().describe("Go authentication library"),
643
- goTesting: z.array(GoTestingSchema).optional().describe("Go testing libraries"),
644
- goRealtime: GoRealtimeSchema.optional().describe("Go realtime/WebSocket library"),
645
- goMessageQueue: GoMessageQueueSchema.optional().describe("Go message queue"),
646
- goCaching: GoCachingSchema.optional().describe("Go caching library"),
647
- goConfig: GoConfigSchema.optional().describe("Go config management"),
648
- goObservability: GoObservabilitySchema.optional().describe("Go observability"),
649
- javaWebFramework: JavaWebFrameworkSchema.optional().describe("Java web framework"),
650
- javaBuildTool: JavaBuildToolSchema.optional().describe("Java build tool"),
651
- javaOrm: JavaOrmSchema.optional().describe("Java ORM"),
652
- javaAuth: JavaAuthSchema.optional().describe("Java authentication library"),
653
- javaApi: JavaApiSchema.optional().describe("Java API layer"),
654
- javaLogging: JavaLoggingSchema.optional().describe("Java logging configuration"),
655
- javaLibraries: z.array(JavaLibrariesSchema).optional().describe("Java application libraries"),
656
- javaTestingLibraries: z.array(JavaTestingLibrariesSchema).optional().describe("Java testing libraries"),
657
- dotnetWebFramework: DotnetWebFrameworkSchema.optional().describe(".NET web framework"),
658
- dotnetOrm: DotnetOrmSchema.optional().describe(".NET ORM/data access"),
659
- dotnetAuth: DotnetAuthSchema.optional().describe(".NET authentication library"),
660
- dotnetApi: DotnetApiSchema.optional().describe(".NET API style"),
661
- dotnetTesting: z.array(DotnetTestingSchema).optional().describe(".NET testing libraries"),
662
- dotnetJobQueue: DotnetJobQueueSchema.optional().describe(".NET jobs and scheduling"),
663
- dotnetRealtime: DotnetRealtimeSchema.optional().describe(".NET realtime feature"),
664
- dotnetObservability: z.array(DotnetObservabilitySchema).optional().describe(".NET observability/logging libraries"),
665
- dotnetValidation: DotnetValidationSchema.optional().describe(".NET validation"),
666
- dotnetCaching: DotnetCachingSchema.optional().describe(".NET caching library"),
667
- dotnetDeploy: DotnetDeploySchema.optional().describe(".NET deployment target"),
668
- elixirWebFramework: ElixirWebFrameworkSchema.optional().describe("Elixir web framework"),
669
- elixirOrm: ElixirOrmSchema.optional().describe("Elixir persistence layer"),
670
- elixirAuth: ElixirAuthSchema.optional().describe("Elixir authentication"),
671
- elixirApi: ElixirApiSchema.optional().describe("Elixir API layer"),
672
- elixirRealtime: ElixirRealtimeSchema.optional().describe("Elixir realtime feature"),
673
- elixirJobs: ElixirJobsSchema.optional().describe("Elixir jobs and scheduling"),
674
- elixirValidation: ElixirValidationSchema.optional().describe("Elixir validation/data"),
675
- elixirHttp: ElixirHttpSchema.optional().describe("Elixir HTTP client"),
676
- elixirJson: ElixirJsonSchema.optional().describe("Elixir JSON library"),
677
- elixirEmail: ElixirEmailSchema.optional().describe("Elixir email library"),
678
- elixirCaching: ElixirCachingSchema.optional().describe("Elixir caching library"),
679
- elixirObservability: ElixirObservabilitySchema.optional().describe("Elixir observability"),
680
- elixirTesting: ElixirTestingSchema.optional().describe("Elixir testing library"),
681
- elixirQuality: ElixirQualitySchema.optional().describe("Elixir code quality/security"),
682
- elixirDeploy: ElixirDeploySchema.optional().describe("Elixir deployment target"),
683
- elixirLibraries: z.array(ElixirLibrariesSchema).optional().describe("Elixir libraries")
684
- };
685
- registerTool("bfs_check_compatibility", "Validates a stack combination and returns auto-adjusted selections with warnings. Call BEFORE creating a project to avoid invalid combinations.", mcpInputSchema({
686
- ecosystem: EcosystemSchema.describe("Language ecosystem"),
687
- frontend: z.array(z.string()).optional().describe("Web frontend frameworks (TypeScript only)"),
688
- backend: z.string().optional().describe("Backend framework"),
689
- runtime: z.string().optional().describe("JavaScript runtime"),
690
- database: z.string().optional().describe("Database type"),
691
- orm: z.string().optional().describe("ORM"),
692
- api: z.string().optional().describe("API layer"),
693
- auth: z.string().optional().describe("Auth provider"),
694
- payments: z.string().optional().describe("Payments provider"),
695
- email: EmailSchema.optional().describe("Email provider"),
696
- fileUpload: FileUploadSchema.optional().describe("File upload provider"),
697
- ai: AISchema.optional().describe("AI SDK"),
698
- stateManagement: StateManagementSchema.optional().describe("State management"),
699
- forms: FormsSchema.optional().describe("Forms library"),
700
- validation: ValidationSchema.optional().describe("Validation library"),
701
- testing: TestingSchema.optional().describe("Testing framework"),
702
- realtime: RealtimeSchema.optional().describe("Realtime library"),
703
- jobQueue: JobQueueSchema.optional().describe("Job queue"),
704
- animation: AnimationSchema.optional().describe("Animation library"),
705
- logging: LoggingSchema.optional().describe("Logging library"),
706
- observability: ObservabilitySchema.optional().describe("Observability provider"),
707
- featureFlags: FeatureFlagsSchema.optional().describe("Feature flags provider"),
708
- analytics: AnalyticsSchema.optional().describe("Analytics provider"),
709
- cms: CMSSchema.optional().describe("CMS"),
710
- caching: CachingSchema.optional().describe("Caching solution"),
711
- rateLimit: RateLimitSchema.optional().describe("Rate limiting solution"),
712
- i18n: I18nSchema.optional().describe("Internationalization library"),
713
- search: SearchSchema.optional().describe("Search engine"),
714
- fileStorage: FileStorageSchema.optional().describe("File storage"),
715
- ...mobileInputSchema,
716
- ...deploymentInputSchema,
717
- astroIntegration: AstroIntegrationSchema.optional().describe("Astro UI framework integration"),
718
- uiLibrary: z.string().optional().describe("UI component library"),
719
- cssFramework: z.string().optional().describe("CSS framework"),
720
- addons: z.array(AddonsSchema).optional().describe("Addon list"),
721
- examples: z.array(ExamplesSchema).optional().describe("Example templates"),
722
- packageManager: PackageManagerSchema.optional().describe("Package manager"),
723
- ...crossEcosystemInputSchema
724
- }), async (input) => {
1974
+ registerTool("bfs_recommend_stack", {
1975
+ description: "Recommends a compatibility-validated stack from a natural-language brief using deterministic keyword rules (no LLM). Returns the config, rationale, any auto-applied compatibility adjustments, the nearest matching preset, and a reproducible CLI command.",
1976
+ inputSchema: mcpInputSchema({
1977
+ brief: z.string().describe("Natural-language description of the app to build (e.g., 'a SaaS with payments and auth')."),
1978
+ ecosystem: EcosystemSchema.optional().describe("Force a language ecosystem. Omit to let the brief decide (defaults to TypeScript)."),
1979
+ projectName: z.string().optional().describe("Project name (kebab-case). Default: 'my-app'.")
1980
+ }),
1981
+ annotations: {
1982
+ title: "Recommend stack",
1983
+ readOnlyHint: true,
1984
+ idempotentHint: false,
1985
+ openWorldHint: false
1986
+ }
1987
+ }, async ({ brief, ecosystem, projectName }) => {
725
1988
  try {
726
- const filtered = filterCompatibilityResult(analyzeStackCompatibility(buildCompatibilityInput(input)), input.ecosystem);
1989
+ const { input: recommended, rationale, matchedPreset } = recommendStackFromBrief(brief, ecosystem);
1990
+ const baseInput = {
1991
+ projectName: projectName ?? "my-app",
1992
+ ...recommended
1993
+ };
1994
+ const compatResult = analyzeStackCompatibility(buildCompatibilityInput(baseInput));
1995
+ const finalConfig = buildProjectConfig(compatResult.adjustedStack ? normalizeAdjustedToInput(compatResult.adjustedStack, baseInput) : baseInput, { projectDir: `/${baseInput.projectName}` });
1996
+ const adjustments = compatResult.changes.map((change) => `${change.category}: ${change.message}`);
1997
+ const graphPreview = getMcpGraphPreview(finalConfig);
1998
+ const reproducibleCommand = generateReproducibleCommand(finalConfig);
727
1999
  return { content: [{
728
2000
  type: "text",
729
- text: JSON.stringify(filtered, null, 2)
2001
+ text: JSON.stringify({
2002
+ brief,
2003
+ config: summarizeRecommendedConfig(finalConfig),
2004
+ rationale,
2005
+ adjustments,
2006
+ matchedPreset,
2007
+ reproducibleCommand,
2008
+ ...graphPreview,
2009
+ nextSteps: "Call bfs_plan_project with this config to preview the files, then bfs_create_project to scaffold it."
2010
+ }, null, 2)
730
2011
  }] };
2012
+ } catch (error) {
2013
+ return {
2014
+ content: [{
2015
+ type: "text",
2016
+ text: `Recommend stack failed: ${error instanceof Error ? error.message : String(error)}`
2017
+ }],
2018
+ isError: true
2019
+ };
2020
+ }
2021
+ });
2022
+ registerTool("bfs_check_compatibility", {
2023
+ description: "Validates a stack combination and returns auto-adjusted selections with warnings. Call BEFORE creating a project to avoid invalid combinations.",
2024
+ outputSchema: compatibilityOutputSchema,
2025
+ annotations: {
2026
+ title: "Check stack compatibility",
2027
+ readOnlyHint: true,
2028
+ idempotentHint: true,
2029
+ openWorldHint: false
2030
+ },
2031
+ inputSchema: mcpInputSchema({
2032
+ ecosystem: EcosystemSchema.describe("Language ecosystem"),
2033
+ frontend: z.array(z.string()).optional().describe("Web frontend frameworks (TypeScript only)"),
2034
+ backend: z.string().optional().describe("Backend framework"),
2035
+ runtime: z.string().optional().describe("JavaScript runtime"),
2036
+ database: z.string().optional().describe("Database type"),
2037
+ orm: z.string().optional().describe("ORM"),
2038
+ api: z.string().optional().describe("API layer"),
2039
+ auth: z.string().optional().describe("Auth provider"),
2040
+ payments: z.string().optional().describe("Payments provider"),
2041
+ email: EmailSchema.optional().describe("Email provider"),
2042
+ fileUpload: FileUploadSchema.optional().describe("File upload provider"),
2043
+ ai: AISchema.optional().describe("AI SDK"),
2044
+ stateManagement: StateManagementSchema.optional().describe("State management"),
2045
+ forms: FormsSchema.optional().describe("Forms library"),
2046
+ validation: ValidationSchema.optional().describe("Validation library"),
2047
+ testing: TestingSchema.optional().describe("Testing framework"),
2048
+ realtime: RealtimeSchema.optional().describe("Realtime library"),
2049
+ jobQueue: JobQueueSchema.optional().describe("Job queue"),
2050
+ animation: AnimationSchema.optional().describe("Animation library"),
2051
+ logging: LoggingSchema.optional().describe("Logging library"),
2052
+ observability: ObservabilitySchema.optional().describe("Observability provider"),
2053
+ featureFlags: FeatureFlagsSchema.optional().describe("Feature flags provider"),
2054
+ analytics: AnalyticsSchema.optional().describe("Analytics provider"),
2055
+ cms: CMSSchema.optional().describe("CMS"),
2056
+ caching: CachingSchema.optional().describe("Caching solution"),
2057
+ rateLimit: RateLimitSchema.optional().describe("Rate limiting solution"),
2058
+ i18n: I18nSchema.optional().describe("Internationalization library"),
2059
+ search: SearchSchema.optional().describe("Search engine"),
2060
+ vectorDb: VectorDbSchema.optional().describe("Vector database (TypeScript only)"),
2061
+ fileStorage: FileStorageSchema.optional().describe("File storage"),
2062
+ ...mobileInputSchema,
2063
+ ...deploymentInputSchema,
2064
+ astroIntegration: AstroIntegrationSchema.optional().describe("Astro UI framework integration"),
2065
+ uiLibrary: z.string().optional().describe("UI component library"),
2066
+ cssFramework: z.string().optional().describe("CSS framework"),
2067
+ shadcnBase: ShadcnBaseSchema.optional().describe("shadcn/ui headless library"),
2068
+ shadcnStyle: ShadcnStyleSchema.optional().describe("shadcn/ui visual style"),
2069
+ shadcnIconLibrary: ShadcnIconLibrarySchema.optional().describe("shadcn/ui icon library"),
2070
+ shadcnColorTheme: ShadcnColorThemeSchema.optional().describe("shadcn/ui color theme"),
2071
+ shadcnBaseColor: ShadcnBaseColorSchema.optional().describe("shadcn/ui base neutral color"),
2072
+ shadcnFont: ShadcnFontSchema.optional().describe("shadcn/ui font"),
2073
+ shadcnRadius: ShadcnRadiusSchema.optional().describe("shadcn/ui border radius"),
2074
+ addons: z.array(AddonsSchema).optional().describe("Addon list"),
2075
+ examples: z.array(ExamplesSchema).optional().describe("Example templates"),
2076
+ packageManager: PackageManagerSchema.optional().describe("Package manager"),
2077
+ ...crossEcosystemInputSchema
2078
+ })
2079
+ }, async (input) => {
2080
+ try {
2081
+ const compatInput = buildCompatibilityInput(input);
2082
+ const filtered = filterCompatibilityResult(analyzeStackCompatibility(compatInput), input.ecosystem);
2083
+ const evaluation = evaluateCompatibility(compatInput);
2084
+ const relevantEcosystem = isMcpEcosystem(input.ecosystem) ? input.ecosystem : "typescript";
2085
+ const relevantIssueKeys = new Set([...getMcpCategoryKeysForEcosystem(relevantEcosystem), ...MCP_SHARED_COMPATIBILITY_KEYS]);
2086
+ const issues = evaluation.issues.filter((issue) => !issue.category || relevantIssueKeys.has(issue.category));
2087
+ const structuredContent = {
2088
+ adjustedStack: filtered.adjustedStack,
2089
+ changes: filtered.changes,
2090
+ issues,
2091
+ hasIssues: issues.length > 0
2092
+ };
2093
+ return {
2094
+ content: [{
2095
+ type: "text",
2096
+ text: JSON.stringify({
2097
+ ...filtered,
2098
+ issues
2099
+ }, null, 2)
2100
+ }],
2101
+ structuredContent
2102
+ };
731
2103
  } catch (error) {
732
2104
  return {
733
2105
  content: [{
@@ -738,64 +2110,39 @@ async function startMcpServer() {
738
2110
  };
739
2111
  }
740
2112
  });
741
- const planCreateSchema = {
742
- projectName: z.string().optional().describe("Project name (kebab-case)"),
743
- ecosystem: EcosystemSchema.optional().describe("Language ecosystem (default: typescript)"),
744
- frontend: z.array(FrontendSchema).optional().describe("Frontend frameworks (TypeScript only)"),
745
- backend: BackendSchema.optional().describe("Backend framework"),
746
- runtime: RuntimeSchema.optional().describe("JavaScript runtime"),
747
- database: DatabaseSchema.optional().describe("Database type"),
748
- orm: ORMSchema.optional().describe("ORM"),
749
- api: APISchema.optional().describe("API layer"),
750
- auth: AuthSchema.optional().describe("Auth provider"),
751
- payments: PaymentsSchema.optional().describe("Payments provider"),
752
- email: EmailSchema.optional().describe("Email provider"),
753
- addons: z.array(AddonsSchema).optional().describe("Addons"),
754
- examples: z.array(ExamplesSchema).optional().describe("Example templates"),
755
- packageManager: PackageManagerSchema.optional().describe("Package manager (default: bun)"),
756
- cssFramework: CSSFrameworkSchema.optional().describe("CSS framework"),
757
- uiLibrary: UILibrarySchema.optional().describe("UI component library"),
758
- ai: AISchema.optional().describe("AI SDK"),
759
- stateManagement: StateManagementSchema.optional().describe("State management"),
760
- forms: FormsSchema.optional().describe("Forms library"),
761
- validation: ValidationSchema.optional().describe("Validation library"),
762
- testing: TestingSchema.optional().describe("Testing framework"),
763
- realtime: RealtimeSchema.optional().describe("Realtime library"),
764
- jobQueue: JobQueueSchema.optional().describe("Job queue"),
765
- animation: AnimationSchema.optional().describe("Animation library"),
766
- logging: LoggingSchema.optional().describe("Logging library"),
767
- observability: ObservabilitySchema.optional().describe("Observability"),
768
- featureFlags: FeatureFlagsSchema.optional().describe("Feature flag provider"),
769
- search: SearchSchema.optional().describe("Search engine"),
770
- caching: CachingSchema.optional().describe("Caching solution"),
771
- rateLimit: RateLimitSchema.optional().describe("Rate limiting solution"),
772
- i18n: I18nSchema.optional().describe("Internationalization (i18n) library"),
773
- cms: CMSSchema.optional().describe("CMS"),
774
- fileStorage: FileStorageSchema.optional().describe("File storage"),
775
- ...mobileInputSchema,
776
- fileUpload: FileUploadSchema.optional().describe("File upload"),
777
- ...deploymentInputSchema,
778
- ...crossEcosystemInputSchema
779
- };
780
- registerTool("bfs_plan_project", "Dry-run: generates a project in-memory and returns the file tree WITHOUT writing to disk. Use this to preview what would be created.", mcpInputSchema(planCreateSchema), async (input) => {
2113
+ registerTool("bfs_plan_project", {
2114
+ description: "Dry-run: generates a project in-memory and returns the file tree WITHOUT writing to disk. Use this to preview what would be created.",
2115
+ inputSchema: mcpInputSchema(MCP_PLAN_CREATE_SCHEMA),
2116
+ outputSchema: planProjectOutputSchema,
2117
+ annotations: {
2118
+ title: "Plan project (dry run)",
2119
+ readOnlyHint: true,
2120
+ idempotentHint: true,
2121
+ openWorldHint: false
2122
+ }
2123
+ }, async (input) => {
781
2124
  try {
782
- const { generateVirtualProject, EMBEDDED_TEMPLATES } = await import("@better-fullstack/template-generator");
2125
+ const { generateVirtualProject: generateVirtualProject$1, EMBEDDED_TEMPLATES: EMBEDDED_TEMPLATES$1 } = await import("@better-fullstack/template-generator");
783
2126
  const config = buildProjectConfig(input);
784
- const result = await generateVirtualProject({
2127
+ const result = await generateVirtualProject$1({
785
2128
  config,
786
- templates: EMBEDDED_TEMPLATES
2129
+ templates: EMBEDDED_TEMPLATES$1
787
2130
  });
788
2131
  if (result.success && result.tree) {
789
2132
  const summary = summarizeTree(result.tree);
790
2133
  const graphPreview = getMcpGraphPreview(config);
791
- return { content: [{
792
- type: "text",
793
- text: JSON.stringify({
794
- success: true,
795
- ...summary,
796
- ...graphPreview
797
- }, null, 2)
798
- }] };
2134
+ const payload = {
2135
+ success: true,
2136
+ ...summary,
2137
+ ...graphPreview
2138
+ };
2139
+ return {
2140
+ content: [{
2141
+ type: "text",
2142
+ text: JSON.stringify(payload, null, 2)
2143
+ }],
2144
+ structuredContent: payload
2145
+ };
799
2146
  }
800
2147
  return {
801
2148
  content: [{
@@ -817,21 +2164,34 @@ async function startMcpServer() {
817
2164
  };
818
2165
  }
819
2166
  });
820
- registerTool("bfs_create_project", "Creates a new fullstack project on disk. Dependencies are NOT installed (agent must tell user to install manually). Call bfs_plan_project first to preview.", mcpInputSchema({
821
- ...planCreateSchema,
822
- projectName: z.string().describe("Project name (kebab-case). Will be the directory name.")
823
- }), async (input) => {
2167
+ registerTool("bfs_create_project", {
2168
+ description: "Creates a new fullstack project on disk. Dependencies are NOT installed (agent must tell user to install manually). Call bfs_plan_project first to preview.",
2169
+ inputSchema: mcpInputSchema({
2170
+ ...MCP_PLAN_CREATE_SCHEMA,
2171
+ projectName: z.string().describe("Project name (kebab-case). Will be the directory name."),
2172
+ targetDir: z.string().optional().describe("Absolute path to the parent directory in which to create the project folder (default: current working directory).")
2173
+ }),
2174
+ outputSchema: createProjectOutputSchema,
2175
+ annotations: {
2176
+ title: "Create project",
2177
+ readOnlyHint: false,
2178
+ destructiveHint: false,
2179
+ idempotentHint: false,
2180
+ openWorldHint: false
2181
+ }
2182
+ }, async (input) => {
824
2183
  try {
825
- const { generateVirtualProject, EMBEDDED_TEMPLATES } = await import("@better-fullstack/template-generator");
826
- const { writeTreeToFilesystem } = await import("@better-fullstack/template-generator/fs-writer");
827
- const path = await import("node:path");
2184
+ const { generateVirtualProject: generateVirtualProject$1, EMBEDDED_TEMPLATES: EMBEDDED_TEMPLATES$1 } = await import("@better-fullstack/template-generator");
2185
+ const { writeTreeToFilesystem: writeTreeToFilesystem$1 } = await import("@better-fullstack/template-generator/fs-writer");
2186
+ const path$1 = await import("node:path");
828
2187
  const projectName = sanitizePath(input.projectName);
829
- const projectDir = path.resolve(process.cwd(), projectName);
2188
+ const targetDir = input.targetDir ? sanitizePath(input.targetDir) : void 0;
2189
+ const projectDir = path$1.resolve(targetDir ?? process.cwd(), projectName);
830
2190
  const config = buildProjectConfig(input, { projectDir });
831
2191
  await (await import("node:fs/promises")).mkdir(projectDir, { recursive: true });
832
- const result = await generateVirtualProject({
2192
+ const result = await generateVirtualProject$1({
833
2193
  config,
834
- templates: EMBEDDED_TEMPLATES
2194
+ templates: EMBEDDED_TEMPLATES$1
835
2195
  });
836
2196
  if (!result.success || !result.tree) return {
837
2197
  content: [{
@@ -843,26 +2203,30 @@ async function startMcpServer() {
843
2203
  }],
844
2204
  isError: true
845
2205
  };
846
- await writeTreeToFilesystem(result.tree, projectDir);
2206
+ await writeTreeToFilesystem$1(result.tree, projectDir);
847
2207
  await writeBtsConfig(config);
848
2208
  const graphPreview = getMcpGraphPreview(config);
849
2209
  let addonWarnings = [];
850
2210
  if (config.addons.length > 0 && config.addons[0] !== "none") {
851
- const { setupAddons } = await import("./addons-setup-CV5uZyhH.mjs");
2211
+ const { setupAddons } = await import("./addons-setup-C8eaCaH5.mjs");
852
2212
  addonWarnings = await setupAddons(config);
853
2213
  }
854
2214
  const installCmd = getInstallCommand(input.ecosystem ?? "typescript", projectName, input.packageManager, input.javaBuildTool, input.javaWebFramework);
855
- return { content: [{
856
- type: "text",
857
- text: JSON.stringify({
858
- success: true,
859
- projectDirectory: projectDir,
860
- fileCount: result.tree.fileCount,
861
- ...graphPreview,
862
- ...addonWarnings.length > 0 ? { addonWarnings } : {},
863
- message: `Project created at ${projectDir}. Tell the user to run: ${installCmd}`
864
- }, null, 2)
865
- }] };
2215
+ const payload = {
2216
+ success: true,
2217
+ projectDirectory: projectDir,
2218
+ fileCount: result.tree.fileCount,
2219
+ ...graphPreview,
2220
+ ...addonWarnings.length > 0 ? { addonWarnings } : {},
2221
+ message: `Project created at ${projectDir}. Tell the user to run: ${installCmd}`
2222
+ };
2223
+ return {
2224
+ content: [{
2225
+ type: "text",
2226
+ text: JSON.stringify(payload, null, 2)
2227
+ }],
2228
+ structuredContent: payload
2229
+ };
866
2230
  } catch (error) {
867
2231
  return {
868
2232
  content: [{
@@ -873,12 +2237,135 @@ async function startMcpServer() {
873
2237
  };
874
2238
  }
875
2239
  });
876
- registerTool("bfs_plan_addition", "Validates what would be added to an existing project. Reads the project config (bts.jsonc) and checks which addons are new.", mcpInputSchema({
877
- projectDir: z.string().describe("Absolute path to the existing project directory"),
878
- addons: z.array(AddonsSchema).optional().describe("Addons to add"),
879
- webDeploy: WebDeploySchema.optional().describe("Web deployment option"),
880
- serverDeploy: ServerDeploySchema.optional().describe("Server deployment option")
881
- }), async ({ projectDir, addons, webDeploy, serverDeploy }) => {
2240
+ function compatibilityWarningsForStackUpdate(proposedConfig) {
2241
+ const compatResult = analyzeStackCompatibility(buildCompatibilityInput(proposedConfig));
2242
+ return compatResult.changes.length > 0 ? compatResult.changes.map((change) => change.message) : void 0;
2243
+ }
2244
+ registerTool("bfs_plan_stack_update", {
2245
+ description: "Plans scaffold-time stack updates for an existing Better-Fullstack project. Supports the same stack fields as project creation (email, auth, payments, API, CMS, search, vector DB, observability, mobile/non-TS categories, addons, deploy, etc.). Does not write files.",
2246
+ inputSchema: mcpInputSchema(MCP_STACK_UPDATE_SCHEMA),
2247
+ outputSchema: stackUpdateOutputSchema,
2248
+ annotations: {
2249
+ title: "Plan stack update",
2250
+ readOnlyHint: true,
2251
+ idempotentHint: true,
2252
+ openWorldHint: false
2253
+ }
2254
+ }, async (input) => {
2255
+ try {
2256
+ const safePath = sanitizePath(input.projectDir);
2257
+ const { projectDir: _projectDir, projectName: _projectName, ...requestedChanges } = input;
2258
+ const plan = await planStackUpdate(safePath, requestedChanges);
2259
+ if (!plan.success) return {
2260
+ content: [{
2261
+ type: "text",
2262
+ text: JSON.stringify(plan, null, 2)
2263
+ }],
2264
+ structuredContent: plan,
2265
+ isError: true
2266
+ };
2267
+ const compatibilityWarnings = compatibilityWarningsForStackUpdate(plan.proposedConfig);
2268
+ const { operations: _operations, filesUnchanged: _filesUnchanged, ...safePlan } = plan;
2269
+ const payload = {
2270
+ ...safePlan,
2271
+ ...plan.compatibilityAdjustments.length > 0 ? { compatibilityAdjustments: plan.compatibilityAdjustments } : {},
2272
+ ...compatibilityWarnings ? { compatibilityWarnings } : {},
2273
+ 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}`
2274
+ };
2275
+ return {
2276
+ content: [{
2277
+ type: "text",
2278
+ text: JSON.stringify(payload, null, 2)
2279
+ }],
2280
+ structuredContent: payload
2281
+ };
2282
+ } catch (error) {
2283
+ const payload = {
2284
+ success: false,
2285
+ projectDir: input.projectDir,
2286
+ error: `Plan stack update failed: ${error instanceof Error ? error.message : String(error)}`
2287
+ };
2288
+ return {
2289
+ content: [{
2290
+ type: "text",
2291
+ text: JSON.stringify(payload, null, 2)
2292
+ }],
2293
+ structuredContent: payload,
2294
+ isError: true
2295
+ };
2296
+ }
2297
+ });
2298
+ registerTool("bfs_apply_stack_update", {
2299
+ description: "Applies a previously reviewed scaffold-time stack update to an existing Better-Fullstack project. Refuses to overwrite user-edited generated files and does not install dependencies.",
2300
+ inputSchema: mcpInputSchema(MCP_STACK_UPDATE_SCHEMA),
2301
+ outputSchema: stackUpdateOutputSchema,
2302
+ annotations: {
2303
+ title: "Apply stack update",
2304
+ readOnlyHint: false,
2305
+ destructiveHint: false,
2306
+ idempotentHint: false,
2307
+ openWorldHint: false
2308
+ }
2309
+ }, async (input) => {
2310
+ try {
2311
+ const safePath = sanitizePath(input.projectDir);
2312
+ const { projectDir: _projectDir, projectName: _projectName, ...requestedChanges } = input;
2313
+ const result = await applyStackUpdate(safePath, requestedChanges);
2314
+ if (!result.success) return {
2315
+ content: [{
2316
+ type: "text",
2317
+ text: JSON.stringify(result, null, 2)
2318
+ }],
2319
+ structuredContent: result,
2320
+ isError: true
2321
+ };
2322
+ const compatibilityWarnings = compatibilityWarningsForStackUpdate(result.proposedConfig);
2323
+ const { operations: _operations, filesUnchanged: _filesUnchanged, ...safeResult } = result;
2324
+ const payload = {
2325
+ ...safeResult,
2326
+ ...result.compatibilityAdjustments.length > 0 ? { compatibilityAdjustments: result.compatibilityAdjustments } : {},
2327
+ ...compatibilityWarnings ? { compatibilityWarnings } : {},
2328
+ message: `Stack update applied. Dependencies were not installed; run: ${result.installCommand}`
2329
+ };
2330
+ return {
2331
+ content: [{
2332
+ type: "text",
2333
+ text: JSON.stringify(payload, null, 2)
2334
+ }],
2335
+ structuredContent: payload
2336
+ };
2337
+ } catch (error) {
2338
+ const payload = {
2339
+ success: false,
2340
+ projectDir: input.projectDir,
2341
+ error: `Apply stack update failed: ${error instanceof Error ? error.message : String(error)}`
2342
+ };
2343
+ return {
2344
+ content: [{
2345
+ type: "text",
2346
+ text: JSON.stringify(payload, null, 2)
2347
+ }],
2348
+ structuredContent: payload,
2349
+ isError: true
2350
+ };
2351
+ }
2352
+ });
2353
+ registerTool("bfs_plan_addition", {
2354
+ description: "Validates what would be added to an existing project. Reads the project config (bts.jsonc) and checks which addons are new.",
2355
+ inputSchema: mcpInputSchema({
2356
+ projectDir: z.string().describe("Absolute path to the existing project directory"),
2357
+ addons: z.array(AddonsSchema).optional().describe("Addons to add"),
2358
+ webDeploy: WebDeploySchema.optional().describe("Web deployment option"),
2359
+ serverDeploy: ServerDeploySchema.optional().describe("Server deployment option")
2360
+ }),
2361
+ outputSchema: planAdditionOutputSchema,
2362
+ annotations: {
2363
+ title: "Plan feature addition",
2364
+ readOnlyHint: true,
2365
+ idempotentHint: true,
2366
+ openWorldHint: false
2367
+ }
2368
+ }, async ({ projectDir, addons, webDeploy, serverDeploy }) => {
882
2369
  try {
883
2370
  const safePath = sanitizePath(projectDir);
884
2371
  const config = await readBtsConfig(safePath);
@@ -909,31 +2396,35 @@ async function startMcpServer() {
909
2396
  serverDeploy: serverDeploy ?? config.serverDeploy
910
2397
  }));
911
2398
  const compatibilityWarnings = compatResult.changes.length > 0 ? compatResult.changes.map((c) => c.message) : void 0;
912
- return { content: [{
913
- type: "text",
914
- text: JSON.stringify({
915
- success: true,
916
- existingConfig: {
917
- ecosystem: config.ecosystem,
918
- frontend: config.frontend,
919
- backend: config.backend,
920
- addons: config.addons,
921
- graphSummary: existingGraphPreview.graphSummary,
922
- effectiveStack: existingGraphPreview.effectiveStack,
923
- stackPartSpecs: existingGraphPreview.stackPartSpecs
924
- },
925
- proposedAdditions: {
926
- newAddons,
927
- webDeploy: webDeploy ?? null,
928
- serverDeploy: serverDeploy ?? null,
929
- graphSummary: proposedGraphPreview.graphSummary,
930
- effectiveStack: proposedGraphPreview.effectiveStack,
931
- stackPartSpecs: proposedGraphPreview.stackPartSpecs
932
- },
933
- alreadyPresent: (addons ?? []).filter((a) => existingAddons.has(a)),
934
- ...compatibilityWarnings ? { compatibilityWarnings } : {}
935
- }, null, 2)
936
- }] };
2399
+ const payload = {
2400
+ success: true,
2401
+ existingConfig: {
2402
+ ecosystem: config.ecosystem,
2403
+ frontend: config.frontend,
2404
+ backend: config.backend,
2405
+ addons: config.addons,
2406
+ graphSummary: existingGraphPreview.graphSummary,
2407
+ effectiveStack: existingGraphPreview.effectiveStack,
2408
+ stackPartSpecs: existingGraphPreview.stackPartSpecs
2409
+ },
2410
+ proposedAdditions: {
2411
+ newAddons,
2412
+ webDeploy: webDeploy ?? null,
2413
+ serverDeploy: serverDeploy ?? null,
2414
+ graphSummary: proposedGraphPreview.graphSummary,
2415
+ effectiveStack: proposedGraphPreview.effectiveStack,
2416
+ stackPartSpecs: proposedGraphPreview.stackPartSpecs
2417
+ },
2418
+ alreadyPresent: (addons ?? []).filter((a) => existingAddons.has(a)),
2419
+ ...compatibilityWarnings ? { compatibilityWarnings } : {}
2420
+ };
2421
+ return {
2422
+ content: [{
2423
+ type: "text",
2424
+ text: JSON.stringify(payload, null, 2)
2425
+ }],
2426
+ structuredContent: payload
2427
+ };
937
2428
  } catch (error) {
938
2429
  return {
939
2430
  content: [{
@@ -944,13 +2435,24 @@ async function startMcpServer() {
944
2435
  };
945
2436
  }
946
2437
  });
947
- registerTool("bfs_add_feature", "Adds addons/features to an existing Better-Fullstack project. Dependencies are NOT installed. Call bfs_plan_addition first to validate.", mcpInputSchema({
948
- projectDir: z.string().describe("Absolute path to the existing project directory"),
949
- addons: z.array(AddonsSchema).optional().describe("Addons to add"),
950
- webDeploy: WebDeploySchema.optional().describe("Web deployment option"),
951
- serverDeploy: ServerDeploySchema.optional().describe("Server deployment option"),
952
- packageManager: PackageManagerSchema.optional().describe("Package manager to use")
953
- }), async (input) => {
2438
+ registerTool("bfs_add_feature", {
2439
+ description: "Adds addons/features to an existing Better-Fullstack project. Dependencies are NOT installed. Call bfs_plan_addition first to validate.",
2440
+ inputSchema: mcpInputSchema({
2441
+ projectDir: z.string().describe("Absolute path to the existing project directory"),
2442
+ addons: z.array(AddonsSchema).optional().describe("Addons to add"),
2443
+ webDeploy: WebDeploySchema.optional().describe("Web deployment option"),
2444
+ serverDeploy: ServerDeploySchema.optional().describe("Server deployment option"),
2445
+ packageManager: PackageManagerSchema.optional().describe("Package manager to use")
2446
+ }),
2447
+ outputSchema: addFeatureOutputSchema,
2448
+ annotations: {
2449
+ title: "Add feature",
2450
+ readOnlyHint: false,
2451
+ destructiveHint: false,
2452
+ idempotentHint: false,
2453
+ openWorldHint: false
2454
+ }
2455
+ }, async (input) => {
954
2456
  try {
955
2457
  const safePath = sanitizePath(input.projectDir);
956
2458
  const { add } = await import("./index.mjs");
@@ -966,16 +2468,20 @@ async function startMcpServer() {
966
2468
  const existingConfig = await readBtsConfig(safePath);
967
2469
  const graphPreview = existingConfig ? getMcpGraphPreview(existingConfig) : void 0;
968
2470
  const installCmd = getInstallCommand(existingConfig?.ecosystem ?? "typescript", safePath.split("/").pop() ?? "project", input.packageManager, existingConfig?.javaBuildTool, existingConfig?.javaWebFramework);
969
- return { content: [{
970
- type: "text",
971
- text: JSON.stringify({
972
- success: true,
973
- addedAddons: result.addedAddons,
974
- projectDir: result.projectDir,
975
- ...graphPreview,
976
- message: `Added ${result.addedAddons.join(", ")} to project. Tell the user to run: ${installCmd}`
977
- }, null, 2)
978
- }] };
2471
+ const payload = {
2472
+ success: true,
2473
+ addedAddons: result.addedAddons,
2474
+ projectDir: result.projectDir,
2475
+ ...graphPreview,
2476
+ message: `Added ${result.addedAddons.join(", ")} to project. Tell the user to run: ${installCmd}`
2477
+ };
2478
+ return {
2479
+ content: [{
2480
+ type: "text",
2481
+ text: JSON.stringify(payload, null, 2)
2482
+ }],
2483
+ structuredContent: payload
2484
+ };
979
2485
  }
980
2486
  return {
981
2487
  content: [{
@@ -1027,4 +2533,4 @@ async function startMcpServer() {
1027
2533
  startMcpServer();
1028
2534
 
1029
2535
  //#endregion
1030
- export { startMcpServer as n, getMcpGraphPreview as t };
2536
+ export { startMcpServer as i, MCP_STACK_UPDATE_SCHEMA as n, getMcpGraphPreview as r, MCP_PLAN_CREATE_SCHEMA as t };