create-better-t-stack 3.7.3-canary.7cbb05fc → 3.7.3-canary.8e4d5716

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.
Files changed (104) hide show
  1. package/package.json +19 -23
  2. package/src/cli.ts +3 -0
  3. package/src/constants.ts +188 -0
  4. package/src/helpers/addons/addons-setup.ts +226 -0
  5. package/src/helpers/addons/examples-setup.ts +104 -0
  6. package/src/helpers/addons/fumadocs-setup.ts +103 -0
  7. package/src/helpers/addons/ruler-setup.ts +139 -0
  8. package/src/helpers/addons/starlight-setup.ts +51 -0
  9. package/src/helpers/addons/tauri-setup.ts +96 -0
  10. package/src/helpers/addons/ultracite-setup.ts +232 -0
  11. package/src/helpers/addons/vite-pwa-setup.ts +59 -0
  12. package/src/helpers/core/add-addons.ts +85 -0
  13. package/src/helpers/core/add-deployment.ts +102 -0
  14. package/src/helpers/core/api-setup.ts +280 -0
  15. package/src/helpers/core/auth-setup.ts +203 -0
  16. package/src/helpers/core/backend-setup.ts +73 -0
  17. package/src/helpers/core/command-handlers.ts +354 -0
  18. package/src/helpers/core/convex-codegen.ts +14 -0
  19. package/src/helpers/core/create-project.ts +133 -0
  20. package/src/helpers/core/create-readme.ts +687 -0
  21. package/src/helpers/core/db-setup.ts +184 -0
  22. package/src/helpers/core/detect-project-config.ts +41 -0
  23. package/src/helpers/core/env-setup.ts +449 -0
  24. package/src/helpers/core/git.ts +31 -0
  25. package/src/helpers/core/install-dependencies.ts +32 -0
  26. package/src/helpers/core/payments-setup.ts +48 -0
  27. package/src/helpers/core/post-installation.ts +383 -0
  28. package/src/helpers/core/project-config.ts +246 -0
  29. package/src/helpers/core/runtime-setup.ts +76 -0
  30. package/src/helpers/core/template-manager.ts +917 -0
  31. package/src/helpers/core/workspace-setup.ts +184 -0
  32. package/src/helpers/database-providers/d1-setup.ts +28 -0
  33. package/src/helpers/database-providers/docker-compose-setup.ts +50 -0
  34. package/src/helpers/database-providers/mongodb-atlas-setup.ts +186 -0
  35. package/src/helpers/database-providers/neon-setup.ts +243 -0
  36. package/src/helpers/database-providers/planetscale-setup.ts +78 -0
  37. package/src/helpers/database-providers/prisma-postgres-setup.ts +196 -0
  38. package/src/helpers/database-providers/supabase-setup.ts +218 -0
  39. package/src/helpers/database-providers/turso-setup.ts +309 -0
  40. package/src/helpers/deployment/alchemy/alchemy-combined-setup.ts +80 -0
  41. package/src/helpers/deployment/alchemy/alchemy-next-setup.ts +51 -0
  42. package/src/helpers/deployment/alchemy/alchemy-nuxt-setup.ts +104 -0
  43. package/src/helpers/deployment/alchemy/alchemy-react-router-setup.ts +32 -0
  44. package/src/helpers/deployment/alchemy/alchemy-solid-setup.ts +32 -0
  45. package/src/helpers/deployment/alchemy/alchemy-svelte-setup.ts +98 -0
  46. package/src/helpers/deployment/alchemy/alchemy-tanstack-router-setup.ts +33 -0
  47. package/src/helpers/deployment/alchemy/alchemy-tanstack-start-setup.ts +98 -0
  48. package/src/helpers/deployment/alchemy/env-dts-setup.ts +76 -0
  49. package/src/helpers/deployment/alchemy/index.ts +7 -0
  50. package/src/helpers/deployment/server-deploy-setup.ts +55 -0
  51. package/src/helpers/deployment/web-deploy-setup.ts +58 -0
  52. package/src/index.ts +253 -0
  53. package/src/prompts/addons.ts +178 -0
  54. package/src/prompts/api.ts +49 -0
  55. package/src/prompts/auth.ts +84 -0
  56. package/src/prompts/backend.ts +83 -0
  57. package/src/prompts/config-prompts.ts +138 -0
  58. package/src/prompts/database-setup.ts +112 -0
  59. package/src/prompts/database.ts +57 -0
  60. package/src/prompts/examples.ts +64 -0
  61. package/src/prompts/frontend.ts +118 -0
  62. package/src/prompts/git.ts +16 -0
  63. package/src/prompts/install.ts +16 -0
  64. package/src/prompts/orm.ts +53 -0
  65. package/src/prompts/package-manager.ts +32 -0
  66. package/src/prompts/payments.ts +50 -0
  67. package/src/prompts/project-name.ts +86 -0
  68. package/src/prompts/runtime.ts +47 -0
  69. package/src/prompts/server-deploy.ts +91 -0
  70. package/src/prompts/web-deploy.ts +107 -0
  71. package/src/types.ts +2 -0
  72. package/src/utils/add-package-deps.ts +57 -0
  73. package/src/utils/analytics.ts +39 -0
  74. package/src/utils/better-auth-plugin-setup.ts +71 -0
  75. package/src/utils/biome-formatter.ts +82 -0
  76. package/src/utils/bts-config.ts +122 -0
  77. package/src/utils/command-exists.ts +16 -0
  78. package/src/utils/compatibility-rules.ts +319 -0
  79. package/src/utils/compatibility.ts +11 -0
  80. package/src/utils/config-processing.ts +130 -0
  81. package/src/utils/config-validation.ts +470 -0
  82. package/src/utils/display-config.ts +96 -0
  83. package/src/utils/docker-utils.ts +70 -0
  84. package/src/utils/errors.ts +32 -0
  85. package/src/utils/generate-reproducible-command.ts +53 -0
  86. package/src/utils/get-latest-cli-version.ts +11 -0
  87. package/src/utils/get-package-manager.ts +13 -0
  88. package/src/utils/open-url.ts +25 -0
  89. package/src/utils/package-runner.ts +23 -0
  90. package/src/utils/project-directory.ts +102 -0
  91. package/src/utils/project-name-validation.ts +43 -0
  92. package/src/utils/render-title.ts +48 -0
  93. package/src/utils/setup-catalogs.ts +192 -0
  94. package/src/utils/sponsors.ts +101 -0
  95. package/src/utils/telemetry.ts +19 -0
  96. package/src/utils/template-processor.ts +64 -0
  97. package/src/utils/templates.ts +94 -0
  98. package/src/utils/ts-morph.ts +26 -0
  99. package/src/validation.ts +117 -0
  100. package/dist/cli.d.mts +0 -1
  101. package/dist/cli.mjs +0 -8
  102. package/dist/index.d.mts +0 -347
  103. package/dist/index.mjs +0 -4
  104. package/dist/src-CxVxLS85.mjs +0 -7077
