create-better-t-stack 2.29.4 → 2.30.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/index.js +238 -336
- package/package.json +1 -1
- package/templates/addons/vibe-rules/.bts/rules.md.hbs +132 -0
package/dist/index.js
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { cancel, confirm, group, groupMultiselect, intro, isCancel, log, multiselect, outro, select, spinner, text } from "@clack/prompts";
|
|
3
|
-
import consola, { consola as consola$1 } from "consola";
|
|
4
3
|
import pc from "picocolors";
|
|
5
4
|
import { createCli, trpcServer } from "trpc-cli";
|
|
6
5
|
import z from "zod";
|
|
7
6
|
import path from "node:path";
|
|
8
7
|
import fs from "fs-extra";
|
|
9
8
|
import { fileURLToPath } from "node:url";
|
|
9
|
+
import consola, { consola as consola$1 } from "consola";
|
|
10
10
|
import gradient from "gradient-string";
|
|
11
11
|
import * as JSONC from "jsonc-parser";
|
|
12
12
|
import { $, execa } from "execa";
|
|
13
|
+
import handlebars from "handlebars";
|
|
13
14
|
import { IndentationText, Node, Project, QuoteKind, SyntaxKind } from "ts-morph";
|
|
14
15
|
import { globby } from "globby";
|
|
15
|
-
import handlebars from "handlebars";
|
|
16
16
|
import os from "node:os";
|
|
17
17
|
|
|
18
18
|
//#region src/utils/get-package-manager.ts
|
|
@@ -136,6 +136,7 @@ const ADDON_COMPATIBILITY = {
|
|
|
136
136
|
turborepo: [],
|
|
137
137
|
starlight: [],
|
|
138
138
|
ultracite: [],
|
|
139
|
+
"vibe-rules": [],
|
|
139
140
|
oxlint: [],
|
|
140
141
|
fumadocs: [],
|
|
141
142
|
none: []
|
|
@@ -189,6 +190,7 @@ const AddonsSchema = z.enum([
|
|
|
189
190
|
"starlight",
|
|
190
191
|
"biome",
|
|
191
192
|
"husky",
|
|
193
|
+
"vibe-rules",
|
|
192
194
|
"turborepo",
|
|
193
195
|
"fumadocs",
|
|
194
196
|
"ultracite",
|
|
@@ -258,6 +260,22 @@ function getCompatibleAddons(allAddons, frontend, existingAddons = []) {
|
|
|
258
260
|
});
|
|
259
261
|
}
|
|
260
262
|
|
|
263
|
+
//#endregion
|
|
264
|
+
//#region src/utils/errors.ts
|
|
265
|
+
function exitWithError(message) {
|
|
266
|
+
consola$1.error(pc.red(message));
|
|
267
|
+
process.exit(1);
|
|
268
|
+
}
|
|
269
|
+
function exitCancelled(message = "Operation cancelled") {
|
|
270
|
+
cancel(pc.red(message));
|
|
271
|
+
process.exit(0);
|
|
272
|
+
}
|
|
273
|
+
function handleError(error, fallbackMessage) {
|
|
274
|
+
const message = error instanceof Error ? error.message : fallbackMessage || String(error);
|
|
275
|
+
consola$1.error(pc.red(message));
|
|
276
|
+
process.exit(1);
|
|
277
|
+
}
|
|
278
|
+
|
|
261
279
|
//#endregion
|
|
262
280
|
//#region src/prompts/addons.ts
|
|
263
281
|
function getAddonDisplay(addon) {
|
|
@@ -288,6 +306,10 @@ function getAddonDisplay(addon) {
|
|
|
288
306
|
label = "Ultracite";
|
|
289
307
|
hint = "Zero-config Biome preset with AI integration";
|
|
290
308
|
break;
|
|
309
|
+
case "vibe-rules":
|
|
310
|
+
label = "vibe-rules";
|
|
311
|
+
hint = "Install and apply BTS rules to editors";
|
|
312
|
+
break;
|
|
291
313
|
case "husky":
|
|
292
314
|
label = "Husky";
|
|
293
315
|
hint = "Modern native Git hooks made easy";
|
|
@@ -317,6 +339,7 @@ const ADDON_GROUPS = {
|
|
|
317
339
|
"ultracite"
|
|
318
340
|
],
|
|
319
341
|
Other: [
|
|
342
|
+
"vibe-rules",
|
|
320
343
|
"turborepo",
|
|
321
344
|
"pwa",
|
|
322
345
|
"tauri",
|
|
@@ -356,10 +379,7 @@ async function getAddonsChoice(addons, frontends) {
|
|
|
356
379
|
required: false,
|
|
357
380
|
selectableGroups: false
|
|
358
381
|
});
|
|
359
|
-
if (isCancel(response))
|
|
360
|
-
cancel(pc.red("Operation cancelled"));
|
|
361
|
-
process.exit(0);
|
|
362
|
-
}
|
|
382
|
+
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
|
363
383
|
return response;
|
|
364
384
|
}
|
|
365
385
|
async function getAddonsToAdd(frontend, existingAddons = []) {
|
|
@@ -391,10 +411,7 @@ async function getAddonsToAdd(frontend, existingAddons = []) {
|
|
|
391
411
|
required: false,
|
|
392
412
|
selectableGroups: false
|
|
393
413
|
});
|
|
394
|
-
if (isCancel(response))
|
|
395
|
-
cancel(pc.red("Operation cancelled"));
|
|
396
|
-
process.exit(0);
|
|
397
|
-
}
|
|
414
|
+
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
|
398
415
|
return response;
|
|
399
416
|
}
|
|
400
417
|
|
|
@@ -425,48 +442,18 @@ function splitFrontends(values = []) {
|
|
|
425
442
|
}
|
|
426
443
|
function ensureSingleWebAndNative(frontends) {
|
|
427
444
|
const { web, native } = splitFrontends(frontends);
|
|
428
|
-
if (web.length > 1)
|
|
429
|
-
|
|
430
|
-
process.exit(1);
|
|
431
|
-
}
|
|
432
|
-
if (native.length > 1) {
|
|
433
|
-
consola$1.fatal("Cannot select multiple native frameworks. Choose only one of: native-nativewind, native-unistyles");
|
|
434
|
-
process.exit(1);
|
|
435
|
-
}
|
|
445
|
+
if (web.length > 1) exitWithError("Cannot select multiple web frameworks. Choose only one of: tanstack-router, tanstack-start, react-router, next, nuxt, svelte, solid");
|
|
446
|
+
if (native.length > 1) exitWithError("Cannot select multiple native frameworks. Choose only one of: native-nativewind, native-unistyles");
|
|
436
447
|
}
|
|
437
448
|
function validateWorkersCompatibility(providedFlags, options, config) {
|
|
438
|
-
if (providedFlags.has("runtime") && options.runtime === "workers" && config.backend && config.backend !== "hono") {
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
}
|
|
442
|
-
if (providedFlags.has("
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
if (providedFlags.has("runtime") && options.runtime === "workers" && config.orm && config.orm !== "drizzle" && config.orm !== "none") {
|
|
447
|
-
consola$1.fatal(`Cloudflare Workers runtime (--runtime workers) is only supported with Drizzle ORM (--orm drizzle) or no ORM (--orm none). Current ORM: ${config.orm}. Please use '--orm drizzle', '--orm none', or choose a different runtime.`);
|
|
448
|
-
process.exit(1);
|
|
449
|
-
}
|
|
450
|
-
if (providedFlags.has("orm") && config.orm && config.orm !== "drizzle" && config.orm !== "none" && config.runtime === "workers") {
|
|
451
|
-
consola$1.fatal(`ORM '${config.orm}' is not compatible with Cloudflare Workers runtime. Cloudflare Workers runtime is only supported with Drizzle ORM or no ORM. Please use '--orm drizzle', '--orm none', or choose a different runtime.`);
|
|
452
|
-
process.exit(1);
|
|
453
|
-
}
|
|
454
|
-
if (providedFlags.has("runtime") && options.runtime === "workers" && config.database === "mongodb") {
|
|
455
|
-
consola$1.fatal("Cloudflare Workers runtime (--runtime workers) is not compatible with MongoDB database. MongoDB requires Prisma or Mongoose ORM, but Workers runtime only supports Drizzle ORM. Please use a different database or runtime.");
|
|
456
|
-
process.exit(1);
|
|
457
|
-
}
|
|
458
|
-
if (providedFlags.has("runtime") && options.runtime === "workers" && config.dbSetup === "docker") {
|
|
459
|
-
consola$1.fatal("Cloudflare Workers runtime (--runtime workers) is not compatible with Docker setup. Workers runtime uses serverless databases (D1) and doesn't support local Docker containers. Please use '--db-setup d1' for SQLite or choose a different runtime.");
|
|
460
|
-
process.exit(1);
|
|
461
|
-
}
|
|
462
|
-
if (providedFlags.has("database") && config.database === "mongodb" && config.runtime === "workers") {
|
|
463
|
-
consola$1.fatal("MongoDB database is not compatible with Cloudflare Workers runtime. MongoDB requires Prisma or Mongoose ORM, but Workers runtime only supports Drizzle ORM. Please use a different database or runtime.");
|
|
464
|
-
process.exit(1);
|
|
465
|
-
}
|
|
466
|
-
if (providedFlags.has("dbSetup") && options.dbSetup === "docker" && config.runtime === "workers") {
|
|
467
|
-
consola$1.fatal("Docker setup (--db-setup docker) is not compatible with Cloudflare Workers runtime. Workers runtime uses serverless databases (D1) and doesn't support local Docker containers. Please use '--db-setup d1' for SQLite or choose a different runtime.");
|
|
468
|
-
process.exit(1);
|
|
469
|
-
}
|
|
449
|
+
if (providedFlags.has("runtime") && options.runtime === "workers" && config.backend && config.backend !== "hono") exitWithError(`Cloudflare Workers runtime (--runtime workers) is only supported with Hono backend (--backend hono). Current backend: ${config.backend}. Please use '--backend hono' or choose a different runtime.`);
|
|
450
|
+
if (providedFlags.has("backend") && config.backend && config.backend !== "hono" && config.runtime === "workers") exitWithError(`Backend '${config.backend}' is not compatible with Cloudflare Workers runtime. Cloudflare Workers runtime is only supported with Hono backend. Please use '--backend hono' or choose a different runtime.`);
|
|
451
|
+
if (providedFlags.has("runtime") && options.runtime === "workers" && config.orm && config.orm !== "drizzle" && config.orm !== "none") exitWithError(`Cloudflare Workers runtime (--runtime workers) is only supported with Drizzle ORM (--orm drizzle) or no ORM (--orm none). Current ORM: ${config.orm}. Please use '--orm drizzle', '--orm none', or choose a different runtime.`);
|
|
452
|
+
if (providedFlags.has("orm") && config.orm && config.orm !== "drizzle" && config.orm !== "none" && config.runtime === "workers") exitWithError(`ORM '${config.orm}' is not compatible with Cloudflare Workers runtime. Cloudflare Workers runtime is only supported with Drizzle ORM or no ORM. Please use '--orm drizzle', '--orm none', or choose a different runtime.`);
|
|
453
|
+
if (providedFlags.has("runtime") && options.runtime === "workers" && config.database === "mongodb") exitWithError("Cloudflare Workers runtime (--runtime workers) is not compatible with MongoDB database. MongoDB requires Prisma or Mongoose ORM, but Workers runtime only supports Drizzle ORM. Please use a different database or runtime.");
|
|
454
|
+
if (providedFlags.has("runtime") && options.runtime === "workers" && config.dbSetup === "docker") exitWithError("Cloudflare Workers runtime (--runtime workers) is not compatible with Docker setup. Workers runtime uses serverless databases (D1) and doesn't support local Docker containers. Please use '--db-setup d1' for SQLite or choose a different runtime.");
|
|
455
|
+
if (providedFlags.has("database") && config.database === "mongodb" && config.runtime === "workers") exitWithError("MongoDB database is not compatible with Cloudflare Workers runtime. MongoDB requires Prisma or Mongoose ORM, but Workers runtime only supports Drizzle ORM. Please use a different database or runtime.");
|
|
456
|
+
if (providedFlags.has("dbSetup") && options.dbSetup === "docker" && config.runtime === "workers") exitWithError("Docker setup (--db-setup docker) is not compatible with Cloudflare Workers runtime. Workers runtime uses serverless databases (D1) and doesn't support local Docker containers. Please use '--db-setup d1' for SQLite or choose a different runtime.");
|
|
470
457
|
}
|
|
471
458
|
function coerceBackendPresets(config) {
|
|
472
459
|
if (config.backend === "convex") {
|
|
@@ -516,10 +503,7 @@ function validateApiFrontendCompatibility(api, frontends = []) {
|
|
|
516
503
|
const includesNuxt = frontends.includes("nuxt");
|
|
517
504
|
const includesSvelte = frontends.includes("svelte");
|
|
518
505
|
const includesSolid = frontends.includes("solid");
|
|
519
|
-
if ((includesNuxt || includesSvelte || includesSolid) && api === "trpc") {
|
|
520
|
-
consola$1.fatal(`tRPC API is not supported with '${includesNuxt ? "nuxt" : includesSvelte ? "svelte" : "solid"}' frontend. Please use --api orpc or --api none or remove '${includesNuxt ? "nuxt" : includesSvelte ? "svelte" : "solid"}' from --frontend.`);
|
|
521
|
-
process.exit(1);
|
|
522
|
-
}
|
|
506
|
+
if ((includesNuxt || includesSvelte || includesSolid) && api === "trpc") exitWithError(`tRPC API is not supported with '${includesNuxt ? "nuxt" : includesSvelte ? "svelte" : "solid"}' frontend. Please use --api orpc or --api none or remove '${includesNuxt ? "nuxt" : includesSvelte ? "svelte" : "solid"}' from --frontend.`);
|
|
523
507
|
}
|
|
524
508
|
function isFrontendAllowedWithBackend(frontend, backend) {
|
|
525
509
|
if (backend === "convex" && frontend === "solid") return false;
|
|
@@ -547,10 +531,7 @@ function isExampleAIAllowed(backend, frontends = []) {
|
|
|
547
531
|
return true;
|
|
548
532
|
}
|
|
549
533
|
function validateWebDeployRequiresWebFrontend(webDeploy, hasWebFrontendFlag) {
|
|
550
|
-
if (webDeploy && webDeploy !== "none" && !hasWebFrontendFlag)
|
|
551
|
-
consola$1.fatal("'--web-deploy' requires a web frontend. Please select a web frontend or set '--web-deploy none'.");
|
|
552
|
-
process.exit(1);
|
|
553
|
-
}
|
|
534
|
+
if (webDeploy && webDeploy !== "none" && !hasWebFrontendFlag) exitWithError("'--web-deploy' requires a web frontend. Please select a web frontend or set '--web-deploy none'.");
|
|
554
535
|
}
|
|
555
536
|
|
|
556
537
|
//#endregion
|
|
@@ -577,10 +558,7 @@ async function getApiChoice(Api, frontend, backend) {
|
|
|
577
558
|
options: apiOptions,
|
|
578
559
|
initialValue: apiOptions[0].value
|
|
579
560
|
});
|
|
580
|
-
if (isCancel(apiType))
|
|
581
|
-
cancel(pc.red("Operation cancelled"));
|
|
582
|
-
process.exit(0);
|
|
583
|
-
}
|
|
561
|
+
if (isCancel(apiType)) return exitCancelled("Operation cancelled");
|
|
584
562
|
return apiType;
|
|
585
563
|
}
|
|
586
564
|
|
|
@@ -594,10 +572,7 @@ async function getAuthChoice(auth, hasDatabase, backend) {
|
|
|
594
572
|
message: "Add authentication with Better-Auth?",
|
|
595
573
|
initialValue: DEFAULT_CONFIG.auth
|
|
596
574
|
});
|
|
597
|
-
if (isCancel(response))
|
|
598
|
-
cancel(pc.red("Operation cancelled"));
|
|
599
|
-
process.exit(0);
|
|
600
|
-
}
|
|
575
|
+
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
|
601
576
|
return response;
|
|
602
577
|
}
|
|
603
578
|
|
|
@@ -648,10 +623,7 @@ async function getBackendFrameworkChoice(backendFramework, frontends) {
|
|
|
648
623
|
options: backendOptions,
|
|
649
624
|
initialValue: DEFAULT_CONFIG.backend
|
|
650
625
|
});
|
|
651
|
-
if (isCancel(response))
|
|
652
|
-
cancel(pc.red("Operation cancelled"));
|
|
653
|
-
process.exit(0);
|
|
654
|
-
}
|
|
626
|
+
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
|
655
627
|
return response;
|
|
656
628
|
}
|
|
657
629
|
|
|
@@ -692,10 +664,7 @@ async function getDatabaseChoice(database, backend, runtime) {
|
|
|
692
664
|
options: databaseOptions,
|
|
693
665
|
initialValue: DEFAULT_CONFIG.database
|
|
694
666
|
});
|
|
695
|
-
if (isCancel(response))
|
|
696
|
-
cancel(pc.red("Operation cancelled"));
|
|
697
|
-
process.exit(0);
|
|
698
|
-
}
|
|
667
|
+
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
|
699
668
|
return response;
|
|
700
669
|
}
|
|
701
670
|
|
|
@@ -783,10 +752,7 @@ async function getDBSetupChoice(databaseType, dbSetup, orm, backend, runtime) {
|
|
|
783
752
|
options,
|
|
784
753
|
initialValue: "none"
|
|
785
754
|
});
|
|
786
|
-
if (isCancel(response))
|
|
787
|
-
cancel(pc.red("Operation cancelled"));
|
|
788
|
-
process.exit(0);
|
|
789
|
-
}
|
|
755
|
+
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
|
790
756
|
return response;
|
|
791
757
|
}
|
|
792
758
|
|
|
@@ -819,10 +785,7 @@ async function getExamplesChoice(examples, database, frontends, backend, api) {
|
|
|
819
785
|
required: false,
|
|
820
786
|
initialValues: DEFAULT_CONFIG.examples?.filter((ex) => options.some((o) => o.value === ex))
|
|
821
787
|
});
|
|
822
|
-
if (isCancel(response))
|
|
823
|
-
cancel(pc.red("Operation cancelled"));
|
|
824
|
-
process.exit(0);
|
|
825
|
-
}
|
|
788
|
+
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
|
826
789
|
return response;
|
|
827
790
|
}
|
|
828
791
|
|
|
@@ -844,10 +807,7 @@ async function getFrontendChoice(frontendOptions, backend) {
|
|
|
844
807
|
required: false,
|
|
845
808
|
initialValues: ["web"]
|
|
846
809
|
});
|
|
847
|
-
if (isCancel(frontendTypes))
|
|
848
|
-
cancel(pc.red("Operation cancelled"));
|
|
849
|
-
process.exit(0);
|
|
850
|
-
}
|
|
810
|
+
if (isCancel(frontendTypes)) return exitCancelled("Operation cancelled");
|
|
851
811
|
const result = [];
|
|
852
812
|
if (frontendTypes.includes("web")) {
|
|
853
813
|
const allWebOptions = [
|
|
@@ -883,7 +843,7 @@ async function getFrontendChoice(frontendOptions, backend) {
|
|
|
883
843
|
},
|
|
884
844
|
{
|
|
885
845
|
value: "tanstack-start",
|
|
886
|
-
label: "TanStack Start
|
|
846
|
+
label: "TanStack Start",
|
|
887
847
|
hint: "SSR, Server Functions, API Routes and more with TanStack Router"
|
|
888
848
|
}
|
|
889
849
|
];
|
|
@@ -893,10 +853,7 @@ async function getFrontendChoice(frontendOptions, backend) {
|
|
|
893
853
|
options: webOptions,
|
|
894
854
|
initialValue: DEFAULT_CONFIG.frontend[0]
|
|
895
855
|
});
|
|
896
|
-
if (isCancel(webFramework))
|
|
897
|
-
cancel(pc.red("Operation cancelled"));
|
|
898
|
-
process.exit(0);
|
|
899
|
-
}
|
|
856
|
+
if (isCancel(webFramework)) return exitCancelled("Operation cancelled");
|
|
900
857
|
result.push(webFramework);
|
|
901
858
|
}
|
|
902
859
|
if (frontendTypes.includes("native")) {
|
|
@@ -913,10 +870,7 @@ async function getFrontendChoice(frontendOptions, backend) {
|
|
|
913
870
|
}],
|
|
914
871
|
initialValue: "native-nativewind"
|
|
915
872
|
});
|
|
916
|
-
if (isCancel(nativeFramework))
|
|
917
|
-
cancel(pc.red("Operation cancelled"));
|
|
918
|
-
process.exit(0);
|
|
919
|
-
}
|
|
873
|
+
if (isCancel(nativeFramework)) return exitCancelled("Operation cancelled");
|
|
920
874
|
result.push(nativeFramework);
|
|
921
875
|
}
|
|
922
876
|
return result;
|
|
@@ -930,10 +884,7 @@ async function getGitChoice(git) {
|
|
|
930
884
|
message: "Initialize git repository?",
|
|
931
885
|
initialValue: DEFAULT_CONFIG.git
|
|
932
886
|
});
|
|
933
|
-
if (isCancel(response))
|
|
934
|
-
cancel(pc.red("Operation cancelled"));
|
|
935
|
-
process.exit(0);
|
|
936
|
-
}
|
|
887
|
+
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
|
937
888
|
return response;
|
|
938
889
|
}
|
|
939
890
|
|
|
@@ -945,10 +896,7 @@ async function getinstallChoice(install) {
|
|
|
945
896
|
message: "Install dependencies?",
|
|
946
897
|
initialValue: DEFAULT_CONFIG.install
|
|
947
898
|
});
|
|
948
|
-
if (isCancel(response))
|
|
949
|
-
cancel(pc.red("Operation cancelled"));
|
|
950
|
-
process.exit(0);
|
|
951
|
-
}
|
|
899
|
+
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
|
952
900
|
return response;
|
|
953
901
|
}
|
|
954
902
|
|
|
@@ -982,10 +930,7 @@ async function getORMChoice(orm, hasDatabase, database, backend, runtime) {
|
|
|
982
930
|
options,
|
|
983
931
|
initialValue: database === "mongodb" ? "prisma" : DEFAULT_CONFIG.orm
|
|
984
932
|
});
|
|
985
|
-
if (isCancel(response))
|
|
986
|
-
cancel(pc.red("Operation cancelled"));
|
|
987
|
-
process.exit(0);
|
|
988
|
-
}
|
|
933
|
+
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
|
989
934
|
return response;
|
|
990
935
|
}
|
|
991
936
|
|
|
@@ -1015,10 +960,7 @@ async function getPackageManagerChoice(packageManager) {
|
|
|
1015
960
|
],
|
|
1016
961
|
initialValue: detectedPackageManager
|
|
1017
962
|
});
|
|
1018
|
-
if (isCancel(response))
|
|
1019
|
-
cancel(pc.red("Operation cancelled"));
|
|
1020
|
-
process.exit(0);
|
|
1021
|
-
}
|
|
963
|
+
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
|
1022
964
|
return response;
|
|
1023
965
|
}
|
|
1024
966
|
|
|
@@ -1047,10 +989,7 @@ async function getRuntimeChoice(runtime, backend) {
|
|
|
1047
989
|
options: runtimeOptions,
|
|
1048
990
|
initialValue: DEFAULT_CONFIG.runtime
|
|
1049
991
|
});
|
|
1050
|
-
if (isCancel(response))
|
|
1051
|
-
cancel(pc.red("Operation cancelled"));
|
|
1052
|
-
process.exit(0);
|
|
1053
|
-
}
|
|
992
|
+
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
|
1054
993
|
return response;
|
|
1055
994
|
}
|
|
1056
995
|
|
|
@@ -1086,10 +1025,7 @@ async function getDeploymentChoice(deployment, _runtime, _backend, frontend = []
|
|
|
1086
1025
|
options,
|
|
1087
1026
|
initialValue: DEFAULT_CONFIG.webDeploy
|
|
1088
1027
|
});
|
|
1089
|
-
if (isCancel(response))
|
|
1090
|
-
cancel(pc.red("Operation cancelled"));
|
|
1091
|
-
process.exit(0);
|
|
1092
|
-
}
|
|
1028
|
+
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
|
1093
1029
|
return response;
|
|
1094
1030
|
}
|
|
1095
1031
|
async function getDeploymentToAdd(frontend, existingDeployment) {
|
|
@@ -1115,10 +1051,7 @@ async function getDeploymentToAdd(frontend, existingDeployment) {
|
|
|
1115
1051
|
options,
|
|
1116
1052
|
initialValue: DEFAULT_CONFIG.webDeploy
|
|
1117
1053
|
});
|
|
1118
|
-
if (isCancel(response))
|
|
1119
|
-
cancel(pc.red("Operation cancelled"));
|
|
1120
|
-
process.exit(0);
|
|
1121
|
-
}
|
|
1054
|
+
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
|
1122
1055
|
return response;
|
|
1123
1056
|
}
|
|
1124
1057
|
|
|
@@ -1140,10 +1073,7 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
|
|
|
1140
1073
|
git: () => getGitChoice(flags.git),
|
|
1141
1074
|
packageManager: () => getPackageManagerChoice(flags.packageManager),
|
|
1142
1075
|
install: () => getinstallChoice(flags.install)
|
|
1143
|
-
}, { onCancel: () =>
|
|
1144
|
-
cancel(pc.red("Operation cancelled"));
|
|
1145
|
-
process.exit(0);
|
|
1146
|
-
} });
|
|
1076
|
+
}, { onCancel: () => exitCancelled("Operation cancelled") });
|
|
1147
1077
|
if (result.backend === "convex") {
|
|
1148
1078
|
result.runtime = "none";
|
|
1149
1079
|
result.database = "none";
|
|
@@ -1235,10 +1165,7 @@ async function getProjectName(initialName) {
|
|
|
1235
1165
|
return void 0;
|
|
1236
1166
|
}
|
|
1237
1167
|
});
|
|
1238
|
-
if (isCancel(response))
|
|
1239
|
-
cancel(pc.red("Operation cancelled."));
|
|
1240
|
-
process.exit(0);
|
|
1241
|
-
}
|
|
1168
|
+
if (isCancel(response)) return exitCancelled("Operation cancelled.");
|
|
1242
1169
|
projectPath = response || defaultName;
|
|
1243
1170
|
isValid = true;
|
|
1244
1171
|
}
|
|
@@ -1410,10 +1337,7 @@ async function handleDirectoryConflict(currentPathInput) {
|
|
|
1410
1337
|
],
|
|
1411
1338
|
initialValue: "rename"
|
|
1412
1339
|
});
|
|
1413
|
-
if (isCancel(action))
|
|
1414
|
-
cancel(pc.red("Operation cancelled."));
|
|
1415
|
-
process.exit(0);
|
|
1416
|
-
}
|
|
1340
|
+
if (isCancel(action)) return exitCancelled("Operation cancelled.");
|
|
1417
1341
|
switch (action) {
|
|
1418
1342
|
case "overwrite": return {
|
|
1419
1343
|
finalPathInput: currentPathInput,
|
|
@@ -1430,9 +1354,7 @@ async function handleDirectoryConflict(currentPathInput) {
|
|
|
1430
1354
|
const newPathInput = await getProjectName(void 0);
|
|
1431
1355
|
return await handleDirectoryConflict(newPathInput);
|
|
1432
1356
|
}
|
|
1433
|
-
case "cancel":
|
|
1434
|
-
cancel(pc.red("Operation cancelled."));
|
|
1435
|
-
process.exit(0);
|
|
1357
|
+
case "cancel": return exitCancelled("Operation cancelled.");
|
|
1436
1358
|
}
|
|
1437
1359
|
}
|
|
1438
1360
|
}
|
|
@@ -1454,8 +1376,7 @@ async function setupProjectDirectory(finalPathInput, shouldClearDirectory) {
|
|
|
1454
1376
|
s.stop(`Directory "${finalResolvedPath}" cleared.`);
|
|
1455
1377
|
} catch (error) {
|
|
1456
1378
|
s.stop(pc.red(`Failed to clear directory "${finalResolvedPath}".`));
|
|
1457
|
-
|
|
1458
|
-
process.exit(1);
|
|
1379
|
+
handleError(error);
|
|
1459
1380
|
}
|
|
1460
1381
|
} else await fs.ensureDir(finalResolvedPath);
|
|
1461
1382
|
return {
|
|
@@ -1515,18 +1436,12 @@ function processAndValidateFlags(options, providedFlags, projectName) {
|
|
|
1515
1436
|
if (options.api) {
|
|
1516
1437
|
config.api = options.api;
|
|
1517
1438
|
if (options.api === "none") {
|
|
1518
|
-
if (options.examples && !(options.examples.length === 1 && options.examples[0] === "none") && options.backend !== "convex")
|
|
1519
|
-
consola$1.fatal("Cannot use '--examples' when '--api' is set to 'none'. Please remove the --examples flag or choose an API type.");
|
|
1520
|
-
process.exit(1);
|
|
1521
|
-
}
|
|
1439
|
+
if (options.examples && !(options.examples.length === 1 && options.examples[0] === "none") && options.backend !== "convex") exitWithError("Cannot use '--examples' when '--api' is set to 'none'. Please remove the --examples flag or choose an API type.");
|
|
1522
1440
|
}
|
|
1523
1441
|
}
|
|
1524
1442
|
if (options.backend) config.backend = options.backend;
|
|
1525
1443
|
if (providedFlags.has("backend") && config.backend && config.backend !== "convex" && config.backend !== "none") {
|
|
1526
|
-
if (providedFlags.has("runtime") && options.runtime === "none")
|
|
1527
|
-
consola$1.fatal(`'--runtime none' is only supported with '--backend convex' or '--backend none'. Please choose 'bun', 'node', or remove the --runtime flag.`);
|
|
1528
|
-
process.exit(1);
|
|
1529
|
-
}
|
|
1444
|
+
if (providedFlags.has("runtime") && options.runtime === "none") exitWithError(`'--runtime none' is only supported with '--backend convex' or '--backend none'. Please choose 'bun', 'node', or remove the --runtime flag.`);
|
|
1530
1445
|
}
|
|
1531
1446
|
if (options.database) config.database = options.database;
|
|
1532
1447
|
if (options.orm) config.orm = options.orm;
|
|
@@ -1539,25 +1454,16 @@ function processAndValidateFlags(options, providedFlags, projectName) {
|
|
|
1539
1454
|
if (options.webDeploy) config.webDeploy = options.webDeploy;
|
|
1540
1455
|
if (projectName) {
|
|
1541
1456
|
const result = ProjectNameSchema.safeParse(path.basename(projectName));
|
|
1542
|
-
if (!result.success) {
|
|
1543
|
-
consola$1.fatal(`Invalid project name: ${result.error.issues[0]?.message || "Invalid project name"}`);
|
|
1544
|
-
process.exit(1);
|
|
1545
|
-
}
|
|
1457
|
+
if (!result.success) exitWithError(`Invalid project name: ${result.error.issues[0]?.message || "Invalid project name"}`);
|
|
1546
1458
|
config.projectName = projectName;
|
|
1547
1459
|
} else if (options.projectDirectory) {
|
|
1548
1460
|
const baseName = path.basename(path.resolve(process.cwd(), options.projectDirectory));
|
|
1549
1461
|
const result = ProjectNameSchema.safeParse(baseName);
|
|
1550
|
-
if (!result.success) {
|
|
1551
|
-
consola$1.fatal(`Invalid project name: ${result.error.issues[0]?.message || "Invalid project name"}`);
|
|
1552
|
-
process.exit(1);
|
|
1553
|
-
}
|
|
1462
|
+
if (!result.success) exitWithError(`Invalid project name: ${result.error.issues[0]?.message || "Invalid project name"}`);
|
|
1554
1463
|
config.projectName = baseName;
|
|
1555
1464
|
}
|
|
1556
1465
|
if (options.frontend && options.frontend.length > 0) if (options.frontend.includes("none")) {
|
|
1557
|
-
if (options.frontend.length > 1)
|
|
1558
|
-
consola$1.fatal(`Cannot combine 'none' with other frontend options.`);
|
|
1559
|
-
process.exit(1);
|
|
1560
|
-
}
|
|
1466
|
+
if (options.frontend.length > 1) exitWithError(`Cannot combine 'none' with other frontend options.`);
|
|
1561
1467
|
config.frontend = [];
|
|
1562
1468
|
} else {
|
|
1563
1469
|
const validOptions = options.frontend.filter((f) => f !== "none");
|
|
@@ -1566,17 +1472,11 @@ function processAndValidateFlags(options, providedFlags, projectName) {
|
|
|
1566
1472
|
}
|
|
1567
1473
|
if (providedFlags.has("api") && providedFlags.has("frontend") && config.api && config.frontend && config.frontend.length > 0) validateApiFrontendCompatibility(config.api, config.frontend);
|
|
1568
1474
|
if (options.addons && options.addons.length > 0) if (options.addons.includes("none")) {
|
|
1569
|
-
if (options.addons.length > 1)
|
|
1570
|
-
consola$1.fatal(`Cannot combine 'none' with other addons.`);
|
|
1571
|
-
process.exit(1);
|
|
1572
|
-
}
|
|
1475
|
+
if (options.addons.length > 1) exitWithError(`Cannot combine 'none' with other addons.`);
|
|
1573
1476
|
config.addons = [];
|
|
1574
1477
|
} else config.addons = options.addons.filter((addon) => addon !== "none");
|
|
1575
1478
|
if (options.examples && options.examples.length > 0) if (options.examples.includes("none")) {
|
|
1576
|
-
if (options.examples.length > 1)
|
|
1577
|
-
consola$1.fatal("Cannot combine 'none' with other examples.");
|
|
1578
|
-
process.exit(1);
|
|
1579
|
-
}
|
|
1479
|
+
if (options.examples.length > 1) exitWithError("Cannot combine 'none' with other examples.");
|
|
1580
1480
|
config.examples = [];
|
|
1581
1481
|
} else {
|
|
1582
1482
|
config.examples = options.examples.filter((ex) => ex !== "none");
|
|
@@ -1584,89 +1484,35 @@ function processAndValidateFlags(options, providedFlags, projectName) {
|
|
|
1584
1484
|
}
|
|
1585
1485
|
if (config.backend === "convex" || config.backend === "none") {
|
|
1586
1486
|
const incompatibleFlags = incompatibleFlagsForBackend(config.backend, providedFlags, options);
|
|
1587
|
-
if (incompatibleFlags.length > 0) {
|
|
1588
|
-
consola$1.fatal(`The following flags are incompatible with '--backend ${config.backend}': ${incompatibleFlags.join(", ")}. Please remove them.`);
|
|
1589
|
-
process.exit(1);
|
|
1590
|
-
}
|
|
1487
|
+
if (incompatibleFlags.length > 0) exitWithError(`The following flags are incompatible with '--backend ${config.backend}': ${incompatibleFlags.join(", ")}. Please remove them.`);
|
|
1591
1488
|
if (config.backend === "convex" && providedFlags.has("frontend") && options.frontend) {
|
|
1592
1489
|
const incompatibleFrontends = options.frontend.filter((f) => f === "solid");
|
|
1593
|
-
if (incompatibleFrontends.length > 0) {
|
|
1594
|
-
consola$1.fatal(`The following frontends are not compatible with '--backend convex': ${incompatibleFrontends.join(", ")}. Please choose a different frontend or backend.`);
|
|
1595
|
-
process.exit(1);
|
|
1596
|
-
}
|
|
1490
|
+
if (incompatibleFrontends.length > 0) exitWithError(`The following frontends are not compatible with '--backend convex': ${incompatibleFrontends.join(", ")}. Please choose a different frontend or backend.`);
|
|
1597
1491
|
}
|
|
1598
1492
|
coerceBackendPresets(config);
|
|
1599
1493
|
}
|
|
1600
|
-
if (providedFlags.has("orm") && providedFlags.has("database") && config.orm === "mongoose" && config.database !== "mongodb")
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
if (providedFlags.has("
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
if (providedFlags.has("
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
if (providedFlags.has("database") && providedFlags.has("orm") && config.database && config.database !== "none" && config.orm === "none") {
|
|
1613
|
-
consola$1.fatal("Database selection requires an ORM. Please choose '--orm drizzle', '--orm prisma', or '--orm mongoose'.");
|
|
1614
|
-
process.exit(1);
|
|
1615
|
-
}
|
|
1616
|
-
if (providedFlags.has("orm") && providedFlags.has("database") && config.orm && config.orm !== "none" && config.database === "none") {
|
|
1617
|
-
consola$1.fatal("ORM selection requires a database. Please choose a database or set '--orm none'.");
|
|
1618
|
-
process.exit(1);
|
|
1619
|
-
}
|
|
1620
|
-
if (providedFlags.has("auth") && providedFlags.has("database") && config.auth && config.database === "none") {
|
|
1621
|
-
consola$1.fatal("Authentication requires a database. Please choose a database or set '--no-auth'.");
|
|
1622
|
-
process.exit(1);
|
|
1623
|
-
}
|
|
1624
|
-
if (providedFlags.has("dbSetup") && providedFlags.has("database") && config.dbSetup && config.dbSetup !== "none" && config.database === "none") {
|
|
1625
|
-
consola$1.fatal("Database setup requires a database. Please choose a database or set '--db-setup none'.");
|
|
1626
|
-
process.exit(1);
|
|
1627
|
-
}
|
|
1628
|
-
if (providedFlags.has("dbSetup") && (config.database ? providedFlags.has("database") : true) && config.dbSetup === "turso" && config.database !== "sqlite") {
|
|
1629
|
-
consola$1.fatal("Turso setup requires SQLite database. Please use '--database sqlite' or choose a different setup.");
|
|
1630
|
-
process.exit(1);
|
|
1631
|
-
}
|
|
1632
|
-
if (providedFlags.has("dbSetup") && (config.database ? providedFlags.has("database") : true) && config.dbSetup === "neon" && config.database !== "postgres") {
|
|
1633
|
-
consola$1.fatal("Neon setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup.");
|
|
1634
|
-
process.exit(1);
|
|
1635
|
-
}
|
|
1636
|
-
if (providedFlags.has("dbSetup") && (config.database ? providedFlags.has("database") : true) && config.dbSetup === "prisma-postgres" && config.database !== "postgres") {
|
|
1637
|
-
consola$1.fatal("Prisma PostgreSQL setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup.");
|
|
1638
|
-
process.exit(1);
|
|
1639
|
-
}
|
|
1640
|
-
if (providedFlags.has("dbSetup") && (config.database ? providedFlags.has("database") : true) && config.dbSetup === "mongodb-atlas" && config.database !== "mongodb") {
|
|
1641
|
-
consola$1.fatal("MongoDB Atlas setup requires MongoDB database. Please use '--database mongodb' or choose a different setup.");
|
|
1642
|
-
process.exit(1);
|
|
1643
|
-
}
|
|
1644
|
-
if (providedFlags.has("dbSetup") && (config.database ? providedFlags.has("database") : true) && config.dbSetup === "supabase" && config.database !== "postgres") {
|
|
1645
|
-
consola$1.fatal("Supabase setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup.");
|
|
1646
|
-
process.exit(1);
|
|
1647
|
-
}
|
|
1494
|
+
if (providedFlags.has("orm") && providedFlags.has("database") && config.orm === "mongoose" && config.database !== "mongodb") exitWithError("Mongoose ORM requires MongoDB database. Please use '--database mongodb' or choose a different ORM.");
|
|
1495
|
+
if (providedFlags.has("database") && providedFlags.has("orm") && config.database === "mongodb" && config.orm && config.orm !== "mongoose" && config.orm !== "prisma") exitWithError("MongoDB database requires Mongoose or Prisma ORM. Please use '--orm mongoose' or '--orm prisma' or choose a different database.");
|
|
1496
|
+
if (providedFlags.has("orm") && providedFlags.has("database") && config.orm === "drizzle" && config.database === "mongodb") exitWithError("Drizzle ORM does not support MongoDB. Please use '--orm mongoose' or '--orm prisma' or choose a different database.");
|
|
1497
|
+
if (providedFlags.has("database") && providedFlags.has("orm") && config.database && config.database !== "none" && config.orm === "none") exitWithError("Database selection requires an ORM. Please choose '--orm drizzle', '--orm prisma', or '--orm mongoose'.");
|
|
1498
|
+
if (providedFlags.has("orm") && providedFlags.has("database") && config.orm && config.orm !== "none" && config.database === "none") exitWithError("ORM selection requires a database. Please choose a database or set '--orm none'.");
|
|
1499
|
+
if (providedFlags.has("auth") && providedFlags.has("database") && config.auth && config.database === "none") exitWithError("Authentication requires a database. Please choose a database or set '--no-auth'.");
|
|
1500
|
+
if (providedFlags.has("dbSetup") && providedFlags.has("database") && config.dbSetup && config.dbSetup !== "none" && config.database === "none") exitWithError("Database setup requires a database. Please choose a database or set '--db-setup none'.");
|
|
1501
|
+
if (providedFlags.has("dbSetup") && (config.database ? providedFlags.has("database") : true) && config.dbSetup === "turso" && config.database !== "sqlite") exitWithError("Turso setup requires SQLite database. Please use '--database sqlite' or choose a different setup.");
|
|
1502
|
+
if (providedFlags.has("dbSetup") && (config.database ? providedFlags.has("database") : true) && config.dbSetup === "neon" && config.database !== "postgres") exitWithError("Neon setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup.");
|
|
1503
|
+
if (providedFlags.has("dbSetup") && (config.database ? providedFlags.has("database") : true) && config.dbSetup === "prisma-postgres" && config.database !== "postgres") exitWithError("Prisma PostgreSQL setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup.");
|
|
1504
|
+
if (providedFlags.has("dbSetup") && (config.database ? providedFlags.has("database") : true) && config.dbSetup === "mongodb-atlas" && config.database !== "mongodb") exitWithError("MongoDB Atlas setup requires MongoDB database. Please use '--database mongodb' or choose a different setup.");
|
|
1505
|
+
if (providedFlags.has("dbSetup") && (config.database ? providedFlags.has("database") : true) && config.dbSetup === "supabase" && config.database !== "postgres") exitWithError("Supabase setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup.");
|
|
1648
1506
|
if (config.dbSetup === "d1") {
|
|
1649
1507
|
if (providedFlags.has("dbSetup") && providedFlags.has("database") || providedFlags.has("dbSetup") && !config.database) {
|
|
1650
|
-
if (config.database !== "sqlite")
|
|
1651
|
-
consola$1.fatal("Cloudflare D1 setup requires SQLite database. Please use '--database sqlite' or choose a different setup.");
|
|
1652
|
-
process.exit(1);
|
|
1653
|
-
}
|
|
1508
|
+
if (config.database !== "sqlite") exitWithError("Cloudflare D1 setup requires SQLite database. Please use '--database sqlite' or choose a different setup.");
|
|
1654
1509
|
}
|
|
1655
1510
|
if (providedFlags.has("dbSetup") && providedFlags.has("runtime") || providedFlags.has("dbSetup") && !config.runtime) {
|
|
1656
|
-
if (config.runtime !== "workers")
|
|
1657
|
-
consola$1.fatal("Cloudflare D1 setup requires the Cloudflare Workers runtime. Please use '--runtime workers' or choose a different setup.");
|
|
1658
|
-
process.exit(1);
|
|
1659
|
-
}
|
|
1511
|
+
if (config.runtime !== "workers") exitWithError("Cloudflare D1 setup requires the Cloudflare Workers runtime. Please use '--runtime workers' or choose a different setup.");
|
|
1660
1512
|
}
|
|
1661
1513
|
}
|
|
1662
|
-
if (providedFlags.has("dbSetup") && providedFlags.has("database") && config.dbSetup === "docker" && config.database === "sqlite")
|
|
1663
|
-
|
|
1664
|
-
process.exit(1);
|
|
1665
|
-
}
|
|
1666
|
-
if (providedFlags.has("dbSetup") && providedFlags.has("runtime") && config.dbSetup === "docker" && config.runtime === "workers") {
|
|
1667
|
-
consola$1.fatal("Docker setup is not compatible with Cloudflare Workers runtime. Workers runtime uses serverless databases (D1) and doesn't support local Docker containers. Please use '--db-setup d1' for SQLite or choose a different runtime.");
|
|
1668
|
-
process.exit(1);
|
|
1669
|
-
}
|
|
1514
|
+
if (providedFlags.has("dbSetup") && providedFlags.has("database") && config.dbSetup === "docker" && config.database === "sqlite") exitWithError("Docker setup is not compatible with SQLite database. SQLite is file-based and doesn't require Docker. Please use '--database postgres', '--database mysql', '--database mongodb', or choose a different setup.");
|
|
1515
|
+
if (providedFlags.has("dbSetup") && providedFlags.has("runtime") && config.dbSetup === "docker" && config.runtime === "workers") exitWithError("Docker setup is not compatible with Cloudflare Workers runtime. Workers runtime uses serverless databases (D1) and doesn't support local Docker containers. Please use '--db-setup d1' for SQLite or choose a different runtime.");
|
|
1670
1516
|
validateWorkersCompatibility(providedFlags, options, config);
|
|
1671
1517
|
const hasWebFrontendFlag = (config.frontend ?? []).some((f) => isWebFrontend(f));
|
|
1672
1518
|
validateWebDeployRequiresWebFrontend(config.webDeploy, hasWebFrontendFlag);
|
|
@@ -1840,10 +1686,7 @@ async function setupFumadocs(config) {
|
|
|
1840
1686
|
})),
|
|
1841
1687
|
initialValue: "next-mdx"
|
|
1842
1688
|
});
|
|
1843
|
-
if (isCancel(template))
|
|
1844
|
-
cancel(pc.red("Operation cancelled"));
|
|
1845
|
-
process.exit(0);
|
|
1846
|
-
}
|
|
1689
|
+
if (isCancel(template)) return exitCancelled("Operation cancelled");
|
|
1847
1690
|
const templateArg = TEMPLATES[template].value;
|
|
1848
1691
|
const commandWithArgs = `create-fumadocs-app@latest fumadocs --template ${templateArg} --src --no-install --pm ${packageManager} --no-eslint --no-git`;
|
|
1849
1692
|
const fumadocsInitCommand = getPackageExecutionCommand(packageManager, commandWithArgs);
|
|
@@ -2007,10 +1850,7 @@ async function setupUltracite(config, hasHusky) {
|
|
|
2007
1850
|
})),
|
|
2008
1851
|
required: false
|
|
2009
1852
|
});
|
|
2010
|
-
if (isCancel(editors))
|
|
2011
|
-
cancel(pc.red("Operation cancelled"));
|
|
2012
|
-
process.exit(0);
|
|
2013
|
-
}
|
|
1853
|
+
if (isCancel(editors)) return exitCancelled("Operation cancelled");
|
|
2014
1854
|
const rules = await multiselect({
|
|
2015
1855
|
message: "Choose rules",
|
|
2016
1856
|
options: Object.entries(RULES).map(([key, rule]) => ({
|
|
@@ -2020,10 +1860,7 @@ async function setupUltracite(config, hasHusky) {
|
|
|
2020
1860
|
})),
|
|
2021
1861
|
required: false
|
|
2022
1862
|
});
|
|
2023
|
-
if (isCancel(rules))
|
|
2024
|
-
cancel(pc.red("Operation cancelled"));
|
|
2025
|
-
process.exit(0);
|
|
2026
|
-
}
|
|
1863
|
+
if (isCancel(rules)) return exitCancelled("Operation cancelled");
|
|
2027
1864
|
const ultraciteArgs = [
|
|
2028
1865
|
"init",
|
|
2029
1866
|
"--pm",
|
|
@@ -2055,6 +1892,135 @@ async function setupUltracite(config, hasHusky) {
|
|
|
2055
1892
|
}
|
|
2056
1893
|
}
|
|
2057
1894
|
|
|
1895
|
+
//#endregion
|
|
1896
|
+
//#region src/utils/template-processor.ts
|
|
1897
|
+
/**
|
|
1898
|
+
* Processes a Handlebars template file and writes the output to the destination.
|
|
1899
|
+
* @param srcPath Path to the source .hbs template file.
|
|
1900
|
+
* @param destPath Path to write the processed file.
|
|
1901
|
+
* @param context Data to be passed to the Handlebars template.
|
|
1902
|
+
*/
|
|
1903
|
+
async function processTemplate(srcPath, destPath, context) {
|
|
1904
|
+
try {
|
|
1905
|
+
const templateContent = await fs.readFile(srcPath, "utf-8");
|
|
1906
|
+
const template = handlebars.compile(templateContent);
|
|
1907
|
+
const processedContent = template(context);
|
|
1908
|
+
await fs.ensureDir(path.dirname(destPath));
|
|
1909
|
+
await fs.writeFile(destPath, processedContent);
|
|
1910
|
+
} catch (error) {
|
|
1911
|
+
consola.error(`Error processing template ${srcPath}:`, error);
|
|
1912
|
+
throw new Error(`Failed to process template ${srcPath}`);
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
handlebars.registerHelper("eq", (a, b) => a === b);
|
|
1916
|
+
handlebars.registerHelper("ne", (a, b) => a !== b);
|
|
1917
|
+
handlebars.registerHelper("and", (a, b) => a && b);
|
|
1918
|
+
handlebars.registerHelper("or", (a, b) => a || b);
|
|
1919
|
+
handlebars.registerHelper("includes", (array, value) => Array.isArray(array) && array.includes(value));
|
|
1920
|
+
|
|
1921
|
+
//#endregion
|
|
1922
|
+
//#region src/helpers/setup/vibe-rules-setup.ts
|
|
1923
|
+
async function setupVibeRules(config) {
|
|
1924
|
+
const { packageManager, projectDir } = config;
|
|
1925
|
+
try {
|
|
1926
|
+
log.info("Setting up vibe-rules...");
|
|
1927
|
+
const rulesDir = path.join(projectDir, ".bts");
|
|
1928
|
+
const ruleFile = path.join(rulesDir, "rules.md");
|
|
1929
|
+
if (!await fs.pathExists(ruleFile)) {
|
|
1930
|
+
const templatePath = path.join(PKG_ROOT, "templates", "addons", "vibe-rules", ".bts", "rules.md.hbs");
|
|
1931
|
+
if (await fs.pathExists(templatePath)) {
|
|
1932
|
+
await fs.ensureDir(rulesDir);
|
|
1933
|
+
await processTemplate(templatePath, ruleFile, config);
|
|
1934
|
+
} else {
|
|
1935
|
+
log.error(pc.red("Rules template not found for vibe-rules addon"));
|
|
1936
|
+
return;
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
const EDITORS$1 = {
|
|
1940
|
+
cursor: {
|
|
1941
|
+
label: "Cursor",
|
|
1942
|
+
hint: ".cursor/rules/*.mdc"
|
|
1943
|
+
},
|
|
1944
|
+
windsurf: {
|
|
1945
|
+
label: "Windsurf",
|
|
1946
|
+
hint: ".windsurfrules"
|
|
1947
|
+
},
|
|
1948
|
+
"claude-code": {
|
|
1949
|
+
label: "Claude Code",
|
|
1950
|
+
hint: "CLAUDE.md"
|
|
1951
|
+
},
|
|
1952
|
+
vscode: {
|
|
1953
|
+
label: "VSCode",
|
|
1954
|
+
hint: ".github/instructions/*.instructions.md"
|
|
1955
|
+
},
|
|
1956
|
+
gemini: {
|
|
1957
|
+
label: "Gemini",
|
|
1958
|
+
hint: "GEMINI.md"
|
|
1959
|
+
},
|
|
1960
|
+
codex: {
|
|
1961
|
+
label: "Codex",
|
|
1962
|
+
hint: "AGENTS.md"
|
|
1963
|
+
},
|
|
1964
|
+
clinerules: {
|
|
1965
|
+
label: "Cline/Roo",
|
|
1966
|
+
hint: ".clinerules/*.md"
|
|
1967
|
+
},
|
|
1968
|
+
roo: {
|
|
1969
|
+
label: "Roo",
|
|
1970
|
+
hint: ".clinerules/*.md"
|
|
1971
|
+
},
|
|
1972
|
+
zed: {
|
|
1973
|
+
label: "Zed",
|
|
1974
|
+
hint: ".rules/*.md"
|
|
1975
|
+
},
|
|
1976
|
+
unified: {
|
|
1977
|
+
label: "Unified",
|
|
1978
|
+
hint: ".rules/*.md"
|
|
1979
|
+
}
|
|
1980
|
+
};
|
|
1981
|
+
const selectedEditors = await multiselect({
|
|
1982
|
+
message: "Choose editors to install BTS rule",
|
|
1983
|
+
options: Object.entries(EDITORS$1).map(([key, v]) => ({
|
|
1984
|
+
value: key,
|
|
1985
|
+
label: v.label,
|
|
1986
|
+
hint: v.hint
|
|
1987
|
+
})),
|
|
1988
|
+
required: false
|
|
1989
|
+
});
|
|
1990
|
+
if (isCancel(selectedEditors)) return exitCancelled("Operation cancelled");
|
|
1991
|
+
const editorsArg = selectedEditors.join(", ");
|
|
1992
|
+
const s = spinner();
|
|
1993
|
+
s.start("Saving and applying BTS rules...");
|
|
1994
|
+
try {
|
|
1995
|
+
const saveCmd = getPackageExecutionCommand(packageManager, `vibe-rules@latest save bts -f ${JSON.stringify(path.relative(projectDir, ruleFile))}`);
|
|
1996
|
+
await execa(saveCmd, {
|
|
1997
|
+
cwd: projectDir,
|
|
1998
|
+
env: { CI: "true" },
|
|
1999
|
+
shell: true
|
|
2000
|
+
});
|
|
2001
|
+
for (const editor of selectedEditors) {
|
|
2002
|
+
const loadCmd = getPackageExecutionCommand(packageManager, `vibe-rules@latest load bts ${editor}`);
|
|
2003
|
+
await execa(loadCmd, {
|
|
2004
|
+
cwd: projectDir,
|
|
2005
|
+
env: { CI: "true" },
|
|
2006
|
+
shell: true
|
|
2007
|
+
});
|
|
2008
|
+
}
|
|
2009
|
+
s.stop(`Applied BTS rules to: ${editorsArg}`);
|
|
2010
|
+
} catch (error) {
|
|
2011
|
+
s.stop(pc.red("Failed to apply BTS rules"));
|
|
2012
|
+
throw error;
|
|
2013
|
+
}
|
|
2014
|
+
try {
|
|
2015
|
+
await fs.remove(rulesDir);
|
|
2016
|
+
} catch (_) {}
|
|
2017
|
+
log.success("vibe-rules setup successfully!");
|
|
2018
|
+
} catch (error) {
|
|
2019
|
+
log.error(pc.red("Failed to set up vibe-rules"));
|
|
2020
|
+
if (error instanceof Error) console.error(pc.red(error.message));
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2058
2024
|
//#endregion
|
|
2059
2025
|
//#region src/utils/ts-morph.ts
|
|
2060
2026
|
const tsProject = new Project({
|
|
@@ -2146,6 +2112,7 @@ ${pc.cyan("Docs:")} ${pc.underline("https://turborepo.com/docs")}
|
|
|
2146
2112
|
}
|
|
2147
2113
|
if (addons.includes("oxlint")) await setupOxlint(projectDir, packageManager);
|
|
2148
2114
|
if (addons.includes("starlight")) await setupStarlight(config);
|
|
2115
|
+
if (addons.includes("vibe-rules")) await setupVibeRules(config);
|
|
2149
2116
|
if (addons.includes("fumadocs")) await setupFumadocs(config);
|
|
2150
2117
|
}
|
|
2151
2118
|
function getWebAppDir(projectDir, frontends) {
|
|
@@ -2290,31 +2257,6 @@ async function installDependencies({ projectDir, packageManager }) {
|
|
|
2290
2257
|
}
|
|
2291
2258
|
}
|
|
2292
2259
|
|
|
2293
|
-
//#endregion
|
|
2294
|
-
//#region src/utils/template-processor.ts
|
|
2295
|
-
/**
|
|
2296
|
-
* Processes a Handlebars template file and writes the output to the destination.
|
|
2297
|
-
* @param srcPath Path to the source .hbs template file.
|
|
2298
|
-
* @param destPath Path to write the processed file.
|
|
2299
|
-
* @param context Data to be passed to the Handlebars template.
|
|
2300
|
-
*/
|
|
2301
|
-
async function processTemplate(srcPath, destPath, context) {
|
|
2302
|
-
try {
|
|
2303
|
-
const templateContent = await fs.readFile(srcPath, "utf-8");
|
|
2304
|
-
const template = handlebars.compile(templateContent);
|
|
2305
|
-
const processedContent = template(context);
|
|
2306
|
-
await fs.ensureDir(path.dirname(destPath));
|
|
2307
|
-
await fs.writeFile(destPath, processedContent);
|
|
2308
|
-
} catch (error) {
|
|
2309
|
-
consola.error(`Error processing template ${srcPath}:`, error);
|
|
2310
|
-
throw new Error(`Failed to process template ${srcPath}`);
|
|
2311
|
-
}
|
|
2312
|
-
}
|
|
2313
|
-
handlebars.registerHelper("eq", (a, b) => a === b);
|
|
2314
|
-
handlebars.registerHelper("and", (a, b) => a && b);
|
|
2315
|
-
handlebars.registerHelper("or", (a, b) => a || b);
|
|
2316
|
-
handlebars.registerHelper("includes", (array, value) => Array.isArray(array) && array.includes(value));
|
|
2317
|
-
|
|
2318
2260
|
//#endregion
|
|
2319
2261
|
//#region src/helpers/project-generation/template-manager.ts
|
|
2320
2262
|
async function processAndCopyFiles(sourcePattern, baseSourceDir, destDir, context, overwrite = true) {
|
|
@@ -2525,6 +2467,7 @@ async function setupAddonsTemplate(projectDir, context) {
|
|
|
2525
2467
|
if (!context.addons || context.addons.length === 0) return;
|
|
2526
2468
|
for (const addon of context.addons) {
|
|
2527
2469
|
if (addon === "none") continue;
|
|
2470
|
+
if (addon === "vibe-rules") continue;
|
|
2528
2471
|
let addonSrcDir = path.join(PKG_ROOT, `templates/addons/${addon}`);
|
|
2529
2472
|
let addonDestDir = projectDir;
|
|
2530
2473
|
if (addon === "pwa") {
|
|
@@ -2685,17 +2628,13 @@ async function setupDeploymentTemplates(projectDir, context) {
|
|
|
2685
2628
|
|
|
2686
2629
|
//#endregion
|
|
2687
2630
|
//#region src/helpers/project-generation/add-addons.ts
|
|
2688
|
-
function exitWithError$1(message) {
|
|
2689
|
-
cancel(pc.red(message));
|
|
2690
|
-
process.exit(1);
|
|
2691
|
-
}
|
|
2692
2631
|
async function addAddonsToProject(input) {
|
|
2693
2632
|
try {
|
|
2694
2633
|
const projectDir = input.projectDir || process.cwd();
|
|
2695
2634
|
const isBetterTStack = await isBetterTStackProject(projectDir);
|
|
2696
|
-
if (!isBetterTStack) exitWithError
|
|
2635
|
+
if (!isBetterTStack) exitWithError("This doesn't appear to be a Better-T Stack project. Please run this command from the root of a Better-T Stack project.");
|
|
2697
2636
|
const detectedConfig = await detectProjectConfig(projectDir);
|
|
2698
|
-
if (!detectedConfig) exitWithError
|
|
2637
|
+
if (!detectedConfig) exitWithError("Could not detect the project configuration. Please ensure this is a valid Better-T Stack project.");
|
|
2699
2638
|
const config = {
|
|
2700
2639
|
projectName: detectedConfig.projectName || path.basename(projectDir),
|
|
2701
2640
|
projectDir,
|
|
@@ -2717,9 +2656,8 @@ async function addAddonsToProject(input) {
|
|
|
2717
2656
|
};
|
|
2718
2657
|
for (const addon of input.addons) {
|
|
2719
2658
|
const { isCompatible, reason } = validateAddonCompatibility(addon, config.frontend);
|
|
2720
|
-
if (!isCompatible) exitWithError
|
|
2659
|
+
if (!isCompatible) exitWithError(reason || `${addon} addon is not compatible with current frontend configuration`);
|
|
2721
2660
|
}
|
|
2722
|
-
log.info(`Adding ${input.addons.join(", ")} to ${config.frontend.join("/")}`);
|
|
2723
2661
|
await setupAddonsTemplate(projectDir, config);
|
|
2724
2662
|
await setupAddons(config, true);
|
|
2725
2663
|
const currentAddons = detectedConfig.addons || [];
|
|
@@ -2732,7 +2670,7 @@ async function addAddonsToProject(input) {
|
|
|
2732
2670
|
else if (!input.suppressInstallMessage) log.info(pc.yellow(`Run ${pc.bold(`${config.packageManager} install`)} to install dependencies`));
|
|
2733
2671
|
} catch (error) {
|
|
2734
2672
|
const message = error instanceof Error ? error.message : String(error);
|
|
2735
|
-
exitWithError
|
|
2673
|
+
exitWithError(`Error adding addons: ${message}`);
|
|
2736
2674
|
}
|
|
2737
2675
|
}
|
|
2738
2676
|
|
|
@@ -2965,10 +2903,6 @@ async function setupNextWorkersDeploy(projectDir, _packageManager) {
|
|
|
2965
2903
|
|
|
2966
2904
|
//#endregion
|
|
2967
2905
|
//#region src/helpers/project-generation/add-deployment.ts
|
|
2968
|
-
function exitWithError(message) {
|
|
2969
|
-
cancel(pc.red(message));
|
|
2970
|
-
process.exit(1);
|
|
2971
|
-
}
|
|
2972
2906
|
async function addDeploymentToProject(input) {
|
|
2973
2907
|
try {
|
|
2974
2908
|
const projectDir = input.projectDir || process.cwd();
|
|
@@ -3768,10 +3702,7 @@ async function setupNeonPostgres(config) {
|
|
|
3768
3702
|
}],
|
|
3769
3703
|
initialValue: "neondb"
|
|
3770
3704
|
});
|
|
3771
|
-
if (isCancel(setupMethod))
|
|
3772
|
-
cancel(pc.red("Operation cancelled"));
|
|
3773
|
-
process.exit(0);
|
|
3774
|
-
}
|
|
3705
|
+
if (isCancel(setupMethod)) return exitCancelled("Operation cancelled");
|
|
3775
3706
|
if (setupMethod === "neondb") await setupWithNeonDb(projectDir, packageManager);
|
|
3776
3707
|
else {
|
|
3777
3708
|
const suggestedProjectName = path.basename(projectDir);
|
|
@@ -3785,10 +3716,7 @@ async function setupNeonPostgres(config) {
|
|
|
3785
3716
|
options: NEON_REGIONS,
|
|
3786
3717
|
initialValue: NEON_REGIONS[0].value
|
|
3787
3718
|
});
|
|
3788
|
-
if (isCancel(projectName) || isCancel(regionId))
|
|
3789
|
-
cancel(pc.red("Operation cancelled"));
|
|
3790
|
-
process.exit(0);
|
|
3791
|
-
}
|
|
3719
|
+
if (isCancel(projectName) || isCancel(regionId)) return exitCancelled("Operation cancelled");
|
|
3792
3720
|
const neonConfig = await createNeonProject(projectName, regionId, packageManager);
|
|
3793
3721
|
if (!neonConfig) throw new Error("Failed to create project - couldn't get connection information");
|
|
3794
3722
|
const finalSpinner = spinner();
|
|
@@ -3823,10 +3751,7 @@ async function setupWithCreateDb(serverDir, packageManager, orm) {
|
|
|
3823
3751
|
if (orm === "drizzle" && !value.includes("?sslmode=require")) return "Please append ?sslmode=require to your database URL when using Drizzle";
|
|
3824
3752
|
}
|
|
3825
3753
|
});
|
|
3826
|
-
if (isCancel(databaseUrl))
|
|
3827
|
-
cancel("Database setup cancelled");
|
|
3828
|
-
return null;
|
|
3829
|
-
}
|
|
3754
|
+
if (isCancel(databaseUrl)) return null;
|
|
3830
3755
|
return { databaseUrl };
|
|
3831
3756
|
} catch (error) {
|
|
3832
3757
|
if (error instanceof Error) consola$1.error(error.message);
|
|
@@ -3852,10 +3777,7 @@ async function initPrismaDatabase(serverDir, packageManager) {
|
|
|
3852
3777
|
if (!value.startsWith("prisma+postgres://")) return "URL should start with prisma+postgres://";
|
|
3853
3778
|
}
|
|
3854
3779
|
});
|
|
3855
|
-
if (isCancel(databaseUrl))
|
|
3856
|
-
cancel("Database setup cancelled");
|
|
3857
|
-
return null;
|
|
3858
|
-
}
|
|
3780
|
+
if (isCancel(databaseUrl)) return null;
|
|
3859
3781
|
return { databaseUrl };
|
|
3860
3782
|
} catch (error) {
|
|
3861
3783
|
if (error instanceof Error) consola$1.error(error.message);
|
|
@@ -3946,10 +3868,7 @@ async function setupPrismaPostgres(config) {
|
|
|
3946
3868
|
options: setupOptions,
|
|
3947
3869
|
initialValue: "create-db"
|
|
3948
3870
|
});
|
|
3949
|
-
if (isCancel(setupMethod))
|
|
3950
|
-
cancel(pc.red("Operation cancelled"));
|
|
3951
|
-
process.exit(0);
|
|
3952
|
-
}
|
|
3871
|
+
if (isCancel(setupMethod)) return exitCancelled("Operation cancelled");
|
|
3953
3872
|
let prismaConfig = null;
|
|
3954
3873
|
if (setupMethod === "create-db") prismaConfig = await setupWithCreateDb(serverDir, packageManager, orm);
|
|
3955
3874
|
else prismaConfig = await initPrismaDatabase(serverDir, packageManager);
|
|
@@ -4183,10 +4102,7 @@ async function selectTursoGroup() {
|
|
|
4183
4102
|
message: "Select a Turso database group:",
|
|
4184
4103
|
options: groupOptions
|
|
4185
4104
|
});
|
|
4186
|
-
if (isCancel(selectedGroup))
|
|
4187
|
-
cancel(pc.red("Operation cancelled"));
|
|
4188
|
-
process.exit(0);
|
|
4189
|
-
}
|
|
4105
|
+
if (isCancel(selectedGroup)) return exitCancelled("Operation cancelled");
|
|
4190
4106
|
return selectedGroup;
|
|
4191
4107
|
}
|
|
4192
4108
|
async function createTursoDatabase(dbName, groupName) {
|
|
@@ -4259,10 +4175,7 @@ async function setupTurso(config) {
|
|
|
4259
4175
|
message: "Would you like to install Turso CLI?",
|
|
4260
4176
|
initialValue: true
|
|
4261
4177
|
});
|
|
4262
|
-
if (isCancel(shouldInstall))
|
|
4263
|
-
cancel(pc.red("Operation cancelled"));
|
|
4264
|
-
process.exit(0);
|
|
4265
|
-
}
|
|
4178
|
+
if (isCancel(shouldInstall)) return exitCancelled("Operation cancelled");
|
|
4266
4179
|
if (!shouldInstall) {
|
|
4267
4180
|
await writeEnvFile(projectDir);
|
|
4268
4181
|
displayManualSetupInstructions();
|
|
@@ -4283,10 +4196,7 @@ async function setupTurso(config) {
|
|
|
4283
4196
|
initialValue: suggestedName,
|
|
4284
4197
|
placeholder: suggestedName
|
|
4285
4198
|
});
|
|
4286
|
-
if (isCancel(dbNameResponse))
|
|
4287
|
-
cancel(pc.red("Operation cancelled"));
|
|
4288
|
-
process.exit(0);
|
|
4289
|
-
}
|
|
4199
|
+
if (isCancel(dbNameResponse)) return exitCancelled("Operation cancelled");
|
|
4290
4200
|
dbName = dbNameResponse;
|
|
4291
4201
|
try {
|
|
4292
4202
|
const config$1 = await createTursoDatabase(dbName, selectedGroup);
|
|
@@ -5203,13 +5113,11 @@ async function createProject(options) {
|
|
|
5203
5113
|
return projectDir;
|
|
5204
5114
|
} catch (error) {
|
|
5205
5115
|
if (error instanceof Error) {
|
|
5206
|
-
cancel(pc.red(`Error during project creation: ${error.message}`));
|
|
5207
5116
|
console.error(error.stack);
|
|
5208
|
-
|
|
5117
|
+
exitWithError(`Error during project creation: ${error.message}`);
|
|
5209
5118
|
} else {
|
|
5210
|
-
cancel(pc.red(`An unexpected error occurred: ${String(error)}`));
|
|
5211
5119
|
console.error(error);
|
|
5212
|
-
|
|
5120
|
+
exitWithError(`An unexpected error occurred: ${String(error)}`);
|
|
5213
5121
|
}
|
|
5214
5122
|
}
|
|
5215
5123
|
}
|
|
@@ -5268,18 +5176,14 @@ async function createProjectHandler(input) {
|
|
|
5268
5176
|
const elapsedTimeInSeconds = ((Date.now() - startTime) / 1e3).toFixed(2);
|
|
5269
5177
|
outro(pc.magenta(`Project created successfully in ${pc.bold(elapsedTimeInSeconds)} seconds!`));
|
|
5270
5178
|
} catch (error) {
|
|
5271
|
-
|
|
5272
|
-
process.exit(1);
|
|
5179
|
+
handleError(error, "Failed to create project");
|
|
5273
5180
|
}
|
|
5274
5181
|
}
|
|
5275
5182
|
async function addAddonsHandler(input) {
|
|
5276
5183
|
try {
|
|
5277
5184
|
const projectDir = input.projectDir || process.cwd();
|
|
5278
5185
|
const detectedConfig = await detectProjectConfig(projectDir);
|
|
5279
|
-
if (!detectedConfig)
|
|
5280
|
-
cancel(pc.red("Could not detect project configuration. Please ensure this is a valid Better-T Stack project."));
|
|
5281
|
-
process.exit(1);
|
|
5282
|
-
}
|
|
5186
|
+
if (!detectedConfig) exitWithError("Could not detect project configuration. Please ensure this is a valid Better-T Stack project.");
|
|
5283
5187
|
if (!input.addons || input.addons.length === 0) {
|
|
5284
5188
|
const addonsPrompt = await getAddonsToAdd(detectedConfig.frontend || [], detectedConfig.addons || []);
|
|
5285
5189
|
if (addonsPrompt.length > 0) input.addons = addonsPrompt;
|
|
@@ -5319,8 +5223,7 @@ async function addAddonsHandler(input) {
|
|
|
5319
5223
|
else log.info(`Run ${pc.bold(`${packageManager} install`)} to install dependencies`);
|
|
5320
5224
|
outro("Add command completed successfully!");
|
|
5321
5225
|
} catch (error) {
|
|
5322
|
-
|
|
5323
|
-
process.exit(1);
|
|
5226
|
+
handleError(error, "Failed to add addons or deployment");
|
|
5324
5227
|
}
|
|
5325
5228
|
}
|
|
5326
5229
|
|
|
@@ -5435,8 +5338,7 @@ const router = t.router({
|
|
|
5435
5338
|
const sponsors = await fetchSponsors();
|
|
5436
5339
|
displaySponsors(sponsors);
|
|
5437
5340
|
} catch (error) {
|
|
5438
|
-
|
|
5439
|
-
process.exit(1);
|
|
5341
|
+
handleError(error, "Failed to display sponsors");
|
|
5440
5342
|
}
|
|
5441
5343
|
}),
|
|
5442
5344
|
docs: t.procedure.meta({ description: "Open Better-T Stack documentation" }).mutation(async () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-better-t-stack",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.30.0",
|
|
4
4
|
"description": "A modern CLI tool for scaffolding end-to-end type-safe TypeScript projects with best practices and customizable configurations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# Better-T-Stack Project Rules
|
|
2
|
+
|
|
3
|
+
This is a {{projectName}} project created with Better-T-Stack CLI.
|
|
4
|
+
|
|
5
|
+
## Project Structure
|
|
6
|
+
|
|
7
|
+
This is a monorepo with the following structure:
|
|
8
|
+
|
|
9
|
+
{{#if (or (includes frontend "tanstack-router") (includes frontend "react-router") (includes frontend "tanstack-start")
|
|
10
|
+
(includes frontend "next") (includes frontend "nuxt") (includes frontend "svelte") (includes frontend "solid"))}}
|
|
11
|
+
- **`apps/web/`** - Frontend application{{#if (includes frontend "tanstack-router")}} (React with TanStack Router){{else
|
|
12
|
+
if (includes frontend "react-router")}} (React with React Router){{else if (includes frontend "next")}} (Next.js){{else
|
|
13
|
+
if (includes frontend "nuxt")}} (Nuxt.js){{else if (includes frontend "svelte")}} (SvelteKit){{else if (includes
|
|
14
|
+
frontend "solid")}} (SolidStart){{/if}}
|
|
15
|
+
{{/if}}
|
|
16
|
+
|
|
17
|
+
{{#if (ne backend "convex")}}
|
|
18
|
+
{{#if (ne backend "none")}}
|
|
19
|
+
- **`apps/server/`** - Backend server{{#if (eq backend "hono")}} (Hono){{else if (eq backend "express")}}
|
|
20
|
+
(Express){{else if (eq backend "fastify")}} (Fastify){{else if (eq backend "elysia")}} (Elysia){{else if (eq backend
|
|
21
|
+
"next")}} (Next.js API){{/if}}
|
|
22
|
+
{{/if}}
|
|
23
|
+
{{else}}
|
|
24
|
+
- **`packages/backend/`** - Convex backend functions
|
|
25
|
+
{{/if}}
|
|
26
|
+
|
|
27
|
+
{{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}}
|
|
28
|
+
- **`apps/native/`** - React Native mobile app{{#if (includes frontend "native-nativewind")}} (with NativeWind){{else if
|
|
29
|
+
(includes frontend "native-unistyles")}} (with Unistyles){{/if}}
|
|
30
|
+
{{/if}}
|
|
31
|
+
|
|
32
|
+
## Available Scripts
|
|
33
|
+
|
|
34
|
+
- `{{packageManager}} run dev` - Start all apps in development mode
|
|
35
|
+
{{#if (or (includes frontend "tanstack-router") (includes frontend "react-router") (includes frontend "tanstack-start")
|
|
36
|
+
(includes frontend "next") (includes frontend "nuxt") (includes frontend "svelte") (includes frontend "solid"))}}
|
|
37
|
+
- `{{packageManager}} run dev:web` - Start only the web app
|
|
38
|
+
{{/if}}
|
|
39
|
+
{{#if (ne backend "none")}}
|
|
40
|
+
{{#if (ne backend "convex")}}
|
|
41
|
+
- `{{packageManager}} run dev:server` - Start only the server
|
|
42
|
+
{{/if}}
|
|
43
|
+
{{/if}}
|
|
44
|
+
{{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}}
|
|
45
|
+
- `{{packageManager}} run dev:native` - Start only the native app
|
|
46
|
+
{{/if}}
|
|
47
|
+
|
|
48
|
+
{{#if (and (ne database "none") (ne orm "none") (ne backend "convex"))}}
|
|
49
|
+
## Database Commands
|
|
50
|
+
|
|
51
|
+
All database operations should be run from the server workspace:
|
|
52
|
+
|
|
53
|
+
- `{{packageManager}} run db:push` - Push schema changes to database
|
|
54
|
+
- `{{packageManager}} run db:studio` - Open database studio
|
|
55
|
+
- `{{packageManager}} run db:generate` - Generate {{#if (eq orm "drizzle")}}Drizzle{{else if (eq orm
|
|
56
|
+
"prisma")}}Prisma{{else}}{{orm}}{{/if}} files
|
|
57
|
+
- `{{packageManager}} run db:migrate` - Run database migrations
|
|
58
|
+
|
|
59
|
+
{{#if (eq orm "drizzle")}}
|
|
60
|
+
Database schema files are located in `apps/server/src/db/schema/`
|
|
61
|
+
{{else if (eq orm "prisma")}}
|
|
62
|
+
Database schema is located in `apps/server/prisma/schema.prisma`
|
|
63
|
+
{{else if (eq orm "mongoose")}}
|
|
64
|
+
Database models are located in `apps/server/src/db/models/`
|
|
65
|
+
{{/if}}
|
|
66
|
+
{{/if}}
|
|
67
|
+
|
|
68
|
+
{{#if (ne api "none")}}
|
|
69
|
+
## API Structure
|
|
70
|
+
|
|
71
|
+
{{#if (eq api "trpc")}}
|
|
72
|
+
- tRPC routers are in `apps/server/src/routers/`
|
|
73
|
+
- Client-side tRPC utils are in `apps/web/src/utils/trpc.ts`
|
|
74
|
+
{{else if (eq api "orpc")}}
|
|
75
|
+
- oRPC endpoints are in `apps/server/src/api/`
|
|
76
|
+
- Client-side API utils are in `apps/web/src/utils/api.ts`
|
|
77
|
+
{{/if}}
|
|
78
|
+
{{/if}}
|
|
79
|
+
|
|
80
|
+
{{#if auth}}
|
|
81
|
+
## Authentication
|
|
82
|
+
|
|
83
|
+
Authentication is enabled in this project:
|
|
84
|
+
{{#if (ne backend "convex")}}
|
|
85
|
+
- Server auth logic is in `apps/server/src/lib/auth.ts`
|
|
86
|
+
{{#if (or (includes frontend "tanstack-router") (includes frontend "react-router") (includes frontend "tanstack-start")
|
|
87
|
+
(includes frontend "next") (includes frontend "nuxt") (includes frontend "svelte") (includes frontend "solid"))}}
|
|
88
|
+
- Web app auth client is in `apps/web/src/lib/auth-client.ts`
|
|
89
|
+
{{/if}}
|
|
90
|
+
{{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}}
|
|
91
|
+
- Native app auth client is in `apps/native/src/lib/auth-client.ts`
|
|
92
|
+
{{/if}}
|
|
93
|
+
{{else}}
|
|
94
|
+
{{/if}}
|
|
95
|
+
{{/if}}
|
|
96
|
+
|
|
97
|
+
## Adding More Features
|
|
98
|
+
|
|
99
|
+
You can add additional addons or deployment options to your project using:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
{{#if (eq packageManager "bun")}}bunx{{else if (eq packageManager "pnpm")}}pnpx{{else}}npx{{/if}} create-better-t-stack
|
|
103
|
+
add
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Available addons you can add:
|
|
107
|
+
- **Documentation**: Starlight, Fumadocs
|
|
108
|
+
- **Linting**: Biome, Oxlint, Ultracite
|
|
109
|
+
- **Other**: vibe-rules, Turborepo, PWA, Tauri, Husky
|
|
110
|
+
|
|
111
|
+
You can also add web deployment configurations like Cloudflare Workers support.
|
|
112
|
+
|
|
113
|
+
## Project Configuration
|
|
114
|
+
|
|
115
|
+
This project includes a `bts.jsonc` configuration file that stores your Better-T-Stack settings:
|
|
116
|
+
|
|
117
|
+
- Contains your selected stack configuration (database, ORM, backend, frontend, etc.)
|
|
118
|
+
- Used by the CLI to understand your project structure
|
|
119
|
+
- Safe to delete if not needed
|
|
120
|
+
- Updated automatically when using the `add` command
|
|
121
|
+
|
|
122
|
+
## Key Points
|
|
123
|
+
|
|
124
|
+
- This is a {{#if (includes addons "turborepo")}}Turborepo {{/if}}monorepo using {{packageManager}} workspaces
|
|
125
|
+
- Each app has its own `package.json` and dependencies
|
|
126
|
+
- Run commands from the root to execute across all workspaces
|
|
127
|
+
- Run workspace-specific commands with `{{packageManager}} run command-name`
|
|
128
|
+
{{#if (includes addons "turborepo")}}
|
|
129
|
+
- Turborepo handles build caching and parallel execution
|
|
130
|
+
{{/if}}
|
|
131
|
+
- Use `{{#if (eq packageManager "bun")}}bunx{{else if (eq packageManager "pnpm")}}pnpx{{else}}npx{{/if}}
|
|
132
|
+
create-better-t-stack add` to add more features later
|