create-better-t-stack 2.29.3 → 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/README.md +10 -13
- package/dist/index.js +391 -423
- 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,19 +136,11 @@ const ADDON_COMPATIBILITY = {
|
|
|
136
136
|
turborepo: [],
|
|
137
137
|
starlight: [],
|
|
138
138
|
ultracite: [],
|
|
139
|
+
"vibe-rules": [],
|
|
139
140
|
oxlint: [],
|
|
140
141
|
fumadocs: [],
|
|
141
142
|
none: []
|
|
142
143
|
};
|
|
143
|
-
const WEB_FRAMEWORKS = [
|
|
144
|
-
"tanstack-router",
|
|
145
|
-
"react-router",
|
|
146
|
-
"tanstack-start",
|
|
147
|
-
"next",
|
|
148
|
-
"nuxt",
|
|
149
|
-
"svelte",
|
|
150
|
-
"solid"
|
|
151
|
-
];
|
|
152
144
|
|
|
153
145
|
//#endregion
|
|
154
146
|
//#region src/types.ts
|
|
@@ -179,7 +171,7 @@ const RuntimeSchema = z.enum([
|
|
|
179
171
|
"node",
|
|
180
172
|
"workers",
|
|
181
173
|
"none"
|
|
182
|
-
]).describe("Runtime environment
|
|
174
|
+
]).describe("Runtime environment");
|
|
183
175
|
const FrontendSchema = z.enum([
|
|
184
176
|
"tanstack-router",
|
|
185
177
|
"react-router",
|
|
@@ -198,6 +190,7 @@ const AddonsSchema = z.enum([
|
|
|
198
190
|
"starlight",
|
|
199
191
|
"biome",
|
|
200
192
|
"husky",
|
|
193
|
+
"vibe-rules",
|
|
201
194
|
"turborepo",
|
|
202
195
|
"fumadocs",
|
|
203
196
|
"ultracite",
|
|
@@ -267,6 +260,22 @@ function getCompatibleAddons(allAddons, frontend, existingAddons = []) {
|
|
|
267
260
|
});
|
|
268
261
|
}
|
|
269
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
|
+
|
|
270
279
|
//#endregion
|
|
271
280
|
//#region src/prompts/addons.ts
|
|
272
281
|
function getAddonDisplay(addon) {
|
|
@@ -297,6 +306,10 @@ function getAddonDisplay(addon) {
|
|
|
297
306
|
label = "Ultracite";
|
|
298
307
|
hint = "Zero-config Biome preset with AI integration";
|
|
299
308
|
break;
|
|
309
|
+
case "vibe-rules":
|
|
310
|
+
label = "vibe-rules";
|
|
311
|
+
hint = "Install and apply BTS rules to editors";
|
|
312
|
+
break;
|
|
300
313
|
case "husky":
|
|
301
314
|
label = "Husky";
|
|
302
315
|
hint = "Modern native Git hooks made easy";
|
|
@@ -326,6 +339,7 @@ const ADDON_GROUPS = {
|
|
|
326
339
|
"ultracite"
|
|
327
340
|
],
|
|
328
341
|
Other: [
|
|
342
|
+
"vibe-rules",
|
|
329
343
|
"turborepo",
|
|
330
344
|
"pwa",
|
|
331
345
|
"tauri",
|
|
@@ -365,10 +379,7 @@ async function getAddonsChoice(addons, frontends) {
|
|
|
365
379
|
required: false,
|
|
366
380
|
selectableGroups: false
|
|
367
381
|
});
|
|
368
|
-
if (isCancel(response))
|
|
369
|
-
cancel(pc.red("Operation cancelled"));
|
|
370
|
-
process.exit(0);
|
|
371
|
-
}
|
|
382
|
+
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
|
372
383
|
return response;
|
|
373
384
|
}
|
|
374
385
|
async function getAddonsToAdd(frontend, existingAddons = []) {
|
|
@@ -400,56 +411,154 @@ async function getAddonsToAdd(frontend, existingAddons = []) {
|
|
|
400
411
|
required: false,
|
|
401
412
|
selectableGroups: false
|
|
402
413
|
});
|
|
403
|
-
if (isCancel(response))
|
|
404
|
-
cancel(pc.red("Operation cancelled"));
|
|
405
|
-
process.exit(0);
|
|
406
|
-
}
|
|
414
|
+
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
|
407
415
|
return response;
|
|
408
416
|
}
|
|
409
417
|
|
|
418
|
+
//#endregion
|
|
419
|
+
//#region src/utils/compatibility.ts
|
|
420
|
+
const WEB_FRAMEWORKS = [
|
|
421
|
+
"tanstack-router",
|
|
422
|
+
"react-router",
|
|
423
|
+
"tanstack-start",
|
|
424
|
+
"next",
|
|
425
|
+
"nuxt",
|
|
426
|
+
"svelte",
|
|
427
|
+
"solid"
|
|
428
|
+
];
|
|
429
|
+
|
|
430
|
+
//#endregion
|
|
431
|
+
//#region src/utils/compatibility-rules.ts
|
|
432
|
+
function isWebFrontend(value) {
|
|
433
|
+
return WEB_FRAMEWORKS.includes(value);
|
|
434
|
+
}
|
|
435
|
+
function splitFrontends(values = []) {
|
|
436
|
+
const web = values.filter((f) => isWebFrontend(f));
|
|
437
|
+
const native = values.filter((f) => f === "native-nativewind" || f === "native-unistyles");
|
|
438
|
+
return {
|
|
439
|
+
web,
|
|
440
|
+
native
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
function ensureSingleWebAndNative(frontends) {
|
|
444
|
+
const { web, native } = splitFrontends(frontends);
|
|
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");
|
|
447
|
+
}
|
|
448
|
+
function validateWorkersCompatibility(providedFlags, options, config) {
|
|
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.");
|
|
457
|
+
}
|
|
458
|
+
function coerceBackendPresets(config) {
|
|
459
|
+
if (config.backend === "convex") {
|
|
460
|
+
config.auth = false;
|
|
461
|
+
config.database = "none";
|
|
462
|
+
config.orm = "none";
|
|
463
|
+
config.api = "none";
|
|
464
|
+
config.runtime = "none";
|
|
465
|
+
config.dbSetup = "none";
|
|
466
|
+
config.examples = ["todo"];
|
|
467
|
+
}
|
|
468
|
+
if (config.backend === "none") {
|
|
469
|
+
config.auth = false;
|
|
470
|
+
config.database = "none";
|
|
471
|
+
config.orm = "none";
|
|
472
|
+
config.api = "none";
|
|
473
|
+
config.runtime = "none";
|
|
474
|
+
config.dbSetup = "none";
|
|
475
|
+
config.examples = [];
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
function incompatibleFlagsForBackend(backend, providedFlags, options) {
|
|
479
|
+
const list = [];
|
|
480
|
+
if (backend === "convex") {
|
|
481
|
+
if (providedFlags.has("auth") && options.auth === true) list.push("--auth");
|
|
482
|
+
if (providedFlags.has("database") && options.database !== "none") list.push(`--database ${options.database}`);
|
|
483
|
+
if (providedFlags.has("orm") && options.orm !== "none") list.push(`--orm ${options.orm}`);
|
|
484
|
+
if (providedFlags.has("api") && options.api !== "none") list.push(`--api ${options.api}`);
|
|
485
|
+
if (providedFlags.has("runtime") && options.runtime !== "none") list.push(`--runtime ${options.runtime}`);
|
|
486
|
+
if (providedFlags.has("dbSetup") && options.dbSetup !== "none") list.push(`--db-setup ${options.dbSetup}`);
|
|
487
|
+
}
|
|
488
|
+
if (backend === "none") {
|
|
489
|
+
if (providedFlags.has("auth") && options.auth === true) list.push("--auth");
|
|
490
|
+
if (providedFlags.has("database") && options.database !== "none") list.push(`--database ${options.database}`);
|
|
491
|
+
if (providedFlags.has("orm") && options.orm !== "none") list.push(`--orm ${options.orm}`);
|
|
492
|
+
if (providedFlags.has("api") && options.api !== "none") list.push(`--api ${options.api}`);
|
|
493
|
+
if (providedFlags.has("runtime") && options.runtime !== "none") list.push(`--runtime ${options.runtime}`);
|
|
494
|
+
if (providedFlags.has("dbSetup") && options.dbSetup !== "none") list.push(`--db-setup ${options.dbSetup}`);
|
|
495
|
+
if (providedFlags.has("examples") && options.examples) {
|
|
496
|
+
const hasNonNoneExamples = options.examples.some((ex) => ex !== "none");
|
|
497
|
+
if (hasNonNoneExamples) list.push("--examples");
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return list;
|
|
501
|
+
}
|
|
502
|
+
function validateApiFrontendCompatibility(api, frontends = []) {
|
|
503
|
+
const includesNuxt = frontends.includes("nuxt");
|
|
504
|
+
const includesSvelte = frontends.includes("svelte");
|
|
505
|
+
const includesSolid = frontends.includes("solid");
|
|
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.`);
|
|
507
|
+
}
|
|
508
|
+
function isFrontendAllowedWithBackend(frontend, backend) {
|
|
509
|
+
if (backend === "convex" && frontend === "solid") return false;
|
|
510
|
+
return true;
|
|
511
|
+
}
|
|
512
|
+
function allowedApisForFrontends(frontends = []) {
|
|
513
|
+
const includesNuxt = frontends.includes("nuxt");
|
|
514
|
+
const includesSvelte = frontends.includes("svelte");
|
|
515
|
+
const includesSolid = frontends.includes("solid");
|
|
516
|
+
const base = [
|
|
517
|
+
"trpc",
|
|
518
|
+
"orpc",
|
|
519
|
+
"none"
|
|
520
|
+
];
|
|
521
|
+
if (includesNuxt || includesSvelte || includesSolid) return ["orpc", "none"];
|
|
522
|
+
return base;
|
|
523
|
+
}
|
|
524
|
+
function isExampleTodoAllowed(backend, database) {
|
|
525
|
+
return !(backend !== "convex" && backend !== "none" && database === "none");
|
|
526
|
+
}
|
|
527
|
+
function isExampleAIAllowed(backend, frontends = []) {
|
|
528
|
+
const includesSolid = frontends.includes("solid");
|
|
529
|
+
if (backend === "elysia") return false;
|
|
530
|
+
if (includesSolid) return false;
|
|
531
|
+
return true;
|
|
532
|
+
}
|
|
533
|
+
function validateWebDeployRequiresWebFrontend(webDeploy, hasWebFrontendFlag) {
|
|
534
|
+
if (webDeploy && webDeploy !== "none" && !hasWebFrontendFlag) exitWithError("'--web-deploy' requires a web frontend. Please select a web frontend or set '--web-deploy none'.");
|
|
535
|
+
}
|
|
536
|
+
|
|
410
537
|
//#endregion
|
|
411
538
|
//#region src/prompts/api.ts
|
|
412
539
|
async function getApiChoice(Api, frontend, backend) {
|
|
413
540
|
if (backend === "convex" || backend === "none") return "none";
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
const
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
label: "tRPC",
|
|
422
|
-
hint: "End-to-end typesafe APIs made easy"
|
|
423
|
-
},
|
|
424
|
-
{
|
|
425
|
-
value: "orpc",
|
|
426
|
-
label: "oRPC",
|
|
427
|
-
hint: "End-to-end type-safe APIs that adhere to OpenAPI standards"
|
|
428
|
-
},
|
|
429
|
-
{
|
|
430
|
-
value: "none",
|
|
431
|
-
label: "None",
|
|
432
|
-
hint: "No API layer (e.g. for full-stack frameworks like Next.js with Route Handlers)"
|
|
433
|
-
}
|
|
434
|
-
];
|
|
435
|
-
if (includesNuxt || includesSvelte || includesSolid) apiOptions = [{
|
|
541
|
+
const allowed = allowedApisForFrontends(frontend ?? []);
|
|
542
|
+
if (Api) return allowed.includes(Api) ? Api : allowed[0];
|
|
543
|
+
const apiOptions = allowed.map((a) => a === "trpc" ? {
|
|
544
|
+
value: "trpc",
|
|
545
|
+
label: "tRPC",
|
|
546
|
+
hint: "End-to-end typesafe APIs made easy"
|
|
547
|
+
} : a === "orpc" ? {
|
|
436
548
|
value: "orpc",
|
|
437
549
|
label: "oRPC",
|
|
438
|
-
hint:
|
|
439
|
-
}
|
|
550
|
+
hint: "End-to-end type-safe APIs that adhere to OpenAPI standards"
|
|
551
|
+
} : {
|
|
440
552
|
value: "none",
|
|
441
553
|
label: "None",
|
|
442
|
-
hint: "No API layer"
|
|
443
|
-
}
|
|
554
|
+
hint: "No API layer (e.g. for full-stack frameworks like Next.js with Route Handlers)"
|
|
555
|
+
});
|
|
444
556
|
const apiType = await select({
|
|
445
557
|
message: "Select API type",
|
|
446
558
|
options: apiOptions,
|
|
447
559
|
initialValue: apiOptions[0].value
|
|
448
560
|
});
|
|
449
|
-
if (isCancel(apiType))
|
|
450
|
-
cancel(pc.red("Operation cancelled"));
|
|
451
|
-
process.exit(0);
|
|
452
|
-
}
|
|
561
|
+
if (isCancel(apiType)) return exitCancelled("Operation cancelled");
|
|
453
562
|
return apiType;
|
|
454
563
|
}
|
|
455
564
|
|
|
@@ -463,10 +572,7 @@ async function getAuthChoice(auth, hasDatabase, backend) {
|
|
|
463
572
|
message: "Add authentication with Better-Auth?",
|
|
464
573
|
initialValue: DEFAULT_CONFIG.auth
|
|
465
574
|
});
|
|
466
|
-
if (isCancel(response))
|
|
467
|
-
cancel(pc.red("Operation cancelled"));
|
|
468
|
-
process.exit(0);
|
|
469
|
-
}
|
|
575
|
+
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
|
470
576
|
return response;
|
|
471
577
|
}
|
|
472
578
|
|
|
@@ -517,10 +623,7 @@ async function getBackendFrameworkChoice(backendFramework, frontends) {
|
|
|
517
623
|
options: backendOptions,
|
|
518
624
|
initialValue: DEFAULT_CONFIG.backend
|
|
519
625
|
});
|
|
520
|
-
if (isCancel(response))
|
|
521
|
-
cancel(pc.red("Operation cancelled"));
|
|
522
|
-
process.exit(0);
|
|
523
|
-
}
|
|
626
|
+
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
|
524
627
|
return response;
|
|
525
628
|
}
|
|
526
629
|
|
|
@@ -561,10 +664,7 @@ async function getDatabaseChoice(database, backend, runtime) {
|
|
|
561
664
|
options: databaseOptions,
|
|
562
665
|
initialValue: DEFAULT_CONFIG.database
|
|
563
666
|
});
|
|
564
|
-
if (isCancel(response))
|
|
565
|
-
cancel(pc.red("Operation cancelled"));
|
|
566
|
-
process.exit(0);
|
|
567
|
-
}
|
|
667
|
+
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
|
568
668
|
return response;
|
|
569
669
|
}
|
|
570
670
|
|
|
@@ -652,10 +752,7 @@ async function getDBSetupChoice(databaseType, dbSetup, orm, backend, runtime) {
|
|
|
652
752
|
options,
|
|
653
753
|
initialValue: "none"
|
|
654
754
|
});
|
|
655
|
-
if (isCancel(response))
|
|
656
|
-
cancel(pc.red("Operation cancelled"));
|
|
657
|
-
process.exit(0);
|
|
658
|
-
}
|
|
755
|
+
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
|
659
756
|
return response;
|
|
660
757
|
}
|
|
661
758
|
|
|
@@ -670,26 +767,25 @@ async function getExamplesChoice(examples, database, frontends, backend, api) {
|
|
|
670
767
|
const noFrontendSelected = !frontends || frontends.length === 0;
|
|
671
768
|
if (noFrontendSelected) return [];
|
|
672
769
|
let response = [];
|
|
673
|
-
const options = [
|
|
770
|
+
const options = [];
|
|
771
|
+
if (isExampleTodoAllowed(backend, database)) options.push({
|
|
674
772
|
value: "todo",
|
|
675
773
|
label: "Todo App",
|
|
676
774
|
hint: "A simple CRUD example app"
|
|
677
|
-
}
|
|
678
|
-
if (backend
|
|
775
|
+
});
|
|
776
|
+
if (isExampleAIAllowed(backend, frontends ?? [])) options.push({
|
|
679
777
|
value: "ai",
|
|
680
778
|
label: "AI Chat",
|
|
681
779
|
hint: "A simple AI chat interface using AI SDK"
|
|
682
780
|
});
|
|
781
|
+
if (options.length === 0) return [];
|
|
683
782
|
response = await multiselect({
|
|
684
783
|
message: "Include examples",
|
|
685
784
|
options,
|
|
686
785
|
required: false,
|
|
687
|
-
initialValues: DEFAULT_CONFIG.examples
|
|
786
|
+
initialValues: DEFAULT_CONFIG.examples?.filter((ex) => options.some((o) => o.value === ex))
|
|
688
787
|
});
|
|
689
|
-
if (isCancel(response))
|
|
690
|
-
cancel(pc.red("Operation cancelled"));
|
|
691
|
-
process.exit(0);
|
|
692
|
-
}
|
|
788
|
+
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
|
693
789
|
return response;
|
|
694
790
|
}
|
|
695
791
|
|
|
@@ -711,10 +807,7 @@ async function getFrontendChoice(frontendOptions, backend) {
|
|
|
711
807
|
required: false,
|
|
712
808
|
initialValues: ["web"]
|
|
713
809
|
});
|
|
714
|
-
if (isCancel(frontendTypes))
|
|
715
|
-
cancel(pc.red("Operation cancelled"));
|
|
716
|
-
process.exit(0);
|
|
717
|
-
}
|
|
810
|
+
if (isCancel(frontendTypes)) return exitCancelled("Operation cancelled");
|
|
718
811
|
const result = [];
|
|
719
812
|
if (frontendTypes.includes("web")) {
|
|
720
813
|
const allWebOptions = [
|
|
@@ -750,23 +843,17 @@ async function getFrontendChoice(frontendOptions, backend) {
|
|
|
750
843
|
},
|
|
751
844
|
{
|
|
752
845
|
value: "tanstack-start",
|
|
753
|
-
label: "TanStack Start
|
|
846
|
+
label: "TanStack Start",
|
|
754
847
|
hint: "SSR, Server Functions, API Routes and more with TanStack Router"
|
|
755
848
|
}
|
|
756
849
|
];
|
|
757
|
-
const webOptions = allWebOptions.filter((option) =>
|
|
758
|
-
if (backend === "convex") return option.value !== "solid";
|
|
759
|
-
return true;
|
|
760
|
-
});
|
|
850
|
+
const webOptions = allWebOptions.filter((option) => isFrontendAllowedWithBackend(option.value, backend));
|
|
761
851
|
const webFramework = await select({
|
|
762
852
|
message: "Choose web",
|
|
763
853
|
options: webOptions,
|
|
764
854
|
initialValue: DEFAULT_CONFIG.frontend[0]
|
|
765
855
|
});
|
|
766
|
-
if (isCancel(webFramework))
|
|
767
|
-
cancel(pc.red("Operation cancelled"));
|
|
768
|
-
process.exit(0);
|
|
769
|
-
}
|
|
856
|
+
if (isCancel(webFramework)) return exitCancelled("Operation cancelled");
|
|
770
857
|
result.push(webFramework);
|
|
771
858
|
}
|
|
772
859
|
if (frontendTypes.includes("native")) {
|
|
@@ -783,10 +870,7 @@ async function getFrontendChoice(frontendOptions, backend) {
|
|
|
783
870
|
}],
|
|
784
871
|
initialValue: "native-nativewind"
|
|
785
872
|
});
|
|
786
|
-
if (isCancel(nativeFramework))
|
|
787
|
-
cancel(pc.red("Operation cancelled"));
|
|
788
|
-
process.exit(0);
|
|
789
|
-
}
|
|
873
|
+
if (isCancel(nativeFramework)) return exitCancelled("Operation cancelled");
|
|
790
874
|
result.push(nativeFramework);
|
|
791
875
|
}
|
|
792
876
|
return result;
|
|
@@ -800,10 +884,7 @@ async function getGitChoice(git) {
|
|
|
800
884
|
message: "Initialize git repository?",
|
|
801
885
|
initialValue: DEFAULT_CONFIG.git
|
|
802
886
|
});
|
|
803
|
-
if (isCancel(response))
|
|
804
|
-
cancel(pc.red("Operation cancelled"));
|
|
805
|
-
process.exit(0);
|
|
806
|
-
}
|
|
887
|
+
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
|
807
888
|
return response;
|
|
808
889
|
}
|
|
809
890
|
|
|
@@ -815,10 +896,7 @@ async function getinstallChoice(install) {
|
|
|
815
896
|
message: "Install dependencies?",
|
|
816
897
|
initialValue: DEFAULT_CONFIG.install
|
|
817
898
|
});
|
|
818
|
-
if (isCancel(response))
|
|
819
|
-
cancel(pc.red("Operation cancelled"));
|
|
820
|
-
process.exit(0);
|
|
821
|
-
}
|
|
899
|
+
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
|
822
900
|
return response;
|
|
823
901
|
}
|
|
824
902
|
|
|
@@ -852,10 +930,7 @@ async function getORMChoice(orm, hasDatabase, database, backend, runtime) {
|
|
|
852
930
|
options,
|
|
853
931
|
initialValue: database === "mongodb" ? "prisma" : DEFAULT_CONFIG.orm
|
|
854
932
|
});
|
|
855
|
-
if (isCancel(response))
|
|
856
|
-
cancel(pc.red("Operation cancelled"));
|
|
857
|
-
process.exit(0);
|
|
858
|
-
}
|
|
933
|
+
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
|
859
934
|
return response;
|
|
860
935
|
}
|
|
861
936
|
|
|
@@ -885,10 +960,7 @@ async function getPackageManagerChoice(packageManager) {
|
|
|
885
960
|
],
|
|
886
961
|
initialValue: detectedPackageManager
|
|
887
962
|
});
|
|
888
|
-
if (isCancel(response))
|
|
889
|
-
cancel(pc.red("Operation cancelled"));
|
|
890
|
-
process.exit(0);
|
|
891
|
-
}
|
|
963
|
+
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
|
892
964
|
return response;
|
|
893
965
|
}
|
|
894
966
|
|
|
@@ -917,10 +989,7 @@ async function getRuntimeChoice(runtime, backend) {
|
|
|
917
989
|
options: runtimeOptions,
|
|
918
990
|
initialValue: DEFAULT_CONFIG.runtime
|
|
919
991
|
});
|
|
920
|
-
if (isCancel(response))
|
|
921
|
-
cancel(pc.red("Operation cancelled"));
|
|
922
|
-
process.exit(0);
|
|
923
|
-
}
|
|
992
|
+
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
|
924
993
|
return response;
|
|
925
994
|
}
|
|
926
995
|
|
|
@@ -956,10 +1025,7 @@ async function getDeploymentChoice(deployment, _runtime, _backend, frontend = []
|
|
|
956
1025
|
options,
|
|
957
1026
|
initialValue: DEFAULT_CONFIG.webDeploy
|
|
958
1027
|
});
|
|
959
|
-
if (isCancel(response))
|
|
960
|
-
cancel(pc.red("Operation cancelled"));
|
|
961
|
-
process.exit(0);
|
|
962
|
-
}
|
|
1028
|
+
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
|
963
1029
|
return response;
|
|
964
1030
|
}
|
|
965
1031
|
async function getDeploymentToAdd(frontend, existingDeployment) {
|
|
@@ -985,10 +1051,7 @@ async function getDeploymentToAdd(frontend, existingDeployment) {
|
|
|
985
1051
|
options,
|
|
986
1052
|
initialValue: DEFAULT_CONFIG.webDeploy
|
|
987
1053
|
});
|
|
988
|
-
if (isCancel(response))
|
|
989
|
-
cancel(pc.red("Operation cancelled"));
|
|
990
|
-
process.exit(0);
|
|
991
|
-
}
|
|
1054
|
+
if (isCancel(response)) return exitCancelled("Operation cancelled");
|
|
992
1055
|
return response;
|
|
993
1056
|
}
|
|
994
1057
|
|
|
@@ -1010,10 +1073,7 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
|
|
|
1010
1073
|
git: () => getGitChoice(flags.git),
|
|
1011
1074
|
packageManager: () => getPackageManagerChoice(flags.packageManager),
|
|
1012
1075
|
install: () => getinstallChoice(flags.install)
|
|
1013
|
-
}, { onCancel: () =>
|
|
1014
|
-
cancel(pc.red("Operation cancelled"));
|
|
1015
|
-
process.exit(0);
|
|
1016
|
-
} });
|
|
1076
|
+
}, { onCancel: () => exitCancelled("Operation cancelled") });
|
|
1017
1077
|
if (result.backend === "convex") {
|
|
1018
1078
|
result.runtime = "none";
|
|
1019
1079
|
result.database = "none";
|
|
@@ -1057,6 +1117,11 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
|
|
|
1057
1117
|
|
|
1058
1118
|
//#endregion
|
|
1059
1119
|
//#region src/prompts/project-name.ts
|
|
1120
|
+
function isPathWithinCwd(targetPath) {
|
|
1121
|
+
const resolved = path.resolve(targetPath);
|
|
1122
|
+
const rel = path.relative(process.cwd(), resolved);
|
|
1123
|
+
return !rel.startsWith("..") && !path.isAbsolute(rel);
|
|
1124
|
+
}
|
|
1060
1125
|
function validateDirectoryName(name) {
|
|
1061
1126
|
if (name === ".") return void 0;
|
|
1062
1127
|
const result = ProjectNameSchema.safeParse(name);
|
|
@@ -1068,7 +1133,11 @@ async function getProjectName(initialName) {
|
|
|
1068
1133
|
if (initialName === ".") return initialName;
|
|
1069
1134
|
const finalDirName = path.basename(initialName);
|
|
1070
1135
|
const validationError = validateDirectoryName(finalDirName);
|
|
1071
|
-
if (!validationError)
|
|
1136
|
+
if (!validationError) {
|
|
1137
|
+
const projectDir = path.resolve(process.cwd(), initialName);
|
|
1138
|
+
if (isPathWithinCwd(projectDir)) return initialName;
|
|
1139
|
+
consola.error(pc.red("Project path must be within current directory"));
|
|
1140
|
+
}
|
|
1072
1141
|
}
|
|
1073
1142
|
let isValid = false;
|
|
1074
1143
|
let projectPath = "";
|
|
@@ -1091,15 +1160,12 @@ async function getProjectName(initialName) {
|
|
|
1091
1160
|
if (validationError) return validationError;
|
|
1092
1161
|
if (nameToUse !== ".") {
|
|
1093
1162
|
const projectDir = path.resolve(process.cwd(), nameToUse);
|
|
1094
|
-
if (!projectDir
|
|
1163
|
+
if (!isPathWithinCwd(projectDir)) return "Project path must be within current directory";
|
|
1095
1164
|
}
|
|
1096
1165
|
return void 0;
|
|
1097
1166
|
}
|
|
1098
1167
|
});
|
|
1099
|
-
if (isCancel(response))
|
|
1100
|
-
cancel(pc.red("Operation cancelled."));
|
|
1101
|
-
process.exit(0);
|
|
1102
|
-
}
|
|
1168
|
+
if (isCancel(response)) return exitCancelled("Operation cancelled.");
|
|
1103
1169
|
projectPath = response || defaultName;
|
|
1104
1170
|
isValid = true;
|
|
1105
1171
|
}
|
|
@@ -1224,11 +1290,11 @@ function generateReproducibleCommand(config) {
|
|
|
1224
1290
|
flags.push(config.git ? "--git" : "--no-git");
|
|
1225
1291
|
flags.push(`--package-manager ${config.packageManager}`);
|
|
1226
1292
|
flags.push(config.install ? "--install" : "--no-install");
|
|
1227
|
-
let baseCommand = "";
|
|
1293
|
+
let baseCommand = "npx create-better-t-stack@latest";
|
|
1228
1294
|
const pkgManager = config.packageManager;
|
|
1229
|
-
if (pkgManager === "
|
|
1295
|
+
if (pkgManager === "bun") baseCommand = "bun create better-t-stack@latest";
|
|
1230
1296
|
else if (pkgManager === "pnpm") baseCommand = "pnpm create better-t-stack@latest";
|
|
1231
|
-
else if (pkgManager === "
|
|
1297
|
+
else if (pkgManager === "npm") baseCommand = "npx create-better-t-stack@latest";
|
|
1232
1298
|
const projectPathArg = config.relativePath ? ` ${config.relativePath}` : "";
|
|
1233
1299
|
return `${baseCommand}${projectPathArg} ${flags.join(" ")}`;
|
|
1234
1300
|
}
|
|
@@ -1271,10 +1337,7 @@ async function handleDirectoryConflict(currentPathInput) {
|
|
|
1271
1337
|
],
|
|
1272
1338
|
initialValue: "rename"
|
|
1273
1339
|
});
|
|
1274
|
-
if (isCancel(action))
|
|
1275
|
-
cancel(pc.red("Operation cancelled."));
|
|
1276
|
-
process.exit(0);
|
|
1277
|
-
}
|
|
1340
|
+
if (isCancel(action)) return exitCancelled("Operation cancelled.");
|
|
1278
1341
|
switch (action) {
|
|
1279
1342
|
case "overwrite": return {
|
|
1280
1343
|
finalPathInput: currentPathInput,
|
|
@@ -1291,9 +1354,7 @@ async function handleDirectoryConflict(currentPathInput) {
|
|
|
1291
1354
|
const newPathInput = await getProjectName(void 0);
|
|
1292
1355
|
return await handleDirectoryConflict(newPathInput);
|
|
1293
1356
|
}
|
|
1294
|
-
case "cancel":
|
|
1295
|
-
cancel(pc.red("Operation cancelled."));
|
|
1296
|
-
process.exit(0);
|
|
1357
|
+
case "cancel": return exitCancelled("Operation cancelled.");
|
|
1297
1358
|
}
|
|
1298
1359
|
}
|
|
1299
1360
|
}
|
|
@@ -1315,8 +1376,7 @@ async function setupProjectDirectory(finalPathInput, shouldClearDirectory) {
|
|
|
1315
1376
|
s.stop(`Directory "${finalResolvedPath}" cleared.`);
|
|
1316
1377
|
} catch (error) {
|
|
1317
1378
|
s.stop(pc.red(`Failed to clear directory "${finalResolvedPath}".`));
|
|
1318
|
-
|
|
1319
|
-
process.exit(1);
|
|
1379
|
+
handleError(error);
|
|
1320
1380
|
}
|
|
1321
1381
|
} else await fs.ensureDir(finalResolvedPath);
|
|
1322
1382
|
return {
|
|
@@ -1376,18 +1436,12 @@ function processAndValidateFlags(options, providedFlags, projectName) {
|
|
|
1376
1436
|
if (options.api) {
|
|
1377
1437
|
config.api = options.api;
|
|
1378
1438
|
if (options.api === "none") {
|
|
1379
|
-
if (options.examples && !(options.examples.length === 1 && options.examples[0] === "none") && options.backend !== "convex")
|
|
1380
|
-
consola$1.fatal("Cannot use '--examples' when '--api' is set to 'none'. Please remove the --examples flag or choose an API type.");
|
|
1381
|
-
process.exit(1);
|
|
1382
|
-
}
|
|
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.");
|
|
1383
1440
|
}
|
|
1384
1441
|
}
|
|
1385
1442
|
if (options.backend) config.backend = options.backend;
|
|
1386
1443
|
if (providedFlags.has("backend") && config.backend && config.backend !== "convex" && config.backend !== "none") {
|
|
1387
|
-
if (providedFlags.has("runtime") && options.runtime === "none")
|
|
1388
|
-
consola$1.fatal(`'--runtime none' is only supported with '--backend convex' or '--backend none'. Please choose 'bun', 'node', or remove the --runtime flag.`);
|
|
1389
|
-
process.exit(1);
|
|
1390
|
-
}
|
|
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.`);
|
|
1391
1445
|
}
|
|
1392
1446
|
if (options.database) config.database = options.database;
|
|
1393
1447
|
if (options.orm) config.orm = options.orm;
|
|
@@ -1400,210 +1454,68 @@ function processAndValidateFlags(options, providedFlags, projectName) {
|
|
|
1400
1454
|
if (options.webDeploy) config.webDeploy = options.webDeploy;
|
|
1401
1455
|
if (projectName) {
|
|
1402
1456
|
const result = ProjectNameSchema.safeParse(path.basename(projectName));
|
|
1403
|
-
if (!result.success) {
|
|
1404
|
-
consola$1.fatal(`Invalid project name: ${result.error.issues[0]?.message || "Invalid project name"}`);
|
|
1405
|
-
process.exit(1);
|
|
1406
|
-
}
|
|
1457
|
+
if (!result.success) exitWithError(`Invalid project name: ${result.error.issues[0]?.message || "Invalid project name"}`);
|
|
1407
1458
|
config.projectName = projectName;
|
|
1408
1459
|
} else if (options.projectDirectory) {
|
|
1409
1460
|
const baseName = path.basename(path.resolve(process.cwd(), options.projectDirectory));
|
|
1410
1461
|
const result = ProjectNameSchema.safeParse(baseName);
|
|
1411
|
-
if (!result.success) {
|
|
1412
|
-
consola$1.fatal(`Invalid project name: ${result.error.issues[0]?.message || "Invalid project name"}`);
|
|
1413
|
-
process.exit(1);
|
|
1414
|
-
}
|
|
1462
|
+
if (!result.success) exitWithError(`Invalid project name: ${result.error.issues[0]?.message || "Invalid project name"}`);
|
|
1415
1463
|
config.projectName = baseName;
|
|
1416
1464
|
}
|
|
1417
1465
|
if (options.frontend && options.frontend.length > 0) if (options.frontend.includes("none")) {
|
|
1418
|
-
if (options.frontend.length > 1)
|
|
1419
|
-
consola$1.fatal(`Cannot combine 'none' with other frontend options.`);
|
|
1420
|
-
process.exit(1);
|
|
1421
|
-
}
|
|
1466
|
+
if (options.frontend.length > 1) exitWithError(`Cannot combine 'none' with other frontend options.`);
|
|
1422
1467
|
config.frontend = [];
|
|
1423
1468
|
} else {
|
|
1424
1469
|
const validOptions = options.frontend.filter((f) => f !== "none");
|
|
1425
|
-
|
|
1426
|
-
const nativeFrontends = validOptions.filter((f) => f === "native-nativewind" || f === "native-unistyles");
|
|
1427
|
-
if (webFrontends.length > 1) {
|
|
1428
|
-
consola$1.fatal("Cannot select multiple web frameworks. Choose only one of: tanstack-router, tanstack-start, react-router, next, nuxt, svelte, solid");
|
|
1429
|
-
process.exit(1);
|
|
1430
|
-
}
|
|
1431
|
-
if (nativeFrontends.length > 1) {
|
|
1432
|
-
consola$1.fatal("Cannot select multiple native frameworks. Choose only one of: native-nativewind, native-unistyles");
|
|
1433
|
-
process.exit(1);
|
|
1434
|
-
}
|
|
1470
|
+
ensureSingleWebAndNative(validOptions);
|
|
1435
1471
|
config.frontend = validOptions;
|
|
1436
1472
|
}
|
|
1473
|
+
if (providedFlags.has("api") && providedFlags.has("frontend") && config.api && config.frontend && config.frontend.length > 0) validateApiFrontendCompatibility(config.api, config.frontend);
|
|
1437
1474
|
if (options.addons && options.addons.length > 0) if (options.addons.includes("none")) {
|
|
1438
|
-
if (options.addons.length > 1)
|
|
1439
|
-
consola$1.fatal(`Cannot combine 'none' with other addons.`);
|
|
1440
|
-
process.exit(1);
|
|
1441
|
-
}
|
|
1475
|
+
if (options.addons.length > 1) exitWithError(`Cannot combine 'none' with other addons.`);
|
|
1442
1476
|
config.addons = [];
|
|
1443
1477
|
} else config.addons = options.addons.filter((addon) => addon !== "none");
|
|
1444
1478
|
if (options.examples && options.examples.length > 0) if (options.examples.includes("none")) {
|
|
1445
|
-
if (options.examples.length > 1)
|
|
1446
|
-
consola$1.fatal("Cannot combine 'none' with other examples.");
|
|
1447
|
-
process.exit(1);
|
|
1448
|
-
}
|
|
1479
|
+
if (options.examples.length > 1) exitWithError("Cannot combine 'none' with other examples.");
|
|
1449
1480
|
config.examples = [];
|
|
1450
1481
|
} else {
|
|
1451
1482
|
config.examples = options.examples.filter((ex) => ex !== "none");
|
|
1452
1483
|
if (options.examples.includes("none") && config.backend !== "convex") config.examples = [];
|
|
1453
1484
|
}
|
|
1454
|
-
if (config.backend === "convex") {
|
|
1455
|
-
const incompatibleFlags =
|
|
1456
|
-
if (
|
|
1457
|
-
if (providedFlags.has("
|
|
1458
|
-
if (providedFlags.has("orm") && options.orm !== "none") incompatibleFlags.push(`--orm ${options.orm}`);
|
|
1459
|
-
if (providedFlags.has("api") && options.api !== "none") incompatibleFlags.push(`--api ${options.api}`);
|
|
1460
|
-
if (providedFlags.has("runtime") && options.runtime !== "none") incompatibleFlags.push(`--runtime ${options.runtime}`);
|
|
1461
|
-
if (providedFlags.has("dbSetup") && options.dbSetup !== "none") incompatibleFlags.push(`--db-setup ${options.dbSetup}`);
|
|
1462
|
-
if (incompatibleFlags.length > 0) {
|
|
1463
|
-
consola$1.fatal(`The following flags are incompatible with '--backend convex': ${incompatibleFlags.join(", ")}. Please remove them.`);
|
|
1464
|
-
process.exit(1);
|
|
1465
|
-
}
|
|
1466
|
-
if (providedFlags.has("frontend") && options.frontend) {
|
|
1485
|
+
if (config.backend === "convex" || config.backend === "none") {
|
|
1486
|
+
const incompatibleFlags = incompatibleFlagsForBackend(config.backend, providedFlags, options);
|
|
1487
|
+
if (incompatibleFlags.length > 0) exitWithError(`The following flags are incompatible with '--backend ${config.backend}': ${incompatibleFlags.join(", ")}. Please remove them.`);
|
|
1488
|
+
if (config.backend === "convex" && providedFlags.has("frontend") && options.frontend) {
|
|
1467
1489
|
const incompatibleFrontends = options.frontend.filter((f) => f === "solid");
|
|
1468
|
-
if (incompatibleFrontends.length > 0) {
|
|
1469
|
-
consola$1.fatal(`The following frontends are not compatible with '--backend convex': ${incompatibleFrontends.join(", ")}. Please choose a different frontend or backend.`);
|
|
1470
|
-
process.exit(1);
|
|
1471
|
-
}
|
|
1490
|
+
if (incompatibleFrontends.length > 0) exitWithError(`The following frontends are not compatible with '--backend convex': ${incompatibleFrontends.join(", ")}. Please choose a different frontend or backend.`);
|
|
1472
1491
|
}
|
|
1473
|
-
config
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
if (providedFlags.has("dbSetup") && options.dbSetup !== "none") incompatibleFlags.push(`--db-setup ${options.dbSetup}`);
|
|
1488
|
-
if (providedFlags.has("examples") && options.examples) {
|
|
1489
|
-
const hasNonNoneExamples = options.examples.some((ex) => ex !== "none");
|
|
1490
|
-
if (hasNonNoneExamples) incompatibleFlags.push("--examples");
|
|
1491
|
-
}
|
|
1492
|
-
if (incompatibleFlags.length > 0) {
|
|
1493
|
-
consola$1.fatal(`The following flags are incompatible with '--backend none': ${incompatibleFlags.join(", ")}. Please remove them.`);
|
|
1494
|
-
process.exit(1);
|
|
1495
|
-
}
|
|
1496
|
-
config.auth = false;
|
|
1497
|
-
config.database = "none";
|
|
1498
|
-
config.orm = "none";
|
|
1499
|
-
config.api = "none";
|
|
1500
|
-
config.runtime = "none";
|
|
1501
|
-
config.dbSetup = "none";
|
|
1502
|
-
config.examples = [];
|
|
1503
|
-
}
|
|
1504
|
-
if (config.orm === "mongoose" && config.database !== "mongodb") {
|
|
1505
|
-
consola$1.fatal("Mongoose ORM requires MongoDB database. Please use '--database mongodb' or choose a different ORM.");
|
|
1506
|
-
process.exit(1);
|
|
1507
|
-
}
|
|
1508
|
-
if (config.database === "mongodb" && config.orm && config.orm !== "mongoose" && config.orm !== "prisma") {
|
|
1509
|
-
consola$1.fatal("MongoDB database requires Mongoose or Prisma ORM. Please use '--orm mongoose' or '--orm prisma' or choose a different database.");
|
|
1510
|
-
process.exit(1);
|
|
1511
|
-
}
|
|
1512
|
-
if (config.orm === "drizzle" && config.database === "mongodb") {
|
|
1513
|
-
consola$1.fatal("Drizzle ORM does not support MongoDB. Please use '--orm mongoose' or '--orm prisma' or choose a different database.");
|
|
1514
|
-
process.exit(1);
|
|
1515
|
-
}
|
|
1516
|
-
if (config.database && config.database !== "none" && config.orm === "none") {
|
|
1517
|
-
consola$1.fatal("Database selection requires an ORM. Please choose '--orm drizzle', '--orm prisma', or '--orm mongoose'.");
|
|
1518
|
-
process.exit(1);
|
|
1519
|
-
}
|
|
1520
|
-
if (config.orm && config.orm !== "none" && config.database === "none") {
|
|
1521
|
-
consola$1.fatal("ORM selection requires a database. Please choose a database or set '--orm none'.");
|
|
1522
|
-
process.exit(1);
|
|
1523
|
-
}
|
|
1524
|
-
if (config.auth && config.database === "none") {
|
|
1525
|
-
consola$1.fatal("Authentication requires a database. Please choose a database or set '--no-auth'.");
|
|
1526
|
-
process.exit(1);
|
|
1527
|
-
}
|
|
1528
|
-
if (config.dbSetup && config.dbSetup !== "none" && config.database === "none") {
|
|
1529
|
-
consola$1.fatal("Database setup requires a database. Please choose a database or set '--db-setup none'.");
|
|
1530
|
-
process.exit(1);
|
|
1531
|
-
}
|
|
1532
|
-
if (config.dbSetup === "turso" && config.database !== "sqlite") {
|
|
1533
|
-
consola$1.fatal("Turso setup requires SQLite database. Please use '--database sqlite' or choose a different setup.");
|
|
1534
|
-
process.exit(1);
|
|
1535
|
-
}
|
|
1536
|
-
if (config.dbSetup === "neon" && config.database !== "postgres") {
|
|
1537
|
-
consola$1.fatal("Neon setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup.");
|
|
1538
|
-
process.exit(1);
|
|
1539
|
-
}
|
|
1540
|
-
if (config.dbSetup === "prisma-postgres" && config.database !== "postgres") {
|
|
1541
|
-
consola$1.fatal("Prisma PostgreSQL setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup.");
|
|
1542
|
-
process.exit(1);
|
|
1543
|
-
}
|
|
1544
|
-
if (config.dbSetup === "mongodb-atlas" && config.database !== "mongodb") {
|
|
1545
|
-
consola$1.fatal("MongoDB Atlas setup requires MongoDB database. Please use '--database mongodb' or choose a different setup.");
|
|
1546
|
-
process.exit(1);
|
|
1547
|
-
}
|
|
1548
|
-
if (config.dbSetup === "supabase" && config.database !== "postgres") {
|
|
1549
|
-
consola$1.fatal("Supabase setup requires PostgreSQL database. Please use '--database postgres' or choose a different setup.");
|
|
1550
|
-
process.exit(1);
|
|
1551
|
-
}
|
|
1492
|
+
coerceBackendPresets(config);
|
|
1493
|
+
}
|
|
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.");
|
|
1552
1506
|
if (config.dbSetup === "d1") {
|
|
1553
|
-
if (
|
|
1554
|
-
|
|
1555
|
-
process.exit(1);
|
|
1507
|
+
if (providedFlags.has("dbSetup") && providedFlags.has("database") || providedFlags.has("dbSetup") && !config.database) {
|
|
1508
|
+
if (config.database !== "sqlite") exitWithError("Cloudflare D1 setup requires SQLite database. Please use '--database sqlite' or choose a different setup.");
|
|
1556
1509
|
}
|
|
1557
|
-
if (
|
|
1558
|
-
|
|
1559
|
-
process.exit(1);
|
|
1510
|
+
if (providedFlags.has("dbSetup") && providedFlags.has("runtime") || providedFlags.has("dbSetup") && !config.runtime) {
|
|
1511
|
+
if (config.runtime !== "workers") exitWithError("Cloudflare D1 setup requires the Cloudflare Workers runtime. Please use '--runtime workers' or choose a different setup.");
|
|
1560
1512
|
}
|
|
1561
1513
|
}
|
|
1562
|
-
if (config.dbSetup === "docker" && config.database === "sqlite")
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
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.");
|
|
1568
|
-
process.exit(1);
|
|
1569
|
-
}
|
|
1570
|
-
if (providedFlags.has("runtime") && options.runtime === "workers" && config.backend && config.backend !== "hono") {
|
|
1571
|
-
consola$1.fatal(`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.`);
|
|
1572
|
-
process.exit(1);
|
|
1573
|
-
}
|
|
1574
|
-
if (providedFlags.has("backend") && config.backend && config.backend !== "hono" && config.runtime === "workers") {
|
|
1575
|
-
consola$1.fatal(`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.`);
|
|
1576
|
-
process.exit(1);
|
|
1577
|
-
}
|
|
1578
|
-
if (providedFlags.has("runtime") && options.runtime === "workers" && config.orm && config.orm !== "drizzle" && config.orm !== "none") {
|
|
1579
|
-
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.`);
|
|
1580
|
-
process.exit(1);
|
|
1581
|
-
}
|
|
1582
|
-
if (providedFlags.has("orm") && config.orm && config.orm !== "drizzle" && config.orm !== "none" && config.runtime === "workers") {
|
|
1583
|
-
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.`);
|
|
1584
|
-
process.exit(1);
|
|
1585
|
-
}
|
|
1586
|
-
if (providedFlags.has("runtime") && options.runtime === "workers" && config.database === "mongodb") {
|
|
1587
|
-
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.");
|
|
1588
|
-
process.exit(1);
|
|
1589
|
-
}
|
|
1590
|
-
if (providedFlags.has("runtime") && options.runtime === "workers" && config.dbSetup === "docker") {
|
|
1591
|
-
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.");
|
|
1592
|
-
process.exit(1);
|
|
1593
|
-
}
|
|
1594
|
-
if (providedFlags.has("database") && config.database === "mongodb" && config.runtime === "workers") {
|
|
1595
|
-
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.");
|
|
1596
|
-
process.exit(1);
|
|
1597
|
-
}
|
|
1598
|
-
if (providedFlags.has("db-setup") && options.dbSetup === "docker" && config.runtime === "workers") {
|
|
1599
|
-
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.");
|
|
1600
|
-
process.exit(1);
|
|
1601
|
-
}
|
|
1602
|
-
const hasWebFrontendFlag = (config.frontend ?? []).some((f) => WEB_FRAMEWORKS.includes(f));
|
|
1603
|
-
if (config.webDeploy && config.webDeploy !== "none" && !hasWebFrontendFlag && providedFlags.has("frontend")) {
|
|
1604
|
-
consola$1.fatal("'--web-deploy' requires a web frontend. Please select a web frontend or set '--web-deploy none'.");
|
|
1605
|
-
process.exit(1);
|
|
1606
|
-
}
|
|
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.");
|
|
1516
|
+
validateWorkersCompatibility(providedFlags, options, config);
|
|
1517
|
+
const hasWebFrontendFlag = (config.frontend ?? []).some((f) => isWebFrontend(f));
|
|
1518
|
+
validateWebDeployRequiresWebFrontend(config.webDeploy, hasWebFrontendFlag);
|
|
1607
1519
|
return config;
|
|
1608
1520
|
}
|
|
1609
1521
|
function getProvidedFlags(options) {
|
|
@@ -1774,12 +1686,9 @@ async function setupFumadocs(config) {
|
|
|
1774
1686
|
})),
|
|
1775
1687
|
initialValue: "next-mdx"
|
|
1776
1688
|
});
|
|
1777
|
-
if (isCancel(template))
|
|
1778
|
-
cancel(pc.red("Operation cancelled"));
|
|
1779
|
-
process.exit(0);
|
|
1780
|
-
}
|
|
1689
|
+
if (isCancel(template)) return exitCancelled("Operation cancelled");
|
|
1781
1690
|
const templateArg = TEMPLATES[template].value;
|
|
1782
|
-
const commandWithArgs = `create-fumadocs-app@latest fumadocs --template ${templateArg} --src --no-install --pm ${packageManager} --no-eslint`;
|
|
1691
|
+
const commandWithArgs = `create-fumadocs-app@latest fumadocs --template ${templateArg} --src --no-install --pm ${packageManager} --no-eslint --no-git`;
|
|
1783
1692
|
const fumadocsInitCommand = getPackageExecutionCommand(packageManager, commandWithArgs);
|
|
1784
1693
|
await execa(fumadocsInitCommand, {
|
|
1785
1694
|
cwd: path.join(projectDir, "apps"),
|
|
@@ -1941,10 +1850,7 @@ async function setupUltracite(config, hasHusky) {
|
|
|
1941
1850
|
})),
|
|
1942
1851
|
required: false
|
|
1943
1852
|
});
|
|
1944
|
-
if (isCancel(editors))
|
|
1945
|
-
cancel(pc.red("Operation cancelled"));
|
|
1946
|
-
process.exit(0);
|
|
1947
|
-
}
|
|
1853
|
+
if (isCancel(editors)) return exitCancelled("Operation cancelled");
|
|
1948
1854
|
const rules = await multiselect({
|
|
1949
1855
|
message: "Choose rules",
|
|
1950
1856
|
options: Object.entries(RULES).map(([key, rule]) => ({
|
|
@@ -1954,10 +1860,7 @@ async function setupUltracite(config, hasHusky) {
|
|
|
1954
1860
|
})),
|
|
1955
1861
|
required: false
|
|
1956
1862
|
});
|
|
1957
|
-
if (isCancel(rules))
|
|
1958
|
-
cancel(pc.red("Operation cancelled"));
|
|
1959
|
-
process.exit(0);
|
|
1960
|
-
}
|
|
1863
|
+
if (isCancel(rules)) return exitCancelled("Operation cancelled");
|
|
1961
1864
|
const ultraciteArgs = [
|
|
1962
1865
|
"init",
|
|
1963
1866
|
"--pm",
|
|
@@ -1989,6 +1892,135 @@ async function setupUltracite(config, hasHusky) {
|
|
|
1989
1892
|
}
|
|
1990
1893
|
}
|
|
1991
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
|
+
|
|
1992
2024
|
//#endregion
|
|
1993
2025
|
//#region src/utils/ts-morph.ts
|
|
1994
2026
|
const tsProject = new Project({
|
|
@@ -2080,6 +2112,7 @@ ${pc.cyan("Docs:")} ${pc.underline("https://turborepo.com/docs")}
|
|
|
2080
2112
|
}
|
|
2081
2113
|
if (addons.includes("oxlint")) await setupOxlint(projectDir, packageManager);
|
|
2082
2114
|
if (addons.includes("starlight")) await setupStarlight(config);
|
|
2115
|
+
if (addons.includes("vibe-rules")) await setupVibeRules(config);
|
|
2083
2116
|
if (addons.includes("fumadocs")) await setupFumadocs(config);
|
|
2084
2117
|
}
|
|
2085
2118
|
function getWebAppDir(projectDir, frontends) {
|
|
@@ -2224,31 +2257,6 @@ async function installDependencies({ projectDir, packageManager }) {
|
|
|
2224
2257
|
}
|
|
2225
2258
|
}
|
|
2226
2259
|
|
|
2227
|
-
//#endregion
|
|
2228
|
-
//#region src/utils/template-processor.ts
|
|
2229
|
-
/**
|
|
2230
|
-
* Processes a Handlebars template file and writes the output to the destination.
|
|
2231
|
-
* @param srcPath Path to the source .hbs template file.
|
|
2232
|
-
* @param destPath Path to write the processed file.
|
|
2233
|
-
* @param context Data to be passed to the Handlebars template.
|
|
2234
|
-
*/
|
|
2235
|
-
async function processTemplate(srcPath, destPath, context) {
|
|
2236
|
-
try {
|
|
2237
|
-
const templateContent = await fs.readFile(srcPath, "utf-8");
|
|
2238
|
-
const template = handlebars.compile(templateContent);
|
|
2239
|
-
const processedContent = template(context);
|
|
2240
|
-
await fs.ensureDir(path.dirname(destPath));
|
|
2241
|
-
await fs.writeFile(destPath, processedContent);
|
|
2242
|
-
} catch (error) {
|
|
2243
|
-
consola.error(`Error processing template ${srcPath}:`, error);
|
|
2244
|
-
throw new Error(`Failed to process template ${srcPath}`);
|
|
2245
|
-
}
|
|
2246
|
-
}
|
|
2247
|
-
handlebars.registerHelper("eq", (a, b) => a === b);
|
|
2248
|
-
handlebars.registerHelper("and", (a, b) => a && b);
|
|
2249
|
-
handlebars.registerHelper("or", (a, b) => a || b);
|
|
2250
|
-
handlebars.registerHelper("includes", (array, value) => Array.isArray(array) && array.includes(value));
|
|
2251
|
-
|
|
2252
2260
|
//#endregion
|
|
2253
2261
|
//#region src/helpers/project-generation/template-manager.ts
|
|
2254
2262
|
async function processAndCopyFiles(sourcePattern, baseSourceDir, destDir, context, overwrite = true) {
|
|
@@ -2459,6 +2467,7 @@ async function setupAddonsTemplate(projectDir, context) {
|
|
|
2459
2467
|
if (!context.addons || context.addons.length === 0) return;
|
|
2460
2468
|
for (const addon of context.addons) {
|
|
2461
2469
|
if (addon === "none") continue;
|
|
2470
|
+
if (addon === "vibe-rules") continue;
|
|
2462
2471
|
let addonSrcDir = path.join(PKG_ROOT, `templates/addons/${addon}`);
|
|
2463
2472
|
let addonDestDir = projectDir;
|
|
2464
2473
|
if (addon === "pwa") {
|
|
@@ -2619,17 +2628,13 @@ async function setupDeploymentTemplates(projectDir, context) {
|
|
|
2619
2628
|
|
|
2620
2629
|
//#endregion
|
|
2621
2630
|
//#region src/helpers/project-generation/add-addons.ts
|
|
2622
|
-
function exitWithError$1(message) {
|
|
2623
|
-
cancel(pc.red(message));
|
|
2624
|
-
process.exit(1);
|
|
2625
|
-
}
|
|
2626
2631
|
async function addAddonsToProject(input) {
|
|
2627
2632
|
try {
|
|
2628
2633
|
const projectDir = input.projectDir || process.cwd();
|
|
2629
2634
|
const isBetterTStack = await isBetterTStackProject(projectDir);
|
|
2630
|
-
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.");
|
|
2631
2636
|
const detectedConfig = await detectProjectConfig(projectDir);
|
|
2632
|
-
if (!detectedConfig) exitWithError
|
|
2637
|
+
if (!detectedConfig) exitWithError("Could not detect the project configuration. Please ensure this is a valid Better-T Stack project.");
|
|
2633
2638
|
const config = {
|
|
2634
2639
|
projectName: detectedConfig.projectName || path.basename(projectDir),
|
|
2635
2640
|
projectDir,
|
|
@@ -2651,9 +2656,8 @@ async function addAddonsToProject(input) {
|
|
|
2651
2656
|
};
|
|
2652
2657
|
for (const addon of input.addons) {
|
|
2653
2658
|
const { isCompatible, reason } = validateAddonCompatibility(addon, config.frontend);
|
|
2654
|
-
if (!isCompatible) exitWithError
|
|
2659
|
+
if (!isCompatible) exitWithError(reason || `${addon} addon is not compatible with current frontend configuration`);
|
|
2655
2660
|
}
|
|
2656
|
-
log.info(`Adding ${input.addons.join(", ")} to ${config.frontend.join("/")}`);
|
|
2657
2661
|
await setupAddonsTemplate(projectDir, config);
|
|
2658
2662
|
await setupAddons(config, true);
|
|
2659
2663
|
const currentAddons = detectedConfig.addons || [];
|
|
@@ -2666,7 +2670,7 @@ async function addAddonsToProject(input) {
|
|
|
2666
2670
|
else if (!input.suppressInstallMessage) log.info(pc.yellow(`Run ${pc.bold(`${config.packageManager} install`)} to install dependencies`));
|
|
2667
2671
|
} catch (error) {
|
|
2668
2672
|
const message = error instanceof Error ? error.message : String(error);
|
|
2669
|
-
exitWithError
|
|
2673
|
+
exitWithError(`Error adding addons: ${message}`);
|
|
2670
2674
|
}
|
|
2671
2675
|
}
|
|
2672
2676
|
|
|
@@ -2899,10 +2903,6 @@ async function setupNextWorkersDeploy(projectDir, _packageManager) {
|
|
|
2899
2903
|
|
|
2900
2904
|
//#endregion
|
|
2901
2905
|
//#region src/helpers/project-generation/add-deployment.ts
|
|
2902
|
-
function exitWithError(message) {
|
|
2903
|
-
cancel(pc.red(message));
|
|
2904
|
-
process.exit(1);
|
|
2905
|
-
}
|
|
2906
2906
|
async function addDeploymentToProject(input) {
|
|
2907
2907
|
try {
|
|
2908
2908
|
const projectDir = input.projectDir || process.cwd();
|
|
@@ -3702,10 +3702,7 @@ async function setupNeonPostgres(config) {
|
|
|
3702
3702
|
}],
|
|
3703
3703
|
initialValue: "neondb"
|
|
3704
3704
|
});
|
|
3705
|
-
if (isCancel(setupMethod))
|
|
3706
|
-
cancel(pc.red("Operation cancelled"));
|
|
3707
|
-
process.exit(0);
|
|
3708
|
-
}
|
|
3705
|
+
if (isCancel(setupMethod)) return exitCancelled("Operation cancelled");
|
|
3709
3706
|
if (setupMethod === "neondb") await setupWithNeonDb(projectDir, packageManager);
|
|
3710
3707
|
else {
|
|
3711
3708
|
const suggestedProjectName = path.basename(projectDir);
|
|
@@ -3719,10 +3716,7 @@ async function setupNeonPostgres(config) {
|
|
|
3719
3716
|
options: NEON_REGIONS,
|
|
3720
3717
|
initialValue: NEON_REGIONS[0].value
|
|
3721
3718
|
});
|
|
3722
|
-
if (isCancel(projectName) || isCancel(regionId))
|
|
3723
|
-
cancel(pc.red("Operation cancelled"));
|
|
3724
|
-
process.exit(0);
|
|
3725
|
-
}
|
|
3719
|
+
if (isCancel(projectName) || isCancel(regionId)) return exitCancelled("Operation cancelled");
|
|
3726
3720
|
const neonConfig = await createNeonProject(projectName, regionId, packageManager);
|
|
3727
3721
|
if (!neonConfig) throw new Error("Failed to create project - couldn't get connection information");
|
|
3728
3722
|
const finalSpinner = spinner();
|
|
@@ -3757,10 +3751,7 @@ async function setupWithCreateDb(serverDir, packageManager, orm) {
|
|
|
3757
3751
|
if (orm === "drizzle" && !value.includes("?sslmode=require")) return "Please append ?sslmode=require to your database URL when using Drizzle";
|
|
3758
3752
|
}
|
|
3759
3753
|
});
|
|
3760
|
-
if (isCancel(databaseUrl))
|
|
3761
|
-
cancel("Database setup cancelled");
|
|
3762
|
-
return null;
|
|
3763
|
-
}
|
|
3754
|
+
if (isCancel(databaseUrl)) return null;
|
|
3764
3755
|
return { databaseUrl };
|
|
3765
3756
|
} catch (error) {
|
|
3766
3757
|
if (error instanceof Error) consola$1.error(error.message);
|
|
@@ -3786,10 +3777,7 @@ async function initPrismaDatabase(serverDir, packageManager) {
|
|
|
3786
3777
|
if (!value.startsWith("prisma+postgres://")) return "URL should start with prisma+postgres://";
|
|
3787
3778
|
}
|
|
3788
3779
|
});
|
|
3789
|
-
if (isCancel(databaseUrl))
|
|
3790
|
-
cancel("Database setup cancelled");
|
|
3791
|
-
return null;
|
|
3792
|
-
}
|
|
3780
|
+
if (isCancel(databaseUrl)) return null;
|
|
3793
3781
|
return { databaseUrl };
|
|
3794
3782
|
} catch (error) {
|
|
3795
3783
|
if (error instanceof Error) consola$1.error(error.message);
|
|
@@ -3880,10 +3868,7 @@ async function setupPrismaPostgres(config) {
|
|
|
3880
3868
|
options: setupOptions,
|
|
3881
3869
|
initialValue: "create-db"
|
|
3882
3870
|
});
|
|
3883
|
-
if (isCancel(setupMethod))
|
|
3884
|
-
cancel(pc.red("Operation cancelled"));
|
|
3885
|
-
process.exit(0);
|
|
3886
|
-
}
|
|
3871
|
+
if (isCancel(setupMethod)) return exitCancelled("Operation cancelled");
|
|
3887
3872
|
let prismaConfig = null;
|
|
3888
3873
|
if (setupMethod === "create-db") prismaConfig = await setupWithCreateDb(serverDir, packageManager, orm);
|
|
3889
3874
|
else prismaConfig = await initPrismaDatabase(serverDir, packageManager);
|
|
@@ -4117,10 +4102,7 @@ async function selectTursoGroup() {
|
|
|
4117
4102
|
message: "Select a Turso database group:",
|
|
4118
4103
|
options: groupOptions
|
|
4119
4104
|
});
|
|
4120
|
-
if (isCancel(selectedGroup))
|
|
4121
|
-
cancel(pc.red("Operation cancelled"));
|
|
4122
|
-
process.exit(0);
|
|
4123
|
-
}
|
|
4105
|
+
if (isCancel(selectedGroup)) return exitCancelled("Operation cancelled");
|
|
4124
4106
|
return selectedGroup;
|
|
4125
4107
|
}
|
|
4126
4108
|
async function createTursoDatabase(dbName, groupName) {
|
|
@@ -4193,10 +4175,7 @@ async function setupTurso(config) {
|
|
|
4193
4175
|
message: "Would you like to install Turso CLI?",
|
|
4194
4176
|
initialValue: true
|
|
4195
4177
|
});
|
|
4196
|
-
if (isCancel(shouldInstall))
|
|
4197
|
-
cancel(pc.red("Operation cancelled"));
|
|
4198
|
-
process.exit(0);
|
|
4199
|
-
}
|
|
4178
|
+
if (isCancel(shouldInstall)) return exitCancelled("Operation cancelled");
|
|
4200
4179
|
if (!shouldInstall) {
|
|
4201
4180
|
await writeEnvFile(projectDir);
|
|
4202
4181
|
displayManualSetupInstructions();
|
|
@@ -4217,10 +4196,7 @@ async function setupTurso(config) {
|
|
|
4217
4196
|
initialValue: suggestedName,
|
|
4218
4197
|
placeholder: suggestedName
|
|
4219
4198
|
});
|
|
4220
|
-
if (isCancel(dbNameResponse))
|
|
4221
|
-
cancel(pc.red("Operation cancelled"));
|
|
4222
|
-
process.exit(0);
|
|
4223
|
-
}
|
|
4199
|
+
if (isCancel(dbNameResponse)) return exitCancelled("Operation cancelled");
|
|
4224
4200
|
dbName = dbNameResponse;
|
|
4225
4201
|
try {
|
|
4226
4202
|
const config$1 = await createTursoDatabase(dbName, selectedGroup);
|
|
@@ -5137,13 +5113,11 @@ async function createProject(options) {
|
|
|
5137
5113
|
return projectDir;
|
|
5138
5114
|
} catch (error) {
|
|
5139
5115
|
if (error instanceof Error) {
|
|
5140
|
-
cancel(pc.red(`Error during project creation: ${error.message}`));
|
|
5141
5116
|
console.error(error.stack);
|
|
5142
|
-
|
|
5117
|
+
exitWithError(`Error during project creation: ${error.message}`);
|
|
5143
5118
|
} else {
|
|
5144
|
-
cancel(pc.red(`An unexpected error occurred: ${String(error)}`));
|
|
5145
5119
|
console.error(error);
|
|
5146
|
-
|
|
5120
|
+
exitWithError(`An unexpected error occurred: ${String(error)}`);
|
|
5147
5121
|
}
|
|
5148
5122
|
}
|
|
5149
5123
|
}
|
|
@@ -5202,18 +5176,14 @@ async function createProjectHandler(input) {
|
|
|
5202
5176
|
const elapsedTimeInSeconds = ((Date.now() - startTime) / 1e3).toFixed(2);
|
|
5203
5177
|
outro(pc.magenta(`Project created successfully in ${pc.bold(elapsedTimeInSeconds)} seconds!`));
|
|
5204
5178
|
} catch (error) {
|
|
5205
|
-
|
|
5206
|
-
process.exit(1);
|
|
5179
|
+
handleError(error, "Failed to create project");
|
|
5207
5180
|
}
|
|
5208
5181
|
}
|
|
5209
5182
|
async function addAddonsHandler(input) {
|
|
5210
5183
|
try {
|
|
5211
5184
|
const projectDir = input.projectDir || process.cwd();
|
|
5212
5185
|
const detectedConfig = await detectProjectConfig(projectDir);
|
|
5213
|
-
if (!detectedConfig)
|
|
5214
|
-
cancel(pc.red("Could not detect project configuration. Please ensure this is a valid Better-T Stack project."));
|
|
5215
|
-
process.exit(1);
|
|
5216
|
-
}
|
|
5186
|
+
if (!detectedConfig) exitWithError("Could not detect project configuration. Please ensure this is a valid Better-T Stack project.");
|
|
5217
5187
|
if (!input.addons || input.addons.length === 0) {
|
|
5218
5188
|
const addonsPrompt = await getAddonsToAdd(detectedConfig.frontend || [], detectedConfig.addons || []);
|
|
5219
5189
|
if (addonsPrompt.length > 0) input.addons = addonsPrompt;
|
|
@@ -5253,8 +5223,7 @@ async function addAddonsHandler(input) {
|
|
|
5253
5223
|
else log.info(`Run ${pc.bold(`${packageManager} install`)} to install dependencies`);
|
|
5254
5224
|
outro("Add command completed successfully!");
|
|
5255
5225
|
} catch (error) {
|
|
5256
|
-
|
|
5257
|
-
process.exit(1);
|
|
5226
|
+
handleError(error, "Failed to add addons or deployment");
|
|
5258
5227
|
}
|
|
5259
5228
|
}
|
|
5260
5229
|
|
|
@@ -5369,8 +5338,7 @@ const router = t.router({
|
|
|
5369
5338
|
const sponsors = await fetchSponsors();
|
|
5370
5339
|
displaySponsors(sponsors);
|
|
5371
5340
|
} catch (error) {
|
|
5372
|
-
|
|
5373
|
-
process.exit(1);
|
|
5341
|
+
handleError(error, "Failed to display sponsors");
|
|
5374
5342
|
}
|
|
5375
5343
|
}),
|
|
5376
5344
|
docs: t.procedure.meta({ description: "Open Better-T Stack documentation" }).mutation(async () => {
|