create-better-fullstack 2.0.3 → 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-BQN6pwGJ.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 +85 -29
- package/dist/index.mjs +10 -6
- package/dist/{install-dependencies-D0Z1dZEx.mjs → install-dependencies-CDjTNvIV.mjs} +120 -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-BhcXBjxh.mjs → run-Cqy-n1iT.mjs} +548 -658
- package/dist/run-DYWSKowD.mjs +10 -0
- package/dist/templates-B351F1yX.mjs +489 -0
- package/package.json +5 -4
- package/dist/addons-setup-CV5uZyhH.mjs +0 -5
- package/dist/run-DCxVLUMS.mjs +0 -7
- /package/dist/{update-deps-CLebIM70.mjs → update-deps-D5OG0KmJ.mjs} +0 -0
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { 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,11 @@ 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
|
+
},
|
|
534
747
|
openapi: {
|
|
535
748
|
value: "openapi",
|
|
536
749
|
label: "OpenAPI",
|
|
@@ -899,6 +1112,11 @@ const CMS_PROMPT_OPTIONS = [
|
|
|
899
1112
|
label: "Directus",
|
|
900
1113
|
hint: "Open data platform and headless CMS for SQL databases"
|
|
901
1114
|
},
|
|
1115
|
+
{
|
|
1116
|
+
value: "keystatic",
|
|
1117
|
+
label: "Keystatic",
|
|
1118
|
+
hint: "Git-backed CMS for Markdown, JSON, and YAML content"
|
|
1119
|
+
},
|
|
902
1120
|
{
|
|
903
1121
|
value: "none",
|
|
904
1122
|
label: "None",
|
|
@@ -2361,7 +2579,7 @@ async function getFrontendChoice(frontendOptions, backend, auth) {
|
|
|
2361
2579
|
});
|
|
2362
2580
|
if (isGoBack(frontendTypes)) return GO_BACK_SYMBOL;
|
|
2363
2581
|
if (isCancel$1(frontendTypes)) return exitCancelled("Operation cancelled");
|
|
2364
|
-
setIsFirstPrompt(false);
|
|
2582
|
+
setIsFirstPrompt$1(false);
|
|
2365
2583
|
const result = [];
|
|
2366
2584
|
let shouldRestart = false;
|
|
2367
2585
|
if (frontendTypes.includes("web")) {
|
|
@@ -2377,7 +2595,7 @@ async function getFrontendChoice(frontendOptions, backend, auth) {
|
|
|
2377
2595
|
else result.push(webFramework);
|
|
2378
2596
|
}
|
|
2379
2597
|
if (shouldRestart) {
|
|
2380
|
-
setIsFirstPrompt(wasFirstPrompt);
|
|
2598
|
+
setIsFirstPrompt$1(wasFirstPrompt);
|
|
2381
2599
|
continue;
|
|
2382
2600
|
}
|
|
2383
2601
|
if (frontendTypes.includes("native")) {
|
|
@@ -2390,14 +2608,14 @@ async function getFrontendChoice(frontendOptions, backend, auth) {
|
|
|
2390
2608
|
});
|
|
2391
2609
|
if (isGoBack(nativeFramework)) if (frontendTypes.includes("web")) shouldRestart = true;
|
|
2392
2610
|
else {
|
|
2393
|
-
setIsFirstPrompt(wasFirstPrompt);
|
|
2611
|
+
setIsFirstPrompt$1(wasFirstPrompt);
|
|
2394
2612
|
continue;
|
|
2395
2613
|
}
|
|
2396
2614
|
else if (isCancel$1(nativeFramework)) return exitCancelled("Operation cancelled");
|
|
2397
2615
|
else result.push(nativeFramework);
|
|
2398
2616
|
}
|
|
2399
2617
|
if (shouldRestart) {
|
|
2400
|
-
setIsFirstPrompt(wasFirstPrompt);
|
|
2618
|
+
setIsFirstPrompt$1(wasFirstPrompt);
|
|
2401
2619
|
continue;
|
|
2402
2620
|
}
|
|
2403
2621
|
return result;
|
|
@@ -2832,17 +3050,36 @@ async function getGoObservabilityChoice(goObservability) {
|
|
|
2832
3050
|
|
|
2833
3051
|
//#endregion
|
|
2834
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
|
+
]);
|
|
2835
3066
|
async function getI18nChoice(i18n, frontend) {
|
|
2836
3067
|
if (i18n !== void 0) return i18n;
|
|
3068
|
+
const hasNext = frontend?.includes("next") ?? false;
|
|
2837
3069
|
const response = await navigableSelect({
|
|
2838
3070
|
message: "Select internationalization (i18n) library",
|
|
2839
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
|
+
}] : [],
|
|
2840
3077
|
{
|
|
2841
3078
|
value: "i18next",
|
|
2842
3079
|
label: "i18next",
|
|
2843
3080
|
hint: "Full-featured i18n framework, works with all frontends"
|
|
2844
3081
|
},
|
|
2845
|
-
...
|
|
3082
|
+
...hasNext ? [{
|
|
2846
3083
|
value: "next-intl",
|
|
2847
3084
|
label: "next-intl",
|
|
2848
3085
|
hint: "Lightweight i18n for Next.js with App Router support"
|
|
@@ -2859,17 +3096,6 @@ async function getI18nChoice(i18n, frontend) {
|
|
|
2859
3096
|
return response;
|
|
2860
3097
|
}
|
|
2861
3098
|
|
|
2862
|
-
//#endregion
|
|
2863
|
-
//#region src/utils/command-exists.ts
|
|
2864
|
-
async function commandExists(command) {
|
|
2865
|
-
try {
|
|
2866
|
-
if (process.platform === "win32") return (await $({ reject: false })`where ${command}`).exitCode === 0;
|
|
2867
|
-
return (await $({ reject: false })`which ${command}`).exitCode === 0;
|
|
2868
|
-
} catch {
|
|
2869
|
-
return false;
|
|
2870
|
-
}
|
|
2871
|
-
}
|
|
2872
|
-
|
|
2873
3099
|
//#endregion
|
|
2874
3100
|
//#region src/prompts/install.ts
|
|
2875
3101
|
async function getinstallChoice(install, ecosystem, javaBuildTool) {
|
|
@@ -5618,7 +5844,7 @@ async function navigableGroup(prompts, opts) {
|
|
|
5618
5844
|
while (currentIndex < promptNames.length) {
|
|
5619
5845
|
const name = promptNames[currentIndex];
|
|
5620
5846
|
const prompt = prompts[name];
|
|
5621
|
-
setIsFirstPrompt
|
|
5847
|
+
setIsFirstPrompt(currentIndex === 0);
|
|
5622
5848
|
setLastPromptShownUI(false);
|
|
5623
5849
|
const result = await prompt({ results })?.catch((e) => {
|
|
5624
5850
|
throw e;
|
|
@@ -5639,7 +5865,7 @@ async function navigableGroup(prompts, opts) {
|
|
|
5639
5865
|
results[name] = "canceled";
|
|
5640
5866
|
opts.onCancel({ results });
|
|
5641
5867
|
}
|
|
5642
|
-
setIsFirstPrompt
|
|
5868
|
+
setIsFirstPrompt(false);
|
|
5643
5869
|
return results;
|
|
5644
5870
|
}
|
|
5645
5871
|
if (goingBack && !didLastPromptShowUI()) {
|
|
@@ -5654,7 +5880,7 @@ async function navigableGroup(prompts, opts) {
|
|
|
5654
5880
|
results[name] = result;
|
|
5655
5881
|
currentIndex++;
|
|
5656
5882
|
}
|
|
5657
|
-
setIsFirstPrompt
|
|
5883
|
+
setIsFirstPrompt(false);
|
|
5658
5884
|
return results;
|
|
5659
5885
|
}
|
|
5660
5886
|
|
|
@@ -6095,6 +6321,11 @@ const SEARCH_PROMPT_OPTIONS = [
|
|
|
6095
6321
|
label: "Elasticsearch",
|
|
6096
6322
|
hint: "Distributed search and analytics engine with local and cloud deployments"
|
|
6097
6323
|
},
|
|
6324
|
+
{
|
|
6325
|
+
value: "opensearch",
|
|
6326
|
+
label: "OpenSearch",
|
|
6327
|
+
hint: "Open-source search and analytics suite compatible with Elasticsearch APIs"
|
|
6328
|
+
},
|
|
6098
6329
|
{
|
|
6099
6330
|
value: "algolia",
|
|
6100
6331
|
label: "Algolia",
|
|
@@ -6149,6 +6380,77 @@ async function getSearchChoice(search, backend, ecosystem) {
|
|
|
6149
6380
|
return response;
|
|
6150
6381
|
}
|
|
6151
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
|
+
|
|
6152
6454
|
//#endregion
|
|
6153
6455
|
//#region src/prompts/server-deploy.ts
|
|
6154
6456
|
async function getServerDeploymentChoice(deployment, runtime, backend, _webDeploy) {
|
|
@@ -6442,7 +6744,7 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
|
|
|
6442
6744
|
},
|
|
6443
6745
|
addons: ({ results }) => {
|
|
6444
6746
|
if (results.ecosystem !== "typescript") {
|
|
6445
|
-
const nonTypeScriptAddons = (flags.addons ?? []).filter((addon) => addon === "docker-compose");
|
|
6747
|
+
const nonTypeScriptAddons = (flags.addons ?? []).filter((addon) => addon === "docker-compose" || addon === "devcontainer" || addon === "github-actions");
|
|
6446
6748
|
return Promise.resolve(nonTypeScriptAddons);
|
|
6447
6749
|
}
|
|
6448
6750
|
return getAddonsChoice(flags.addons, results.frontend, results.auth, results.backend, results.runtime);
|
|
@@ -6536,6 +6838,10 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
|
|
|
6536
6838
|
if (results.ecosystem === "react-native" || results.ecosystem === "elixir") return Promise.resolve("none");
|
|
6537
6839
|
return getSearchChoice(flags.search, results.backend, results.ecosystem);
|
|
6538
6840
|
},
|
|
6841
|
+
vectorDb: ({ results }) => {
|
|
6842
|
+
if (results.ecosystem !== "typescript") return Promise.resolve("none");
|
|
6843
|
+
return getVectorDbChoice(flags.vectorDb, results.backend, results.ecosystem);
|
|
6844
|
+
},
|
|
6539
6845
|
fileStorage: ({ results }) => {
|
|
6540
6846
|
if (results.ecosystem !== "typescript") return Promise.resolve("none");
|
|
6541
6847
|
return getFileStorageChoice(flags.fileStorage, results.backend);
|
|
@@ -6943,6 +7249,7 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
|
|
|
6943
7249
|
rateLimit: result.rateLimit,
|
|
6944
7250
|
i18n: result.i18n,
|
|
6945
7251
|
search: result.search,
|
|
7252
|
+
vectorDb: result.vectorDb,
|
|
6946
7253
|
fileStorage: result.fileStorage,
|
|
6947
7254
|
mobileNavigation: result.mobileNavigation,
|
|
6948
7255
|
mobileUI: result.mobileUI,
|
|
@@ -7111,35 +7418,76 @@ async function getVersionChannelChoice(versionChannel) {
|
|
|
7111
7418
|
}
|
|
7112
7419
|
|
|
7113
7420
|
//#endregion
|
|
7114
|
-
//#region src/utils/
|
|
7115
|
-
|
|
7116
|
-
|
|
7117
|
-
|
|
7118
|
-
|
|
7119
|
-
|
|
7120
|
-
|
|
7121
|
-
|
|
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;
|
|
7122
7440
|
}
|
|
7123
|
-
|
|
7124
|
-
|
|
7125
|
-
|
|
7126
|
-
|
|
7127
|
-
|
|
7128
|
-
|
|
7129
|
-
|
|
7130
|
-
|
|
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
|
+
};
|
|
7131
7460
|
}
|
|
7132
|
-
|
|
7133
|
-
|
|
7134
|
-
|
|
7135
|
-
|
|
7136
|
-
|
|
7137
|
-
|
|
7138
|
-
|
|
7139
|
-
|
|
7140
|
-
|
|
7141
|
-
|
|
7142
|
-
|
|
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);
|
|
7143
7491
|
}
|
|
7144
7492
|
|
|
7145
7493
|
//#endregion
|
|
@@ -7284,480 +7632,6 @@ function displayConfig(config) {
|
|
|
7284
7632
|
return configDisplay.join("\n");
|
|
7285
7633
|
}
|
|
7286
7634
|
|
|
7287
|
-
//#endregion
|
|
7288
|
-
//#region src/utils/generate-reproducible-command.ts
|
|
7289
|
-
function getBaseCommand(packageManager) {
|
|
7290
|
-
switch (packageManager) {
|
|
7291
|
-
case "bun": return "bun create better-fullstack@latest";
|
|
7292
|
-
case "pnpm": return "pnpm create better-fullstack@latest";
|
|
7293
|
-
case "yarn": return "yarn create better-fullstack@latest";
|
|
7294
|
-
case "npm":
|
|
7295
|
-
default: return "npx create-better-fullstack@latest";
|
|
7296
|
-
}
|
|
7297
|
-
}
|
|
7298
|
-
function formatArrayFlag(flag, values) {
|
|
7299
|
-
const normalizedValues = values.filter((value) => value !== "none");
|
|
7300
|
-
if (normalizedValues.length === 0) return `--${flag} none`;
|
|
7301
|
-
return `--${flag} ${normalizedValues.join(" ")}`;
|
|
7302
|
-
}
|
|
7303
|
-
function appendCommonFlags(flags, config) {
|
|
7304
|
-
if (config.aiDocs && config.aiDocs.length > 0) flags.push(formatArrayFlag("ai-docs", config.aiDocs));
|
|
7305
|
-
else flags.push("--ai-docs none");
|
|
7306
|
-
flags.push(config.git ? "--git" : "--no-git");
|
|
7307
|
-
flags.push(`--package-manager ${config.packageManager}`);
|
|
7308
|
-
if (config.versionChannel !== "stable") flags.push(`--version-channel ${config.versionChannel}`);
|
|
7309
|
-
flags.push(config.install ? "--install" : "--no-install");
|
|
7310
|
-
}
|
|
7311
|
-
function hasGraphPrimaryPart(config, role, ecosystem) {
|
|
7312
|
-
return config.stackParts?.some((part) => part.source !== "provided" && part.role === role && !part.ownerPartId && (!ecosystem || part.ecosystem === ecosystem));
|
|
7313
|
-
}
|
|
7314
|
-
function hasOwnedGraphPart(config, ownerRole, role, ecosystem, toolId) {
|
|
7315
|
-
const owner = config.stackParts?.find((part) => part.source !== "provided" && part.role === ownerRole && !part.ownerPartId && (!ecosystem || part.ecosystem === ecosystem));
|
|
7316
|
-
if (!owner) return false;
|
|
7317
|
-
return Boolean(config.stackParts?.some((part) => part.source !== "provided" && part.role === role && part.ownerPartId === owner.id && (!ecosystem || part.ecosystem === ecosystem) && (!toolId || part.toolId === toolId)));
|
|
7318
|
-
}
|
|
7319
|
-
function hasGraphAddonPart(config, addon) {
|
|
7320
|
-
const binding = (0, types_exports.getAddonStackPartBinding)(addon);
|
|
7321
|
-
if (!binding) return false;
|
|
7322
|
-
if (!binding.ownerRole) return Boolean(config.stackParts?.some((part) => part.source !== "provided" && part.role === binding.role && part.ecosystem === binding.ecosystem && part.toolId === addon && !part.ownerPartId));
|
|
7323
|
-
const owner = config.stackParts?.find((part) => part.source !== "provided" && part.role === binding.ownerRole && !part.ownerPartId && part.ecosystem === binding.ecosystem);
|
|
7324
|
-
if (!owner) return false;
|
|
7325
|
-
return Boolean(config.stackParts?.some((part) => part.source !== "provided" && part.role === binding.role && part.ecosystem === binding.ecosystem && part.toolId === addon && part.ownerPartId === owner.id));
|
|
7326
|
-
}
|
|
7327
|
-
function hasGraphExamplePart(config, example) {
|
|
7328
|
-
return Boolean(config.stackParts?.some((part) => part.source !== "provided" && part.role === "examples" && part.ecosystem === "universal" && part.toolId === example && !part.ownerPartId));
|
|
7329
|
-
}
|
|
7330
|
-
function hasGraphArrayParts(config, values, hasPart) {
|
|
7331
|
-
const normalizedValues = values.filter((value) => value !== "none");
|
|
7332
|
-
return normalizedValues.length > 0 && normalizedValues.every((value) => hasPart(config, value));
|
|
7333
|
-
}
|
|
7334
|
-
function appendChangedStringFlag(flags, flag, value, defaultValue) {
|
|
7335
|
-
if (value !== defaultValue) flags.push(`--${flag} ${value}`);
|
|
7336
|
-
}
|
|
7337
|
-
function appendChangedGraphStringFlag(flags, config, role, ecosystem, flag, value, defaultValue) {
|
|
7338
|
-
if (hasGraphPart(config, role, ecosystem)) return;
|
|
7339
|
-
appendChangedStringFlag(flags, flag, value, defaultValue);
|
|
7340
|
-
}
|
|
7341
|
-
function appendChangedOwnedGraphStringFlag(flags, config, ownerRole, role, ecosystem, flag, value, defaultValue) {
|
|
7342
|
-
if (hasOwnedGraphPart(config, ownerRole, role, ecosystem, value)) return;
|
|
7343
|
-
appendChangedStringFlag(flags, flag, value, defaultValue);
|
|
7344
|
-
}
|
|
7345
|
-
function hasOwnedGraphArrayParts(config, ownerRole, role, ecosystem, values) {
|
|
7346
|
-
const normalizedValues = values.filter((value) => value !== "none");
|
|
7347
|
-
if (normalizedValues.length === 0) return false;
|
|
7348
|
-
return normalizedValues.every((value) => hasOwnedGraphPart(config, ownerRole, role, ecosystem, value));
|
|
7349
|
-
}
|
|
7350
|
-
function appendChangedOwnedGraphArrayFlag(flags, config, ownerRole, role, ecosystem, flag, values, defaultValues) {
|
|
7351
|
-
if (hasOwnedGraphArrayParts(config, ownerRole, role, ecosystem, values)) return;
|
|
7352
|
-
appendChangedArrayFlag(flags, flag, values, defaultValues);
|
|
7353
|
-
}
|
|
7354
|
-
function appendChangedArrayFlag(flags, flag, values, defaultValues) {
|
|
7355
|
-
if (values.length !== defaultValues.length || values.some((value, index) => value !== defaultValues[index])) flags.push(formatArrayFlag(flag, values));
|
|
7356
|
-
}
|
|
7357
|
-
function appendAstroIntegrationFlag(flags, config) {
|
|
7358
|
-
if (config.frontend.includes("astro") && config.astroIntegration !== "none") flags.push(`--astro-integration ${config.astroIntegration}`);
|
|
7359
|
-
}
|
|
7360
|
-
function appendGraphExtraFlags(flags, config) {
|
|
7361
|
-
if (!hasGraphArrayParts(config, config.addons, hasGraphAddonPart)) appendChangedArrayFlag(flags, "addons", config.addons, ["turborepo"]);
|
|
7362
|
-
if (!hasGraphArrayParts(config, config.examples, hasGraphExamplePart)) appendChangedArrayFlag(flags, "examples", config.examples, []);
|
|
7363
|
-
if (hasGraphPrimaryPart(config, "database")) appendChangedOwnedGraphStringFlag(flags, config, "database", "dbSetup", "universal", "db-setup", config.dbSetup, "none");
|
|
7364
|
-
else appendChangedStringFlag(flags, "db-setup", config.dbSetup, "none");
|
|
7365
|
-
if (hasGraphPrimaryPart(config, "frontend", "typescript")) {
|
|
7366
|
-
appendAstroIntegrationFlag(flags, config);
|
|
7367
|
-
appendChangedOwnedGraphStringFlag(flags, config, "frontend", "deploy", "typescript", "web-deploy", config.webDeploy, "none");
|
|
7368
|
-
appendChangedGraphStringFlag(flags, config, "css", "typescript", "css-framework", config.cssFramework, "tailwind");
|
|
7369
|
-
appendChangedGraphStringFlag(flags, config, "ui", "typescript", "ui-library", config.uiLibrary, "shadcn-ui");
|
|
7370
|
-
if (config.uiLibrary === "shadcn-ui") {
|
|
7371
|
-
appendChangedStringFlag(flags, "shadcn-base", config.shadcnBase ?? "radix", "radix");
|
|
7372
|
-
appendChangedStringFlag(flags, "shadcn-style", config.shadcnStyle ?? "nova", "nova");
|
|
7373
|
-
appendChangedStringFlag(flags, "shadcn-icon-library", config.shadcnIconLibrary ?? "lucide", "lucide");
|
|
7374
|
-
appendChangedStringFlag(flags, "shadcn-color-theme", config.shadcnColorTheme ?? "neutral", "neutral");
|
|
7375
|
-
appendChangedStringFlag(flags, "shadcn-base-color", config.shadcnBaseColor ?? "neutral", "neutral");
|
|
7376
|
-
appendChangedStringFlag(flags, "shadcn-font", config.shadcnFont ?? "inter", "inter");
|
|
7377
|
-
appendChangedStringFlag(flags, "shadcn-radius", config.shadcnRadius ?? "default", "default");
|
|
7378
|
-
}
|
|
7379
|
-
appendChangedGraphStringFlag(flags, config, "stateManagement", "typescript", "state-management", config.stateManagement, "none");
|
|
7380
|
-
appendChangedGraphStringFlag(flags, config, "forms", "typescript", "forms", config.forms, "react-hook-form");
|
|
7381
|
-
appendChangedStringFlag(flags, "validation", config.validation, "zod");
|
|
7382
|
-
appendChangedStringFlag(flags, "testing", config.testing, "vitest");
|
|
7383
|
-
appendChangedGraphStringFlag(flags, config, "animation", "typescript", "animation", config.animation, "none");
|
|
7384
|
-
appendChangedGraphStringFlag(flags, config, "fileUpload", "typescript", "file-upload", config.fileUpload, "none");
|
|
7385
|
-
appendChangedGraphStringFlag(flags, config, "i18n", "typescript", "i18n", config.i18n, "none");
|
|
7386
|
-
appendChangedGraphStringFlag(flags, config, "analytics", "typescript", "analytics", config.analytics, "none");
|
|
7387
|
-
}
|
|
7388
|
-
if (hasGraphPrimaryPart(config, "frontend", "typescript") || hasGraphPrimaryPart(config, "backend", "typescript")) {
|
|
7389
|
-
if (hasGraphPrimaryPart(config, "backend", "typescript")) {
|
|
7390
|
-
appendChangedOwnedGraphStringFlag(flags, config, "backend", "runtime", "typescript", "runtime", config.runtime, "bun");
|
|
7391
|
-
appendChangedOwnedGraphStringFlag(flags, config, "backend", "deploy", "typescript", "server-deploy", config.serverDeploy, "none");
|
|
7392
|
-
} else appendChangedStringFlag(flags, "server-deploy", config.serverDeploy, "none");
|
|
7393
|
-
appendChangedGraphStringFlag(flags, config, "payments", "typescript", "payments", config.payments, "none");
|
|
7394
|
-
appendChangedGraphStringFlag(flags, config, "email", "typescript", "email", config.email, "none");
|
|
7395
|
-
appendChangedStringFlag(flags, "effect", config.effect, "none");
|
|
7396
|
-
appendChangedGraphStringFlag(flags, config, "ai", "typescript", "ai", config.ai, "none");
|
|
7397
|
-
appendChangedGraphStringFlag(flags, config, "realtime", "typescript", "realtime", config.realtime, "none");
|
|
7398
|
-
appendChangedGraphStringFlag(flags, config, "jobQueue", "typescript", "job-queue", config.jobQueue, "none");
|
|
7399
|
-
appendChangedGraphStringFlag(flags, config, "logging", "typescript", "logging", config.logging, "none");
|
|
7400
|
-
appendChangedGraphStringFlag(flags, config, "observability", "typescript", "observability", config.observability, "none");
|
|
7401
|
-
appendChangedGraphStringFlag(flags, config, "featureFlags", "typescript", "feature-flags", config.featureFlags, "none");
|
|
7402
|
-
appendChangedGraphStringFlag(flags, config, "caching", "typescript", "caching", config.caching, "none");
|
|
7403
|
-
appendChangedGraphStringFlag(flags, config, "rateLimit", "typescript", "rate-limit", config.rateLimit, "none");
|
|
7404
|
-
appendChangedGraphStringFlag(flags, config, "cms", "typescript", "cms", config.cms, "none");
|
|
7405
|
-
appendChangedGraphStringFlag(flags, config, "search", "typescript", "search", config.search, "none");
|
|
7406
|
-
appendChangedGraphStringFlag(flags, config, "fileStorage", "typescript", "file-storage", config.fileStorage, "none");
|
|
7407
|
-
}
|
|
7408
|
-
if (hasGraphPrimaryPart(config, "mobile")) {
|
|
7409
|
-
appendChangedOwnedGraphStringFlag(flags, config, "mobile", "navigation", "react-native", "mobile-navigation", config.mobileNavigation, "expo-router");
|
|
7410
|
-
appendChangedOwnedGraphStringFlag(flags, config, "mobile", "ui", "react-native", "mobile-ui", config.mobileUI, "none");
|
|
7411
|
-
appendChangedOwnedGraphStringFlag(flags, config, "mobile", "storage", "react-native", "mobile-storage", config.mobileStorage, "none");
|
|
7412
|
-
appendChangedOwnedGraphStringFlag(flags, config, "mobile", "testing", "react-native", "mobile-testing", config.mobileTesting, "none");
|
|
7413
|
-
appendChangedStringFlag(flags, "mobile-push", config.mobilePush, "none");
|
|
7414
|
-
appendChangedStringFlag(flags, "mobile-ota", config.mobileOTA, "none");
|
|
7415
|
-
appendChangedStringFlag(flags, "mobile-deep-linking", config.mobileDeepLinking, "none");
|
|
7416
|
-
}
|
|
7417
|
-
if (hasGraphPrimaryPart(config, "frontend", "rust")) appendChangedStringFlag(flags, "rust-frontend", config.rustFrontend, "none");
|
|
7418
|
-
if (hasGraphPrimaryPart(config, "backend", "rust")) {
|
|
7419
|
-
appendChangedOwnedGraphStringFlag(flags, config, "backend", "cli", "rust", "rust-cli", config.rustCli, "none");
|
|
7420
|
-
appendChangedOwnedGraphArrayFlag(flags, config, "backend", "libraries", "rust", "rust-libraries", config.rustLibraries, []);
|
|
7421
|
-
appendChangedOwnedGraphStringFlag(flags, config, "backend", "logging", "rust", "rust-logging", config.rustLogging, "tracing");
|
|
7422
|
-
appendChangedOwnedGraphStringFlag(flags, config, "backend", "errorHandling", "rust", "rust-error-handling", config.rustErrorHandling, "anyhow-thiserror");
|
|
7423
|
-
appendChangedOwnedGraphStringFlag(flags, config, "backend", "caching", "rust", "rust-caching", config.rustCaching, "none");
|
|
7424
|
-
}
|
|
7425
|
-
if (hasGraphPrimaryPart(config, "backend", "python")) {
|
|
7426
|
-
appendChangedOwnedGraphStringFlag(flags, config, "backend", "validation", "python", "python-validation", config.pythonValidation, "none");
|
|
7427
|
-
appendChangedOwnedGraphArrayFlag(flags, config, "backend", "ai", "python", "python-ai", config.pythonAi, []);
|
|
7428
|
-
appendChangedOwnedGraphStringFlag(flags, config, "backend", "jobQueue", "python", "python-task-queue", config.pythonTaskQueue, "none");
|
|
7429
|
-
appendChangedOwnedGraphStringFlag(flags, config, "backend", "api", "python", "python-graphql", config.pythonGraphql, "none");
|
|
7430
|
-
appendChangedOwnedGraphStringFlag(flags, config, "backend", "codeQuality", "python", "python-quality", config.pythonQuality, "none");
|
|
7431
|
-
}
|
|
7432
|
-
if (hasGraphPrimaryPart(config, "backend", "go")) {
|
|
7433
|
-
appendChangedOwnedGraphStringFlag(flags, config, "backend", "cli", "go", "go-cli", config.goCli, "none");
|
|
7434
|
-
appendChangedOwnedGraphStringFlag(flags, config, "backend", "logging", "go", "go-logging", config.goLogging, "none");
|
|
7435
|
-
}
|
|
7436
|
-
if (hasGraphPrimaryPart(config, "backend", "java")) {
|
|
7437
|
-
appendChangedOwnedGraphStringFlag(flags, config, "backend", "buildTool", "java", "java-build-tool", config.javaBuildTool, "maven");
|
|
7438
|
-
appendChangedOwnedGraphArrayFlag(flags, config, "backend", "libraries", "java", "java-libraries", config.javaLibraries, []);
|
|
7439
|
-
appendChangedOwnedGraphArrayFlag(flags, config, "backend", "testing", "java", "java-testing-libraries", config.javaTestingLibraries, ["junit5"]);
|
|
7440
|
-
}
|
|
7441
|
-
if (hasGraphPrimaryPart(config, "backend", "elixir")) {
|
|
7442
|
-
appendChangedGraphStringFlag(flags, config, "realtime", "elixir", "elixir-realtime", config.elixirRealtime, "channels");
|
|
7443
|
-
appendChangedGraphStringFlag(flags, config, "jobQueue", "elixir", "elixir-jobs", config.elixirJobs, "none");
|
|
7444
|
-
appendChangedGraphStringFlag(flags, config, "validation", "elixir", "elixir-validation", config.elixirValidation, "ecto-changesets");
|
|
7445
|
-
appendChangedOwnedGraphStringFlag(flags, config, "backend", "httpClient", "elixir", "elixir-http", config.elixirHttp, "req");
|
|
7446
|
-
appendChangedStringFlag(flags, "elixir-json", config.elixirJson, "jason");
|
|
7447
|
-
appendChangedGraphStringFlag(flags, config, "email", "elixir", "elixir-email", config.elixirEmail, "none");
|
|
7448
|
-
appendChangedGraphStringFlag(flags, config, "caching", "elixir", "elixir-caching", config.elixirCaching, "none");
|
|
7449
|
-
appendChangedGraphStringFlag(flags, config, "observability", "elixir", "elixir-observability", config.elixirObservability, "telemetry");
|
|
7450
|
-
appendChangedGraphStringFlag(flags, config, "testing", "elixir", "elixir-testing", config.elixirTesting, "ex_unit");
|
|
7451
|
-
appendChangedOwnedGraphStringFlag(flags, config, "backend", "codeQuality", "elixir", "elixir-quality", config.elixirQuality, "credo");
|
|
7452
|
-
appendChangedGraphStringFlag(flags, config, "deploy", "elixir", "elixir-deploy", config.elixirDeploy, "none");
|
|
7453
|
-
}
|
|
7454
|
-
}
|
|
7455
|
-
function appendSharedNonTypeScriptFlags(flags, config) {
|
|
7456
|
-
flags.push(`--email ${config.email}`);
|
|
7457
|
-
flags.push(`--observability ${config.observability}`);
|
|
7458
|
-
flags.push(`--caching ${config.caching}`);
|
|
7459
|
-
flags.push(`--search ${config.search}`);
|
|
7460
|
-
flags.push(formatArrayFlag("addons", config.addons));
|
|
7461
|
-
flags.push(formatArrayFlag("examples", config.examples));
|
|
7462
|
-
flags.push(`--db-setup ${config.dbSetup}`);
|
|
7463
|
-
flags.push(`--web-deploy ${config.webDeploy}`);
|
|
7464
|
-
flags.push(`--server-deploy ${config.serverDeploy}`);
|
|
7465
|
-
}
|
|
7466
|
-
function getTypeScriptFlags(config) {
|
|
7467
|
-
const flags = [];
|
|
7468
|
-
if (config.frontend && config.frontend.length > 0) flags.push(`--frontend ${config.frontend.join(" ")}`);
|
|
7469
|
-
else flags.push("--frontend none");
|
|
7470
|
-
appendAstroIntegrationFlag(flags, config);
|
|
7471
|
-
flags.push(`--backend ${config.backend}`);
|
|
7472
|
-
flags.push(`--runtime ${config.runtime}`);
|
|
7473
|
-
flags.push(`--database ${config.database}`);
|
|
7474
|
-
flags.push(`--orm ${config.orm}`);
|
|
7475
|
-
flags.push(`--api ${config.api}`);
|
|
7476
|
-
flags.push(`--auth ${config.auth}`);
|
|
7477
|
-
flags.push(`--payments ${config.payments}`);
|
|
7478
|
-
flags.push(`--email ${config.email}`);
|
|
7479
|
-
flags.push(`--file-upload ${config.fileUpload}`);
|
|
7480
|
-
flags.push(`--effect ${config.effect}`);
|
|
7481
|
-
flags.push(`--css-framework ${config.cssFramework}`);
|
|
7482
|
-
flags.push(`--ui-library ${config.uiLibrary}`);
|
|
7483
|
-
if (config.uiLibrary === "shadcn-ui") {
|
|
7484
|
-
flags.push(`--shadcn-base ${config.shadcnBase}`);
|
|
7485
|
-
flags.push(`--shadcn-style ${config.shadcnStyle}`);
|
|
7486
|
-
flags.push(`--shadcn-icon-library ${config.shadcnIconLibrary}`);
|
|
7487
|
-
flags.push(`--shadcn-color-theme ${config.shadcnColorTheme}`);
|
|
7488
|
-
flags.push(`--shadcn-base-color ${config.shadcnBaseColor}`);
|
|
7489
|
-
flags.push(`--shadcn-font ${config.shadcnFont}`);
|
|
7490
|
-
flags.push(`--shadcn-radius ${config.shadcnRadius}`);
|
|
7491
|
-
}
|
|
7492
|
-
flags.push(`--ai ${config.ai}`);
|
|
7493
|
-
flags.push(`--state-management ${config.stateManagement}`);
|
|
7494
|
-
flags.push(`--forms ${config.forms}`);
|
|
7495
|
-
flags.push(`--validation ${config.validation}`);
|
|
7496
|
-
flags.push(`--testing ${config.testing}`);
|
|
7497
|
-
flags.push(`--animation ${config.animation}`);
|
|
7498
|
-
flags.push(`--realtime ${config.realtime}`);
|
|
7499
|
-
flags.push(`--job-queue ${config.jobQueue}`);
|
|
7500
|
-
flags.push(`--logging ${config.logging}`);
|
|
7501
|
-
flags.push(`--observability ${config.observability}`);
|
|
7502
|
-
flags.push(`--feature-flags ${config.featureFlags}`);
|
|
7503
|
-
flags.push(`--caching ${config.caching}`);
|
|
7504
|
-
flags.push(`--rate-limit ${config.rateLimit}`);
|
|
7505
|
-
flags.push(`--i18n ${config.i18n}`);
|
|
7506
|
-
flags.push(`--cms ${config.cms}`);
|
|
7507
|
-
flags.push(`--search ${config.search}`);
|
|
7508
|
-
flags.push(`--file-storage ${config.fileStorage}`);
|
|
7509
|
-
flags.push(`--mobile-navigation ${config.mobileNavigation}`);
|
|
7510
|
-
flags.push(`--mobile-ui ${config.mobileUI}`);
|
|
7511
|
-
flags.push(`--mobile-storage ${config.mobileStorage}`);
|
|
7512
|
-
flags.push(`--mobile-testing ${config.mobileTesting}`);
|
|
7513
|
-
flags.push(`--mobile-push ${config.mobilePush}`);
|
|
7514
|
-
flags.push(`--mobile-ota ${config.mobileOTA}`);
|
|
7515
|
-
flags.push(`--mobile-deep-linking ${config.mobileDeepLinking}`);
|
|
7516
|
-
if (config.addons && config.addons.length > 0) flags.push(`--addons ${config.addons.join(" ")}`);
|
|
7517
|
-
else flags.push("--addons none");
|
|
7518
|
-
if (config.examples && config.examples.length > 0) flags.push(`--examples ${config.examples.join(" ")}`);
|
|
7519
|
-
else flags.push("--examples none");
|
|
7520
|
-
flags.push(`--db-setup ${config.dbSetup}`);
|
|
7521
|
-
flags.push(`--web-deploy ${config.webDeploy}`);
|
|
7522
|
-
flags.push(`--server-deploy ${config.serverDeploy}`);
|
|
7523
|
-
appendCommonFlags(flags, config);
|
|
7524
|
-
return flags;
|
|
7525
|
-
}
|
|
7526
|
-
function getReactNativeFlags(config) {
|
|
7527
|
-
const flags = ["--ecosystem react-native"];
|
|
7528
|
-
if (config.frontend && config.frontend.length > 0) flags.push(`--frontend ${config.frontend.join(" ")}`);
|
|
7529
|
-
else flags.push("--frontend native-bare");
|
|
7530
|
-
flags.push(`--auth ${config.auth}`);
|
|
7531
|
-
flags.push(`--mobile-navigation ${config.mobileNavigation}`);
|
|
7532
|
-
flags.push(`--mobile-ui ${config.mobileUI}`);
|
|
7533
|
-
flags.push(`--mobile-storage ${config.mobileStorage}`);
|
|
7534
|
-
flags.push(`--mobile-testing ${config.mobileTesting}`);
|
|
7535
|
-
flags.push(`--mobile-push ${config.mobilePush}`);
|
|
7536
|
-
flags.push(`--mobile-ota ${config.mobileOTA}`);
|
|
7537
|
-
flags.push(`--mobile-deep-linking ${config.mobileDeepLinking}`);
|
|
7538
|
-
appendCommonFlags(flags, config);
|
|
7539
|
-
return flags;
|
|
7540
|
-
}
|
|
7541
|
-
function getRustFlags(config) {
|
|
7542
|
-
const flags = ["--ecosystem rust"];
|
|
7543
|
-
flags.push(`--rust-web-framework ${config.rustWebFramework}`);
|
|
7544
|
-
flags.push(`--rust-frontend ${config.rustFrontend}`);
|
|
7545
|
-
flags.push(`--rust-orm ${config.rustOrm}`);
|
|
7546
|
-
flags.push(`--rust-api ${config.rustApi}`);
|
|
7547
|
-
flags.push(`--rust-cli ${config.rustCli}`);
|
|
7548
|
-
flags.push(formatArrayFlag("rust-libraries", config.rustLibraries));
|
|
7549
|
-
flags.push(`--rust-logging ${config.rustLogging}`);
|
|
7550
|
-
flags.push(`--rust-error-handling ${config.rustErrorHandling}`);
|
|
7551
|
-
flags.push(`--rust-caching ${config.rustCaching}`);
|
|
7552
|
-
flags.push(`--rust-auth ${config.rustAuth}`);
|
|
7553
|
-
flags.push(`--rust-realtime ${config.rustRealtime}`);
|
|
7554
|
-
flags.push(`--rust-message-queue ${config.rustMessageQueue}`);
|
|
7555
|
-
flags.push(`--rust-observability ${config.rustObservability}`);
|
|
7556
|
-
flags.push(`--rust-templating ${config.rustTemplating}`);
|
|
7557
|
-
appendSharedNonTypeScriptFlags(flags, config);
|
|
7558
|
-
appendCommonFlags(flags, config);
|
|
7559
|
-
return flags;
|
|
7560
|
-
}
|
|
7561
|
-
function getPythonFlags(config) {
|
|
7562
|
-
const flags = ["--ecosystem python"];
|
|
7563
|
-
flags.push(`--python-web-framework ${config.pythonWebFramework}`);
|
|
7564
|
-
flags.push(`--python-orm ${config.pythonOrm}`);
|
|
7565
|
-
flags.push(`--python-validation ${config.pythonValidation}`);
|
|
7566
|
-
flags.push(formatArrayFlag("python-ai", config.pythonAi));
|
|
7567
|
-
flags.push(`--python-auth ${config.pythonAuth}`);
|
|
7568
|
-
flags.push(`--python-api ${config.pythonApi}`);
|
|
7569
|
-
flags.push(`--python-task-queue ${config.pythonTaskQueue}`);
|
|
7570
|
-
flags.push(`--python-graphql ${config.pythonGraphql}`);
|
|
7571
|
-
flags.push(`--python-quality ${config.pythonQuality}`);
|
|
7572
|
-
flags.push(formatArrayFlag("python-testing", config.pythonTesting));
|
|
7573
|
-
flags.push(`--python-caching ${config.pythonCaching}`);
|
|
7574
|
-
flags.push(`--python-realtime ${config.pythonRealtime}`);
|
|
7575
|
-
flags.push(`--python-observability ${config.pythonObservability}`);
|
|
7576
|
-
flags.push(formatArrayFlag("python-cli", config.pythonCli));
|
|
7577
|
-
appendSharedNonTypeScriptFlags(flags, config);
|
|
7578
|
-
appendCommonFlags(flags, config);
|
|
7579
|
-
return flags;
|
|
7580
|
-
}
|
|
7581
|
-
function getGoFlags(config) {
|
|
7582
|
-
const flags = ["--ecosystem go"];
|
|
7583
|
-
flags.push(`--go-web-framework ${config.goWebFramework}`);
|
|
7584
|
-
flags.push(`--go-orm ${config.goOrm}`);
|
|
7585
|
-
flags.push(`--go-api ${config.goApi}`);
|
|
7586
|
-
flags.push(`--go-cli ${config.goCli}`);
|
|
7587
|
-
flags.push(`--go-logging ${config.goLogging}`);
|
|
7588
|
-
flags.push(`--go-auth ${config.goAuth}`);
|
|
7589
|
-
flags.push(formatArrayFlag("go-testing", config.goTesting));
|
|
7590
|
-
flags.push(`--go-realtime ${config.goRealtime}`);
|
|
7591
|
-
flags.push(`--go-message-queue ${config.goMessageQueue}`);
|
|
7592
|
-
flags.push(`--go-caching ${config.goCaching}`);
|
|
7593
|
-
flags.push(`--go-config ${config.goConfig}`);
|
|
7594
|
-
flags.push(`--go-observability ${config.goObservability}`);
|
|
7595
|
-
flags.push(`--auth ${config.auth}`);
|
|
7596
|
-
appendSharedNonTypeScriptFlags(flags, config);
|
|
7597
|
-
appendCommonFlags(flags, config);
|
|
7598
|
-
return flags;
|
|
7599
|
-
}
|
|
7600
|
-
function getJavaFlags(config) {
|
|
7601
|
-
const flags = ["--ecosystem java"];
|
|
7602
|
-
flags.push(`--java-web-framework ${config.javaWebFramework}`);
|
|
7603
|
-
flags.push(`--java-build-tool ${config.javaBuildTool}`);
|
|
7604
|
-
flags.push(`--java-orm ${config.javaOrm}`);
|
|
7605
|
-
flags.push(`--java-auth ${config.javaAuth}`);
|
|
7606
|
-
flags.push(`--java-api ${config.javaApi}`);
|
|
7607
|
-
flags.push(`--java-logging ${config.javaLogging}`);
|
|
7608
|
-
flags.push(formatArrayFlag("java-libraries", config.javaLibraries));
|
|
7609
|
-
flags.push(formatArrayFlag("java-testing-libraries", config.javaTestingLibraries));
|
|
7610
|
-
appendSharedNonTypeScriptFlags(flags, config);
|
|
7611
|
-
appendCommonFlags(flags, config);
|
|
7612
|
-
return flags;
|
|
7613
|
-
}
|
|
7614
|
-
function getDotnetFlags(config) {
|
|
7615
|
-
const flags = ["--ecosystem dotnet"];
|
|
7616
|
-
flags.push(`--dotnet-web-framework ${config.dotnetWebFramework}`);
|
|
7617
|
-
flags.push(`--dotnet-orm ${config.dotnetOrm}`);
|
|
7618
|
-
flags.push(`--dotnet-auth ${config.dotnetAuth}`);
|
|
7619
|
-
flags.push(`--dotnet-api ${config.dotnetApi}`);
|
|
7620
|
-
flags.push(formatArrayFlag("dotnet-testing", config.dotnetTesting));
|
|
7621
|
-
flags.push(`--dotnet-job-queue ${config.dotnetJobQueue}`);
|
|
7622
|
-
flags.push(`--dotnet-realtime ${config.dotnetRealtime}`);
|
|
7623
|
-
flags.push(formatArrayFlag("dotnet-observability", config.dotnetObservability));
|
|
7624
|
-
flags.push(`--dotnet-validation ${config.dotnetValidation}`);
|
|
7625
|
-
flags.push(`--dotnet-caching ${config.dotnetCaching}`);
|
|
7626
|
-
flags.push(`--dotnet-deploy ${config.dotnetDeploy}`);
|
|
7627
|
-
appendCommonFlags(flags, config);
|
|
7628
|
-
return flags;
|
|
7629
|
-
}
|
|
7630
|
-
function getElixirFlags(config) {
|
|
7631
|
-
const flags = ["--ecosystem elixir"];
|
|
7632
|
-
flags.push(`--elixir-web-framework ${config.elixirWebFramework}`);
|
|
7633
|
-
flags.push(`--elixir-orm ${config.elixirOrm}`);
|
|
7634
|
-
flags.push(`--elixir-auth ${config.elixirAuth}`);
|
|
7635
|
-
flags.push(`--elixir-api ${config.elixirApi}`);
|
|
7636
|
-
flags.push(`--elixir-realtime ${config.elixirRealtime}`);
|
|
7637
|
-
flags.push(`--elixir-jobs ${config.elixirJobs}`);
|
|
7638
|
-
flags.push(`--elixir-validation ${config.elixirValidation}`);
|
|
7639
|
-
flags.push(`--elixir-http ${config.elixirHttp}`);
|
|
7640
|
-
flags.push(`--elixir-json ${config.elixirJson}`);
|
|
7641
|
-
flags.push(`--elixir-email ${config.elixirEmail}`);
|
|
7642
|
-
flags.push(`--elixir-caching ${config.elixirCaching}`);
|
|
7643
|
-
flags.push(`--elixir-observability ${config.elixirObservability}`);
|
|
7644
|
-
flags.push(`--elixir-testing ${config.elixirTesting}`);
|
|
7645
|
-
flags.push(`--elixir-quality ${config.elixirQuality}`);
|
|
7646
|
-
flags.push(`--elixir-deploy ${config.elixirDeploy}`);
|
|
7647
|
-
flags.push(formatArrayFlag("elixir-libraries", config.elixirLibraries));
|
|
7648
|
-
appendCommonFlags(flags, config);
|
|
7649
|
-
return flags;
|
|
7650
|
-
}
|
|
7651
|
-
function generateReproducibleCommand(config) {
|
|
7652
|
-
let flags;
|
|
7653
|
-
if (config.stackParts && config.stackParts.length > 0) {
|
|
7654
|
-
flags = config.stackParts.filter((part) => part.source !== "provided").map((part) => `--part ${(0, types_exports.formatStackPartSpec)(part, config.stackParts ?? [])}`);
|
|
7655
|
-
appendGraphExtraFlags(flags, config);
|
|
7656
|
-
appendCommonFlags(flags, config);
|
|
7657
|
-
return `${getBaseCommand(config.packageManager)}${config.relativePath ? ` ${config.relativePath}` : ""} ${flags.join(" ")}`;
|
|
7658
|
-
}
|
|
7659
|
-
switch (config.ecosystem) {
|
|
7660
|
-
case "react-native":
|
|
7661
|
-
flags = getReactNativeFlags(config);
|
|
7662
|
-
break;
|
|
7663
|
-
case "rust":
|
|
7664
|
-
flags = getRustFlags(config);
|
|
7665
|
-
break;
|
|
7666
|
-
case "python":
|
|
7667
|
-
flags = getPythonFlags(config);
|
|
7668
|
-
break;
|
|
7669
|
-
case "go":
|
|
7670
|
-
flags = getGoFlags(config);
|
|
7671
|
-
break;
|
|
7672
|
-
case "java":
|
|
7673
|
-
flags = getJavaFlags(config);
|
|
7674
|
-
break;
|
|
7675
|
-
case "dotnet":
|
|
7676
|
-
flags = getDotnetFlags(config);
|
|
7677
|
-
break;
|
|
7678
|
-
case "elixir":
|
|
7679
|
-
flags = getElixirFlags(config);
|
|
7680
|
-
break;
|
|
7681
|
-
case "typescript":
|
|
7682
|
-
default:
|
|
7683
|
-
flags = getTypeScriptFlags(config);
|
|
7684
|
-
break;
|
|
7685
|
-
}
|
|
7686
|
-
return `${getBaseCommand(config.packageManager)}${config.relativePath ? ` ${config.relativePath}` : ""} ${flags.join(" ")}`;
|
|
7687
|
-
}
|
|
7688
|
-
|
|
7689
|
-
//#endregion
|
|
7690
|
-
//#region src/utils/generated-checks.ts
|
|
7691
|
-
function getGraphTarget(config) {
|
|
7692
|
-
const backend = getPrimaryGraphPart(config, "backend");
|
|
7693
|
-
if (!backend || backend.ecosystem === "typescript" || backend.ecosystem === "react-native" || backend.ecosystem === "universal") return null;
|
|
7694
|
-
return {
|
|
7695
|
-
ecosystem: backend.ecosystem,
|
|
7696
|
-
projectDir: path.join(config.projectDir, backend.targetPath ?? "apps/server")
|
|
7697
|
-
};
|
|
7698
|
-
}
|
|
7699
|
-
function getSingleEcosystemTarget(config) {
|
|
7700
|
-
if (config.ecosystem === "typescript" || config.ecosystem === "react-native" || config.ecosystem === "java") return null;
|
|
7701
|
-
return {
|
|
7702
|
-
ecosystem: config.ecosystem,
|
|
7703
|
-
projectDir: config.projectDir
|
|
7704
|
-
};
|
|
7705
|
-
}
|
|
7706
|
-
async function runCommand(cwd, command, args) {
|
|
7707
|
-
await $({
|
|
7708
|
-
cwd,
|
|
7709
|
-
stdout: "inherit",
|
|
7710
|
-
stderr: "inherit"
|
|
7711
|
-
})`${command} ${args}`;
|
|
7712
|
-
}
|
|
7713
|
-
async function verifyTarget(target) {
|
|
7714
|
-
const s = spinner();
|
|
7715
|
-
const cwd = target.projectDir;
|
|
7716
|
-
switch (target.ecosystem) {
|
|
7717
|
-
case "go":
|
|
7718
|
-
s.start("Verifying generated Go server...");
|
|
7719
|
-
await runCommand(cwd, "go", ["mod", "tidy"]);
|
|
7720
|
-
await runCommand(cwd, "go", ["test", "./..."]);
|
|
7721
|
-
s.stop("Generated Go server checks passed");
|
|
7722
|
-
return;
|
|
7723
|
-
case "rust":
|
|
7724
|
-
s.start("Verifying generated Rust server...");
|
|
7725
|
-
await runCommand(cwd, "cargo", ["check"]);
|
|
7726
|
-
s.stop("Generated Rust server checks passed");
|
|
7727
|
-
return;
|
|
7728
|
-
case "python":
|
|
7729
|
-
s.start("Verifying generated Python server...");
|
|
7730
|
-
await runCommand(cwd, "uv", ["sync"]);
|
|
7731
|
-
await runCommand(cwd, "uv", [
|
|
7732
|
-
"run",
|
|
7733
|
-
"ruff",
|
|
7734
|
-
"check",
|
|
7735
|
-
"."
|
|
7736
|
-
]);
|
|
7737
|
-
s.stop("Generated Python server checks passed");
|
|
7738
|
-
return;
|
|
7739
|
-
case "elixir":
|
|
7740
|
-
if (!await commandExists("mix")) {
|
|
7741
|
-
log.warn(pc.yellow("Skipping Elixir verification because mix is not on PATH"));
|
|
7742
|
-
return;
|
|
7743
|
-
}
|
|
7744
|
-
s.start("Verifying generated Elixir server...");
|
|
7745
|
-
await runCommand(cwd, "mix", ["deps.get"]);
|
|
7746
|
-
await runCommand(cwd, "mix", ["compile"]);
|
|
7747
|
-
s.stop("Generated Elixir server checks passed");
|
|
7748
|
-
return;
|
|
7749
|
-
default: log.warn(pc.yellow(`No generated checks are configured for ${target.ecosystem}`));
|
|
7750
|
-
}
|
|
7751
|
-
}
|
|
7752
|
-
async function runGeneratedChecks(config) {
|
|
7753
|
-
const target = getGraphTarget(config) ?? getSingleEcosystemTarget(config);
|
|
7754
|
-
if (!target) {
|
|
7755
|
-
log.warn(pc.yellow("No generated checks are configured for this stack"));
|
|
7756
|
-
return;
|
|
7757
|
-
}
|
|
7758
|
-
await verifyTarget(target);
|
|
7759
|
-
}
|
|
7760
|
-
|
|
7761
7635
|
//#endregion
|
|
7762
7636
|
//#region src/utils/preflight-display.ts
|
|
7763
7637
|
function displayPreflightWarnings({ warnings }) {
|
|
@@ -7861,87 +7735,6 @@ async function setupProjectDirectory(finalPathInput, shouldClearDirectory) {
|
|
|
7861
7735
|
};
|
|
7862
7736
|
}
|
|
7863
7737
|
|
|
7864
|
-
//#endregion
|
|
7865
|
-
//#region src/utils/templates.ts
|
|
7866
|
-
const TEMPLATE_PRESETS = {
|
|
7867
|
-
mern: {
|
|
7868
|
-
database: "mongodb",
|
|
7869
|
-
orm: "mongoose",
|
|
7870
|
-
backend: "express",
|
|
7871
|
-
runtime: "node",
|
|
7872
|
-
frontend: ["react-router"],
|
|
7873
|
-
api: "orpc",
|
|
7874
|
-
auth: "better-auth",
|
|
7875
|
-
payments: "none",
|
|
7876
|
-
addons: ["turborepo"],
|
|
7877
|
-
examples: ["none"],
|
|
7878
|
-
dbSetup: "mongodb-atlas",
|
|
7879
|
-
webDeploy: "none",
|
|
7880
|
-
serverDeploy: "none"
|
|
7881
|
-
},
|
|
7882
|
-
pern: {
|
|
7883
|
-
database: "postgres",
|
|
7884
|
-
orm: "drizzle",
|
|
7885
|
-
backend: "express",
|
|
7886
|
-
runtime: "node",
|
|
7887
|
-
frontend: ["tanstack-router"],
|
|
7888
|
-
api: "trpc",
|
|
7889
|
-
auth: "better-auth",
|
|
7890
|
-
payments: "none",
|
|
7891
|
-
addons: ["turborepo"],
|
|
7892
|
-
examples: ["none"],
|
|
7893
|
-
dbSetup: "none",
|
|
7894
|
-
webDeploy: "none",
|
|
7895
|
-
serverDeploy: "none"
|
|
7896
|
-
},
|
|
7897
|
-
t3: {
|
|
7898
|
-
database: "postgres",
|
|
7899
|
-
orm: "prisma",
|
|
7900
|
-
backend: "self",
|
|
7901
|
-
runtime: "none",
|
|
7902
|
-
frontend: ["next"],
|
|
7903
|
-
api: "trpc",
|
|
7904
|
-
auth: "better-auth",
|
|
7905
|
-
payments: "none",
|
|
7906
|
-
addons: ["biome", "turborepo"],
|
|
7907
|
-
examples: ["none"],
|
|
7908
|
-
dbSetup: "none",
|
|
7909
|
-
webDeploy: "none",
|
|
7910
|
-
serverDeploy: "none"
|
|
7911
|
-
},
|
|
7912
|
-
uniwind: {
|
|
7913
|
-
database: "none",
|
|
7914
|
-
orm: "none",
|
|
7915
|
-
backend: "none",
|
|
7916
|
-
runtime: "none",
|
|
7917
|
-
frontend: ["native-uniwind"],
|
|
7918
|
-
api: "none",
|
|
7919
|
-
auth: "none",
|
|
7920
|
-
payments: "none",
|
|
7921
|
-
addons: ["none"],
|
|
7922
|
-
examples: ["none"],
|
|
7923
|
-
dbSetup: "none",
|
|
7924
|
-
webDeploy: "none",
|
|
7925
|
-
serverDeploy: "none"
|
|
7926
|
-
},
|
|
7927
|
-
none: null
|
|
7928
|
-
};
|
|
7929
|
-
function getTemplateConfig(template) {
|
|
7930
|
-
if (template === "none" || !template) return null;
|
|
7931
|
-
const config = TEMPLATE_PRESETS[template];
|
|
7932
|
-
if (!config) throw new Error(`Unknown template: ${template}`);
|
|
7933
|
-
return config;
|
|
7934
|
-
}
|
|
7935
|
-
function getTemplateDescription(template) {
|
|
7936
|
-
return {
|
|
7937
|
-
mern: "MongoDB + Express + React + Node.js - Classic MERN stack",
|
|
7938
|
-
pern: "PostgreSQL + Express + React + Node.js - Popular PERN stack",
|
|
7939
|
-
t3: "T3 Stack - Next.js + tRPC + Prisma + PostgreSQL + Better Auth",
|
|
7940
|
-
uniwind: "Expo + Uniwind native app with no backend services",
|
|
7941
|
-
none: "No template - Full customization"
|
|
7942
|
-
}[template] || "";
|
|
7943
|
-
}
|
|
7944
|
-
|
|
7945
7738
|
//#endregion
|
|
7946
7739
|
//#region src/utils/config-processing.ts
|
|
7947
7740
|
function deriveProjectName(projectName, projectDirectory) {
|
|
@@ -8445,16 +8238,17 @@ function validateFrontendConstraints(config, providedFlags) {
|
|
|
8445
8238
|
validateWebDeployFrontendTemplates(config.webDeploy, frontend);
|
|
8446
8239
|
}
|
|
8447
8240
|
function validateApiConstraints(config, _options) {
|
|
8448
|
-
if (config.api !== "openapi") return;
|
|
8241
|
+
if (config.api !== "openapi" && config.api !== "apollo-server") return;
|
|
8242
|
+
const apiDisplayName = config.api === "apollo-server" ? "Apollo Server" : "OpenAPI";
|
|
8449
8243
|
const frontend = config.frontend ?? [];
|
|
8450
8244
|
if (frontend.some((item) => [
|
|
8451
8245
|
"native-bare",
|
|
8452
8246
|
"native-uniwind",
|
|
8453
8247
|
"native-unistyles"
|
|
8454
8248
|
].includes(item))) incompatibilityError({
|
|
8455
|
-
message:
|
|
8249
|
+
message: `${apiDisplayName} is currently available for web frontends, not React Native.`,
|
|
8456
8250
|
provided: {
|
|
8457
|
-
api:
|
|
8251
|
+
api: config.api,
|
|
8458
8252
|
frontend
|
|
8459
8253
|
},
|
|
8460
8254
|
suggestions: [
|
|
@@ -8469,9 +8263,9 @@ function validateApiConstraints(config, _options) {
|
|
|
8469
8263
|
"fastify",
|
|
8470
8264
|
"elysia"
|
|
8471
8265
|
].includes(config.backend)) incompatibilityError({
|
|
8472
|
-
message:
|
|
8266
|
+
message: `${apiDisplayName} currently supports Hono, Express, Fastify, and Elysia backends.`,
|
|
8473
8267
|
provided: {
|
|
8474
|
-
api:
|
|
8268
|
+
api: config.api,
|
|
8475
8269
|
backend: config.backend ?? "none"
|
|
8476
8270
|
},
|
|
8477
8271
|
suggestions: [
|
|
@@ -8740,10 +8534,11 @@ function validateRateLimitConstraints(config) {
|
|
|
8740
8534
|
}
|
|
8741
8535
|
function validateSearchConstraints(config) {
|
|
8742
8536
|
if (!config.search || config.search === "none") return;
|
|
8743
|
-
|
|
8537
|
+
const ecosystem = config.ecosystem ?? "typescript";
|
|
8538
|
+
if (ecosystem !== "typescript" && config.search !== "meilisearch") incompatibilityError({
|
|
8744
8539
|
message: "Only Meilisearch search is available for non-TypeScript ecosystems.",
|
|
8745
8540
|
provided: {
|
|
8746
|
-
ecosystem
|
|
8541
|
+
ecosystem,
|
|
8747
8542
|
search: config.search
|
|
8748
8543
|
},
|
|
8749
8544
|
suggestions: ["Use --search meilisearch", "Use --search none"]
|
|
@@ -10084,10 +9879,10 @@ async function setupDatabase(config, cliInput) {
|
|
|
10084
9879
|
const serverDbDir = path.join(projectDir, "apps/server/src/db");
|
|
10085
9880
|
if (await fs.pathExists(serverDbDir)) await fs.remove(serverDbDir);
|
|
10086
9881
|
}
|
|
10087
|
-
return;
|
|
9882
|
+
return null;
|
|
10088
9883
|
}
|
|
10089
9884
|
const dbPackageDir = path.join(projectDir, "packages/db");
|
|
10090
|
-
if (!await fs.pathExists(dbPackageDir)) return;
|
|
9885
|
+
if (!await fs.pathExists(dbPackageDir)) return null;
|
|
10091
9886
|
try {
|
|
10092
9887
|
if (dbSetup === "docker") await setupDockerCompose(config);
|
|
10093
9888
|
else if (database === "sqlite" && dbSetup === "turso") await setupTurso(config, cliInput);
|
|
@@ -10100,8 +9895,18 @@ async function setupDatabase(config, cliInput) {
|
|
|
10100
9895
|
} else if (database === "mysql" && dbSetup === "planetscale") await setupPlanetScale(config);
|
|
10101
9896
|
else if (database === "mongodb" && dbSetup === "mongodb-atlas") await setupMongoDBAtlas(config, cliInput);
|
|
10102
9897
|
else if (database === "redis" && dbSetup === "upstash") await setupUpstash(config, cliInput);
|
|
9898
|
+
return {
|
|
9899
|
+
step: "Database setup",
|
|
9900
|
+
success: true
|
|
9901
|
+
};
|
|
10103
9902
|
} catch (error) {
|
|
10104
|
-
|
|
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
|
+
};
|
|
10105
9910
|
}
|
|
10106
9911
|
}
|
|
10107
9912
|
|
|
@@ -10701,14 +10506,14 @@ function displayJavaInstructions(config) {
|
|
|
10701
10506
|
}
|
|
10702
10507
|
const effectiveJavaTestingLibraries = javaBuildTool === "none" ? [] : javaTestingLibraries.filter((library) => library !== "none");
|
|
10703
10508
|
const buildToolCommand = javaBuildTool === "none" ? null : javaBuildTool === "gradle" ? process.platform === "win32" ? "gradlew.bat" : "./gradlew" : process.platform === "win32" ? "mvnw.cmd" : "./mvnw";
|
|
10704
|
-
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;
|
|
10705
10510
|
const packageCommand = buildToolCommand ? javaBuildTool === "gradle" ? `${buildToolCommand} build` : `${buildToolCommand} package` : null;
|
|
10706
10511
|
const sourceCompileCommand = buildToolCommand ? null : `javac -d out ${getJavaMainSourcePath(projectName)}`;
|
|
10707
10512
|
const sourceRunCommand = buildToolCommand ? null : `java -cp out ${getJavaMainClass(projectName)}`;
|
|
10708
10513
|
let output = `${pc.bold("Next steps")}\n${pc.cyan("1.")} ${cdCmd}\n`;
|
|
10709
10514
|
let stepCounter = 2;
|
|
10710
10515
|
if (!depsInstalled && buildToolCommand && effectiveJavaTestingLibraries.length > 0) output += `${pc.cyan(`${stepCounter++}.`)} ${buildToolCommand} test\n`;
|
|
10711
|
-
if (runCommand
|
|
10516
|
+
if (runCommand) output += `${pc.cyan(`${stepCounter++}.`)} ${runCommand}\n`;
|
|
10712
10517
|
else if (sourceCompileCommand && sourceRunCommand) {
|
|
10713
10518
|
output += `${pc.cyan(`${stepCounter++}.`)} ${sourceCompileCommand}\n`;
|
|
10714
10519
|
output += `${pc.cyan(`${stepCounter++}.`)} ${sourceRunCommand}\n`;
|
|
@@ -10756,9 +10561,9 @@ function displayJavaInstructions(config) {
|
|
|
10756
10561
|
output += `${pc.cyan("•")} Testing: ${testingList}\n`;
|
|
10757
10562
|
}
|
|
10758
10563
|
output += `\n${pc.bold("Common Java commands:")}\n`;
|
|
10759
|
-
if (buildToolCommand && runCommand
|
|
10564
|
+
if (buildToolCommand && runCommand && packageCommand) {
|
|
10760
10565
|
if (effectiveJavaTestingLibraries.length > 0) output += `${pc.cyan("•")} Test: ${buildToolCommand} test\n`;
|
|
10761
|
-
output += `${pc.cyan("•")} Run: ${runCommand
|
|
10566
|
+
output += `${pc.cyan("•")} Run: ${runCommand}\n`;
|
|
10762
10567
|
output += `${pc.cyan("•")} Package: ${packageCommand}\n`;
|
|
10763
10568
|
} else if (sourceCompileCommand && sourceRunCommand) {
|
|
10764
10569
|
output += `${pc.cyan("•")} Compile: ${sourceCompileCommand}\n`;
|
|
@@ -10776,14 +10581,14 @@ function displayJavaInstructions(config) {
|
|
|
10776
10581
|
function displayPythonInstructions(config) {
|
|
10777
10582
|
const { relativePath, depsInstalled, pythonWebFramework, pythonOrm, pythonValidation, pythonAi, pythonApi, pythonTaskQueue, pythonQuality } = config;
|
|
10778
10583
|
const cdCmd = `cd ${relativePath}`;
|
|
10779
|
-
let runCommand
|
|
10780
|
-
if (pythonWebFramework === "django") runCommand
|
|
10781
|
-
else if (pythonWebFramework === "flask") runCommand
|
|
10782
|
-
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";
|
|
10783
10588
|
let output = `${pc.bold("Next steps")}\n${pc.cyan("1.")} ${cdCmd}\n`;
|
|
10784
10589
|
let stepCounter = 2;
|
|
10785
10590
|
if (!depsInstalled) output += `${pc.cyan(`${stepCounter++}.`)} uv sync\n`;
|
|
10786
|
-
output += `${pc.cyan(`${stepCounter++}.`)} ${runCommand
|
|
10591
|
+
output += `${pc.cyan(`${stepCounter++}.`)} ${runCommand}\n`;
|
|
10787
10592
|
output += `\n${pc.bold("Your Python project includes:")}\n`;
|
|
10788
10593
|
if (pythonWebFramework && pythonWebFramework !== "none") output += `${pc.cyan("•")} Web Framework: ${{
|
|
10789
10594
|
fastapi: "FastAPI",
|
|
@@ -10821,7 +10626,7 @@ function displayPythonInstructions(config) {
|
|
|
10821
10626
|
}[pythonQuality] || pythonQuality}\n`;
|
|
10822
10627
|
output += `\n${pc.bold("Common Python commands:")}\n`;
|
|
10823
10628
|
output += `${pc.cyan("•")} Install: uv sync\n`;
|
|
10824
|
-
output += `${pc.cyan("•")} Run: ${runCommand
|
|
10629
|
+
output += `${pc.cyan("•")} Run: ${runCommand}\n`;
|
|
10825
10630
|
output += `${pc.cyan("•")} Test: uv run pytest\n`;
|
|
10826
10631
|
if (pythonQuality === "ruff") {
|
|
10827
10632
|
output += `${pc.cyan("•")} Format: uv run ruff format .\n`;
|
|
@@ -10871,6 +10676,8 @@ function displayElixirInstructions(config) {
|
|
|
10871
10676
|
async function createProject(options, cliInput = {}) {
|
|
10872
10677
|
const projectDir = options.projectDir;
|
|
10873
10678
|
const isConvex = options.backend === "convex";
|
|
10679
|
+
const setupFailures = [];
|
|
10680
|
+
const dirHadContentBefore = await fs.pathExists(projectDir) && (await fs.readdir(projectDir)).length > 0;
|
|
10874
10681
|
try {
|
|
10875
10682
|
await fs.ensureDir(projectDir);
|
|
10876
10683
|
const [{ generateVirtualProject, EMBEDDED_TEMPLATES }, { writeTreeToFilesystem }] = await Promise.all([import("@better-fullstack/template-generator"), import("@better-fullstack/template-generator/fs-writer")]);
|
|
@@ -10882,29 +10689,53 @@ async function createProject(options, cliInput = {}) {
|
|
|
10882
10689
|
await writeTreeToFilesystem(result.tree, projectDir);
|
|
10883
10690
|
await setPackageManagerVersion(projectDir, options.packageManager);
|
|
10884
10691
|
await ensurePackageManagerProjectFiles(projectDir, options.packageManager);
|
|
10885
|
-
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
|
+
}
|
|
10886
10696
|
if (options.addons.length > 0 && options.addons[0] !== "none") await setupAddons(options);
|
|
10887
10697
|
await applyDependencyVersionChannel(projectDir, options.versionChannel);
|
|
10888
10698
|
await writeBtsConfig(options);
|
|
10889
10699
|
await formatProject(projectDir);
|
|
10890
10700
|
if (!isSilent()) log.success("Project template successfully scaffolded!");
|
|
10891
|
-
if (options.install && (options.ecosystem === "typescript" || options.ecosystem === "react-native"))
|
|
10892
|
-
|
|
10893
|
-
|
|
10894
|
-
|
|
10895
|
-
|
|
10896
|
-
|
|
10897
|
-
|
|
10898
|
-
if (options.install && options.ecosystem === "
|
|
10899
|
-
|
|
10900
|
-
|
|
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
|
+
}
|
|
10901
10728
|
await initializeGit(projectDir, options.git);
|
|
10902
10729
|
if (!isSilent()) await displayPostInstallInstructions({
|
|
10903
10730
|
...options,
|
|
10904
10731
|
depsInstalled: options.install
|
|
10905
10732
|
});
|
|
10906
|
-
return
|
|
10733
|
+
return {
|
|
10734
|
+
projectDir,
|
|
10735
|
+
setupFailures
|
|
10736
|
+
};
|
|
10907
10737
|
} catch (error) {
|
|
10738
|
+
await rollbackPartialProject(projectDir, dirHadContentBefore);
|
|
10908
10739
|
if (error instanceof Error) {
|
|
10909
10740
|
if (!isSilent()) console.error(error.stack);
|
|
10910
10741
|
exitWithError(`Error during project creation: ${error.message}`);
|
|
@@ -10914,6 +10745,23 @@ async function createProject(options, cliInput = {}) {
|
|
|
10914
10745
|
}
|
|
10915
10746
|
}
|
|
10916
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
|
+
}
|
|
10917
10765
|
async function setPackageManagerVersion(projectDir, packageManager) {
|
|
10918
10766
|
const pkgJsonPath = path.join(projectDir, "package.json");
|
|
10919
10767
|
if (!await fs.pathExists(pkgJsonPath)) return;
|
|
@@ -10974,6 +10822,7 @@ function getYesBaseConfig(flagConfig) {
|
|
|
10974
10822
|
rateLimit: "none",
|
|
10975
10823
|
i18n: "none",
|
|
10976
10824
|
search: "none",
|
|
10825
|
+
vectorDb: "none",
|
|
10977
10826
|
fileStorage: "none",
|
|
10978
10827
|
mobileNavigation: "expo-router",
|
|
10979
10828
|
mobileUI: "none",
|
|
@@ -10984,8 +10833,8 @@ function getYesBaseConfig(flagConfig) {
|
|
|
10984
10833
|
mobileDeepLinking: "none"
|
|
10985
10834
|
};
|
|
10986
10835
|
}
|
|
10987
|
-
function shouldPromptForVersionChannel(input) {
|
|
10988
|
-
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;
|
|
10989
10838
|
return canPromptInteractively();
|
|
10990
10839
|
}
|
|
10991
10840
|
async function createProjectHandler(input, options = {}) {
|
|
@@ -10996,10 +10845,15 @@ async function createProjectHandler(input, options = {}) {
|
|
|
10996
10845
|
try {
|
|
10997
10846
|
if (!isSilent() && input.renderTitle !== false) renderTitle();
|
|
10998
10847
|
if (!isSilent()) intro(pc.magenta("Creating a new Better Fullstack project"));
|
|
10848
|
+
await maybeShowTelemetryNotice();
|
|
10999
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;
|
|
11000
10854
|
let currentPathInput;
|
|
11001
|
-
if (
|
|
11002
|
-
else if (
|
|
10855
|
+
if (useDefaultsForName && input.projectName) currentPathInput = input.projectName;
|
|
10856
|
+
else if (useDefaultsForName) {
|
|
11003
10857
|
const defaultConfig = getDefaultConfig();
|
|
11004
10858
|
let defaultName = defaultConfig.relativePath;
|
|
11005
10859
|
let counter = 1;
|
|
@@ -11009,7 +10863,7 @@ async function createProjectHandler(input, options = {}) {
|
|
|
11009
10863
|
}
|
|
11010
10864
|
currentPathInput = defaultName;
|
|
11011
10865
|
} else currentPathInput = await getProjectName(input.projectName);
|
|
11012
|
-
const versionChannel = shouldPromptForVersionChannel(input) ? await getVersionChannelChoice() : input.versionChannel ?? "stable";
|
|
10866
|
+
const versionChannel = shouldPromptForVersionChannel(input, hasConfigBase) ? await getVersionChannelChoice() : input.versionChannel ?? configBase?.versionChannel ?? "stable";
|
|
11013
10867
|
let finalResolvedPath;
|
|
11014
10868
|
let finalBaseName;
|
|
11015
10869
|
if (input.dryRun) {
|
|
@@ -11088,6 +10942,7 @@ async function createProjectHandler(input, options = {}) {
|
|
|
11088
10942
|
rateLimit: "none",
|
|
11089
10943
|
i18n: "none",
|
|
11090
10944
|
search: "none",
|
|
10945
|
+
vectorDb: "none",
|
|
11091
10946
|
featureFlags: "none",
|
|
11092
10947
|
analytics: "none",
|
|
11093
10948
|
fileStorage: "none",
|
|
@@ -11174,11 +11029,15 @@ async function createProjectHandler(input, options = {}) {
|
|
|
11174
11029
|
finalBaseName = setupResult.finalBaseName;
|
|
11175
11030
|
currentPathInput = finalPathInput;
|
|
11176
11031
|
}
|
|
11177
|
-
const
|
|
11178
|
-
...input,
|
|
11032
|
+
const explicitInput = {
|
|
11033
|
+
...Object.fromEntries(Object.entries(input).filter(([, value]) => value !== void 0)),
|
|
11179
11034
|
projectDirectory: input.projectName
|
|
11180
11035
|
};
|
|
11181
|
-
const
|
|
11036
|
+
const originalInput = {
|
|
11037
|
+
...configBase,
|
|
11038
|
+
...explicitInput
|
|
11039
|
+
};
|
|
11040
|
+
const providedFlags = getProvidedFlags(explicitInput);
|
|
11182
11041
|
let cliInput = originalInput;
|
|
11183
11042
|
if (input.template && input.template !== "none") {
|
|
11184
11043
|
const templateConfig = getTemplateConfig(input.template);
|
|
@@ -11201,7 +11060,7 @@ async function createProjectHandler(input, options = {}) {
|
|
|
11201
11060
|
}
|
|
11202
11061
|
const { validatePreflightConfig } = await import("@better-fullstack/template-generator");
|
|
11203
11062
|
let config;
|
|
11204
|
-
if (cliInput.yes || cliInput.part?.length) {
|
|
11063
|
+
if (cliInput.yes || cliInput.part?.length || hasConfigBase) {
|
|
11205
11064
|
const flagConfig = processProvidedFlagsWithoutValidation(cliInput, finalBaseName);
|
|
11206
11065
|
config = {
|
|
11207
11066
|
...getYesBaseConfig(flagConfig),
|
|
@@ -11277,7 +11136,7 @@ async function createProjectHandler(input, options = {}) {
|
|
|
11277
11136
|
files
|
|
11278
11137
|
};
|
|
11279
11138
|
}
|
|
11280
|
-
await createProject(config, { manualDb: cliInput.manualDb ?? input.manualDb });
|
|
11139
|
+
const setupFailures = (await createProject(config, { manualDb: cliInput.manualDb ?? input.manualDb }))?.setupFailures ?? [];
|
|
11281
11140
|
if (cliInput.verify ?? input.verify) await runGeneratedChecks(config);
|
|
11282
11141
|
const reproducibleCommand = generateReproducibleCommand(config);
|
|
11283
11142
|
if (!isSilent()) log.success(pc.blue(`You can reproduce this setup with the following command:\n${reproducibleCommand}`));
|
|
@@ -11290,7 +11149,12 @@ async function createProjectHandler(input, options = {}) {
|
|
|
11290
11149
|
const elapsedTimeMs = Date.now() - startTime;
|
|
11291
11150
|
if (!isSilent()) {
|
|
11292
11151
|
const elapsedTimeInSeconds = (elapsedTimeMs / 1e3).toFixed(2);
|
|
11293
|
-
|
|
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!`));
|
|
11294
11158
|
}
|
|
11295
11159
|
return {
|
|
11296
11160
|
success: true,
|
|
@@ -11299,7 +11163,8 @@ async function createProjectHandler(input, options = {}) {
|
|
|
11299
11163
|
timeScaffolded,
|
|
11300
11164
|
elapsedTimeMs,
|
|
11301
11165
|
projectDirectory: config.projectDir,
|
|
11302
|
-
relativePath: config.relativePath
|
|
11166
|
+
relativePath: config.relativePath,
|
|
11167
|
+
setupFailures
|
|
11303
11168
|
};
|
|
11304
11169
|
} catch (error) {
|
|
11305
11170
|
if (error instanceof UserCancelledError) {
|
|
@@ -11476,7 +11341,7 @@ const router = os.router({
|
|
|
11476
11341
|
packageManager: types_exports.PackageManagerSchema.optional().describe("Package manager to use"),
|
|
11477
11342
|
projectDir: z.string().optional().describe("Project directory (defaults to current)")
|
|
11478
11343
|
})).handler(async ({ input }) => {
|
|
11479
|
-
const { addHandler } = await import("./add-handler-
|
|
11344
|
+
const { addHandler } = await import("./add-handler-BjTmOwEN.mjs");
|
|
11480
11345
|
await addHandler(input);
|
|
11481
11346
|
}),
|
|
11482
11347
|
history: os.meta({ description: "Show history of scaffolded projects with reproducible commands" }).input(z.object({
|
|
@@ -11486,6 +11351,17 @@ const router = os.router({
|
|
|
11486
11351
|
})).handler(async ({ input }) => {
|
|
11487
11352
|
await historyHandler(input);
|
|
11488
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
|
+
}),
|
|
11489
11365
|
"update-deps": os.meta({ description: "Check and update dependency versions in add-deps.ts" }).input(z.object({
|
|
11490
11366
|
check: z.boolean().default(false).describe("Report only, no changes"),
|
|
11491
11367
|
patch: z.boolean().default(false).describe("Apply patch/minor updates only"),
|
|
@@ -11493,7 +11369,7 @@ const router = os.router({
|
|
|
11493
11369
|
ecosystem: z.string().optional().describe("Filter by ecosystem (effect, tanstack, prisma, etc.)"),
|
|
11494
11370
|
"list-ecosystems": z.boolean().default(false).describe("List available ecosystems")
|
|
11495
11371
|
})).handler(async ({ input }) => {
|
|
11496
|
-
const { updateDepsHandler, showEcosystems } = await import("./update-deps-
|
|
11372
|
+
const { updateDepsHandler, showEcosystems } = await import("./update-deps-D5OG0KmJ.mjs");
|
|
11497
11373
|
if (input["list-ecosystems"]) {
|
|
11498
11374
|
showEcosystems();
|
|
11499
11375
|
return;
|
|
@@ -11508,6 +11384,17 @@ const router = os.router({
|
|
|
11508
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 () => {
|
|
11509
11385
|
log.message("MCP server is started via the 'mcp' subcommand intercepted in cli.ts.");
|
|
11510
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
|
+
});
|
|
11511
11398
|
})
|
|
11512
11399
|
});
|
|
11513
11400
|
const caller = createRouterClient(router, { context: {} });
|
|
@@ -11573,7 +11460,7 @@ async function builder() {
|
|
|
11573
11460
|
return caller.builder();
|
|
11574
11461
|
}
|
|
11575
11462
|
async function add(input) {
|
|
11576
|
-
const { addHandler } = await import("./add-handler-
|
|
11463
|
+
const { addHandler } = await import("./add-handler-BjTmOwEN.mjs");
|
|
11577
11464
|
return addHandler(input, { silent: true });
|
|
11578
11465
|
}
|
|
11579
11466
|
async function history(options) {
|
|
@@ -11583,6 +11470,9 @@ async function history(options) {
|
|
|
11583
11470
|
json: options?.json ?? false
|
|
11584
11471
|
});
|
|
11585
11472
|
}
|
|
11473
|
+
async function telemetry(action = "status", options) {
|
|
11474
|
+
return caller.telemetry([action, { json: options?.json ?? false }]);
|
|
11475
|
+
}
|
|
11586
11476
|
|
|
11587
11477
|
//#endregion
|
|
11588
|
-
export { docs as a, sponsors as c, createBtsCli as i, builder as n, history as o, create as r, router as s, add as t };
|
|
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 };
|