create-better-fullstack 2.1.0 → 2.1.2

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,11 +1,902 @@
1
1
  #!/usr/bin/env node
2
- import { f as getEffectiveStack, g as getGraphSummary, n as previewBtsConfigUpdate, o as writeBtsConfig, r as readBtsConfig, s as getLatestCLIVersion } from "./bts-config-InNcw1aJ.mjs";
3
- import { n as getTemplateDescription, r as generateReproducibleCommand, t as getTemplateConfig } from "./templates-B351F1yX.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";
4
5
  import z from "zod";
5
- 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, StateManagementSchema, TEMPLATE_VALUES, TestingSchema, UILibrarySchema, ValidationSchema, VectorDbSchema, VersionChannelSchema, WebDeploySchema, analyzeStackCompatibility, evaluateCompatibility, 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";
6
11
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7
12
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
13
+ import { writeSelectedFiles } from "@better-fullstack/template-generator/fs-writer";
8
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
9
900
  //#region src/mcp.ts
10
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.
11
902
 
@@ -17,8 +908,9 @@ RECOMMENDED WORKFLOW:
17
908
  5. Call bfs_create_project to scaffold the project on disk.
18
909
 
19
910
  For existing projects:
20
- 1. Call bfs_plan_addition to validate proposed changes.
21
- 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.
22
914
 
23
915
  CRITICAL RULES:
24
916
  - Dependency installation is ALWAYS skipped in MCP mode (timeout risk). After scaffolding, tell the user to run install manually.
@@ -35,7 +927,8 @@ function getGuidance() {
35
927
  "Call bfs_check_compatibility to validate your planned stack before creation.",
36
928
  "Call bfs_plan_project to preview the generated project (dry-run, no files written).",
37
929
  "Call bfs_create_project to scaffold the project on disk.",
38
- "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."
39
932
  ],
