create-better-fullstack 2.0.3 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{add-handler-BQN6pwGJ.mjs → add-handler-9F-AsGM-.mjs} +11 -9
- package/dist/addons-setup-C8eaCaH5.mjs +5 -0
- package/dist/{addons-setup-HSghQS7c.mjs → addons-setup-C_xrNtkL.mjs} +3 -83
- package/dist/cli.mjs +2 -2
- package/dist/compatibility-rules-D7zYNVjC.mjs +372 -0
- package/dist/config-validation-C4glouQh.mjs +1512 -0
- package/dist/doctor-BFSSbS-U.mjs +280 -0
- package/dist/{bts-config-Bg1Qea9Y.mjs → errors-D9yiiGVq.mjs} +215 -72
- package/dist/generated-checks-DUvVXWId.mjs +91 -0
- package/dist/index.d.mts +85 -29
- package/dist/index.mjs +11 -6
- package/dist/{install-dependencies-D0Z1dZEx.mjs → install-dependencies-DDGF-zDG.mjs} +102 -397
- package/dist/mcp-BXLhb7wW.mjs +7 -0
- package/dist/mcp-entry.mjs +1802 -296
- package/dist/render-title-zvyKC1ej.mjs +34 -0
- package/dist/run-BmRKR2wG.mjs +11 -0
- package/dist/{run-BhcXBjxh.mjs → run-DRzP53v3.mjs} +534 -1717
- package/package.json +5 -4
- package/dist/addons-setup-CV5uZyhH.mjs +0 -5
- package/dist/mcp-58r70ZcL.mjs +0 -5
- package/dist/run-DCxVLUMS.mjs +0 -7
- /package/dist/{update-deps-CLebIM70.mjs → update-deps-D5OG0KmJ.mjs} +0 -0
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { A as types_exports, D as getGraphSummary, E as getGraphPart, S as getUserPkgManager, T as getGraphBackendUrl, _ as writeBtsConfig, a as handleError, b as DEFAULT_UI_LIBRARY_BY_FRONTEND, c as isSilent, d as setLastPromptShownUI, f as buildBtsConfigForPersistence, h as readBtsConfigFromFile, i as exitWithError, k as hasGraphPart, l as runWithContextAsync, n as UserCancelledError, o as didLastPromptShowUI, r as exitCancelled, s as isFirstPrompt, t as CLIError, u as setIsFirstPrompt, v as getLatestCLIVersion, w as getGraphBackendDeployInstructions, x as getDefaultConfig, y as DEFAULT_CONFIG } from "./errors-D9yiiGVq.mjs";
|
|
3
|
+
import { t as renderTitle } from "./render-title-zvyKC1ej.mjs";
|
|
4
|
+
import { i as canPromptInteractively, n as getPackageExecutionArgs, r as addPackageDependency, t as setupAddons } from "./addons-setup-C_xrNtkL.mjs";
|
|
5
|
+
import { a as generateReproducibleCommand, i as getTemplateDescription, n as validateFullConfig, o as CreateCommandInputSchema, r as getTemplateConfig, t as validateConfigForProgrammaticUse } from "./config-validation-C4glouQh.mjs";
|
|
6
|
+
import { a as getCompatibleUILibraries$1, c as isExampleChatSdkAllowed$1, d as requiresChatSdkVercelAI, f as splitFrontends$1, i as getCompatibleCSSFrameworks$1, l as isFrontendAllowedWithBackend$1, o as hasWebStyling$1, s as isExampleAIAllowed$1, t as allowedApisForFrontends$1, u as isWebFrontend$1 } from "./compatibility-rules-D7zYNVjC.mjs";
|
|
7
|
+
import { _ as isGoBack, a as runMavenTests, c as applyDependencyVersionChannel, d as isCancel$1, f as navigableConfirm, g as GO_BACK_SYMBOL, h as setIsFirstPrompt$1, i as runGradleTests, l as getAddonsChoice, m as navigableSelect, n as runCargoBuild, o as runMixCompile, p as navigableMultiselect, r as runGoModTidy, s as runUvSync, t as installDependencies } from "./install-dependencies-DDGF-zDG.mjs";
|
|
8
|
+
import { n as commandExists, t as runGeneratedChecks } from "./generated-checks-DUvVXWId.mjs";
|
|
5
9
|
import { cancel, confirm, intro, isCancel, log, outro, select, spinner, text } from "@clack/prompts";
|
|
6
10
|
import { createRouterClient, os } from "@orpc/server";
|
|
7
11
|
import pc from "picocolors";
|
|
@@ -18,10 +22,10 @@ import os$1 from "node:os";
|
|
|
18
22
|
import { format } from "oxfmt";
|
|
19
23
|
|
|
20
24
|
//#region src/utils/project-history.ts
|
|
21
|
-
const paths = envPaths("better-fullstack", { suffix: "" });
|
|
25
|
+
const paths$1 = envPaths("better-fullstack", { suffix: "" });
|
|
22
26
|
const HISTORY_FILE = "history.json";
|
|
23
27
|
function getHistoryPath() {
|
|
24
|
-
return path.join(paths.data, HISTORY_FILE);
|
|
28
|
+
return path.join(paths$1.data, HISTORY_FILE);
|
|
25
29
|
}
|
|
26
30
|
function generateId() {
|
|
27
31
|
return `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
@@ -44,11 +48,17 @@ async function readHistory() {
|
|
|
44
48
|
}
|
|
45
49
|
}
|
|
46
50
|
async function writeHistory(history$1) {
|
|
47
|
-
await fs.ensureDir(paths.data);
|
|
51
|
+
await fs.ensureDir(paths$1.data);
|
|
48
52
|
await fs.writeJson(getHistoryPath(), history$1, { spaces: 2 });
|
|
49
53
|
}
|
|
50
54
|
async function addToHistory(config, reproducibleCommand) {
|
|
51
55
|
const history$1 = await readHistory();
|
|
56
|
+
let configSnapshot;
|
|
57
|
+
try {
|
|
58
|
+
configSnapshot = buildBtsConfigForPersistence(config);
|
|
59
|
+
} catch {
|
|
60
|
+
configSnapshot = void 0;
|
|
61
|
+
}
|
|
52
62
|
const entry = {
|
|
53
63
|
id: generateId(),
|
|
54
64
|
projectName: config.projectName,
|
|
@@ -69,7 +79,8 @@ async function addToHistory(config, reproducibleCommand) {
|
|
|
69
79
|
packageManager: config.packageManager
|
|
70
80
|
},
|
|
71
81
|
cliVersion: getLatestCLIVersion(),
|
|
72
|
-
reproducibleCommand
|
|
82
|
+
reproducibleCommand,
|
|
83
|
+
...configSnapshot ? { config: configSnapshot } : {}
|
|
73
84
|
};
|
|
74
85
|
history$1.entries.unshift(entry);
|
|
75
86
|
if (history$1.entries.length > 100) history$1.entries = history$1.entries.slice(0, 100);
|
|
@@ -78,6 +89,17 @@ async function addToHistory(config, reproducibleCommand) {
|
|
|
78
89
|
async function getHistory(limit = 10) {
|
|
79
90
|
return (await readHistory()).entries.slice(0, limit);
|
|
80
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* Returns the entry at a 1-based position (1 = most recent), or null when the
|
|
94
|
+
* position is out of range.
|
|
95
|
+
*/
|
|
96
|
+
async function getHistoryEntry(position) {
|
|
97
|
+
if (!Number.isInteger(position) || position < 1) return null;
|
|
98
|
+
return (await readHistory()).entries[position - 1] ?? null;
|
|
99
|
+
}
|
|
100
|
+
async function getHistoryCount() {
|
|
101
|
+
return (await readHistory()).entries.length;
|
|
102
|
+
}
|
|
81
103
|
async function clearHistory() {
|
|
82
104
|
const historyPath = getHistoryPath();
|
|
83
105
|
if (await fs.pathExists(historyPath)) await fs.remove(historyPath);
|
|
@@ -134,153 +156,187 @@ async function historyHandler(input) {
|
|
|
134
156
|
}
|
|
135
157
|
|
|
136
158
|
//#endregion
|
|
137
|
-
//#region src/
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
}
|
|
283
|
-
|
|
159
|
+
//#region src/utils/telemetry-settings.ts
|
|
160
|
+
const paths = envPaths("better-fullstack", { suffix: "" });
|
|
161
|
+
const SETTINGS_FILE = "telemetry.json";
|
|
162
|
+
function getSettingsPath() {
|
|
163
|
+
return path.join(paths.data, SETTINGS_FILE);
|
|
164
|
+
}
|
|
165
|
+
function emptySettings() {
|
|
166
|
+
return {
|
|
167
|
+
version: 1,
|
|
168
|
+
noticeShown: false
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
async function readTelemetrySettings() {
|
|
172
|
+
const settingsPath = getSettingsPath();
|
|
173
|
+
if (!await fs.pathExists(settingsPath)) return emptySettings();
|
|
174
|
+
try {
|
|
175
|
+
const data = await fs.readJson(settingsPath);
|
|
176
|
+
if (!data || typeof data !== "object") return emptySettings();
|
|
177
|
+
return {
|
|
178
|
+
version: typeof data.version === "number" ? data.version : 1,
|
|
179
|
+
enabled: typeof data.enabled === "boolean" ? data.enabled : void 0,
|
|
180
|
+
noticeShown: data.noticeShown === true
|
|
181
|
+
};
|
|
182
|
+
} catch {
|
|
183
|
+
return emptySettings();
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
async function writeTelemetrySettings(settings) {
|
|
187
|
+
await fs.ensureDir(paths.data);
|
|
188
|
+
await fs.writeJson(getSettingsPath(), settings, { spaces: 2 });
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* The user's explicit persisted preference, or `undefined` when they never chose.
|
|
192
|
+
*/
|
|
193
|
+
async function getPersistedTelemetryPreference() {
|
|
194
|
+
return (await readTelemetrySettings()).enabled;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Persist an explicit opt-in/opt-out. Choosing a preference also marks the
|
|
198
|
+
* first-run notice as shown so it never appears afterwards.
|
|
199
|
+
*/
|
|
200
|
+
async function setTelemetryPreference(enabled) {
|
|
201
|
+
const settings = await readTelemetrySettings();
|
|
202
|
+
settings.enabled = enabled;
|
|
203
|
+
settings.noticeShown = true;
|
|
204
|
+
await writeTelemetrySettings(settings);
|
|
205
|
+
}
|
|
206
|
+
async function hasTelemetryNoticeBeenShown() {
|
|
207
|
+
return (await readTelemetrySettings()).noticeShown;
|
|
208
|
+
}
|
|
209
|
+
async function markTelemetryNoticeShown() {
|
|
210
|
+
const settings = await readTelemetrySettings();
|
|
211
|
+
settings.noticeShown = true;
|
|
212
|
+
await writeTelemetrySettings(settings);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
//#endregion
|
|
216
|
+
//#region src/utils/analytics.ts
|
|
217
|
+
const CONVEX_INGEST_URL = "https://curious-elephant-653.convex.site/api/analytics/ingest";
|
|
218
|
+
/**
|
|
219
|
+
* Whether telemetry is explicitly overridden at runtime.
|
|
220
|
+
*
|
|
221
|
+
* Only `BTS_TELEMETRY_DISABLED` is a runtime override: `BTS_TELEMETRY` is inlined
|
|
222
|
+
* by the bundler at build time (see tsdown.config.ts) and therefore acts as a
|
|
223
|
+
* build-time default, not a runtime switch.
|
|
224
|
+
*/
|
|
225
|
+
function hasTelemetryEnvOverride() {
|
|
226
|
+
return process.env.BTS_TELEMETRY_DISABLED !== void 0;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Resolve whether telemetry is enabled.
|
|
230
|
+
*
|
|
231
|
+
* Precedence: runtime env override (`BTS_TELEMETRY_DISABLED`) > persisted
|
|
232
|
+
* preference > default. The default honors the build-time `BTS_TELEMETRY` flag
|
|
233
|
+
* (inlined by the bundler, "0" by default) and falls back to enabled when the
|
|
234
|
+
* flag is unset (e.g. running from source).
|
|
235
|
+
*
|
|
236
|
+
* `BTS_TELEMETRY` is intentionally evaluated last: the bundler replaces it with
|
|
237
|
+
* a literal, so an early `!== undefined` check would always short-circuit and
|
|
238
|
+
* make the persisted preference unreachable in the shipped CLI.
|
|
239
|
+
*/
|
|
240
|
+
async function isTelemetryEnabled() {
|
|
241
|
+
const disabled = process.env.BTS_TELEMETRY_DISABLED;
|
|
242
|
+
if (disabled !== void 0) return disabled !== "1";
|
|
243
|
+
const persisted = await getPersistedTelemetryPreference();
|
|
244
|
+
if (persisted !== void 0) return persisted;
|
|
245
|
+
const buildDefault = "1";
|
|
246
|
+
return buildDefault === void 0 ? true : buildDefault === "1";
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Print a one-time notice describing the anonymous telemetry the CLI collects
|
|
250
|
+
* and how to opt out, then remember that it was shown so it never repeats.
|
|
251
|
+
*
|
|
252
|
+
* No-ops when telemetry is explicitly configured via env var, when a persisted
|
|
253
|
+
* preference already exists, when the notice was already shown, when telemetry
|
|
254
|
+
* is disabled by the build default, or when the CLI is not running interactively
|
|
255
|
+
* (CI / silent / non-TTY).
|
|
256
|
+
*/
|
|
257
|
+
async function maybeShowTelemetryNotice() {
|
|
258
|
+
if (hasTelemetryEnvOverride()) return;
|
|
259
|
+
if (!canPromptInteractively()) return;
|
|
260
|
+
if (await getPersistedTelemetryPreference() !== void 0) return;
|
|
261
|
+
if (await hasTelemetryNoticeBeenShown()) return;
|
|
262
|
+
if (!await isTelemetryEnabled()) return;
|
|
263
|
+
log.info(`${pc.bold("Anonymous usage telemetry is enabled.")}\n${pc.dim("We collect your selected stack options (e.g. frontend, backend, database),")}\n${pc.dim("plus CLI version, Node.js version, and OS platform — never project names,")}\n${pc.dim("file paths, or any personal data.")}\nOpt out anytime with ${pc.cyan("create-better-fullstack telemetry disable")} or ${pc.cyan("BTS_TELEMETRY_DISABLED=1")}.`);
|
|
264
|
+
try {
|
|
265
|
+
await markTelemetryNoticeShown();
|
|
266
|
+
} catch {}
|
|
267
|
+
}
|
|
268
|
+
async function sendConvexEvent(payload) {
|
|
269
|
+
try {
|
|
270
|
+
await fetch(CONVEX_INGEST_URL, {
|
|
271
|
+
method: "POST",
|
|
272
|
+
headers: { "Content-Type": "application/json" },
|
|
273
|
+
body: JSON.stringify(payload)
|
|
274
|
+
});
|
|
275
|
+
} catch {}
|
|
276
|
+
}
|
|
277
|
+
async function trackProjectCreation(config, disableAnalytics = false) {
|
|
278
|
+
if (disableAnalytics || !await isTelemetryEnabled()) return;
|
|
279
|
+
const { projectName: _projectName, projectDir: _projectDir, relativePath: _relativePath, ...safeConfig } = config;
|
|
280
|
+
try {
|
|
281
|
+
await sendConvexEvent({
|
|
282
|
+
...safeConfig,
|
|
283
|
+
cli_version: getLatestCLIVersion(),
|
|
284
|
+
node_version: typeof process !== "undefined" ? process.version : "",
|
|
285
|
+
platform: typeof process !== "undefined" ? process.platform : ""
|
|
286
|
+
});
|
|
287
|
+
} catch {}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
//#endregion
|
|
291
|
+
//#region src/commands/telemetry.ts
|
|
292
|
+
const COLLECTED = ["Selected stack options (frontend, backend, database, ORM, auth, API, etc.)", "CLI version, Node.js version, and OS platform"];
|
|
293
|
+
const NOT_COLLECTED = ["Project names, directory paths, or file contents", "Personal or otherwise identifying information"];
|
|
294
|
+
function resolveSource(persisted) {
|
|
295
|
+
if (hasTelemetryEnvOverride()) return "env";
|
|
296
|
+
if (persisted !== void 0) return "preference";
|
|
297
|
+
return "default";
|
|
298
|
+
}
|
|
299
|
+
function describeSource(source) {
|
|
300
|
+
switch (source) {
|
|
301
|
+
case "env": return "environment variable (BTS_TELEMETRY_DISABLED)";
|
|
302
|
+
case "preference": return "saved preference (create-better-fullstack telemetry enable/disable)";
|
|
303
|
+
default: return "default";
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
async function telemetryHandler(input) {
|
|
307
|
+
if (input.action === "enable") {
|
|
308
|
+
await setTelemetryPreference(true);
|
|
309
|
+
log.success(pc.green("Telemetry enabled. Thanks for helping improve Better Fullstack."));
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
if (input.action === "disable") {
|
|
313
|
+
await setTelemetryPreference(false);
|
|
314
|
+
log.success(pc.green("Telemetry disabled. No anonymous usage data will be sent."));
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const enabled = await isTelemetryEnabled();
|
|
318
|
+
const persisted = await getPersistedTelemetryPreference();
|
|
319
|
+
const source = resolveSource(persisted);
|
|
320
|
+
if (input.json) {
|
|
321
|
+
console.log(JSON.stringify({
|
|
322
|
+
enabled,
|
|
323
|
+
source,
|
|
324
|
+
persisted: persisted ?? null,
|
|
325
|
+
envOverride: hasTelemetryEnvOverride()
|
|
326
|
+
}, null, 2));
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
renderTitle();
|
|
330
|
+
intro(pc.magenta("Telemetry"));
|
|
331
|
+
log.message(`${pc.bold("Status:")} ${enabled ? pc.green("enabled") : pc.yellow("disabled")} ${pc.dim(`(${describeSource(source)})`)}`);
|
|
332
|
+
log.message("");
|
|
333
|
+
log.message(pc.bold("What is sent when enabled:"));
|
|
334
|
+
for (const item of COLLECTED) log.message(` ${pc.green("+")} ${item}`);
|
|
335
|
+
log.message(pc.bold("What is never sent:"));
|
|
336
|
+
for (const item of NOT_COLLECTED) log.message(` ${pc.red("-")} ${item}`);
|
|
337
|
+
log.message("");
|
|
338
|
+
log.message(pc.dim(`Change anytime: ${pc.cyan("create-better-fullstack telemetry enable")} / ${pc.cyan("create-better-fullstack telemetry disable")} (or set ${pc.cyan("BTS_TELEMETRY_DISABLED=1")}).`));
|
|
339
|
+
}
|
|
284
340
|
|
|
285
341
|
//#endregion
|
|
286
342
|
//#region src/prompts/prompt-contract.ts
|
|
@@ -400,7 +456,7 @@ const AI_DOCS_OPTIONS = [
|
|
|
400
456
|
},
|
|
401
457
|
{
|
|
402
458
|
value: "agents-md",
|
|
403
|
-
label: "
|
|
459
|
+
label: "AGENTS.md",
|
|
404
460
|
hint: "Generic AI assistant documentation"
|
|
405
461
|
},
|
|
406
462
|
{
|
|
@@ -531,6 +587,11 @@ const API_PROMPT_OPTION_MAP = {
|
|
|
531
587
|
label: "GraphQL Yoga",
|
|
532
588
|
hint: "Batteries-included GraphQL server with Pothos schema builder"
|
|
533
589
|
},
|
|
590
|
+
"apollo-server": {
|
|
591
|
+
value: "apollo-server",
|
|
592
|
+
label: "Apollo Server",
|
|
593
|
+
hint: "Spec-compliant GraphQL server with a schema-first starter"
|
|
594
|
+
},
|
|
534
595
|
openapi: {
|
|
535
596
|
value: "openapi",
|
|
536
597
|
label: "OpenAPI",
|
|
@@ -899,6 +960,11 @@ const CMS_PROMPT_OPTIONS = [
|
|
|
899
960
|
label: "Directus",
|
|
900
961
|
hint: "Open data platform and headless CMS for SQL databases"
|
|
901
962
|
},
|
|
963
|
+
{
|
|
964
|
+
value: "keystatic",
|
|
965
|
+
label: "Keystatic",
|
|
966
|
+
hint: "Git-backed CMS for Markdown, JSON, and YAML content"
|
|
967
|
+
},
|
|
902
968
|
{
|
|
903
969
|
value: "none",
|
|
904
970
|
label: "None",
|
|
@@ -2361,7 +2427,7 @@ async function getFrontendChoice(frontendOptions, backend, auth) {
|
|
|
2361
2427
|
});
|
|
2362
2428
|
if (isGoBack(frontendTypes)) return GO_BACK_SYMBOL;
|
|
2363
2429
|
if (isCancel$1(frontendTypes)) return exitCancelled("Operation cancelled");
|
|
2364
|
-
setIsFirstPrompt(false);
|
|
2430
|
+
setIsFirstPrompt$1(false);
|
|
2365
2431
|
const result = [];
|
|
2366
2432
|
let shouldRestart = false;
|
|
2367
2433
|
if (frontendTypes.includes("web")) {
|
|
@@ -2377,7 +2443,7 @@ async function getFrontendChoice(frontendOptions, backend, auth) {
|
|
|
2377
2443
|
else result.push(webFramework);
|
|
2378
2444
|
}
|
|
2379
2445
|
if (shouldRestart) {
|
|
2380
|
-
setIsFirstPrompt(wasFirstPrompt);
|
|
2446
|
+
setIsFirstPrompt$1(wasFirstPrompt);
|
|
2381
2447
|
continue;
|
|
2382
2448
|
}
|
|
2383
2449
|
if (frontendTypes.includes("native")) {
|
|
@@ -2390,14 +2456,14 @@ async function getFrontendChoice(frontendOptions, backend, auth) {
|
|
|
2390
2456
|
});
|
|
2391
2457
|
if (isGoBack(nativeFramework)) if (frontendTypes.includes("web")) shouldRestart = true;
|
|
2392
2458
|
else {
|
|
2393
|
-
setIsFirstPrompt(wasFirstPrompt);
|
|
2459
|
+
setIsFirstPrompt$1(wasFirstPrompt);
|
|
2394
2460
|
continue;
|
|
2395
2461
|
}
|
|
2396
2462
|
else if (isCancel$1(nativeFramework)) return exitCancelled("Operation cancelled");
|
|
2397
2463
|
else result.push(nativeFramework);
|
|
2398
2464
|
}
|
|
2399
2465
|
if (shouldRestart) {
|
|
2400
|
-
setIsFirstPrompt(wasFirstPrompt);
|
|
2466
|
+
setIsFirstPrompt$1(wasFirstPrompt);
|
|
2401
2467
|
continue;
|
|
2402
2468
|
}
|
|
2403
2469
|
return result;
|
|
@@ -2832,17 +2898,36 @@ async function getGoObservabilityChoice(goObservability) {
|
|
|
2832
2898
|
|
|
2833
2899
|
//#endregion
|
|
2834
2900
|
//#region src/prompts/i18n.ts
|
|
2901
|
+
const PARAGLIDE_FRONTENDS = new Set([
|
|
2902
|
+
"next",
|
|
2903
|
+
"nuxt",
|
|
2904
|
+
"vinext",
|
|
2905
|
+
"tanstack-router",
|
|
2906
|
+
"tanstack-start",
|
|
2907
|
+
"react-router",
|
|
2908
|
+
"react-vite",
|
|
2909
|
+
"svelte",
|
|
2910
|
+
"solid",
|
|
2911
|
+
"solid-start",
|
|
2912
|
+
"astro"
|
|
2913
|
+
]);
|
|
2835
2914
|
async function getI18nChoice(i18n, frontend) {
|
|
2836
2915
|
if (i18n !== void 0) return i18n;
|
|
2916
|
+
const hasNext = frontend?.includes("next") ?? false;
|
|
2837
2917
|
const response = await navigableSelect({
|
|
2838
2918
|
message: "Select internationalization (i18n) library",
|
|
2839
2919
|
options: [
|
|
2920
|
+
...frontend?.some((f) => PARAGLIDE_FRONTENDS.has(f)) ?? false ? [{
|
|
2921
|
+
value: "paraglide",
|
|
2922
|
+
label: "Paraglide",
|
|
2923
|
+
hint: "Type-safe, compiler-based i18n for modern web frontends"
|
|
2924
|
+
}] : [],
|
|
2840
2925
|
{
|
|
2841
2926
|
value: "i18next",
|
|
2842
2927
|
label: "i18next",
|
|
2843
2928
|
hint: "Full-featured i18n framework, works with all frontends"
|
|
2844
2929
|
},
|
|
2845
|
-
...
|
|
2930
|
+
...hasNext ? [{
|
|
2846
2931
|
value: "next-intl",
|
|
2847
2932
|
label: "next-intl",
|
|
2848
2933
|
hint: "Lightweight i18n for Next.js with App Router support"
|
|
@@ -2859,17 +2944,6 @@ async function getI18nChoice(i18n, frontend) {
|
|
|
2859
2944
|
return response;
|
|
2860
2945
|
}
|
|
2861
2946
|
|
|
2862
|
-
//#endregion
|
|
2863
|
-
//#region src/utils/command-exists.ts
|
|
2864
|
-
async function commandExists(command) {
|
|
2865
|
-
try {
|
|
2866
|
-
if (process.platform === "win32") return (await $({ reject: false })`where ${command}`).exitCode === 0;
|
|
2867
|
-
return (await $({ reject: false })`which ${command}`).exitCode === 0;
|
|
2868
|
-
} catch {
|
|
2869
|
-
return false;
|
|
2870
|
-
}
|
|
2871
|
-
}
|
|
2872
|
-
|
|
2873
2947
|
//#endregion
|
|
2874
2948
|
//#region src/prompts/install.ts
|
|
2875
2949
|
async function getinstallChoice(install, ecosystem, javaBuildTool) {
|
|
@@ -5618,7 +5692,7 @@ async function navigableGroup(prompts, opts) {
|
|
|
5618
5692
|
while (currentIndex < promptNames.length) {
|
|
5619
5693
|
const name = promptNames[currentIndex];
|
|
5620
5694
|
const prompt = prompts[name];
|
|
5621
|
-
setIsFirstPrompt
|
|
5695
|
+
setIsFirstPrompt(currentIndex === 0);
|
|
5622
5696
|
setLastPromptShownUI(false);
|
|
5623
5697
|
const result = await prompt({ results })?.catch((e) => {
|
|
5624
5698
|
throw e;
|
|
@@ -5639,7 +5713,7 @@ async function navigableGroup(prompts, opts) {
|
|
|
5639
5713
|
results[name] = "canceled";
|
|
5640
5714
|
opts.onCancel({ results });
|
|
5641
5715
|
}
|
|
5642
|
-
setIsFirstPrompt
|
|
5716
|
+
setIsFirstPrompt(false);
|
|
5643
5717
|
return results;
|
|
5644
5718
|
}
|
|
5645
5719
|
if (goingBack && !didLastPromptShowUI()) {
|
|
@@ -5654,7 +5728,7 @@ async function navigableGroup(prompts, opts) {
|
|
|
5654
5728
|
results[name] = result;
|
|
5655
5729
|
currentIndex++;
|
|
5656
5730
|
}
|
|
5657
|
-
setIsFirstPrompt
|
|
5731
|
+
setIsFirstPrompt(false);
|
|
5658
5732
|
return results;
|
|
5659
5733
|
}
|
|
5660
5734
|
|
|
@@ -6095,6 +6169,11 @@ const SEARCH_PROMPT_OPTIONS = [
|
|
|
6095
6169
|
label: "Elasticsearch",
|
|
6096
6170
|
hint: "Distributed search and analytics engine with local and cloud deployments"
|
|
6097
6171
|
},
|
|
6172
|
+
{
|
|
6173
|
+
value: "opensearch",
|
|
6174
|
+
label: "OpenSearch",
|
|
6175
|
+
hint: "Open-source search and analytics suite compatible with Elasticsearch APIs"
|
|
6176
|
+
},
|
|
6098
6177
|
{
|
|
6099
6178
|
value: "algolia",
|
|
6100
6179
|
label: "Algolia",
|
|
@@ -6149,6 +6228,77 @@ async function getSearchChoice(search, backend, ecosystem) {
|
|
|
6149
6228
|
return response;
|
|
6150
6229
|
}
|
|
6151
6230
|
|
|
6231
|
+
//#endregion
|
|
6232
|
+
//#region src/prompts/vector-db.ts
|
|
6233
|
+
const VECTOR_DB_PROMPT_OPTIONS = [
|
|
6234
|
+
{
|
|
6235
|
+
value: "pgvector",
|
|
6236
|
+
label: "pgvector",
|
|
6237
|
+
hint: "Self-hosted Postgres + pgvector extension for embeddings"
|
|
6238
|
+
},
|
|
6239
|
+
{
|
|
6240
|
+
value: "qdrant",
|
|
6241
|
+
label: "Qdrant",
|
|
6242
|
+
hint: "High-performance open-source vector database"
|
|
6243
|
+
},
|
|
6244
|
+
{
|
|
6245
|
+
value: "chroma",
|
|
6246
|
+
label: "Chroma",
|
|
6247
|
+
hint: "Lightweight open-source embedding database"
|
|
6248
|
+
},
|
|
6249
|
+
{
|
|
6250
|
+
value: "pinecone",
|
|
6251
|
+
label: "Pinecone",
|
|
6252
|
+
hint: "Fully managed serverless vector database"
|
|
6253
|
+
},
|
|
6254
|
+
{
|
|
6255
|
+
value: "none",
|
|
6256
|
+
label: "None",
|
|
6257
|
+
hint: "Skip vector database setup"
|
|
6258
|
+
}
|
|
6259
|
+
];
|
|
6260
|
+
/**
|
|
6261
|
+
* Vector DB is a TypeScript-ecosystem feature backed by a standalone server.
|
|
6262
|
+
* Every provider (including pgvector via a dedicated Postgres instance) is a
|
|
6263
|
+
* separate service, so there is no dependency on the primary database choice.
|
|
6264
|
+
*/
|
|
6265
|
+
function resolveVectorDbPrompt(context = {}) {
|
|
6266
|
+
const skip = () => ({
|
|
6267
|
+
shouldPrompt: false,
|
|
6268
|
+
mode: "single",
|
|
6269
|
+
options: [],
|
|
6270
|
+
autoValue: "none"
|
|
6271
|
+
});
|
|
6272
|
+
if (context.ecosystem && context.ecosystem !== "typescript") return skip();
|
|
6273
|
+
if (context.backend === "none" || context.backend === "convex") return skip();
|
|
6274
|
+
return context.vectorDb !== void 0 ? {
|
|
6275
|
+
shouldPrompt: false,
|
|
6276
|
+
mode: "single",
|
|
6277
|
+
options: VECTOR_DB_PROMPT_OPTIONS,
|
|
6278
|
+
autoValue: context.vectorDb
|
|
6279
|
+
} : {
|
|
6280
|
+
shouldPrompt: true,
|
|
6281
|
+
mode: "single",
|
|
6282
|
+
options: VECTOR_DB_PROMPT_OPTIONS,
|
|
6283
|
+
initialValue: "none"
|
|
6284
|
+
};
|
|
6285
|
+
}
|
|
6286
|
+
async function getVectorDbChoice(vectorDb, backend, ecosystem) {
|
|
6287
|
+
const resolution = resolveVectorDbPrompt({
|
|
6288
|
+
vectorDb,
|
|
6289
|
+
backend,
|
|
6290
|
+
ecosystem
|
|
6291
|
+
});
|
|
6292
|
+
if (!resolution.shouldPrompt) return resolution.autoValue ?? "none";
|
|
6293
|
+
const response = await navigableSelect({
|
|
6294
|
+
message: "Select vector database",
|
|
6295
|
+
options: resolution.options,
|
|
6296
|
+
initialValue: resolution.initialValue
|
|
6297
|
+
});
|
|
6298
|
+
if (isCancel$1(response)) return exitCancelled("Operation cancelled");
|
|
6299
|
+
return response;
|
|
6300
|
+
}
|
|
6301
|
+
|
|
6152
6302
|
//#endregion
|
|
6153
6303
|
//#region src/prompts/server-deploy.ts
|
|
6154
6304
|
async function getServerDeploymentChoice(deployment, runtime, backend, _webDeploy) {
|
|
@@ -6442,7 +6592,7 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
|
|
|
6442
6592
|
},
|
|
6443
6593
|
addons: ({ results }) => {
|
|
6444
6594
|
if (results.ecosystem !== "typescript") {
|
|
6445
|
-
const nonTypeScriptAddons = (flags.addons ?? []).filter((addon) => addon === "docker-compose");
|
|
6595
|
+
const nonTypeScriptAddons = (flags.addons ?? []).filter((addon) => addon === "docker-compose" || addon === "devcontainer" || addon === "github-actions");
|
|
6446
6596
|
return Promise.resolve(nonTypeScriptAddons);
|
|
6447
6597
|
}
|
|
6448
6598
|
return getAddonsChoice(flags.addons, results.frontend, results.auth, results.backend, results.runtime);
|
|
@@ -6536,6 +6686,10 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
|
|
|
6536
6686
|
if (results.ecosystem === "react-native" || results.ecosystem === "elixir") return Promise.resolve("none");
|
|
6537
6687
|
return getSearchChoice(flags.search, results.backend, results.ecosystem);
|
|
6538
6688
|
},
|
|
6689
|
+
vectorDb: ({ results }) => {
|
|
6690
|
+
if (results.ecosystem !== "typescript") return Promise.resolve("none");
|
|
6691
|
+
return getVectorDbChoice(flags.vectorDb, results.backend, results.ecosystem);
|
|
6692
|
+
},
|
|
6539
6693
|
fileStorage: ({ results }) => {
|
|
6540
6694
|
if (results.ecosystem !== "typescript") return Promise.resolve("none");
|
|
6541
6695
|
return getFileStorageChoice(flags.fileStorage, results.backend);
|
|
@@ -6943,6 +7097,7 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
|
|
|
6943
7097
|
rateLimit: result.rateLimit,
|
|
6944
7098
|
i18n: result.i18n,
|
|
6945
7099
|
search: result.search,
|
|
7100
|
+
vectorDb: result.vectorDb,
|
|
6946
7101
|
fileStorage: result.fileStorage,
|
|
6947
7102
|
mobileNavigation: result.mobileNavigation,
|
|
6948
7103
|
mobileUI: result.mobileUI,
|
|
@@ -7111,35 +7266,76 @@ async function getVersionChannelChoice(versionChannel) {
|
|
|
7111
7266
|
}
|
|
7112
7267
|
|
|
7113
7268
|
//#endregion
|
|
7114
|
-
//#region src/utils/
|
|
7115
|
-
|
|
7116
|
-
|
|
7117
|
-
|
|
7118
|
-
|
|
7119
|
-
|
|
7120
|
-
|
|
7121
|
-
|
|
7269
|
+
//#region src/utils/config-source.ts
|
|
7270
|
+
/**
|
|
7271
|
+
* CreateInput flag keys that can be sourced from a stored config. `projectName`
|
|
7272
|
+
* is excluded so a replayed config never reuses the original project name.
|
|
7273
|
+
*/
|
|
7274
|
+
const COPYABLE_CREATE_INPUT_KEYS = Object.keys(types_exports.CreateInputSchema.shape).filter((key) => key !== "projectName");
|
|
7275
|
+
/**
|
|
7276
|
+
* Projects a persisted Better-Fullstack config (bts.jsonc shape) onto the
|
|
7277
|
+
* subset of `create` flags it can drive. Only defined values are copied so the
|
|
7278
|
+
* result can be safely overlaid by explicitly-passed CLI flags.
|
|
7279
|
+
*/
|
|
7280
|
+
function betterTStackConfigToCreateInput(config) {
|
|
7281
|
+
const source = config;
|
|
7282
|
+
const result = {};
|
|
7283
|
+
for (const key of COPYABLE_CREATE_INPUT_KEYS) {
|
|
7284
|
+
const value = source[key];
|
|
7285
|
+
if (value !== void 0 && value !== null) result[key] = value;
|
|
7286
|
+
}
|
|
7287
|
+
return result;
|
|
7122
7288
|
}
|
|
7123
|
-
|
|
7124
|
-
|
|
7125
|
-
|
|
7126
|
-
|
|
7127
|
-
|
|
7128
|
-
|
|
7129
|
-
|
|
7130
|
-
|
|
7289
|
+
/**
|
|
7290
|
+
* Maps a legacy history entry (written before the full config snapshot existed)
|
|
7291
|
+
* onto `create` flags using the limited stack summary it stored.
|
|
7292
|
+
*/
|
|
7293
|
+
function historyStackToCreateInput(stack) {
|
|
7294
|
+
return {
|
|
7295
|
+
frontend: stack.frontend,
|
|
7296
|
+
backend: stack.backend,
|
|
7297
|
+
database: stack.database,
|
|
7298
|
+
orm: stack.orm,
|
|
7299
|
+
runtime: stack.runtime,
|
|
7300
|
+
auth: stack.auth,
|
|
7301
|
+
payments: stack.payments,
|
|
7302
|
+
api: stack.api,
|
|
7303
|
+
addons: stack.addons,
|
|
7304
|
+
examples: stack.examples,
|
|
7305
|
+
dbSetup: stack.dbSetup,
|
|
7306
|
+
packageManager: stack.packageManager
|
|
7307
|
+
};
|
|
7131
7308
|
}
|
|
7132
|
-
|
|
7133
|
-
|
|
7134
|
-
|
|
7135
|
-
|
|
7136
|
-
|
|
7137
|
-
|
|
7138
|
-
|
|
7139
|
-
|
|
7140
|
-
|
|
7141
|
-
|
|
7142
|
-
|
|
7309
|
+
/**
|
|
7310
|
+
* Resolves the `--from-history` / `--config` flags into a base set of `create`
|
|
7311
|
+
* flags. Returns `undefined` when neither flag is used. The two flags are
|
|
7312
|
+
* mutually exclusive, and both are incompatible with `--yes`, `--template`, and
|
|
7313
|
+
* `--part` (each of those also wants to own the full stack). On any user error
|
|
7314
|
+
* this calls `exitWithError`, which exits the CLI (or throws a CLIError in
|
|
7315
|
+
* silent/programmatic mode).
|
|
7316
|
+
*/
|
|
7317
|
+
async function resolveCreateConfigBase(input) {
|
|
7318
|
+
const hasHistory = input.fromHistory !== void 0;
|
|
7319
|
+
const hasConfig = input.config !== void 0 && input.config !== "";
|
|
7320
|
+
if (!hasHistory && !hasConfig) return;
|
|
7321
|
+
if (hasHistory && hasConfig) exitWithError("Cannot combine --from-history with --config. Pass only one config source.");
|
|
7322
|
+
const sourceFlag = hasHistory ? "--from-history" : "--config";
|
|
7323
|
+
if (input.yes) exitWithError(`Cannot combine --yes with ${sourceFlag}: the config already provides a complete stack. Remove --yes.`);
|
|
7324
|
+
if (input.template && input.template !== "none") exitWithError(`Cannot combine --template with ${sourceFlag}. Choose either a template or a saved config as the base.`);
|
|
7325
|
+
if (input.part && input.part.length > 0) exitWithError(`Cannot combine --part with ${sourceFlag}. Use stack parts or a saved config as the base, not both.`);
|
|
7326
|
+
if (hasConfig) {
|
|
7327
|
+
const loaded = await readBtsConfigFromFile(input.config);
|
|
7328
|
+
if (!loaded) exitWithError(`Could not load config file: ${input.config}. Ensure the path points to a valid bts.jsonc/JSON config.`);
|
|
7329
|
+
return betterTStackConfigToCreateInput(loaded);
|
|
7330
|
+
}
|
|
7331
|
+
const position = input.fromHistory;
|
|
7332
|
+
if (!Number.isInteger(position) || position < 1) exitWithError(`Invalid --from-history value: ${position}. Provide a positive integer (1 = most recent).`);
|
|
7333
|
+
const entry = await getHistoryEntry(position);
|
|
7334
|
+
if (!entry) {
|
|
7335
|
+
const count = await getHistoryCount();
|
|
7336
|
+
exitWithError(`No project history entry at position ${position}.${count === 0 ? " Project history is empty." : ` History has ${count} ${count === 1 ? "entry" : "entries"} (use 1-${count}).`}`);
|
|
7337
|
+
}
|
|
7338
|
+
return entry.config ? betterTStackConfigToCreateInput(entry.config) : historyStackToCreateInput(entry.stack);
|
|
7143
7339
|
}
|
|
7144
7340
|
|
|
7145
7341
|
//#endregion
|
|
@@ -7284,480 +7480,6 @@ function displayConfig(config) {
|
|
|
7284
7480
|
return configDisplay.join("\n");
|
|
7285
7481
|
}
|
|
7286
7482
|
|
|
7287
|
-
//#endregion
|
|
7288
|
-
//#region src/utils/generate-reproducible-command.ts
|
|
7289
|
-
function getBaseCommand(packageManager) {
|
|
7290
|
-
switch (packageManager) {
|
|
7291
|
-
case "bun": return "bun create better-fullstack@latest";
|
|
7292
|
-
case "pnpm": return "pnpm create better-fullstack@latest";
|
|
7293
|
-
case "yarn": return "yarn create better-fullstack@latest";
|
|
7294
|
-
case "npm":
|
|
7295
|
-
default: return "npx create-better-fullstack@latest";
|
|
7296
|
-
}
|
|
7297
|
-
}
|
|
7298
|
-
function formatArrayFlag(flag, values) {
|
|
7299
|
-
const normalizedValues = values.filter((value) => value !== "none");
|
|
7300
|
-
if (normalizedValues.length === 0) return `--${flag} none`;
|
|
7301
|
-
return `--${flag} ${normalizedValues.join(" ")}`;
|
|
7302
|
-
}
|
|
7303
|
-
function appendCommonFlags(flags, config) {
|
|
7304
|
-
if (config.aiDocs && config.aiDocs.length > 0) flags.push(formatArrayFlag("ai-docs", config.aiDocs));
|
|
7305
|
-
else flags.push("--ai-docs none");
|
|
7306
|
-
flags.push(config.git ? "--git" : "--no-git");
|
|
7307
|
-
flags.push(`--package-manager ${config.packageManager}`);
|
|
7308
|
-
if (config.versionChannel !== "stable") flags.push(`--version-channel ${config.versionChannel}`);
|
|
7309
|
-
flags.push(config.install ? "--install" : "--no-install");
|
|
7310
|
-
}
|
|
7311
|
-
function hasGraphPrimaryPart(config, role, ecosystem) {
|
|
7312
|
-
return config.stackParts?.some((part) => part.source !== "provided" && part.role === role && !part.ownerPartId && (!ecosystem || part.ecosystem === ecosystem));
|
|
7313
|
-
}
|
|
7314
|
-
function hasOwnedGraphPart(config, ownerRole, role, ecosystem, toolId) {
|
|
7315
|
-
const owner = config.stackParts?.find((part) => part.source !== "provided" && part.role === ownerRole && !part.ownerPartId && (!ecosystem || part.ecosystem === ecosystem));
|
|
7316
|
-
if (!owner) return false;
|
|
7317
|
-
return Boolean(config.stackParts?.some((part) => part.source !== "provided" && part.role === role && part.ownerPartId === owner.id && (!ecosystem || part.ecosystem === ecosystem) && (!toolId || part.toolId === toolId)));
|
|
7318
|
-
}
|
|
7319
|
-
function hasGraphAddonPart(config, addon) {
|
|
7320
|
-
const binding = (0, types_exports.getAddonStackPartBinding)(addon);
|
|
7321
|
-
if (!binding) return false;
|
|
7322
|
-
if (!binding.ownerRole) return Boolean(config.stackParts?.some((part) => part.source !== "provided" && part.role === binding.role && part.ecosystem === binding.ecosystem && part.toolId === addon && !part.ownerPartId));
|
|
7323
|
-
const owner = config.stackParts?.find((part) => part.source !== "provided" && part.role === binding.ownerRole && !part.ownerPartId && part.ecosystem === binding.ecosystem);
|
|
7324
|
-
if (!owner) return false;
|
|
7325
|
-
return Boolean(config.stackParts?.some((part) => part.source !== "provided" && part.role === binding.role && part.ecosystem === binding.ecosystem && part.toolId === addon && part.ownerPartId === owner.id));
|
|
7326
|
-
}
|
|
7327
|
-
function hasGraphExamplePart(config, example) {
|
|
7328
|
-
return Boolean(config.stackParts?.some((part) => part.source !== "provided" && part.role === "examples" && part.ecosystem === "universal" && part.toolId === example && !part.ownerPartId));
|
|
7329
|
-
}
|
|
7330
|
-
function hasGraphArrayParts(config, values, hasPart) {
|
|
7331
|
-
const normalizedValues = values.filter((value) => value !== "none");
|
|
7332
|
-
return normalizedValues.length > 0 && normalizedValues.every((value) => hasPart(config, value));
|
|
7333
|
-
}
|
|
7334
|
-
function appendChangedStringFlag(flags, flag, value, defaultValue) {
|
|
7335
|
-
if (value !== defaultValue) flags.push(`--${flag} ${value}`);
|
|
7336
|
-
}
|
|
7337
|
-
function appendChangedGraphStringFlag(flags, config, role, ecosystem, flag, value, defaultValue) {
|
|
7338
|
-
if (hasGraphPart(config, role, ecosystem)) return;
|
|
7339
|
-
appendChangedStringFlag(flags, flag, value, defaultValue);
|
|
7340
|
-
}
|
|
7341
|
-
function appendChangedOwnedGraphStringFlag(flags, config, ownerRole, role, ecosystem, flag, value, defaultValue) {
|
|
7342
|
-
if (hasOwnedGraphPart(config, ownerRole, role, ecosystem, value)) return;
|
|
7343
|
-
appendChangedStringFlag(flags, flag, value, defaultValue);
|
|
7344
|
-
}
|
|
7345
|
-
function hasOwnedGraphArrayParts(config, ownerRole, role, ecosystem, values) {
|
|
7346
|
-
const normalizedValues = values.filter((value) => value !== "none");
|
|
7347
|
-
if (normalizedValues.length === 0) return false;
|
|
7348
|
-
return normalizedValues.every((value) => hasOwnedGraphPart(config, ownerRole, role, ecosystem, value));
|
|
7349
|
-
}
|
|
7350
|
-
function appendChangedOwnedGraphArrayFlag(flags, config, ownerRole, role, ecosystem, flag, values, defaultValues) {
|
|
7351
|
-
if (hasOwnedGraphArrayParts(config, ownerRole, role, ecosystem, values)) return;
|
|
7352
|
-
appendChangedArrayFlag(flags, flag, values, defaultValues);
|
|
7353
|
-
}
|
|
7354
|
-
function appendChangedArrayFlag(flags, flag, values, defaultValues) {
|
|
7355
|
-
if (values.length !== defaultValues.length || values.some((value, index) => value !== defaultValues[index])) flags.push(formatArrayFlag(flag, values));
|
|
7356
|
-
}
|
|
7357
|
-
function appendAstroIntegrationFlag(flags, config) {
|
|
7358
|
-
if (config.frontend.includes("astro") && config.astroIntegration !== "none") flags.push(`--astro-integration ${config.astroIntegration}`);
|
|
7359
|
-
}
|
|
7360
|
-
function appendGraphExtraFlags(flags, config) {
|
|
7361
|
-
if (!hasGraphArrayParts(config, config.addons, hasGraphAddonPart)) appendChangedArrayFlag(flags, "addons", config.addons, ["turborepo"]);
|
|
7362
|
-
if (!hasGraphArrayParts(config, config.examples, hasGraphExamplePart)) appendChangedArrayFlag(flags, "examples", config.examples, []);
|
|
7363
|
-
if (hasGraphPrimaryPart(config, "database")) appendChangedOwnedGraphStringFlag(flags, config, "database", "dbSetup", "universal", "db-setup", config.dbSetup, "none");
|
|
7364
|
-
else appendChangedStringFlag(flags, "db-setup", config.dbSetup, "none");
|
|
7365
|
-
if (hasGraphPrimaryPart(config, "frontend", "typescript")) {
|
|
7366
|
-
appendAstroIntegrationFlag(flags, config);
|
|
7367
|
-
appendChangedOwnedGraphStringFlag(flags, config, "frontend", "deploy", "typescript", "web-deploy", config.webDeploy, "none");
|
|
7368
|
-
appendChangedGraphStringFlag(flags, config, "css", "typescript", "css-framework", config.cssFramework, "tailwind");
|
|
7369
|
-
appendChangedGraphStringFlag(flags, config, "ui", "typescript", "ui-library", config.uiLibrary, "shadcn-ui");
|
|
7370
|
-
if (config.uiLibrary === "shadcn-ui") {
|
|
7371
|
-
appendChangedStringFlag(flags, "shadcn-base", config.shadcnBase ?? "radix", "radix");
|
|
7372
|
-
appendChangedStringFlag(flags, "shadcn-style", config.shadcnStyle ?? "nova", "nova");
|
|
7373
|
-
appendChangedStringFlag(flags, "shadcn-icon-library", config.shadcnIconLibrary ?? "lucide", "lucide");
|
|
7374
|
-
appendChangedStringFlag(flags, "shadcn-color-theme", config.shadcnColorTheme ?? "neutral", "neutral");
|
|
7375
|
-
appendChangedStringFlag(flags, "shadcn-base-color", config.shadcnBaseColor ?? "neutral", "neutral");
|
|
7376
|
-
appendChangedStringFlag(flags, "shadcn-font", config.shadcnFont ?? "inter", "inter");
|
|
7377
|
-
appendChangedStringFlag(flags, "shadcn-radius", config.shadcnRadius ?? "default", "default");
|
|
7378
|
-
}
|
|
7379
|
-
appendChangedGraphStringFlag(flags, config, "stateManagement", "typescript", "state-management", config.stateManagement, "none");
|
|
7380
|
-
appendChangedGraphStringFlag(flags, config, "forms", "typescript", "forms", config.forms, "react-hook-form");
|
|
7381
|
-
appendChangedStringFlag(flags, "validation", config.validation, "zod");
|
|
7382
|
-
appendChangedStringFlag(flags, "testing", config.testing, "vitest");
|
|
7383
|
-
appendChangedGraphStringFlag(flags, config, "animation", "typescript", "animation", config.animation, "none");
|
|
7384
|
-
appendChangedGraphStringFlag(flags, config, "fileUpload", "typescript", "file-upload", config.fileUpload, "none");
|
|
7385
|
-
appendChangedGraphStringFlag(flags, config, "i18n", "typescript", "i18n", config.i18n, "none");
|
|
7386
|
-
appendChangedGraphStringFlag(flags, config, "analytics", "typescript", "analytics", config.analytics, "none");
|
|
7387
|
-
}
|
|
7388
|
-
if (hasGraphPrimaryPart(config, "frontend", "typescript") || hasGraphPrimaryPart(config, "backend", "typescript")) {
|
|
7389
|
-
if (hasGraphPrimaryPart(config, "backend", "typescript")) {
|
|
7390
|
-
appendChangedOwnedGraphStringFlag(flags, config, "backend", "runtime", "typescript", "runtime", config.runtime, "bun");
|
|
7391
|
-
appendChangedOwnedGraphStringFlag(flags, config, "backend", "deploy", "typescript", "server-deploy", config.serverDeploy, "none");
|
|
7392
|
-
} else appendChangedStringFlag(flags, "server-deploy", config.serverDeploy, "none");
|
|
7393
|
-
appendChangedGraphStringFlag(flags, config, "payments", "typescript", "payments", config.payments, "none");
|
|
7394
|
-
appendChangedGraphStringFlag(flags, config, "email", "typescript", "email", config.email, "none");
|
|
7395
|
-
appendChangedStringFlag(flags, "effect", config.effect, "none");
|
|
7396
|
-
appendChangedGraphStringFlag(flags, config, "ai", "typescript", "ai", config.ai, "none");
|
|
7397
|
-
appendChangedGraphStringFlag(flags, config, "realtime", "typescript", "realtime", config.realtime, "none");
|
|
7398
|
-
appendChangedGraphStringFlag(flags, config, "jobQueue", "typescript", "job-queue", config.jobQueue, "none");
|
|
7399
|
-
appendChangedGraphStringFlag(flags, config, "logging", "typescript", "logging", config.logging, "none");
|
|
7400
|
-
appendChangedGraphStringFlag(flags, config, "observability", "typescript", "observability", config.observability, "none");
|
|
7401
|
-
appendChangedGraphStringFlag(flags, config, "featureFlags", "typescript", "feature-flags", config.featureFlags, "none");
|
|
7402
|
-
appendChangedGraphStringFlag(flags, config, "caching", "typescript", "caching", config.caching, "none");
|
|
7403
|
-
appendChangedGraphStringFlag(flags, config, "rateLimit", "typescript", "rate-limit", config.rateLimit, "none");
|
|
7404
|
-
appendChangedGraphStringFlag(flags, config, "cms", "typescript", "cms", config.cms, "none");
|
|
7405
|
-
appendChangedGraphStringFlag(flags, config, "search", "typescript", "search", config.search, "none");
|
|
7406
|
-
appendChangedGraphStringFlag(flags, config, "fileStorage", "typescript", "file-storage", config.fileStorage, "none");
|
|
7407
|
-
}
|
|
7408
|
-
if (hasGraphPrimaryPart(config, "mobile")) {
|
|
7409
|
-
appendChangedOwnedGraphStringFlag(flags, config, "mobile", "navigation", "react-native", "mobile-navigation", config.mobileNavigation, "expo-router");
|
|
7410
|
-
appendChangedOwnedGraphStringFlag(flags, config, "mobile", "ui", "react-native", "mobile-ui", config.mobileUI, "none");
|
|
7411
|
-
appendChangedOwnedGraphStringFlag(flags, config, "mobile", "storage", "react-native", "mobile-storage", config.mobileStorage, "none");
|
|
7412
|
-
appendChangedOwnedGraphStringFlag(flags, config, "mobile", "testing", "react-native", "mobile-testing", config.mobileTesting, "none");
|
|
7413
|
-
appendChangedStringFlag(flags, "mobile-push", config.mobilePush, "none");
|
|
7414
|
-
appendChangedStringFlag(flags, "mobile-ota", config.mobileOTA, "none");
|
|
7415
|
-
appendChangedStringFlag(flags, "mobile-deep-linking", config.mobileDeepLinking, "none");
|
|
7416
|
-
}
|
|
7417
|
-
if (hasGraphPrimaryPart(config, "frontend", "rust")) appendChangedStringFlag(flags, "rust-frontend", config.rustFrontend, "none");
|
|
7418
|
-
if (hasGraphPrimaryPart(config, "backend", "rust")) {
|
|
7419
|
-
appendChangedOwnedGraphStringFlag(flags, config, "backend", "cli", "rust", "rust-cli", config.rustCli, "none");
|
|
7420
|
-
appendChangedOwnedGraphArrayFlag(flags, config, "backend", "libraries", "rust", "rust-libraries", config.rustLibraries, []);
|
|
7421
|
-
appendChangedOwnedGraphStringFlag(flags, config, "backend", "logging", "rust", "rust-logging", config.rustLogging, "tracing");
|
|
7422
|
-
appendChangedOwnedGraphStringFlag(flags, config, "backend", "errorHandling", "rust", "rust-error-handling", config.rustErrorHandling, "anyhow-thiserror");
|
|
7423
|
-
appendChangedOwnedGraphStringFlag(flags, config, "backend", "caching", "rust", "rust-caching", config.rustCaching, "none");
|
|
7424
|
-
}
|
|
7425
|
-
if (hasGraphPrimaryPart(config, "backend", "python")) {
|
|
7426
|
-
appendChangedOwnedGraphStringFlag(flags, config, "backend", "validation", "python", "python-validation", config.pythonValidation, "none");
|
|
7427
|
-
appendChangedOwnedGraphArrayFlag(flags, config, "backend", "ai", "python", "python-ai", config.pythonAi, []);
|
|
7428
|
-
appendChangedOwnedGraphStringFlag(flags, config, "backend", "jobQueue", "python", "python-task-queue", config.pythonTaskQueue, "none");
|
|
7429
|
-
appendChangedOwnedGraphStringFlag(flags, config, "backend", "api", "python", "python-graphql", config.pythonGraphql, "none");
|
|
7430
|
-
appendChangedOwnedGraphStringFlag(flags, config, "backend", "codeQuality", "python", "python-quality", config.pythonQuality, "none");
|
|
7431
|
-
}
|
|
7432
|
-
if (hasGraphPrimaryPart(config, "backend", "go")) {
|
|
7433
|
-
appendChangedOwnedGraphStringFlag(flags, config, "backend", "cli", "go", "go-cli", config.goCli, "none");
|
|
7434
|
-
appendChangedOwnedGraphStringFlag(flags, config, "backend", "logging", "go", "go-logging", config.goLogging, "none");
|
|
7435
|
-
}
|
|
7436
|
-
if (hasGraphPrimaryPart(config, "backend", "java")) {
|
|
7437
|
-
appendChangedOwnedGraphStringFlag(flags, config, "backend", "buildTool", "java", "java-build-tool", config.javaBuildTool, "maven");
|
|
7438
|
-
appendChangedOwnedGraphArrayFlag(flags, config, "backend", "libraries", "java", "java-libraries", config.javaLibraries, []);
|
|
7439
|
-
appendChangedOwnedGraphArrayFlag(flags, config, "backend", "testing", "java", "java-testing-libraries", config.javaTestingLibraries, ["junit5"]);
|
|
7440
|
-
}
|
|
7441
|
-
if (hasGraphPrimaryPart(config, "backend", "elixir")) {
|
|
7442
|
-
appendChangedGraphStringFlag(flags, config, "realtime", "elixir", "elixir-realtime", config.elixirRealtime, "channels");
|
|
7443
|
-
appendChangedGraphStringFlag(flags, config, "jobQueue", "elixir", "elixir-jobs", config.elixirJobs, "none");
|
|
7444
|
-
appendChangedGraphStringFlag(flags, config, "validation", "elixir", "elixir-validation", config.elixirValidation, "ecto-changesets");
|
|
7445
|
-
appendChangedOwnedGraphStringFlag(flags, config, "backend", "httpClient", "elixir", "elixir-http", config.elixirHttp, "req");
|
|
7446
|
-
appendChangedStringFlag(flags, "elixir-json", config.elixirJson, "jason");
|
|
7447
|
-
appendChangedGraphStringFlag(flags, config, "email", "elixir", "elixir-email", config.elixirEmail, "none");
|
|
7448
|
-
appendChangedGraphStringFlag(flags, config, "caching", "elixir", "elixir-caching", config.elixirCaching, "none");
|
|
7449
|
-
appendChangedGraphStringFlag(flags, config, "observability", "elixir", "elixir-observability", config.elixirObservability, "telemetry");
|
|
7450
|
-
appendChangedGraphStringFlag(flags, config, "testing", "elixir", "elixir-testing", config.elixirTesting, "ex_unit");
|
|
7451
|
-
appendChangedOwnedGraphStringFlag(flags, config, "backend", "codeQuality", "elixir", "elixir-quality", config.elixirQuality, "credo");
|
|
7452
|
-
appendChangedGraphStringFlag(flags, config, "deploy", "elixir", "elixir-deploy", config.elixirDeploy, "none");
|
|
7453
|
-
}
|
|
7454
|
-
}
|
|
7455
|
-
function appendSharedNonTypeScriptFlags(flags, config) {
|
|
7456
|
-
flags.push(`--email ${config.email}`);
|
|
7457
|
-
flags.push(`--observability ${config.observability}`);
|
|
7458
|
-
flags.push(`--caching ${config.caching}`);
|
|
7459
|
-
flags.push(`--search ${config.search}`);
|
|
7460
|
-
flags.push(formatArrayFlag("addons", config.addons));
|
|
7461
|
-
flags.push(formatArrayFlag("examples", config.examples));
|
|
7462
|
-
flags.push(`--db-setup ${config.dbSetup}`);
|
|
7463
|
-
flags.push(`--web-deploy ${config.webDeploy}`);
|
|
7464
|
-
flags.push(`--server-deploy ${config.serverDeploy}`);
|
|
7465
|
-
}
|
|
7466
|
-
function getTypeScriptFlags(config) {
|
|
7467
|
-
const flags = [];
|
|
7468
|
-
if (config.frontend && config.frontend.length > 0) flags.push(`--frontend ${config.frontend.join(" ")}`);
|
|
7469
|
-
else flags.push("--frontend none");
|
|
7470
|
-
appendAstroIntegrationFlag(flags, config);
|
|
7471
|
-
flags.push(`--backend ${config.backend}`);
|
|
7472
|
-
flags.push(`--runtime ${config.runtime}`);
|
|
7473
|
-
flags.push(`--database ${config.database}`);
|
|
7474
|
-
flags.push(`--orm ${config.orm}`);
|
|
7475
|
-
flags.push(`--api ${config.api}`);
|
|
7476
|
-
flags.push(`--auth ${config.auth}`);
|
|
7477
|
-
flags.push(`--payments ${config.payments}`);
|
|
7478
|
-
flags.push(`--email ${config.email}`);
|
|
7479
|
-
flags.push(`--file-upload ${config.fileUpload}`);
|
|
7480
|
-
flags.push(`--effect ${config.effect}`);
|
|
7481
|
-
flags.push(`--css-framework ${config.cssFramework}`);
|
|
7482
|
-
flags.push(`--ui-library ${config.uiLibrary}`);
|
|
7483
|
-
if (config.uiLibrary === "shadcn-ui") {
|
|
7484
|
-
flags.push(`--shadcn-base ${config.shadcnBase}`);
|
|
7485
|
-
flags.push(`--shadcn-style ${config.shadcnStyle}`);
|
|
7486
|
-
flags.push(`--shadcn-icon-library ${config.shadcnIconLibrary}`);
|
|
7487
|
-
flags.push(`--shadcn-color-theme ${config.shadcnColorTheme}`);
|
|
7488
|
-
flags.push(`--shadcn-base-color ${config.shadcnBaseColor}`);
|
|
7489
|
-
flags.push(`--shadcn-font ${config.shadcnFont}`);
|
|
7490
|
-
flags.push(`--shadcn-radius ${config.shadcnRadius}`);
|
|
7491
|
-
}
|
|
7492
|
-
flags.push(`--ai ${config.ai}`);
|
|
7493
|
-
flags.push(`--state-management ${config.stateManagement}`);
|
|
7494
|
-
flags.push(`--forms ${config.forms}`);
|
|
7495
|
-
flags.push(`--validation ${config.validation}`);
|
|
7496
|
-
flags.push(`--testing ${config.testing}`);
|
|
7497
|
-
flags.push(`--animation ${config.animation}`);
|
|
7498
|
-
flags.push(`--realtime ${config.realtime}`);
|
|
7499
|
-
flags.push(`--job-queue ${config.jobQueue}`);
|
|
7500
|
-
flags.push(`--logging ${config.logging}`);
|
|
7501
|
-
flags.push(`--observability ${config.observability}`);
|
|
7502
|
-
flags.push(`--feature-flags ${config.featureFlags}`);
|
|
7503
|
-
flags.push(`--caching ${config.caching}`);
|
|
7504
|
-
flags.push(`--rate-limit ${config.rateLimit}`);
|
|
7505
|
-
flags.push(`--i18n ${config.i18n}`);
|
|
7506
|
-
flags.push(`--cms ${config.cms}`);
|
|
7507
|
-
flags.push(`--search ${config.search}`);
|
|
7508
|
-
flags.push(`--file-storage ${config.fileStorage}`);
|
|
7509
|
-
flags.push(`--mobile-navigation ${config.mobileNavigation}`);
|
|
7510
|
-
flags.push(`--mobile-ui ${config.mobileUI}`);
|
|
7511
|
-
flags.push(`--mobile-storage ${config.mobileStorage}`);
|
|
7512
|
-
flags.push(`--mobile-testing ${config.mobileTesting}`);
|
|
7513
|
-
flags.push(`--mobile-push ${config.mobilePush}`);
|
|
7514
|
-
flags.push(`--mobile-ota ${config.mobileOTA}`);
|
|
7515
|
-
flags.push(`--mobile-deep-linking ${config.mobileDeepLinking}`);
|
|
7516
|
-
if (config.addons && config.addons.length > 0) flags.push(`--addons ${config.addons.join(" ")}`);
|
|
7517
|
-
else flags.push("--addons none");
|
|
7518
|
-
if (config.examples && config.examples.length > 0) flags.push(`--examples ${config.examples.join(" ")}`);
|
|
7519
|
-
else flags.push("--examples none");
|
|
7520
|
-
flags.push(`--db-setup ${config.dbSetup}`);
|
|
7521
|
-
flags.push(`--web-deploy ${config.webDeploy}`);
|
|
7522
|
-
flags.push(`--server-deploy ${config.serverDeploy}`);
|
|
7523
|
-
appendCommonFlags(flags, config);
|
|
7524
|
-
return flags;
|
|
7525
|
-
}
|
|
7526
|
-
function getReactNativeFlags(config) {
|
|
7527
|
-
const flags = ["--ecosystem react-native"];
|
|
7528
|
-
if (config.frontend && config.frontend.length > 0) flags.push(`--frontend ${config.frontend.join(" ")}`);
|
|
7529
|
-
else flags.push("--frontend native-bare");
|
|
7530
|
-
flags.push(`--auth ${config.auth}`);
|
|
7531
|
-
flags.push(`--mobile-navigation ${config.mobileNavigation}`);
|
|
7532
|
-
flags.push(`--mobile-ui ${config.mobileUI}`);
|
|
7533
|
-
flags.push(`--mobile-storage ${config.mobileStorage}`);
|
|
7534
|
-
flags.push(`--mobile-testing ${config.mobileTesting}`);
|
|
7535
|
-
flags.push(`--mobile-push ${config.mobilePush}`);
|
|
7536
|
-
flags.push(`--mobile-ota ${config.mobileOTA}`);
|
|
7537
|
-
flags.push(`--mobile-deep-linking ${config.mobileDeepLinking}`);
|
|
7538
|
-
appendCommonFlags(flags, config);
|
|
7539
|
-
return flags;
|
|
7540
|
-
}
|
|
7541
|
-
function getRustFlags(config) {
|
|
7542
|
-
const flags = ["--ecosystem rust"];
|
|
7543
|
-
flags.push(`--rust-web-framework ${config.rustWebFramework}`);
|
|
7544
|
-
flags.push(`--rust-frontend ${config.rustFrontend}`);
|
|
7545
|
-
flags.push(`--rust-orm ${config.rustOrm}`);
|
|
7546
|
-
flags.push(`--rust-api ${config.rustApi}`);
|
|
7547
|
-
flags.push(`--rust-cli ${config.rustCli}`);
|
|
7548
|
-
flags.push(formatArrayFlag("rust-libraries", config.rustLibraries));
|
|
7549
|
-
flags.push(`--rust-logging ${config.rustLogging}`);
|
|
7550
|
-
flags.push(`--rust-error-handling ${config.rustErrorHandling}`);
|
|
7551
|
-
flags.push(`--rust-caching ${config.rustCaching}`);
|
|
7552
|
-
flags.push(`--rust-auth ${config.rustAuth}`);
|
|
7553
|
-
flags.push(`--rust-realtime ${config.rustRealtime}`);
|
|
7554
|
-
flags.push(`--rust-message-queue ${config.rustMessageQueue}`);
|
|
7555
|
-
flags.push(`--rust-observability ${config.rustObservability}`);
|
|
7556
|
-
flags.push(`--rust-templating ${config.rustTemplating}`);
|
|
7557
|
-
appendSharedNonTypeScriptFlags(flags, config);
|
|
7558
|
-
appendCommonFlags(flags, config);
|
|
7559
|
-
return flags;
|
|
7560
|
-
}
|
|
7561
|
-
function getPythonFlags(config) {
|
|
7562
|
-
const flags = ["--ecosystem python"];
|
|
7563
|
-
flags.push(`--python-web-framework ${config.pythonWebFramework}`);
|
|
7564
|
-
flags.push(`--python-orm ${config.pythonOrm}`);
|
|
7565
|
-
flags.push(`--python-validation ${config.pythonValidation}`);
|
|
7566
|
-
flags.push(formatArrayFlag("python-ai", config.pythonAi));
|
|
7567
|
-
flags.push(`--python-auth ${config.pythonAuth}`);
|
|
7568
|
-
flags.push(`--python-api ${config.pythonApi}`);
|
|
7569
|
-
flags.push(`--python-task-queue ${config.pythonTaskQueue}`);
|
|
7570
|
-
flags.push(`--python-graphql ${config.pythonGraphql}`);
|
|
7571
|
-
flags.push(`--python-quality ${config.pythonQuality}`);
|
|
7572
|
-
flags.push(formatArrayFlag("python-testing", config.pythonTesting));
|
|
7573
|
-
flags.push(`--python-caching ${config.pythonCaching}`);
|
|
7574
|
-
flags.push(`--python-realtime ${config.pythonRealtime}`);
|
|
7575
|
-
flags.push(`--python-observability ${config.pythonObservability}`);
|
|
7576
|
-
flags.push(formatArrayFlag("python-cli", config.pythonCli));
|
|
7577
|
-
appendSharedNonTypeScriptFlags(flags, config);
|
|
7578
|
-
appendCommonFlags(flags, config);
|
|
7579
|
-
return flags;
|
|
7580
|
-
}
|
|
7581
|
-
function getGoFlags(config) {
|
|
7582
|
-
const flags = ["--ecosystem go"];
|
|
7583
|
-
flags.push(`--go-web-framework ${config.goWebFramework}`);
|
|
7584
|
-
flags.push(`--go-orm ${config.goOrm}`);
|
|
7585
|
-
flags.push(`--go-api ${config.goApi}`);
|
|
7586
|
-
flags.push(`--go-cli ${config.goCli}`);
|
|
7587
|
-
flags.push(`--go-logging ${config.goLogging}`);
|
|
7588
|
-
flags.push(`--go-auth ${config.goAuth}`);
|
|
7589
|
-
flags.push(formatArrayFlag("go-testing", config.goTesting));
|
|
7590
|
-
flags.push(`--go-realtime ${config.goRealtime}`);
|
|
7591
|
-
flags.push(`--go-message-queue ${config.goMessageQueue}`);
|
|
7592
|
-
flags.push(`--go-caching ${config.goCaching}`);
|
|
7593
|
-
flags.push(`--go-config ${config.goConfig}`);
|
|
7594
|
-
flags.push(`--go-observability ${config.goObservability}`);
|
|
7595
|
-
flags.push(`--auth ${config.auth}`);
|
|
7596
|
-
appendSharedNonTypeScriptFlags(flags, config);
|
|
7597
|
-
appendCommonFlags(flags, config);
|
|
7598
|
-
return flags;
|
|
7599
|
-
}
|
|
7600
|
-
function getJavaFlags(config) {
|
|
7601
|
-
const flags = ["--ecosystem java"];
|
|
7602
|
-
flags.push(`--java-web-framework ${config.javaWebFramework}`);
|
|
7603
|
-
flags.push(`--java-build-tool ${config.javaBuildTool}`);
|
|
7604
|
-
flags.push(`--java-orm ${config.javaOrm}`);
|
|
7605
|
-
flags.push(`--java-auth ${config.javaAuth}`);
|
|
7606
|
-
flags.push(`--java-api ${config.javaApi}`);
|
|
7607
|
-
flags.push(`--java-logging ${config.javaLogging}`);
|
|
7608
|
-
flags.push(formatArrayFlag("java-libraries", config.javaLibraries));
|
|
7609
|
-
flags.push(formatArrayFlag("java-testing-libraries", config.javaTestingLibraries));
|
|
7610
|
-
appendSharedNonTypeScriptFlags(flags, config);
|
|
7611
|
-
appendCommonFlags(flags, config);
|
|
7612
|
-
return flags;
|
|
7613
|
-
}
|
|
7614
|
-
function getDotnetFlags(config) {
|
|
7615
|
-
const flags = ["--ecosystem dotnet"];
|
|
7616
|
-
flags.push(`--dotnet-web-framework ${config.dotnetWebFramework}`);
|
|
7617
|
-
flags.push(`--dotnet-orm ${config.dotnetOrm}`);
|
|
7618
|
-
flags.push(`--dotnet-auth ${config.dotnetAuth}`);
|
|
7619
|
-
flags.push(`--dotnet-api ${config.dotnetApi}`);
|
|
7620
|
-
flags.push(formatArrayFlag("dotnet-testing", config.dotnetTesting));
|
|
7621
|
-
flags.push(`--dotnet-job-queue ${config.dotnetJobQueue}`);
|
|
7622
|
-
flags.push(`--dotnet-realtime ${config.dotnetRealtime}`);
|
|
7623
|
-
flags.push(formatArrayFlag("dotnet-observability", config.dotnetObservability));
|
|
7624
|
-
flags.push(`--dotnet-validation ${config.dotnetValidation}`);
|
|
7625
|
-
flags.push(`--dotnet-caching ${config.dotnetCaching}`);
|
|
7626
|
-
flags.push(`--dotnet-deploy ${config.dotnetDeploy}`);
|
|
7627
|
-
appendCommonFlags(flags, config);
|
|
7628
|
-
return flags;
|
|
7629
|
-
}
|
|
7630
|
-
function getElixirFlags(config) {
|
|
7631
|
-
const flags = ["--ecosystem elixir"];
|
|
7632
|
-
flags.push(`--elixir-web-framework ${config.elixirWebFramework}`);
|
|
7633
|
-
flags.push(`--elixir-orm ${config.elixirOrm}`);
|
|
7634
|
-
flags.push(`--elixir-auth ${config.elixirAuth}`);
|
|
7635
|
-
flags.push(`--elixir-api ${config.elixirApi}`);
|
|
7636
|
-
flags.push(`--elixir-realtime ${config.elixirRealtime}`);
|
|
7637
|
-
flags.push(`--elixir-jobs ${config.elixirJobs}`);
|
|
7638
|
-
flags.push(`--elixir-validation ${config.elixirValidation}`);
|
|
7639
|
-
flags.push(`--elixir-http ${config.elixirHttp}`);
|
|
7640
|
-
flags.push(`--elixir-json ${config.elixirJson}`);
|
|
7641
|
-
flags.push(`--elixir-email ${config.elixirEmail}`);
|
|
7642
|
-
flags.push(`--elixir-caching ${config.elixirCaching}`);
|
|
7643
|
-
flags.push(`--elixir-observability ${config.elixirObservability}`);
|
|
7644
|
-
flags.push(`--elixir-testing ${config.elixirTesting}`);
|
|
7645
|
-
flags.push(`--elixir-quality ${config.elixirQuality}`);
|
|
7646
|
-
flags.push(`--elixir-deploy ${config.elixirDeploy}`);
|
|
7647
|
-
flags.push(formatArrayFlag("elixir-libraries", config.elixirLibraries));
|
|
7648
|
-
appendCommonFlags(flags, config);
|
|
7649
|
-
return flags;
|
|
7650
|
-
}
|
|
7651
|
-
function generateReproducibleCommand(config) {
|
|
7652
|
-
let flags;
|
|
7653
|
-
if (config.stackParts && config.stackParts.length > 0) {
|
|
7654
|
-
flags = config.stackParts.filter((part) => part.source !== "provided").map((part) => `--part ${(0, types_exports.formatStackPartSpec)(part, config.stackParts ?? [])}`);
|
|
7655
|
-
appendGraphExtraFlags(flags, config);
|
|
7656
|
-
appendCommonFlags(flags, config);
|
|
7657
|
-
return `${getBaseCommand(config.packageManager)}${config.relativePath ? ` ${config.relativePath}` : ""} ${flags.join(" ")}`;
|
|
7658
|
-
}
|
|
7659
|
-
switch (config.ecosystem) {
|
|
7660
|
-
case "react-native":
|
|
7661
|
-
flags = getReactNativeFlags(config);
|
|
7662
|
-
break;
|
|
7663
|
-
case "rust":
|
|
7664
|
-
flags = getRustFlags(config);
|
|
7665
|
-
break;
|
|
7666
|
-
case "python":
|
|
7667
|
-
flags = getPythonFlags(config);
|
|
7668
|
-
break;
|
|
7669
|
-
case "go":
|
|
7670
|
-
flags = getGoFlags(config);
|
|
7671
|
-
break;
|
|
7672
|
-
case "java":
|
|
7673
|
-
flags = getJavaFlags(config);
|
|
7674
|
-
break;
|
|
7675
|
-
case "dotnet":
|
|
7676
|
-
flags = getDotnetFlags(config);
|
|
7677
|
-
break;
|
|
7678
|
-
case "elixir":
|
|
7679
|
-
flags = getElixirFlags(config);
|
|
7680
|
-
break;
|
|
7681
|
-
case "typescript":
|
|
7682
|
-
default:
|
|
7683
|
-
flags = getTypeScriptFlags(config);
|
|
7684
|
-
break;
|
|
7685
|
-
}
|
|
7686
|
-
return `${getBaseCommand(config.packageManager)}${config.relativePath ? ` ${config.relativePath}` : ""} ${flags.join(" ")}`;
|
|
7687
|
-
}
|
|
7688
|
-
|
|
7689
|
-
//#endregion
|
|
7690
|
-
//#region src/utils/generated-checks.ts
|
|
7691
|
-
function getGraphTarget(config) {
|
|
7692
|
-
const backend = getPrimaryGraphPart(config, "backend");
|
|
7693
|
-
if (!backend || backend.ecosystem === "typescript" || backend.ecosystem === "react-native" || backend.ecosystem === "universal") return null;
|
|
7694
|
-
return {
|
|
7695
|
-
ecosystem: backend.ecosystem,
|
|
7696
|
-
projectDir: path.join(config.projectDir, backend.targetPath ?? "apps/server")
|
|
7697
|
-
};
|
|
7698
|
-
}
|
|
7699
|
-
function getSingleEcosystemTarget(config) {
|
|
7700
|
-
if (config.ecosystem === "typescript" || config.ecosystem === "react-native" || config.ecosystem === "java") return null;
|
|
7701
|
-
return {
|
|
7702
|
-
ecosystem: config.ecosystem,
|
|
7703
|
-
projectDir: config.projectDir
|
|
7704
|
-
};
|
|
7705
|
-
}
|
|
7706
|
-
async function runCommand(cwd, command, args) {
|
|
7707
|
-
await $({
|
|
7708
|
-
cwd,
|
|
7709
|
-
stdout: "inherit",
|
|
7710
|
-
stderr: "inherit"
|
|
7711
|
-
})`${command} ${args}`;
|
|
7712
|
-
}
|
|
7713
|
-
async function verifyTarget(target) {
|
|
7714
|
-
const s = spinner();
|
|
7715
|
-
const cwd = target.projectDir;
|
|
7716
|
-
switch (target.ecosystem) {
|
|
7717
|
-
case "go":
|
|
7718
|
-
s.start("Verifying generated Go server...");
|
|
7719
|
-
await runCommand(cwd, "go", ["mod", "tidy"]);
|
|
7720
|
-
await runCommand(cwd, "go", ["test", "./..."]);
|
|
7721
|
-
s.stop("Generated Go server checks passed");
|
|
7722
|
-
return;
|
|
7723
|
-
case "rust":
|
|
7724
|
-
s.start("Verifying generated Rust server...");
|
|
7725
|
-
await runCommand(cwd, "cargo", ["check"]);
|
|
7726
|
-
s.stop("Generated Rust server checks passed");
|
|
7727
|
-
return;
|
|
7728
|
-
case "python":
|
|
7729
|
-
s.start("Verifying generated Python server...");
|
|
7730
|
-
await runCommand(cwd, "uv", ["sync"]);
|
|
7731
|
-
await runCommand(cwd, "uv", [
|
|
7732
|
-
"run",
|
|
7733
|
-
"ruff",
|
|
7734
|
-
"check",
|
|
7735
|
-
"."
|
|
7736
|
-
]);
|
|
7737
|
-
s.stop("Generated Python server checks passed");
|
|
7738
|
-
return;
|
|
7739
|
-
case "elixir":
|
|
7740
|
-
if (!await commandExists("mix")) {
|
|
7741
|
-
log.warn(pc.yellow("Skipping Elixir verification because mix is not on PATH"));
|
|
7742
|
-
return;
|
|
7743
|
-
}
|
|
7744
|
-
s.start("Verifying generated Elixir server...");
|
|
7745
|
-
await runCommand(cwd, "mix", ["deps.get"]);
|
|
7746
|
-
await runCommand(cwd, "mix", ["compile"]);
|
|
7747
|
-
s.stop("Generated Elixir server checks passed");
|
|
7748
|
-
return;
|
|
7749
|
-
default: log.warn(pc.yellow(`No generated checks are configured for ${target.ecosystem}`));
|
|
7750
|
-
}
|
|
7751
|
-
}
|
|
7752
|
-
async function runGeneratedChecks(config) {
|
|
7753
|
-
const target = getGraphTarget(config) ?? getSingleEcosystemTarget(config);
|
|
7754
|
-
if (!target) {
|
|
7755
|
-
log.warn(pc.yellow("No generated checks are configured for this stack"));
|
|
7756
|
-
return;
|
|
7757
|
-
}
|
|
7758
|
-
await verifyTarget(target);
|
|
7759
|
-
}
|
|
7760
|
-
|
|
7761
7483
|
//#endregion
|
|
7762
7484
|
//#region src/utils/preflight-display.ts
|
|
7763
7485
|
function displayPreflightWarnings({ warnings }) {
|
|
@@ -7861,87 +7583,6 @@ async function setupProjectDirectory(finalPathInput, shouldClearDirectory) {
|
|
|
7861
7583
|
};
|
|
7862
7584
|
}
|
|
7863
7585
|
|
|
7864
|
-
//#endregion
|
|
7865
|
-
//#region src/utils/templates.ts
|
|
7866
|
-
const TEMPLATE_PRESETS = {
|
|
7867
|
-
mern: {
|
|
7868
|
-
database: "mongodb",
|
|
7869
|
-
orm: "mongoose",
|
|
7870
|
-
backend: "express",
|
|
7871
|
-
runtime: "node",
|
|
7872
|
-
frontend: ["react-router"],
|
|
7873
|
-
api: "orpc",
|
|
7874
|
-
auth: "better-auth",
|
|
7875
|
-
payments: "none",
|
|
7876
|
-
addons: ["turborepo"],
|
|
7877
|
-
examples: ["none"],
|
|
7878
|
-
dbSetup: "mongodb-atlas",
|
|
7879
|
-
webDeploy: "none",
|
|
7880
|
-
serverDeploy: "none"
|
|
7881
|
-
},
|
|
7882
|
-
pern: {
|
|
7883
|
-
database: "postgres",
|
|
7884
|
-
orm: "drizzle",
|
|
7885
|
-
backend: "express",
|
|
7886
|
-
runtime: "node",
|
|
7887
|
-
frontend: ["tanstack-router"],
|
|
7888
|
-
api: "trpc",
|
|
7889
|
-
auth: "better-auth",
|
|
7890
|
-
payments: "none",
|
|
7891
|
-
addons: ["turborepo"],
|
|
7892
|
-
examples: ["none"],
|
|
7893
|
-
dbSetup: "none",
|
|
7894
|
-
webDeploy: "none",
|
|
7895
|
-
serverDeploy: "none"
|
|
7896
|
-
},
|
|
7897
|
-
t3: {
|
|
7898
|
-
database: "postgres",
|
|
7899
|
-
orm: "prisma",
|
|
7900
|
-
backend: "self",
|
|
7901
|
-
runtime: "none",
|
|
7902
|
-
frontend: ["next"],
|
|
7903
|
-
api: "trpc",
|
|
7904
|
-
auth: "better-auth",
|
|
7905
|
-
payments: "none",
|
|
7906
|
-
addons: ["biome", "turborepo"],
|
|
7907
|
-
examples: ["none"],
|
|
7908
|
-
dbSetup: "none",
|
|
7909
|
-
webDeploy: "none",
|
|
7910
|
-
serverDeploy: "none"
|
|
7911
|
-
},
|
|
7912
|
-
uniwind: {
|
|
7913
|
-
database: "none",
|
|
7914
|
-
orm: "none",
|
|
7915
|
-
backend: "none",
|
|
7916
|
-
runtime: "none",
|
|
7917
|
-
frontend: ["native-uniwind"],
|
|
7918
|
-
api: "none",
|
|
7919
|
-
auth: "none",
|
|
7920
|
-
payments: "none",
|
|
7921
|
-
addons: ["none"],
|
|
7922
|
-
examples: ["none"],
|
|
7923
|
-
dbSetup: "none",
|
|
7924
|
-
webDeploy: "none",
|
|
7925
|
-
serverDeploy: "none"
|
|
7926
|
-
},
|
|
7927
|
-
none: null
|
|
7928
|
-
};
|
|
7929
|
-
function getTemplateConfig(template) {
|
|
7930
|
-
if (template === "none" || !template) return null;
|
|
7931
|
-
const config = TEMPLATE_PRESETS[template];
|
|
7932
|
-
if (!config) throw new Error(`Unknown template: ${template}`);
|
|
7933
|
-
return config;
|
|
7934
|
-
}
|
|
7935
|
-
function getTemplateDescription(template) {
|
|
7936
|
-
return {
|
|
7937
|
-
mern: "MongoDB + Express + React + Node.js - Classic MERN stack",
|
|
7938
|
-
pern: "PostgreSQL + Express + React + Node.js - Popular PERN stack",
|
|
7939
|
-
t3: "T3 Stack - Next.js + tRPC + Prisma + PostgreSQL + Better Auth",
|
|
7940
|
-
uniwind: "Expo + Uniwind native app with no backend services",
|
|
7941
|
-
none: "No template - Full customization"
|
|
7942
|
-
}[template] || "";
|
|
7943
|
-
}
|
|
7944
|
-
|
|
7945
7586
|
//#endregion
|
|
7946
7587
|
//#region src/utils/config-processing.ts
|
|
7947
7588
|
function deriveProjectName(projectName, projectDirectory) {
|
|
@@ -7971,925 +7612,6 @@ function validateArrayOptions(options) {
|
|
|
7971
7612
|
validateNoneExclusivity(options.javaTestingLibraries, "java testing libraries");
|
|
7972
7613
|
}
|
|
7973
7614
|
|
|
7974
|
-
//#endregion
|
|
7975
|
-
//#region src/utils/peer-dependency-conflicts.ts
|
|
7976
|
-
/**
|
|
7977
|
-
* Known peer dependency conflicts.
|
|
7978
|
-
*
|
|
7979
|
-
* Add new conflicts here as they are discovered.
|
|
7980
|
-
* The validator will check these against the user's selected options.
|
|
7981
|
-
*/
|
|
7982
|
-
const PEER_DEPENDENCY_CONFLICTS = [
|
|
7983
|
-
{
|
|
7984
|
-
id: "redux-toolkit-react19",
|
|
7985
|
-
description: "redux-toolkit with React 19 may have peer dependency warnings",
|
|
7986
|
-
packages: ["@reduxjs/toolkit", "react-redux"],
|
|
7987
|
-
severity: "warning",
|
|
7988
|
-
resolution: "Consider using zustand or jotai for React 19 projects, or ensure react-redux v9+",
|
|
7989
|
-
triggeredBy: [{
|
|
7990
|
-
optionKey: "stateManagement",
|
|
7991
|
-
values: ["redux-toolkit"]
|
|
7992
|
-
}],
|
|
7993
|
-
conflictsWithOptions: [{
|
|
7994
|
-
optionKey: "frontend",
|
|
7995
|
-
values: ["next", "vinext"]
|
|
7996
|
-
}]
|
|
7997
|
-
},
|
|
7998
|
-
{
|
|
7999
|
-
id: "effect-schema-zod-overlap",
|
|
8000
|
-
description: "effect/Schema (built into effect) and zod both provide validation - may cause confusion",
|
|
8001
|
-
packages: ["effect", "zod"],
|
|
8002
|
-
severity: "warning",
|
|
8003
|
-
resolution: "Use --validation effect-schema with Effect, or --effect none with Zod",
|
|
8004
|
-
triggeredBy: [{
|
|
8005
|
-
optionKey: "effect",
|
|
8006
|
-
values: ["effect-full"]
|
|
8007
|
-
}],
|
|
8008
|
-
conflictsWithOptions: [{
|
|
8009
|
-
optionKey: "validation",
|
|
8010
|
-
values: ["zod"]
|
|
8011
|
-
}]
|
|
8012
|
-
},
|
|
8013
|
-
{
|
|
8014
|
-
id: "prisma-d1-version",
|
|
8015
|
-
description: "Prisma with D1 requires Prisma 5.x+ and specific adapter setup",
|
|
8016
|
-
packages: ["prisma", "@prisma/adapter-d1"],
|
|
8017
|
-
severity: "warning",
|
|
8018
|
-
resolution: "Consider Drizzle ORM for simpler D1 integration",
|
|
8019
|
-
triggeredBy: [{
|
|
8020
|
-
optionKey: "orm",
|
|
8021
|
-
values: ["prisma"]
|
|
8022
|
-
}],
|
|
8023
|
-
conflictsWithOptions: [{
|
|
8024
|
-
optionKey: "dbSetup",
|
|
8025
|
-
values: ["d1"]
|
|
8026
|
-
}]
|
|
8027
|
-
},
|
|
8028
|
-
{
|
|
8029
|
-
id: "biome-linting-overlap",
|
|
8030
|
-
description: "Biome includes linting/formatting - other linting addons are redundant",
|
|
8031
|
-
packages: [
|
|
8032
|
-
"@biomejs/biome",
|
|
8033
|
-
"eslint",
|
|
8034
|
-
"prettier"
|
|
8035
|
-
],
|
|
8036
|
-
severity: "warning",
|
|
8037
|
-
resolution: "Choose either Biome (all-in-one) or other linting tools, not both",
|
|
8038
|
-
triggeredBy: [{
|
|
8039
|
-
optionKey: "addons",
|
|
8040
|
-
values: ["biome"]
|
|
8041
|
-
}],
|
|
8042
|
-
conflictsWithOptions: [{
|
|
8043
|
-
optionKey: "addons",
|
|
8044
|
-
values: ["ultracite", "oxlint"]
|
|
8045
|
-
}]
|
|
8046
|
-
},
|
|
8047
|
-
{
|
|
8048
|
-
id: "framer-motion-react19",
|
|
8049
|
-
description: "framer-motion versions <12 may have React 19 issues",
|
|
8050
|
-
packages: ["framer-motion"],
|
|
8051
|
-
severity: "warning",
|
|
8052
|
-
resolution: "The CLI uses motion (framer-motion v12+) which supports React 19",
|
|
8053
|
-
triggeredBy: [{
|
|
8054
|
-
optionKey: "animation",
|
|
8055
|
-
values: ["framer-motion"]
|
|
8056
|
-
}],
|
|
8057
|
-
conflictsWithOptions: [{
|
|
8058
|
-
optionKey: "frontend",
|
|
8059
|
-
values: ["next", "vinext"]
|
|
8060
|
-
}]
|
|
8061
|
-
}
|
|
8062
|
-
];
|
|
8063
|
-
|
|
8064
|
-
//#endregion
|
|
8065
|
-
//#region src/utils/peer-dependency-validator.ts
|
|
8066
|
-
/**
|
|
8067
|
-
* Peer dependency conflict validator.
|
|
8068
|
-
*
|
|
8069
|
-
* Checks project configuration against known conflicts and provides
|
|
8070
|
-
* warnings or errors before project creation begins.
|
|
8071
|
-
*/
|
|
8072
|
-
/**
|
|
8073
|
-
* Checks if a config value matches any of the specified values.
|
|
8074
|
-
*/
|
|
8075
|
-
function matchesValue(configValue, values) {
|
|
8076
|
-
if (configValue === void 0 || configValue === null) return false;
|
|
8077
|
-
if (Array.isArray(configValue)) return values.some((v) => configValue.includes(v));
|
|
8078
|
-
return values.includes(configValue);
|
|
8079
|
-
}
|
|
8080
|
-
/**
|
|
8081
|
-
* Checks the configuration against known peer dependency conflicts.
|
|
8082
|
-
*/
|
|
8083
|
-
function checkPeerDependencyConflicts(config) {
|
|
8084
|
-
const errors = [];
|
|
8085
|
-
const warnings = [];
|
|
8086
|
-
for (const conflict of PEER_DEPENDENCY_CONFLICTS) {
|
|
8087
|
-
if (!conflict.triggeredBy.some((trigger) => {
|
|
8088
|
-
const configValue = config[trigger.optionKey];
|
|
8089
|
-
return matchesValue(configValue, trigger.values);
|
|
8090
|
-
})) continue;
|
|
8091
|
-
if (conflict.conflictsWithOptions.some((opt) => {
|
|
8092
|
-
const configValue = config[opt.optionKey];
|
|
8093
|
-
return matchesValue(configValue, opt.values);
|
|
8094
|
-
})) if (conflict.severity === "error") errors.push(conflict);
|
|
8095
|
-
else warnings.push(conflict);
|
|
8096
|
-
}
|
|
8097
|
-
return {
|
|
8098
|
-
errors,
|
|
8099
|
-
warnings
|
|
8100
|
-
};
|
|
8101
|
-
}
|
|
8102
|
-
/**
|
|
8103
|
-
* Prints a warning message for a dependency conflict.
|
|
8104
|
-
*/
|
|
8105
|
-
function warnDependencyConflict(message, resolution) {
|
|
8106
|
-
if (isSilent()) return;
|
|
8107
|
-
consola.warn(pc.yellow(`Peer Dependency Warning: ${message}`));
|
|
8108
|
-
consola.log(pc.dim(` → ${resolution}`));
|
|
8109
|
-
}
|
|
8110
|
-
/**
|
|
8111
|
-
* Handles conflict results by printing warnings and exiting on errors.
|
|
8112
|
-
*/
|
|
8113
|
-
function handleConflictResult(result) {
|
|
8114
|
-
for (const warning of result.warnings) warnDependencyConflict(warning.description, warning.resolution);
|
|
8115
|
-
if (result.errors.length > 0) exitWithError(`Peer Dependency Conflict:\n${result.errors.map((e) => `${e.description}\n → ${e.resolution}`).join("\n\n")}`);
|
|
8116
|
-
}
|
|
8117
|
-
/**
|
|
8118
|
-
* Validates peer dependencies for the given configuration.
|
|
8119
|
-
* Prints warnings and exits on errors.
|
|
8120
|
-
*/
|
|
8121
|
-
function validatePeerDependencies(config) {
|
|
8122
|
-
handleConflictResult(checkPeerDependencyConflicts(config));
|
|
8123
|
-
}
|
|
8124
|
-
|
|
8125
|
-
//#endregion
|
|
8126
|
-
//#region src/utils/config-validation.ts
|
|
8127
|
-
function validateDatabaseOrmAuth(cfg, flags) {
|
|
8128
|
-
const db = cfg.database;
|
|
8129
|
-
const orm = cfg.orm;
|
|
8130
|
-
const has = (k) => flags ? flags.has(k) : true;
|
|
8131
|
-
const hasGraphOrm = cfg.stackParts?.some((part) => part.role === "orm" && part.source !== "provided");
|
|
8132
|
-
const ecosystemOrm = getEcosystemOrm(cfg);
|
|
8133
|
-
const hasEcosystemOrm = ecosystemOrm !== void 0 && ecosystemOrm !== "none";
|
|
8134
|
-
const isNonTypeScriptSqliteDefault = cfg.ecosystem !== void 0 && cfg.ecosystem !== "typescript" && cfg.ecosystem !== "react-native" && db === "sqlite" && !hasEcosystemOrm;
|
|
8135
|
-
if (has("orm") && has("database") && orm === "mongoose" && db !== "mongodb") incompatibilityError({
|
|
8136
|
-
message: "Mongoose ORM requires MongoDB database.",
|
|
8137
|
-
provided: {
|
|
8138
|
-
orm: "mongoose",
|
|
8139
|
-
database: db || "none"
|
|
8140
|
-
},
|
|
8141
|
-
suggestions: ["Use --database mongodb", "Choose a different ORM (drizzle, prisma)"]
|
|
8142
|
-
});
|
|
8143
|
-
if (has("orm") && has("database") && orm === "drizzle" && db === "mongodb") incompatibilityError({
|
|
8144
|
-
message: "Drizzle ORM does not support MongoDB.",
|
|
8145
|
-
provided: {
|
|
8146
|
-
orm: "drizzle",
|
|
8147
|
-
database: "mongodb"
|
|
8148
|
-
},
|
|
8149
|
-
suggestions: ["Use --orm mongoose or --orm prisma for MongoDB", "Choose a different database (postgres, sqlite, mysql)"]
|
|
8150
|
-
});
|
|
8151
|
-
if (has("orm") && has("database") && orm === "typeorm" && db === "mongodb") incompatibilityError({
|
|
8152
|
-
message: "TypeORM does not support MongoDB in Better Fullstack.",
|
|
8153
|
-
provided: {
|
|
8154
|
-
orm: "typeorm",
|
|
8155
|
-
database: "mongodb"
|
|
8156
|
-
},
|
|
8157
|
-
suggestions: ["Use --orm mongoose or --orm prisma for MongoDB", "Choose a different database (postgres, sqlite, mysql)"]
|
|
8158
|
-
});
|
|
8159
|
-
if (has("orm") && has("database") && orm === "kysely" && db === "mongodb") incompatibilityError({
|
|
8160
|
-
message: "Kysely does not support MongoDB.",
|
|
8161
|
-
provided: {
|
|
8162
|
-
orm: "kysely",
|
|
8163
|
-
database: "mongodb"
|
|
8164
|
-
},
|
|
8165
|
-
suggestions: ["Use --orm mongoose or --orm prisma for MongoDB", "Choose a different database (postgres, sqlite, mysql)"]
|
|
8166
|
-
});
|
|
8167
|
-
if (has("orm") && has("database") && orm === "mikroorm" && db === "mongodb") incompatibilityError({
|
|
8168
|
-
message: "MikroORM does not support MongoDB in Better Fullstack.",
|
|
8169
|
-
provided: {
|
|
8170
|
-
orm: "mikroorm",
|
|
8171
|
-
database: "mongodb"
|
|
8172
|
-
},
|
|
8173
|
-
suggestions: ["Use --orm mongoose or --orm prisma for MongoDB", "Choose a different database (postgres, sqlite, mysql)"]
|
|
8174
|
-
});
|
|
8175
|
-
if (has("orm") && has("database") && orm === "sequelize" && db === "mongodb") incompatibilityError({
|
|
8176
|
-
message: "Sequelize does not support MongoDB.",
|
|
8177
|
-
provided: {
|
|
8178
|
-
orm: "sequelize",
|
|
8179
|
-
database: "mongodb"
|
|
8180
|
-
},
|
|
8181
|
-
suggestions: ["Use --orm mongoose or --orm prisma for MongoDB", "Choose a different database (postgres, sqlite, mysql)"]
|
|
8182
|
-
});
|
|
8183
|
-
if (has("database") && has("orm") && db === "mongodb" && orm && orm !== "mongoose" && orm !== "prisma" && orm !== "none") incompatibilityError({
|
|
8184
|
-
message: "In Better-Fullstack, MongoDB is currently supported only with Mongoose or Prisma ORM.",
|
|
8185
|
-
provided: {
|
|
8186
|
-
database: "mongodb",
|
|
8187
|
-
orm
|
|
8188
|
-
},
|
|
8189
|
-
suggestions: ["Use --orm mongoose", "Use --orm prisma"]
|
|
8190
|
-
});
|
|
8191
|
-
if (has("database") && has("orm") && db && db !== "none" && db !== "edgedb" && db !== "redis" && orm === "none" && !hasGraphOrm && !hasEcosystemOrm && !isNonTypeScriptSqliteDefault) missingRequirementError({
|
|
8192
|
-
message: "Database selection requires an ORM.",
|
|
8193
|
-
provided: {
|
|
8194
|
-
database: db,
|
|
8195
|
-
orm: "none"
|
|
8196
|
-
},
|
|
8197
|
-
suggestions: [
|
|
8198
|
-
"Use --orm drizzle (recommended)",
|
|
8199
|
-
"Use --orm prisma",
|
|
8200
|
-
"Use --orm mongoose (MongoDB only)"
|
|
8201
|
-
]
|
|
8202
|
-
});
|
|
8203
|
-
if (has("database") && has("orm") && db === "edgedb" && orm && orm !== "none") incompatibilityError({
|
|
8204
|
-
message: "EdgeDB has its own built-in query builder and does not require an ORM.",
|
|
8205
|
-
provided: {
|
|
8206
|
-
database: "edgedb",
|
|
8207
|
-
orm
|
|
8208
|
-
},
|
|
8209
|
-
suggestions: ["Use --orm none with EdgeDB", "Choose a different database if you want to use an ORM"]
|
|
8210
|
-
});
|
|
8211
|
-
if (has("database") && has("orm") && db === "redis" && orm && orm !== "none") incompatibilityError({
|
|
8212
|
-
message: "Redis is a key-value store and does not require an ORM.",
|
|
8213
|
-
provided: {
|
|
8214
|
-
database: "redis",
|
|
8215
|
-
orm
|
|
8216
|
-
},
|
|
8217
|
-
suggestions: ["Use --orm none with Redis", "Choose a different database if you want to use an ORM"]
|
|
8218
|
-
});
|
|
8219
|
-
if (has("orm") && has("database") && orm && orm !== "none" && db === "none") missingRequirementError({
|
|
8220
|
-
message: "ORM selection requires a database.",
|
|
8221
|
-
provided: {
|
|
8222
|
-
orm,
|
|
8223
|
-
database: "none"
|
|
8224
|
-
},
|
|
8225
|
-
suggestions: [
|
|
8226
|
-
"Use --database postgres",
|
|
8227
|
-
"Use --database sqlite",
|
|
8228
|
-
"Use --database mysql",
|
|
8229
|
-
"Set --orm none"
|
|
8230
|
-
]
|
|
8231
|
-
});
|
|
8232
|
-
}
|
|
8233
|
-
function getEcosystemOrm(cfg) {
|
|
8234
|
-
switch (cfg.ecosystem) {
|
|
8235
|
-
case "rust": return cfg.rustOrm;
|
|
8236
|
-
case "python": return cfg.pythonOrm;
|
|
8237
|
-
case "go": return cfg.goOrm;
|
|
8238
|
-
case "java": return cfg.javaOrm;
|
|
8239
|
-
case "dotnet": return cfg.dotnetOrm;
|
|
8240
|
-
case "elixir": return cfg.elixirOrm;
|
|
8241
|
-
default: return;
|
|
8242
|
-
}
|
|
8243
|
-
}
|
|
8244
|
-
function getEcosystemBackend(cfg) {
|
|
8245
|
-
switch (cfg.ecosystem) {
|
|
8246
|
-
case "rust": return cfg.rustWebFramework && cfg.rustWebFramework !== "none" ? cfg.rustWebFramework : void 0;
|
|
8247
|
-
case "python": return cfg.pythonWebFramework && cfg.pythonWebFramework !== "none" ? cfg.pythonWebFramework : void 0;
|
|
8248
|
-
case "go": return cfg.goWebFramework && cfg.goWebFramework !== "none" ? cfg.goWebFramework : void 0;
|
|
8249
|
-
case "java": return cfg.javaWebFramework && cfg.javaWebFramework !== "none" ? cfg.javaWebFramework : void 0;
|
|
8250
|
-
case "dotnet": return cfg.dotnetWebFramework && cfg.dotnetWebFramework !== "none" ? cfg.dotnetWebFramework : void 0;
|
|
8251
|
-
case "elixir": return cfg.elixirWebFramework && cfg.elixirWebFramework !== "none" ? cfg.elixirWebFramework : void 0;
|
|
8252
|
-
default: return;
|
|
8253
|
-
}
|
|
8254
|
-
}
|
|
8255
|
-
function validateDatabaseSetup(config, providedFlags) {
|
|
8256
|
-
const { dbSetup, database, runtime } = config;
|
|
8257
|
-
if (providedFlags.has("dbSetup") && providedFlags.has("database") && dbSetup && dbSetup !== "none" && database === "none") exitWithError("Database setup requires a database. Please choose a database or set '--db-setup none'.");
|
|
8258
|
-
const setupValidations = {
|
|
8259
|
-
turso: {
|
|
8260
|
-
database: "sqlite",
|
|
8261
|
-
errorMessage: "Turso setup requires SQLite database. Please use '--database sqlite' or choose a different setup."
|
|
8262
|
-
},
|
|
8263
|
-
neon: {
|
|
8264
|
-
database: "postgres",
|
|
8265
|
-
errorMessage: "Neon setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup."
|
|
8266
|
-
},
|
|
8267
|
-
"prisma-postgres": {
|
|
8268
|
-
database: "postgres",
|
|
8269
|
-
errorMessage: "Prisma PostgreSQL setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup."
|
|
8270
|
-
},
|
|
8271
|
-
planetscale: { errorMessage: "PlanetScale setup requires PostgreSQL or MySQL database. Please use '--database postgres' or '--database mysql' or choose a different setup." },
|
|
8272
|
-
"mongodb-atlas": {
|
|
8273
|
-
database: "mongodb",
|
|
8274
|
-
errorMessage: "MongoDB Atlas setup requires MongoDB database. Please use '--database mongodb' or choose a different setup."
|
|
8275
|
-
},
|
|
8276
|
-
upstash: {
|
|
8277
|
-
database: "redis",
|
|
8278
|
-
errorMessage: "Upstash setup requires Redis database. Please use '--database redis' or choose a different setup."
|
|
8279
|
-
},
|
|
8280
|
-
supabase: {
|
|
8281
|
-
database: "postgres",
|
|
8282
|
-
errorMessage: "Supabase setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup."
|
|
8283
|
-
},
|
|
8284
|
-
d1: {
|
|
8285
|
-
database: "sqlite",
|
|
8286
|
-
runtime: "workers",
|
|
8287
|
-
errorMessage: "Cloudflare D1 setup requires SQLite database and Cloudflare Workers runtime."
|
|
8288
|
-
},
|
|
8289
|
-
docker: { errorMessage: "In Better-Fullstack, Docker setup is currently not available with SQLite database or Cloudflare Workers runtime." },
|
|
8290
|
-
none: { errorMessage: "" }
|
|
8291
|
-
};
|
|
8292
|
-
if (dbSetup && dbSetup !== "none") {
|
|
8293
|
-
const validation = setupValidations[dbSetup];
|
|
8294
|
-
if (dbSetup === "planetscale") {
|
|
8295
|
-
if (database !== "postgres" && database !== "mysql") exitWithError(validation.errorMessage);
|
|
8296
|
-
} else if (validation.database && database !== validation.database) exitWithError(validation.errorMessage);
|
|
8297
|
-
if (validation.runtime && runtime !== validation.runtime) exitWithError(validation.errorMessage);
|
|
8298
|
-
if (dbSetup === "docker") {
|
|
8299
|
-
if (database === "sqlite") exitWithError("In Better-Fullstack, Docker setup is currently not available with SQLite database. SQLite is file-based and doesn't require Docker. Please use '--database postgres', '--database mysql', '--database mongodb', or choose a different setup.");
|
|
8300
|
-
if (runtime === "workers") exitWithError("In Better-Fullstack, Docker setup is currently not available with Cloudflare Workers runtime. Workers runtime uses serverless databases (D1) and doesn't support local Docker containers. Please use '--db-setup d1' for SQLite or choose a different runtime.");
|
|
8301
|
-
}
|
|
8302
|
-
}
|
|
8303
|
-
}
|
|
8304
|
-
function validateEcosystemAuthCompatibility(config, providedFlags) {
|
|
8305
|
-
const auth = config.auth;
|
|
8306
|
-
if (!auth || auth === "none") return;
|
|
8307
|
-
if ((auth === "better-auth" || auth === "better-auth-organizations") && config.orm && [
|
|
8308
|
-
"typeorm",
|
|
8309
|
-
"sequelize",
|
|
8310
|
-
"mikroorm"
|
|
8311
|
-
].includes(config.orm)) {
|
|
8312
|
-
config.auth = "none";
|
|
8313
|
-
if (providedFlags?.has("auth") && !isSilent()) consola.warn(pc.yellow(`Unsupported auth selection '${auth}' with ${config.orm}: no Better Auth adapter exists. Falling back to '--auth none'.`));
|
|
8314
|
-
return;
|
|
8315
|
-
}
|
|
8316
|
-
const normalized = (0, types_exports.normalizeCapabilitySelection)("auth", {
|
|
8317
|
-
ecosystem: config.ecosystem,
|
|
8318
|
-
backend: config.backend,
|
|
8319
|
-
frontend: config.frontend
|
|
8320
|
-
}, auth);
|
|
8321
|
-
if (!normalized.normalized || normalized.value === auth) return;
|
|
8322
|
-
config.auth = normalized.value;
|
|
8323
|
-
if (providedFlags?.has("auth") && normalized.reason && !isSilent()) consola.warn(pc.yellow(`Unsupported auth selection '${auth}' for the current stack: ${normalized.reason}. Falling back to '--auth ${normalized.value}'.`));
|
|
8324
|
-
}
|
|
8325
|
-
function validateConvexConstraints(config, providedFlags) {
|
|
8326
|
-
const { backend } = config;
|
|
8327
|
-
if (backend !== "convex") return;
|
|
8328
|
-
const has = (k) => providedFlags.has(k);
|
|
8329
|
-
if (has("runtime") && config.runtime !== "none") constraintError({
|
|
8330
|
-
message: "Convex backend manages its own runtime.",
|
|
8331
|
-
provided: {
|
|
8332
|
-
backend: "convex",
|
|
8333
|
-
runtime: config.runtime || ""
|
|
8334
|
-
},
|
|
8335
|
-
suggestions: ["Remove --runtime flag", "Set --runtime none"]
|
|
8336
|
-
});
|
|
8337
|
-
if (has("database") && config.database !== "none") constraintError({
|
|
8338
|
-
message: "Convex backend has its own built-in database.",
|
|
8339
|
-
provided: {
|
|
8340
|
-
backend: "convex",
|
|
8341
|
-
database: config.database || ""
|
|
8342
|
-
},
|
|
8343
|
-
suggestions: ["Remove --database flag", "Set --database none"]
|
|
8344
|
-
});
|
|
8345
|
-
if (has("orm") && config.orm !== "none") constraintError({
|
|
8346
|
-
message: "Convex backend has its own data layer (no ORM needed).",
|
|
8347
|
-
provided: {
|
|
8348
|
-
backend: "convex",
|
|
8349
|
-
orm: config.orm || ""
|
|
8350
|
-
},
|
|
8351
|
-
suggestions: ["Remove --orm flag", "Set --orm none"]
|
|
8352
|
-
});
|
|
8353
|
-
if (has("api") && config.api !== "none") constraintError({
|
|
8354
|
-
message: "Convex backend has its own built-in API layer.",
|
|
8355
|
-
provided: {
|
|
8356
|
-
backend: "convex",
|
|
8357
|
-
api: config.api || ""
|
|
8358
|
-
},
|
|
8359
|
-
suggestions: ["Remove --api flag", "Set --api none"]
|
|
8360
|
-
});
|
|
8361
|
-
if (has("dbSetup") && config.dbSetup !== "none") constraintError({
|
|
8362
|
-
message: "Convex backend manages its own database infrastructure.",
|
|
8363
|
-
provided: {
|
|
8364
|
-
backend: "convex",
|
|
8365
|
-
"db-setup": config.dbSetup || ""
|
|
8366
|
-
},
|
|
8367
|
-
suggestions: ["Remove --db-setup flag", "Set --db-setup none"]
|
|
8368
|
-
});
|
|
8369
|
-
if (has("serverDeploy") && config.serverDeploy !== "none") constraintError({
|
|
8370
|
-
message: "Convex backend has its own deployment platform.",
|
|
8371
|
-
provided: {
|
|
8372
|
-
backend: "convex",
|
|
8373
|
-
"server-deploy": config.serverDeploy || ""
|
|
8374
|
-
},
|
|
8375
|
-
suggestions: ["Remove --server-deploy flag", "Set --server-deploy none"]
|
|
8376
|
-
});
|
|
8377
|
-
}
|
|
8378
|
-
function validateBackendNoneConstraints(config, providedFlags) {
|
|
8379
|
-
const { backend } = config;
|
|
8380
|
-
const hasGraphBackend = config.stackParts?.some((part) => part.role === "backend" && !part.ownerPartId && part.source !== "provided" && part.ecosystem !== "typescript" && part.ecosystem !== "react-native" && part.ecosystem !== "universal");
|
|
8381
|
-
const hasEcosystemBackend = getEcosystemBackend(config) !== void 0;
|
|
8382
|
-
if (backend !== "none" || hasGraphBackend || hasEcosystemBackend) return;
|
|
8383
|
-
const has = (k) => providedFlags.has(k);
|
|
8384
|
-
if (has("runtime") && config.runtime !== "none") exitWithError("Backend 'none' requires '--runtime none'. Please remove the --runtime flag or set it to 'none'.");
|
|
8385
|
-
if (has("database") && config.database !== "none") exitWithError("Backend 'none' requires '--database none'. Please remove the --database flag or set it to 'none'.");
|
|
8386
|
-
if (has("orm") && config.orm !== "none") exitWithError("Backend 'none' requires '--orm none'. Please remove the --orm flag or set it to 'none'.");
|
|
8387
|
-
if (has("api") && config.api !== "none") exitWithError("Backend 'none' requires '--api none'. Please remove the --api flag or set it to 'none'.");
|
|
8388
|
-
if (has("payments") && config.payments !== "none") exitWithError("Backend 'none' requires '--payments none'. Please remove the --payments flag or set it to 'none'.");
|
|
8389
|
-
if (has("dbSetup") && config.dbSetup !== "none") exitWithError("Backend 'none' requires '--db-setup none'. Please remove the --db-setup flag or set it to 'none'.");
|
|
8390
|
-
if (has("serverDeploy") && config.serverDeploy !== "none") exitWithError("Backend 'none' requires '--server-deploy none'. Please remove the --server-deploy flag or set it to 'none'.");
|
|
8391
|
-
}
|
|
8392
|
-
function validateSelfBackendConstraints(config, providedFlags) {
|
|
8393
|
-
const { backend } = config;
|
|
8394
|
-
if (backend !== "self") return;
|
|
8395
|
-
const has = (k) => providedFlags.has(k);
|
|
8396
|
-
if (has("runtime") && config.runtime !== "none") exitWithError("Backend 'self' (fullstack) requires '--runtime none'. Please remove the --runtime flag or set it to 'none'.");
|
|
8397
|
-
}
|
|
8398
|
-
function validateEncoreConstraints(config, providedFlags) {
|
|
8399
|
-
const { backend } = config;
|
|
8400
|
-
if (backend !== "encore") return;
|
|
8401
|
-
const has = (k) => providedFlags.has(k);
|
|
8402
|
-
if (has("runtime") && config.runtime !== "none") exitWithError("Encore.ts backend requires '--runtime none'. Encore has its own runtime via the Encore CLI. Please remove the --runtime flag or set it to 'none'.");
|
|
8403
|
-
if (has("api") && config.api !== "none") exitWithError("Encore.ts backend requires '--api none'. Encore has its own type-safe API system. Please remove the --api flag or set it to 'none'.");
|
|
8404
|
-
if (has("database") && config.database !== "none") exitWithError("Encore.ts backend requires '--database none'. Encore manages databases through its infrastructure primitives. Please remove the --database flag or set it to 'none'.");
|
|
8405
|
-
if (has("orm") && config.orm !== "none") exitWithError("Encore.ts backend requires '--orm none'. Encore has its own database abstractions. Please remove the --orm flag or set it to 'none'.");
|
|
8406
|
-
if (has("dbSetup") && config.dbSetup !== "none") exitWithError("Encore.ts backend requires '--db-setup none'. Encore manages infrastructure automatically. Please remove the --db-setup flag or set it to 'none'.");
|
|
8407
|
-
if (has("serverDeploy") && config.serverDeploy !== "none") exitWithError("Encore.ts backend requires '--server-deploy none'. Encore has its own deployment platform. Please remove the --server-deploy flag or set it to 'none'.");
|
|
8408
|
-
}
|
|
8409
|
-
function validateAdonisJSConstraints(config, providedFlags) {
|
|
8410
|
-
const { backend } = config;
|
|
8411
|
-
if (backend !== "adonisjs") return;
|
|
8412
|
-
const has = (k) => providedFlags.has(k);
|
|
8413
|
-
if (has("runtime") && config.runtime !== "node") exitWithError("AdonisJS backend requires '--runtime node'. AdonisJS currently only supports Node.js runtime. Please use '--runtime node' or remove the --runtime flag.");
|
|
8414
|
-
}
|
|
8415
|
-
function validateBackendConstraints(config, providedFlags, options) {
|
|
8416
|
-
const { backend } = config;
|
|
8417
|
-
if (providedFlags.has("backend") && backend && backend !== "convex" && backend !== "none" && backend !== "self" && backend !== "encore") {
|
|
8418
|
-
if (providedFlags.has("runtime") && options.runtime === "none") exitWithError("'--runtime none' is only supported with '--backend convex', '--backend none', '--backend self', or '--backend encore'. Please choose 'bun', 'node', or remove the --runtime flag.");
|
|
8419
|
-
}
|
|
8420
|
-
if (backend === "convex" && providedFlags.has("frontend") && options.frontend) {
|
|
8421
|
-
const incompatibleFrontends = options.frontend.filter((f) => ["solid", "astro"].includes(f));
|
|
8422
|
-
if (incompatibleFrontends.length > 0) exitWithError(`The following frontends are not compatible with '--backend convex': ${incompatibleFrontends.join(", ")}. Please choose a different frontend or backend.`);
|
|
8423
|
-
}
|
|
8424
|
-
}
|
|
8425
|
-
function validateFrontendConstraints(config, providedFlags) {
|
|
8426
|
-
const { frontend } = config;
|
|
8427
|
-
if (frontend && frontend.length > 0) {
|
|
8428
|
-
if (config.ecosystem === "react-native" && frontend.some((item) => !item.startsWith("native-") && item !== "none")) incompatibilityError({
|
|
8429
|
-
message: "React Native ecosystem only supports native Expo frontends.",
|
|
8430
|
-
provided: {
|
|
8431
|
-
ecosystem: "react-native",
|
|
8432
|
-
frontend: frontend.join(" ")
|
|
8433
|
-
},
|
|
8434
|
-
suggestions: [
|
|
8435
|
-
"Use --frontend native-bare",
|
|
8436
|
-
"Use --frontend native-uniwind",
|
|
8437
|
-
"Use --frontend native-unistyles"
|
|
8438
|
-
]
|
|
8439
|
-
});
|
|
8440
|
-
ensureSingleWebAndNative(frontend);
|
|
8441
|
-
if (providedFlags.has("api") && providedFlags.has("frontend") && config.api) validateApiFrontendCompatibility(config.api, frontend, config.astroIntegration);
|
|
8442
|
-
}
|
|
8443
|
-
const hasWebFrontendFlag = (frontend ?? []).some((f) => isWebFrontend$1(f));
|
|
8444
|
-
validateWebDeployRequiresWebFrontend(config.webDeploy, hasWebFrontendFlag);
|
|
8445
|
-
validateWebDeployFrontendTemplates(config.webDeploy, frontend);
|
|
8446
|
-
}
|
|
8447
|
-
function validateApiConstraints(config, _options) {
|
|
8448
|
-
if (config.api !== "openapi") return;
|
|
8449
|
-
const frontend = config.frontend ?? [];
|
|
8450
|
-
if (frontend.some((item) => [
|
|
8451
|
-
"native-bare",
|
|
8452
|
-
"native-uniwind",
|
|
8453
|
-
"native-unistyles"
|
|
8454
|
-
].includes(item))) incompatibilityError({
|
|
8455
|
-
message: "OpenAPI is currently available for web frontends, not React Native.",
|
|
8456
|
-
provided: {
|
|
8457
|
-
api: "openapi",
|
|
8458
|
-
frontend
|
|
8459
|
-
},
|
|
8460
|
-
suggestions: [
|
|
8461
|
-
"Use --api trpc",
|
|
8462
|
-
"Use --api orpc",
|
|
8463
|
-
"Use a web frontend"
|
|
8464
|
-
]
|
|
8465
|
-
});
|
|
8466
|
-
if (!config.backend || ![
|
|
8467
|
-
"hono",
|
|
8468
|
-
"express",
|
|
8469
|
-
"fastify",
|
|
8470
|
-
"elysia"
|
|
8471
|
-
].includes(config.backend)) incompatibilityError({
|
|
8472
|
-
message: "OpenAPI currently supports Hono, Express, Fastify, and Elysia backends.",
|
|
8473
|
-
provided: {
|
|
8474
|
-
api: "openapi",
|
|
8475
|
-
backend: config.backend ?? "none"
|
|
8476
|
-
},
|
|
8477
|
-
suggestions: [
|
|
8478
|
-
"Use --backend hono",
|
|
8479
|
-
"Use --backend express",
|
|
8480
|
-
"Use --backend fastify",
|
|
8481
|
-
"Use --backend elysia"
|
|
8482
|
-
]
|
|
8483
|
-
});
|
|
8484
|
-
}
|
|
8485
|
-
function validateJavaConstraints(config, providedFlags = /* @__PURE__ */ new Set()) {
|
|
8486
|
-
if (config.ecosystem !== "java") return;
|
|
8487
|
-
const hasSpringBoot = config.javaWebFramework === "spring-boot";
|
|
8488
|
-
const hasJavaWebFramework = config.javaWebFramework !== "none";
|
|
8489
|
-
const hasNoBuildTool = config.javaBuildTool === "none";
|
|
8490
|
-
const hasJavaLibraries = (config.javaLibraries ?? []).some((library) => library !== "none");
|
|
8491
|
-
const hasJavaTestingLibraries = (config.javaTestingLibraries ?? []).some((library) => library !== "none");
|
|
8492
|
-
const hasSpringOnlyFeatures = config.javaOrm !== "none" || config.javaAuth !== "none" || hasJavaLibraries;
|
|
8493
|
-
if (hasNoBuildTool && hasJavaWebFramework) incompatibilityError({
|
|
8494
|
-
message: "Java web frameworks require Maven or Gradle in the Java scaffold.",
|
|
8495
|
-
provided: {
|
|
8496
|
-
"java-web-framework": config.javaWebFramework ?? "none",
|
|
8497
|
-
"java-build-tool": config.javaBuildTool ?? "none"
|
|
8498
|
-
},
|
|
8499
|
-
suggestions: ["Use --java-build-tool maven or --java-build-tool gradle with Java web frameworks", "Use --java-web-framework none for a plain Java source-only scaffold"]
|
|
8500
|
-
});
|
|
8501
|
-
if ((!hasSpringBoot || hasNoBuildTool) && hasSpringOnlyFeatures) incompatibilityError({
|
|
8502
|
-
message: "Spring-only Java features require the Spring Boot scaffold with Maven or Gradle.",
|
|
8503
|
-
provided: {
|
|
8504
|
-
"java-web-framework": config.javaWebFramework ?? "none",
|
|
8505
|
-
"java-build-tool": config.javaBuildTool ?? "none",
|
|
8506
|
-
"java-orm": config.javaOrm ?? "none",
|
|
8507
|
-
"java-auth": config.javaAuth ?? "none",
|
|
8508
|
-
"java-libraries": (config.javaLibraries ?? []).join(" ") || "none"
|
|
8509
|
-
},
|
|
8510
|
-
suggestions: ["Use --java-web-framework spring-boot and a real build tool for Spring features", "Clear --java-orm, --java-auth, and --java-libraries when using plain Java or Quarkus"]
|
|
8511
|
-
});
|
|
8512
|
-
if (hasNoBuildTool && hasJavaTestingLibraries) incompatibilityError({
|
|
8513
|
-
message: "Java testing libraries require Maven or Gradle to manage test dependencies.",
|
|
8514
|
-
provided: {
|
|
8515
|
-
"java-build-tool": config.javaBuildTool ?? "none",
|
|
8516
|
-
"java-testing-libraries": (config.javaTestingLibraries ?? []).join(" ") || "none"
|
|
8517
|
-
},
|
|
8518
|
-
suggestions: ["Use --java-build-tool maven or --java-build-tool gradle to enable JUnit/Mockito/Testcontainers", "Set --java-testing-libraries none for a source-only plain Java scaffold"]
|
|
8519
|
-
});
|
|
8520
|
-
}
|
|
8521
|
-
function validateElixirConstraints(config) {
|
|
8522
|
-
if (config.ecosystem !== "elixir") return;
|
|
8523
|
-
const hasPhoenix = config.elixirWebFramework !== "none";
|
|
8524
|
-
const hasEcto = config.elixirOrm !== "none";
|
|
8525
|
-
const unsupportedSelections = [
|
|
8526
|
-
{
|
|
8527
|
-
flag: "elixir-orm",
|
|
8528
|
-
value: config.elixirOrm,
|
|
8529
|
-
unsupported: ["ecto"],
|
|
8530
|
-
message: "Plain Ecto without SQL Repo wiring is not generated yet.",
|
|
8531
|
-
suggestions: ["Use --elixir-orm ecto-sql", "Use --elixir-orm none"]
|
|
8532
|
-
},
|
|
8533
|
-
{
|
|
8534
|
-
flag: "elixir-auth",
|
|
8535
|
-
value: config.elixirAuth,
|
|
8536
|
-
unsupported: ["ueberauth", "guardian"],
|
|
8537
|
-
message: "Only phx.gen.auth currently generates Phoenix auth files.",
|
|
8538
|
-
suggestions: ["Use --elixir-auth phx-gen-auth", "Use --elixir-auth none"]
|
|
8539
|
-
},
|
|
8540
|
-
{
|
|
8541
|
-
flag: "elixir-validation",
|
|
8542
|
-
value: config.elixirValidation,
|
|
8543
|
-
unsupported: ["nimble-options"],
|
|
8544
|
-
message: "NimbleOptions is not generated yet.",
|
|
8545
|
-
suggestions: ["Use --elixir-validation ecto-changesets", "Use --elixir-validation none"]
|
|
8546
|
-
},
|
|
8547
|
-
{
|
|
8548
|
-
flag: "elixir-caching",
|
|
8549
|
-
value: config.elixirCaching,
|
|
8550
|
-
unsupported: ["nebulex"],
|
|
8551
|
-
message: "Nebulex cache modules are not generated yet.",
|
|
8552
|
-
suggestions: ["Use --elixir-caching cachex", "Use --elixir-caching none"]
|
|
8553
|
-
},
|
|
8554
|
-
{
|
|
8555
|
-
flag: "elixir-observability",
|
|
8556
|
-
value: config.elixirObservability,
|
|
8557
|
-
unsupported: ["opentelemetry", "prom_ex"],
|
|
8558
|
-
message: "OpenTelemetry and PromEx setup are not generated yet.",
|
|
8559
|
-
suggestions: ["Use --elixir-observability telemetry", "Use --elixir-observability none"]
|
|
8560
|
-
},
|
|
8561
|
-
{
|
|
8562
|
-
flag: "elixir-testing",
|
|
8563
|
-
value: config.elixirTesting,
|
|
8564
|
-
unsupported: [
|
|
8565
|
-
"mox",
|
|
8566
|
-
"bypass",
|
|
8567
|
-
"wallaby"
|
|
8568
|
-
],
|
|
8569
|
-
message: "Generated Phoenix projects currently include ExUnit tests only.",
|
|
8570
|
-
suggestions: ["Use --elixir-testing ex_unit"]
|
|
8571
|
-
},
|
|
8572
|
-
{
|
|
8573
|
-
flag: "elixir-deploy",
|
|
8574
|
-
value: config.elixirDeploy,
|
|
8575
|
-
unsupported: ["fly", "gigalixir"],
|
|
8576
|
-
message: "Fly.io and Gigalixir config files are not generated yet.",
|
|
8577
|
-
suggestions: ["Use --elixir-deploy docker", "Use --elixir-deploy mix-release"]
|
|
8578
|
-
}
|
|
8579
|
-
];
|
|
8580
|
-
for (const selection of unsupportedSelections) if (selection.value && selection.unsupported.includes(selection.value)) incompatibilityError({
|
|
8581
|
-
message: selection.message,
|
|
8582
|
-
provided: { [selection.flag]: selection.value },
|
|
8583
|
-
suggestions: selection.suggestions
|
|
8584
|
-
});
|
|
8585
|
-
if (!hasPhoenix) {
|
|
8586
|
-
const phoenixOnlySelections = [
|
|
8587
|
-
{
|
|
8588
|
-
flag: "elixir-auth",
|
|
8589
|
-
value: config.elixirAuth,
|
|
8590
|
-
message: "Elixir auth scaffolds require Phoenix."
|
|
8591
|
-
},
|
|
8592
|
-
{
|
|
8593
|
-
flag: "elixir-api",
|
|
8594
|
-
value: config.elixirApi,
|
|
8595
|
-
message: "Elixir API scaffolds require Phoenix."
|
|
8596
|
-
},
|
|
8597
|
-
{
|
|
8598
|
-
flag: "elixir-realtime",
|
|
8599
|
-
value: config.elixirRealtime,
|
|
8600
|
-
message: "Elixir realtime scaffolds require Phoenix."
|
|
8601
|
-
}
|
|
8602
|
-
];
|
|
8603
|
-
for (const selection of phoenixOnlySelections) {
|
|
8604
|
-
if (!selection.value || selection.value === "none") continue;
|
|
8605
|
-
incompatibilityError({
|
|
8606
|
-
message: selection.message,
|
|
8607
|
-
provided: {
|
|
8608
|
-
"elixir-web-framework": config.elixirWebFramework ?? "none",
|
|
8609
|
-
[selection.flag]: selection.value
|
|
8610
|
-
},
|
|
8611
|
-
suggestions: [
|
|
8612
|
-
"Use --elixir-web-framework phoenix",
|
|
8613
|
-
"Use --elixir-web-framework phoenix-live-view",
|
|
8614
|
-
`Use --${selection.flag} none`
|
|
8615
|
-
]
|
|
8616
|
-
});
|
|
8617
|
-
}
|
|
8618
|
-
}
|
|
8619
|
-
if (hasPhoenix && config.elixirJson === "none") incompatibilityError({
|
|
8620
|
-
message: "Phoenix JSON scaffolds require Jason.",
|
|
8621
|
-
provided: { "elixir-json": "none" },
|
|
8622
|
-
suggestions: ["Use --elixir-json jason"]
|
|
8623
|
-
});
|
|
8624
|
-
if (config.elixirAuth === "phx-gen-auth" && !hasEcto) incompatibilityError({
|
|
8625
|
-
message: "phx.gen.auth requires Ecto in the generated Phoenix scaffold.",
|
|
8626
|
-
provided: {
|
|
8627
|
-
"elixir-auth": "phx-gen-auth",
|
|
8628
|
-
"elixir-orm": config.elixirOrm ?? "none"
|
|
8629
|
-
},
|
|
8630
|
-
suggestions: ["Use --elixir-orm ecto-sql", "Use --elixir-auth none"]
|
|
8631
|
-
});
|
|
8632
|
-
if (config.elixirJobs === "oban" && config.elixirOrm !== "ecto-sql") incompatibilityError({
|
|
8633
|
-
message: "Oban requires Ecto SQL with PostgreSQL in the generated Phoenix scaffold.",
|
|
8634
|
-
provided: {
|
|
8635
|
-
"elixir-jobs": "oban",
|
|
8636
|
-
"elixir-orm": config.elixirOrm ?? "none"
|
|
8637
|
-
},
|
|
8638
|
-
suggestions: ["Use --elixir-orm ecto-sql", "Use --elixir-jobs none"]
|
|
8639
|
-
});
|
|
8640
|
-
if (config.elixirRealtime === "live-view-streams" && config.elixirWebFramework !== "phoenix-live-view") incompatibilityError({
|
|
8641
|
-
message: "LiveView Streams require Phoenix LiveView.",
|
|
8642
|
-
provided: {
|
|
8643
|
-
"elixir-realtime": "live-view-streams",
|
|
8644
|
-
"elixir-web-framework": config.elixirWebFramework ?? "none"
|
|
8645
|
-
},
|
|
8646
|
-
suggestions: ["Use --elixir-web-framework phoenix-live-view", "Use --elixir-realtime channels"]
|
|
8647
|
-
});
|
|
8648
|
-
if (config.elixirApi === "absinthe" && !hasEcto) incompatibilityError({
|
|
8649
|
-
message: "Absinthe GraphQL requires Ecto in the current generated Phoenix scaffold.",
|
|
8650
|
-
provided: {
|
|
8651
|
-
"elixir-api": "absinthe",
|
|
8652
|
-
"elixir-orm": config.elixirOrm ?? "none"
|
|
8653
|
-
},
|
|
8654
|
-
suggestions: ["Use --elixir-orm ecto-sql", "Use --elixir-api rest"]
|
|
8655
|
-
});
|
|
8656
|
-
}
|
|
8657
|
-
function validateEmailConstraints(config) {
|
|
8658
|
-
if (!config.email || config.email === "none") return;
|
|
8659
|
-
if (config.ecosystem !== "typescript" && config.email !== "resend") incompatibilityError({
|
|
8660
|
-
message: "Only Resend email is available for non-TypeScript ecosystems.",
|
|
8661
|
-
provided: {
|
|
8662
|
-
ecosystem: config.ecosystem ?? "typescript",
|
|
8663
|
-
email: config.email
|
|
8664
|
-
},
|
|
8665
|
-
suggestions: ["Use --email resend", "Use --email none"]
|
|
8666
|
-
});
|
|
8667
|
-
if (config.ecosystem === "java" && config.email === "resend" && config.javaBuildTool === "none") incompatibilityError({
|
|
8668
|
-
message: "Resend email for Java requires Maven or Gradle to manage the SDK dependency.",
|
|
8669
|
-
provided: {
|
|
8670
|
-
"java-build-tool": "none",
|
|
8671
|
-
email: "resend"
|
|
8672
|
-
},
|
|
8673
|
-
suggestions: ["Use --java-build-tool maven", "Use --java-build-tool gradle"]
|
|
8674
|
-
});
|
|
8675
|
-
}
|
|
8676
|
-
function validateObservabilityConstraints(config) {
|
|
8677
|
-
if (!config.observability || config.observability === "none") return;
|
|
8678
|
-
if (config.ecosystem !== "typescript" && config.observability !== "sentry") incompatibilityError({
|
|
8679
|
-
message: "Only Sentry observability is available for non-TypeScript ecosystems.",
|
|
8680
|
-
provided: {
|
|
8681
|
-
ecosystem: config.ecosystem ?? "typescript",
|
|
8682
|
-
observability: config.observability
|
|
8683
|
-
},
|
|
8684
|
-
suggestions: ["Use --observability sentry", "Use --observability none"]
|
|
8685
|
-
});
|
|
8686
|
-
if (config.ecosystem === "java" && config.observability === "sentry" && config.javaBuildTool === "none") incompatibilityError({
|
|
8687
|
-
message: "Sentry observability for Java requires Maven or Gradle to manage the SDK dependency.",
|
|
8688
|
-
provided: {
|
|
8689
|
-
"java-build-tool": "none",
|
|
8690
|
-
observability: "sentry"
|
|
8691
|
-
},
|
|
8692
|
-
suggestions: ["Use --java-build-tool maven", "Use --java-build-tool gradle"]
|
|
8693
|
-
});
|
|
8694
|
-
}
|
|
8695
|
-
function validateCachingConstraints(config) {
|
|
8696
|
-
if (!config.caching || config.caching === "none") return;
|
|
8697
|
-
if (config.ecosystem !== "typescript" && config.caching !== "upstash-redis") incompatibilityError({
|
|
8698
|
-
message: "Only Upstash Redis caching is available for non-TypeScript ecosystems.",
|
|
8699
|
-
provided: {
|
|
8700
|
-
ecosystem: config.ecosystem ?? "typescript",
|
|
8701
|
-
caching: config.caching
|
|
8702
|
-
},
|
|
8703
|
-
suggestions: ["Use --caching upstash-redis", "Use --caching none"]
|
|
8704
|
-
});
|
|
8705
|
-
if (config.ecosystem === "java" && config.caching === "upstash-redis" && config.javaBuildTool === "none") incompatibilityError({
|
|
8706
|
-
message: "Upstash Redis caching for Java requires Maven or Gradle to manage the Redis client dependency.",
|
|
8707
|
-
provided: {
|
|
8708
|
-
"java-build-tool": "none",
|
|
8709
|
-
caching: "upstash-redis"
|
|
8710
|
-
},
|
|
8711
|
-
suggestions: ["Use --java-build-tool maven", "Use --java-build-tool gradle"]
|
|
8712
|
-
});
|
|
8713
|
-
}
|
|
8714
|
-
function validateRateLimitConstraints(config) {
|
|
8715
|
-
if (!config.rateLimit || config.rateLimit === "none") return;
|
|
8716
|
-
if (config.ecosystem !== "typescript") incompatibilityError({
|
|
8717
|
-
message: "Rate limiting helpers are currently available for TypeScript stacks only.",
|
|
8718
|
-
provided: {
|
|
8719
|
-
ecosystem: config.ecosystem ?? "typescript",
|
|
8720
|
-
"rate-limit": config.rateLimit
|
|
8721
|
-
},
|
|
8722
|
-
suggestions: ["Use --rate-limit none"]
|
|
8723
|
-
});
|
|
8724
|
-
if (config.backend === "convex") incompatibilityError({
|
|
8725
|
-
message: "Rate limiting helpers are not generated with Convex backend.",
|
|
8726
|
-
provided: {
|
|
8727
|
-
backend: "convex",
|
|
8728
|
-
"rate-limit": config.rateLimit
|
|
8729
|
-
},
|
|
8730
|
-
suggestions: ["Use --rate-limit none"]
|
|
8731
|
-
});
|
|
8732
|
-
if (config.backend === "none") incompatibilityError({
|
|
8733
|
-
message: "Rate limiting requires a backend.",
|
|
8734
|
-
provided: {
|
|
8735
|
-
backend: "none",
|
|
8736
|
-
"rate-limit": config.rateLimit
|
|
8737
|
-
},
|
|
8738
|
-
suggestions: ["Use --backend hono", "Use --rate-limit none"]
|
|
8739
|
-
});
|
|
8740
|
-
}
|
|
8741
|
-
function validateSearchConstraints(config) {
|
|
8742
|
-
if (!config.search || config.search === "none") return;
|
|
8743
|
-
if (config.ecosystem !== "typescript" && config.search !== "meilisearch") incompatibilityError({
|
|
8744
|
-
message: "Only Meilisearch search is available for non-TypeScript ecosystems.",
|
|
8745
|
-
provided: {
|
|
8746
|
-
ecosystem: config.ecosystem ?? "typescript",
|
|
8747
|
-
search: config.search
|
|
8748
|
-
},
|
|
8749
|
-
suggestions: ["Use --search meilisearch", "Use --search none"]
|
|
8750
|
-
});
|
|
8751
|
-
if (config.ecosystem === "java" && config.search === "meilisearch" && config.javaBuildTool === "none") incompatibilityError({
|
|
8752
|
-
message: "Meilisearch search for Java requires Maven or Gradle to manage the SDK dependency.",
|
|
8753
|
-
provided: {
|
|
8754
|
-
"java-build-tool": "none",
|
|
8755
|
-
search: "meilisearch"
|
|
8756
|
-
},
|
|
8757
|
-
suggestions: ["Use --java-build-tool maven", "Use --java-build-tool gradle"]
|
|
8758
|
-
});
|
|
8759
|
-
}
|
|
8760
|
-
function validateShadcnConstraints(config, providedFlags) {
|
|
8761
|
-
const shadcnFlagMap = {
|
|
8762
|
-
shadcnBase: "--shadcn-base",
|
|
8763
|
-
shadcnStyle: "--shadcn-style",
|
|
8764
|
-
shadcnIconLibrary: "--shadcn-icon-library",
|
|
8765
|
-
shadcnColorTheme: "--shadcn-color-theme",
|
|
8766
|
-
shadcnBaseColor: "--shadcn-base-color",
|
|
8767
|
-
shadcnFont: "--shadcn-font",
|
|
8768
|
-
shadcnRadius: "--shadcn-radius"
|
|
8769
|
-
};
|
|
8770
|
-
const providedShadcnFlags = Object.keys(shadcnFlagMap).filter((f) => providedFlags.has(f));
|
|
8771
|
-
if (providedShadcnFlags.length > 0 && config.uiLibrary !== "shadcn-ui") incompatibilityError({
|
|
8772
|
-
message: "shadcn/ui customization flags require --ui-library shadcn-ui.",
|
|
8773
|
-
provided: {
|
|
8774
|
-
"ui-library": config.uiLibrary || "none",
|
|
8775
|
-
...Object.fromEntries(providedShadcnFlags.map((f) => [shadcnFlagMap[f], String(config[f] ?? "")]))
|
|
8776
|
-
},
|
|
8777
|
-
suggestions: ["Add --ui-library shadcn-ui to use shadcn customization flags", "Remove the --shadcn-* flags if not using shadcn/ui"]
|
|
8778
|
-
});
|
|
8779
|
-
}
|
|
8780
|
-
function validatePythonApiConstraints(config) {
|
|
8781
|
-
if (config.ecosystem === "python" && config.pythonApi && config.pythonApi !== "none" && config.pythonWebFramework !== "django") incompatibilityError({
|
|
8782
|
-
message: "Python API frameworks require --python-web-framework django.",
|
|
8783
|
-
provided: {
|
|
8784
|
-
"python-web-framework": config.pythonWebFramework || "none",
|
|
8785
|
-
"python-api": config.pythonApi
|
|
8786
|
-
},
|
|
8787
|
-
suggestions: ["Use --python-web-framework django with --python-api django-rest-framework or django-ninja", "Set --python-api none for FastAPI, Flask, Litestar, or no Python web framework"]
|
|
8788
|
-
});
|
|
8789
|
-
}
|
|
8790
|
-
function validateFullConfig(config, providedFlags, options) {
|
|
8791
|
-
if (config.stackParts && !options.yolo) {
|
|
8792
|
-
const graphValidation = (0, types_exports.validateStackParts)(config.stackParts);
|
|
8793
|
-
if (graphValidation.issues.length > 0) exitWithError(graphValidation.issues.map((issue) => issue.message).join("\n"));
|
|
8794
|
-
}
|
|
8795
|
-
validateEcosystemAuthCompatibility(config, providedFlags);
|
|
8796
|
-
validateDatabaseOrmAuth(config, providedFlags);
|
|
8797
|
-
validateDatabaseSetup(config, providedFlags);
|
|
8798
|
-
validateConvexConstraints(config, providedFlags);
|
|
8799
|
-
validateBackendNoneConstraints(config, providedFlags);
|
|
8800
|
-
validateSelfBackendConstraints(config, providedFlags);
|
|
8801
|
-
validateEncoreConstraints(config, providedFlags);
|
|
8802
|
-
validateAdonisJSConstraints(config, providedFlags);
|
|
8803
|
-
validateBackendConstraints(config, providedFlags, options);
|
|
8804
|
-
validateFrontendConstraints(config, providedFlags);
|
|
8805
|
-
validateApiConstraints(config, options);
|
|
8806
|
-
validatePythonApiConstraints(config);
|
|
8807
|
-
validateEmailConstraints(config);
|
|
8808
|
-
validateObservabilityConstraints(config);
|
|
8809
|
-
validateCachingConstraints(config);
|
|
8810
|
-
validateRateLimitConstraints(config);
|
|
8811
|
-
validateSearchConstraints(config);
|
|
8812
|
-
validateJavaConstraints(config, providedFlags);
|
|
8813
|
-
validateElixirConstraints(config);
|
|
8814
|
-
const hasGraphBackend = config.stackParts?.some((part) => part.role === "backend" && !part.ownerPartId && part.source !== "provided" && part.ecosystem !== "typescript" && part.ecosystem !== "react-native" && part.ecosystem !== "universal");
|
|
8815
|
-
if (!(providedFlags.has("serverDeploy") && !options.yes && !options.part?.length && options.ecosystem === void 0 && options.backend === void 0 && config.stackParts === void 0)) validateServerDeployRequiresBackend(config.serverDeploy, config.backend, Boolean(hasGraphBackend));
|
|
8816
|
-
validateSelfBackendCompatibility(providedFlags, options, config);
|
|
8817
|
-
validateWorkersCompatibility(providedFlags, options, config);
|
|
8818
|
-
if (config.runtime === "workers" && config.serverDeploy === "none") exitWithError("Cloudflare Workers runtime requires a server deployment. Please choose 'alchemy' for --server-deploy.");
|
|
8819
|
-
if (providedFlags.has("serverDeploy") && config.serverDeploy === "cloudflare" && config.runtime !== "workers") exitWithError(`Server deployment '${config.serverDeploy}' requires '--runtime workers'. Please use '--runtime workers' or choose a different server deployment.`);
|
|
8820
|
-
if (config.serverDeploy === "vercel" && [
|
|
8821
|
-
"nestjs",
|
|
8822
|
-
"adonisjs",
|
|
8823
|
-
"encore"
|
|
8824
|
-
].includes(config.backend)) incompatibilityError({
|
|
8825
|
-
message: "Vercel serverless functions cannot host persistent-process backends",
|
|
8826
|
-
provided: {
|
|
8827
|
-
backend: config.backend,
|
|
8828
|
-
serverDeploy: config.serverDeploy
|
|
8829
|
-
},
|
|
8830
|
-
suggestions: ["Use --server-deploy fly or --server-deploy railway for NestJS/AdonisJS", "Switch to a serverless-compatible backend like Hono or Express"]
|
|
8831
|
-
});
|
|
8832
|
-
if (config.serverDeploy === "netlify") {
|
|
8833
|
-
if (config.backend !== "hono") incompatibilityError({
|
|
8834
|
-
message: "Netlify Functions server deploy is currently supported only with Hono",
|
|
8835
|
-
provided: {
|
|
8836
|
-
backend: config.backend,
|
|
8837
|
-
serverDeploy: config.serverDeploy
|
|
8838
|
-
},
|
|
8839
|
-
suggestions: ["Use --backend hono", "Use --server-deploy fly, railway, render, or docker for this backend"]
|
|
8840
|
-
});
|
|
8841
|
-
if (config.runtime !== "node") incompatibilityError({
|
|
8842
|
-
message: "Netlify Functions server deploy requires Node.js runtime",
|
|
8843
|
-
provided: {
|
|
8844
|
-
runtime: config.runtime,
|
|
8845
|
-
serverDeploy: config.serverDeploy
|
|
8846
|
-
},
|
|
8847
|
-
suggestions: ["Use --runtime node", "Choose a different server deployment target"]
|
|
8848
|
-
});
|
|
8849
|
-
}
|
|
8850
|
-
if (config.addons && config.addons.length > 0) {
|
|
8851
|
-
validateAddonsAgainstFrontends(config.addons, config.frontend, config.auth, config.backend, config.runtime, config.ecosystem, config.rustFrontend, config.javaWebFramework, config.database);
|
|
8852
|
-
config.addons = [...new Set(config.addons)];
|
|
8853
|
-
}
|
|
8854
|
-
validateExamplesCompatibility(config.examples ?? [], config.backend, config.frontend ?? [], config.runtime, config.ai);
|
|
8855
|
-
validatePaymentsCompatibility(config.payments, config.auth, config.backend, config.frontend ?? []);
|
|
8856
|
-
validateAIFrontendCompatibility(config.ai, config.frontend ?? []);
|
|
8857
|
-
validateUILibraryFrontendCompatibility(config.uiLibrary, config.frontend ?? [], config.astroIntegration);
|
|
8858
|
-
validateUILibraryCSSFrameworkCompatibility(config.uiLibrary, config.cssFramework);
|
|
8859
|
-
validateShadcnConstraints(config, providedFlags);
|
|
8860
|
-
validatePeerDependencies(config);
|
|
8861
|
-
}
|
|
8862
|
-
function validateConfigForProgrammaticUse(config) {
|
|
8863
|
-
try {
|
|
8864
|
-
if (config.stackParts) {
|
|
8865
|
-
const graphValidation = (0, types_exports.validateStackParts)(config.stackParts);
|
|
8866
|
-
if (graphValidation.issues.length > 0) throw new Error(graphValidation.issues.map((issue) => issue.message).join("\n"));
|
|
8867
|
-
}
|
|
8868
|
-
validateEcosystemAuthCompatibility(config);
|
|
8869
|
-
validateDatabaseOrmAuth(config);
|
|
8870
|
-
if (config.frontend && config.frontend.length > 0) ensureSingleWebAndNative(config.frontend);
|
|
8871
|
-
validateApiFrontendCompatibility(config.api, config.frontend, config.astroIntegration);
|
|
8872
|
-
validatePythonApiConstraints(config);
|
|
8873
|
-
validateEmailConstraints(config);
|
|
8874
|
-
validateObservabilityConstraints(config);
|
|
8875
|
-
validateCachingConstraints(config);
|
|
8876
|
-
validateRateLimitConstraints(config);
|
|
8877
|
-
validateSearchConstraints(config);
|
|
8878
|
-
validateJavaConstraints(config);
|
|
8879
|
-
validateElixirConstraints(config);
|
|
8880
|
-
validatePaymentsCompatibility(config.payments, config.auth, config.backend, config.frontend);
|
|
8881
|
-
if (config.addons && config.addons.length > 0) validateAddonsAgainstFrontends(config.addons, config.frontend, config.auth, config.backend, config.runtime, config.ecosystem, config.rustFrontend, config.javaWebFramework, config.database);
|
|
8882
|
-
validateExamplesCompatibility(config.examples ?? [], config.backend, config.frontend ?? [], config.runtime, config.ai);
|
|
8883
|
-
validateAIFrontendCompatibility(config.ai, config.frontend ?? []);
|
|
8884
|
-
validateUILibraryFrontendCompatibility(config.uiLibrary, config.frontend ?? [], config.astroIntegration);
|
|
8885
|
-
validateUILibraryCSSFrameworkCompatibility(config.uiLibrary, config.cssFramework);
|
|
8886
|
-
validatePeerDependencies(config);
|
|
8887
|
-
} catch (error) {
|
|
8888
|
-
if (error instanceof Error) throw error;
|
|
8889
|
-
throw new Error(String(error), { cause: error });
|
|
8890
|
-
}
|
|
8891
|
-
}
|
|
8892
|
-
|
|
8893
7615
|
//#endregion
|
|
8894
7616
|
//#region src/validation.ts
|
|
8895
7617
|
const CORE_STACK_FLAGS = new Set([
|
|
@@ -10084,10 +8806,10 @@ async function setupDatabase(config, cliInput) {
|
|
|
10084
8806
|
const serverDbDir = path.join(projectDir, "apps/server/src/db");
|
|
10085
8807
|
if (await fs.pathExists(serverDbDir)) await fs.remove(serverDbDir);
|
|
10086
8808
|
}
|
|
10087
|
-
return;
|
|
8809
|
+
return null;
|
|
10088
8810
|
}
|
|
10089
8811
|
const dbPackageDir = path.join(projectDir, "packages/db");
|
|
10090
|
-
if (!await fs.pathExists(dbPackageDir)) return;
|
|
8812
|
+
if (!await fs.pathExists(dbPackageDir)) return null;
|
|
10091
8813
|
try {
|
|
10092
8814
|
if (dbSetup === "docker") await setupDockerCompose(config);
|
|
10093
8815
|
else if (database === "sqlite" && dbSetup === "turso") await setupTurso(config, cliInput);
|
|
@@ -10100,8 +8822,18 @@ async function setupDatabase(config, cliInput) {
|
|
|
10100
8822
|
} else if (database === "mysql" && dbSetup === "planetscale") await setupPlanetScale(config);
|
|
10101
8823
|
else if (database === "mongodb" && dbSetup === "mongodb-atlas") await setupMongoDBAtlas(config, cliInput);
|
|
10102
8824
|
else if (database === "redis" && dbSetup === "upstash") await setupUpstash(config, cliInput);
|
|
8825
|
+
return {
|
|
8826
|
+
step: "Database setup",
|
|
8827
|
+
success: true
|
|
8828
|
+
};
|
|
10103
8829
|
} catch (error) {
|
|
10104
|
-
|
|
8830
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
8831
|
+
consola.error(pc.red(errorMessage));
|
|
8832
|
+
return {
|
|
8833
|
+
step: "Database setup",
|
|
8834
|
+
success: false,
|
|
8835
|
+
errorMessage
|
|
8836
|
+
};
|
|
10105
8837
|
}
|
|
10106
8838
|
}
|
|
10107
8839
|
|
|
@@ -10701,14 +9433,14 @@ function displayJavaInstructions(config) {
|
|
|
10701
9433
|
}
|
|
10702
9434
|
const effectiveJavaTestingLibraries = javaBuildTool === "none" ? [] : javaTestingLibraries.filter((library) => library !== "none");
|
|
10703
9435
|
const buildToolCommand = javaBuildTool === "none" ? null : javaBuildTool === "gradle" ? process.platform === "win32" ? "gradlew.bat" : "./gradlew" : process.platform === "win32" ? "mvnw.cmd" : "./mvnw";
|
|
10704
|
-
const runCommand
|
|
9436
|
+
const runCommand = buildToolCommand ? isSpringBoot ? javaBuildTool === "gradle" ? `${buildToolCommand} bootRun` : `${buildToolCommand} spring-boot:run` : isQuarkus ? javaBuildTool === "gradle" ? `${buildToolCommand} quarkusDev` : `${buildToolCommand} quarkus:dev` : javaBuildTool === "gradle" ? `${buildToolCommand} run` : `${buildToolCommand} exec:java` : null;
|
|
10705
9437
|
const packageCommand = buildToolCommand ? javaBuildTool === "gradle" ? `${buildToolCommand} build` : `${buildToolCommand} package` : null;
|
|
10706
9438
|
const sourceCompileCommand = buildToolCommand ? null : `javac -d out ${getJavaMainSourcePath(projectName)}`;
|
|
10707
9439
|
const sourceRunCommand = buildToolCommand ? null : `java -cp out ${getJavaMainClass(projectName)}`;
|
|
10708
9440
|
let output = `${pc.bold("Next steps")}\n${pc.cyan("1.")} ${cdCmd}\n`;
|
|
10709
9441
|
let stepCounter = 2;
|
|
10710
9442
|
if (!depsInstalled && buildToolCommand && effectiveJavaTestingLibraries.length > 0) output += `${pc.cyan(`${stepCounter++}.`)} ${buildToolCommand} test\n`;
|
|
10711
|
-
if (runCommand
|
|
9443
|
+
if (runCommand) output += `${pc.cyan(`${stepCounter++}.`)} ${runCommand}\n`;
|
|
10712
9444
|
else if (sourceCompileCommand && sourceRunCommand) {
|
|
10713
9445
|
output += `${pc.cyan(`${stepCounter++}.`)} ${sourceCompileCommand}\n`;
|
|
10714
9446
|
output += `${pc.cyan(`${stepCounter++}.`)} ${sourceRunCommand}\n`;
|
|
@@ -10756,9 +9488,9 @@ function displayJavaInstructions(config) {
|
|
|
10756
9488
|
output += `${pc.cyan("•")} Testing: ${testingList}\n`;
|
|
10757
9489
|
}
|
|
10758
9490
|
output += `\n${pc.bold("Common Java commands:")}\n`;
|
|
10759
|
-
if (buildToolCommand && runCommand
|
|
9491
|
+
if (buildToolCommand && runCommand && packageCommand) {
|
|
10760
9492
|
if (effectiveJavaTestingLibraries.length > 0) output += `${pc.cyan("•")} Test: ${buildToolCommand} test\n`;
|
|
10761
|
-
output += `${pc.cyan("•")} Run: ${runCommand
|
|
9493
|
+
output += `${pc.cyan("•")} Run: ${runCommand}\n`;
|
|
10762
9494
|
output += `${pc.cyan("•")} Package: ${packageCommand}\n`;
|
|
10763
9495
|
} else if (sourceCompileCommand && sourceRunCommand) {
|
|
10764
9496
|
output += `${pc.cyan("•")} Compile: ${sourceCompileCommand}\n`;
|
|
@@ -10776,14 +9508,14 @@ function displayJavaInstructions(config) {
|
|
|
10776
9508
|
function displayPythonInstructions(config) {
|
|
10777
9509
|
const { relativePath, depsInstalled, pythonWebFramework, pythonOrm, pythonValidation, pythonAi, pythonApi, pythonTaskQueue, pythonQuality } = config;
|
|
10778
9510
|
const cdCmd = `cd ${relativePath}`;
|
|
10779
|
-
let runCommand
|
|
10780
|
-
if (pythonWebFramework === "django") runCommand
|
|
10781
|
-
else if (pythonWebFramework === "flask") runCommand
|
|
10782
|
-
else if (pythonWebFramework === "litestar") runCommand
|
|
9511
|
+
let runCommand = "uv run uvicorn app.main:app --reload";
|
|
9512
|
+
if (pythonWebFramework === "django") runCommand = "uv run python manage.py runserver";
|
|
9513
|
+
else if (pythonWebFramework === "flask") runCommand = "uv run flask --app app.main run --reload";
|
|
9514
|
+
else if (pythonWebFramework === "litestar") runCommand = "litestar --app src.app.main:app run --reload --port 3001";
|
|
10783
9515
|
let output = `${pc.bold("Next steps")}\n${pc.cyan("1.")} ${cdCmd}\n`;
|
|
10784
9516
|
let stepCounter = 2;
|
|
10785
9517
|
if (!depsInstalled) output += `${pc.cyan(`${stepCounter++}.`)} uv sync\n`;
|
|
10786
|
-
output += `${pc.cyan(`${stepCounter++}.`)} ${runCommand
|
|
9518
|
+
output += `${pc.cyan(`${stepCounter++}.`)} ${runCommand}\n`;
|
|
10787
9519
|
output += `\n${pc.bold("Your Python project includes:")}\n`;
|
|
10788
9520
|
if (pythonWebFramework && pythonWebFramework !== "none") output += `${pc.cyan("•")} Web Framework: ${{
|
|
10789
9521
|
fastapi: "FastAPI",
|
|
@@ -10821,7 +9553,7 @@ function displayPythonInstructions(config) {
|
|
|
10821
9553
|
}[pythonQuality] || pythonQuality}\n`;
|
|
10822
9554
|
output += `\n${pc.bold("Common Python commands:")}\n`;
|
|
10823
9555
|
output += `${pc.cyan("•")} Install: uv sync\n`;
|
|
10824
|
-
output += `${pc.cyan("•")} Run: ${runCommand
|
|
9556
|
+
output += `${pc.cyan("•")} Run: ${runCommand}\n`;
|
|
10825
9557
|
output += `${pc.cyan("•")} Test: uv run pytest\n`;
|
|
10826
9558
|
if (pythonQuality === "ruff") {
|
|
10827
9559
|
output += `${pc.cyan("•")} Format: uv run ruff format .\n`;
|
|
@@ -10871,6 +9603,8 @@ function displayElixirInstructions(config) {
|
|
|
10871
9603
|
async function createProject(options, cliInput = {}) {
|
|
10872
9604
|
const projectDir = options.projectDir;
|
|
10873
9605
|
const isConvex = options.backend === "convex";
|
|
9606
|
+
const setupFailures = [];
|
|
9607
|
+
const dirHadContentBefore = await fs.pathExists(projectDir) && (await fs.readdir(projectDir)).length > 0;
|
|
10874
9608
|
try {
|
|
10875
9609
|
await fs.ensureDir(projectDir);
|
|
10876
9610
|
const [{ generateVirtualProject, EMBEDDED_TEMPLATES }, { writeTreeToFilesystem }] = await Promise.all([import("@better-fullstack/template-generator"), import("@better-fullstack/template-generator/fs-writer")]);
|
|
@@ -10882,29 +9616,53 @@ async function createProject(options, cliInput = {}) {
|
|
|
10882
9616
|
await writeTreeToFilesystem(result.tree, projectDir);
|
|
10883
9617
|
await setPackageManagerVersion(projectDir, options.packageManager);
|
|
10884
9618
|
await ensurePackageManagerProjectFiles(projectDir, options.packageManager);
|
|
10885
|
-
if (!isConvex && options.database !== "none")
|
|
9619
|
+
if (!isConvex && options.database !== "none") {
|
|
9620
|
+
const dbResult = await setupDatabase(options, cliInput);
|
|
9621
|
+
if (dbResult && !dbResult.success) setupFailures.push(dbResult);
|
|
9622
|
+
}
|
|
10886
9623
|
if (options.addons.length > 0 && options.addons[0] !== "none") await setupAddons(options);
|
|
10887
9624
|
await applyDependencyVersionChannel(projectDir, options.versionChannel);
|
|
10888
9625
|
await writeBtsConfig(options);
|
|
10889
9626
|
await formatProject(projectDir);
|
|
10890
9627
|
if (!isSilent()) log.success("Project template successfully scaffolded!");
|
|
10891
|
-
if (options.install && (options.ecosystem === "typescript" || options.ecosystem === "react-native"))
|
|
10892
|
-
|
|
10893
|
-
|
|
10894
|
-
|
|
10895
|
-
|
|
10896
|
-
|
|
10897
|
-
|
|
10898
|
-
if (options.install && options.ecosystem === "
|
|
10899
|
-
|
|
10900
|
-
|
|
9628
|
+
if (options.install && (options.ecosystem === "typescript" || options.ecosystem === "react-native")) {
|
|
9629
|
+
const result$1 = await installDependencies({
|
|
9630
|
+
projectDir,
|
|
9631
|
+
packageManager: options.packageManager
|
|
9632
|
+
});
|
|
9633
|
+
if (!result$1.success) setupFailures.push(result$1);
|
|
9634
|
+
}
|
|
9635
|
+
if (options.install && options.ecosystem === "rust") {
|
|
9636
|
+
const result$1 = await runCargoBuild({ projectDir });
|
|
9637
|
+
if (!result$1.success) setupFailures.push(result$1);
|
|
9638
|
+
}
|
|
9639
|
+
if (options.install && options.ecosystem === "python") {
|
|
9640
|
+
const result$1 = await runUvSync({ projectDir });
|
|
9641
|
+
if (!result$1.success) setupFailures.push(result$1);
|
|
9642
|
+
}
|
|
9643
|
+
if (options.install && options.ecosystem === "go") {
|
|
9644
|
+
const result$1 = await runGoModTidy({ projectDir });
|
|
9645
|
+
if (!result$1.success) setupFailures.push(result$1);
|
|
9646
|
+
}
|
|
9647
|
+
if (options.install && options.ecosystem === "java" && options.javaBuildTool !== "none") {
|
|
9648
|
+
const result$1 = options.javaBuildTool === "gradle" ? await runGradleTests({ projectDir }) : await runMavenTests({ projectDir });
|
|
9649
|
+
if (!result$1.success) setupFailures.push(result$1);
|
|
9650
|
+
}
|
|
9651
|
+
if (options.install && options.ecosystem === "elixir") {
|
|
9652
|
+
const result$1 = await runMixCompile({ projectDir });
|
|
9653
|
+
if (!result$1.success) setupFailures.push(result$1);
|
|
9654
|
+
}
|
|
10901
9655
|
await initializeGit(projectDir, options.git);
|
|
10902
9656
|
if (!isSilent()) await displayPostInstallInstructions({
|
|
10903
9657
|
...options,
|
|
10904
9658
|
depsInstalled: options.install
|
|
10905
9659
|
});
|
|
10906
|
-
return
|
|
9660
|
+
return {
|
|
9661
|
+
projectDir,
|
|
9662
|
+
setupFailures
|
|
9663
|
+
};
|
|
10907
9664
|
} catch (error) {
|
|
9665
|
+
await rollbackPartialProject(projectDir, dirHadContentBefore);
|
|
10908
9666
|
if (error instanceof Error) {
|
|
10909
9667
|
if (!isSilent()) console.error(error.stack);
|
|
10910
9668
|
exitWithError(`Error during project creation: ${error.message}`);
|
|
@@ -10914,6 +9672,23 @@ async function createProject(options, cliInput = {}) {
|
|
|
10914
9672
|
}
|
|
10915
9673
|
}
|
|
10916
9674
|
}
|
|
9675
|
+
/**
|
|
9676
|
+
* Remove a half-written project directory after a fatal scaffolding error so the
|
|
9677
|
+
* user is not left with a broken, partially generated project. Only removes
|
|
9678
|
+
* directories we created ourselves (empty/new before scaffolding) — never one
|
|
9679
|
+
* that already had user content (merge mode). Set BTS_KEEP_FAILED_OUTPUT=1 to
|
|
9680
|
+
* keep the partial output for debugging template failures.
|
|
9681
|
+
*/
|
|
9682
|
+
async function rollbackPartialProject(projectDir, dirHadContentBefore) {
|
|
9683
|
+
if (dirHadContentBefore || process.env.BTS_KEEP_FAILED_OUTPUT) {
|
|
9684
|
+
if (!isSilent() && dirHadContentBefore) log.warn(`Left partially created files in ${projectDir} (directory already existed). Review and clean up manually.`);
|
|
9685
|
+
return;
|
|
9686
|
+
}
|
|
9687
|
+
try {
|
|
9688
|
+
await fs.remove(projectDir);
|
|
9689
|
+
if (!isSilent()) log.warn(`Cleaned up partially created project at ${projectDir}. Set BTS_KEEP_FAILED_OUTPUT=1 to keep it for debugging.`);
|
|
9690
|
+
} catch {}
|
|
9691
|
+
}
|
|
10917
9692
|
async function setPackageManagerVersion(projectDir, packageManager) {
|
|
10918
9693
|
const pkgJsonPath = path.join(projectDir, "package.json");
|
|
10919
9694
|
if (!await fs.pathExists(pkgJsonPath)) return;
|
|
@@ -10974,6 +9749,7 @@ function getYesBaseConfig(flagConfig) {
|
|
|
10974
9749
|
rateLimit: "none",
|
|
10975
9750
|
i18n: "none",
|
|
10976
9751
|
search: "none",
|
|
9752
|
+
vectorDb: "none",
|
|
10977
9753
|
fileStorage: "none",
|
|
10978
9754
|
mobileNavigation: "expo-router",
|
|
10979
9755
|
mobileUI: "none",
|
|
@@ -10984,8 +9760,8 @@ function getYesBaseConfig(flagConfig) {
|
|
|
10984
9760
|
mobileDeepLinking: "none"
|
|
10985
9761
|
};
|
|
10986
9762
|
}
|
|
10987
|
-
function shouldPromptForVersionChannel(input) {
|
|
10988
|
-
if (input.yes || input.part?.length || input.versionChannel !== void 0 || isSilent()) return false;
|
|
9763
|
+
function shouldPromptForVersionChannel(input, hasConfigBase) {
|
|
9764
|
+
if (input.yes || input.part?.length || hasConfigBase || input.versionChannel !== void 0 || isSilent()) return false;
|
|
10989
9765
|
return canPromptInteractively();
|
|
10990
9766
|
}
|
|
10991
9767
|
async function createProjectHandler(input, options = {}) {
|
|
@@ -10996,10 +9772,15 @@ async function createProjectHandler(input, options = {}) {
|
|
|
10996
9772
|
try {
|
|
10997
9773
|
if (!isSilent() && input.renderTitle !== false) renderTitle();
|
|
10998
9774
|
if (!isSilent()) intro(pc.magenta("Creating a new Better Fullstack project"));
|
|
9775
|
+
await maybeShowTelemetryNotice();
|
|
10999
9776
|
if (!isSilent() && input.yolo) consola.fatal("YOLO mode enabled - skipping checks. Things may break!");
|
|
9777
|
+
const configBase = await resolveCreateConfigBase(input);
|
|
9778
|
+
const hasConfigBase = configBase !== void 0;
|
|
9779
|
+
if (hasConfigBase && !isSilent()) log.info(pc.cyan(input.config ? `Using stack from config file: ${input.config}` : `Using stack from history entry #${input.fromHistory}`));
|
|
9780
|
+
const useDefaultsForName = Boolean(input.yes) || hasConfigBase;
|
|
11000
9781
|
let currentPathInput;
|
|
11001
|
-
if (
|
|
11002
|
-
else if (
|
|
9782
|
+
if (useDefaultsForName && input.projectName) currentPathInput = input.projectName;
|
|
9783
|
+
else if (useDefaultsForName) {
|
|
11003
9784
|
const defaultConfig = getDefaultConfig();
|
|
11004
9785
|
let defaultName = defaultConfig.relativePath;
|
|
11005
9786
|
let counter = 1;
|
|
@@ -11009,7 +9790,7 @@ async function createProjectHandler(input, options = {}) {
|
|
|
11009
9790
|
}
|
|
11010
9791
|
currentPathInput = defaultName;
|
|
11011
9792
|
} else currentPathInput = await getProjectName(input.projectName);
|
|
11012
|
-
const versionChannel = shouldPromptForVersionChannel(input) ? await getVersionChannelChoice() : input.versionChannel ?? "stable";
|
|
9793
|
+
const versionChannel = shouldPromptForVersionChannel(input, hasConfigBase) ? await getVersionChannelChoice() : input.versionChannel ?? configBase?.versionChannel ?? "stable";
|
|
11013
9794
|
let finalResolvedPath;
|
|
11014
9795
|
let finalBaseName;
|
|
11015
9796
|
if (input.dryRun) {
|
|
@@ -11088,6 +9869,7 @@ async function createProjectHandler(input, options = {}) {
|
|
|
11088
9869
|
rateLimit: "none",
|
|
11089
9870
|
i18n: "none",
|
|
11090
9871
|
search: "none",
|
|
9872
|
+
vectorDb: "none",
|
|
11091
9873
|
featureFlags: "none",
|
|
11092
9874
|
analytics: "none",
|
|
11093
9875
|
fileStorage: "none",
|
|
@@ -11174,11 +9956,15 @@ async function createProjectHandler(input, options = {}) {
|
|
|
11174
9956
|
finalBaseName = setupResult.finalBaseName;
|
|
11175
9957
|
currentPathInput = finalPathInput;
|
|
11176
9958
|
}
|
|
11177
|
-
const
|
|
11178
|
-
...input,
|
|
9959
|
+
const explicitInput = {
|
|
9960
|
+
...Object.fromEntries(Object.entries(input).filter(([, value]) => value !== void 0)),
|
|
11179
9961
|
projectDirectory: input.projectName
|
|
11180
9962
|
};
|
|
11181
|
-
const
|
|
9963
|
+
const originalInput = {
|
|
9964
|
+
...configBase,
|
|
9965
|
+
...explicitInput
|
|
9966
|
+
};
|
|
9967
|
+
const providedFlags = getProvidedFlags(explicitInput);
|
|
11182
9968
|
let cliInput = originalInput;
|
|
11183
9969
|
if (input.template && input.template !== "none") {
|
|
11184
9970
|
const templateConfig = getTemplateConfig(input.template);
|
|
@@ -11201,7 +9987,7 @@ async function createProjectHandler(input, options = {}) {
|
|
|
11201
9987
|
}
|
|
11202
9988
|
const { validatePreflightConfig } = await import("@better-fullstack/template-generator");
|
|
11203
9989
|
let config;
|
|
11204
|
-
if (cliInput.yes || cliInput.part?.length) {
|
|
9990
|
+
if (cliInput.yes || cliInput.part?.length || hasConfigBase) {
|
|
11205
9991
|
const flagConfig = processProvidedFlagsWithoutValidation(cliInput, finalBaseName);
|
|
11206
9992
|
config = {
|
|
11207
9993
|
...getYesBaseConfig(flagConfig),
|
|
@@ -11277,7 +10063,7 @@ async function createProjectHandler(input, options = {}) {
|
|
|
11277
10063
|
files
|
|
11278
10064
|
};
|
|
11279
10065
|
}
|
|
11280
|
-
await createProject(config, { manualDb: cliInput.manualDb ?? input.manualDb });
|
|
10066
|
+
const setupFailures = (await createProject(config, { manualDb: cliInput.manualDb ?? input.manualDb }))?.setupFailures ?? [];
|
|
11281
10067
|
if (cliInput.verify ?? input.verify) await runGeneratedChecks(config);
|
|
11282
10068
|
const reproducibleCommand = generateReproducibleCommand(config);
|
|
11283
10069
|
if (!isSilent()) log.success(pc.blue(`You can reproduce this setup with the following command:\n${reproducibleCommand}`));
|
|
@@ -11290,7 +10076,12 @@ async function createProjectHandler(input, options = {}) {
|
|
|
11290
10076
|
const elapsedTimeMs = Date.now() - startTime;
|
|
11291
10077
|
if (!isSilent()) {
|
|
11292
10078
|
const elapsedTimeInSeconds = (elapsedTimeMs / 1e3).toFixed(2);
|
|
11293
|
-
|
|
10079
|
+
if (setupFailures.length > 0) {
|
|
10080
|
+
const stepList = setupFailures.map((f) => f.step).join(", ");
|
|
10081
|
+
const installCmd = config.packageManager === "npm" ? "npm install" : `${config.packageManager} install`;
|
|
10082
|
+
log.warn(pc.yellow(`Project files were scaffolded in ${config.relativePath}, but ${setupFailures.length} setup step(s) did not complete: ${stepList}.\nReview the errors above, then finish setup manually (for example, run '${installCmd}' inside the project).`));
|
|
10083
|
+
outro(pc.yellow(`Project created with ${setupFailures.length} unfinished setup step(s) in ${pc.bold(elapsedTimeInSeconds)}s.`));
|
|
10084
|
+
} else outro(pc.magenta(`Project created successfully in ${pc.bold(elapsedTimeInSeconds)} seconds!`));
|
|
11294
10085
|
}
|
|
11295
10086
|
return {
|
|
11296
10087
|
success: true,
|
|
@@ -11299,7 +10090,8 @@ async function createProjectHandler(input, options = {}) {
|
|
|
11299
10090
|
timeScaffolded,
|
|
11300
10091
|
elapsedTimeMs,
|
|
11301
10092
|
projectDirectory: config.projectDir,
|
|
11302
|
-
relativePath: config.relativePath
|
|
10093
|
+
relativePath: config.relativePath,
|
|
10094
|
+
setupFailures
|
|
11303
10095
|
};
|
|
11304
10096
|
} catch (error) {
|
|
11305
10097
|
if (error instanceof UserCancelledError) {
|
|
@@ -11476,7 +10268,7 @@ const router = os.router({
|
|
|
11476
10268
|
packageManager: types_exports.PackageManagerSchema.optional().describe("Package manager to use"),
|
|
11477
10269
|
projectDir: z.string().optional().describe("Project directory (defaults to current)")
|
|
11478
10270
|
})).handler(async ({ input }) => {
|
|
11479
|
-
const { addHandler } = await import("./add-handler-
|
|
10271
|
+
const { addHandler } = await import("./add-handler-9F-AsGM-.mjs");
|
|
11480
10272
|
await addHandler(input);
|
|
11481
10273
|
}),
|
|
11482
10274
|
history: os.meta({ description: "Show history of scaffolded projects with reproducible commands" }).input(z.object({
|
|
@@ -11486,6 +10278,17 @@ const router = os.router({
|
|
|
11486
10278
|
})).handler(async ({ input }) => {
|
|
11487
10279
|
await historyHandler(input);
|
|
11488
10280
|
}),
|
|
10281
|
+
telemetry: os.meta({ description: "View or change anonymous usage telemetry collection (status | enable | disable)" }).input(z.tuple([z.enum([
|
|
10282
|
+
"status",
|
|
10283
|
+
"enable",
|
|
10284
|
+
"disable"
|
|
10285
|
+
]).optional().default("status").describe("Action to perform: status (default), enable, or disable"), z.object({ json: z.boolean().optional().default(false).describe("Output status as JSON") })])).handler(async ({ input }) => {
|
|
10286
|
+
const [action, options] = input;
|
|
10287
|
+
await telemetryHandler({
|
|
10288
|
+
action,
|
|
10289
|
+
json: options.json
|
|
10290
|
+
});
|
|
10291
|
+
}),
|
|
11489
10292
|
"update-deps": os.meta({ description: "Check and update dependency versions in add-deps.ts" }).input(z.object({
|
|
11490
10293
|
check: z.boolean().default(false).describe("Report only, no changes"),
|
|
11491
10294
|
patch: z.boolean().default(false).describe("Apply patch/minor updates only"),
|
|
@@ -11493,7 +10296,7 @@ const router = os.router({
|
|
|
11493
10296
|
ecosystem: z.string().optional().describe("Filter by ecosystem (effect, tanstack, prisma, etc.)"),
|
|
11494
10297
|
"list-ecosystems": z.boolean().default(false).describe("List available ecosystems")
|
|
11495
10298
|
})).handler(async ({ input }) => {
|
|
11496
|
-
const { updateDepsHandler, showEcosystems } = await import("./update-deps-
|
|
10299
|
+
const { updateDepsHandler, showEcosystems } = await import("./update-deps-D5OG0KmJ.mjs");
|
|
11497
10300
|
if (input["list-ecosystems"]) {
|
|
11498
10301
|
showEcosystems();
|
|
11499
10302
|
return;
|
|
@@ -11508,6 +10311,17 @@ const router = os.router({
|
|
|
11508
10311
|
mcp: os.meta({ description: "Start the Better Fullstack MCP server so AI agents can inspect the schema, plan stacks, and scaffold projects over stdio" }).handler(async () => {
|
|
11509
10312
|
log.message("MCP server is started via the 'mcp' subcommand intercepted in cli.ts.");
|
|
11510
10313
|
log.message("Run: create-better-fullstack mcp");
|
|
10314
|
+
}),
|
|
10315
|
+
doctor: os.meta({ description: "Diagnose a scaffolded Better Fullstack project: verify its bts.jsonc, installed dependencies, required env vars, and run ecosystem build/type checks" }).input(z.tuple([z.string().optional().describe("Project directory to diagnose (defaults to current directory)"), z.object({
|
|
10316
|
+
skipChecks: z.boolean().optional().default(false).describe("Skip the ecosystem build/type checks (config + deps + env only)"),
|
|
10317
|
+
json: z.boolean().optional().default(false).describe("Output the diagnosis as JSON")
|
|
10318
|
+
})])).handler(async ({ input }) => {
|
|
10319
|
+
const [projectDir, options] = input;
|
|
10320
|
+
const { doctorCommand } = await import("./doctor-BFSSbS-U.mjs");
|
|
10321
|
+
await doctorCommand({
|
|
10322
|
+
projectDir,
|
|
10323
|
+
...options
|
|
10324
|
+
});
|
|
11511
10325
|
})
|
|
11512
10326
|
});
|
|
11513
10327
|
const caller = createRouterClient(router, { context: {} });
|
|
@@ -11573,7 +10387,7 @@ async function builder() {
|
|
|
11573
10387
|
return caller.builder();
|
|
11574
10388
|
}
|
|
11575
10389
|
async function add(input) {
|
|
11576
|
-
const { addHandler } = await import("./add-handler-
|
|
10390
|
+
const { addHandler } = await import("./add-handler-9F-AsGM-.mjs");
|
|
11577
10391
|
return addHandler(input, { silent: true });
|
|
11578
10392
|
}
|
|
11579
10393
|
async function history(options) {
|
|
@@ -11583,6 +10397,9 @@ async function history(options) {
|
|
|
11583
10397
|
json: options?.json ?? false
|
|
11584
10398
|
});
|
|
11585
10399
|
}
|
|
10400
|
+
async function telemetry(action = "status", options) {
|
|
10401
|
+
return caller.telemetry([action, { json: options?.json ?? false }]);
|
|
10402
|
+
}
|
|
11586
10403
|
|
|
11587
10404
|
//#endregion
|
|
11588
|
-
export { docs as a, sponsors as c, createBtsCli as i, builder as n, history as o, create as r, router as s, add as t };
|
|
10405
|
+
export { docs as a, sponsors as c, createBtsCli as i, telemetry as l, builder as n, history as o, create as r, router as s, add as t };
|