create-better-fullstack 1.8.1 → 2.0.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/README.md CHANGED
@@ -43,13 +43,27 @@ Configure your stack visually — pick every option from a UI, preview your choi
43
43
  --yolo # Scaffold a random stack — good for exploring
44
44
  --template <name> # Use a preset (t3, mern, pern, uniwind)
45
45
  --ecosystem <lang> # Start in typescript, react-native, rust, python, go, java, or elixir mode
46
+ --part <binding> # Add a multi-ecosystem stack part, e.g. frontend:typescript:next
46
47
  --version-channel # Dependency channel: stable, latest, beta
47
48
  --no-git # Skip git initialization
48
49
  --no-install # Skip dependency installation
50
+ --verify # Run generated project checks without starting dev servers
49
51
  --package-manager # Package manager (bun, pnpm, npm, yarn)
50
52
  --verbose # Show detailed output
51
53
  ```
52
54
 
55
+ ## Multi-Ecosystem Example
56
+
57
+ Use repeated `--part` flags to bind each generated app or capability to an ecosystem:
58
+
59
+ ```bash
60
+ bun create better-fullstack@latest my-mixed-app \
61
+ --part frontend:typescript:next \
62
+ --part backend:go:gin \
63
+ --part backend.orm:go:gorm \
64
+ --part database:universal:postgres
65
+ ```
66
+
53
67
  ## Links
54
68
 
55
69
  - [Website](https://better-fullstack.dev)
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import "./bts-config-BMniWIbd.mjs";
3
+ import { i as setupLefthook, n as setupBiome, r as setupHusky, t as setupAddons } from "./addons-setup-DHoByttt.mjs";
4
+
5
+ export { setupAddons };
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { s as dependencyVersionMap, t as readBtsConfig } from "./bts-config-YcroedMK.mjs";
2
+ import { m as dependencyVersionMap, t as readBtsConfig } from "./bts-config-BMniWIbd.mjs";
3
3
  import { autocompleteMultiselect, cancel, group, isCancel, log, multiselect, select, spinner } from "@clack/prompts";
4
4
  import pc from "picocolors";
5
5
  import fs from "fs-extra";
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { t as __reExport } from "./chunk-CCII7kTE.mjs";
2
3
  import fs from "fs-extra";
3
4
  import path from "node:path";
4
5
  import { fileURLToPath } from "node:url";
@@ -70,13 +71,169 @@ const getLatestCLIVersion = () => {
70
71
  return fs.readJSONSync(packageJsonPath).version ?? "1.0.0";
71
72
  };
72
73
 
74
+ //#endregion
75
+ //#region src/types.ts
76
+ var types_exports = {};
77
+ import * as import__better_fullstack_types from "@better-fullstack/types";
78
+ __reExport(types_exports, import__better_fullstack_types);
79
+
80
+ //#endregion
81
+ //#region src/utils/graph-summary.ts
82
+ const FRONTEND_LABELS = {
83
+ next: "Next.js web app",
84
+ astro: "Astro web app",
85
+ "react-vite": "React + Vite app",
86
+ "react-router": "React Router app",
87
+ "tanstack-router": "TanStack Router app",
88
+ "tanstack-start": "TanStack Start app",
89
+ nuxt: "Nuxt app",
90
+ svelte: "SvelteKit app",
91
+ solid: "Solid app",
92
+ "solid-start": "SolidStart app",
93
+ qwik: "Qwik app",
94
+ angular: "Angular app",
95
+ redwood: "RedwoodJS app",
96
+ fresh: "Fresh app"
97
+ };
98
+ const BACKEND_LABELS = {
99
+ "rust:axum": "Rust Axum API",
100
+ "rust:actix-web": "Rust Actix Web API",
101
+ "rust:rocket": "Rust Rocket API",
102
+ "rust:warp": "Rust Warp API",
103
+ "rust:poem": "Rust Poem API",
104
+ "rust:salvo": "Rust Salvo API",
105
+ "python:fastapi": "Python FastAPI API",
106
+ "python:django": "Python Django API",
107
+ "python:flask": "Python Flask API",
108
+ "python:litestar": "Python Litestar API",
109
+ "go:gin": "Go Gin API",
110
+ "go:echo": "Go Echo API",
111
+ "go:fiber": "Go Fiber API",
112
+ "go:chi": "Go Chi API",
113
+ "java:spring-boot": "Java Spring Boot API",
114
+ "java:quarkus": "Java Quarkus API",
115
+ "elixir:phoenix": "Elixir Phoenix API",
116
+ "elixir:phoenix-live-view": "Elixir Phoenix LiveView API"
117
+ };
118
+ const TOOL_LABELS = {
119
+ postgres: "Postgres",
120
+ sqlite: "SQLite",
121
+ mysql: "MySQL",
122
+ mongodb: "MongoDB",
123
+ redis: "Redis",
124
+ edgedb: "EdgeDB",
125
+ gorm: "GORM",
126
+ sqlc: "sqlc",
127
+ ent: "Ent",
128
+ "ecto-sql": "Ecto SQL",
129
+ ecto: "Ecto",
130
+ sqlx: "SQLx",
131
+ "sea-orm": "SeaORM",
132
+ sqlalchemy: "SQLAlchemy",
133
+ sqlmodel: "SQLModel",
134
+ "django-orm": "Django ORM",
135
+ "spring-data-jpa": "Spring Data JPA"
136
+ };
137
+ function getSelectedGraphParts(config) {
138
+ return (config.stackParts ?? []).filter((part) => part.source !== "provided");
139
+ }
140
+ function getGraphPart(config, role, ecosystem) {
141
+ return getSelectedGraphParts(config).find((part) => part.role === role && (!ecosystem || part.ecosystem === ecosystem));
142
+ }
143
+ function hasGraphPart(config, role, ecosystem) {
144
+ return Boolean(getGraphPart(config, role, ecosystem));
145
+ }
146
+ function getPrimaryGraphPart(config, role, ecosystem) {
147
+ return getSelectedGraphParts(config).find((part) => part.role === role && !part.ownerPartId && (!ecosystem || part.ecosystem === ecosystem));
148
+ }
149
+ function getGraphBackendUrl(config) {
150
+ switch (getPrimaryGraphPart(config, "backend")?.ecosystem) {
151
+ case "elixir": return "http://localhost:4000";
152
+ case "rust": return "http://localhost:3000";
153
+ case "python": return "http://localhost:8000";
154
+ case "go":
155
+ case "java": return "http://localhost:8080";
156
+ default: return null;
157
+ }
158
+ }
159
+ function getEffectiveStack(config) {
160
+ const effectiveStack = {};
161
+ const parts = getSelectedGraphParts(config);
162
+ for (const part of parts) {
163
+ const key = part.ownerPartId ? `backend.${part.role}` : part.role;
164
+ effectiveStack[key] = `${part.ecosystem}:${part.toolId}`;
165
+ }
166
+ return effectiveStack;
167
+ }
168
+ function labelFor(part) {
169
+ if (!part) return null;
170
+ if (part.role === "frontend" && part.ecosystem === "typescript") return FRONTEND_LABELS[part.toolId] ?? `${part.toolId} web app`;
171
+ if (part.role === "backend") return BACKEND_LABELS[`${part.ecosystem}:${part.toolId}`] ?? `${part.ecosystem} ${part.toolId} API`;
172
+ return TOOL_LABELS[part.toolId] ?? part.toolId;
173
+ }
174
+ function getGraphSummary(config) {
175
+ const selectedParts = getSelectedGraphParts(config);
176
+ if (selectedParts.length === 0) return null;
177
+ const frontend = getPrimaryGraphPart(config, "frontend");
178
+ const backend = getPrimaryGraphPart(config, "backend");
179
+ const database = getPrimaryGraphPart(config, "database");
180
+ const orm = selectedParts.find((part) => part.role === "orm");
181
+ const segments = [labelFor(frontend), labelFor(backend)].filter(Boolean);
182
+ const dataLabel = [labelFor(orm), labelFor(database)].filter(Boolean).join("/");
183
+ if (dataLabel) segments.push(dataLabel);
184
+ return segments.length > 0 ? segments.join(" + ") : selectedParts.map((part) => (0, types_exports.formatStackPartSpec)(part, selectedParts)).join(" + ");
185
+ }
186
+ function getGraphBackendDeployInstructions(config) {
187
+ const backend = getPrimaryGraphPart(config, "backend");
188
+ if (!backend || config.serverDeploy === "none") return "";
189
+ const targetPath = backend.targetPath ?? "apps/server";
190
+ const backendLabel = labelFor(backend) ?? `${backend.ecosystem}:${backend.toolId}`;
191
+ switch (config.serverDeploy) {
192
+ case "railway": return [
193
+ "Server deployment with Railway:",
194
+ `* Backend: ${backendLabel}`,
195
+ `* Config: ${targetPath}/railway.toml`,
196
+ `* Deploy: cd ${targetPath} && railway up`
197
+ ].join("\n");
198
+ case "docker": return [
199
+ "Server deployment with Docker:",
200
+ `* Backend: ${backendLabel}`,
201
+ `* Build: cd ${targetPath} && docker build -t better-fullstack-server .`
202
+ ].join("\n");
203
+ case "fly": return [
204
+ "Server deployment with Fly:",
205
+ `* Backend: ${backendLabel}`,
206
+ `* Deploy: cd ${targetPath} && fly launch`
207
+ ].join("\n");
208
+ case "vercel": return [
209
+ "Server deployment with Vercel:",
210
+ `* Backend: ${backendLabel}`,
211
+ `* Deploy from: ${targetPath}`
212
+ ].join("\n");
213
+ case "cloudflare":
214
+ case "sst": return [
215
+ `Server deployment with ${config.serverDeploy}:`,
216
+ `* Backend: ${backendLabel}`,
217
+ `* Review generated files in ${targetPath}`
218
+ ].join("\n");
219
+ default: return "";
220
+ }
221
+ }
222
+
73
223
  //#endregion
74
224
  //#region src/utils/bts-config.ts
75
225
  const BTS_CONFIG_FILE = "bts.jsonc";
76
226
  async function writeBtsConfig(projectConfig) {
227
+ const stackParts = projectConfig.stackParts ?? (0, types_exports.legacyProjectConfigToStackParts)(projectConfig);
228
+ const graphSummary = projectConfig.stackParts ? getGraphSummary({ stackParts }) : null;
229
+ const effectiveStack = projectConfig.stackParts ? getEffectiveStack({ stackParts }) : void 0;
77
230
  const btsConfig = {
78
231
  version: getLatestCLIVersion(),
79
232
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
233
+ ...graphSummary ? {
234
+ graphSummary,
235
+ effectiveStack
236
+ } : {},
80
237
  ecosystem: projectConfig.ecosystem,
81
238
  database: projectConfig.database,
82
239
  orm: projectConfig.orm,
@@ -168,12 +325,17 @@ async function writeBtsConfig(projectConfig) {
168
325
  elixirTesting: projectConfig.elixirTesting,
169
326
  elixirQuality: projectConfig.elixirQuality,
170
327
  elixirDeploy: projectConfig.elixirDeploy,
171
- aiDocs: projectConfig.aiDocs
328
+ aiDocs: projectConfig.aiDocs,
329
+ stackParts
172
330
  };
173
331
  const baseContent = {
174
332
  $schema: "https://better-fullstack-web.vercel.app/schema.json",
175
333
  version: btsConfig.version,
176
334
  createdAt: btsConfig.createdAt,
335
+ ...btsConfig.graphSummary ? {
336
+ graphSummary: btsConfig.graphSummary,
337
+ effectiveStack: btsConfig.effectiveStack
338
+ } : {},
177
339
  ecosystem: btsConfig.ecosystem,
178
340
  database: btsConfig.database,
179
341
  orm: btsConfig.orm,
@@ -265,7 +427,8 @@ async function writeBtsConfig(projectConfig) {
265
427
  elixirTesting: btsConfig.elixirTesting,
266
428
  elixirQuality: btsConfig.elixirQuality,
267
429
  elixirDeploy: btsConfig.elixirDeploy,
268
- aiDocs: btsConfig.aiDocs
430
+ aiDocs: btsConfig.aiDocs,
431
+ stackParts: btsConfig.stackParts
269
432
  };
270
433
  let configContent = JSON.stringify(baseContent);
271
434
  const formatResult = JSONC.format(configContent, void 0, {
@@ -276,6 +439,7 @@ async function writeBtsConfig(projectConfig) {
276
439
  configContent = JSONC.applyEdits(configContent, formatResult);
277
440
  const finalContent = `// Better Fullstack configuration file
