create-better-fullstack 2.1.0 → 2.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{add-handler-BjTmOwEN.mjs → add-handler-9F-AsGM-.mjs} +4 -4
- package/dist/addons-setup-C8eaCaH5.mjs +5 -0
- package/dist/{addons-setup-uSvqHagW.mjs → addons-setup-C_xrNtkL.mjs} +1 -2
- 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-Bm9qvkya.mjs → doctor-BFSSbS-U.mjs} +2 -3
- package/dist/{bts-config-InNcw1aJ.mjs → errors-D9yiiGVq.mjs} +126 -5
- package/dist/{generated-checks-CUhFFfVo.mjs → generated-checks-DUvVXWId.mjs} +1 -1
- package/dist/index.d.mts +7 -7
- package/dist/index.mjs +7 -6
- package/dist/{install-dependencies-CDjTNvIV.mjs → install-dependencies-DDGF-zDG.mjs} +5 -371
- package/dist/mcp-BXLhb7wW.mjs +7 -0
- package/dist/mcp-entry.mjs +1217 -175
- package/dist/run-BmRKR2wG.mjs +11 -0
- package/dist/{run-Cqy-n1iT.mjs → run-DRzP53v3.mjs} +9 -1082
- package/package.json +3 -3
- package/dist/addons-setup-CSmj5ClW.mjs +0 -6
- package/dist/errors-ns_o2OKg.mjs +0 -86
- package/dist/mcp-BaLHDRdC.mjs +0 -5
- package/dist/run-DYWSKowD.mjs +0 -10
- package/dist/templates-B351F1yX.mjs +0 -489
package/dist/mcp-entry.mjs
CHANGED
|
@@ -1,11 +1,902 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
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";
|
|
4
5
|
import z from "zod";
|
|
5
|
-
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";
|
|
6
11
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7
12
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
13
|
+
import { writeSelectedFiles } from "@better-fullstack/template-generator/fs-writer";
|
|
8
14
|
|
|
15
|
+
//#region src/helpers/core/stack-update.ts
|
|
16
|
+
const ARRAY_UPDATE_KEYS = new Set([
|
|
17
|
+
"frontend",
|
|
18
|
+
"addons",
|
|
19
|
+
"examples",
|
|
20
|
+
"aiDocs",
|
|
21
|
+
"rustLibraries",
|
|
22
|
+
"pythonAi",
|
|
23
|
+
"pythonTesting",
|
|
24
|
+
"pythonCli",
|
|
25
|
+
"goTesting",
|
|
26
|
+
"javaLibraries",
|
|
27
|
+
"javaTestingLibraries",
|
|
28
|
+
"dotnetTesting",
|
|
29
|
+
"dotnetObservability",
|
|
30
|
+
"elixirLibraries"
|
|
31
|
+
]);
|
|
32
|
+
const NON_STACK_UPDATE_CREATE_KEYS = new Set([
|
|
33
|
+
"template",
|
|
34
|
+
"fromHistory",
|
|
35
|
+
"config",
|
|
36
|
+
"yes",
|
|
37
|
+
"yolo",
|
|
38
|
+
"verbose",
|
|
39
|
+
"dryRun",
|
|
40
|
+
"verify",
|
|
41
|
+
"git",
|
|
42
|
+
"install",
|
|
43
|
+
"directoryConflict",
|
|
44
|
+
"renderTitle",
|
|
45
|
+
"disableAnalytics",
|
|
46
|
+
"manualDb"
|
|
47
|
+
]);
|
|
48
|
+
const SUPPORTED_STACK_UPDATE_KEYS = Object.keys(CreateCommandOptionsSchema.shape).filter((key) => !NON_STACK_UPDATE_CREATE_KEYS.has(key)).sort();
|
|
49
|
+
const SUPPORTED_STACK_UPDATE_KEY_SET = new Set(SUPPORTED_STACK_UPDATE_KEYS);
|
|
50
|
+
const IGNORED_REQUEST_KEYS = new Set([
|
|
51
|
+
"projectDir",
|
|
52
|
+
"projectName",
|
|
53
|
+
"install",
|
|
54
|
+
"git"
|
|
55
|
+
]);
|
|
56
|
+
const PACKAGE_JSON_SECTIONS = [
|
|
57
|
+
"dependencies",
|
|
58
|
+
"devDependencies",
|
|
59
|
+
"peerDependencies",
|
|
60
|
+
"scripts"
|
|
61
|
+
];
|
|
62
|
+
const BINARY_FILE_MARKER = "[Binary file]";
|
|
63
|
+
function isEnvFilePath(filePath) {
|
|
64
|
+
const name = path.basename(filePath);
|
|
65
|
+
return name === ".env" || name.endsWith(".env.example");
|
|
66
|
+
}
|
|
67
|
+
function isSkippableGeneratedDoc(filePath) {
|
|
68
|
+
return path.basename(filePath).toLowerCase() === "readme.md";
|
|
69
|
+
}
|
|
70
|
+
function isGeneratedBinaryFile(file) {
|
|
71
|
+
return file?.content === BINARY_FILE_MARKER && typeof file.sourcePath === "string";
|
|
72
|
+
}
|
|
73
|
+
async function readGeneratedFileBytes(tree, filePath, file) {
|
|
74
|
+
if (!isGeneratedBinaryFile(file)) return void 0;
|
|
75
|
+
const tempDir = await fs.mkdtemp(path.join(tmpdir(), "bfs-stack-update-binary-"));
|
|
76
|
+
try {
|
|
77
|
+
if (!(await writeSelectedFiles(tree, tempDir, (candidatePath) => candidatePath === filePath)).includes(filePath)) return void 0;
|
|
78
|
+
return await fs.readFile(path.join(tempDir, filePath));
|
|
79
|
+
} finally {
|
|
80
|
+
await fs.rm(tempDir, {
|
|
81
|
+
recursive: true,
|
|
82
|
+
force: true
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function buffersEqual(left, right) {
|
|
87
|
+
return Boolean(left && right && left.equals(right));
|
|
88
|
+
}
|
|
89
|
+
async function inferProjectName(projectDir) {
|
|
90
|
+
const packageJsonPath = path.join(projectDir, "package.json");
|
|
91
|
+
const packageJson = await fs.readJson(packageJsonPath).catch(() => null);
|
|
92
|
+
if (packageJson && typeof packageJson.name === "string" && packageJson.name.trim()) return packageJson.name.trim();
|
|
93
|
+
return path.basename(projectDir);
|
|
94
|
+
}
|
|
95
|
+
function configFromBtsConfig(config, projectDir, projectName) {
|
|
96
|
+
return {
|
|
97
|
+
...getDefaultConfig(),
|
|
98
|
+
...config,
|
|
99
|
+
projectName,
|
|
100
|
+
projectDir,
|
|
101
|
+
relativePath: ".",
|
|
102
|
+
git: false,
|
|
103
|
+
install: false
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function buildRequestedChanges(input) {
|
|
107
|
+
const changes = {};
|
|
108
|
+
const stackPartSpecs = [];
|
|
109
|
+
const unsupportedKeys = [];
|
|
110
|
+
for (const [key, value] of Object.entries(input)) {
|
|
111
|
+
if (value === void 0 || IGNORED_REQUEST_KEYS.has(key)) continue;
|
|
112
|
+
if (!SUPPORTED_STACK_UPDATE_KEY_SET.has(key)) {
|
|
113
|
+
unsupportedKeys.push(key);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (key === "part") {
|
|
117
|
+
if (Array.isArray(value)) stackPartSpecs.push(...value.filter((item) => typeof item === "string"));
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
changes[key] = value;
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
changes,
|
|
124
|
+
stackPartSpecs,
|
|
125
|
+
unsupportedKeys
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function mergeProjectConfig(currentConfig, requestedChanges) {
|
|
129
|
+
const next = { ...currentConfig };
|
|
130
|
+
for (const [key, value] of Object.entries(requestedChanges)) {
|
|
131
|
+
if (value === void 0) continue;
|
|
132
|
+
if (ARRAY_UPDATE_KEYS.has(key) && Array.isArray(value)) {
|
|
133
|
+
const arrayValue = value;
|
|
134
|
+
const requested = arrayValue.filter((item) => typeof item === "string" && item !== "none");
|
|
135
|
+
if (requested.length === 0 && arrayValue.includes("none")) next[key] = [];
|
|
136
|
+
else {
|
|
137
|
+
const existing = Array.isArray(next[key]) ? next[key].filter((item) => item !== "none") : [];
|
|
138
|
+
next[key] = [...new Set([...existing, ...requested])];
|
|
139
|
+
}
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
next[key] = value;
|
|
143
|
+
}
|
|
144
|
+
return next;
|
|
145
|
+
}
|
|
146
|
+
function mergeStackPartSpecs(currentConfig, specs) {
|
|
147
|
+
if (specs.length === 0) return {};
|
|
148
|
+
const currentStackParts = currentConfig.stackParts?.length ? currentConfig.stackParts : (0, types_exports.legacyProjectConfigToStackParts)(currentConfig);
|
|
149
|
+
const currentSpecs = currentStackParts.filter((part) => part.source !== "provided").map((part) => (0, types_exports.formatStackPartSpec)(part, currentStackParts));
|
|
150
|
+
const stackParts = (0, types_exports.parseStackPartSpecs)([...new Set([...currentSpecs, ...specs])], "selected");
|
|
151
|
+
return {
|
|
152
|
+
...(0, types_exports.stackPartsToLegacyProjectConfigPartial)(stackParts),
|
|
153
|
+
stackParts
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
function asString(value, fallback = "none") {
|
|
157
|
+
return typeof value === "string" ? value : fallback;
|
|
158
|
+
}
|
|
159
|
+
function asStringArray(value) {
|
|
160
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
161
|
+
}
|
|
162
|
+
function getCompatibilityBackend(config, webFrontend) {
|
|
163
|
+
if (config.backend !== "self") return asString(config.backend, "hono");
|
|
164
|
+
if (webFrontend.includes("next")) return "self-next";
|
|
165
|
+
if (webFrontend.includes("vinext")) return "self-vinext";
|
|
166
|
+
if (webFrontend.includes("tanstack-start")) return "self-tanstack-start";
|
|
167
|
+
if (webFrontend.includes("astro")) return "self-astro";
|
|
168
|
+
if (webFrontend.includes("nuxt")) return "self-nuxt";
|
|
169
|
+
if (webFrontend.includes("svelte")) return "self-svelte";
|
|
170
|
+
if (webFrontend.includes("solid-start")) return "self-solid-start";
|
|
171
|
+
return "self";
|
|
172
|
+
}
|
|
173
|
+
function getProjectBackendFromCompatibility(backend) {
|
|
174
|
+
return backend.startsWith("self-") ? "self" : backend;
|
|
175
|
+
}
|
|
176
|
+
function getDefaultDatabaseForDbSetup(dbSetup) {
|
|
177
|
+
switch (dbSetup) {
|
|
178
|
+
case "turso":
|
|
179
|
+
case "d1": return "sqlite";
|
|
180
|
+
case "neon":
|
|
181
|
+
case "prisma-postgres":
|
|
182
|
+
case "planetscale":
|
|
183
|
+
case "supabase":
|
|
184
|
+
case "docker": return "postgres";
|
|
185
|
+
case "mongodb-atlas": return "mongodb";
|
|
186
|
+
case "upstash": return "redis";
|
|
187
|
+
default: return;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function getDefaultOrmForDatabase(database) {
|
|
191
|
+
switch (database) {
|
|
192
|
+
case "sqlite":
|
|
193
|
+
case "postgres":
|
|
194
|
+
case "mysql": return "drizzle";
|
|
195
|
+
case "mongodb": return "prisma";
|
|
196
|
+
default: return;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
function getRequiredCssFrameworkForUiLibrary(uiLibrary) {
|
|
200
|
+
switch (uiLibrary) {
|
|
201
|
+
case "shadcn-ui":
|
|
202
|
+
case "shadcn-svelte":
|
|
203
|
+
case "daisyui":
|
|
204
|
+
case "nextui":
|
|
205
|
+
case "park-ui": return "tailwind";
|
|
206
|
+
default: return;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
function isBetterAuth(auth) {
|
|
210
|
+
return auth === "better-auth" || auth === "better-auth-organizations";
|
|
211
|
+
}
|
|
212
|
+
function withNativeFrontendVariant(frontend, nativeFrontend) {
|
|
213
|
+
return [...(Array.isArray(frontend) ? frontend : []).filter((item) => !item.startsWith("native-") && item !== "none"), nativeFrontend];
|
|
214
|
+
}
|
|
215
|
+
function withWebFrontendVariant(frontend, webFrontend) {
|
|
216
|
+
return [webFrontend, ...(Array.isArray(frontend) ? frontend : []).filter((item) => item.startsWith("native-"))];
|
|
217
|
+
}
|
|
218
|
+
function hasRequestedNonNoneValue(requestedChanges, key) {
|
|
219
|
+
const value = requestedChanges[key];
|
|
220
|
+
if (Array.isArray(value)) return value.some((item) => item !== "none");
|
|
221
|
+
return value !== void 0 && value !== "none";
|
|
222
|
+
}
|
|
223
|
+
function needsStandaloneBackendForRequestedUpdate(requestedChanges) {
|
|
224
|
+
return hasRequestedNonNoneValue(requestedChanges, "database") || hasRequestedNonNoneValue(requestedChanges, "orm") || hasRequestedNonNoneValue(requestedChanges, "dbSetup") || hasRequestedNonNoneValue(requestedChanges, "api") || isBetterAuth(requestedChanges.auth) || hasRequestedNonNoneValue(requestedChanges, "payments") || hasRequestedNonNoneValue(requestedChanges, "email") || hasRequestedNonNoneValue(requestedChanges, "logging") || hasRequestedNonNoneValue(requestedChanges, "observability") || hasRequestedNonNoneValue(requestedChanges, "caching") || hasRequestedNonNoneValue(requestedChanges, "rateLimit") || hasRequestedNonNoneValue(requestedChanges, "jobQueue") || hasRequestedNonNoneValue(requestedChanges, "realtime") || hasRequestedNonNoneValue(requestedChanges, "search") || hasRequestedNonNoneValue(requestedChanges, "vectorDb") || hasRequestedNonNoneValue(requestedChanges, "fileStorage") || hasRequestedNonNoneValue(requestedChanges, "serverDeploy");
|
|
225
|
+
}
|
|
226
|
+
function getDefaultWebFrontendForRequestedUpdate(requestedChanges) {
|
|
227
|
+
if (hasRequestedNonNoneValue(requestedChanges, "payments")) return "next";
|
|
228
|
+
if (requestedChanges.i18n === "next-intl") return "next";
|
|
229
|
+
if (requestedChanges.cms === "payload" || requestedChanges.cms === "keystatic") return "next";
|
|
230
|
+
if (requestedChanges.uiLibrary === "shadcn-svelte") return "svelte";
|
|
231
|
+
if (asStringArray(requestedChanges.addons).some((addon) => addon === "pwa" || addon === "tauri")) return "react-vite";
|
|
232
|
+
return hasRequestedNonNoneValue(requestedChanges, "cssFramework") || hasRequestedNonNoneValue(requestedChanges, "uiLibrary") || hasRequestedNonNoneValue(requestedChanges, "forms") || hasRequestedNonNoneValue(requestedChanges, "stateManagement") || hasRequestedNonNoneValue(requestedChanges, "animation") || hasRequestedNonNoneValue(requestedChanges, "fileUpload") || hasRequestedNonNoneValue(requestedChanges, "i18n") || hasRequestedNonNoneValue(requestedChanges, "cms") || hasRequestedNonNoneValue(requestedChanges, "analytics") || hasRequestedNonNoneValue(requestedChanges, "webDeploy") || requestedChanges.ai === "tanstack-ai" ? "react-vite" : void 0;
|
|
233
|
+
}
|
|
234
|
+
function getDefaultNativeFrontendForRequestedUpdate(requestedChanges) {
|
|
235
|
+
if (requestedChanges.mobileUI === "uniwind" || requestedChanges.mobileUI === "unistyles") return;
|
|
236
|
+
return hasRequestedNonNoneValue(requestedChanges, "mobileNavigation") || hasRequestedNonNoneValue(requestedChanges, "mobileUI") || hasRequestedNonNoneValue(requestedChanges, "mobileStorage") || hasRequestedNonNoneValue(requestedChanges, "mobileTesting") || hasRequestedNonNoneValue(requestedChanges, "mobilePush") || hasRequestedNonNoneValue(requestedChanges, "mobileOTA") || hasRequestedNonNoneValue(requestedChanges, "mobileDeepLinking") ? "native-bare" : void 0;
|
|
237
|
+
}
|
|
238
|
+
function buildCompatibilityInputFromConfig(config) {
|
|
239
|
+
const frontend = asStringArray(config.frontend);
|
|
240
|
+
const addons = asStringArray(config.addons);
|
|
241
|
+
const webFrontend = frontend.filter((item) => !item.startsWith("native-") && item !== "none");
|
|
242
|
+
const nativeFrontend = frontend.filter((item) => item.startsWith("native-"));
|
|
243
|
+
const codeQuality = [];
|
|
244
|
+
const documentation = [];
|
|
245
|
+
const appPlatforms = [];
|
|
246
|
+
for (const addon of addons) {
|
|
247
|
+
const binding = (0, types_exports.getAddonStackPartBinding)(addon);
|
|
248
|
+
if (binding?.role === "codeQuality") codeQuality.push(addon);
|
|
249
|
+
else if (binding?.role === "documentation") documentation.push(addon);
|
|
250
|
+
else if (addon !== "none") appPlatforms.push(addon);
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
ecosystem: config.ecosystem,
|
|
254
|
+
projectName: config.projectName ?? null,
|
|
255
|
+
webFrontend,
|
|
256
|
+
nativeFrontend,
|
|
257
|
+
astroIntegration: asString(config.astroIntegration),
|
|
258
|
+
runtime: asString(config.runtime, "bun"),
|
|
259
|
+
backend: getCompatibilityBackend(config, webFrontend),
|
|
260
|
+
database: asString(config.database),
|
|
261
|
+
orm: asString(config.orm),
|
|
262
|
+
dbSetup: asString(config.dbSetup),
|
|
263
|
+
auth: asString(config.auth),
|
|
264
|
+
payments: asString(config.payments),
|
|
265
|
+
email: asString(config.email),
|
|
266
|
+
fileUpload: asString(config.fileUpload),
|
|
267
|
+
logging: asString(config.logging),
|
|
268
|
+
observability: asString(config.observability),
|
|
269
|
+
featureFlags: asString(config.featureFlags),
|
|
270
|
+
analytics: asString(config.analytics),
|
|
271
|
+
backendLibraries: "none",
|
|
272
|
+
stateManagement: asString(config.stateManagement),
|
|
273
|
+
forms: asString(config.forms),
|
|
274
|
+
validation: asString(config.validation),
|
|
275
|
+
testing: asString(config.testing),
|
|
276
|
+
realtime: asString(config.realtime),
|
|
277
|
+
jobQueue: asString(config.jobQueue),
|
|
278
|
+
caching: asString(config.caching),
|
|
279
|
+
rateLimit: asString(config.rateLimit),
|
|
280
|
+
animation: asString(config.animation),
|
|
281
|
+
cssFramework: asString(config.cssFramework),
|
|
282
|
+
uiLibrary: asString(config.uiLibrary),
|
|
283
|
+
shadcnBase: asString(config.shadcnBase, "radix"),
|
|
284
|
+
shadcnStyle: asString(config.shadcnStyle, "nova"),
|
|
285
|
+
shadcnIconLibrary: asString(config.shadcnIconLibrary, "lucide"),
|
|
286
|
+
shadcnColorTheme: asString(config.shadcnColorTheme, "neutral"),
|
|
287
|
+
shadcnBaseColor: asString(config.shadcnBaseColor, "neutral"),
|
|
288
|
+
shadcnFont: asString(config.shadcnFont, "inter"),
|
|
289
|
+
shadcnRadius: asString(config.shadcnRadius, "default"),
|
|
290
|
+
cms: asString(config.cms),
|
|
291
|
+
i18n: asString(config.i18n),
|
|
292
|
+
search: asString(config.search),
|
|
293
|
+
vectorDb: asString(config.vectorDb),
|
|
294
|
+
fileStorage: asString(config.fileStorage),
|
|
295
|
+
mobileNavigation: asString(config.mobileNavigation),
|
|
296
|
+
mobileUI: asString(config.mobileUI),
|
|
297
|
+
mobileStorage: asString(config.mobileStorage),
|
|
298
|
+
mobileTesting: asString(config.mobileTesting),
|
|
299
|
+
mobilePush: asString(config.mobilePush),
|
|
300
|
+
mobileOTA: asString(config.mobileOTA),
|
|
301
|
+
mobileDeepLinking: asString(config.mobileDeepLinking),
|
|
302
|
+
codeQuality,
|
|
303
|
+
documentation,
|
|
304
|
+
appPlatforms,
|
|
305
|
+
packageManager: asString(config.packageManager, "bun"),
|
|
306
|
+
versionChannel: asString(config.versionChannel, "stable"),
|
|
307
|
+
examples: asStringArray(config.examples),
|
|
308
|
+
aiSdk: asString(config.ai),
|
|
309
|
+
aiDocs: asStringArray(config.aiDocs),
|
|
310
|
+
git: "false",
|
|
311
|
+
install: "false",
|
|
312
|
+
api: asString(config.api),
|
|
313
|
+
webDeploy: asString(config.webDeploy),
|
|
314
|
+
serverDeploy: asString(config.serverDeploy),
|
|
315
|
+
yolo: "false",
|
|
316
|
+
rustWebFramework: asString(config.rustWebFramework),
|
|
317
|
+
rustFrontend: asString(config.rustFrontend),
|
|
318
|
+
rustOrm: asString(config.rustOrm),
|
|
319
|
+
rustApi: asString(config.rustApi),
|
|
320
|
+
rustCli: asString(config.rustCli),
|
|
321
|
+
rustLibraries: asStringArray(config.rustLibraries),
|
|
322
|
+
rustLogging: asString(config.rustLogging),
|
|
323
|
+
rustErrorHandling: asString(config.rustErrorHandling),
|
|
324
|
+
rustCaching: asString(config.rustCaching),
|
|
325
|
+
rustAuth: asString(config.rustAuth),
|
|
326
|
+
rustRealtime: asString(config.rustRealtime),
|
|
327
|
+
rustMessageQueue: asString(config.rustMessageQueue),
|
|
328
|
+
rustObservability: asString(config.rustObservability),
|
|
329
|
+
rustTemplating: asString(config.rustTemplating),
|
|
330
|
+
pythonWebFramework: asString(config.pythonWebFramework),
|
|
331
|
+
pythonOrm: asString(config.pythonOrm),
|
|
332
|
+
pythonValidation: asString(config.pythonValidation),
|
|
333
|
+
pythonAi: asStringArray(config.pythonAi),
|
|
334
|
+
pythonAuth: asString(config.pythonAuth),
|
|
335
|
+
pythonApi: asString(config.pythonApi),
|
|
336
|
+
pythonTaskQueue: asString(config.pythonTaskQueue),
|
|
337
|
+
pythonGraphql: asString(config.pythonGraphql),
|
|
338
|
+
pythonQuality: asString(config.pythonQuality),
|
|
339
|
+
pythonTesting: asStringArray(config.pythonTesting),
|
|
340
|
+
pythonCaching: asString(config.pythonCaching),
|
|
341
|
+
pythonRealtime: asString(config.pythonRealtime),
|
|
342
|
+
pythonObservability: asString(config.pythonObservability),
|
|
343
|
+
pythonCli: asStringArray(config.pythonCli),
|
|
344
|
+
goWebFramework: asString(config.goWebFramework),
|
|
345
|
+
goOrm: asString(config.goOrm),
|
|
346
|
+
goApi: asString(config.goApi),
|
|
347
|
+
goCli: asString(config.goCli),
|
|
348
|
+
goLogging: asString(config.goLogging),
|
|
349
|
+
goAuth: asString(config.goAuth),
|
|
350
|
+
goTesting: asStringArray(config.goTesting),
|
|
351
|
+
goRealtime: asString(config.goRealtime),
|
|
352
|
+
goMessageQueue: asString(config.goMessageQueue),
|
|
353
|
+
goCaching: asString(config.goCaching),
|
|
354
|
+
goConfig: asString(config.goConfig),
|
|
355
|
+
goObservability: asString(config.goObservability),
|
|
356
|
+
javaWebFramework: asString(config.javaWebFramework),
|
|
357
|
+
javaBuildTool: asString(config.javaBuildTool),
|
|
358
|
+
javaOrm: asString(config.javaOrm),
|
|
359
|
+
javaAuth: asString(config.javaAuth),
|
|
360
|
+
javaApi: asString(config.javaApi),
|
|
361
|
+
javaLogging: asString(config.javaLogging),
|
|
362
|
+
javaLibraries: asStringArray(config.javaLibraries),
|
|
363
|
+
javaTestingLibraries: asStringArray(config.javaTestingLibraries),
|
|
364
|
+
dotnetWebFramework: asString(config.dotnetWebFramework),
|
|
365
|
+
dotnetOrm: asString(config.dotnetOrm),
|
|
366
|
+
dotnetAuth: asString(config.dotnetAuth),
|
|
367
|
+
dotnetApi: asString(config.dotnetApi),
|
|
368
|
+
dotnetTesting: asStringArray(config.dotnetTesting),
|
|
369
|
+
dotnetJobQueue: asString(config.dotnetJobQueue),
|
|
370
|
+
dotnetRealtime: asString(config.dotnetRealtime),
|
|
371
|
+
dotnetObservability: asStringArray(config.dotnetObservability),
|
|
372
|
+
dotnetValidation: asString(config.dotnetValidation),
|
|
373
|
+
dotnetCaching: asString(config.dotnetCaching),
|
|
374
|
+
dotnetDeploy: asString(config.dotnetDeploy),
|
|
375
|
+
elixirWebFramework: asString(config.elixirWebFramework),
|
|
376
|
+
elixirOrm: asString(config.elixirOrm),
|
|
377
|
+
elixirAuth: asString(config.elixirAuth),
|
|
378
|
+
elixirApi: asString(config.elixirApi),
|
|
379
|
+
elixirRealtime: asString(config.elixirRealtime),
|
|
380
|
+
elixirJobs: asString(config.elixirJobs),
|
|
381
|
+
elixirValidation: asString(config.elixirValidation),
|
|
382
|
+
elixirHttp: asString(config.elixirHttp),
|
|
383
|
+
elixirJson: asString(config.elixirJson),
|
|
384
|
+
elixirEmail: asString(config.elixirEmail),
|
|
385
|
+
elixirCaching: asString(config.elixirCaching),
|
|
386
|
+
elixirObservability: asString(config.elixirObservability),
|
|
387
|
+
elixirTesting: asString(config.elixirTesting),
|
|
388
|
+
elixirQuality: asString(config.elixirQuality),
|
|
389
|
+
elixirDeploy: asString(config.elixirDeploy),
|
|
390
|
+
elixirLibraries: asStringArray(config.elixirLibraries)
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
function compatibilityChangesToProjectConfig(adjusted, baseConfig) {
|
|
394
|
+
const frontend = [...adjusted.webFrontend, ...adjusted.nativeFrontend];
|
|
395
|
+
const ignoredCompatibilityKeys = new Set([
|
|
396
|
+
"webFrontend",
|
|
397
|
+
"nativeFrontend",
|
|
398
|
+
"codeQuality",
|
|
399
|
+
"documentation",
|
|
400
|
+
"appPlatforms",
|
|
401
|
+
"aiSdk",
|
|
402
|
+
"backendLibraries",
|
|
403
|
+
"projectName",
|
|
404
|
+
"git",
|
|
405
|
+
"install",
|
|
406
|
+
"yolo"
|
|
407
|
+
]);
|
|
408
|
+
const changes = {};
|
|
409
|
+
for (const [key, value] of Object.entries(adjusted)) if (!ignoredCompatibilityKeys.has(key)) changes[key] = value;
|
|
410
|
+
changes.frontend = frontend.length > 0 ? frontend : baseConfig.frontend;
|
|
411
|
+
changes.addons = [
|
|
412
|
+
...adjusted.codeQuality,
|
|
413
|
+
...adjusted.documentation,
|
|
414
|
+
...adjusted.appPlatforms
|
|
415
|
+
];
|
|
416
|
+
changes.ai = adjusted.aiSdk;
|
|
417
|
+
changes.backend = getProjectBackendFromCompatibility(adjusted.backend);
|
|
418
|
+
return changes;
|
|
419
|
+
}
|
|
420
|
+
function applyKnownDependencyExpansions(config, requestedChanges) {
|
|
421
|
+
const next = { ...config };
|
|
422
|
+
const adjustments = [];
|
|
423
|
+
const requestedKeys = new Set(Object.keys(requestedChanges));
|
|
424
|
+
const hasWebFrontend = asStringArray(next.frontend).some((item) => !item.startsWith("native-") && item !== "none");
|
|
425
|
+
if (next.ecosystem === "typescript" && !hasWebFrontend && !requestedKeys.has("frontend")) {
|
|
426
|
+
const defaultWebFrontend = getDefaultWebFrontendForRequestedUpdate(requestedChanges);
|
|
427
|
+
if (defaultWebFrontend) {
|
|
428
|
+
next.frontend = withWebFrontendVariant(next.frontend, defaultWebFrontend);
|
|
429
|
+
adjustments.push(`frontend: Web frontend set to '${defaultWebFrontend}' (requested feature requires a web app)`);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
if (next.ecosystem === "typescript" && !requestedKeys.has("frontend")) {
|
|
433
|
+
const hasNativeFrontend = asStringArray(next.frontend).some((item) => item.startsWith("native-"));
|
|
434
|
+
const defaultNativeFrontend = getDefaultNativeFrontendForRequestedUpdate(requestedChanges);
|
|
435
|
+
if (defaultNativeFrontend && !hasNativeFrontend) {
|
|
436
|
+
next.frontend = withNativeFrontendVariant(next.frontend, defaultNativeFrontend);
|
|
437
|
+
adjustments.push(`mobile: Native frontend set to '${defaultNativeFrontend}' (requested feature requires a native app)`);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
if (next.ecosystem === "typescript" && next.backend === "none" && needsStandaloneBackendForRequestedUpdate(requestedChanges)) {
|
|
441
|
+
if (!requestedKeys.has("backend")) {
|
|
442
|
+
next.backend = "hono";
|
|
443
|
+
adjustments.push("backend: Backend set to 'hono' (requested feature requires a server)");
|
|
444
|
+
}
|
|
445
|
+
if (next.backend !== "none" && next.runtime === "none" && !requestedKeys.has("runtime")) {
|
|
446
|
+
next.runtime = "bun";
|
|
447
|
+
adjustments.push("backend: Runtime set to 'bun' (Hono server default)");
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
if (hasRequestedNonNoneValue(requestedChanges, "orm") && next.database === "none" && !requestedKeys.has("database")) {
|
|
451
|
+
next.database = "sqlite";
|
|
452
|
+
adjustments.push("orm: Database set to 'sqlite' (ORM requires a database)");
|
|
453
|
+
}
|
|
454
|
+
if (isBetterAuth(requestedChanges.auth) || requestedChanges.payments === "polar") {
|
|
455
|
+
if (requestedChanges.payments === "polar" && !isBetterAuth(next.auth) && !requestedKeys.has("auth")) {
|
|
456
|
+
next.auth = "better-auth";
|
|
457
|
+
adjustments.push("payments: Auth set to 'better-auth' (Polar requires Better Auth)");
|
|
458
|
+
}
|
|
459
|
+
if (next.database === "none" && !requestedKeys.has("database")) {
|
|
460
|
+
next.database = "sqlite";
|
|
461
|
+
const reason = requestedChanges.payments === "polar" ? "payments: Database set to 'sqlite' (Better Auth requires a SQL database)" : "auth: Database set to 'sqlite' (Better Auth requires a SQL database)";
|
|
462
|
+
adjustments.push(reason);
|
|
463
|
+
}
|
|
464
|
+
if (next.orm === "none" && !requestedKeys.has("orm")) {
|
|
465
|
+
next.orm = "drizzle";
|
|
466
|
+
const reason = requestedChanges.payments === "polar" ? "payments: ORM set to 'drizzle' (Better Auth requires an adapter)" : "auth: ORM set to 'drizzle' (Better Auth requires an adapter)";
|
|
467
|
+
adjustments.push(reason);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
if (requestedChanges.uiLibrary && requestedChanges.uiLibrary !== "none") {
|
|
471
|
+
const requiredCssFramework = getRequiredCssFrameworkForUiLibrary(requestedChanges.uiLibrary);
|
|
472
|
+
if (requiredCssFramework && next.cssFramework !== requiredCssFramework && !requestedKeys.has("cssFramework")) {
|
|
473
|
+
next.cssFramework = requiredCssFramework;
|
|
474
|
+
adjustments.push(`uiLibrary: CSS framework set to '${requiredCssFramework}' (${requestedChanges.uiLibrary} requires Tailwind CSS)`);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
if (requestedChanges.mobileUI === "uniwind" && !requestedKeys.has("frontend")) {
|
|
478
|
+
if (!asStringArray(next.frontend).includes("native-uniwind")) {
|
|
479
|
+
next.frontend = withNativeFrontendVariant(next.frontend, "native-uniwind");
|
|
480
|
+
adjustments.push("mobileUI: Native frontend set to 'native-uniwind' (Uniwind mobile UI requires Expo + Uniwind)");
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
if (requestedChanges.mobileUI === "unistyles" && !requestedKeys.has("frontend")) {
|
|
484
|
+
if (!asStringArray(next.frontend).includes("native-unistyles")) {
|
|
485
|
+
next.frontend = withNativeFrontendVariant(next.frontend, "native-unistyles");
|
|
486
|
+
adjustments.push("mobileUI: Native frontend set to 'native-unistyles' (Unistyles mobile UI requires Expo + Unistyles)");
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
if (requestedChanges.dbSetup && requestedChanges.dbSetup !== "none") {
|
|
490
|
+
const defaultDatabase = getDefaultDatabaseForDbSetup(requestedChanges.dbSetup);
|
|
491
|
+
if (defaultDatabase && next.database !== defaultDatabase && !requestedKeys.has("database")) {
|
|
492
|
+
next.database = defaultDatabase;
|
|
493
|
+
adjustments.push(`dbSetup: Database set to '${defaultDatabase}' (${requestedChanges.dbSetup} requires ${defaultDatabase})`);
|
|
494
|
+
}
|
|
495
|
+
const defaultOrm = getDefaultOrmForDatabase(next.database);
|
|
496
|
+
if (defaultOrm && next.orm === "none" && !requestedKeys.has("orm")) {
|
|
497
|
+
next.orm = defaultOrm;
|
|
498
|
+
adjustments.push(`dbSetup: ORM set to '${defaultOrm}' (${requestedChanges.dbSetup} requires a database adapter)`);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
if (requestedChanges.dbSetup === "d1") {
|
|
502
|
+
if (next.runtime !== "workers" && !requestedKeys.has("runtime")) {
|
|
503
|
+
next.runtime = "workers";
|
|
504
|
+
adjustments.push("dbSetup: Runtime set to 'workers' (D1 requires Workers)");
|
|
505
|
+
}
|
|
506
|
+
if (next.backend !== "hono" && !requestedKeys.has("backend")) {
|
|
507
|
+
next.backend = "hono";
|
|
508
|
+
adjustments.push("dbSetup: Backend set to 'hono' (Workers requires Hono)");
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
if (requestedChanges.serverDeploy === "cloudflare") {
|
|
512
|
+
if (next.backend === "hono" && next.runtime !== "workers" && !requestedKeys.has("runtime")) {
|
|
513
|
+
next.runtime = "workers";
|
|
514
|
+
adjustments.push("serverDeploy: Runtime set to 'workers' (Cloudflare requires Workers)");
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
if (requestedChanges.serverDeploy === "netlify") {
|
|
518
|
+
if (next.backend === "hono" && next.runtime !== "node" && !requestedKeys.has("runtime")) {
|
|
519
|
+
next.runtime = "node";
|
|
520
|
+
adjustments.push("serverDeploy: Runtime set to 'node' (Netlify Functions requires Node)");
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
if (asStringArray(next.examples).includes("chat-sdk")) {
|
|
524
|
+
if (getCompatibilityBackend(next, asStringArray(next.frontend).filter((item) => !item.startsWith("native-") && item !== "none")) === "hono" && next.runtime !== "node" && !requestedKeys.has("runtime")) {
|
|
525
|
+
next.runtime = "node";
|
|
526
|
+
adjustments.push("examples: Runtime set to 'node' (Chat SDK Hono profile requires Node)");
|
|
527
|
+
}
|
|
528
|
+
if ((0, types_exports.requiresChatSdkVercelAI)(buildCompatibilityInputFromConfig(next)) && next.ai !== "vercel-ai" && !requestedKeys.has("ai")) {
|
|
529
|
+
next.ai = "vercel-ai";
|
|
530
|
+
adjustments.push("examples: AI SDK set to 'vercel-ai' (Chat SDK profile requires Vercel AI)");
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
return {
|
|
534
|
+
config: next,
|
|
535
|
+
adjustments
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
async function generateTree(config) {
|
|
539
|
+
const result = await generateVirtualProject({
|
|
540
|
+
config,
|
|
541
|
+
templates: EMBEDDED_TEMPLATES
|
|
542
|
+
});
|
|
543
|
+
if (!result.success || !result.tree) throw new Error(result.error ?? "Failed to generate virtual project");
|
|
544
|
+
return result.tree;
|
|
545
|
+
}
|
|
546
|
+
function treeToFileMap(tree) {
|
|
547
|
+
const files = /* @__PURE__ */ new Map();
|
|
548
|
+
function walk(nodes) {
|
|
549
|
+
for (const node of nodes) if (node.type === "file") files.set(node.path, node);
|
|
550
|
+
else walk(node.children);
|
|
551
|
+
}
|
|
552
|
+
walk(tree.root.children);
|
|
553
|
+
return files;
|
|
554
|
+
}
|
|
555
|
+
function parseJson(content) {
|
|
556
|
+
if (!content) return null;
|
|
557
|
+
try {
|
|
558
|
+
const parsed = JSON.parse(content);
|
|
559
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
560
|
+
} catch {
|
|
561
|
+
return null;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
function stringifyJson(data) {
|
|
565
|
+
return `${JSON.stringify(data, null, 2)}\n`;
|
|
566
|
+
}
|
|
567
|
+
function isPlainObject(value) {
|
|
568
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
569
|
+
}
|
|
570
|
+
function diffJsonSection(current, previous, proposed, section) {
|
|
571
|
+
const previousSection = isPlainObject(previous[section]) ? previous[section] : {};
|
|
572
|
+
const proposedSection = isPlainObject(proposed[section]) ? proposed[section] : {};
|
|
573
|
+
const currentSection = isPlainObject(current[section]) ? current[section] : {};
|
|
574
|
+
const values = {};
|
|
575
|
+
const blockers = [];
|
|
576
|
+
for (const [name, proposedValue] of Object.entries(proposedSection)) {
|
|
577
|
+
if (previousSection[name] === proposedValue) continue;
|
|
578
|
+
const currentValue = currentSection[name];
|
|
579
|
+
if (currentValue !== void 0 && currentValue !== previousSection[name]) {
|
|
580
|
+
blockers.push(`${section}.${name}`);
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
values[name] = String(proposedValue);
|
|
584
|
+
}
|
|
585
|
+
return {
|
|
586
|
+
values,
|
|
587
|
+
blockers
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
function mergePackageJson(existingContent, previousContent, proposedContent) {
|
|
591
|
+
const existing = parseJson(existingContent);
|
|
592
|
+
const previous = parseJson(previousContent);
|
|
593
|
+
const proposed = parseJson(proposedContent);
|
|
594
|
+
if (!existing || !previous || !proposed) return {
|
|
595
|
+
summary: [],
|
|
596
|
+
blockers: ["package.json is not valid JSON or has no generated baseline"],
|
|
597
|
+
dependencyChanges: {},
|
|
598
|
+
scriptChanges: []
|
|
599
|
+
};
|
|
600
|
+
const next = structuredClone(existing);
|
|
601
|
+
const summary = [];
|
|
602
|
+
const blockers = [];
|
|
603
|
+
const dependencyChanges = {};
|
|
604
|
+
const scriptChanges = [];
|
|
605
|
+
for (const section of PACKAGE_JSON_SECTIONS) {
|
|
606
|
+
const diff = diffJsonSection(existing, previous, proposed, section);
|
|
607
|
+
blockers.push(...diff.blockers);
|
|
608
|
+
if (Object.keys(diff.values).length === 0) continue;
|
|
609
|
+
const target = isPlainObject(next[section]) ? { ...next[section] } : {};
|
|
610
|
+
for (const [name, value] of Object.entries(diff.values)) target[name] = value;
|
|
611
|
+
next[section] = Object.fromEntries(Object.entries(target).sort(([a], [b]) => a.localeCompare(b)));
|
|
612
|
+
summary.push(`${section}: ${Object.keys(diff.values).join(", ")}`);
|
|
613
|
+
if (section === "scripts") scriptChanges.push(...Object.keys(diff.values));
|
|
614
|
+
else dependencyChanges[section] = diff.values;
|
|
615
|
+
}
|
|
616
|
+
return {
|
|
617
|
+
content: blockers.length === 0 && summary.length > 0 ? stringifyJson(next) : void 0,
|
|
618
|
+
summary,
|
|
619
|
+
blockers,
|
|
620
|
+
dependencyChanges,
|
|
621
|
+
scriptChanges
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
function parseEnvKeys(content) {
|
|
625
|
+
const keys = /* @__PURE__ */ new Set();
|
|
626
|
+
for (const rawLine of content.split("\n")) {
|
|
627
|
+
const line = rawLine.trim();
|
|
628
|
+
if (!line || line.startsWith("#")) continue;
|
|
629
|
+
const eq = line.indexOf("=");
|
|
630
|
+
if (eq === -1) continue;
|
|
631
|
+
const key = line.slice(0, eq).trim();
|
|
632
|
+
if (key) keys.add(key);
|
|
633
|
+
}
|
|
634
|
+
return keys;
|
|
635
|
+
}
|
|
636
|
+
function mergeEnvExample(existingContent, previousContent, proposedContent) {
|
|
637
|
+
const existingKeys = parseEnvKeys(existingContent);
|
|
638
|
+
const previousKeys = previousContent ? parseEnvKeys(previousContent) : /* @__PURE__ */ new Set();
|
|
639
|
+
const nextLines = [];
|
|
640
|
+
const keys = [];
|
|
641
|
+
for (const line of proposedContent.split("\n")) {
|
|
642
|
+
const trimmed = line.trim();
|
|
643
|
+
const eq = trimmed.indexOf("=");
|
|
644
|
+
if (!trimmed || trimmed.startsWith("#") || eq === -1) continue;
|
|
645
|
+
const key = trimmed.slice(0, eq).trim();
|
|
646
|
+
if (!key || previousKeys.has(key) || existingKeys.has(key)) continue;
|
|
647
|
+
nextLines.push(line);
|
|
648
|
+
keys.push(key);
|
|
649
|
+
}
|
|
650
|
+
if (nextLines.length === 0) return { keys };
|
|
651
|
+
return {
|
|
652
|
+
content: `${existingContent}${existingContent.endsWith("\n") ? "" : "\n"}${nextLines.join("\n")}\n`,
|
|
653
|
+
keys
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
function collectEnvReferences(content) {
|
|
657
|
+
const keys = /* @__PURE__ */ new Set();
|
|
658
|
+
if (!content) return keys;
|
|
659
|
+
for (const pattern of [
|
|
660
|
+
/\bprocess\.env\.([A-Z][A-Z0-9_]*)/g,
|
|
661
|
+
/\bDeno\.env\.get\(\s*["']([A-Z][A-Z0-9_]*)["']/g,
|
|
662
|
+
/\bSystem\.getenv\(\s*["']([A-Z][A-Z0-9_]*)["']/g,
|
|
663
|
+
/\bstd::env::var\(\s*["']([A-Z][A-Z0-9_]*)["']/g,
|
|
664
|
+
/\bos\.Getenv\(\s*["']([A-Z][A-Z0-9_]*)["']/g,
|
|
665
|
+
/\bos\.environ\[\s*["']([A-Z][A-Z0-9_]*)["']\s*\]/g,
|
|
666
|
+
/\bos\.getenv\(\s*["']([A-Z][A-Z0-9_]*)["']/g,
|
|
667
|
+
/^([A-Z][A-Z0-9_]*)=/gm
|
|
668
|
+
]) for (const match of content.matchAll(pattern)) if (match[1]) keys.add(match[1]);
|
|
669
|
+
return keys;
|
|
670
|
+
}
|
|
671
|
+
function recordEnvReferenceChanges(envChanges, filePath, previousContent, proposedContent) {
|
|
672
|
+
const previous = collectEnvReferences(previousContent);
|
|
673
|
+
const added = [...collectEnvReferences(proposedContent)].filter((key) => !previous.has(key)).sort();
|
|
674
|
+
if (added.length > 0) envChanges[filePath] = [...new Set([...envChanges[filePath] ?? [], ...added])].sort();
|
|
675
|
+
}
|
|
676
|
+
function getInstallCommand$1(config) {
|
|
677
|
+
switch (config.ecosystem) {
|
|
678
|
+
case "rust": return "cargo build";
|
|
679
|
+
case "python": return "uv sync";
|
|
680
|
+
case "go": return "go mod tidy";
|
|
681
|
+
case "java": return config.javaBuildTool === "gradle" ? "./gradlew test" : "./mvnw test";
|
|
682
|
+
case "elixir": return "mix deps.get && mix compile";
|
|
683
|
+
default: return config.packageManager === "npm" ? "npm install" : `${config.packageManager} install`;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
function getGraphPreview(config) {
|
|
687
|
+
const stackParts = config.stackParts?.length ? config.stackParts : (0, types_exports.legacyProjectConfigToStackParts)(config);
|
|
688
|
+
const graphSummary = stackParts.length > 0 ? getGraphSummary({ stackParts }) : void 0;
|
|
689
|
+
const effectiveStack = stackParts.length > 0 ? getEffectiveStack({ stackParts }) : void 0;
|
|
690
|
+
const stackPartSpecs = stackParts.filter((part) => part.source !== "provided").map((part) => (0, types_exports.formatStackPartSpec)(part, stackParts));
|
|
691
|
+
return {
|
|
692
|
+
...graphSummary ? {
|
|
693
|
+
graphSummary,
|
|
694
|
+
effectiveStack
|
|
695
|
+
} : {},
|
|
696
|
+
stackPartSpecs
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
async function planStackUpdate(projectDirInput, input) {
|
|
700
|
+
const projectDir = path.resolve(projectDirInput);
|
|
701
|
+
const currentBtsConfig = await readBtsConfig(projectDir);
|
|
702
|
+
if (!currentBtsConfig) return {
|
|
703
|
+
success: false,
|
|
704
|
+
projectDir,
|
|
705
|
+
error: `No bts.jsonc found in ${projectDir}. Is this a Better-Fullstack project?`
|
|
706
|
+
};
|
|
707
|
+
const projectName = await inferProjectName(projectDir);
|
|
708
|
+
const currentConfig = configFromBtsConfig(currentBtsConfig, projectDir, projectName);
|
|
709
|
+
const { changes: requestedChanges, stackPartSpecs, unsupportedKeys } = buildRequestedChanges(input);
|
|
710
|
+
if (unsupportedKeys.length > 0) return {
|
|
711
|
+
success: false,
|
|
712
|
+
projectDir,
|
|
713
|
+
error: `Unsupported stack update field(s): ${unsupportedKeys.sort().join(", ")}`
|
|
714
|
+
};
|
|
715
|
+
let proposedConfig = mergeProjectConfig(currentConfig, requestedChanges);
|
|
716
|
+
const dependencyExpansion = applyKnownDependencyExpansions(proposedConfig, requestedChanges);
|
|
717
|
+
proposedConfig = dependencyExpansion.config;
|
|
718
|
+
const compatibilityResult = proposedConfig.ecosystem === "typescript" || proposedConfig.ecosystem === "react-native" ? (0, types_exports.analyzeStackCompatibility)(buildCompatibilityInputFromConfig(proposedConfig)) : {
|
|
719
|
+
adjustedStack: null,
|
|
720
|
+
changes: []
|
|
721
|
+
};
|
|
722
|
+
const compatibilityAdjustments = [...dependencyExpansion.adjustments, ...compatibilityResult.changes.map((change) => `${change.category}: ${change.message}`)];
|
|
723
|
+
if (compatibilityResult.adjustedStack) proposedConfig = mergeProjectConfig(proposedConfig, compatibilityChangesToProjectConfig(compatibilityResult.adjustedStack, proposedConfig));
|
|
724
|
+
proposedConfig.stackParts = (0, types_exports.legacyProjectConfigToStackParts)(proposedConfig);
|
|
725
|
+
Object.assign(proposedConfig, mergeStackPartSpecs(proposedConfig, stackPartSpecs));
|
|
726
|
+
try {
|
|
727
|
+
validateConfigForProgrammaticUse(proposedConfig);
|
|
728
|
+
} catch (error) {
|
|
729
|
+
return {
|
|
730
|
+
success: false,
|
|
731
|
+
projectDir,
|
|
732
|
+
error: `Invalid stack update: ${error instanceof Error ? error.message : String(error)}`
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
const persistedProposedConfig = buildBtsConfigForPersistence(proposedConfig, {
|
|
736
|
+
version: currentBtsConfig.version,
|
|
737
|
+
createdAt: currentBtsConfig.createdAt
|
|
738
|
+
});
|
|
739
|
+
const normalizedProposedConfig = configFromBtsConfig(persistedProposedConfig, projectDir, projectName);
|
|
740
|
+
let currentTree;
|
|
741
|
+
let proposedTree;
|
|
742
|
+
try {
|
|
743
|
+
[currentTree, proposedTree] = await Promise.all([generateTree(currentConfig), generateTree(normalizedProposedConfig)]);
|
|
744
|
+
} catch (error) {
|
|
745
|
+
return {
|
|
746
|
+
success: false,
|
|
747
|
+
projectDir,
|
|
748
|
+
error: `Failed to generate stack update plan: ${error instanceof Error ? error.message : String(error)}`
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
const currentGeneratedFiles = treeToFileMap(currentTree);
|
|
752
|
+
const proposedGeneratedFiles = treeToFileMap(proposedTree);
|
|
753
|
+
const operations = [];
|
|
754
|
+
const filesToAdd = [];
|
|
755
|
+
const filesToPatch = [];
|
|
756
|
+
const filesUnchanged = [];
|
|
757
|
+
const manualReviewBlockers = [];
|
|
758
|
+
const dependencyChanges = {};
|
|
759
|
+
const scriptChanges = {};
|
|
760
|
+
const envChanges = {};
|
|
761
|
+
const proposedFileEntries = await Promise.all([...proposedGeneratedFiles].map(async ([filePath, proposedFile]) => {
|
|
762
|
+
const existingPath = path.join(projectDir, filePath);
|
|
763
|
+
const exists = await fs.pathExists(existingPath);
|
|
764
|
+
return {
|
|
765
|
+
filePath,
|
|
766
|
+
proposedFile,
|
|
767
|
+
exists,
|
|
768
|
+
existingBuffer: exists ? await fs.readFile(existingPath).catch(() => void 0) : void 0
|
|
769
|
+
};
|
|
770
|
+
}));
|
|
771
|
+
for (const { filePath, proposedFile, exists, existingBuffer } of proposedFileEntries) {
|
|
772
|
+
const previousFile = currentGeneratedFiles.get(filePath);
|
|
773
|
+
const proposedContent = proposedFile.content;
|
|
774
|
+
const previousContent = previousFile?.content;
|
|
775
|
+
const isBinaryFile = isGeneratedBinaryFile(proposedFile) || isGeneratedBinaryFile(previousFile);
|
|
776
|
+
const existingContent = exists && existingBuffer && !isBinaryFile ? existingBuffer.toString("utf-8") : void 0;
|
|
777
|
+
if (!exists) {
|
|
778
|
+
filesToAdd.push(filePath);
|
|
779
|
+
operations.push({
|
|
780
|
+
kind: "add",
|
|
781
|
+
path: filePath,
|
|
782
|
+
writeMode: "generated"
|
|
783
|
+
});
|
|
784
|
+
if (isEnvFilePath(filePath)) envChanges[filePath] = [...parseEnvKeys(proposedContent)].sort();
|
|
785
|
+
recordEnvReferenceChanges(envChanges, filePath, void 0, proposedContent);
|
|
786
|
+
continue;
|
|
787
|
+
}
|
|
788
|
+
if (isBinaryFile) {
|
|
789
|
+
const [previousBinaryContent, proposedBinaryContent] = await Promise.all([readGeneratedFileBytes(currentTree, filePath, previousFile), readGeneratedFileBytes(proposedTree, filePath, proposedFile)]);
|
|
790
|
+
if (buffersEqual(existingBuffer, proposedBinaryContent)) {
|
|
791
|
+
filesUnchanged.push(filePath);
|
|
792
|
+
continue;
|
|
793
|
+
}
|
|
794
|
+
if (buffersEqual(existingBuffer, previousBinaryContent)) {
|
|
795
|
+
filesToPatch.push(filePath);
|
|
796
|
+
operations.push({
|
|
797
|
+
kind: "replace",
|
|
798
|
+
path: filePath,
|
|
799
|
+
writeMode: "generated"
|
|
800
|
+
});
|
|
801
|
+
continue;
|
|
802
|
+
}
|
|
803
|
+
manualReviewBlockers.push(`${filePath}: existing binary file differs from the generated baseline`);
|
|
804
|
+
continue;
|
|
805
|
+
}
|
|
806
|
+
if (existingContent === void 0 || existingContent === proposedContent) {
|
|
807
|
+
filesUnchanged.push(filePath);
|
|
808
|
+
continue;
|
|
809
|
+
}
|
|
810
|
+
if (filePath.endsWith("package.json")) {
|
|
811
|
+
const merged = mergePackageJson(existingContent, previousContent, proposedContent);
|
|
812
|
+
for (const blocker of merged.blockers) manualReviewBlockers.push(`${filePath}: ${blocker}`);
|
|
813
|
+
if (merged.content) {
|
|
814
|
+
filesToPatch.push(filePath);
|
|
815
|
+
operations.push({
|
|
816
|
+
kind: "merge",
|
|
817
|
+
path: filePath,
|
|
818
|
+
writeMode: "content",
|
|
819
|
+
content: merged.content,
|
|
820
|
+
summary: merged.summary
|
|
821
|
+
});
|
|
822
|
+
for (const [section, values] of Object.entries(merged.dependencyChanges)) dependencyChanges[`${filePath}:${section}`] = values;
|
|
823
|
+
if (merged.scriptChanges.length > 0) scriptChanges[filePath] = merged.scriptChanges;
|
|
824
|
+
}
|
|
825
|
+
continue;
|
|
826
|
+
}
|
|
827
|
+
if (isEnvFilePath(filePath)) {
|
|
828
|
+
const merged = mergeEnvExample(existingContent, previousContent, proposedContent);
|
|
829
|
+
if (merged.content) {
|
|
830
|
+
filesToPatch.push(filePath);
|
|
831
|
+
operations.push({
|
|
832
|
+
kind: "merge",
|
|
833
|
+
path: filePath,
|
|
834
|
+
writeMode: "content",
|
|
835
|
+
content: merged.content,
|
|
836
|
+
summary: [`env: ${merged.keys.join(", ")}`]
|
|
837
|
+
});
|
|
838
|
+
envChanges[filePath] = merged.keys;
|
|
839
|
+
}
|
|
840
|
+
continue;
|
|
841
|
+
}
|
|
842
|
+
if (previousContent !== void 0 && existingContent === previousContent) {
|
|
843
|
+
filesToPatch.push(filePath);
|
|
844
|
+
operations.push({
|
|
845
|
+
kind: "replace",
|
|
846
|
+
path: filePath,
|
|
847
|
+
writeMode: "generated"
|
|
848
|
+
});
|
|
849
|
+
recordEnvReferenceChanges(envChanges, filePath, previousContent, proposedContent);
|
|
850
|
+
continue;
|
|
851
|
+
}
|
|
852
|
+
if (isSkippableGeneratedDoc(filePath)) continue;
|
|
853
|
+
manualReviewBlockers.push(`${filePath}: existing file differs from the generated baseline`);
|
|
854
|
+
}
|
|
855
|
+
const graphPreview = getGraphPreview(persistedProposedConfig);
|
|
856
|
+
return {
|
|
857
|
+
success: true,
|
|
858
|
+
projectDir,
|
|
859
|
+
requestedChanges,
|
|
860
|
+
proposedConfig: persistedProposedConfig,
|
|
861
|
+
filesToAdd: [...new Set(filesToAdd)].sort(),
|
|
862
|
+
filesToPatch: [...new Set(filesToPatch)].sort(),
|
|
863
|
+
filesUnchanged: [...new Set(filesUnchanged)].sort(),
|
|
864
|
+
dependencyChanges,
|
|
865
|
+
scriptChanges,
|
|
866
|
+
envChanges,
|
|
867
|
+
manualReviewBlockers,
|
|
868
|
+
operations,
|
|
869
|
+
installCommand: getInstallCommand$1(normalizedProposedConfig),
|
|
870
|
+
compatibilityAdjustments,
|
|
871
|
+
...graphPreview
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
async function applyStackUpdate(projectDirInput, input) {
|
|
875
|
+
const plan = await planStackUpdate(projectDirInput, input);
|
|
876
|
+
if (!plan.success) return plan;
|
|
877
|
+
if (plan.manualReviewBlockers.length > 0) return {
|
|
878
|
+
success: false,
|
|
879
|
+
projectDir: plan.projectDir,
|
|
880
|
+
error: `Manual review required before applying stack update: ${plan.manualReviewBlockers.join("; ")}`
|
|
881
|
+
};
|
|
882
|
+
const projectName = await inferProjectName(plan.projectDir);
|
|
883
|
+
const proposedConfig = configFromBtsConfig(plan.proposedConfig, plan.projectDir, projectName);
|
|
884
|
+
const proposedTree = await generateTree(proposedConfig);
|
|
885
|
+
const generatedPaths = new Set(plan.operations.filter((operation) => operation.writeMode === "generated").map((operation) => operation.path));
|
|
886
|
+
if (generatedPaths.size > 0) await writeSelectedFiles(proposedTree, plan.projectDir, (filePath) => generatedPaths.has(filePath));
|
|
887
|
+
await Promise.all(plan.operations.filter((operation) => operation.writeMode === "content").map(async (operation) => {
|
|
888
|
+
const targetPath = path.join(plan.projectDir, operation.path);
|
|
889
|
+
await fs.ensureDir(path.dirname(targetPath));
|
|
890
|
+
await fs.writeFile(targetPath, operation.content, "utf-8");
|
|
891
|
+
}));
|
|
892
|
+
await writeBtsConfig(proposedConfig, {
|
|
893
|
+
version: plan.proposedConfig.version,
|
|
894
|
+
createdAt: plan.proposedConfig.createdAt
|
|
895
|
+
});
|
|
896
|
+
return plan;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
//#endregion
|
|
9
900
|
//#region src/mcp.ts
|
|
10
901
|
const INSTRUCTIONS = `Better-Fullstack scaffolds fullstack projects across TypeScript, React Native, Rust, Go, Python, Java, .NET, and Elixir ecosystems with ${Object.values(OPTION_CATEGORY_METADATA).reduce((sum, metadata) => sum + metadata.options.length, 0)} configurable options.
|
|
11
902
|
|
|
@@ -17,8 +908,9 @@ RECOMMENDED WORKFLOW:
|
|
|
17
908
|
5. Call bfs_create_project to scaffold the project on disk.
|
|
18
909
|
|
|
19
910
|
For existing projects:
|
|
20
|
-
1. Call
|
|
21
|
-
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.
|
|
22
914
|
|
|
23
915
|
CRITICAL RULES:
|
|
24
916
|
- Dependency installation is ALWAYS skipped in MCP mode (timeout risk). After scaffolding, tell the user to run install manually.
|
|
@@ -35,7 +927,8 @@ function getGuidance() {
|
|
|
35
927
|
"Call bfs_check_compatibility to validate your planned stack before creation.",
|
|
36
928
|
"Call bfs_plan_project to preview the generated project (dry-run, no files written).",
|
|
37
929
|
"Call bfs_create_project to scaffold the project on disk.",
|
|
38
|
-
"For existing projects: call
|
|
930
|
+
"For existing projects: call bfs_plan_stack_update, then bfs_apply_stack_update.",
|
|
931
|
+
"Use bfs_plan_addition / bfs_add_feature only for legacy addon/deploy-only flows."
|
|
39
932
|
],
|
|
40
933
|
ecosystems: {
|
|
41
934
|
typescript: "Full-featured web: frontend + backend + database + ORM + auth + payments + 20+ feature categories.",
|
|
@@ -120,14 +1013,7 @@ const MCP_SCHEMA_EXCLUDED_CATEGORIES = new Set([
|
|
|
120
1013
|
"aiDocs",
|
|
121
1014
|
"git",
|
|
122
1015
|
"install",
|
|
123
|
-
"versionChannel"
|
|
124
|
-
"shadcnBase",
|
|
125
|
-
"shadcnStyle",
|
|
126
|
-
"shadcnIconLibrary",
|
|
127
|
-
"shadcnColorTheme",
|
|
128
|
-
"shadcnBaseColor",
|
|
129
|
-
"shadcnFont",
|
|
130
|
-
"shadcnRadius"
|
|
1016
|
+
"versionChannel"
|
|
131
1017
|
]);
|
|
132
1018
|
const MCP_SCHEMA_OPTION_OVERRIDES = {
|
|
133
1019
|
ecosystem: EcosystemSchema.options,
|
|
@@ -367,7 +1253,7 @@ function buildProjectConfig(input, overrides) {
|
|
|
367
1253
|
const hasNativeFrontend = frontend.some((item) => item.startsWith("native-"));
|
|
368
1254
|
const hasMobileProject = ecosystem === "react-native" || hasNativeFrontend;
|
|
369
1255
|
const defaults = getMcpProjectConfigDefaults(input);
|
|
370
|
-
|
|
1256
|
+
const config = {
|
|
371
1257
|
projectName,
|
|
372
1258
|
projectDir: overrides?.projectDir ?? "/virtual",
|
|
373
1259
|
relativePath: overrides ? `./${projectName}` : "./virtual",
|
|
@@ -379,17 +1265,22 @@ function buildProjectConfig(input, overrides) {
|
|
|
379
1265
|
cssFramework: input.cssFramework ?? (ecosystem === "react-native" ? "none" : defaults.cssFramework),
|
|
380
1266
|
mobileNavigation: input.mobileNavigation ?? (hasMobileProject ? "expo-router" : "none"),
|
|
381
1267
|
mobileDeepLinking: input.mobileDeepLinking ?? (hasMobileProject ? "expo-linking" : "none"),
|
|
382
|
-
shadcnBase: "radix",
|
|
383
|
-
shadcnStyle: "nova",
|
|
384
|
-
shadcnIconLibrary: "lucide",
|
|
385
|
-
shadcnColorTheme: "neutral",
|
|
386
|
-
shadcnBaseColor: "neutral",
|
|
387
|
-
shadcnFont: "inter",
|
|
388
|
-
shadcnRadius: "default",
|
|
1268
|
+
shadcnBase: input.shadcnBase ?? "radix",
|
|
1269
|
+
shadcnStyle: input.shadcnStyle ?? "nova",
|
|
1270
|
+
shadcnIconLibrary: input.shadcnIconLibrary ?? "lucide",
|
|
1271
|
+
shadcnColorTheme: input.shadcnColorTheme ?? "neutral",
|
|
1272
|
+
shadcnBaseColor: input.shadcnBaseColor ?? "neutral",
|
|
1273
|
+
shadcnFont: input.shadcnFont ?? "inter",
|
|
1274
|
+
shadcnRadius: input.shadcnRadius ?? "default",
|
|
389
1275
|
aiDocs: input.aiDocs ?? ["claude-md", "agents-md"],
|
|
390
1276
|
git: !!overrides,
|
|
391
1277
|
install: false
|
|
392
1278
|
};
|
|
1279
|
+
if (Array.isArray(input.part) && input.part.length > 0) {
|
|
1280
|
+
const stackParts = parseStackPartSpecs(input.part.filter((part) => typeof part === "string"), "selected");
|
|
1281
|
+
Object.assign(config, stackPartsToLegacyProjectConfigPartial(stackParts), { stackParts });
|
|
1282
|
+
}
|
|
1283
|
+
return config;
|
|
393
1284
|
}
|
|
394
1285
|
function sanitizePath(input) {
|
|
395
1286
|
for (const ch of input) if (ch.charCodeAt(0) < 32) throw new Error("Path contains control characters");
|
|
@@ -562,9 +1453,10 @@ const GETTING_STARTED_MD = `# Getting Started with Better-Fullstack MCP
|
|
|
562
1453
|
2. Tell the user to run: cd my-elixir-app && mix deps.get && mix phx.server
|
|
563
1454
|
|
|
564
1455
|
## Adding Features to Existing Projects
|
|
565
|
-
1. Call
|
|
566
|
-
2.
|
|
567
|
-
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.
|
|
568
1460
|
`;
|
|
569
1461
|
const guidanceOutputSchema = {
|
|
570
1462
|
workflow: z.array(z.string()),
|
|
@@ -645,6 +1537,24 @@ const addFeatureOutputSchema = {
|
|
|
645
1537
|
message: z.string().optional(),
|
|
646
1538
|
...graphPreviewOutputShape
|
|
647
1539
|
};
|
|
1540
|
+
const stackUpdateOutputSchema = {
|
|
1541
|
+
success: z.boolean(),
|
|
1542
|
+
projectDir: z.string().optional(),
|
|
1543
|
+
error: z.string().optional(),
|
|
1544
|
+
requestedChanges: z.record(z.string(), z.unknown()).optional(),
|
|
1545
|
+
proposedConfig: z.record(z.string(), z.unknown()).optional(),
|
|
1546
|
+
filesToAdd: z.array(z.string()).optional(),
|
|
1547
|
+
filesToPatch: z.array(z.string()).optional(),
|
|
1548
|
+
dependencyChanges: z.record(z.string(), z.record(z.string(), z.string())).optional(),
|
|
1549
|
+
scriptChanges: z.record(z.string(), z.array(z.string())).optional(),
|
|
1550
|
+
envChanges: z.record(z.string(), z.array(z.string())).optional(),
|
|
1551
|
+
manualReviewBlockers: z.array(z.string()).optional(),
|
|
1552
|
+
compatibilityAdjustments: z.array(z.string()).optional(),
|
|
1553
|
+
compatibilityWarnings: z.array(z.string()).optional(),
|
|
1554
|
+
installCommand: z.string().optional(),
|
|
1555
|
+
message: z.string().optional(),
|
|
1556
|
+
...graphPreviewOutputShape
|
|
1557
|
+
};
|
|
648
1558
|
function buildPresetStackSummary(config) {
|
|
649
1559
|
const parts = [];
|
|
650
1560
|
const frontend = (config.frontend ?? []).filter((item) => item !== "none");
|
|
@@ -839,6 +1749,154 @@ function summarizeRecommendedConfig(config) {
|
|
|
839
1749
|
addons: config.addons
|
|
840
1750
|
};
|
|
841
1751
|
}
|
|
1752
|
+
const mobileInputSchema = {
|
|
1753
|
+
mobileNavigation: MobileNavigationSchema.optional().describe("Mobile navigation"),
|
|
1754
|
+
mobileUI: MobileUISchema.optional().describe("Mobile UI"),
|
|
1755
|
+
mobileStorage: MobileStorageSchema.optional().describe("Mobile storage"),
|
|
1756
|
+
mobileTesting: MobileTestingSchema.optional().describe("Mobile testing"),
|
|
1757
|
+
mobilePush: MobilePushSchema.optional().describe("Mobile push notifications"),
|
|
1758
|
+
mobileOTA: MobileOTASchema.optional().describe("Mobile OTA updates"),
|
|
1759
|
+
mobileDeepLinking: MobileDeepLinkingSchema.optional().describe("Mobile deep linking")
|
|
1760
|
+
};
|
|
1761
|
+
const deploymentInputSchema = {
|
|
1762
|
+
dbSetup: DatabaseSetupSchema.optional().describe("Database hosting provider"),
|
|
1763
|
+
webDeploy: WebDeploySchema.optional().describe("Web deployment target"),
|
|
1764
|
+
serverDeploy: ServerDeploySchema.optional().describe("Server deployment target")
|
|
1765
|
+
};
|
|
1766
|
+
const crossEcosystemInputSchema = {
|
|
1767
|
+
rustWebFramework: RustWebFrameworkSchema.optional().describe("Rust web framework"),
|
|
1768
|
+
rustFrontend: RustFrontendSchema.optional().describe("Rust frontend (WASM)"),
|
|
1769
|
+
rustOrm: RustOrmSchema.optional().describe("Rust ORM"),
|
|
1770
|
+
rustApi: RustApiSchema.optional().describe("Rust API layer"),
|
|
1771
|
+
rustCli: RustCliSchema.optional().describe("Rust CLI framework"),
|
|
1772
|
+
rustLibraries: z.array(RustLibrariesSchema).optional().describe("Rust libraries"),
|
|
1773
|
+
rustLogging: RustLoggingSchema.optional().describe("Rust logging library"),
|
|
1774
|
+
rustErrorHandling: RustErrorHandlingSchema.optional().describe("Rust error handling library"),
|
|
1775
|
+
rustCaching: RustCachingSchema.optional().describe("Rust caching library"),
|
|
1776
|
+
rustAuth: RustAuthSchema.optional().describe("Rust authentication library"),
|
|
1777
|
+
rustRealtime: RustRealtimeSchema.optional().describe("Rust realtime library"),
|
|
1778
|
+
rustMessageQueue: RustMessageQueueSchema.optional().describe("Rust message queue"),
|
|
1779
|
+
rustObservability: RustObservabilitySchema.optional().describe("Rust observability"),
|
|
1780
|
+
rustTemplating: RustTemplatingSchema.optional().describe("Rust template engine"),
|
|
1781
|
+
pythonWebFramework: PythonWebFrameworkSchema.optional().describe("Python web framework"),
|
|
1782
|
+
pythonOrm: PythonOrmSchema.optional().describe("Python ORM"),
|
|
1783
|
+
pythonValidation: PythonValidationSchema.optional().describe("Python validation"),
|
|
1784
|
+
pythonAi: z.array(PythonAiSchema).optional().describe("Python AI libraries"),
|
|
1785
|
+
pythonAuth: PythonAuthSchema.optional().describe("Python auth library"),
|
|
1786
|
+
pythonApi: PythonApiSchema.optional().describe("Python API framework"),
|
|
1787
|
+
pythonTaskQueue: PythonTaskQueueSchema.optional().describe("Python task queue"),
|
|
1788
|
+
pythonGraphql: PythonGraphqlSchema.optional().describe("Python GraphQL framework"),
|
|
1789
|
+
pythonQuality: PythonQualitySchema.optional().describe("Python code quality"),
|
|
1790
|
+
pythonTesting: z.array(PythonTestingSchema).optional().describe("Python testing libraries"),
|
|
1791
|
+
pythonCaching: PythonCachingSchema.optional().describe("Python caching library"),
|
|
1792
|
+
pythonRealtime: PythonRealtimeSchema.optional().describe("Python realtime library"),
|
|
1793
|
+
pythonObservability: PythonObservabilitySchema.optional().describe("Python observability"),
|
|
1794
|
+
pythonCli: z.array(PythonCliSchema).optional().describe("Python CLI tooling"),
|
|
1795
|
+
goWebFramework: GoWebFrameworkSchema.optional().describe("Go web framework"),
|
|
1796
|
+
goOrm: GoOrmSchema.optional().describe("Go ORM"),
|
|
1797
|
+
goApi: GoApiSchema.optional().describe("Go API layer"),
|
|
1798
|
+
goCli: GoCliSchema.optional().describe("Go CLI framework"),
|
|
1799
|
+
goLogging: GoLoggingSchema.optional().describe("Go logging library"),
|
|
1800
|
+
goAuth: GoAuthSchema.optional().describe("Go authentication library"),
|
|
1801
|
+
goTesting: z.array(GoTestingSchema).optional().describe("Go testing libraries"),
|
|
1802
|
+
goRealtime: GoRealtimeSchema.optional().describe("Go realtime/WebSocket library"),
|
|
1803
|
+
goMessageQueue: GoMessageQueueSchema.optional().describe("Go message queue"),
|
|
1804
|
+
goCaching: GoCachingSchema.optional().describe("Go caching library"),
|
|
1805
|
+
goConfig: GoConfigSchema.optional().describe("Go config management"),
|
|
1806
|
+
goObservability: GoObservabilitySchema.optional().describe("Go observability"),
|
|
1807
|
+
javaWebFramework: JavaWebFrameworkSchema.optional().describe("Java web framework"),
|
|
1808
|
+
javaBuildTool: JavaBuildToolSchema.optional().describe("Java build tool"),
|
|
1809
|
+
javaOrm: JavaOrmSchema.optional().describe("Java ORM"),
|
|
1810
|
+
javaAuth: JavaAuthSchema.optional().describe("Java authentication library"),
|
|
1811
|
+
javaApi: JavaApiSchema.optional().describe("Java API layer"),
|
|
1812
|
+
javaLogging: JavaLoggingSchema.optional().describe("Java logging configuration"),
|
|
1813
|
+
javaLibraries: z.array(JavaLibrariesSchema).optional().describe("Java application libraries"),
|
|
1814
|
+
javaTestingLibraries: z.array(JavaTestingLibrariesSchema).optional().describe("Java testing libraries"),
|
|
1815
|
+
dotnetWebFramework: DotnetWebFrameworkSchema.optional().describe(".NET web framework"),
|
|
1816
|
+
dotnetOrm: DotnetOrmSchema.optional().describe(".NET ORM/data access"),
|
|
1817
|
+
dotnetAuth: DotnetAuthSchema.optional().describe(".NET authentication library"),
|
|
1818
|
+
dotnetApi: DotnetApiSchema.optional().describe(".NET API style"),
|
|
1819
|
+
dotnetTesting: z.array(DotnetTestingSchema).optional().describe(".NET testing libraries"),
|
|
1820
|
+
dotnetJobQueue: DotnetJobQueueSchema.optional().describe(".NET jobs and scheduling"),
|
|
1821
|
+
dotnetRealtime: DotnetRealtimeSchema.optional().describe(".NET realtime feature"),
|
|
1822
|
+
dotnetObservability: z.array(DotnetObservabilitySchema).optional().describe(".NET observability/logging libraries"),
|
|
1823
|
+
dotnetValidation: DotnetValidationSchema.optional().describe(".NET validation"),
|
|
1824
|
+
dotnetCaching: DotnetCachingSchema.optional().describe(".NET caching library"),
|
|
1825
|
+
dotnetDeploy: DotnetDeploySchema.optional().describe(".NET deployment target"),
|
|
1826
|
+
elixirWebFramework: ElixirWebFrameworkSchema.optional().describe("Elixir web framework"),
|
|
1827
|
+
elixirOrm: ElixirOrmSchema.optional().describe("Elixir persistence layer"),
|
|
1828
|
+
elixirAuth: ElixirAuthSchema.optional().describe("Elixir authentication"),
|
|
1829
|
+
elixirApi: ElixirApiSchema.optional().describe("Elixir API layer"),
|
|
1830
|
+
elixirRealtime: ElixirRealtimeSchema.optional().describe("Elixir realtime feature"),
|
|
1831
|
+
elixirJobs: ElixirJobsSchema.optional().describe("Elixir jobs and scheduling"),
|
|
1832
|
+
elixirValidation: ElixirValidationSchema.optional().describe("Elixir validation/data"),
|
|
1833
|
+
elixirHttp: ElixirHttpSchema.optional().describe("Elixir HTTP client"),
|
|
1834
|
+
elixirJson: ElixirJsonSchema.optional().describe("Elixir JSON library"),
|
|
1835
|
+
elixirEmail: ElixirEmailSchema.optional().describe("Elixir email library"),
|
|
1836
|
+
elixirCaching: ElixirCachingSchema.optional().describe("Elixir caching library"),
|
|
1837
|
+
elixirObservability: ElixirObservabilitySchema.optional().describe("Elixir observability"),
|
|
1838
|
+
elixirTesting: ElixirTestingSchema.optional().describe("Elixir testing library"),
|
|
1839
|
+
elixirQuality: ElixirQualitySchema.optional().describe("Elixir code quality/security"),
|
|
1840
|
+
elixirDeploy: ElixirDeploySchema.optional().describe("Elixir deployment target"),
|
|
1841
|
+
elixirLibraries: z.array(ElixirLibrariesSchema).optional().describe("Elixir libraries")
|
|
1842
|
+
};
|
|
1843
|
+
const MCP_PLAN_CREATE_SCHEMA = {
|
|
1844
|
+
projectName: z.string().optional().describe("Project name (kebab-case)"),
|
|
1845
|
+
part: z.array(z.string()).optional().describe("Stack graph part binding, e.g. frontend:typescript:next or backend.orm:go:gorm"),
|
|
1846
|
+
ecosystem: EcosystemSchema.optional().describe("Language ecosystem (default: typescript)"),
|
|
1847
|
+
frontend: z.array(FrontendSchema).optional().describe("Frontend frameworks (TypeScript only)"),
|
|
1848
|
+
backend: BackendSchema.optional().describe("Backend framework"),
|
|
1849
|
+
runtime: RuntimeSchema.optional().describe("JavaScript runtime"),
|
|
1850
|
+
database: DatabaseSchema.optional().describe("Database type"),
|
|
1851
|
+
orm: ORMSchema.optional().describe("ORM"),
|
|
1852
|
+
api: APISchema.optional().describe("API layer"),
|
|
1853
|
+
auth: AuthSchema.optional().describe("Auth provider"),
|
|
1854
|
+
payments: PaymentsSchema.optional().describe("Payments provider"),
|
|
1855
|
+
email: EmailSchema.optional().describe("Email provider"),
|
|
1856
|
+
addons: z.array(AddonsSchema).optional().describe("Addons"),
|
|
1857
|
+
examples: z.array(ExamplesSchema).optional().describe("Example templates"),
|
|
1858
|
+
packageManager: PackageManagerSchema.optional().describe("Package manager (default: bun)"),
|
|
1859
|
+
cssFramework: CSSFrameworkSchema.optional().describe("CSS framework"),
|
|
1860
|
+
uiLibrary: UILibrarySchema.optional().describe("UI component library"),
|
|
1861
|
+
shadcnBase: ShadcnBaseSchema.optional().describe("shadcn/ui headless library"),
|
|
1862
|
+
shadcnStyle: ShadcnStyleSchema.optional().describe("shadcn/ui visual style"),
|
|
1863
|
+
shadcnIconLibrary: ShadcnIconLibrarySchema.optional().describe("shadcn/ui icon library"),
|
|
1864
|
+
shadcnColorTheme: ShadcnColorThemeSchema.optional().describe("shadcn/ui color theme"),
|
|
1865
|
+
shadcnBaseColor: ShadcnBaseColorSchema.optional().describe("shadcn/ui base neutral color"),
|
|
1866
|
+
shadcnFont: ShadcnFontSchema.optional().describe("shadcn/ui font"),
|
|
1867
|
+
shadcnRadius: ShadcnRadiusSchema.optional().describe("shadcn/ui border radius"),
|
|
1868
|
+
ai: AISchema.optional().describe("AI SDK"),
|
|
1869
|
+
stateManagement: StateManagementSchema.optional().describe("State management"),
|
|
1870
|
+
forms: FormsSchema.optional().describe("Forms library"),
|
|
1871
|
+
validation: ValidationSchema.optional().describe("Validation library"),
|
|
1872
|
+
testing: TestingSchema.optional().describe("Testing framework"),
|
|
1873
|
+
realtime: RealtimeSchema.optional().describe("Realtime library"),
|
|
1874
|
+
jobQueue: JobQueueSchema.optional().describe("Job queue"),
|
|
1875
|
+
animation: AnimationSchema.optional().describe("Animation library"),
|
|
1876
|
+
logging: LoggingSchema.optional().describe("Logging library"),
|
|
1877
|
+
observability: ObservabilitySchema.optional().describe("Observability"),
|
|
1878
|
+
featureFlags: FeatureFlagsSchema.optional().describe("Feature flag provider"),
|
|
1879
|
+
search: SearchSchema.optional().describe("Search engine"),
|
|
1880
|
+
vectorDb: VectorDbSchema.optional().describe("Vector database (TypeScript only)"),
|
|
1881
|
+
caching: CachingSchema.optional().describe("Caching solution"),
|
|
1882
|
+
rateLimit: RateLimitSchema.optional().describe("Rate limiting solution"),
|
|
1883
|
+
i18n: I18nSchema.optional().describe("Internationalization (i18n) library"),
|
|
1884
|
+
cms: CMSSchema.optional().describe("CMS"),
|
|
1885
|
+
fileStorage: FileStorageSchema.optional().describe("File storage"),
|
|
1886
|
+
...mobileInputSchema,
|
|
1887
|
+
fileUpload: FileUploadSchema.optional().describe("File upload"),
|
|
1888
|
+
...deploymentInputSchema,
|
|
1889
|
+
effect: EffectSchema.optional().describe("Effect ecosystem (effect, effect-full)"),
|
|
1890
|
+
analytics: AnalyticsSchema.optional().describe("Privacy-focused analytics provider"),
|
|
1891
|
+
astroIntegration: AstroIntegrationSchema.optional().describe("Astro UI framework integration (react, vue, svelte, solid)"),
|
|
1892
|
+
aiDocs: z.array(AiDocsSchema).optional().describe("AI documentation files (claude-md, agents-md, cursorrules)"),
|
|
1893
|
+
versionChannel: VersionChannelSchema.optional().describe("Dependency version channel (stable, latest, beta)"),
|
|
1894
|
+
...crossEcosystemInputSchema
|
|
1895
|
+
};
|
|
1896
|
+
const MCP_STACK_UPDATE_SCHEMA = {
|
|
1897
|
+
...MCP_PLAN_CREATE_SCHEMA,
|
|
1898
|
+
projectDir: z.string().describe("Absolute path to the existing Better-Fullstack project")
|
|
1899
|
+
};
|
|
842
1900
|
async function startMcpServer() {
|
|
843
1901
|
const server = new McpServer({
|
|
844
1902
|
name: "better-fullstack",
|
|
@@ -961,97 +2019,6 @@ async function startMcpServer() {
|
|
|
961
2019
|
};
|
|
962
2020
|
}
|
|
963
2021
|
});
|
|
964
|
-
const mobileInputSchema = {
|
|
965
|
-
mobileNavigation: MobileNavigationSchema.optional().describe("Mobile navigation"),
|
|
966
|
-
mobileUI: MobileUISchema.optional().describe("Mobile UI"),
|
|
967
|
-
mobileStorage: MobileStorageSchema.optional().describe("Mobile storage"),
|
|
968
|
-
mobileTesting: MobileTestingSchema.optional().describe("Mobile testing"),
|
|
969
|
-
mobilePush: MobilePushSchema.optional().describe("Mobile push notifications"),
|
|
970
|
-
mobileOTA: MobileOTASchema.optional().describe("Mobile OTA updates"),
|
|
971
|
-
mobileDeepLinking: MobileDeepLinkingSchema.optional().describe("Mobile deep linking")
|
|
972
|
-
};
|
|
973
|
-
const deploymentInputSchema = {
|
|
974
|
-
dbSetup: DatabaseSetupSchema.optional().describe("Database hosting provider"),
|
|
975
|
-
webDeploy: WebDeploySchema.optional().describe("Web deployment target"),
|
|
976
|
-
serverDeploy: ServerDeploySchema.optional().describe("Server deployment target")
|
|
977
|
-
};
|
|
978
|
-
const crossEcosystemInputSchema = {
|
|
979
|
-
rustWebFramework: RustWebFrameworkSchema.optional().describe("Rust web framework"),
|
|
980
|
-
rustFrontend: RustFrontendSchema.optional().describe("Rust frontend (WASM)"),
|
|
981
|
-
rustOrm: RustOrmSchema.optional().describe("Rust ORM"),
|
|
982
|
-
rustApi: RustApiSchema.optional().describe("Rust API layer"),
|
|
983
|
-
rustCli: RustCliSchema.optional().describe("Rust CLI framework"),
|
|
984
|
-
rustLibraries: z.array(RustLibrariesSchema).optional().describe("Rust libraries"),
|
|
985
|
-
rustLogging: RustLoggingSchema.optional().describe("Rust logging library"),
|
|
986
|
-
rustErrorHandling: RustErrorHandlingSchema.optional().describe("Rust error handling library"),
|
|
987
|
-
rustCaching: RustCachingSchema.optional().describe("Rust caching library"),
|
|
988
|
-
rustAuth: RustAuthSchema.optional().describe("Rust authentication library"),
|
|
989
|
-
rustRealtime: RustRealtimeSchema.optional().describe("Rust realtime library"),
|
|
990
|
-
rustMessageQueue: RustMessageQueueSchema.optional().describe("Rust message queue"),
|
|
991
|
-
rustObservability: RustObservabilitySchema.optional().describe("Rust observability"),
|
|
992
|
-
rustTemplating: RustTemplatingSchema.optional().describe("Rust template engine"),
|
|
993
|
-
pythonWebFramework: PythonWebFrameworkSchema.optional().describe("Python web framework"),
|
|
994
|
-
pythonOrm: PythonOrmSchema.optional().describe("Python ORM"),
|
|
995
|
-
pythonValidation: PythonValidationSchema.optional().describe("Python validation"),
|
|
996
|
-
pythonAi: z.array(PythonAiSchema).optional().describe("Python AI libraries"),
|
|
997
|
-
pythonAuth: PythonAuthSchema.optional().describe("Python auth library"),
|
|
998
|
-
pythonApi: PythonApiSchema.optional().describe("Python API framework"),
|
|
999
|
-
pythonTaskQueue: PythonTaskQueueSchema.optional().describe("Python task queue"),
|
|
1000
|
-
pythonGraphql: PythonGraphqlSchema.optional().describe("Python GraphQL framework"),
|
|
1001
|
-
pythonQuality: PythonQualitySchema.optional().describe("Python code quality"),
|
|
1002
|
-
pythonTesting: z.array(PythonTestingSchema).optional().describe("Python testing libraries"),
|
|
1003
|
-
pythonCaching: PythonCachingSchema.optional().describe("Python caching library"),
|
|
1004
|
-
pythonRealtime: PythonRealtimeSchema.optional().describe("Python realtime library"),
|
|
1005
|
-
pythonObservability: PythonObservabilitySchema.optional().describe("Python observability"),
|
|
1006
|
-
pythonCli: z.array(PythonCliSchema).optional().describe("Python CLI tooling"),
|
|
1007
|
-
goWebFramework: GoWebFrameworkSchema.optional().describe("Go web framework"),
|
|
1008
|
-
goOrm: GoOrmSchema.optional().describe("Go ORM"),
|
|
1009
|
-
goApi: GoApiSchema.optional().describe("Go API layer"),
|
|
1010
|
-
goCli: GoCliSchema.optional().describe("Go CLI framework"),
|
|
1011
|
-
goLogging: GoLoggingSchema.optional().describe("Go logging library"),
|
|
1012
|
-
goAuth: GoAuthSchema.optional().describe("Go authentication library"),
|
|
1013
|
-
goTesting: z.array(GoTestingSchema).optional().describe("Go testing libraries"),
|
|
1014
|
-
goRealtime: GoRealtimeSchema.optional().describe("Go realtime/WebSocket library"),
|
|
1015
|
-
goMessageQueue: GoMessageQueueSchema.optional().describe("Go message queue"),
|
|
1016
|
-
goCaching: GoCachingSchema.optional().describe("Go caching library"),
|
|
1017
|
-
goConfig: GoConfigSchema.optional().describe("Go config management"),
|
|
1018
|
-
goObservability: GoObservabilitySchema.optional().describe("Go observability"),
|
|
1019
|
-
javaWebFramework: JavaWebFrameworkSchema.optional().describe("Java web framework"),
|
|
1020
|
-
javaBuildTool: JavaBuildToolSchema.optional().describe("Java build tool"),
|
|
1021
|
-
javaOrm: JavaOrmSchema.optional().describe("Java ORM"),
|
|
1022
|
-
javaAuth: JavaAuthSchema.optional().describe("Java authentication library"),
|
|
1023
|
-
javaApi: JavaApiSchema.optional().describe("Java API layer"),
|
|
1024
|
-
javaLogging: JavaLoggingSchema.optional().describe("Java logging configuration"),
|
|
1025
|
-
javaLibraries: z.array(JavaLibrariesSchema).optional().describe("Java application libraries"),
|
|
1026
|
-
javaTestingLibraries: z.array(JavaTestingLibrariesSchema).optional().describe("Java testing libraries"),
|
|
1027
|
-
dotnetWebFramework: DotnetWebFrameworkSchema.optional().describe(".NET web framework"),
|
|
1028
|
-
dotnetOrm: DotnetOrmSchema.optional().describe(".NET ORM/data access"),
|
|
1029
|
-
dotnetAuth: DotnetAuthSchema.optional().describe(".NET authentication library"),
|
|
1030
|
-
dotnetApi: DotnetApiSchema.optional().describe(".NET API style"),
|
|
1031
|
-
dotnetTesting: z.array(DotnetTestingSchema).optional().describe(".NET testing libraries"),
|
|
1032
|
-
dotnetJobQueue: DotnetJobQueueSchema.optional().describe(".NET jobs and scheduling"),
|
|
1033
|
-
dotnetRealtime: DotnetRealtimeSchema.optional().describe(".NET realtime feature"),
|
|
1034
|
-
dotnetObservability: z.array(DotnetObservabilitySchema).optional().describe(".NET observability/logging libraries"),
|
|
1035
|
-
dotnetValidation: DotnetValidationSchema.optional().describe(".NET validation"),
|
|
1036
|
-
dotnetCaching: DotnetCachingSchema.optional().describe(".NET caching library"),
|
|
1037
|
-
dotnetDeploy: DotnetDeploySchema.optional().describe(".NET deployment target"),
|
|
1038
|
-
elixirWebFramework: ElixirWebFrameworkSchema.optional().describe("Elixir web framework"),
|
|
1039
|
-
elixirOrm: ElixirOrmSchema.optional().describe("Elixir persistence layer"),
|
|
1040
|
-
elixirAuth: ElixirAuthSchema.optional().describe("Elixir authentication"),
|
|
1041
|
-
elixirApi: ElixirApiSchema.optional().describe("Elixir API layer"),
|
|
1042
|
-
elixirRealtime: ElixirRealtimeSchema.optional().describe("Elixir realtime feature"),
|
|
1043
|
-
elixirJobs: ElixirJobsSchema.optional().describe("Elixir jobs and scheduling"),
|
|
1044
|
-
elixirValidation: ElixirValidationSchema.optional().describe("Elixir validation/data"),
|
|
1045
|
-
elixirHttp: ElixirHttpSchema.optional().describe("Elixir HTTP client"),
|
|
1046
|
-
elixirJson: ElixirJsonSchema.optional().describe("Elixir JSON library"),
|
|
1047
|
-
elixirEmail: ElixirEmailSchema.optional().describe("Elixir email library"),
|
|
1048
|
-
elixirCaching: ElixirCachingSchema.optional().describe("Elixir caching library"),
|
|
1049
|
-
elixirObservability: ElixirObservabilitySchema.optional().describe("Elixir observability"),
|
|
1050
|
-
elixirTesting: ElixirTestingSchema.optional().describe("Elixir testing library"),
|
|
1051
|
-
elixirQuality: ElixirQualitySchema.optional().describe("Elixir code quality/security"),
|
|
1052
|
-
elixirDeploy: ElixirDeploySchema.optional().describe("Elixir deployment target"),
|
|
1053
|
-
elixirLibraries: z.array(ElixirLibrariesSchema).optional().describe("Elixir libraries")
|
|
1054
|
-
};
|
|
1055
2022
|
registerTool("bfs_check_compatibility", {
|
|
1056
2023
|
description: "Validates a stack combination and returns auto-adjusted selections with warnings. Call BEFORE creating a project to avoid invalid combinations.",
|
|
1057
2024
|
outputSchema: compatibilityOutputSchema,
|
|
@@ -1097,6 +2064,13 @@ async function startMcpServer() {
|
|
|
1097
2064
|
astroIntegration: AstroIntegrationSchema.optional().describe("Astro UI framework integration"),
|
|
1098
2065
|
uiLibrary: z.string().optional().describe("UI component library"),
|
|
1099
2066
|
cssFramework: z.string().optional().describe("CSS framework"),
|
|
2067
|
+
shadcnBase: ShadcnBaseSchema.optional().describe("shadcn/ui headless library"),
|
|
2068
|
+
shadcnStyle: ShadcnStyleSchema.optional().describe("shadcn/ui visual style"),
|
|
2069
|
+
shadcnIconLibrary: ShadcnIconLibrarySchema.optional().describe("shadcn/ui icon library"),
|
|
2070
|
+
shadcnColorTheme: ShadcnColorThemeSchema.optional().describe("shadcn/ui color theme"),
|
|
2071
|
+
shadcnBaseColor: ShadcnBaseColorSchema.optional().describe("shadcn/ui base neutral color"),
|
|
2072
|
+
shadcnFont: ShadcnFontSchema.optional().describe("shadcn/ui font"),
|
|
2073
|
+
shadcnRadius: ShadcnRadiusSchema.optional().describe("shadcn/ui border radius"),
|
|
1100
2074
|
addons: z.array(AddonsSchema).optional().describe("Addon list"),
|
|
1101
2075
|
examples: z.array(ExamplesSchema).optional().describe("Example templates"),
|
|
1102
2076
|
packageManager: PackageManagerSchema.optional().describe("Package manager"),
|
|
@@ -1136,54 +2110,9 @@ async function startMcpServer() {
|
|
|
1136
2110
|
};
|
|
1137
2111
|
}
|
|
1138
2112
|
});
|
|
1139
|
-
const planCreateSchema = {
|
|
1140
|
-
projectName: z.string().optional().describe("Project name (kebab-case)"),
|
|
1141
|
-
ecosystem: EcosystemSchema.optional().describe("Language ecosystem (default: typescript)"),
|
|
1142
|
-
frontend: z.array(FrontendSchema).optional().describe("Frontend frameworks (TypeScript only)"),
|
|
1143
|
-
backend: BackendSchema.optional().describe("Backend framework"),
|
|
1144
|
-
runtime: RuntimeSchema.optional().describe("JavaScript runtime"),
|
|
1145
|
-
database: DatabaseSchema.optional().describe("Database type"),
|
|
1146
|
-
orm: ORMSchema.optional().describe("ORM"),
|
|
1147
|
-
api: APISchema.optional().describe("API layer"),
|
|
1148
|
-
auth: AuthSchema.optional().describe("Auth provider"),
|
|
1149
|
-
payments: PaymentsSchema.optional().describe("Payments provider"),
|
|
1150
|
-
email: EmailSchema.optional().describe("Email provider"),
|
|
1151
|
-
addons: z.array(AddonsSchema).optional().describe("Addons"),
|
|
1152
|
-
examples: z.array(ExamplesSchema).optional().describe("Example templates"),
|
|
1153
|
-
packageManager: PackageManagerSchema.optional().describe("Package manager (default: bun)"),
|
|
1154
|
-
cssFramework: CSSFrameworkSchema.optional().describe("CSS framework"),
|
|
1155
|
-
uiLibrary: UILibrarySchema.optional().describe("UI component library"),
|
|
1156
|
-
ai: AISchema.optional().describe("AI SDK"),
|
|
1157
|
-
stateManagement: StateManagementSchema.optional().describe("State management"),
|
|
1158
|
-
forms: FormsSchema.optional().describe("Forms library"),
|
|
1159
|
-
validation: ValidationSchema.optional().describe("Validation library"),
|
|
1160
|
-
testing: TestingSchema.optional().describe("Testing framework"),
|
|
1161
|
-
realtime: RealtimeSchema.optional().describe("Realtime library"),
|
|
1162
|
-
jobQueue: JobQueueSchema.optional().describe("Job queue"),
|
|
1163
|
-
animation: AnimationSchema.optional().describe("Animation library"),
|
|
1164
|
-
logging: LoggingSchema.optional().describe("Logging library"),
|
|
1165
|
-
observability: ObservabilitySchema.optional().describe("Observability"),
|
|
1166
|
-
featureFlags: FeatureFlagsSchema.optional().describe("Feature flag provider"),
|
|
1167
|
-
search: SearchSchema.optional().describe("Search engine"),
|
|
1168
|
-
vectorDb: VectorDbSchema.optional().describe("Vector database (TypeScript only)"),
|
|
1169
|
-
caching: CachingSchema.optional().describe("Caching solution"),
|
|
1170
|
-
rateLimit: RateLimitSchema.optional().describe("Rate limiting solution"),
|
|
1171
|
-
i18n: I18nSchema.optional().describe("Internationalization (i18n) library"),
|
|
1172
|
-
cms: CMSSchema.optional().describe("CMS"),
|
|
1173
|
-
fileStorage: FileStorageSchema.optional().describe("File storage"),
|
|
1174
|
-
...mobileInputSchema,
|
|
1175
|
-
fileUpload: FileUploadSchema.optional().describe("File upload"),
|
|
1176
|
-
...deploymentInputSchema,
|
|
1177
|
-
effect: EffectSchema.optional().describe("Effect ecosystem (effect, effect-full)"),
|
|
1178
|
-
analytics: AnalyticsSchema.optional().describe("Privacy-focused analytics provider"),
|
|
1179
|
-
astroIntegration: AstroIntegrationSchema.optional().describe("Astro UI framework integration (react, vue, svelte, solid)"),
|
|
1180
|
-
aiDocs: z.array(AiDocsSchema).optional().describe("AI documentation files (claude-md, agents-md, cursorrules)"),
|
|
1181
|
-
versionChannel: VersionChannelSchema.optional().describe("Dependency version channel (stable, latest, beta)"),
|
|
1182
|
-
...crossEcosystemInputSchema
|
|
1183
|
-
};
|
|
1184
2113
|
registerTool("bfs_plan_project", {
|
|
1185
2114
|
description: "Dry-run: generates a project in-memory and returns the file tree WITHOUT writing to disk. Use this to preview what would be created.",
|
|
1186
|
-
inputSchema: mcpInputSchema(
|
|
2115
|
+
inputSchema: mcpInputSchema(MCP_PLAN_CREATE_SCHEMA),
|
|
1187
2116
|
outputSchema: planProjectOutputSchema,
|
|
1188
2117
|
annotations: {
|
|
1189
2118
|
title: "Plan project (dry run)",
|
|
@@ -1193,11 +2122,11 @@ async function startMcpServer() {
|
|
|
1193
2122
|
}
|
|
1194
2123
|
}, async (input) => {
|
|
1195
2124
|
try {
|
|
1196
|
-
const { generateVirtualProject, EMBEDDED_TEMPLATES } = await import("@better-fullstack/template-generator");
|
|
2125
|
+
const { generateVirtualProject: generateVirtualProject$1, EMBEDDED_TEMPLATES: EMBEDDED_TEMPLATES$1 } = await import("@better-fullstack/template-generator");
|
|
1197
2126
|
const config = buildProjectConfig(input);
|
|
1198
|
-
const result = await generateVirtualProject({
|
|
2127
|
+
const result = await generateVirtualProject$1({
|
|
1199
2128
|
config,
|
|
1200
|
-
templates: EMBEDDED_TEMPLATES
|
|
2129
|
+
templates: EMBEDDED_TEMPLATES$1
|
|
1201
2130
|
});
|
|
1202
2131
|
if (result.success && result.tree) {
|
|
1203
2132
|
const summary = summarizeTree(result.tree);
|
|
@@ -1238,7 +2167,7 @@ async function startMcpServer() {
|
|
|
1238
2167
|
registerTool("bfs_create_project", {
|
|
1239
2168
|
description: "Creates a new fullstack project on disk. Dependencies are NOT installed (agent must tell user to install manually). Call bfs_plan_project first to preview.",
|
|
1240
2169
|
inputSchema: mcpInputSchema({
|
|
1241
|
-
...
|
|
2170
|
+
...MCP_PLAN_CREATE_SCHEMA,
|
|
1242
2171
|
projectName: z.string().describe("Project name (kebab-case). Will be the directory name."),
|
|
1243
2172
|
targetDir: z.string().optional().describe("Absolute path to the parent directory in which to create the project folder (default: current working directory).")
|
|
1244
2173
|
}),
|
|
@@ -1252,17 +2181,17 @@ async function startMcpServer() {
|
|
|
1252
2181
|
}
|
|
1253
2182
|
}, async (input) => {
|
|
1254
2183
|
try {
|
|
1255
|
-
const { generateVirtualProject, EMBEDDED_TEMPLATES } = await import("@better-fullstack/template-generator");
|
|
1256
|
-
const { writeTreeToFilesystem } = await import("@better-fullstack/template-generator/fs-writer");
|
|
1257
|
-
const path = await import("node:path");
|
|
2184
|
+
const { generateVirtualProject: generateVirtualProject$1, EMBEDDED_TEMPLATES: EMBEDDED_TEMPLATES$1 } = await import("@better-fullstack/template-generator");
|
|
2185
|
+
const { writeTreeToFilesystem: writeTreeToFilesystem$1 } = await import("@better-fullstack/template-generator/fs-writer");
|
|
2186
|
+
const path$1 = await import("node:path");
|
|
1258
2187
|
const projectName = sanitizePath(input.projectName);
|
|
1259
2188
|
const targetDir = input.targetDir ? sanitizePath(input.targetDir) : void 0;
|
|
1260
|
-
const projectDir = path.resolve(targetDir ?? process.cwd(), projectName);
|
|
2189
|
+
const projectDir = path$1.resolve(targetDir ?? process.cwd(), projectName);
|
|
1261
2190
|
const config = buildProjectConfig(input, { projectDir });
|
|
1262
2191
|
await (await import("node:fs/promises")).mkdir(projectDir, { recursive: true });
|
|
1263
|
-
const result = await generateVirtualProject({
|
|
2192
|
+
const result = await generateVirtualProject$1({
|
|
1264
2193
|
config,
|
|
1265
|
-
templates: EMBEDDED_TEMPLATES
|
|
2194
|
+
templates: EMBEDDED_TEMPLATES$1
|
|
1266
2195
|
});
|
|
1267
2196
|
if (!result.success || !result.tree) return {
|
|
1268
2197
|
content: [{
|
|
@@ -1274,12 +2203,12 @@ async function startMcpServer() {
|
|
|
1274
2203
|
}],
|
|
1275
2204
|
isError: true
|
|
1276
2205
|
};
|
|
1277
|
-
await writeTreeToFilesystem(result.tree, projectDir);
|
|
2206
|
+
await writeTreeToFilesystem$1(result.tree, projectDir);
|
|
1278
2207
|
await writeBtsConfig(config);
|
|
1279
2208
|
const graphPreview = getMcpGraphPreview(config);
|
|
1280
2209
|
let addonWarnings = [];
|
|
1281
2210
|
if (config.addons.length > 0 && config.addons[0] !== "none") {
|
|
1282
|
-
const { setupAddons } = await import("./addons-setup-
|
|
2211
|
+
const { setupAddons } = await import("./addons-setup-C8eaCaH5.mjs");
|
|
1283
2212
|
addonWarnings = await setupAddons(config);
|
|
1284
2213
|
}
|
|
1285
2214
|
const installCmd = getInstallCommand(input.ecosystem ?? "typescript", projectName, input.packageManager, input.javaBuildTool, input.javaWebFramework);
|
|
@@ -1308,6 +2237,119 @@ async function startMcpServer() {
|
|
|
1308
2237
|
};
|
|
1309
2238
|
}
|
|
1310
2239
|
});
|
|
2240
|
+
function compatibilityWarningsForStackUpdate(proposedConfig) {
|
|
2241
|
+
const compatResult = analyzeStackCompatibility(buildCompatibilityInput(proposedConfig));
|
|
2242
|
+
return compatResult.changes.length > 0 ? compatResult.changes.map((change) => change.message) : void 0;
|
|
2243
|
+
}
|
|
2244
|
+
registerTool("bfs_plan_stack_update", {
|
|
2245
|
+
description: "Plans scaffold-time stack updates for an existing Better-Fullstack project. Supports the same stack fields as project creation (email, auth, payments, API, CMS, search, vector DB, observability, mobile/non-TS categories, addons, deploy, etc.). Does not write files.",
|
|
2246
|
+
inputSchema: mcpInputSchema(MCP_STACK_UPDATE_SCHEMA),
|
|
2247
|
+
outputSchema: stackUpdateOutputSchema,
|
|
2248
|
+
annotations: {
|
|
2249
|
+
title: "Plan stack update",
|
|
2250
|
+
readOnlyHint: true,
|
|
2251
|
+
idempotentHint: true,
|
|
2252
|
+
openWorldHint: false
|
|
2253
|
+
}
|
|
2254
|
+
}, async (input) => {
|
|
2255
|
+
try {
|
|
2256
|
+
const safePath = sanitizePath(input.projectDir);
|
|
2257
|
+
const { projectDir: _projectDir, projectName: _projectName, ...requestedChanges } = input;
|
|
2258
|
+
const plan = await planStackUpdate(safePath, requestedChanges);
|
|
2259
|
+
if (!plan.success) return {
|
|
2260
|
+
content: [{
|
|
2261
|
+
type: "text",
|
|
2262
|
+
text: JSON.stringify(plan, null, 2)
|
|
2263
|
+
}],
|
|
2264
|
+
structuredContent: plan,
|
|
2265
|
+
isError: true
|
|
2266
|
+
};
|
|
2267
|
+
const compatibilityWarnings = compatibilityWarningsForStackUpdate(plan.proposedConfig);
|
|
2268
|
+
const { operations: _operations, filesUnchanged: _filesUnchanged, ...safePlan } = plan;
|
|
2269
|
+
const payload = {
|
|
2270
|
+
...safePlan,
|
|
2271
|
+
...plan.compatibilityAdjustments.length > 0 ? { compatibilityAdjustments: plan.compatibilityAdjustments } : {},
|
|
2272
|
+
...compatibilityWarnings ? { compatibilityWarnings } : {},
|
|
2273
|
+
message: plan.manualReviewBlockers.length > 0 ? "Plan created, but manual review is required before applying." : `Plan created. If approved, call bfs_apply_stack_update, then run: ${plan.installCommand}`
|
|
2274
|
+
};
|
|
2275
|
+
return {
|
|
2276
|
+
content: [{
|
|
2277
|
+
type: "text",
|
|
2278
|
+
text: JSON.stringify(payload, null, 2)
|
|
2279
|
+
}],
|
|
2280
|
+
structuredContent: payload
|
|
2281
|
+
};
|
|
2282
|
+
} catch (error) {
|
|
2283
|
+
const payload = {
|
|
2284
|
+
success: false,
|
|
2285
|
+
projectDir: input.projectDir,
|
|
2286
|
+
error: `Plan stack update failed: ${error instanceof Error ? error.message : String(error)}`
|
|
2287
|
+
};
|
|
2288
|
+
return {
|
|
2289
|
+
content: [{
|
|
2290
|
+
type: "text",
|
|
2291
|
+
text: JSON.stringify(payload, null, 2)
|
|
2292
|
+
}],
|
|
2293
|
+
structuredContent: payload,
|
|
2294
|
+
isError: true
|
|
2295
|
+
};
|
|
2296
|
+
}
|
|
2297
|
+
});
|
|
2298
|
+
registerTool("bfs_apply_stack_update", {
|
|
2299
|
+
description: "Applies a previously reviewed scaffold-time stack update to an existing Better-Fullstack project. Refuses to overwrite user-edited generated files and does not install dependencies.",
|
|
2300
|
+
inputSchema: mcpInputSchema(MCP_STACK_UPDATE_SCHEMA),
|
|
2301
|
+
outputSchema: stackUpdateOutputSchema,
|
|
2302
|
+
annotations: {
|
|
2303
|
+
title: "Apply stack update",
|
|
2304
|
+
readOnlyHint: false,
|
|
2305
|
+
destructiveHint: false,
|
|
2306
|
+
idempotentHint: false,
|
|
2307
|
+
openWorldHint: false
|
|
2308
|
+
}
|
|
2309
|
+
}, async (input) => {
|
|
2310
|
+
try {
|
|
2311
|
+
const safePath = sanitizePath(input.projectDir);
|
|
2312
|
+
const { projectDir: _projectDir, projectName: _projectName, ...requestedChanges } = input;
|
|
2313
|
+
const result = await applyStackUpdate(safePath, requestedChanges);
|
|
2314
|
+
if (!result.success) return {
|
|
2315
|
+
content: [{
|
|
2316
|
+
type: "text",
|
|
2317
|
+
text: JSON.stringify(result, null, 2)
|
|
2318
|
+
}],
|
|
2319
|
+
structuredContent: result,
|
|
2320
|
+
isError: true
|
|
2321
|
+
};
|
|
2322
|
+
const compatibilityWarnings = compatibilityWarningsForStackUpdate(result.proposedConfig);
|
|
2323
|
+
const { operations: _operations, filesUnchanged: _filesUnchanged, ...safeResult } = result;
|
|
2324
|
+
const payload = {
|
|
2325
|
+
...safeResult,
|
|
2326
|
+
...result.compatibilityAdjustments.length > 0 ? { compatibilityAdjustments: result.compatibilityAdjustments } : {},
|
|
2327
|
+
...compatibilityWarnings ? { compatibilityWarnings } : {},
|
|
2328
|
+
message: `Stack update applied. Dependencies were not installed; run: ${result.installCommand}`
|
|
2329
|
+
};
|
|
2330
|
+
return {
|
|
2331
|
+
content: [{
|
|
2332
|
+
type: "text",
|
|
2333
|
+
text: JSON.stringify(payload, null, 2)
|
|
2334
|
+
}],
|
|
2335
|
+
structuredContent: payload
|
|
2336
|
+
};
|
|
2337
|
+
} catch (error) {
|
|
2338
|
+
const payload = {
|
|
2339
|
+
success: false,
|
|
2340
|
+
projectDir: input.projectDir,
|
|
2341
|
+
error: `Apply stack update failed: ${error instanceof Error ? error.message : String(error)}`
|
|
2342
|
+
};
|
|
2343
|
+
return {
|
|
2344
|
+
content: [{
|
|
2345
|
+
type: "text",
|
|
2346
|
+
text: JSON.stringify(payload, null, 2)
|
|
2347
|
+
}],
|
|
2348
|
+
structuredContent: payload,
|
|
2349
|
+
isError: true
|
|
2350
|
+
};
|
|
2351
|
+
}
|
|
2352
|
+
});
|
|
1311
2353
|
registerTool("bfs_plan_addition", {
|
|
1312
2354
|
description: "Validates what would be added to an existing project. Reads the project config (bts.jsonc) and checks which addons are new.",
|
|
1313
2355
|
inputSchema: mcpInputSchema({
|
|
@@ -1491,4 +2533,4 @@ async function startMcpServer() {
|
|
|
1491
2533
|
startMcpServer();
|
|
1492
2534
|
|
|
1493
2535
|
//#endregion
|
|
1494
|
-
export { startMcpServer as n, getMcpGraphPreview as t };
|
|
2536
|
+
export { startMcpServer as i, MCP_STACK_UPDATE_SCHEMA as n, getMcpGraphPreview as r, MCP_PLAN_CREATE_SCHEMA as t };
|