create-better-fullstack 2.0.2 → 2.1.0
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-BPAZM8Zo.mjs → add-handler-BjTmOwEN.mjs} +11 -9
- package/dist/addons-setup-CSmj5ClW.mjs +6 -0
- package/dist/{addons-setup-HSghQS7c.mjs → addons-setup-uSvqHagW.mjs} +4 -83
- package/dist/{bts-config-Bg1Qea9Y.mjs → bts-config-InNcw1aJ.mjs} +90 -68
- package/dist/cli.mjs +2 -2
- package/dist/doctor-Bm9qvkya.mjs +281 -0
- package/dist/errors-ns_o2OKg.mjs +86 -0
- package/dist/generated-checks-CUhFFfVo.mjs +91 -0
- package/dist/index.d.mts +100 -41
- package/dist/index.mjs +10 -6
- package/dist/{install-dependencies-D6-GO3BZ.mjs → install-dependencies-CDjTNvIV.mjs} +126 -49
- package/dist/{mcp-58r70ZcL.mjs → mcp-BaLHDRdC.mjs} +1 -1
- package/dist/mcp-entry.mjs +601 -137
- package/dist/render-title-zvyKC1ej.mjs +34 -0
- package/dist/{run-DQlymC4z.mjs → run-Cqy-n1iT.mjs} +586 -654
- package/dist/run-DYWSKowD.mjs +10 -0
- package/dist/templates-B351F1yX.mjs +489 -0
- package/package.json +7 -6
- package/dist/addons-setup-CV5uZyhH.mjs +0 -5
- package/dist/run-QRBymn-p.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 { a as
|
|
2
|
+
import { c as DEFAULT_CONFIG, d as getUserPkgManager, g as getGraphSummary, h as getGraphPart, i as readBtsConfigFromFile, l as DEFAULT_UI_LIBRARY_BY_FRONTEND, m as getGraphBackendUrl, o as writeBtsConfig, p as getGraphBackendDeployInstructions, s as getLatestCLIVersion, t as buildBtsConfigForPersistence, u as getDefaultConfig, v as hasGraphPart, y as types_exports } from "./bts-config-InNcw1aJ.mjs";
|
|
3
|
+
import { t as renderTitle } from "./render-title-zvyKC1ej.mjs";
|
|
4
|
+
import { a as handleError, c as isSilent, d as setLastPromptShownUI, i as exitWithError, l as runWithContextAsync, n as UserCancelledError, o as didLastPromptShowUI, r as exitCancelled, s as isFirstPrompt, t as CLIError, u as setIsFirstPrompt } from "./errors-ns_o2OKg.mjs";
|
|
5
|
+
import { i as canPromptInteractively, n as getPackageExecutionArgs, r as addPackageDependency, t as setupAddons } from "./addons-setup-uSvqHagW.mjs";
|
|
6
|
+
import { A as validateAddonsAgainstFrontends, B as validateWorkersCompatibility, C as isExampleAIAllowed$1, D as requiresChatSdkVercelAI, E as isWebFrontend$1, F as validateServerDeployRequiresBackend, H as incompatibilityError, I as validateUILibraryCSSFrameworkCompatibility, L as validateUILibraryFrontendCompatibility, M as validateExamplesCompatibility, N as validatePaymentsCompatibility, O as splitFrontends$1, P as validateSelfBackendCompatibility, R as validateWebDeployFrontendTemplates, S as hasWebStyling$1, T as isFrontendAllowedWithBackend$1, U as missingRequirementError, V as constraintError, _ as isGoBack, a as runMavenTests, b as getCompatibleCSSFrameworks$1, c as applyDependencyVersionChannel, d as isCancel$1, f as navigableConfirm, g as GO_BACK_SYMBOL, h as setIsFirstPrompt$1, i as runGradleTests, j as validateApiFrontendCompatibility, k as validateAIFrontendCompatibility, l as getAddonsChoice, m as navigableSelect, n as runCargoBuild, o as runMixCompile, p as navigableMultiselect, r as runGoModTidy, s as runUvSync, t as installDependencies, v as allowedApisForFrontends$1, w as isExampleChatSdkAllowed$1, x as getCompatibleUILibraries$1, y as ensureSingleWebAndNative, z as validateWebDeployRequiresWebFrontend } from "./install-dependencies-CDjTNvIV.mjs";
|
|
7
|
+
import { n as commandExists, t as runGeneratedChecks } from "./generated-checks-CUhFFfVo.mjs";
|
|
8
|
+
import { n as getTemplateDescription, r as generateReproducibleCommand, t as getTemplateConfig } from "./templates-B351F1yX.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);
|
|
@@ -133,10 +155,195 @@ async function historyHandler(input) {
|
|
|
133
155
|
}
|
|
134
156
|
}
|
|
135
157
|
|
|
158
|
+
//#endregion
|
|
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
|
+
}
|
|
340
|
+
|
|
136
341
|
//#endregion
|
|
137
342
|
//#region src/create-command-input.ts
|
|
138
343
|
const CreateCommandOptionsSchema = z.object({
|
|
139
344
|
template: types_exports.TemplateSchema.optional().describe("Use a predefined template"),
|
|
345
|
+
fromHistory: z.number().optional().describe("Replay the stack of the Nth most-recent project from history (1 = most recent)"),
|
|
346
|
+
config: z.string().optional().describe("Path to a bts.jsonc/JSON config file to use as the base stack"),
|
|
140
347
|
yes: z.boolean().optional().default(false).describe("Use default configuration"),
|
|
141
348
|
yolo: z.boolean().optional().default(false).describe("(WARNING - NOT RECOMMENDED) Bypass validations and compatibility checks"),
|
|
142
349
|
part: z.array(z.string()).optional().describe("Stack graph part binding, e.g. frontend:typescript:next or backend.orm:go:gorm"),
|
|
@@ -168,6 +375,7 @@ const CreateCommandOptionsSchema = z.object({
|
|
|
168
375
|
rateLimit: types_exports.RateLimitSchema.optional().describe("Rate limiting solution"),
|
|
169
376
|
i18n: types_exports.I18nSchema.optional().describe("Internationalization (i18n) library"),
|
|
170
377
|
search: types_exports.SearchSchema.optional().describe("Search engine solution"),
|
|
378
|
+
vectorDb: types_exports.VectorDbSchema.optional().describe("Vector database for AI embeddings (pgvector, qdrant, chroma, pinecone)"),
|
|
171
379
|
fileStorage: types_exports.FileStorageSchema.optional().describe("File storage solution (S3, R2)"),
|
|
172
380
|
mobileNavigation: types_exports.MobileNavigationSchema.optional().describe("Mobile navigation (expo-router, react-navigation)"),
|
|
173
381
|
mobileUI: types_exports.MobileUISchema.optional().describe("Mobile UI (tamagui, gluestack-ui, uniwind, unistyles)"),
|
|
@@ -400,7 +608,7 @@ const AI_DOCS_OPTIONS = [
|
|
|
400
608
|
},
|
|
401
609
|
{
|
|
402
610
|
value: "agents-md",
|
|
403
|
-
label: "
|
|
611
|
+
label: "AGENTS.md",
|
|
404
612
|
hint: "Generic AI assistant documentation"
|
|
405
613
|
},
|
|
406
614
|
{
|
|
@@ -531,6 +739,16 @@ const API_PROMPT_OPTION_MAP = {
|
|
|
531
739
|
label: "GraphQL Yoga",
|
|
532
740
|
hint: "Batteries-included GraphQL server with Pothos schema builder"
|
|
533
741
|
},
|
|
742
|
+
"apollo-server": {
|
|
743
|
+
value: "apollo-server",
|
|
744
|
+
label: "Apollo Server",
|
|
745
|
+
hint: "Spec-compliant GraphQL server with a schema-first starter"
|
|
746
|
+
},
|
|
747
|
+
openapi: {
|
|
748
|
+
value: "openapi",
|
|
749
|
+
label: "OpenAPI",
|
|
750
|
+
hint: "REST scaffolding with an OpenAPI 3.1 spec and Scalar docs"
|
|
751
|
+
},
|
|
534
752
|
none: {
|
|
535
753
|
value: "none",
|
|
536
754
|
label: "None",
|
|
@@ -894,6 +1112,11 @@ const CMS_PROMPT_OPTIONS = [
|
|
|
894
1112
|
label: "Directus",
|
|
895
1113
|
hint: "Open data platform and headless CMS for SQL databases"
|
|
896
1114
|
},
|
|
1115
|
+
{
|
|
1116
|
+
value: "keystatic",
|
|
1117
|
+
label: "Keystatic",
|
|
1118
|
+
hint: "Git-backed CMS for Markdown, JSON, and YAML content"
|
|
1119
|
+
},
|
|
897
1120
|
{
|
|
898
1121
|
value: "none",
|
|
899
1122
|
label: "None",
|
|
@@ -2356,7 +2579,7 @@ async function getFrontendChoice(frontendOptions, backend, auth) {
|
|
|
2356
2579
|
});
|
|
2357
2580
|
if (isGoBack(frontendTypes)) return GO_BACK_SYMBOL;
|
|
2358
2581
|
if (isCancel$1(frontendTypes)) return exitCancelled("Operation cancelled");
|
|
2359
|
-
setIsFirstPrompt(false);
|
|
2582
|
+
setIsFirstPrompt$1(false);
|
|
2360
2583
|
const result = [];
|
|
2361
2584
|
let shouldRestart = false;
|
|
2362
2585
|
if (frontendTypes.includes("web")) {
|
|
@@ -2372,7 +2595,7 @@ async function getFrontendChoice(frontendOptions, backend, auth) {
|
|
|
2372
2595
|
else result.push(webFramework);
|
|
2373
2596
|
}
|
|
2374
2597
|
if (shouldRestart) {
|
|
2375
|
-
setIsFirstPrompt(wasFirstPrompt);
|
|
2598
|
+
setIsFirstPrompt$1(wasFirstPrompt);
|
|
2376
2599
|
continue;
|
|
2377
2600
|
}
|
|
2378
2601
|
if (frontendTypes.includes("native")) {
|
|
@@ -2385,14 +2608,14 @@ async function getFrontendChoice(frontendOptions, backend, auth) {
|
|
|
2385
2608
|
});
|
|
2386
2609
|
if (isGoBack(nativeFramework)) if (frontendTypes.includes("web")) shouldRestart = true;
|
|
2387
2610
|
else {
|
|
2388
|
-
setIsFirstPrompt(wasFirstPrompt);
|
|
2611
|
+
setIsFirstPrompt$1(wasFirstPrompt);
|
|
2389
2612
|
continue;
|
|
2390
2613
|
}
|
|
2391
2614
|
else if (isCancel$1(nativeFramework)) return exitCancelled("Operation cancelled");
|
|
2392
2615
|
else result.push(nativeFramework);
|
|
2393
2616
|
}
|
|
2394
2617
|
if (shouldRestart) {
|
|
2395
|
-
setIsFirstPrompt(wasFirstPrompt);
|
|
2618
|
+
setIsFirstPrompt$1(wasFirstPrompt);
|
|
2396
2619
|
continue;
|
|
2397
2620
|
}
|
|
2398
2621
|
return result;
|
|
@@ -2827,17 +3050,36 @@ async function getGoObservabilityChoice(goObservability) {
|
|
|
2827
3050
|
|
|
2828
3051
|
//#endregion
|
|
2829
3052
|
//#region src/prompts/i18n.ts
|
|
3053
|
+
const PARAGLIDE_FRONTENDS = new Set([
|
|
3054
|
+
"next",
|
|
3055
|
+
"nuxt",
|
|
3056
|
+
"vinext",
|
|
3057
|
+
"tanstack-router",
|
|
3058
|
+
"tanstack-start",
|
|
3059
|
+
"react-router",
|
|
3060
|
+
"react-vite",
|
|
3061
|
+
"svelte",
|
|
3062
|
+
"solid",
|
|
3063
|
+
"solid-start",
|
|
3064
|
+
"astro"
|
|
3065
|
+
]);
|
|
2830
3066
|
async function getI18nChoice(i18n, frontend) {
|
|
2831
3067
|
if (i18n !== void 0) return i18n;
|
|
3068
|
+
const hasNext = frontend?.includes("next") ?? false;
|
|
2832
3069
|
const response = await navigableSelect({
|
|
2833
3070
|
message: "Select internationalization (i18n) library",
|
|
2834
3071
|
options: [
|
|
3072
|
+
...frontend?.some((f) => PARAGLIDE_FRONTENDS.has(f)) ?? false ? [{
|
|
3073
|
+
value: "paraglide",
|
|
3074
|
+
label: "Paraglide",
|
|
3075
|
+
hint: "Type-safe, compiler-based i18n for modern web frontends"
|
|
3076
|
+
}] : [],
|
|
2835
3077
|
{
|
|
2836
3078
|
value: "i18next",
|
|
2837
3079
|
label: "i18next",
|
|
2838
3080
|
hint: "Full-featured i18n framework, works with all frontends"
|
|
2839
3081
|
},
|
|
2840
|
-
...
|
|
3082
|
+
...hasNext ? [{
|
|
2841
3083
|
value: "next-intl",
|
|
2842
3084
|
label: "next-intl",
|
|
2843
3085
|
hint: "Lightweight i18n for Next.js with App Router support"
|
|
@@ -2854,17 +3096,6 @@ async function getI18nChoice(i18n, frontend) {
|
|
|
2854
3096
|
return response;
|
|
2855
3097
|
}
|
|
2856
3098
|
|
|
2857
|
-
//#endregion
|
|
2858
|
-
//#region src/utils/command-exists.ts
|
|
2859
|
-
async function commandExists(command) {
|
|
2860
|
-
try {
|
|
2861
|
-
if (process.platform === "win32") return (await $({ reject: false })`where ${command}`).exitCode === 0;
|
|
2862
|
-
return (await $({ reject: false })`which ${command}`).exitCode === 0;
|
|
2863
|
-
} catch {
|
|
2864
|
-
return false;
|
|
2865
|
-
}
|
|
2866
|
-
}
|
|
2867
|
-
|
|
2868
3099
|
//#endregion
|
|
2869
3100
|
//#region src/prompts/install.ts
|
|
2870
3101
|
async function getinstallChoice(install, ecosystem, javaBuildTool) {
|
|
@@ -5613,7 +5844,7 @@ async function navigableGroup(prompts, opts) {
|
|
|
5613
5844
|
while (currentIndex < promptNames.length) {
|
|
5614
5845
|
const name = promptNames[currentIndex];
|
|
5615
5846
|
const prompt = prompts[name];
|
|
5616
|
-
setIsFirstPrompt
|
|
5847
|
+
setIsFirstPrompt(currentIndex === 0);
|
|
5617
5848
|
setLastPromptShownUI(false);
|
|
5618
5849
|
const result = await prompt({ results })?.catch((e) => {
|
|
5619
5850
|
throw e;
|
|
@@ -5634,7 +5865,7 @@ async function navigableGroup(prompts, opts) {
|
|
|
5634
5865
|
results[name] = "canceled";
|
|
5635
5866
|
opts.onCancel({ results });
|
|
5636
5867
|
}
|
|
5637
|
-
setIsFirstPrompt
|
|
5868
|
+
setIsFirstPrompt(false);
|
|
5638
5869
|
return results;
|
|
5639
5870
|
}
|
|
5640
5871
|
if (goingBack && !didLastPromptShowUI()) {
|
|
@@ -5649,7 +5880,7 @@ async function navigableGroup(prompts, opts) {
|
|
|
5649
5880
|
results[name] = result;
|
|
5650
5881
|
currentIndex++;
|
|
5651
5882
|
}
|
|
5652
|
-
setIsFirstPrompt
|
|
5883
|
+
setIsFirstPrompt(false);
|
|
5653
5884
|
return results;
|
|
5654
5885
|
}
|
|
5655
5886
|
|
|
@@ -6090,6 +6321,11 @@ const SEARCH_PROMPT_OPTIONS = [
|
|
|
6090
6321
|
label: "Elasticsearch",
|
|
6091
6322
|
hint: "Distributed search and analytics engine with local and cloud deployments"
|
|
6092
6323
|
},
|
|
6324
|
+
{
|
|
6325
|
+
value: "opensearch",
|
|
6326
|
+
label: "OpenSearch",
|
|
6327
|
+
hint: "Open-source search and analytics suite compatible with Elasticsearch APIs"
|
|
6328
|
+
},
|
|
6093
6329
|
{
|
|
6094
6330
|
value: "algolia",
|
|
6095
6331
|
label: "Algolia",
|
|
@@ -6144,6 +6380,77 @@ async function getSearchChoice(search, backend, ecosystem) {
|
|
|
6144
6380
|
return response;
|
|
6145
6381
|
}
|
|
6146
6382
|
|
|
6383
|
+
//#endregion
|
|
6384
|
+
//#region src/prompts/vector-db.ts
|
|
6385
|
+
const VECTOR_DB_PROMPT_OPTIONS = [
|
|
6386
|
+
{
|
|
6387
|
+
value: "pgvector",
|
|
6388
|
+
label: "pgvector",
|
|
6389
|
+
hint: "Self-hosted Postgres + pgvector extension for embeddings"
|
|
6390
|
+
},
|
|
6391
|
+
{
|
|
6392
|
+
value: "qdrant",
|
|
6393
|
+
label: "Qdrant",
|
|
6394
|
+
hint: "High-performance open-source vector database"
|
|
6395
|
+
},
|
|
6396
|
+
{
|
|
6397
|
+
value: "chroma",
|
|
6398
|
+
label: "Chroma",
|
|
6399
|
+
hint: "Lightweight open-source embedding database"
|
|
6400
|
+
},
|
|
6401
|
+
{
|
|
6402
|
+
value: "pinecone",
|
|
6403
|
+
label: "Pinecone",
|
|
6404
|
+
hint: "Fully managed serverless vector database"
|
|
6405
|
+
},
|
|
6406
|
+
{
|
|
6407
|
+
value: "none",
|
|
6408
|
+
label: "None",
|
|
6409
|
+
hint: "Skip vector database setup"
|
|
6410
|
+
}
|
|
6411
|
+
];
|
|
6412
|
+
/**
|
|
6413
|
+
* Vector DB is a TypeScript-ecosystem feature backed by a standalone server.
|
|
6414
|
+
* Every provider (including pgvector via a dedicated Postgres instance) is a
|
|
6415
|
+
* separate service, so there is no dependency on the primary database choice.
|
|
6416
|
+
*/
|
|
6417
|
+
function resolveVectorDbPrompt(context = {}) {
|
|
6418
|
+
const skip = () => ({
|
|
6419
|
+
shouldPrompt: false,
|
|
6420
|
+
mode: "single",
|
|
6421
|
+
options: [],
|
|
6422
|
+
autoValue: "none"
|
|
6423
|
+
});
|
|
6424
|
+
if (context.ecosystem && context.ecosystem !== "typescript") return skip();
|
|
6425
|
+
if (context.backend === "none" || context.backend === "convex") return skip();
|
|
6426
|
+
return context.vectorDb !== void 0 ? {
|
|
6427
|
+
shouldPrompt: false,
|
|
6428
|
+
mode: "single",
|
|
6429
|
+
options: VECTOR_DB_PROMPT_OPTIONS,
|
|
6430
|
+
autoValue: context.vectorDb
|
|
6431
|
+
} : {
|
|
6432
|
+
shouldPrompt: true,
|
|
6433
|
+
mode: "single",
|
|
6434
|
+
options: VECTOR_DB_PROMPT_OPTIONS,
|
|
6435
|
+
initialValue: "none"
|
|
6436
|
+
};
|
|
6437
|
+
}
|
|
6438
|
+
async function getVectorDbChoice(vectorDb, backend, ecosystem) {
|
|
6439
|
+
const resolution = resolveVectorDbPrompt({
|
|
6440
|
+
vectorDb,
|
|
6441
|
+
backend,
|
|
6442
|
+
ecosystem
|
|
6443
|
+
});
|
|
6444
|
+
if (!resolution.shouldPrompt) return resolution.autoValue ?? "none";
|
|
6445
|
+
const response = await navigableSelect({
|
|
6446
|
+
message: "Select vector database",
|
|
6447
|
+
options: resolution.options,
|
|
6448
|
+
initialValue: resolution.initialValue
|
|
6449
|
+
});
|
|
6450
|
+
if (isCancel$1(response)) return exitCancelled("Operation cancelled");
|
|
6451
|
+
return response;
|
|
6452
|
+
}
|
|
6453
|
+
|
|
6147
6454
|
//#endregion
|
|
6148
6455
|
//#region src/prompts/server-deploy.ts
|
|
6149
6456
|
async function getServerDeploymentChoice(deployment, runtime, backend, _webDeploy) {
|
|
@@ -6437,7 +6744,7 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
|
|
|
6437
6744
|
},
|
|
6438
6745
|
addons: ({ results }) => {
|
|
6439
6746
|
if (results.ecosystem !== "typescript") {
|
|
6440
|
-
const nonTypeScriptAddons = (flags.addons ?? []).filter((addon) => addon === "docker-compose");
|
|
6747
|
+
const nonTypeScriptAddons = (flags.addons ?? []).filter((addon) => addon === "docker-compose" || addon === "devcontainer" || addon === "github-actions");
|
|
6441
6748
|
return Promise.resolve(nonTypeScriptAddons);
|
|
6442
6749
|
}
|
|
6443
6750
|
return getAddonsChoice(flags.addons, results.frontend, results.auth, results.backend, results.runtime);
|
|
@@ -6531,6 +6838,10 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
|
|
|
6531
6838
|
if (results.ecosystem === "react-native" || results.ecosystem === "elixir") return Promise.resolve("none");
|
|
6532
6839
|
return getSearchChoice(flags.search, results.backend, results.ecosystem);
|
|
6533
6840
|
},
|
|
6841
|
+
vectorDb: ({ results }) => {
|
|
6842
|
+
if (results.ecosystem !== "typescript") return Promise.resolve("none");
|
|
6843
|
+
return getVectorDbChoice(flags.vectorDb, results.backend, results.ecosystem);
|
|
6844
|
+
},
|
|
6534
6845
|
fileStorage: ({ results }) => {
|
|
6535
6846
|
if (results.ecosystem !== "typescript") return Promise.resolve("none");
|
|
6536
6847
|
return getFileStorageChoice(flags.fileStorage, results.backend);
|
|
@@ -6938,6 +7249,7 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
|
|
|
6938
7249
|
rateLimit: result.rateLimit,
|
|
6939
7250
|
i18n: result.i18n,
|
|
6940
7251
|
search: result.search,
|
|
7252
|
+
vectorDb: result.vectorDb,
|
|
6941
7253
|
fileStorage: result.fileStorage,
|
|
6942
7254
|
mobileNavigation: result.mobileNavigation,
|
|
6943
7255
|
mobileUI: result.mobileUI,
|
|
@@ -7106,35 +7418,76 @@ async function getVersionChannelChoice(versionChannel) {
|
|
|
7106
7418
|
}
|
|
7107
7419
|
|
|
7108
7420
|
//#endregion
|
|
7109
|
-
//#region src/utils/
|
|
7110
|
-
|
|
7111
|
-
|
|
7112
|
-
|
|
7113
|
-
|
|
7114
|
-
|
|
7115
|
-
|
|
7116
|
-
|
|
7421
|
+
//#region src/utils/config-source.ts
|
|
7422
|
+
/**
|
|
7423
|
+
* CreateInput flag keys that can be sourced from a stored config. `projectName`
|
|
7424
|
+
* is excluded so a replayed config never reuses the original project name.
|
|
7425
|
+
*/
|
|
7426
|
+
const COPYABLE_CREATE_INPUT_KEYS = Object.keys(types_exports.CreateInputSchema.shape).filter((key) => key !== "projectName");
|
|
7427
|
+
/**
|
|
7428
|
+
* Projects a persisted Better-Fullstack config (bts.jsonc shape) onto the
|
|
7429
|
+
* subset of `create` flags it can drive. Only defined values are copied so the
|
|
7430
|
+
* result can be safely overlaid by explicitly-passed CLI flags.
|
|
7431
|
+
*/
|
|
7432
|
+
function betterTStackConfigToCreateInput(config) {
|
|
7433
|
+
const source = config;
|
|
7434
|
+
const result = {};
|
|
7435
|
+
for (const key of COPYABLE_CREATE_INPUT_KEYS) {
|
|
7436
|
+
const value = source[key];
|
|
7437
|
+
if (value !== void 0 && value !== null) result[key] = value;
|
|
7438
|
+
}
|
|
7439
|
+
return result;
|
|
7117
7440
|
}
|
|
7118
|
-
|
|
7119
|
-
|
|
7120
|
-
|
|
7121
|
-
|
|
7122
|
-
|
|
7123
|
-
|
|
7124
|
-
|
|
7125
|
-
|
|
7441
|
+
/**
|
|
7442
|
+
* Maps a legacy history entry (written before the full config snapshot existed)
|
|
7443
|
+
* onto `create` flags using the limited stack summary it stored.
|
|
7444
|
+
*/
|
|
7445
|
+
function historyStackToCreateInput(stack) {
|
|
7446
|
+
return {
|
|
7447
|
+
frontend: stack.frontend,
|
|
7448
|
+
backend: stack.backend,
|
|
7449
|
+
database: stack.database,
|
|
7450
|
+
orm: stack.orm,
|
|
7451
|
+
runtime: stack.runtime,
|
|
7452
|
+
auth: stack.auth,
|
|
7453
|
+
payments: stack.payments,
|
|
7454
|
+
api: stack.api,
|
|
7455
|
+
addons: stack.addons,
|
|
7456
|
+
examples: stack.examples,
|
|
7457
|
+
dbSetup: stack.dbSetup,
|
|
7458
|
+
packageManager: stack.packageManager
|
|
7459
|
+
};
|
|
7126
7460
|
}
|
|
7127
|
-
|
|
7128
|
-
|
|
7129
|
-
|
|
7130
|
-
|
|
7131
|
-
|
|
7132
|
-
|
|
7133
|
-
|
|
7134
|
-
|
|
7135
|
-
|
|
7136
|
-
|
|
7137
|
-
|
|
7461
|
+
/**
|
|
7462
|
+
* Resolves the `--from-history` / `--config` flags into a base set of `create`
|
|
7463
|
+
* flags. Returns `undefined` when neither flag is used. The two flags are
|
|
7464
|
+
* mutually exclusive, and both are incompatible with `--yes`, `--template`, and
|
|
7465
|
+
* `--part` (each of those also wants to own the full stack). On any user error
|
|
7466
|
+
* this calls `exitWithError`, which exits the CLI (or throws a CLIError in
|
|
7467
|
+
* silent/programmatic mode).
|
|
7468
|
+
*/
|
|
7469
|
+
async function resolveCreateConfigBase(input) {
|
|
7470
|
+
const hasHistory = input.fromHistory !== void 0;
|
|
7471
|
+
const hasConfig = input.config !== void 0 && input.config !== "";
|
|
7472
|
+
if (!hasHistory && !hasConfig) return;
|
|
7473
|
+
if (hasHistory && hasConfig) exitWithError("Cannot combine --from-history with --config. Pass only one config source.");
|
|
7474
|
+
const sourceFlag = hasHistory ? "--from-history" : "--config";
|
|
7475
|
+
if (input.yes) exitWithError(`Cannot combine --yes with ${sourceFlag}: the config already provides a complete stack. Remove --yes.`);
|
|
7476
|
+
if (input.template && input.template !== "none") exitWithError(`Cannot combine --template with ${sourceFlag}. Choose either a template or a saved config as the base.`);
|
|
7477
|
+
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.`);
|
|
7478
|
+
if (hasConfig) {
|
|
7479
|
+
const loaded = await readBtsConfigFromFile(input.config);
|
|
7480
|
+
if (!loaded) exitWithError(`Could not load config file: ${input.config}. Ensure the path points to a valid bts.jsonc/JSON config.`);
|
|
7481
|
+
return betterTStackConfigToCreateInput(loaded);
|
|
7482
|
+
}
|
|
7483
|
+
const position = input.fromHistory;
|
|
7484
|
+
if (!Number.isInteger(position) || position < 1) exitWithError(`Invalid --from-history value: ${position}. Provide a positive integer (1 = most recent).`);
|
|
7485
|
+
const entry = await getHistoryEntry(position);
|
|
7486
|
+
if (!entry) {
|
|
7487
|
+
const count = await getHistoryCount();
|
|
7488
|
+
exitWithError(`No project history entry at position ${position}.${count === 0 ? " Project history is empty." : ` History has ${count} ${count === 1 ? "entry" : "entries"} (use 1-${count}).`}`);
|
|
7489
|
+
}
|
|
7490
|
+
return entry.config ? betterTStackConfigToCreateInput(entry.config) : historyStackToCreateInput(entry.stack);
|
|
7138
7491
|
}
|
|
7139
7492
|
|
|
7140
7493
|
//#endregion
|
|
@@ -7279,480 +7632,6 @@ function displayConfig(config) {
|
|
|
7279
7632
|
return configDisplay.join("\n");
|
|
7280
7633
|
}
|
|
7281
7634
|
|
|
7282
|
-
//#endregion
|
|
7283
|
-
//#region src/utils/generate-reproducible-command.ts
|
|
7284
|
-
function getBaseCommand(packageManager) {
|
|
7285
|
-
switch (packageManager) {
|
|
7286
|
-
case "bun": return "bun create better-fullstack@latest";
|
|
7287
|
-
case "pnpm": return "pnpm create better-fullstack@latest";
|
|
7288
|
-
case "yarn": return "yarn create better-fullstack@latest";
|
|
7289
|
-
case "npm":
|
|
7290
|
-
default: return "npx create-better-fullstack@latest";
|
|
7291
|
-
}
|
|
7292
|
-
}
|
|
7293
|
-
function formatArrayFlag(flag, values) {
|
|
7294
|
-
const normalizedValues = values.filter((value) => value !== "none");
|
|
7295
|
-
if (normalizedValues.length === 0) return `--${flag} none`;
|
|
7296
|
-
return `--${flag} ${normalizedValues.join(" ")}`;
|
|
7297
|
-
}
|
|
7298
|
-
function appendCommonFlags(flags, config) {
|
|
7299
|
-
if (config.aiDocs && config.aiDocs.length > 0) flags.push(formatArrayFlag("ai-docs", config.aiDocs));
|
|
7300
|
-
else flags.push("--ai-docs none");
|
|
7301
|
-
flags.push(config.git ? "--git" : "--no-git");
|
|
7302
|
-
flags.push(`--package-manager ${config.packageManager}`);
|
|
7303
|
-
if (config.versionChannel !== "stable") flags.push(`--version-channel ${config.versionChannel}`);
|
|
7304
|
-
flags.push(config.install ? "--install" : "--no-install");
|
|
7305
|
-
}
|
|
7306
|
-
function hasGraphPrimaryPart(config, role, ecosystem) {
|
|
7307
|
-
return config.stackParts?.some((part) => part.source !== "provided" && part.role === role && !part.ownerPartId && (!ecosystem || part.ecosystem === ecosystem));
|
|
7308
|
-
}
|
|
7309
|
-
function hasOwnedGraphPart(config, ownerRole, role, ecosystem, toolId) {
|
|
7310
|
-
const owner = config.stackParts?.find((part) => part.source !== "provided" && part.role === ownerRole && !part.ownerPartId && (!ecosystem || part.ecosystem === ecosystem));
|
|
7311
|
-
if (!owner) return false;
|
|
7312
|
-
return Boolean(config.stackParts?.some((part) => part.source !== "provided" && part.role === role && part.ownerPartId === owner.id && (!ecosystem || part.ecosystem === ecosystem) && (!toolId || part.toolId === toolId)));
|
|
7313
|
-
}
|
|
7314
|
-
function hasGraphAddonPart(config, addon) {
|
|
7315
|
-
const binding = (0, types_exports.getAddonStackPartBinding)(addon);
|
|
7316
|
-
if (!binding) return false;
|
|
7317
|
-
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));
|
|
7318
|
-
const owner = config.stackParts?.find((part) => part.source !== "provided" && part.role === binding.ownerRole && !part.ownerPartId && part.ecosystem === binding.ecosystem);
|
|
7319
|
-
if (!owner) return false;
|
|
7320
|
-
return Boolean(config.stackParts?.some((part) => part.source !== "provided" && part.role === binding.role && part.ecosystem === binding.ecosystem && part.toolId === addon && part.ownerPartId === owner.id));
|
|
7321
|
-
}
|
|
7322
|
-
function hasGraphExamplePart(config, example) {
|
|
7323
|
-
return Boolean(config.stackParts?.some((part) => part.source !== "provided" && part.role === "examples" && part.ecosystem === "universal" && part.toolId === example && !part.ownerPartId));
|
|
7324
|
-
}
|
|
7325
|
-
function hasGraphArrayParts(config, values, hasPart) {
|
|
7326
|
-
const normalizedValues = values.filter((value) => value !== "none");
|
|
7327
|
-
return normalizedValues.length > 0 && normalizedValues.every((value) => hasPart(config, value));
|
|
7328
|
-
}
|
|
7329
|
-
function appendChangedStringFlag(flags, flag, value, defaultValue) {
|
|
7330
|
-
if (value !== defaultValue) flags.push(`--${flag} ${value}`);
|
|
7331
|
-
}
|
|
7332
|
-
function appendChangedGraphStringFlag(flags, config, role, ecosystem, flag, value, defaultValue) {
|
|
7333
|
-
if (hasGraphPart(config, role, ecosystem)) return;
|
|
7334
|
-
appendChangedStringFlag(flags, flag, value, defaultValue);
|
|
7335
|
-
}
|
|
7336
|
-
function appendChangedOwnedGraphStringFlag(flags, config, ownerRole, role, ecosystem, flag, value, defaultValue) {
|
|
7337
|
-
if (hasOwnedGraphPart(config, ownerRole, role, ecosystem, value)) return;
|
|
7338
|
-
appendChangedStringFlag(flags, flag, value, defaultValue);
|
|
7339
|
-
}
|
|
7340
|
-
function hasOwnedGraphArrayParts(config, ownerRole, role, ecosystem, values) {
|
|
7341
|
-
const normalizedValues = values.filter((value) => value !== "none");
|
|
7342
|
-
if (normalizedValues.length === 0) return false;
|
|
7343
|
-
return normalizedValues.every((value) => hasOwnedGraphPart(config, ownerRole, role, ecosystem, value));
|
|
7344
|
-
}
|
|
7345
|
-
function appendChangedOwnedGraphArrayFlag(flags, config, ownerRole, role, ecosystem, flag, values, defaultValues) {
|
|
7346
|
-
if (hasOwnedGraphArrayParts(config, ownerRole, role, ecosystem, values)) return;
|
|
7347
|
-
appendChangedArrayFlag(flags, flag, values, defaultValues);
|
|
7348
|
-
}
|
|
7349
|
-
function appendChangedArrayFlag(flags, flag, values, defaultValues) {
|
|
7350
|
-
if (values.length !== defaultValues.length || values.some((value, index) => value !== defaultValues[index])) flags.push(formatArrayFlag(flag, values));
|
|
7351
|
-
}
|
|
7352
|
-
function appendAstroIntegrationFlag(flags, config) {
|
|
7353
|
-
if (config.frontend.includes("astro") && config.astroIntegration !== "none") flags.push(`--astro-integration ${config.astroIntegration}`);
|
|
7354
|
-
}
|
|
7355
|
-
function appendGraphExtraFlags(flags, config) {
|
|
7356
|
-
if (!hasGraphArrayParts(config, config.addons, hasGraphAddonPart)) appendChangedArrayFlag(flags, "addons", config.addons, ["turborepo"]);
|
|
7357
|
-
if (!hasGraphArrayParts(config, config.examples, hasGraphExamplePart)) appendChangedArrayFlag(flags, "examples", config.examples, []);
|
|
7358
|
-
if (hasGraphPrimaryPart(config, "database")) appendChangedOwnedGraphStringFlag(flags, config, "database", "dbSetup", "universal", "db-setup", config.dbSetup, "none");
|
|
7359
|
-
else appendChangedStringFlag(flags, "db-setup", config.dbSetup, "none");
|
|
7360
|
-
if (hasGraphPrimaryPart(config, "frontend", "typescript")) {
|
|
7361
|
-
appendAstroIntegrationFlag(flags, config);
|
|
7362
|
-
appendChangedOwnedGraphStringFlag(flags, config, "frontend", "deploy", "typescript", "web-deploy", config.webDeploy, "none");
|
|
7363
|
-
appendChangedGraphStringFlag(flags, config, "css", "typescript", "css-framework", config.cssFramework, "tailwind");
|
|
7364
|
-
appendChangedGraphStringFlag(flags, config, "ui", "typescript", "ui-library", config.uiLibrary, "shadcn-ui");
|
|
7365
|
-
if (config.uiLibrary === "shadcn-ui") {
|
|
7366
|
-
appendChangedStringFlag(flags, "shadcn-base", config.shadcnBase ?? "radix", "radix");
|
|
7367
|
-
appendChangedStringFlag(flags, "shadcn-style", config.shadcnStyle ?? "nova", "nova");
|
|
7368
|
-
appendChangedStringFlag(flags, "shadcn-icon-library", config.shadcnIconLibrary ?? "lucide", "lucide");
|
|
7369
|
-
appendChangedStringFlag(flags, "shadcn-color-theme", config.shadcnColorTheme ?? "neutral", "neutral");
|
|
7370
|
-
appendChangedStringFlag(flags, "shadcn-base-color", config.shadcnBaseColor ?? "neutral", "neutral");
|
|
7371
|
-
appendChangedStringFlag(flags, "shadcn-font", config.shadcnFont ?? "inter", "inter");
|
|
7372
|
-
appendChangedStringFlag(flags, "shadcn-radius", config.shadcnRadius ?? "default", "default");
|
|
7373
|
-
}
|
|
7374
|
-
appendChangedGraphStringFlag(flags, config, "stateManagement", "typescript", "state-management", config.stateManagement, "none");
|
|
7375
|
-
appendChangedGraphStringFlag(flags, config, "forms", "typescript", "forms", config.forms, "react-hook-form");
|
|
7376
|
-
appendChangedStringFlag(flags, "validation", config.validation, "zod");
|
|
7377
|
-
appendChangedStringFlag(flags, "testing", config.testing, "vitest");
|
|
7378
|
-
appendChangedGraphStringFlag(flags, config, "animation", "typescript", "animation", config.animation, "none");
|
|
7379
|
-
appendChangedGraphStringFlag(flags, config, "fileUpload", "typescript", "file-upload", config.fileUpload, "none");
|
|
7380
|
-
appendChangedGraphStringFlag(flags, config, "i18n", "typescript", "i18n", config.i18n, "none");
|
|
7381
|
-
appendChangedGraphStringFlag(flags, config, "analytics", "typescript", "analytics", config.analytics, "none");
|
|
7382
|
-
}
|
|
7383
|
-
if (hasGraphPrimaryPart(config, "frontend", "typescript") || hasGraphPrimaryPart(config, "backend", "typescript")) {
|
|
7384
|
-
if (hasGraphPrimaryPart(config, "backend", "typescript")) {
|
|
7385
|
-
appendChangedOwnedGraphStringFlag(flags, config, "backend", "runtime", "typescript", "runtime", config.runtime, "bun");
|
|
7386
|
-
appendChangedOwnedGraphStringFlag(flags, config, "backend", "deploy", "typescript", "server-deploy", config.serverDeploy, "none");
|
|
7387
|
-
} else appendChangedStringFlag(flags, "server-deploy", config.serverDeploy, "none");
|
|
7388
|
-
appendChangedGraphStringFlag(flags, config, "payments", "typescript", "payments", config.payments, "none");
|
|
7389
|
-
appendChangedGraphStringFlag(flags, config, "email", "typescript", "email", config.email, "none");
|
|
7390
|
-
appendChangedStringFlag(flags, "effect", config.effect, "none");
|
|
7391
|
-
appendChangedGraphStringFlag(flags, config, "ai", "typescript", "ai", config.ai, "none");
|
|
7392
|
-
appendChangedGraphStringFlag(flags, config, "realtime", "typescript", "realtime", config.realtime, "none");
|
|
7393
|
-
appendChangedGraphStringFlag(flags, config, "jobQueue", "typescript", "job-queue", config.jobQueue, "none");
|
|
7394
|
-
appendChangedGraphStringFlag(flags, config, "logging", "typescript", "logging", config.logging, "none");
|
|
7395
|
-
appendChangedGraphStringFlag(flags, config, "observability", "typescript", "observability", config.observability, "none");
|
|
7396
|
-
appendChangedGraphStringFlag(flags, config, "featureFlags", "typescript", "feature-flags", config.featureFlags, "none");
|
|
7397
|
-
appendChangedGraphStringFlag(flags, config, "caching", "typescript", "caching", config.caching, "none");
|
|
7398
|
-
appendChangedGraphStringFlag(flags, config, "rateLimit", "typescript", "rate-limit", config.rateLimit, "none");
|
|
7399
|
-
appendChangedGraphStringFlag(flags, config, "cms", "typescript", "cms", config.cms, "none");
|
|
7400
|
-
appendChangedGraphStringFlag(flags, config, "search", "typescript", "search", config.search, "none");
|
|
7401
|
-
appendChangedGraphStringFlag(flags, config, "fileStorage", "typescript", "file-storage", config.fileStorage, "none");
|
|
7402
|
-
}
|
|
7403
|
-
if (hasGraphPrimaryPart(config, "mobile")) {
|
|
7404
|
-
appendChangedOwnedGraphStringFlag(flags, config, "mobile", "navigation", "react-native", "mobile-navigation", config.mobileNavigation, "expo-router");
|
|
7405
|
-
appendChangedOwnedGraphStringFlag(flags, config, "mobile", "ui", "react-native", "mobile-ui", config.mobileUI, "none");
|
|
7406
|
-
appendChangedOwnedGraphStringFlag(flags, config, "mobile", "storage", "react-native", "mobile-storage", config.mobileStorage, "none");
|
|
7407
|
-
appendChangedOwnedGraphStringFlag(flags, config, "mobile", "testing", "react-native", "mobile-testing", config.mobileTesting, "none");
|
|
7408
|
-
appendChangedStringFlag(flags, "mobile-push", config.mobilePush, "none");
|
|
7409
|
-
appendChangedStringFlag(flags, "mobile-ota", config.mobileOTA, "none");
|
|
7410
|
-
appendChangedStringFlag(flags, "mobile-deep-linking", config.mobileDeepLinking, "none");
|
|
7411
|
-
}
|
|
7412
|
-
if (hasGraphPrimaryPart(config, "frontend", "rust")) appendChangedStringFlag(flags, "rust-frontend", config.rustFrontend, "none");
|
|
7413
|
-
if (hasGraphPrimaryPart(config, "backend", "rust")) {
|
|
7414
|
-
appendChangedOwnedGraphStringFlag(flags, config, "backend", "cli", "rust", "rust-cli", config.rustCli, "none");
|
|
7415
|
-
appendChangedOwnedGraphArrayFlag(flags, config, "backend", "libraries", "rust", "rust-libraries", config.rustLibraries, []);
|
|
7416
|
-
appendChangedOwnedGraphStringFlag(flags, config, "backend", "logging", "rust", "rust-logging", config.rustLogging, "tracing");
|
|
7417
|
-
appendChangedOwnedGraphStringFlag(flags, config, "backend", "errorHandling", "rust", "rust-error-handling", config.rustErrorHandling, "anyhow-thiserror");
|
|
7418
|
-
appendChangedOwnedGraphStringFlag(flags, config, "backend", "caching", "rust", "rust-caching", config.rustCaching, "none");
|
|
7419
|
-
}
|
|
7420
|
-
if (hasGraphPrimaryPart(config, "backend", "python")) {
|
|
7421
|
-
appendChangedOwnedGraphStringFlag(flags, config, "backend", "validation", "python", "python-validation", config.pythonValidation, "none");
|
|
7422
|
-
appendChangedOwnedGraphArrayFlag(flags, config, "backend", "ai", "python", "python-ai", config.pythonAi, []);
|
|
7423
|
-
appendChangedOwnedGraphStringFlag(flags, config, "backend", "jobQueue", "python", "python-task-queue", config.pythonTaskQueue, "none");
|
|
7424
|
-
appendChangedOwnedGraphStringFlag(flags, config, "backend", "api", "python", "python-graphql", config.pythonGraphql, "none");
|
|
7425
|
-
appendChangedOwnedGraphStringFlag(flags, config, "backend", "codeQuality", "python", "python-quality", config.pythonQuality, "none");
|
|
7426
|
-
}
|
|
7427
|
-
if (hasGraphPrimaryPart(config, "backend", "go")) {
|
|
7428
|
-
appendChangedOwnedGraphStringFlag(flags, config, "backend", "cli", "go", "go-cli", config.goCli, "none");
|
|
7429
|
-
appendChangedOwnedGraphStringFlag(flags, config, "backend", "logging", "go", "go-logging", config.goLogging, "none");
|
|
7430
|
-
}
|
|
7431
|
-
if (hasGraphPrimaryPart(config, "backend", "java")) {
|
|
7432
|
-
appendChangedOwnedGraphStringFlag(flags, config, "backend", "buildTool", "java", "java-build-tool", config.javaBuildTool, "maven");
|
|
7433
|
-
appendChangedOwnedGraphArrayFlag(flags, config, "backend", "libraries", "java", "java-libraries", config.javaLibraries, []);
|
|
7434
|
-
appendChangedOwnedGraphArrayFlag(flags, config, "backend", "testing", "java", "java-testing-libraries", config.javaTestingLibraries, ["junit5"]);
|
|
7435
|
-
}
|
|
7436
|
-
if (hasGraphPrimaryPart(config, "backend", "elixir")) {
|
|
7437
|
-
appendChangedGraphStringFlag(flags, config, "realtime", "elixir", "elixir-realtime", config.elixirRealtime, "channels");
|
|
7438
|
-
appendChangedGraphStringFlag(flags, config, "jobQueue", "elixir", "elixir-jobs", config.elixirJobs, "none");
|
|
7439
|
-
appendChangedGraphStringFlag(flags, config, "validation", "elixir", "elixir-validation", config.elixirValidation, "ecto-changesets");
|
|
7440
|
-
appendChangedOwnedGraphStringFlag(flags, config, "backend", "httpClient", "elixir", "elixir-http", config.elixirHttp, "req");
|
|
7441
|
-
appendChangedStringFlag(flags, "elixir-json", config.elixirJson, "jason");
|
|
7442
|
-
appendChangedGraphStringFlag(flags, config, "email", "elixir", "elixir-email", config.elixirEmail, "none");
|
|
7443
|
-
appendChangedGraphStringFlag(flags, config, "caching", "elixir", "elixir-caching", config.elixirCaching, "none");
|
|
7444
|
-
appendChangedGraphStringFlag(flags, config, "observability", "elixir", "elixir-observability", config.elixirObservability, "telemetry");
|
|
7445
|
-
appendChangedGraphStringFlag(flags, config, "testing", "elixir", "elixir-testing", config.elixirTesting, "ex_unit");
|
|
7446
|
-
appendChangedOwnedGraphStringFlag(flags, config, "backend", "codeQuality", "elixir", "elixir-quality", config.elixirQuality, "credo");
|
|
7447
|
-
appendChangedGraphStringFlag(flags, config, "deploy", "elixir", "elixir-deploy", config.elixirDeploy, "none");
|
|
7448
|
-
}
|
|
7449
|
-
}
|
|
7450
|
-
function appendSharedNonTypeScriptFlags(flags, config) {
|
|
7451
|
-
flags.push(`--email ${config.email}`);
|
|
7452
|
-
flags.push(`--observability ${config.observability}`);
|
|
7453
|
-
flags.push(`--caching ${config.caching}`);
|
|
7454
|
-
flags.push(`--search ${config.search}`);
|
|
7455
|
-
flags.push(formatArrayFlag("addons", config.addons));
|
|
7456
|
-
flags.push(formatArrayFlag("examples", config.examples));
|
|
7457
|
-
flags.push(`--db-setup ${config.dbSetup}`);
|
|
7458
|
-
flags.push(`--web-deploy ${config.webDeploy}`);
|
|
7459
|
-
flags.push(`--server-deploy ${config.serverDeploy}`);
|
|
7460
|
-
}
|
|
7461
|
-
function getTypeScriptFlags(config) {
|
|
7462
|
-
const flags = [];
|
|
7463
|
-
if (config.frontend && config.frontend.length > 0) flags.push(`--frontend ${config.frontend.join(" ")}`);
|
|
7464
|
-
else flags.push("--frontend none");
|
|
7465
|
-
appendAstroIntegrationFlag(flags, config);
|
|
7466
|
-
flags.push(`--backend ${config.backend}`);
|
|
7467
|
-
flags.push(`--runtime ${config.runtime}`);
|
|
7468
|
-
flags.push(`--database ${config.database}`);
|
|
7469
|
-
flags.push(`--orm ${config.orm}`);
|
|
7470
|
-
flags.push(`--api ${config.api}`);
|
|
7471
|
-
flags.push(`--auth ${config.auth}`);
|
|
7472
|
-
flags.push(`--payments ${config.payments}`);
|
|
7473
|
-
flags.push(`--email ${config.email}`);
|
|
7474
|
-
flags.push(`--file-upload ${config.fileUpload}`);
|
|
7475
|
-
flags.push(`--effect ${config.effect}`);
|
|
7476
|
-
flags.push(`--css-framework ${config.cssFramework}`);
|
|
7477
|
-
flags.push(`--ui-library ${config.uiLibrary}`);
|
|
7478
|
-
if (config.uiLibrary === "shadcn-ui") {
|
|
7479
|
-
flags.push(`--shadcn-base ${config.shadcnBase}`);
|
|
7480
|
-
flags.push(`--shadcn-style ${config.shadcnStyle}`);
|
|
7481
|
-
flags.push(`--shadcn-icon-library ${config.shadcnIconLibrary}`);
|
|
7482
|
-
flags.push(`--shadcn-color-theme ${config.shadcnColorTheme}`);
|
|
7483
|
-
flags.push(`--shadcn-base-color ${config.shadcnBaseColor}`);
|
|
7484
|
-
flags.push(`--shadcn-font ${config.shadcnFont}`);
|
|
7485
|
-
flags.push(`--shadcn-radius ${config.shadcnRadius}`);
|
|
7486
|
-
}
|
|
7487
|
-
flags.push(`--ai ${config.ai}`);
|
|
7488
|
-
flags.push(`--state-management ${config.stateManagement}`);
|
|
7489
|
-
flags.push(`--forms ${config.forms}`);
|
|
7490
|
-
flags.push(`--validation ${config.validation}`);
|
|
7491
|
-
flags.push(`--testing ${config.testing}`);
|
|
7492
|
-
flags.push(`--animation ${config.animation}`);
|
|
7493
|
-
flags.push(`--realtime ${config.realtime}`);
|
|
7494
|
-
flags.push(`--job-queue ${config.jobQueue}`);
|
|
7495
|
-
flags.push(`--logging ${config.logging}`);
|
|
7496
|
-
flags.push(`--observability ${config.observability}`);
|
|
7497
|
-
flags.push(`--feature-flags ${config.featureFlags}`);
|
|
7498
|
-
flags.push(`--caching ${config.caching}`);
|
|
7499
|
-
flags.push(`--rate-limit ${config.rateLimit}`);
|
|
7500
|
-
flags.push(`--i18n ${config.i18n}`);
|
|
7501
|
-
flags.push(`--cms ${config.cms}`);
|
|
7502
|
-
flags.push(`--search ${config.search}`);
|
|
7503
|
-
flags.push(`--file-storage ${config.fileStorage}`);
|
|
7504
|
-
flags.push(`--mobile-navigation ${config.mobileNavigation}`);
|
|
7505
|
-
flags.push(`--mobile-ui ${config.mobileUI}`);
|
|
7506
|
-
flags.push(`--mobile-storage ${config.mobileStorage}`);
|
|
7507
|
-
flags.push(`--mobile-testing ${config.mobileTesting}`);
|
|
7508
|
-
flags.push(`--mobile-push ${config.mobilePush}`);
|
|
7509
|
-
flags.push(`--mobile-ota ${config.mobileOTA}`);
|
|
7510
|
-
flags.push(`--mobile-deep-linking ${config.mobileDeepLinking}`);
|
|
7511
|
-
if (config.addons && config.addons.length > 0) flags.push(`--addons ${config.addons.join(" ")}`);
|
|
7512
|
-
else flags.push("--addons none");
|
|
7513
|
-
if (config.examples && config.examples.length > 0) flags.push(`--examples ${config.examples.join(" ")}`);
|
|
7514
|
-
else flags.push("--examples none");
|
|
7515
|
-
flags.push(`--db-setup ${config.dbSetup}`);
|
|
7516
|
-
flags.push(`--web-deploy ${config.webDeploy}`);
|
|
7517
|
-
flags.push(`--server-deploy ${config.serverDeploy}`);
|
|
7518
|
-
appendCommonFlags(flags, config);
|
|
7519
|
-
return flags;
|
|
7520
|
-
}
|
|
7521
|
-
function getReactNativeFlags(config) {
|
|
7522
|
-
const flags = ["--ecosystem react-native"];
|
|
7523
|
-
if (config.frontend && config.frontend.length > 0) flags.push(`--frontend ${config.frontend.join(" ")}`);
|
|
7524
|
-
else flags.push("--frontend native-bare");
|
|
7525
|
-
flags.push(`--auth ${config.auth}`);
|
|
7526
|
-
flags.push(`--mobile-navigation ${config.mobileNavigation}`);
|
|
7527
|
-
flags.push(`--mobile-ui ${config.mobileUI}`);
|
|
7528
|
-
flags.push(`--mobile-storage ${config.mobileStorage}`);
|
|
7529
|
-
flags.push(`--mobile-testing ${config.mobileTesting}`);
|
|
7530
|
-
flags.push(`--mobile-push ${config.mobilePush}`);
|
|
7531
|
-
flags.push(`--mobile-ota ${config.mobileOTA}`);
|
|
7532
|
-
flags.push(`--mobile-deep-linking ${config.mobileDeepLinking}`);
|
|
7533
|
-
appendCommonFlags(flags, config);
|
|
7534
|
-
return flags;
|
|
7535
|
-
}
|
|
7536
|
-
function getRustFlags(config) {
|
|
7537
|
-
const flags = ["--ecosystem rust"];
|
|
7538
|
-
flags.push(`--rust-web-framework ${config.rustWebFramework}`);
|
|
7539
|
-
flags.push(`--rust-frontend ${config.rustFrontend}`);
|
|
7540
|
-
flags.push(`--rust-orm ${config.rustOrm}`);
|
|
7541
|
-
flags.push(`--rust-api ${config.rustApi}`);
|
|
7542
|
-
flags.push(`--rust-cli ${config.rustCli}`);
|
|
7543
|
-
flags.push(formatArrayFlag("rust-libraries", config.rustLibraries));
|
|
7544
|
-
flags.push(`--rust-logging ${config.rustLogging}`);
|
|
7545
|
-
flags.push(`--rust-error-handling ${config.rustErrorHandling}`);
|
|
7546
|
-
flags.push(`--rust-caching ${config.rustCaching}`);
|
|
7547
|
-
flags.push(`--rust-auth ${config.rustAuth}`);
|
|
7548
|
-
flags.push(`--rust-realtime ${config.rustRealtime}`);
|
|
7549
|
-
flags.push(`--rust-message-queue ${config.rustMessageQueue}`);
|
|
7550
|
-
flags.push(`--rust-observability ${config.rustObservability}`);
|
|
7551
|
-
flags.push(`--rust-templating ${config.rustTemplating}`);
|
|
7552
|
-
appendSharedNonTypeScriptFlags(flags, config);
|
|
7553
|
-
appendCommonFlags(flags, config);
|
|
7554
|
-
return flags;
|
|
7555
|
-
}
|
|
7556
|
-
function getPythonFlags(config) {
|
|
7557
|
-
const flags = ["--ecosystem python"];
|
|
7558
|
-
flags.push(`--python-web-framework ${config.pythonWebFramework}`);
|
|
7559
|
-
flags.push(`--python-orm ${config.pythonOrm}`);
|
|
7560
|
-
flags.push(`--python-validation ${config.pythonValidation}`);
|
|
7561
|
-
flags.push(formatArrayFlag("python-ai", config.pythonAi));
|
|
7562
|
-
flags.push(`--python-auth ${config.pythonAuth}`);
|
|
7563
|
-
flags.push(`--python-api ${config.pythonApi}`);
|
|
7564
|
-
flags.push(`--python-task-queue ${config.pythonTaskQueue}`);
|
|
7565
|
-
flags.push(`--python-graphql ${config.pythonGraphql}`);
|
|
7566
|
-
flags.push(`--python-quality ${config.pythonQuality}`);
|
|
7567
|
-
flags.push(formatArrayFlag("python-testing", config.pythonTesting));
|
|
7568
|
-
flags.push(`--python-caching ${config.pythonCaching}`);
|
|
7569
|
-
flags.push(`--python-realtime ${config.pythonRealtime}`);
|
|
7570
|
-
flags.push(`--python-observability ${config.pythonObservability}`);
|
|
7571
|
-
flags.push(formatArrayFlag("python-cli", config.pythonCli));
|
|
7572
|
-
appendSharedNonTypeScriptFlags(flags, config);
|
|
7573
|
-
appendCommonFlags(flags, config);
|
|
7574
|
-
return flags;
|
|
7575
|
-
}
|
|
7576
|
-
function getGoFlags(config) {
|
|
7577
|
-
const flags = ["--ecosystem go"];
|
|
7578
|
-
flags.push(`--go-web-framework ${config.goWebFramework}`);
|
|
7579
|
-
flags.push(`--go-orm ${config.goOrm}`);
|
|
7580
|
-
flags.push(`--go-api ${config.goApi}`);
|
|
7581
|
-
flags.push(`--go-cli ${config.goCli}`);
|
|
7582
|
-
flags.push(`--go-logging ${config.goLogging}`);
|
|
7583
|
-
flags.push(`--go-auth ${config.goAuth}`);
|
|
7584
|
-
flags.push(formatArrayFlag("go-testing", config.goTesting));
|
|
7585
|
-
flags.push(`--go-realtime ${config.goRealtime}`);
|
|
7586
|
-
flags.push(`--go-message-queue ${config.goMessageQueue}`);
|
|
7587
|
-
flags.push(`--go-caching ${config.goCaching}`);
|
|
7588
|
-
flags.push(`--go-config ${config.goConfig}`);
|
|
7589
|
-
flags.push(`--go-observability ${config.goObservability}`);
|
|
7590
|
-
flags.push(`--auth ${config.auth}`);
|
|
7591
|
-
appendSharedNonTypeScriptFlags(flags, config);
|
|
7592
|
-
appendCommonFlags(flags, config);
|
|
7593
|
-
return flags;
|
|
7594
|
-
}
|
|
7595
|
-
function getJavaFlags(config) {
|
|
7596
|
-
const flags = ["--ecosystem java"];
|
|
7597
|
-
flags.push(`--java-web-framework ${config.javaWebFramework}`);
|
|
7598
|
-
flags.push(`--java-build-tool ${config.javaBuildTool}`);
|
|
7599
|
-
flags.push(`--java-orm ${config.javaOrm}`);
|
|
7600
|
-
flags.push(`--java-auth ${config.javaAuth}`);
|
|
7601
|
-
flags.push(`--java-api ${config.javaApi}`);
|
|
7602
|
-
flags.push(`--java-logging ${config.javaLogging}`);
|
|
7603
|
-
flags.push(formatArrayFlag("java-libraries", config.javaLibraries));
|
|
7604
|
-
flags.push(formatArrayFlag("java-testing-libraries", config.javaTestingLibraries));
|
|
7605
|
-
appendSharedNonTypeScriptFlags(flags, config);
|
|
7606
|
-
appendCommonFlags(flags, config);
|
|
7607
|
-
return flags;
|
|
7608
|
-
}
|
|
7609
|
-
function getDotnetFlags(config) {
|
|
7610
|
-
const flags = ["--ecosystem dotnet"];
|
|
7611
|
-
flags.push(`--dotnet-web-framework ${config.dotnetWebFramework}`);
|
|
7612
|
-
flags.push(`--dotnet-orm ${config.dotnetOrm}`);
|
|
7613
|
-
flags.push(`--dotnet-auth ${config.dotnetAuth}`);
|
|
7614
|
-
flags.push(`--dotnet-api ${config.dotnetApi}`);
|
|
7615
|
-
flags.push(formatArrayFlag("dotnet-testing", config.dotnetTesting));
|
|
7616
|
-
flags.push(`--dotnet-job-queue ${config.dotnetJobQueue}`);
|
|
7617
|
-
flags.push(`--dotnet-realtime ${config.dotnetRealtime}`);
|
|
7618
|
-
flags.push(formatArrayFlag("dotnet-observability", config.dotnetObservability));
|
|
7619
|
-
flags.push(`--dotnet-validation ${config.dotnetValidation}`);
|
|
7620
|
-
flags.push(`--dotnet-caching ${config.dotnetCaching}`);
|
|
7621
|
-
flags.push(`--dotnet-deploy ${config.dotnetDeploy}`);
|
|
7622
|
-
appendCommonFlags(flags, config);
|
|
7623
|
-
return flags;
|
|
7624
|
-
}
|
|
7625
|
-
function getElixirFlags(config) {
|
|
7626
|
-
const flags = ["--ecosystem elixir"];
|
|
7627
|
-
flags.push(`--elixir-web-framework ${config.elixirWebFramework}`);
|
|
7628
|
-
flags.push(`--elixir-orm ${config.elixirOrm}`);
|
|
7629
|
-
flags.push(`--elixir-auth ${config.elixirAuth}`);
|
|
7630
|
-
flags.push(`--elixir-api ${config.elixirApi}`);
|
|
7631
|
-
flags.push(`--elixir-realtime ${config.elixirRealtime}`);
|
|
7632
|
-
flags.push(`--elixir-jobs ${config.elixirJobs}`);
|
|
7633
|
-
flags.push(`--elixir-validation ${config.elixirValidation}`);
|
|
7634
|
-
flags.push(`--elixir-http ${config.elixirHttp}`);
|
|
7635
|
-
flags.push(`--elixir-json ${config.elixirJson}`);
|
|
7636
|
-
flags.push(`--elixir-email ${config.elixirEmail}`);
|
|
7637
|
-
flags.push(`--elixir-caching ${config.elixirCaching}`);
|
|
7638
|
-
flags.push(`--elixir-observability ${config.elixirObservability}`);
|
|
7639
|
-
flags.push(`--elixir-testing ${config.elixirTesting}`);
|
|
7640
|
-
flags.push(`--elixir-quality ${config.elixirQuality}`);
|
|
7641
|
-
flags.push(`--elixir-deploy ${config.elixirDeploy}`);
|
|
7642
|
-
flags.push(formatArrayFlag("elixir-libraries", config.elixirLibraries));
|
|
7643
|
-
appendCommonFlags(flags, config);
|
|
7644
|
-
return flags;
|
|
7645
|
-
}
|
|
7646
|
-
function generateReproducibleCommand(config) {
|
|
7647
|
-
let flags;
|
|
7648
|
-
if (config.stackParts && config.stackParts.length > 0) {
|
|
7649
|
-
flags = config.stackParts.filter((part) => part.source !== "provided").map((part) => `--part ${(0, types_exports.formatStackPartSpec)(part, config.stackParts ?? [])}`);
|
|
7650
|
-
appendGraphExtraFlags(flags, config);
|
|
7651
|
-
appendCommonFlags(flags, config);
|
|
7652
|
-
return `${getBaseCommand(config.packageManager)}${config.relativePath ? ` ${config.relativePath}` : ""} ${flags.join(" ")}`;
|
|
7653
|
-
}
|
|
7654
|
-
switch (config.ecosystem) {
|
|
7655
|
-
case "react-native":
|
|
7656
|
-
flags = getReactNativeFlags(config);
|
|
7657
|
-
break;
|
|
7658
|
-
case "rust":
|
|
7659
|
-
flags = getRustFlags(config);
|
|
7660
|
-
break;
|
|
7661
|
-
case "python":
|
|
7662
|
-
flags = getPythonFlags(config);
|
|
7663
|
-
break;
|
|
7664
|
-
case "go":
|
|
7665
|
-
flags = getGoFlags(config);
|
|
7666
|
-
break;
|
|
7667
|
-
case "java":
|
|
7668
|
-
flags = getJavaFlags(config);
|
|
7669
|
-
break;
|
|
7670
|
-
case "dotnet":
|
|
7671
|
-
flags = getDotnetFlags(config);
|
|
7672
|
-
break;
|
|
7673
|
-
case "elixir":
|
|
7674
|
-
flags = getElixirFlags(config);
|
|
7675
|
-
break;
|
|
7676
|
-
case "typescript":
|
|
7677
|
-
default:
|
|
7678
|
-
flags = getTypeScriptFlags(config);
|
|
7679
|
-
break;
|
|
7680
|
-
}
|
|
7681
|
-
return `${getBaseCommand(config.packageManager)}${config.relativePath ? ` ${config.relativePath}` : ""} ${flags.join(" ")}`;
|
|
7682
|
-
}
|
|
7683
|
-
|
|
7684
|
-
//#endregion
|
|
7685
|
-
//#region src/utils/generated-checks.ts
|
|
7686
|
-
function getGraphTarget(config) {
|
|
7687
|
-
const backend = getPrimaryGraphPart(config, "backend");
|
|
7688
|
-
if (!backend || backend.ecosystem === "typescript" || backend.ecosystem === "react-native" || backend.ecosystem === "universal") return null;
|
|
7689
|
-
return {
|
|
7690
|
-
ecosystem: backend.ecosystem,
|
|
7691
|
-
projectDir: path.join(config.projectDir, backend.targetPath ?? "apps/server")
|
|
7692
|
-
};
|
|
7693
|
-
}
|
|
7694
|
-
function getSingleEcosystemTarget(config) {
|
|
7695
|
-
if (config.ecosystem === "typescript" || config.ecosystem === "react-native" || config.ecosystem === "java") return null;
|
|
7696
|
-
return {
|
|
7697
|
-
ecosystem: config.ecosystem,
|
|
7698
|
-
projectDir: config.projectDir
|
|
7699
|
-
};
|
|
7700
|
-
}
|
|
7701
|
-
async function runCommand(cwd, command, args) {
|
|
7702
|
-
await $({
|
|
7703
|
-
cwd,
|
|
7704
|
-
stdout: "inherit",
|
|
7705
|
-
stderr: "inherit"
|
|
7706
|
-
})`${command} ${args}`;
|
|
7707
|
-
}
|
|
7708
|
-
async function verifyTarget(target) {
|
|
7709
|
-
const s = spinner();
|
|
7710
|
-
const cwd = target.projectDir;
|
|
7711
|
-
switch (target.ecosystem) {
|
|
7712
|
-
case "go":
|
|
7713
|
-
s.start("Verifying generated Go server...");
|
|
7714
|
-
await runCommand(cwd, "go", ["mod", "tidy"]);
|
|
7715
|
-
await runCommand(cwd, "go", ["test", "./..."]);
|
|
7716
|
-
s.stop("Generated Go server checks passed");
|
|
7717
|
-
return;
|
|
7718
|
-
case "rust":
|
|
7719
|
-
s.start("Verifying generated Rust server...");
|
|
7720
|
-
await runCommand(cwd, "cargo", ["check"]);
|
|
7721
|
-
s.stop("Generated Rust server checks passed");
|
|
7722
|
-
return;
|
|
7723
|
-
case "python":
|
|
7724
|
-
s.start("Verifying generated Python server...");
|
|
7725
|
-
await runCommand(cwd, "uv", ["sync"]);
|
|
7726
|
-
await runCommand(cwd, "uv", [
|
|
7727
|
-
"run",
|
|
7728
|
-
"ruff",
|
|
7729
|
-
"check",
|
|
7730
|
-
"."
|
|
7731
|
-
]);
|
|
7732
|
-
s.stop("Generated Python server checks passed");
|
|
7733
|
-
return;
|
|
7734
|
-
case "elixir":
|
|
7735
|
-
if (!await commandExists("mix")) {
|
|
7736
|
-
log.warn(pc.yellow("Skipping Elixir verification because mix is not on PATH"));
|
|
7737
|
-
return;
|
|
7738
|
-
}
|
|
7739
|
-
s.start("Verifying generated Elixir server...");
|
|
7740
|
-
await runCommand(cwd, "mix", ["deps.get"]);
|
|
7741
|
-
await runCommand(cwd, "mix", ["compile"]);
|
|
7742
|
-
s.stop("Generated Elixir server checks passed");
|
|
7743
|
-
return;
|
|
7744
|
-
default: log.warn(pc.yellow(`No generated checks are configured for ${target.ecosystem}`));
|
|
7745
|
-
}
|
|
7746
|
-
}
|
|
7747
|
-
async function runGeneratedChecks(config) {
|
|
7748
|
-
const target = getGraphTarget(config) ?? getSingleEcosystemTarget(config);
|
|
7749
|
-
if (!target) {
|
|
7750
|
-
log.warn(pc.yellow("No generated checks are configured for this stack"));
|
|
7751
|
-
return;
|
|
7752
|
-
}
|
|
7753
|
-
await verifyTarget(target);
|
|
7754
|
-
}
|
|
7755
|
-
|
|
7756
7635
|
//#endregion
|
|
7757
7636
|
//#region src/utils/preflight-display.ts
|
|
7758
7637
|
function displayPreflightWarnings({ warnings }) {
|
|
@@ -7856,87 +7735,6 @@ async function setupProjectDirectory(finalPathInput, shouldClearDirectory) {
|
|
|
7856
7735
|
};
|
|
7857
7736
|
}
|
|
7858
7737
|
|
|
7859
|
-
//#endregion
|
|
7860
|
-
//#region src/utils/templates.ts
|
|
7861
|
-
const TEMPLATE_PRESETS = {
|
|
7862
|
-
mern: {
|
|
7863
|
-
database: "mongodb",
|
|
7864
|
-
orm: "mongoose",
|
|
7865
|
-
backend: "express",
|
|
7866
|
-
runtime: "node",
|
|
7867
|
-
frontend: ["react-router"],
|
|
7868
|
-
api: "orpc",
|
|
7869
|
-
auth: "better-auth",
|
|
7870
|
-
payments: "none",
|
|
7871
|
-
addons: ["turborepo"],
|
|
7872
|
-
examples: ["none"],
|
|
7873
|
-
dbSetup: "mongodb-atlas",
|
|
7874
|
-
webDeploy: "none",
|
|
7875
|
-
serverDeploy: "none"
|
|
7876
|
-
},
|
|
7877
|
-
pern: {
|
|
7878
|
-
database: "postgres",
|
|
7879
|
-
orm: "drizzle",
|
|
7880
|
-
backend: "express",
|
|
7881
|
-
runtime: "node",
|
|
7882
|
-
frontend: ["tanstack-router"],
|
|
7883
|
-
api: "trpc",
|
|
7884
|
-
auth: "better-auth",
|
|
7885
|
-
payments: "none",
|
|
7886
|
-
addons: ["turborepo"],
|
|
7887
|
-
examples: ["none"],
|
|
7888
|
-
dbSetup: "none",
|
|
7889
|
-
webDeploy: "none",
|
|
7890
|
-
serverDeploy: "none"
|
|
7891
|
-
},
|
|
7892
|
-
t3: {
|
|
7893
|
-
database: "postgres",
|
|
7894
|
-
orm: "prisma",
|
|
7895
|
-
backend: "self",
|
|
7896
|
-
runtime: "none",
|
|
7897
|
-
frontend: ["next"],
|
|
7898
|
-
api: "trpc",
|
|
7899
|
-
auth: "better-auth",
|
|
7900
|
-
payments: "none",
|
|
7901
|
-
addons: ["biome", "turborepo"],
|
|
7902
|
-
examples: ["none"],
|
|
7903
|
-
dbSetup: "none",
|
|
7904
|
-
webDeploy: "none",
|
|
7905
|
-
serverDeploy: "none"
|
|
7906
|
-
},
|
|
7907
|
-
uniwind: {
|
|
7908
|
-
database: "none",
|
|
7909
|
-
orm: "none",
|
|
7910
|
-
backend: "none",
|
|
7911
|
-
runtime: "none",
|
|
7912
|
-
frontend: ["native-uniwind"],
|
|
7913
|
-
api: "none",
|
|
7914
|
-
auth: "none",
|
|
7915
|
-
payments: "none",
|
|
7916
|
-
addons: ["none"],
|
|
7917
|
-
examples: ["none"],
|
|
7918
|
-
dbSetup: "none",
|
|
7919
|
-
webDeploy: "none",
|
|
7920
|
-
serverDeploy: "none"
|
|
7921
|
-
},
|
|
7922
|
-
none: null
|
|
7923
|
-
};
|
|
7924
|
-
function getTemplateConfig(template) {
|
|
7925
|
-
if (template === "none" || !template) return null;
|
|
7926
|
-
const config = TEMPLATE_PRESETS[template];
|
|
7927
|
-
if (!config) throw new Error(`Unknown template: ${template}`);
|
|
7928
|
-
return config;
|
|
7929
|
-
}
|
|
7930
|
-
function getTemplateDescription(template) {
|
|
7931
|
-
return {
|
|
7932
|
-
mern: "MongoDB + Express + React + Node.js - Classic MERN stack",
|
|
7933
|
-
pern: "PostgreSQL + Express + React + Node.js - Popular PERN stack",
|
|
7934
|
-
t3: "T3 Stack - Next.js + tRPC + Prisma + PostgreSQL + Better Auth",
|
|
7935
|
-
uniwind: "Expo + Uniwind native app with no backend services",
|
|
7936
|
-
none: "No template - Full customization"
|
|
7937
|
-
}[template] || "";
|
|
7938
|
-
}
|
|
7939
|
-
|
|
7940
7738
|
//#endregion
|
|
7941
7739
|
//#region src/utils/config-processing.ts
|
|
7942
7740
|
function deriveProjectName(projectName, projectDirectory) {
|
|
@@ -8439,7 +8237,45 @@ function validateFrontendConstraints(config, providedFlags) {
|
|
|
8439
8237
|
validateWebDeployRequiresWebFrontend(config.webDeploy, hasWebFrontendFlag);
|
|
8440
8238
|
validateWebDeployFrontendTemplates(config.webDeploy, frontend);
|
|
8441
8239
|
}
|
|
8442
|
-
function validateApiConstraints(
|
|
8240
|
+
function validateApiConstraints(config, _options) {
|
|
8241
|
+
if (config.api !== "openapi" && config.api !== "apollo-server") return;
|
|
8242
|
+
const apiDisplayName = config.api === "apollo-server" ? "Apollo Server" : "OpenAPI";
|
|
8243
|
+
const frontend = config.frontend ?? [];
|
|
8244
|
+
if (frontend.some((item) => [
|
|
8245
|
+
"native-bare",
|
|
8246
|
+
"native-uniwind",
|
|
8247
|
+
"native-unistyles"
|
|
8248
|
+
].includes(item))) incompatibilityError({
|
|
8249
|
+
message: `${apiDisplayName} is currently available for web frontends, not React Native.`,
|
|
8250
|
+
provided: {
|
|
8251
|
+
api: config.api,
|
|
8252
|
+
frontend
|
|
8253
|
+
},
|
|
8254
|
+
suggestions: [
|
|
8255
|
+
"Use --api trpc",
|
|
8256
|
+
"Use --api orpc",
|
|
8257
|
+
"Use a web frontend"
|
|
8258
|
+
]
|
|
8259
|
+
});
|
|
8260
|
+
if (!config.backend || ![
|
|
8261
|
+
"hono",
|
|
8262
|
+
"express",
|
|
8263
|
+
"fastify",
|
|
8264
|
+
"elysia"
|
|
8265
|
+
].includes(config.backend)) incompatibilityError({
|
|
8266
|
+
message: `${apiDisplayName} currently supports Hono, Express, Fastify, and Elysia backends.`,
|
|
8267
|
+
provided: {
|
|
8268
|
+
api: config.api,
|
|
8269
|
+
backend: config.backend ?? "none"
|
|
8270
|
+
},
|
|
8271
|
+
suggestions: [
|
|
8272
|
+
"Use --backend hono",
|
|
8273
|
+
"Use --backend express",
|
|
8274
|
+
"Use --backend fastify",
|
|
8275
|
+
"Use --backend elysia"
|
|
8276
|
+
]
|
|
8277
|
+
});
|
|
8278
|
+
}
|
|
8443
8279
|
function validateJavaConstraints(config, providedFlags = /* @__PURE__ */ new Set()) {
|
|
8444
8280
|
if (config.ecosystem !== "java") return;
|
|
8445
8281
|
const hasSpringBoot = config.javaWebFramework === "spring-boot";
|
|
@@ -8698,10 +8534,11 @@ function validateRateLimitConstraints(config) {
|
|
|
8698
8534
|
}
|
|
8699
8535
|
function validateSearchConstraints(config) {
|
|
8700
8536
|
if (!config.search || config.search === "none") return;
|
|
8701
|
-
|
|
8537
|
+
const ecosystem = config.ecosystem ?? "typescript";
|
|
8538
|
+
if (ecosystem !== "typescript" && config.search !== "meilisearch") incompatibilityError({
|
|
8702
8539
|
message: "Only Meilisearch search is available for non-TypeScript ecosystems.",
|
|
8703
8540
|
provided: {
|
|
8704
|
-
ecosystem
|
|
8541
|
+
ecosystem,
|
|
8705
8542
|
search: config.search
|
|
8706
8543
|
},
|
|
8707
8544
|
suggestions: ["Use --search meilisearch", "Use --search none"]
|
|
@@ -10042,10 +9879,10 @@ async function setupDatabase(config, cliInput) {
|
|
|
10042
9879
|
const serverDbDir = path.join(projectDir, "apps/server/src/db");
|
|
10043
9880
|
if (await fs.pathExists(serverDbDir)) await fs.remove(serverDbDir);
|
|
10044
9881
|
}
|
|
10045
|
-
return;
|
|
9882
|
+
return null;
|
|
10046
9883
|
}
|
|
10047
9884
|
const dbPackageDir = path.join(projectDir, "packages/db");
|
|
10048
|
-
if (!await fs.pathExists(dbPackageDir)) return;
|
|
9885
|
+
if (!await fs.pathExists(dbPackageDir)) return null;
|
|
10049
9886
|
try {
|
|
10050
9887
|
if (dbSetup === "docker") await setupDockerCompose(config);
|
|
10051
9888
|
else if (database === "sqlite" && dbSetup === "turso") await setupTurso(config, cliInput);
|
|
@@ -10058,8 +9895,18 @@ async function setupDatabase(config, cliInput) {
|
|
|
10058
9895
|
} else if (database === "mysql" && dbSetup === "planetscale") await setupPlanetScale(config);
|
|
10059
9896
|
else if (database === "mongodb" && dbSetup === "mongodb-atlas") await setupMongoDBAtlas(config, cliInput);
|
|
10060
9897
|
else if (database === "redis" && dbSetup === "upstash") await setupUpstash(config, cliInput);
|
|
9898
|
+
return {
|
|
9899
|
+
step: "Database setup",
|
|
9900
|
+
success: true
|
|
9901
|
+
};
|
|
10061
9902
|
} catch (error) {
|
|
10062
|
-
|
|
9903
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
9904
|
+
consola.error(pc.red(errorMessage));
|
|
9905
|
+
return {
|
|
9906
|
+
step: "Database setup",
|
|
9907
|
+
success: false,
|
|
9908
|
+
errorMessage
|
|
9909
|
+
};
|
|
10063
9910
|
}
|
|
10064
9911
|
}
|
|
10065
9912
|
|
|
@@ -10659,14 +10506,14 @@ function displayJavaInstructions(config) {
|
|
|
10659
10506
|
}
|
|
10660
10507
|
const effectiveJavaTestingLibraries = javaBuildTool === "none" ? [] : javaTestingLibraries.filter((library) => library !== "none");
|
|
10661
10508
|
const buildToolCommand = javaBuildTool === "none" ? null : javaBuildTool === "gradle" ? process.platform === "win32" ? "gradlew.bat" : "./gradlew" : process.platform === "win32" ? "mvnw.cmd" : "./mvnw";
|
|
10662
|
-
const runCommand
|
|
10509
|
+
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;
|
|
10663
10510
|
const packageCommand = buildToolCommand ? javaBuildTool === "gradle" ? `${buildToolCommand} build` : `${buildToolCommand} package` : null;
|
|
10664
10511
|
const sourceCompileCommand = buildToolCommand ? null : `javac -d out ${getJavaMainSourcePath(projectName)}`;
|
|
10665
10512
|
const sourceRunCommand = buildToolCommand ? null : `java -cp out ${getJavaMainClass(projectName)}`;
|
|
10666
10513
|
let output = `${pc.bold("Next steps")}\n${pc.cyan("1.")} ${cdCmd}\n`;
|
|
10667
10514
|
let stepCounter = 2;
|
|
10668
10515
|
if (!depsInstalled && buildToolCommand && effectiveJavaTestingLibraries.length > 0) output += `${pc.cyan(`${stepCounter++}.`)} ${buildToolCommand} test\n`;
|
|
10669
|
-
if (runCommand
|
|
10516
|
+
if (runCommand) output += `${pc.cyan(`${stepCounter++}.`)} ${runCommand}\n`;
|
|
10670
10517
|
else if (sourceCompileCommand && sourceRunCommand) {
|
|
10671
10518
|
output += `${pc.cyan(`${stepCounter++}.`)} ${sourceCompileCommand}\n`;
|
|
10672
10519
|
output += `${pc.cyan(`${stepCounter++}.`)} ${sourceRunCommand}\n`;
|
|
@@ -10714,9 +10561,9 @@ function displayJavaInstructions(config) {
|
|
|
10714
10561
|
output += `${pc.cyan("•")} Testing: ${testingList}\n`;
|
|
10715
10562
|
}
|
|
10716
10563
|
output += `\n${pc.bold("Common Java commands:")}\n`;
|
|
10717
|
-
if (buildToolCommand && runCommand
|
|
10564
|
+
if (buildToolCommand && runCommand && packageCommand) {
|
|
10718
10565
|
if (effectiveJavaTestingLibraries.length > 0) output += `${pc.cyan("•")} Test: ${buildToolCommand} test\n`;
|
|
10719
|
-
output += `${pc.cyan("•")} Run: ${runCommand
|
|
10566
|
+
output += `${pc.cyan("•")} Run: ${runCommand}\n`;
|
|
10720
10567
|
output += `${pc.cyan("•")} Package: ${packageCommand}\n`;
|
|
10721
10568
|
} else if (sourceCompileCommand && sourceRunCommand) {
|
|
10722
10569
|
output += `${pc.cyan("•")} Compile: ${sourceCompileCommand}\n`;
|
|
@@ -10734,14 +10581,14 @@ function displayJavaInstructions(config) {
|
|
|
10734
10581
|
function displayPythonInstructions(config) {
|
|
10735
10582
|
const { relativePath, depsInstalled, pythonWebFramework, pythonOrm, pythonValidation, pythonAi, pythonApi, pythonTaskQueue, pythonQuality } = config;
|
|
10736
10583
|
const cdCmd = `cd ${relativePath}`;
|
|
10737
|
-
let runCommand
|
|
10738
|
-
if (pythonWebFramework === "django") runCommand
|
|
10739
|
-
else if (pythonWebFramework === "flask") runCommand
|
|
10740
|
-
else if (pythonWebFramework === "litestar") runCommand
|
|
10584
|
+
let runCommand = "uv run uvicorn app.main:app --reload";
|
|
10585
|
+
if (pythonWebFramework === "django") runCommand = "uv run python manage.py runserver";
|
|
10586
|
+
else if (pythonWebFramework === "flask") runCommand = "uv run flask --app app.main run --reload";
|
|
10587
|
+
else if (pythonWebFramework === "litestar") runCommand = "litestar --app src.app.main:app run --reload --port 3001";
|
|
10741
10588
|
let output = `${pc.bold("Next steps")}\n${pc.cyan("1.")} ${cdCmd}\n`;
|
|
10742
10589
|
let stepCounter = 2;
|
|
10743
10590
|
if (!depsInstalled) output += `${pc.cyan(`${stepCounter++}.`)} uv sync\n`;
|
|
10744
|
-
output += `${pc.cyan(`${stepCounter++}.`)} ${runCommand
|
|
10591
|
+
output += `${pc.cyan(`${stepCounter++}.`)} ${runCommand}\n`;
|
|
10745
10592
|
output += `\n${pc.bold("Your Python project includes:")}\n`;
|
|
10746
10593
|
if (pythonWebFramework && pythonWebFramework !== "none") output += `${pc.cyan("•")} Web Framework: ${{
|
|
10747
10594
|
fastapi: "FastAPI",
|
|
@@ -10779,7 +10626,7 @@ function displayPythonInstructions(config) {
|
|
|
10779
10626
|
}[pythonQuality] || pythonQuality}\n`;
|
|
10780
10627
|
output += `\n${pc.bold("Common Python commands:")}\n`;
|
|
10781
10628
|
output += `${pc.cyan("•")} Install: uv sync\n`;
|
|
10782
|
-
output += `${pc.cyan("•")} Run: ${runCommand
|
|
10629
|
+
output += `${pc.cyan("•")} Run: ${runCommand}\n`;
|
|
10783
10630
|
output += `${pc.cyan("•")} Test: uv run pytest\n`;
|
|
10784
10631
|
if (pythonQuality === "ruff") {
|
|
10785
10632
|
output += `${pc.cyan("•")} Format: uv run ruff format .\n`;
|
|
@@ -10829,6 +10676,8 @@ function displayElixirInstructions(config) {
|
|
|
10829
10676
|
async function createProject(options, cliInput = {}) {
|
|
10830
10677
|
const projectDir = options.projectDir;
|
|
10831
10678
|
const isConvex = options.backend === "convex";
|
|
10679
|
+
const setupFailures = [];
|
|
10680
|
+
const dirHadContentBefore = await fs.pathExists(projectDir) && (await fs.readdir(projectDir)).length > 0;
|
|
10832
10681
|
try {
|
|
10833
10682
|
await fs.ensureDir(projectDir);
|
|
10834
10683
|
const [{ generateVirtualProject, EMBEDDED_TEMPLATES }, { writeTreeToFilesystem }] = await Promise.all([import("@better-fullstack/template-generator"), import("@better-fullstack/template-generator/fs-writer")]);
|
|
@@ -10840,29 +10689,53 @@ async function createProject(options, cliInput = {}) {
|
|
|
10840
10689
|
await writeTreeToFilesystem(result.tree, projectDir);
|
|
10841
10690
|
await setPackageManagerVersion(projectDir, options.packageManager);
|
|
10842
10691
|
await ensurePackageManagerProjectFiles(projectDir, options.packageManager);
|
|
10843
|
-
if (!isConvex && options.database !== "none")
|
|
10692
|
+
if (!isConvex && options.database !== "none") {
|
|
10693
|
+
const dbResult = await setupDatabase(options, cliInput);
|
|
10694
|
+
if (dbResult && !dbResult.success) setupFailures.push(dbResult);
|
|
10695
|
+
}
|
|
10844
10696
|
if (options.addons.length > 0 && options.addons[0] !== "none") await setupAddons(options);
|
|
10845
10697
|
await applyDependencyVersionChannel(projectDir, options.versionChannel);
|
|
10846
10698
|
await writeBtsConfig(options);
|
|
10847
10699
|
await formatProject(projectDir);
|
|
10848
10700
|
if (!isSilent()) log.success("Project template successfully scaffolded!");
|
|
10849
|
-
if (options.install && (options.ecosystem === "typescript" || options.ecosystem === "react-native"))
|
|
10850
|
-
|
|
10851
|
-
|
|
10852
|
-
|
|
10853
|
-
|
|
10854
|
-
|
|
10855
|
-
|
|
10856
|
-
if (options.install && options.ecosystem === "
|
|
10857
|
-
|
|
10858
|
-
|
|
10701
|
+
if (options.install && (options.ecosystem === "typescript" || options.ecosystem === "react-native")) {
|
|
10702
|
+
const result$1 = await installDependencies({
|
|
10703
|
+
projectDir,
|
|
10704
|
+
packageManager: options.packageManager
|
|
10705
|
+
});
|
|
10706
|
+
if (!result$1.success) setupFailures.push(result$1);
|
|
10707
|
+
}
|
|
10708
|
+
if (options.install && options.ecosystem === "rust") {
|
|
10709
|
+
const result$1 = await runCargoBuild({ projectDir });
|
|
10710
|
+
if (!result$1.success) setupFailures.push(result$1);
|
|
10711
|
+
}
|
|
10712
|
+
if (options.install && options.ecosystem === "python") {
|
|
10713
|
+
const result$1 = await runUvSync({ projectDir });
|
|
10714
|
+
if (!result$1.success) setupFailures.push(result$1);
|
|
10715
|
+
}
|
|
10716
|
+
if (options.install && options.ecosystem === "go") {
|
|
10717
|
+
const result$1 = await runGoModTidy({ projectDir });
|
|
10718
|
+
if (!result$1.success) setupFailures.push(result$1);
|
|
10719
|
+
}
|
|
10720
|
+
if (options.install && options.ecosystem === "java" && options.javaBuildTool !== "none") {
|
|
10721
|
+
const result$1 = options.javaBuildTool === "gradle" ? await runGradleTests({ projectDir }) : await runMavenTests({ projectDir });
|
|
10722
|
+
if (!result$1.success) setupFailures.push(result$1);
|
|
10723
|
+
}
|
|
10724
|
+
if (options.install && options.ecosystem === "elixir") {
|
|
10725
|
+
const result$1 = await runMixCompile({ projectDir });
|
|
10726
|
+
if (!result$1.success) setupFailures.push(result$1);
|
|
10727
|
+
}
|
|
10859
10728
|
await initializeGit(projectDir, options.git);
|
|
10860
10729
|
if (!isSilent()) await displayPostInstallInstructions({
|
|
10861
10730
|
...options,
|
|
10862
10731
|
depsInstalled: options.install
|
|
10863
10732
|
});
|
|
10864
|
-
return
|
|
10733
|
+
return {
|
|
10734
|
+
projectDir,
|
|
10735
|
+
setupFailures
|
|
10736
|
+
};
|
|
10865
10737
|
} catch (error) {
|
|
10738
|
+
await rollbackPartialProject(projectDir, dirHadContentBefore);
|
|
10866
10739
|
if (error instanceof Error) {
|
|
10867
10740
|
if (!isSilent()) console.error(error.stack);
|
|
10868
10741
|
exitWithError(`Error during project creation: ${error.message}`);
|
|
@@ -10872,6 +10745,23 @@ async function createProject(options, cliInput = {}) {
|
|
|
10872
10745
|
}
|
|
10873
10746
|
}
|
|
10874
10747
|
}
|
|
10748
|
+
/**
|
|
10749
|
+
* Remove a half-written project directory after a fatal scaffolding error so the
|
|
10750
|
+
* user is not left with a broken, partially generated project. Only removes
|
|
10751
|
+
* directories we created ourselves (empty/new before scaffolding) — never one
|
|
10752
|
+
* that already had user content (merge mode). Set BTS_KEEP_FAILED_OUTPUT=1 to
|
|
10753
|
+
* keep the partial output for debugging template failures.
|
|
10754
|
+
*/
|
|
10755
|
+
async function rollbackPartialProject(projectDir, dirHadContentBefore) {
|
|
10756
|
+
if (dirHadContentBefore || process.env.BTS_KEEP_FAILED_OUTPUT) {
|
|
10757
|
+
if (!isSilent() && dirHadContentBefore) log.warn(`Left partially created files in ${projectDir} (directory already existed). Review and clean up manually.`);
|
|
10758
|
+
return;
|
|
10759
|
+
}
|
|
10760
|
+
try {
|
|
10761
|
+
await fs.remove(projectDir);
|
|
10762
|
+
if (!isSilent()) log.warn(`Cleaned up partially created project at ${projectDir}. Set BTS_KEEP_FAILED_OUTPUT=1 to keep it for debugging.`);
|
|
10763
|
+
} catch {}
|
|
10764
|
+
}
|
|
10875
10765
|
async function setPackageManagerVersion(projectDir, packageManager) {
|
|
10876
10766
|
const pkgJsonPath = path.join(projectDir, "package.json");
|
|
10877
10767
|
if (!await fs.pathExists(pkgJsonPath)) return;
|
|
@@ -10932,6 +10822,7 @@ function getYesBaseConfig(flagConfig) {
|
|
|
10932
10822
|
rateLimit: "none",
|
|
10933
10823
|
i18n: "none",
|
|
10934
10824
|
search: "none",
|
|
10825
|
+
vectorDb: "none",
|
|
10935
10826
|
fileStorage: "none",
|
|
10936
10827
|
mobileNavigation: "expo-router",
|
|
10937
10828
|
mobileUI: "none",
|
|
@@ -10942,8 +10833,8 @@ function getYesBaseConfig(flagConfig) {
|
|
|
10942
10833
|
mobileDeepLinking: "none"
|
|
10943
10834
|
};
|
|
10944
10835
|
}
|
|
10945
|
-
function shouldPromptForVersionChannel(input) {
|
|
10946
|
-
if (input.yes || input.part?.length || input.versionChannel !== void 0 || isSilent()) return false;
|
|
10836
|
+
function shouldPromptForVersionChannel(input, hasConfigBase) {
|
|
10837
|
+
if (input.yes || input.part?.length || hasConfigBase || input.versionChannel !== void 0 || isSilent()) return false;
|
|
10947
10838
|
return canPromptInteractively();
|
|
10948
10839
|
}
|
|
10949
10840
|
async function createProjectHandler(input, options = {}) {
|
|
@@ -10954,10 +10845,15 @@ async function createProjectHandler(input, options = {}) {
|
|
|
10954
10845
|
try {
|
|
10955
10846
|
if (!isSilent() && input.renderTitle !== false) renderTitle();
|
|
10956
10847
|
if (!isSilent()) intro(pc.magenta("Creating a new Better Fullstack project"));
|
|
10848
|
+
await maybeShowTelemetryNotice();
|
|
10957
10849
|
if (!isSilent() && input.yolo) consola.fatal("YOLO mode enabled - skipping checks. Things may break!");
|
|
10850
|
+
const configBase = await resolveCreateConfigBase(input);
|
|
10851
|
+
const hasConfigBase = configBase !== void 0;
|
|
10852
|
+
if (hasConfigBase && !isSilent()) log.info(pc.cyan(input.config ? `Using stack from config file: ${input.config}` : `Using stack from history entry #${input.fromHistory}`));
|
|
10853
|
+
const useDefaultsForName = Boolean(input.yes) || hasConfigBase;
|
|
10958
10854
|
let currentPathInput;
|
|
10959
|
-
if (
|
|
10960
|
-
else if (
|
|
10855
|
+
if (useDefaultsForName && input.projectName) currentPathInput = input.projectName;
|
|
10856
|
+
else if (useDefaultsForName) {
|
|
10961
10857
|
const defaultConfig = getDefaultConfig();
|
|
10962
10858
|
let defaultName = defaultConfig.relativePath;
|
|
10963
10859
|
let counter = 1;
|
|
@@ -10967,7 +10863,7 @@ async function createProjectHandler(input, options = {}) {
|
|
|
10967
10863
|
}
|
|
10968
10864
|
currentPathInput = defaultName;
|
|
10969
10865
|
} else currentPathInput = await getProjectName(input.projectName);
|
|
10970
|
-
const versionChannel = shouldPromptForVersionChannel(input) ? await getVersionChannelChoice() : input.versionChannel ?? "stable";
|
|
10866
|
+
const versionChannel = shouldPromptForVersionChannel(input, hasConfigBase) ? await getVersionChannelChoice() : input.versionChannel ?? configBase?.versionChannel ?? "stable";
|
|
10971
10867
|
let finalResolvedPath;
|
|
10972
10868
|
let finalBaseName;
|
|
10973
10869
|
if (input.dryRun) {
|
|
@@ -11046,6 +10942,7 @@ async function createProjectHandler(input, options = {}) {
|
|
|
11046
10942
|
rateLimit: "none",
|
|
11047
10943
|
i18n: "none",
|
|
11048
10944
|
search: "none",
|
|
10945
|
+
vectorDb: "none",
|
|
11049
10946
|
featureFlags: "none",
|
|
11050
10947
|
analytics: "none",
|
|
11051
10948
|
fileStorage: "none",
|
|
@@ -11132,11 +11029,15 @@ async function createProjectHandler(input, options = {}) {
|
|
|
11132
11029
|
finalBaseName = setupResult.finalBaseName;
|
|
11133
11030
|
currentPathInput = finalPathInput;
|
|
11134
11031
|
}
|
|
11135
|
-
const
|
|
11136
|
-
...input,
|
|
11032
|
+
const explicitInput = {
|
|
11033
|
+
...Object.fromEntries(Object.entries(input).filter(([, value]) => value !== void 0)),
|
|
11137
11034
|
projectDirectory: input.projectName
|
|
11138
11035
|
};
|
|
11139
|
-
const
|
|
11036
|
+
const originalInput = {
|
|
11037
|
+
...configBase,
|
|
11038
|
+
...explicitInput
|
|
11039
|
+
};
|
|
11040
|
+
const providedFlags = getProvidedFlags(explicitInput);
|
|
11140
11041
|
let cliInput = originalInput;
|
|
11141
11042
|
if (input.template && input.template !== "none") {
|
|
11142
11043
|
const templateConfig = getTemplateConfig(input.template);
|
|
@@ -11159,7 +11060,7 @@ async function createProjectHandler(input, options = {}) {
|
|
|
11159
11060
|
}
|
|
11160
11061
|
const { validatePreflightConfig } = await import("@better-fullstack/template-generator");
|
|
11161
11062
|
let config;
|
|
11162
|
-
if (cliInput.yes || cliInput.part?.length) {
|
|
11063
|
+
if (cliInput.yes || cliInput.part?.length || hasConfigBase) {
|
|
11163
11064
|
const flagConfig = processProvidedFlagsWithoutValidation(cliInput, finalBaseName);
|
|
11164
11065
|
config = {
|
|
11165
11066
|
...getYesBaseConfig(flagConfig),
|
|
@@ -11235,7 +11136,7 @@ async function createProjectHandler(input, options = {}) {
|
|
|
11235
11136
|
files
|
|
11236
11137
|
};
|
|
11237
11138
|
}
|
|
11238
|
-
await createProject(config, { manualDb: cliInput.manualDb ?? input.manualDb });
|
|
11139
|
+
const setupFailures = (await createProject(config, { manualDb: cliInput.manualDb ?? input.manualDb }))?.setupFailures ?? [];
|
|
11239
11140
|
if (cliInput.verify ?? input.verify) await runGeneratedChecks(config);
|
|
11240
11141
|
const reproducibleCommand = generateReproducibleCommand(config);
|
|
11241
11142
|
if (!isSilent()) log.success(pc.blue(`You can reproduce this setup with the following command:\n${reproducibleCommand}`));
|
|
@@ -11248,7 +11149,12 @@ async function createProjectHandler(input, options = {}) {
|
|
|
11248
11149
|
const elapsedTimeMs = Date.now() - startTime;
|
|
11249
11150
|
if (!isSilent()) {
|
|
11250
11151
|
const elapsedTimeInSeconds = (elapsedTimeMs / 1e3).toFixed(2);
|
|
11251
|
-
|
|
11152
|
+
if (setupFailures.length > 0) {
|
|
11153
|
+
const stepList = setupFailures.map((f) => f.step).join(", ");
|
|
11154
|
+
const installCmd = config.packageManager === "npm" ? "npm install" : `${config.packageManager} install`;
|
|
11155
|
+
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).`));
|
|
11156
|
+
outro(pc.yellow(`Project created with ${setupFailures.length} unfinished setup step(s) in ${pc.bold(elapsedTimeInSeconds)}s.`));
|
|
11157
|
+
} else outro(pc.magenta(`Project created successfully in ${pc.bold(elapsedTimeInSeconds)} seconds!`));
|
|
11252
11158
|
}
|
|
11253
11159
|
return {
|
|
11254
11160
|
success: true,
|
|
@@ -11257,7 +11163,8 @@ async function createProjectHandler(input, options = {}) {
|
|
|
11257
11163
|
timeScaffolded,
|
|
11258
11164
|
elapsedTimeMs,
|
|
11259
11165
|
projectDirectory: config.projectDir,
|
|
11260
|
-
relativePath: config.relativePath
|
|
11166
|
+
relativePath: config.relativePath,
|
|
11167
|
+
setupFailures
|
|
11261
11168
|
};
|
|
11262
11169
|
} catch (error) {
|
|
11263
11170
|
if (error instanceof UserCancelledError) {
|
|
@@ -11434,7 +11341,7 @@ const router = os.router({
|
|
|
11434
11341
|
packageManager: types_exports.PackageManagerSchema.optional().describe("Package manager to use"),
|
|
11435
11342
|
projectDir: z.string().optional().describe("Project directory (defaults to current)")
|
|
11436
11343
|
})).handler(async ({ input }) => {
|
|
11437
|
-
const { addHandler } = await import("./add-handler-
|
|
11344
|
+
const { addHandler } = await import("./add-handler-BjTmOwEN.mjs");
|
|
11438
11345
|
await addHandler(input);
|
|
11439
11346
|
}),
|
|
11440
11347
|
history: os.meta({ description: "Show history of scaffolded projects with reproducible commands" }).input(z.object({
|
|
@@ -11444,6 +11351,17 @@ const router = os.router({
|
|
|
11444
11351
|
})).handler(async ({ input }) => {
|
|
11445
11352
|
await historyHandler(input);
|
|
11446
11353
|
}),
|
|
11354
|
+
telemetry: os.meta({ description: "View or change anonymous usage telemetry collection (status | enable | disable)" }).input(z.tuple([z.enum([
|
|
11355
|
+
"status",
|
|
11356
|
+
"enable",
|
|
11357
|
+
"disable"
|
|
11358
|
+
]).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 }) => {
|
|
11359
|
+
const [action, options] = input;
|
|
11360
|
+
await telemetryHandler({
|
|
11361
|
+
action,
|
|
11362
|
+
json: options.json
|
|
11363
|
+
});
|
|
11364
|
+
}),
|
|
11447
11365
|
"update-deps": os.meta({ description: "Check and update dependency versions in add-deps.ts" }).input(z.object({
|
|
11448
11366
|
check: z.boolean().default(false).describe("Report only, no changes"),
|
|
11449
11367
|
patch: z.boolean().default(false).describe("Apply patch/minor updates only"),
|
|
@@ -11451,7 +11369,7 @@ const router = os.router({
|
|
|
11451
11369
|
ecosystem: z.string().optional().describe("Filter by ecosystem (effect, tanstack, prisma, etc.)"),
|
|
11452
11370
|
"list-ecosystems": z.boolean().default(false).describe("List available ecosystems")
|
|
11453
11371
|
})).handler(async ({ input }) => {
|
|
11454
|
-
const { updateDepsHandler, showEcosystems } = await import("./update-deps-
|
|
11372
|
+
const { updateDepsHandler, showEcosystems } = await import("./update-deps-D5OG0KmJ.mjs");
|
|
11455
11373
|
if (input["list-ecosystems"]) {
|
|
11456
11374
|
showEcosystems();
|
|
11457
11375
|
return;
|
|
@@ -11466,6 +11384,17 @@ const router = os.router({
|
|
|
11466
11384
|
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 () => {
|
|
11467
11385
|
log.message("MCP server is started via the 'mcp' subcommand intercepted in cli.ts.");
|
|
11468
11386
|
log.message("Run: create-better-fullstack mcp");
|
|
11387
|
+
}),
|
|
11388
|
+
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({
|
|
11389
|
+
skipChecks: z.boolean().optional().default(false).describe("Skip the ecosystem build/type checks (config + deps + env only)"),
|
|
11390
|
+
json: z.boolean().optional().default(false).describe("Output the diagnosis as JSON")
|
|
11391
|
+
})])).handler(async ({ input }) => {
|
|
11392
|
+
const [projectDir, options] = input;
|
|
11393
|
+
const { doctorCommand } = await import("./doctor-Bm9qvkya.mjs");
|
|
11394
|
+
await doctorCommand({
|
|
11395
|
+
projectDir,
|
|
11396
|
+
...options
|
|
11397
|
+
});
|
|
11469
11398
|
})
|
|
11470
11399
|
});
|
|
11471
11400
|
const caller = createRouterClient(router, { context: {} });
|
|
@@ -11531,7 +11460,7 @@ async function builder() {
|
|
|
11531
11460
|
return caller.builder();
|
|
11532
11461
|
}
|
|
11533
11462
|
async function add(input) {
|
|
11534
|
-
const { addHandler } = await import("./add-handler-
|
|
11463
|
+
const { addHandler } = await import("./add-handler-BjTmOwEN.mjs");
|
|
11535
11464
|
return addHandler(input, { silent: true });
|
|
11536
11465
|
}
|
|
11537
11466
|
async function history(options) {
|
|
@@ -11541,6 +11470,9 @@ async function history(options) {
|
|
|
11541
11470
|
json: options?.json ?? false
|
|
11542
11471
|
});
|
|
11543
11472
|
}
|
|
11473
|
+
async function telemetry(action = "status", options) {
|
|
11474
|
+
return caller.telemetry([action, { json: options?.json ?? false }]);
|
|
11475
|
+
}
|
|
11544
11476
|
|
|
11545
11477
|
//#endregion
|
|
11546
|
-
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 };
|
|
11478
|
+
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 };
|