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.
- package/dist/{add-handler-BQN6pwGJ.mjs → add-handler-9F-AsGM-.mjs} +11 -9
- package/dist/addons-setup-C8eaCaH5.mjs +5 -0
- package/dist/{addons-setup-HSghQS7c.mjs → addons-setup-C_xrNtkL.mjs} +3 -83
- package/dist/cli.mjs +2 -2
- package/dist/compatibility-rules-D7zYNVjC.mjs +372 -0
- package/dist/config-validation-C4glouQh.mjs +1512 -0
- package/dist/doctor-BFSSbS-U.mjs +280 -0
- package/dist/{bts-config-Bg1Qea9Y.mjs → errors-D9yiiGVq.mjs} +215 -72
- package/dist/generated-checks-DUvVXWId.mjs +91 -0
- package/dist/index.d.mts +85 -29
- package/dist/index.mjs +11 -6
- package/dist/{install-dependencies-D0Z1dZEx.mjs → install-dependencies-DDGF-zDG.mjs} +102 -397
- package/dist/mcp-BXLhb7wW.mjs +7 -0
- package/dist/mcp-entry.mjs +1802 -296
- package/dist/render-title-zvyKC1ej.mjs +34 -0
- package/dist/run-BmRKR2wG.mjs +11 -0
- package/dist/{run-BhcXBjxh.mjs → run-DRzP53v3.mjs} +534 -1717
- package/package.json +5 -4
- package/dist/addons-setup-CV5uZyhH.mjs +0 -5
- package/dist/mcp-58r70ZcL.mjs +0 -5
- package/dist/run-DCxVLUMS.mjs +0 -7
- /package/dist/{update-deps-CLebIM70.mjs → update-deps-D5OG0KmJ.mjs} +0 -0
package/dist/mcp-entry.mjs
CHANGED
|
@@ -1,10 +1,902 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
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
|
|
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
|
|
20
|
-
2. Call
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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
|
|
565
|
-
2.
|
|
566
|
-
3.
|
|
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 =
|
|
577
|
-
|
|
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 {
|
|
580
|
-
|
|
581
|
-
|
|
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",
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
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(
|
|
1971
|
+
text: JSON.stringify({ presets }, null, 2)
|
|
592
1972
|
}] };
|
|
593
1973
|
});
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
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
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
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",
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
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
|
|
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-
|
|
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
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
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
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
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
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
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",
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
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
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
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 };
|