278
441
  // safe to delete
442
+ ${graphSummary ? "// For multi-ecosystem projects, graphSummary/effectiveStack and stackParts are the source of truth.\n// Legacy fields such as backend/orm may stay as compatibility fallbacks.\n" : ""}
279
443
 
280
444
  ${configContent}`;
281
445
  const configPath = path.join(projectConfig.projectDir, BTS_CONFIG_FILE);
@@ -295,7 +459,19 @@ async function readBtsConfig(projectDir) {
295
459
  console.warn("Warning: Found errors parsing bts.jsonc:", errors);
296
460
  return null;
297
461
  }
298
- return config;
462
+ if (config.stackParts && config.stackParts.length > 0) {
463
+ const diagnostics = (0, types_exports.compareLegacyConfigToStackParts)(config, config.stackParts);
464
+ if (diagnostics.length > 0) console.warn(`Warning: bts.jsonc legacy fields differ from stackParts; using stackParts for ${diagnostics.map((diagnostic) => diagnostic.path).filter(Boolean).join(", ")}.`);
465
+ return {
466
+ ...config,
467
+ ...(0, types_exports.stackPartsToLegacyProjectConfigPartial)(config.stackParts),
468
+ stackParts: config.stackParts
469
+ };
470
+ }
471
+ return {
472
+ ...config,
473
+ stackParts: (0, types_exports.legacyProjectConfigToStackParts)(config)
474
+ };
299
475
  } catch {
300
476
  return null;
301
477
  }
@@ -318,4 +494,4 @@ async function updateBtsConfig(projectDir, updates) {
318
494
  }
319
495
 
320
496
  //#endregion
321
- export { DEFAULT_CONFIG as a, getDefaultConfig as c, getLatestCLIVersion as i, getUserPkgManager as l, updateBtsConfig as n, DEFAULT_UI_LIBRARY_BY_FRONTEND as o, writeBtsConfig as r, dependencyVersionMap as s, readBtsConfig as t };
497
+ export { getGraphBackendUrl as a, getPrimaryGraphPart as c, getLatestCLIVersion as d, DEFAULT_CONFIG as f, getUserPkgManager as g, getDefaultConfig as h, getGraphBackendDeployInstructions as i, hasGraphPart as l, dependencyVersionMap as m, updateBtsConfig as n, getGraphPart as o, DEFAULT_UI_LIBRARY_BY_FRONTEND as p, writeBtsConfig as r, getGraphSummary as s, readBtsConfig as t, types_exports as u };
package/dist/cli.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  //#region src/cli.ts
3
- if (process.argv[2] === "mcp" && process.argv.length === 3) import("./mcp-DoPutOIG.mjs").then((m) => m.startMcpServer());
3
+ if (process.argv[2] === "mcp" && process.argv.length === 3) import("./mcp-YvDMaVXB.mjs").then((m) => m.startMcpServer());
4
4
  else import("./index.mjs").then((m) => m.createBtsCli().run());
5
5
 
6
6
  //#endregion
package/dist/index.d.mts CHANGED
@@ -22,15 +22,17 @@ declare const router: {
22
22
  create: _orpc_server0.Procedure<_orpc_server0.MergedInitialContext<Record<never, never>, Record<never, never>, Record<never, never>>, Record<never, never>, z.ZodTuple<[z.ZodOptional<z.ZodString>, z.ZodObject<{
23
23
  template: z.ZodOptional<z.ZodEnum<{
24
24
  none: "none";
25
- uniwind: "uniwind";
26
25
  mern: "mern";
27
26
  pern: "pern";
28
27
  t3: "t3";
28
+ uniwind: "uniwind";
29
29
  }>>;
30
30
  yes: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
31
31
  yolo: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
32
+ part: z.ZodOptional<z.ZodArray<z.ZodString>>;
32
33
  verbose: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
33
34
  dryRun: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
35
+ verify: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
34
36
  ecosystem: z.ZodOptional<z.ZodEnum<{
35
37
  typescript: "typescript";
36
38
  "react-native": "react-native";
@@ -238,9 +240,9 @@ declare const router: {
238
240
  }>>;
239
241
  mobileUI: z.ZodOptional<z.ZodEnum<{
240
242
  none: "none";
243
+ uniwind: "uniwind";
241
244
  tamagui: "tamagui";
242
245
  "gluestack-ui": "gluestack-ui";
243
- uniwind: "uniwind";
244
246
  unistyles: "unistyles";
245
247
  }>>;
246
248
  mobileStorage: z.ZodOptional<z.ZodEnum<{
@@ -903,6 +905,17 @@ declare const router: {
903
905
  shadcnBaseColor?: "neutral" | "stone" | "zinc" | "gray" | undefined;
904
906
  shadcnFont?: "inter" | "geist" | "noto-sans" | "nunito-sans" | "figtree" | "roboto" | "raleway" | "dm-sans" | "public-sans" | "outfit" | "jetbrains-mono" | "geist-mono" | undefined;
905
907
  shadcnRadius?: "default" | "none" | "small" | "medium" | "large" | undefined;
908
+ stackParts?: {
909
+ id: string;
910
+ role: "api" | "runtime" | "backend" | "database" | "orm" | "auth" | "payments" | "email" | "logging" | "observability" | "stateManagement" | "forms" | "validation" | "testing" | "jobQueue" | "caching" | "i18n" | "search" | "fileStorage" | "cms" | "featureFlags" | "analytics" | "codeQuality" | "documentation" | "ai" | "frontend" | "mobile" | "deploy" | "ui" | "css" | "appPlatform";
911
+ toolId: string;
912
+ ecosystem: "typescript" | "react-native" | "rust" | "python" | "go" | "java" | "elixir" | "universal";
913
+ source: "selected" | "defaulted" | "provided" | "legacy" | "adjusted";
914
+ ownerPartId?: string | undefined;
915
+ providedByPartId?: string | undefined;
916
+ targetPath?: string | undefined;
917
+ settings?: Record<string, unknown> | undefined;
918
+ }[] | undefined;
906
919
  };
907
920
  reproducibleCommand: string;
908
921
  timeScaffolded: string;
@@ -1022,6 +1035,17 @@ declare const router: {
1022
1035
  shadcnBaseColor?: "neutral" | "stone" | "zinc" | "gray" | undefined;
1023
1036
  shadcnFont?: "inter" | "geist" | "noto-sans" | "nunito-sans" | "figtree" | "roboto" | "raleway" | "dm-sans" | "public-sans" | "outfit" | "jetbrains-mono" | "geist-mono" | undefined;
1024
1037
  shadcnRadius?: "default" | "none" | "small" | "medium" | "large" | undefined;
1038
+ stackParts?: {
1039
+ id: string;
1040
+ role: "api" | "runtime" | "backend" | "database" | "orm" | "auth" | "payments" | "email" | "logging" | "observability" | "stateManagement" | "forms" | "validation" | "testing" | "jobQueue" | "caching" | "i18n" | "search" | "fileStorage" | "cms" | "featureFlags" | "analytics" | "codeQuality" | "documentation" | "ai" | "frontend" | "mobile" | "deploy" | "ui" | "css" | "appPlatform";
1041
+ toolId: string;
1042
+ ecosystem: "typescript" | "react-native" | "rust" | "python" | "go" | "java" | "elixir" | "universal";
1043
+ source: "selected" | "defaulted" | "provided" | "legacy" | "adjusted";
1044
+ ownerPartId?: string | undefined;
1045
+ providedByPartId?: string | undefined;
1046
+ targetPath?: string | undefined;
1047
+ settings?: Record<string, unknown> | undefined;
1048
+ }[] | undefined;
1025
1049
  };
1026
1050
  reproducibleCommand: string;
1027
1051
  timeScaffolded: string;
@@ -1154,6 +1178,17 @@ declare const router: {
1154
1178
  shadcnBaseColor?: "neutral" | "stone" | "zinc" | "gray" | undefined;
1155
1179
  shadcnFont?: "inter" | "geist" | "noto-sans" | "nunito-sans" | "figtree" | "roboto" | "raleway" | "dm-sans" | "public-sans" | "outfit" | "jetbrains-mono" | "geist-mono" | undefined;
1156
1180
  shadcnRadius?: "default" | "none" | "small" | "medium" | "large" | undefined;
1181
+ stackParts?: {
1182
+ id: string;
1183
+ role: "api" | "runtime" | "backend" | "database" | "orm" | "auth" | "payments" | "email" | "logging" | "observability" | "stateManagement" | "forms" | "validation" | "testing" | "jobQueue" | "caching" | "i18n" | "search" | "fileStorage" | "cms" | "featureFlags" | "analytics" | "codeQuality" | "documentation" | "ai" | "frontend" | "mobile" | "deploy" | "ui" | "css" | "appPlatform";
1184
+ toolId: string;
1185
+ ecosystem: "typescript" | "react-native" | "rust" | "python" | "go" | "java" | "elixir" | "universal";
1186
+ source: "selected" | "defaulted" | "provided" | "legacy" | "adjusted";
1187
+ ownerPartId?: string | undefined;
1188
+ providedByPartId?: string | undefined;
1189
+ targetPath?: string | undefined;
1190
+ settings?: Record<string, unknown> | undefined;
1191
+ }[] | undefined;
1157
1192
  };
1158
1193
  reproducibleCommand: string;
1159
1194
  timeScaffolded: string;
@@ -1273,6 +1308,17 @@ declare const router: {
1273
1308
  shadcnBaseColor?: "neutral" | "stone" | "zinc" | "gray" | undefined;
1274
1309
  shadcnFont?: "inter" | "geist" | "noto-sans" | "nunito-sans" | "figtree" | "roboto" | "raleway" | "dm-sans" | "public-sans" | "outfit" | "jetbrains-mono" | "geist-mono" | undefined;
1275
1310
  shadcnRadius?: "default" | "none" | "small" | "medium" | "large" | undefined;
1311
+ stackParts?: {
1312
+ id: string;
1313
+ role: "api" | "runtime" | "backend" | "database" | "orm" | "auth" | "payments" | "email" | "logging" | "observability" | "stateManagement" | "forms" | "validation" | "testing" | "jobQueue" | "caching" | "i18n" | "search" | "fileStorage" | "cms" | "featureFlags" | "analytics" | "codeQuality" | "documentation" | "ai" | "frontend" | "mobile" | "deploy" | "ui" | "css" | "appPlatform";
1314
+ toolId: string;
1315
+ ecosystem: "typescript" | "react-native" | "rust" | "python" | "go" | "java" | "elixir" | "universal";
1316
+ source: "selected" | "defaulted" | "provided" | "legacy" | "adjusted";
1317
+ ownerPartId?: string | undefined;
1318
+ providedByPartId?: string | undefined;
1319
+ targetPath?: string | undefined;
1320
+ settings?: Record<string, unknown> | undefined;
1321
+ }[] | undefined;
1276
1322
  };
1277
1323
  reproducibleCommand: string;
1278
1324
  timeScaffolded: string;