40
933
  ecosystems: {
41
934
  typescript: "Full-featured web: frontend + backend + database + ORM + auth + payments + 20+ feature categories.",
@@ -120,14 +1013,7 @@ const MCP_SCHEMA_EXCLUDED_CATEGORIES = new Set([
120
1013
  "aiDocs",
121
1014
  "git",
122
1015
  "install",
123
- "versionChannel",
124
- "shadcnBase",
125
- "shadcnStyle",
126
- "shadcnIconLibrary",
127
- "shadcnColorTheme",
128
- "shadcnBaseColor",
129
- "shadcnFont",
130
- "shadcnRadius"
1016
+ "versionChannel"
131
1017
  ]);
132
1018
  const MCP_SCHEMA_OPTION_OVERRIDES = {
133
1019
  ecosystem: EcosystemSchema.options,
@@ -367,7 +1253,7 @@ function buildProjectConfig(input, overrides) {
367
1253
  const hasNativeFrontend = frontend.some((item) => item.startsWith("native-"));
368
1254
  const hasMobileProject = ecosystem === "react-native" || hasNativeFrontend;
369
1255
  const defaults = getMcpProjectConfigDefaults(input);
370
- return {
1256
+ const config = {
371
1257
  projectName,
372
1258
  projectDir: overrides?.projectDir ?? "/virtual",
373
1259
  relativePath: overrides ? `./${projectName}` : "./virtual",
@@ -379,17 +1265,22 @@ function buildProjectConfig(input, overrides) {
379
1265
  cssFramework: input.cssFramework ?? (ecosystem === "react-native" ? "none" : defaults.cssFramework),
380
1266
  mobileNavigation: input.mobileNavigation ?? (hasMobileProject ? "expo-router" : "none"),
381
1267
  mobileDeepLinking: input.mobileDeepLinking ?? (hasMobileProject ? "expo-linking" : "none"),
382
- shadcnBase: "radix",
383
- shadcnStyle: "nova",
384
- shadcnIconLibrary: "lucide",
385
- shadcnColorTheme: "neutral",
386
- shadcnBaseColor: "neutral",
387
- shadcnFont: "inter",
388
- shadcnRadius: "default",
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",
389
1275
  aiDocs: input.aiDocs ?? ["claude-md", "agents-md"],
390
1276
  git: !!overrides,
391
1277
  install: false
392
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;
393
1284
  }
394
1285
  function sanitizePath(input) {
395
1286
  for (const ch of input) if (ch.charCodeAt(0) < 32) throw new Error("Path contains control characters");
@@ -562,9 +1453,10 @@ const GETTING_STARTED_MD = `# Getting Started with Better-Fullstack MCP
562
1453
  2. Tell the user to run: cd my-elixir-app && mix deps.get && mix phx.server
563
1454
 
564
1455
  ## Adding Features to Existing Projects
565
- 1. Call bfs_add_feature with projectDir pointing to the project root.
566
- 2. Provide addons array with features to add (e.g., ["biome", "turborepo"]).
567
- 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.
568
1460
  `;
569
1461
  const guidanceOutputSchema = {
570
1462
  workflow: z.array(z.string()),
@@ -645,6 +1537,24 @@ const addFeatureOutputSchema = {
645
1537
  message: z.string().optional(),
646
1538
  ...graphPreviewOutputShape
647
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
+ };
648
1558
  function buildPresetStackSummary(config) {
649
1559
  const parts = [];
650
1560
  const frontend = (config.frontend ?? []).filter((item) => item !== "none");
@@ -839,6 +1749,154 @@ function summarizeRecommendedConfig(config) {
839
1749
  addons: config.addons
840
1750
  };
841
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
+ };
842
1900
  async function startMcpServer() {
843
1901
  const server = new McpServer({
844
1902
  name: "better-fullstack",
@@ -961,97 +2019,6 @@ async function startMcpServer() {
961
2019
  };
962
2020
  }
963
2021
  });
964
- const mobileInputSchema = {
965
- mobileNavigation: MobileNavigationSchema.optional().describe("Mobile navigation"),
966
- mobileUI: MobileUISchema.optional().describe("Mobile UI"),
967
- mobileStorage: MobileStorageSchema.optional().describe("Mobile storage"),
968
- mobileTesting: MobileTestingSchema.optional().describe("Mobile testing"),
969
- mobilePush: MobilePushSchema.optional().describe("Mobile push notifications"),
970
- mobileOTA: MobileOTASchema.optional().describe("Mobile OTA updates"),
971
- mobileDeepLinking: MobileDeepLinkingSchema.optional().describe("Mobile deep linking")
972
- };
973
- const deploymentInputSchema = {
974
- dbSetup: DatabaseSetupSchema.optional().describe("Database hosting provider"),
975
- webDeploy: WebDeploySchema.optional().describe("Web deployment target"),
976
- serverDeploy: ServerDeploySchema.optional().describe("Server deployment target")
977
- };
978
- const crossEcosystemInputSchema = {
979
- rustWebFramework: RustWebFrameworkSchema.optional().describe("Rust web framework"),
980
- rustFrontend: RustFrontendSchema.optional().describe("Rust frontend (WASM)"),
981
- rustOrm: RustOrmSchema.optional().describe("Rust ORM"),
982
- rustApi: RustApiSchema.optional().describe("Rust API layer"),
983
- rustCli: RustCliSchema.optional().describe("Rust CLI framework"),
984
- rustLibraries: z.array(RustLibrariesSchema).optional().describe("Rust libraries"),
985
- rustLogging: RustLoggingSchema.optional().describe("Rust logging library"),
986
- rustErrorHandling: RustErrorHandlingSchema.optional().describe("Rust error handling library"),
987
- rustCaching: RustCachingSchema.optional().describe("Rust caching library"),
988
- rustAuth: RustAuthSchema.optional().describe("Rust authentication library"),
989
- rustRealtime: RustRealtimeSchema.optional().describe("Rust realtime library"),
990
- rustMessageQueue: RustMessageQueueSchema.optional().describe("Rust message queue"),
991
- rustObservability: RustObservabilitySchema.optional().describe("Rust observability"),
992
- rustTemplating: RustTemplatingSchema.optional().describe("Rust template engine"),
993
- pythonWebFramework: PythonWebFrameworkSchema.optional().describe("Python web framework"),
994
- pythonOrm: PythonOrmSchema.optional().describe("Python ORM"),
995
- pythonValidation: PythonValidationSchema.optional().describe("Python validation"),
996
- pythonAi: z.array(PythonAiSchema).optional().describe("Python AI libraries"),
997
- pythonAuth: PythonAuthSchema.optional().describe("Python auth library"),
998
- pythonApi: PythonApiSchema.optional().describe("Python API framework"),
999
- pythonTaskQueue: PythonTaskQueueSchema.optional().describe("Python task queue"),
1000
- pythonGraphql: PythonGraphqlSchema.optional().describe("Python GraphQL framework"),
1001
- pythonQuality: PythonQualitySchema.optional().describe("Python code quality"),
1002
- pythonTesting: z.array(PythonTestingSchema).optional().describe("Python testing libraries"),
1003
- pythonCaching: PythonCachingSchema.optional().describe("Python caching library"),
1004
- pythonRealtime: PythonRealtimeSchema.optional().describe("Python realtime library"),
1005
- pythonObservability: PythonObservabilitySchema.optional().describe("Python observability"),
1006
- pythonCli: z.array(PythonCliSchema).optional().describe("Python CLI tooling"),
1007
- goWebFramework: GoWebFrameworkSchema.optional().describe("Go web framework"),
1008
- goOrm: GoOrmSchema.optional().describe("Go ORM"),
1009
- goApi: GoApiSchema.optional().describe("Go API layer"),
1010
- goCli: GoCliSchema.optional().describe("Go CLI framework"),
1011
- goLogging: GoLoggingSchema.optional().describe("Go logging library"),
1012
- goAuth: GoAuthSchema.optional().describe("Go authentication library"),
1013
- goTesting: z.array(GoTestingSchema).optional().describe("Go testing libraries"),
1014
- goRealtime: GoRealtimeSchema.optional().describe("Go realtime/WebSocket library"),
1015
- goMessageQueue: GoMessageQueueSchema.optional().describe("Go message queue"),
1016
- goCaching: GoCachingSchema.optional().describe("Go caching library"),
1017
- goConfig: GoConfigSchema.optional().describe("Go config management"),
1018
- goObservability: GoObservabilitySchema.optional().describe("Go observability"),
1019
- javaWebFramework: JavaWebFrameworkSchema.optional().describe("Java web framework"),
1020
- javaBuildTool: JavaBuildToolSchema.optional().describe("Java build tool"),
1021
- javaOrm: JavaOrmSchema.optional().describe("Java ORM"),
1022
- javaAuth: JavaAuthSchema.optional().describe("Java authentication library"),
1023
- javaApi: JavaApiSchema.optional().describe("Java API layer"),
1024
- javaLogging: JavaLoggingSchema.optional().describe("Java logging configuration"),
1025
- javaLibraries: z.array(JavaLibrariesSchema).optional().describe("Java application libraries"),
1026
- javaTestingLibraries: z.array(JavaTestingLibrariesSchema).optional().describe("Java testing libraries"),
1027
- dotnetWebFramework: DotnetWebFrameworkSchema.optional().describe(".NET web framework"),
1028
- dotnetOrm: DotnetOrmSchema.optional().describe(".NET ORM/data access"),
1029
- dotnetAuth: DotnetAuthSchema.optional().describe(".NET authentication library"),
1030
- dotnetApi: DotnetApiSchema.optional().describe(".NET API style"),
1031
- dotnetTesting: z.array(DotnetTestingSchema).optional().describe(".NET testing libraries"),
1032
- dotnetJobQueue: DotnetJobQueueSchema.optional().describe(".NET jobs and scheduling"),
1033
- dotnetRealtime: DotnetRealtimeSchema.optional().describe(".NET realtime feature"),
1034
- dotnetObservability: z.array(DotnetObservabilitySchema).optional().describe(".NET observability/logging libraries"),
1035
- dotnetValidation: DotnetValidationSchema.optional().describe(".NET validation"),
1036
- dotnetCaching: DotnetCachingSchema.optional().describe(".NET caching library"),
1037
- dotnetDeploy: DotnetDeploySchema.optional().describe(".NET deployment target"),
1038
- elixirWebFramework: ElixirWebFrameworkSchema.optional().describe("Elixir web framework"),
1039
- elixirOrm: ElixirOrmSchema.optional().describe("Elixir persistence layer"),
1040
- elixirAuth: ElixirAuthSchema.optional().describe("Elixir authentication"),
1041
- elixirApi: ElixirApiSchema.optional().describe("Elixir API layer"),
1042
- elixirRealtime: ElixirRealtimeSchema.optional().describe("Elixir realtime feature"),
1043
- elixirJobs: ElixirJobsSchema.optional().describe("Elixir jobs and scheduling"),
1044
- elixirValidation: ElixirValidationSchema.optional().describe("Elixir validation/data"),
1045
- elixirHttp: ElixirHttpSchema.optional().describe("Elixir HTTP client"),
1046
- elixirJson: ElixirJsonSchema.optional().describe("Elixir JSON library"),
1047
- elixirEmail: ElixirEmailSchema.optional().describe("Elixir email library"),
1048
- elixirCaching: ElixirCachingSchema.optional().describe("Elixir caching library"),
1049
- elixirObservability: ElixirObservabilitySchema.optional().describe("Elixir observability"),
1050
- elixirTesting: ElixirTestingSchema.optional().describe("Elixir testing library"),
1051
- elixirQuality: ElixirQualitySchema.optional().describe("Elixir code quality/security"),
1052
- elixirDeploy: ElixirDeploySchema.optional().describe("Elixir deployment target"),
1053
- elixirLibraries: z.array(ElixirLibrariesSchema).optional().describe("Elixir libraries")
1054
- };
1055
2022
  registerTool("bfs_check_compatibility", {
1056
2023
  description: "Validates a stack combination and returns auto-adjusted selections with warnings. Call BEFORE creating a project to avoid invalid combinations.",
1057
2024
  outputSchema: compatibilityOutputSchema,
@@ -1097,6 +2064,13 @@ async function startMcpServer() {
1097
2064
  astroIntegration: AstroIntegrationSchema.optional().describe("Astro UI framework integration"),
1098
2065
  uiLibrary: z.string().optional().describe("UI component library"),
1099
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"),
1100
2074
  addons: z.array(AddonsSchema).optional().describe("Addon list"),
1101
2075
  examples: z.array(ExamplesSchema).optional().describe("Example templates"),
1102
2076
  packageManager: PackageManagerSchema.optional().describe("Package manager"),
@@ -1136,54 +2110,9 @@ async function startMcpServer() {
1136
2110
  };
1137
2111
  }
1138
2112
  });
1139
- const planCreateSchema = {
1140
- projectName: z.string().optional().describe("Project name (kebab-case)"),
1141
- ecosystem: EcosystemSchema.optional().describe("Language ecosystem (default: typescript)"),
1142
- frontend: z.array(FrontendSchema).optional().describe("Frontend frameworks (TypeScript only)"),
1143
- backend: BackendSchema.optional().describe("Backend framework"),
1144
- runtime: RuntimeSchema.optional().describe("JavaScript runtime"),
1145
- database: DatabaseSchema.optional().describe("Database type"),
1146
- orm: ORMSchema.optional().describe("ORM"),
1147
- api: APISchema.optional().describe("API layer"),
1148
- auth: AuthSchema.optional().describe("Auth provider"),
1149
- payments: PaymentsSchema.optional().describe("Payments provider"),
1150
- email: EmailSchema.optional().describe("Email provider"),
1151
- addons: z.array(AddonsSchema).optional().describe("Addons"),
1152
- examples: z.array(ExamplesSchema).optional().describe("Example templates"),
1153
- packageManager: PackageManagerSchema.optional().describe("Package manager (default: bun)"),
1154
- cssFramework: CSSFrameworkSchema.optional().describe("CSS framework"),
1155
- uiLibrary: UILibrarySchema.optional().describe("UI component library"),
1156
- ai: AISchema.optional().describe("AI SDK"),
1157
- stateManagement: StateManagementSchema.optional().describe("State management"),
1158
- forms: FormsSchema.optional().describe("Forms library"),
1159
- validation: ValidationSchema.optional().describe("Validation library"),
1160
- testing: TestingSchema.optional().describe("Testing framework"),
1161
- realtime: RealtimeSchema.optional().describe("Realtime library"),
1162
- jobQueue: JobQueueSchema.optional().describe("Job queue"),
1163
- animation: AnimationSchema.optional().describe("Animation library"),
1164
- logging: LoggingSchema.optional().describe("Logging library"),
1165
- observability: ObservabilitySchema.optional().describe("Observability"),
1166
- featureFlags: FeatureFlagsSchema.optional().describe("Feature flag provider"),
1167
- search: SearchSchema.optional().describe("Search engine"),
1168
- vectorDb: VectorDbSchema.optional().describe("Vector database (TypeScript only)"),
1169
- caching: CachingSchema.optional().describe("Caching solution"),
1170
- rateLimit: RateLimitSchema.optional().describe("Rate limiting solution"),
1171
- i18n: I18nSchema.optional().describe("Internationalization (i18n) library"),
1172
- cms: CMSSchema.optional().describe("CMS"),
1173
- fileStorage: FileStorageSchema.optional().describe("File storage"),
1174
- ...mobileInputSchema,
1175
- fileUpload: FileUploadSchema.optional().describe("File upload"),
1176
- ...deploymentInputSchema,
1177
- effect: EffectSchema.optional().describe("Effect ecosystem (effect, effect-full)"),
1178
- analytics: AnalyticsSchema.optional().describe("Privacy-focused analytics provider"),
1179
- astroIntegration: AstroIntegrationSchema.optional().describe("Astro UI framework integration (react, vue, svelte, solid)"),
1180
- aiDocs: z.array(AiDocsSchema).optional().describe("AI documentation files (claude-md, agents-md, cursorrules)"),
1181
- versionChannel: VersionChannelSchema.optional().describe("Dependency version channel (stable, latest, beta)"),
1182
- ...crossEcosystemInputSchema
1183
- };
1184
2113
  registerTool("bfs_plan_project", {
1185
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.",
1186
- inputSchema: mcpInputSchema(planCreateSchema),
2115
+ inputSchema: mcpInputSchema(MCP_PLAN_CREATE_SCHEMA),
1187
2116
  outputSchema: planProjectOutputSchema,
1188
2117
  annotations: {
1189
2118
  title: "Plan project (dry run)",
@@ -1193,11 +2122,11 @@ async function startMcpServer() {
1193
2122
  }
1194
2123
  }, async (input) => {
1195
2124
  try {
1196
- 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");
1197
2126
  const config = buildProjectConfig(input);
1198
- const result = await generateVirtualProject({
2127
+ const result = await generateVirtualProject$1({
1199
2128
  config,
1200
- templates: EMBEDDED_TEMPLATES
2129
+ templates: EMBEDDED_TEMPLATES$1
1201
2130
  });
1202
2131
  if (result.success && result.tree) {
1203
2132
  const summary = summarizeTree(result.tree);
@@ -1238,7 +2167,7 @@ async function startMcpServer() {
1238
2167
  registerTool("bfs_create_project", {
1239
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.",
1240
2169
  inputSchema: mcpInputSchema({
1241
- ...planCreateSchema,
2170
+ ...MCP_PLAN_CREATE_SCHEMA,
1242
2171
  projectName: z.string().describe("Project name (kebab-case). Will be the directory name."),
1243
2172
  targetDir: z.string().optional().describe("Absolute path to the parent directory in which to create the project folder (default: current working directory).")
1244
2173
  }),
@@ -1252,17 +2181,17 @@ async function startMcpServer() {
1252
2181
  }
1253
2182
  }, async (input) => {
1254
2183
  try {
1255
- const { generateVirtualProject, EMBEDDED_TEMPLATES } = await import("@better-fullstack/template-generator");
1256
- const { writeTreeToFilesystem } = await import("@better-fullstack/template-generator/fs-writer");
1257
- 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");
1258
2187
  const projectName = sanitizePath(input.projectName);
1259
2188
  const targetDir = input.targetDir ? sanitizePath(input.targetDir) : void 0;
1260
- const projectDir = path.resolve(targetDir ?? process.cwd(), projectName);
2189
+ const projectDir = path$1.resolve(targetDir ?? process.cwd(), projectName);
1261
2190
  const config = buildProjectConfig(input, { projectDir });
1262
2191
  await (await import("node:fs/promises")).mkdir(projectDir, { recursive: true });
1263
- const result = await generateVirtualProject({
2192
+ const result = await generateVirtualProject$1({
1264
2193
  config,
1265
- templates: EMBEDDED_TEMPLATES
2194
+ templates: EMBEDDED_TEMPLATES$1
1266
2195
  });
1267
2196
  if (!result.success || !result.tree) return {
1268
2197
  content: [{
@@ -1274,12 +2203,12 @@ async function startMcpServer() {
1274
2203
  }],
1275
2204
  isError: true
1276
2205
  };
1277
- await writeTreeToFilesystem(result.tree, projectDir);
2206
+ await writeTreeToFilesystem$1(result.tree, projectDir);
1278
2207
  await writeBtsConfig(config);
1279
2208
  const graphPreview = getMcpGraphPreview(config);
1280
2209
  let addonWarnings = [];
1281
2210
  if (config.addons.length > 0 && config.addons[0] !== "none") {
1282
- const { setupAddons } = await import("./addons-setup-CSmj5ClW.mjs");
2211
+ const { setupAddons } = await import("./addons-setup-C8eaCaH5.mjs");
1283
2212
  addonWarnings = await setupAddons(config);
1284
2213
  }
1285
2214
  const installCmd = getInstallCommand(input.ecosystem ?? "typescript", projectName, input.packageManager, input.javaBuildTool, input.javaWebFramework);
@@ -1308,6 +2237,119 @@ async function startMcpServer() {
1308
2237
  };
1309
2238
  }
1310
2239
  });
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
+ });
1311
2353
  registerTool("bfs_plan_addition", {
1312
2354
  description: "Validates what would be added to an existing project. Reads the project config (bts.jsonc) and checks which addons are new.",
1313
2355
  inputSchema: mcpInputSchema({
@@ -1491,4 +2533,4 @@ async function startMcpServer() {
1491
2533
  startMcpServer();
1492
2534
 
1493
2535
  //#endregion
1494
- 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 };