@@ -0,0 +1,53 @@
1
+ import type { ProjectConfig } from "../types";
2
+
3
+ export function generateReproducibleCommand(config: ProjectConfig) {
4
+ const flags: string[] = [];
5
+
6
+ if (config.frontend && config.frontend.length > 0) {
7
+ flags.push(`--frontend ${config.frontend.join(" ")}`);
8
+ } else {
9
+ flags.push("--frontend none");
10
+ }
11
+
12
+ flags.push(`--backend ${config.backend}`);
13
+ flags.push(`--runtime ${config.runtime}`);
14
+ flags.push(`--database ${config.database}`);
15
+ flags.push(`--orm ${config.orm}`);
16
+ flags.push(`--api ${config.api}`);
17
+ flags.push(`--auth ${config.auth}`);
18
+ flags.push(`--payments ${config.payments}`);
19
+
20
+ if (config.addons && config.addons.length > 0) {
21
+ flags.push(`--addons ${config.addons.join(" ")}`);
22
+ } else {
23
+ flags.push("--addons none");
24
+ }
25
+
26
+ if (config.examples && config.examples.length > 0) {
27
+ flags.push(`--examples ${config.examples.join(" ")}`);
28
+ } else {
29
+ flags.push("--examples none");
30
+ }
31
+
32
+ flags.push(`--db-setup ${config.dbSetup}`);
33
+ flags.push(`--web-deploy ${config.webDeploy}`);
34
+ flags.push(`--server-deploy ${config.serverDeploy}`);
35
+ flags.push(config.git ? "--git" : "--no-git");
36
+ flags.push(`--package-manager ${config.packageManager}`);
37
+ flags.push(config.install ? "--install" : "--no-install");
38
+
39
+ let baseCommand = "npx create-better-t-stack@latest";
40
+ const pkgManager = config.packageManager;
41
+
42
+ if (pkgManager === "bun") {
43
+ baseCommand = "bun create better-t-stack@latest";
44
+ } else if (pkgManager === "pnpm") {
45
+ baseCommand = "pnpm create better-t-stack@latest";
46
+ } else if (pkgManager === "npm") {
47
+ baseCommand = "npx create-better-t-stack@latest";
48
+ }
49
+
50
+ const projectPathArg = config.relativePath ? ` ${config.relativePath}` : "";
51
+
52
+ return `${baseCommand}${projectPathArg} ${flags.join(" ")}`;
53
+ }
@@ -0,0 +1,11 @@
1
+ import path from "node:path";
2
+ import fs from "fs-extra";
3
+ import { PKG_ROOT } from "../constants";
4
+
5
+ export const getLatestCLIVersion = () => {
6
+ const packageJsonPath = path.join(PKG_ROOT, "package.json");
7
+
8
+ const packageJsonContent = fs.readJSONSync(packageJsonPath);
9
+
10
+ return packageJsonContent.version ?? "1.0.0";
11
+ };
@@ -0,0 +1,13 @@
1
+ import type { PackageManager } from "../types";
2
+
3
+ export const getUserPkgManager: () => PackageManager = () => {
4
+ const userAgent = process.env.npm_config_user_agent;
5
+
6
+ if (userAgent?.startsWith("pnpm")) {
7
+ return "pnpm";
8
+ }
9
+ if (userAgent?.startsWith("bun")) {
10
+ return "bun";
11
+ }
12
+ return "npm";
13
+ };
@@ -0,0 +1,25 @@
1
+ import { log } from "@clack/prompts";
2
+ import { execa } from "execa";
3
+
4
+ export async function openUrl(url: string) {
5
+ const platform = process.platform;
6
+ let command: string;
7
+ let args: string[] = [];
8
+
9
+ if (platform === "darwin") {
10
+ command = "open";
11
+ args = [url];
12
+ } else if (platform === "win32") {
13
+ command = "cmd";
14
+ args = ["/c", "start", "", url.replace(/&/g, "^&")];
15
+ } else {
16
+ command = "xdg-open";
17
+ args = [url];
18
+ }
19
+
20
+ try {
21
+ await execa(command, args, { stdio: "ignore" });
22
+ } catch {
23
+ log.message(`Please open ${url} in your browser.`);
24
+ }
25
+ }
@@ -0,0 +1,23 @@
1
+ import type { PackageManager } from "../types";
2
+
3
+ /**
4
+ * Returns the appropriate command for running a package without installing it globally,
5
+ * based on the selected package manager.
6
+ *
7
+ * @param packageManager - The selected package manager (e.g., 'npm', 'yarn', 'pnpm', 'bun').
8
+ * @param commandWithArgs - The command to run, including arguments (e.g., "prisma generate --schema=./prisma/schema.prisma").
9
+ * @returns The full command string (e.g., "npx prisma generate --schema=./prisma/schema.prisma").
10
+ */
11
+ export function getPackageExecutionCommand(
12
+ packageManager: PackageManager | null | undefined,
13
+ commandWithArgs: string,
14
+ ) {
15
+ switch (packageManager) {
16
+ case "pnpm":
17
+ return `pnpm dlx ${commandWithArgs}`;
18
+ case "bun":
19
+ return `bunx ${commandWithArgs}`;
20
+ default:
21
+ return `npx ${commandWithArgs}`;
22
+ }
23
+ }
@@ -0,0 +1,102 @@
1
+ import path from "node:path";
2
+ import { isCancel, log, select, spinner } from "@clack/prompts";
3
+ import fs from "fs-extra";
4
+ import pc from "picocolors";
5
+ import { getProjectName } from "../prompts/project-name";
6
+ import { exitCancelled, handleError } from "./errors";
7
+
8
+ export async function handleDirectoryConflict(currentPathInput: string, silent = false) {
9
+ while (true) {
10
+ const resolvedPath = path.resolve(process.cwd(), currentPathInput);
11
+ const dirExists = await fs.pathExists(resolvedPath);
12
+ const dirIsNotEmpty = dirExists && (await fs.readdir(resolvedPath)).length > 0;
13
+
14
+ if (!dirIsNotEmpty) {
15
+ return { finalPathInput: currentPathInput, shouldClearDirectory: false };
16
+ }
17
+
18
+ if (silent) {
19
+ throw new Error(
20
+ `Directory "${currentPathInput}" already exists and is not empty. In silent mode, please provide a different project name or clear the directory manually.`,
21
+ );
22
+ }
23
+
24
+ log.warn(`Directory "${pc.yellow(currentPathInput)}" already exists and is not empty.`);
25
+
26
+ const action = await select<"overwrite" | "merge" | "rename" | "cancel">({
27
+ message: "What would you like to do?",
28
+ options: [
29
+ {
30
+ value: "overwrite",
31
+ label: "Overwrite",
32
+ hint: "Empty the directory and create the project",
33
+ },
34
+ {
35
+ value: "merge",
36
+ label: "Merge",
37
+ hint: "Create project files inside, potentially overwriting conflicts",
38
+ },
39
+ {
40
+ value: "rename",
41
+ label: "Choose a different name/path",
42
+ hint: "Keep the existing directory and create a new one",
43
+ },
44
+ { value: "cancel", label: "Cancel", hint: "Abort the process" },
45
+ ],
46
+ initialValue: "rename",
47
+ });
48
+
49
+ if (isCancel(action)) return exitCancelled("Operation cancelled.");
50
+
51
+ switch (action) {
52
+ case "overwrite":
53
+ return { finalPathInput: currentPathInput, shouldClearDirectory: true };
54
+ case "merge":
55
+ log.info(
56
+ `Proceeding into existing directory "${pc.yellow(
57
+ currentPathInput,
58
+ )}". Files may be overwritten.`,
59
+ );
60
+ return {
61
+ finalPathInput: currentPathInput,
62
+ shouldClearDirectory: false,
63
+ };
64
+ case "rename": {
65
+ log.info("Please choose a different project name or path.");
66
+ const newPathInput = await getProjectName(undefined);
67
+ return await handleDirectoryConflict(newPathInput);
68
+ }
69
+ case "cancel":
70
+ return exitCancelled("Operation cancelled.");
71
+ }
72
+ }
73
+ }
74
+
75
+ export async function setupProjectDirectory(finalPathInput: string, shouldClearDirectory: boolean) {
76
+ let finalResolvedPath: string;
77
+ let finalBaseName: string;
78
+
79
+ if (finalPathInput === ".") {
80
+ finalResolvedPath = process.cwd();
81
+ finalBaseName = path.basename(finalResolvedPath);
82
+ } else {
83
+ finalResolvedPath = path.resolve(process.cwd(), finalPathInput);
84
+ finalBaseName = path.basename(finalResolvedPath);
85
+ }
86
+
87
+ if (shouldClearDirectory) {
88
+ const s = spinner();
89
+ s.start(`Clearing directory "${finalResolvedPath}"...`);
90
+ try {
91
+ await fs.emptyDir(finalResolvedPath);
92
+ s.stop(`Directory "${finalResolvedPath}" cleared.`);
93
+ } catch (error) {
94
+ s.stop(pc.red(`Failed to clear directory "${finalResolvedPath}".`));
95
+ handleError(error);
96
+ }
97
+ } else {
98
+ await fs.ensureDir(finalResolvedPath);
99
+ }
100
+
101
+ return { finalResolvedPath, finalBaseName };
102
+ }
@@ -0,0 +1,43 @@
1
+ import path from "node:path";
2
+ import { ProjectNameSchema } from "../types";
3
+ import { exitWithError } from "./errors";
4
+
5
+ export function validateProjectName(name: string) {
6
+ const result = ProjectNameSchema.safeParse(name);
7
+ if (!result.success) {
8
+ exitWithError(
9
+ `Invalid project name: ${result.error.issues[0]?.message || "Invalid project name"}`,
10
+ );
11
+ }
12
+ }
13
+
14
+ export function validateProjectNameThrow(name: string) {
15
+ const result = ProjectNameSchema.safeParse(name);
16
+ if (!result.success) {
17
+ throw new Error(`Invalid project name: ${result.error.issues[0]?.message}`);
18
+ }
19
+ }
20
+
21
+ export function extractAndValidateProjectName(
22
+ projectName?: string,
23
+ projectDirectory?: string,
24
+ throwOnError = false,
25
+ ) {
26
+ const derivedName =
27
+ projectName ||
28
+ (projectDirectory ? path.basename(path.resolve(process.cwd(), projectDirectory)) : "");
29
+
30
+ if (!derivedName) {
31
+ return "";
32
+ }
33
+
34
+ const nameToValidate = projectName ? path.basename(projectName) : derivedName;
35
+
36
+ if (throwOnError) {
37
+ validateProjectNameThrow(nameToValidate);
38
+ } else {
39
+ validateProjectName(nameToValidate);
40
+ }
41
+
42
+ return projectName || derivedName;
43
+ }
@@ -0,0 +1,48 @@
1
+ import gradient from "gradient-string";
2
+
3
+ export const TITLE_TEXT = `
4
+ ██████╗ ███████╗████████╗████████╗███████╗██████╗
5
+ ██╔══██╗██╔════╝╚══██╔══╝╚══██╔══╝██╔════╝██╔══██╗
6
+ ██████╔╝█████╗ ██║ ██║ █████╗ ██████╔╝
7
+ ██╔══██╗██╔══╝ ██║ ██║ ██╔══╝ ██╔══██╗
8
+ ██████╔╝███████╗ ██║ ██║ ███████╗██║ ██║
9
+ ╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝
10
+
11
+ ████████╗ ███████╗████████╗ █████╗ ██████╗██╗ ██╗
12
+ ╚══██╔══╝ ██╔════╝╚══██╔══╝██╔══██╗██╔════╝██║ ██╔╝
13
+ ██║ ███████╗ ██║ ███████║██║ █████╔╝
14
+ ██║ ╚════██║ ██║ ██╔══██║██║ ██╔═██╗
15
+ ██║ ███████║ ██║ ██║ ██║╚██████╗██║ ██╗
16
+ ╚═╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝
17
+ `;
18
+
19
+ const catppuccinTheme = {
20
+ pink: "#F5C2E7",
21
+ mauve: "#CBA6F7",
22
+ red: "#F38BA8",
23
+ maroon: "#E78284",
24
+ peach: "#FAB387",
25
+ yellow: "#F9E2AF",
26
+ green: "#A6E3A1",
27
+ teal: "#94E2D5",
28
+ sky: "#89DCEB",
29
+ sapphire: "#74C7EC",
30
+ lavender: "#B4BEFE",
31
+ };
32
+
33
+ export const renderTitle = () => {
34
+ const terminalWidth = process.stdout.columns || 80;
35
+ const titleLines = TITLE_TEXT.split("\n");
36
+ const titleWidth = Math.max(...titleLines.map((line) => line.length));
37
+
38
+ if (terminalWidth < titleWidth) {
39
+ const simplifiedTitle = `
40
+ ╔══════════════════╗
41
+ ║ Better T Stack ║
42
+ ╚══════════════════╝
43
+ `;
44
+ console.log(gradient(Object.values(catppuccinTheme)).multiline(simplifiedTitle));
45
+ } else {
46
+ console.log(gradient(Object.values(catppuccinTheme)).multiline(TITLE_TEXT));
47
+ }
48
+ };
@@ -0,0 +1,192 @@
1
+ import path from "node:path";
2
+ import fs from "fs-extra";
3
+ import yaml from "yaml";
4
+ import type { ProjectConfig } from "../types";
5
+
6
+ type PackageInfo = {
7
+ path: string;
8
+ dependencies: Record<string, string>;
9
+ devDependencies: Record<string, string>;
10
+ };
11
+
12
+ type CatalogEntry = {
13
+ versions: Set<string>;
14
+ packages: string[];
15
+ };
16
+
17
+ export async function setupCatalogs(projectDir: string, options: ProjectConfig) {
18
+ if (options.packageManager === "npm") {
19
+ return;
20
+ }
21
+
22
+ const packagePaths = [
23
+ ".", // root monorepo
24
+ "apps/server",
25
+ "apps/web",
26
+ "apps/native",
27
+ "apps/fumadocs",
28
+ "apps/docs",
29
+ "packages/api",
30
+ "packages/db",
31
+ "packages/auth",
32
+ "packages/backend",
33
+ "packages/config",
34
+ ];
35
+
36
+ const packagesInfo: PackageInfo[] = [];
37
+
38
+ for (const pkgPath of packagePaths) {
39
+ const fullPath = path.join(projectDir, pkgPath);
40
+ const pkgJsonPath = path.join(fullPath, "package.json");
41
+
42
+ if (await fs.pathExists(pkgJsonPath)) {
43
+ const pkgJson = await fs.readJson(pkgJsonPath);
44
+ packagesInfo.push({
45
+ path: fullPath,
46
+ dependencies: pkgJson.dependencies || {},
47
+ devDependencies: pkgJson.devDependencies || {},
48
+ });
49
+ }
50
+ }
51
+
52
+ const catalog = findDuplicateDependencies(packagesInfo, options.projectName);
53
+
54
+ if (Object.keys(catalog).length === 0) {
55
+ return;
56
+ }
57
+
58
+ if (options.packageManager === "bun") {
59
+ await setupBunCatalogs(projectDir, catalog);
60
+ } else if (options.packageManager === "pnpm") {
61
+ await setupPnpmCatalogs(projectDir, catalog);
62
+ }
63
+
64
+ await updatePackageJsonsWithCatalogs(packagesInfo, catalog);
65
+ }
66
+
67
+ function findDuplicateDependencies(
68
+ packagesInfo: PackageInfo[],
69
+ projectName: string,
70
+ ): Record<string, string> {
71
+ const depCount = new Map<string, CatalogEntry>();
72
+ const projectScope = `@${projectName}/`;
73
+
74
+ for (const pkg of packagesInfo) {
75
+ const allDeps = {
76
+ ...pkg.dependencies,
77
+ ...pkg.devDependencies,
78
+ };
79
+
80
+ for (const [depName, version] of Object.entries(allDeps)) {
81
+ if (depName.startsWith(projectScope)) {
82
+ continue;
83
+ }
84
+
85
+ if (version.startsWith("workspace:")) {
86
+ continue;
87
+ }
88
+
89
+ const existing = depCount.get(depName);
90
+ if (existing) {
91
+ existing.versions.add(version);
92
+ existing.packages.push(pkg.path);
93
+ } else {
94
+ depCount.set(depName, {
95
+ versions: new Set([version]),
96
+ packages: [pkg.path],
97
+ });
98
+ }
99
+ }
100
+ }
101
+
102
+ const catalog: Record<string, string> = {};
103
+ for (const [depName, info] of depCount.entries()) {
104
+ if (info.packages.length > 1 && info.versions.size === 1) {
105
+ catalog[depName] = Array.from(info.versions)[0];
106
+ }
107
+ }
108
+
109
+ return catalog;
110
+ }
111
+
112
+ async function setupBunCatalogs(projectDir: string, catalog: Record<string, string>) {
113
+ const rootPkgJsonPath = path.join(projectDir, "package.json");
114
+ const rootPkgJson = await fs.readJson(rootPkgJsonPath);
115
+
116
+ if (!rootPkgJson.workspaces) {
117
+ rootPkgJson.workspaces = {};
118
+ }
119
+
120
+ if (Array.isArray(rootPkgJson.workspaces)) {
121
+ rootPkgJson.workspaces = {
122
+ packages: rootPkgJson.workspaces,
123
+ catalog,
124
+ };
125
+ } else if (typeof rootPkgJson.workspaces === "object") {
126
+ if (!rootPkgJson.workspaces.catalog) {
127
+ rootPkgJson.workspaces.catalog = {};
128
+ }
129
+ rootPkgJson.workspaces.catalog = {
130
+ ...rootPkgJson.workspaces.catalog,
131
+ ...catalog,
132
+ };
133
+ }
134
+
135
+ await fs.writeJson(rootPkgJsonPath, rootPkgJson, { spaces: 2 });
136
+ }
137
+
138
+ async function setupPnpmCatalogs(projectDir: string, catalog: Record<string, string>) {
139
+ const workspaceYamlPath = path.join(projectDir, "pnpm-workspace.yaml");
140
+
141
+ if (!(await fs.pathExists(workspaceYamlPath))) {
142
+ return;
143
+ }
144
+
145
+ const workspaceContent = await fs.readFile(workspaceYamlPath, "utf-8");
146
+ const workspaceYaml = yaml.parse(workspaceContent);
147
+
148
+ if (!workspaceYaml.catalog) {
149
+ workspaceYaml.catalog = {};
150
+ }
151
+
152
+ workspaceYaml.catalog = {
153
+ ...workspaceYaml.catalog,
154
+ ...catalog,
155
+ };
156
+
157
+ await fs.writeFile(workspaceYamlPath, yaml.stringify(workspaceYaml));
158
+ }
159
+
160
+ async function updatePackageJsonsWithCatalogs(
161
+ packagesInfo: PackageInfo[],
162
+ catalog: Record<string, string>,
163
+ ) {
164
+ for (const pkg of packagesInfo) {
165
+ const pkgJsonPath = path.join(pkg.path, "package.json");
166
+ const pkgJson = await fs.readJson(pkgJsonPath);
167
+
168
+ let updated = false;
169
+
170
+ if (pkgJson.dependencies) {
171
+ for (const depName of Object.keys(pkgJson.dependencies)) {
172
+ if (catalog[depName]) {
173
+ pkgJson.dependencies[depName] = "catalog:";
174
+ updated = true;
175
+ }
176
+ }
177
+ }
178
+
179
+ if (pkgJson.devDependencies) {
180
+ for (const depName of Object.keys(pkgJson.devDependencies)) {
181
+ if (catalog[depName]) {
182
+ pkgJson.devDependencies[depName] = "catalog:";
183
+ updated = true;
184
+ }
185
+ }
186
+ }
187
+
188
+ if (updated) {
189
+ await fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 });
190
+ }
191
+ }
192
+ }
@@ -0,0 +1,101 @@
1
+ import { log, outro, spinner } from "@clack/prompts";
2
+ import { consola } from "consola";
3
+ import pc from "picocolors";
4
+
5
+ type SponsorSummary = {
6
+ total_sponsors: number;
7
+ total_lifetime_amount: number;
8
+ total_current_monthly: number;
9
+ special_sponsors: number;
10
+ current_sponsors: number;
11
+ past_sponsors: number;
12
+ backers: number;
13
+ top_sponsor?: {
14
+ name: string;
15
+ amount: number;
16
+ };
17
+ };
18
+
19
+ type Sponsor = {
20
+ name?: string;
21
+ githubId: string;
22
+ avatarUrl: string;
23
+ websiteUrl?: string;
24
+ githubUrl: string;
25
+ tierName?: string;
26
+ sinceWhen: string;
27
+ transactionCount: number;
28
+ totalProcessedAmount?: number;
29
+ formattedAmount?: string;
30
+ };
31
+
32
+ type SponsorEntry = {
33
+ generated_at: string;
34
+ summary: SponsorSummary;
35
+ specialSponsors: Sponsor[];
36
+ sponsors: Sponsor[];
37
+ pastSponsors: Sponsor[];
38
+ backers: Sponsor[];
39
+ };
40
+
41
+ export const SPONSORS_JSON_URL = "https://sponsors.better-t-stack.dev/sponsors.json";
42
+
43
+ export async function fetchSponsors(url: string = SPONSORS_JSON_URL) {
44
+ const s = spinner();
45
+ s.start("Fetching sponsors…");
46
+
47
+ const response = await fetch(url);
48
+ if (!response.ok) {
49
+ s.stop(pc.red(`Failed to fetch sponsors: ${response.statusText}`));
50
+ throw new Error(`Failed to fetch sponsors: ${response.statusText}`);
51
+ }
52
+
53
+ const sponsors = (await response.json()) as SponsorEntry;
54
+ s.stop("Sponsors fetched successfully!");
55
+ return sponsors;
56
+ }
57
+
58
+ export function displaySponsors(sponsors: SponsorEntry) {
59
+ const { total_sponsors } = sponsors.summary;
60
+ if (total_sponsors === 0) {
61
+ log.info("No sponsors found. You can be the first one! ✨");
62
+ outro(pc.cyan("Visit https://github.com/sponsors/AmanVarshney01 to become a sponsor."));
63
+ return;
64
+ }
65
+
66
+ displaySponsorsBox(sponsors);
67
+
68
+ if (total_sponsors - sponsors.specialSponsors.length > 0) {
69
+ log.message(
70
+ pc.blue(`+${total_sponsors - sponsors.specialSponsors.length} more amazing sponsors.\n`),
71
+ );
72
+ }
73
+ outro(pc.magenta("Visit https://github.com/sponsors/AmanVarshney01 to become a sponsor."));
74
+ }
75
+
76
+ function displaySponsorsBox(sponsors: SponsorEntry) {
77
+ if (sponsors.specialSponsors.length === 0) {
78
+ return;
79
+ }
80
+
81
+ let output = `${pc.bold(pc.cyan("-> Special Sponsors"))}\n\n`;
82
+
83
+ sponsors.specialSponsors.forEach((sponsor: Sponsor, idx: number) => {
84
+ const displayName = sponsor.name ?? sponsor.githubId;
85
+ const tier = sponsor.tierName ? ` ${pc.yellow(`(${sponsor.tierName})`)}` : "";
86
+
87
+ output += `${pc.green(`• ${displayName}`)}${tier}\n`;
88
+ output += ` ${pc.dim("GitHub:")} https://github.com/${sponsor.githubId}\n`;
89
+
90
+ const website = sponsor.websiteUrl ?? sponsor.githubUrl;
91
+ if (website) {
92
+ output += ` ${pc.dim("Website:")} ${website}\n`;
93
+ }
94
+
95
+ if (idx < sponsors.specialSponsors.length - 1) {
96
+ output += "\n";
97
+ }
98
+ });
99
+
100
+ consola.box(output);
101
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Returns true if telemetry/analytics should be enabled, false otherwise.
3
+ *
4
+ * - If BTS_TELEMETRY_DISABLED is present and "1", disables analytics.
5
+ * - Otherwise, BTS_TELEMETRY: "0" disables, "1" enables (default: enabled).
6
+ */
7
+ export function isTelemetryEnabled() {
8
+ const BTS_TELEMETRY_DISABLED = process.env.BTS_TELEMETRY_DISABLED;
9
+ const BTS_TELEMETRY = process.env.BTS_TELEMETRY;
10
+
11
+ if (BTS_TELEMETRY_DISABLED !== undefined) {
12
+ return BTS_TELEMETRY_DISABLED !== "1";
13
+ }
14
+ if (BTS_TELEMETRY !== undefined) {
15
+ return BTS_TELEMETRY === "1";
16
+ }
17
+ // Default: enabled
18
+ return true;
19
+ }