create-better-t-stack 2.32.2 → 2.33.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { createBtsCli } from "./src-DVlkWMko.js";
2
+ import { createBtsCli } from "./src-T8rBM5gC.js";
3
3
 
4
4
  //#region src/cli.ts
5
5
  createBtsCli().run();
package/dist/index.d.ts CHANGED
@@ -56,7 +56,7 @@ declare const AddonsSchema: z.ZodEnum<{
56
56
  starlight: "starlight";
57
57
  biome: "biome";
58
58
  husky: "husky";
59
- "vibe-rules": "vibe-rules";
59
+ ruler: "ruler";
60
60
  turborepo: "turborepo";
61
61
  fumadocs: "fumadocs";
62
62
  ultracite: "ultracite";
@@ -196,7 +196,7 @@ declare const router: trpcServer.TRPCBuiltRouter<{
196
196
  orm?: "none" | "drizzle" | "prisma" | "mongoose" | undefined;
197
197
  auth?: boolean | undefined;
198
198
  frontend?: ("none" | "tanstack-router" | "react-router" | "tanstack-start" | "next" | "nuxt" | "native-nativewind" | "native-unistyles" | "svelte" | "solid")[] | undefined;
199
- addons?: ("none" | "pwa" | "tauri" | "starlight" | "biome" | "husky" | "vibe-rules" | "turborepo" | "fumadocs" | "ultracite" | "oxlint")[] | undefined;
199
+ addons?: ("none" | "pwa" | "tauri" | "starlight" | "biome" | "husky" | "ruler" | "turborepo" | "fumadocs" | "ultracite" | "oxlint")[] | undefined;
200
200
  examples?: ("none" | "todo" | "ai")[] | undefined;
201
201
  git?: boolean | undefined;
202
202
  packageManager?: "npm" | "pnpm" | "bun" | undefined;
@@ -215,7 +215,7 @@ declare const router: trpcServer.TRPCBuiltRouter<{
215
215
  }>;
216
216
  add: trpcServer.TRPCMutationProcedure<{
217
217
  input: [{
218
- addons?: ("none" | "pwa" | "tauri" | "starlight" | "biome" | "husky" | "vibe-rules" | "turborepo" | "fumadocs" | "ultracite" | "oxlint")[] | undefined;
218
+ addons?: ("none" | "pwa" | "tauri" | "starlight" | "biome" | "husky" | "ruler" | "turborepo" | "fumadocs" | "ultracite" | "oxlint")[] | undefined;
219
219
  webDeploy?: "none" | "workers" | undefined;
220
220
  projectDir?: string | undefined;
221
221
  install?: boolean | undefined;
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import { builder, createBtsCli, docs, init, router, sponsors } from "./src-DVlkWMko.js";
2
+ import { builder, createBtsCli, docs, init, router, sponsors } from "./src-T8rBM5gC.js";
3
3
 
4
4
  export { builder, createBtsCli, docs, init, router, sponsors };
@@ -10,9 +10,9 @@ import { fileURLToPath } from "node:url";
10
10
  import gradient from "gradient-string";
11
11
  import * as JSONC from "jsonc-parser";
12
12
  import { $, execa } from "execa";
13
+ import { globby } from "globby";
13
14
  import handlebars from "handlebars";
14
15
  import { IndentationText, Node, Project, QuoteKind, SyntaxKind } from "ts-morph";
15
- import { globby } from "globby";
16
16
  import os from "node:os";
17
17
 
18
18
  //#region src/utils/get-package-manager.ts
@@ -136,7 +136,7 @@ const ADDON_COMPATIBILITY = {
136
136
  turborepo: [],
137
137
  starlight: [],
138
138
  ultracite: [],
139
- "vibe-rules": [],
139
+ "ruler": [],
140
140
  oxlint: [],
141
141
  fumadocs: [],
142
142
  none: []
@@ -190,7 +190,7 @@ const AddonsSchema = z.enum([
190
190
  "starlight",
191
191
  "biome",
192
192
  "husky",
193
- "vibe-rules",
193
+ "ruler",
194
194
  "turborepo",
195
195
  "fumadocs",
196
196
  "ultracite",
@@ -318,8 +318,8 @@ function getAddonDisplay(addon) {
318
318
  label = "Ultracite";
319
319
  hint = "Zero-config Biome preset with AI integration";
320
320
  break;
321
- case "vibe-rules":
322
- label = "vibe-rules";
321
+ case "ruler":
322
+ label = "Ruler";
323
323
  hint = "Install and apply BTS rules to editors";
324
324
  break;
325
325
  case "husky":
@@ -351,7 +351,7 @@ const ADDON_GROUPS = {
351
351
  "ultracite"
352
352
  ],
353
353
  Other: [
354
- "vibe-rules",
354
+ "ruler",
355
355
  "turborepo",
356
356
  "pwa",
357
357
  "tauri",
@@ -1942,373 +1942,143 @@ handlebars.registerHelper("or", (a, b) => a || b);
1942
1942
  handlebars.registerHelper("includes", (array, value) => Array.isArray(array) && array.includes(value));
1943
1943
 
1944
1944
  //#endregion
1945
- //#region src/helpers/setup/vibe-rules-setup.ts
1946
- async function setupVibeRules(config) {
1947
- const { packageManager, projectDir } = config;
1948
- try {
1949
- log.info("Setting up vibe-rules...");
1950
- const rulesDir = path.join(projectDir, ".bts");
1951
- const ruleFile = path.join(rulesDir, "rules.md");
1952
- if (!await fs.pathExists(ruleFile)) {
1953
- const templatePath = path.join(PKG_ROOT, "templates", "addons", "vibe-rules", ".bts", "rules.md.hbs");
1954
- if (await fs.pathExists(templatePath)) {
1955
- await fs.ensureDir(rulesDir);
1956
- await processTemplate(templatePath, ruleFile, config);
1957
- } else {
1958
- log.error(pc.red("Rules template not found for vibe-rules addon"));
1959
- return;
1945
+ //#region src/helpers/project-generation/template-manager.ts
1946
+ async function processAndCopyFiles(sourcePattern, baseSourceDir, destDir, context, overwrite = true, ignorePatterns) {
1947
+ const sourceFiles = await globby(sourcePattern, {
1948
+ cwd: baseSourceDir,
1949
+ dot: true,
1950
+ onlyFiles: true,
1951
+ absolute: false,
1952
+ ignore: ignorePatterns
1953
+ });
1954
+ for (const relativeSrcPath of sourceFiles) {
1955
+ const srcPath = path.join(baseSourceDir, relativeSrcPath);
1956
+ let relativeDestPath = relativeSrcPath;
1957
+ if (relativeSrcPath.endsWith(".hbs")) relativeDestPath = relativeSrcPath.slice(0, -4);
1958
+ const basename = path.basename(relativeDestPath);
1959
+ if (basename === "_gitignore") relativeDestPath = path.join(path.dirname(relativeDestPath), ".gitignore");
1960
+ else if (basename === "_npmrc") relativeDestPath = path.join(path.dirname(relativeDestPath), ".npmrc");
1961
+ const destPath = path.join(destDir, relativeDestPath);
1962
+ await fs.ensureDir(path.dirname(destPath));
1963
+ if (!overwrite && await fs.pathExists(destPath)) continue;
1964
+ if (srcPath.endsWith(".hbs")) await processTemplate(srcPath, destPath, context);
1965
+ else await fs.copy(srcPath, destPath, { overwrite: true });
1966
+ }
1967
+ }
1968
+ async function copyBaseTemplate(projectDir, context) {
1969
+ const templateDir = path.join(PKG_ROOT, "templates/base");
1970
+ await processAndCopyFiles(["**/*"], templateDir, projectDir, context);
1971
+ }
1972
+ async function setupFrontendTemplates(projectDir, context) {
1973
+ const hasReactWeb = context.frontend.some((f) => [
1974
+ "tanstack-router",
1975
+ "react-router",
1976
+ "tanstack-start",
1977
+ "next"
1978
+ ].includes(f));
1979
+ const hasNuxtWeb = context.frontend.includes("nuxt");
1980
+ const hasSvelteWeb = context.frontend.includes("svelte");
1981
+ const hasSolidWeb = context.frontend.includes("solid");
1982
+ const hasNativeWind = context.frontend.includes("native-nativewind");
1983
+ const hasUnistyles = context.frontend.includes("native-unistyles");
1984
+ const isConvex = context.backend === "convex";
1985
+ if (hasReactWeb || hasNuxtWeb || hasSvelteWeb || hasSolidWeb) {
1986
+ const webAppDir = path.join(projectDir, "apps/web");
1987
+ await fs.ensureDir(webAppDir);
1988
+ if (hasReactWeb) {
1989
+ const webBaseDir = path.join(PKG_ROOT, "templates/frontend/react/web-base");
1990
+ if (await fs.pathExists(webBaseDir)) await processAndCopyFiles("**/*", webBaseDir, webAppDir, context);
1991
+ const reactFramework = context.frontend.find((f) => [
1992
+ "tanstack-router",
1993
+ "react-router",
1994
+ "tanstack-start",
1995
+ "next"
1996
+ ].includes(f));
1997
+ if (reactFramework) {
1998
+ const frameworkSrcDir = path.join(PKG_ROOT, `templates/frontend/react/${reactFramework}`);
1999
+ if (await fs.pathExists(frameworkSrcDir)) await processAndCopyFiles("**/*", frameworkSrcDir, webAppDir, context);
2000
+ if (!isConvex && context.api !== "none") {
2001
+ const apiWebBaseDir = path.join(PKG_ROOT, `templates/api/${context.api}/web/react/base`);
2002
+ if (await fs.pathExists(apiWebBaseDir)) await processAndCopyFiles("**/*", apiWebBaseDir, webAppDir, context);
2003
+ }
1960
2004
  }
1961
- }
1962
- const EDITORS$1 = {
1963
- cursor: {
1964
- label: "Cursor",
1965
- hint: ".cursor/rules/*.mdc"
1966
- },
1967
- windsurf: {
1968
- label: "Windsurf",
1969
- hint: ".windsurfrules"
1970
- },
1971
- "claude-code": {
1972
- label: "Claude Code",
1973
- hint: "CLAUDE.md"
1974
- },
1975
- vscode: {
1976
- label: "VSCode",
1977
- hint: ".github/instructions/*.instructions.md"
1978
- },
1979
- gemini: {
1980
- label: "Gemini",
1981
- hint: "GEMINI.md"
1982
- },
1983
- codex: {
1984
- label: "Codex",
1985
- hint: "AGENTS.md"
1986
- },
1987
- clinerules: {
1988
- label: "Cline/Roo",
1989
- hint: ".clinerules/*.md"
1990
- },
1991
- roo: {
1992
- label: "Roo",
1993
- hint: ".clinerules/*.md"
1994
- },
1995
- zed: {
1996
- label: "Zed",
1997
- hint: ".rules/*.md"
1998
- },
1999
- unified: {
2000
- label: "Unified",
2001
- hint: ".rules/*.md"
2005
+ } else if (hasNuxtWeb) {
2006
+ const nuxtBaseDir = path.join(PKG_ROOT, "templates/frontend/nuxt");
2007
+ if (await fs.pathExists(nuxtBaseDir)) await processAndCopyFiles("**/*", nuxtBaseDir, webAppDir, context);
2008
+ if (!isConvex && context.api === "orpc") {
2009
+ const apiWebNuxtDir = path.join(PKG_ROOT, `templates/api/${context.api}/web/nuxt`);
2010
+ if (await fs.pathExists(apiWebNuxtDir)) await processAndCopyFiles("**/*", apiWebNuxtDir, webAppDir, context);
2002
2011
  }
2003
- };
2004
- const selectedEditors = await multiselect({
2005
- message: "Choose editors to install BTS rule",
2006
- options: Object.entries(EDITORS$1).map(([key, v]) => ({
2007
- value: key,
2008
- label: v.label,
2009
- hint: v.hint
2010
- })),
2011
- required: false
2012
- });
2013
- if (isCancel(selectedEditors)) return exitCancelled("Operation cancelled");
2014
- const editorsArg = selectedEditors.join(", ");
2015
- const s = spinner();
2016
- s.start("Saving and applying BTS rules...");
2017
- try {
2018
- const saveCmd = getPackageExecutionCommand(packageManager, `vibe-rules@latest save bts -f ${JSON.stringify(path.relative(projectDir, ruleFile))}`);
2019
- await execa(saveCmd, {
2020
- cwd: projectDir,
2021
- env: { CI: "true" },
2022
- shell: true
2023
- });
2024
- for (const editor of selectedEditors) {
2025
- const loadCmd = getPackageExecutionCommand(packageManager, `vibe-rules@latest load bts ${editor}`);
2026
- await execa(loadCmd, {
2027
- cwd: projectDir,
2028
- env: { CI: "true" },
2029
- shell: true
2030
- });
2012
+ } else if (hasSvelteWeb) {
2013
+ const svelteBaseDir = path.join(PKG_ROOT, "templates/frontend/svelte");
2014
+ if (await fs.pathExists(svelteBaseDir)) await processAndCopyFiles("**/*", svelteBaseDir, webAppDir, context);
2015
+ if (!isConvex && context.api === "orpc") {
2016
+ const apiWebSvelteDir = path.join(PKG_ROOT, `templates/api/${context.api}/web/svelte`);
2017
+ if (await fs.pathExists(apiWebSvelteDir)) await processAndCopyFiles("**/*", apiWebSvelteDir, webAppDir, context);
2018
+ }
2019
+ } else if (hasSolidWeb) {
2020
+ const solidBaseDir = path.join(PKG_ROOT, "templates/frontend/solid");
2021
+ if (await fs.pathExists(solidBaseDir)) await processAndCopyFiles("**/*", solidBaseDir, webAppDir, context);
2022
+ if (!isConvex && context.api === "orpc") {
2023
+ const apiWebSolidDir = path.join(PKG_ROOT, `templates/api/${context.api}/web/solid`);
2024
+ if (await fs.pathExists(apiWebSolidDir)) await processAndCopyFiles("**/*", apiWebSolidDir, webAppDir, context);
2031
2025
  }
2032
- s.stop(`Applied BTS rules to: ${editorsArg}`);
2033
- } catch (error) {
2034
- s.stop(pc.red("Failed to apply BTS rules"));
2035
- throw error;
2036
2026
  }
2037
- try {
2038
- await fs.remove(rulesDir);
2039
- } catch (_) {}
2040
- log.success("vibe-rules setup successfully!");
2041
- } catch (error) {
2042
- log.error(pc.red("Failed to set up vibe-rules"));
2043
- if (error instanceof Error) console.error(pc.red(error.message));
2044
2027
  }
2045
- }
2046
-
2047
- //#endregion
2048
- //#region src/utils/ts-morph.ts
2049
- const tsProject = new Project({
2050
- useInMemoryFileSystem: false,
2051
- skipAddingFilesFromTsConfig: true,
2052
- manipulationSettings: {
2053
- quoteKind: QuoteKind.Single,
2054
- indentationText: IndentationText.TwoSpaces
2028
+ if (hasNativeWind || hasUnistyles) {
2029
+ const nativeAppDir = path.join(projectDir, "apps/native");
2030
+ await fs.ensureDir(nativeAppDir);
2031
+ const nativeBaseCommonDir = path.join(PKG_ROOT, "templates/frontend/native/native-base");
2032
+ if (await fs.pathExists(nativeBaseCommonDir)) await processAndCopyFiles("**/*", nativeBaseCommonDir, nativeAppDir, context);
2033
+ let nativeFrameworkPath = "";
2034
+ if (hasNativeWind) nativeFrameworkPath = "nativewind";
2035
+ else if (hasUnistyles) nativeFrameworkPath = "unistyles";
2036
+ const nativeSpecificDir = path.join(PKG_ROOT, `templates/frontend/native/${nativeFrameworkPath}`);
2037
+ if (await fs.pathExists(nativeSpecificDir)) await processAndCopyFiles("**/*", nativeSpecificDir, nativeAppDir, context, true);
2038
+ if (!isConvex && (context.api === "trpc" || context.api === "orpc")) {
2039
+ const apiNativeSrcDir = path.join(PKG_ROOT, `templates/api/${context.api}/native`);
2040
+ if (await fs.pathExists(apiNativeSrcDir)) await processAndCopyFiles("**/*", apiNativeSrcDir, nativeAppDir, context);
2041
+ }
2055
2042
  }
2056
- });
2057
- function ensureArrayProperty(obj, name) {
2058
- return obj.getProperty(name)?.getFirstDescendantByKind(SyntaxKind.ArrayLiteralExpression) ?? obj.addPropertyAssignment({
2059
- name,
2060
- initializer: "[]"
2061
- }).getFirstDescendantByKindOrThrow(SyntaxKind.ArrayLiteralExpression);
2062
- }
2063
-
2064
- //#endregion
2065
- //#region src/helpers/setup/vite-pwa-setup.ts
2066
- async function addPwaToViteConfig(viteConfigPath, projectName) {
2067
- const sourceFile = tsProject.addSourceFileAtPathIfExists(viteConfigPath);
2068
- if (!sourceFile) throw new Error("vite config not found");
2069
- const hasImport = sourceFile.getImportDeclarations().some((imp) => imp.getModuleSpecifierValue() === "vite-plugin-pwa");
2070
- if (!hasImport) sourceFile.insertImportDeclaration(0, {
2071
- namedImports: ["VitePWA"],
2072
- moduleSpecifier: "vite-plugin-pwa"
2073
- });
2074
- const defineCall = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression).find((expr) => {
2075
- const expression = expr.getExpression();
2076
- return Node.isIdentifier(expression) && expression.getText() === "defineConfig";
2077
- });
2078
- if (!defineCall) throw new Error("Could not find defineConfig call in vite config");
2079
- const callExpr = defineCall;
2080
- const configObject = callExpr.getArguments()[0];
2081
- if (!configObject) throw new Error("defineConfig argument is not an object literal");
2082
- const pluginsArray = ensureArrayProperty(configObject, "plugins");
2083
- const alreadyPresent = pluginsArray.getElements().some((el) => el.getText().startsWith("VitePWA("));
2084
- if (!alreadyPresent) pluginsArray.addElement(`VitePWA({
2085
- registerType: "autoUpdate",
2086
- manifest: {
2087
- name: "${projectName}",
2088
- short_name: "${projectName}",
2089
- description: "${projectName} - PWA Application",
2090
- theme_color: "#0c0c0c",
2091
- },
2092
- pwaAssets: { disabled: false, config: true },
2093
- devOptions: { enabled: true },
2094
- })`);
2095
- await tsProject.save();
2096
2043
  }
2097
-
2098
- //#endregion
2099
- //#region src/helpers/setup/addons-setup.ts
2100
- async function setupAddons(config, isAddCommand = false) {
2101
- const { addons, frontend, projectDir, packageManager } = config;
2102
- const hasReactWebFrontend = frontend.includes("react-router") || frontend.includes("tanstack-router") || frontend.includes("next");
2103
- const hasNuxtFrontend = frontend.includes("nuxt");
2104
- const hasSvelteFrontend = frontend.includes("svelte");
2105
- const hasSolidFrontend = frontend.includes("solid");
2106
- const hasNextFrontend = frontend.includes("next");
2107
- if (addons.includes("turborepo")) {
2108
- await addPackageDependency({
2109
- devDependencies: ["turbo"],
2110
- projectDir
2111
- });
2112
- if (isAddCommand) log.info(`${pc.yellow("Update your package.json scripts:")}
2113
-
2114
- ${pc.dim("Replace:")} ${pc.yellow("\"pnpm -r dev\"")} ${pc.dim("→")} ${pc.green("\"turbo dev\"")}
2115
- ${pc.dim("Replace:")} ${pc.yellow("\"pnpm --filter web dev\"")} ${pc.dim("→")} ${pc.green("\"turbo -F web dev\"")}
2116
-
2117
- ${pc.cyan("Docs:")} ${pc.underline("https://turborepo.com/docs")}
2118
- `);
2044
+ async function setupBackendFramework(projectDir, context) {
2045
+ if (context.backend === "none") return;
2046
+ const serverAppDir = path.join(projectDir, "apps/server");
2047
+ if (context.backend === "convex") {
2048
+ if (await fs.pathExists(serverAppDir)) await fs.remove(serverAppDir);
2049
+ const convexBackendDestDir = path.join(projectDir, "packages/backend");
2050
+ const convexSrcDir = path.join(PKG_ROOT, "templates/backend/convex/packages/backend");
2051
+ await fs.ensureDir(convexBackendDestDir);
2052
+ if (await fs.pathExists(convexSrcDir)) await processAndCopyFiles("**/*", convexSrcDir, convexBackendDestDir, context);
2053
+ return;
2119
2054
  }
2120
- if (addons.includes("pwa") && (hasReactWebFrontend || hasSolidFrontend)) await setupPwa(projectDir, frontend);
2121
- if (addons.includes("tauri") && (hasReactWebFrontend || hasNuxtFrontend || hasSvelteFrontend || hasSolidFrontend || hasNextFrontend)) await setupTauri(config);
2122
- const hasUltracite = addons.includes("ultracite");
2123
- const hasBiome = addons.includes("biome");
2124
- const hasHusky = addons.includes("husky");
2125
- const hasOxlint = addons.includes("oxlint");
2126
- if (hasUltracite) await setupUltracite(config, hasHusky);
2127
- else {
2128
- if (hasBiome) await setupBiome(projectDir);
2129
- if (hasHusky) {
2130
- let linter;
2131
- if (hasOxlint) linter = "oxlint";
2132
- else if (hasBiome) linter = "biome";
2133
- await setupHusky(projectDir, linter);
2134
- }
2055
+ await fs.ensureDir(serverAppDir);
2056
+ const serverBaseDir = path.join(PKG_ROOT, "templates/backend/server/server-base");
2057
+ if (await fs.pathExists(serverBaseDir)) await processAndCopyFiles("**/*", serverBaseDir, serverAppDir, context);
2058
+ const frameworkSrcDir = path.join(PKG_ROOT, `templates/backend/server/${context.backend}`);
2059
+ if (await fs.pathExists(frameworkSrcDir)) await processAndCopyFiles("**/*", frameworkSrcDir, serverAppDir, context, true);
2060
+ if (context.api !== "none") {
2061
+ const apiServerBaseDir = path.join(PKG_ROOT, `templates/api/${context.api}/server/base`);
2062
+ if (await fs.pathExists(apiServerBaseDir)) await processAndCopyFiles("**/*", apiServerBaseDir, serverAppDir, context, true);
2063
+ const apiServerFrameworkDir = path.join(PKG_ROOT, `templates/api/${context.api}/server/${context.backend}`);
2064
+ if (await fs.pathExists(apiServerFrameworkDir)) await processAndCopyFiles("**/*", apiServerFrameworkDir, serverAppDir, context, true);
2135
2065
  }
2136
- if (addons.includes("oxlint")) await setupOxlint(projectDir, packageManager);
2137
- if (addons.includes("starlight")) await setupStarlight(config);
2138
- if (addons.includes("vibe-rules")) await setupVibeRules(config);
2139
- if (addons.includes("fumadocs")) await setupFumadocs(config);
2140
2066
  }
2141
- function getWebAppDir(projectDir, frontends) {
2142
- if (frontends.some((f) => [
2143
- "react-router",
2144
- "tanstack-router",
2145
- "nuxt",
2146
- "svelte",
2147
- "solid"
2148
- ].includes(f))) return path.join(projectDir, "apps/web");
2149
- return path.join(projectDir, "apps/web");
2067
+ async function setupDbOrmTemplates(projectDir, context) {
2068
+ if (context.backend === "convex" || context.orm === "none" || context.database === "none") return;
2069
+ const serverAppDir = path.join(projectDir, "apps/server");
2070
+ await fs.ensureDir(serverAppDir);
2071
+ const dbOrmSrcDir = path.join(PKG_ROOT, `templates/db/${context.orm}/${context.database}`);
2072
+ if (await fs.pathExists(dbOrmSrcDir)) await processAndCopyFiles("**/*", dbOrmSrcDir, serverAppDir, context);
2150
2073
  }
2151
- async function setupBiome(projectDir) {
2152
- await addPackageDependency({
2153
- devDependencies: ["@biomejs/biome"],
2154
- projectDir
2155
- });
2156
- const packageJsonPath = path.join(projectDir, "package.json");
2157
- if (await fs.pathExists(packageJsonPath)) {
2158
- const packageJson = await fs.readJson(packageJsonPath);
2159
- packageJson.scripts = {
2160
- ...packageJson.scripts,
2161
- check: "biome check --write ."
2162
- };
2163
- await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
2164
- }
2165
- }
2166
- async function setupHusky(projectDir, linter) {
2167
- await addPackageDependency({
2168
- devDependencies: ["husky", "lint-staged"],
2169
- projectDir
2170
- });
2171
- const packageJsonPath = path.join(projectDir, "package.json");
2172
- if (await fs.pathExists(packageJsonPath)) {
2173
- const packageJson = await fs.readJson(packageJsonPath);
2174
- packageJson.scripts = {
2175
- ...packageJson.scripts,
2176
- prepare: "husky"
2177
- };
2178
- if (linter === "oxlint") packageJson["lint-staged"] = { "**/*.{js,mjs,cjs,jsx,ts,mts,cts,tsx,vue,astro,svelte}": "oxlint" };
2179
- else if (linter === "biome") packageJson["lint-staged"] = { "*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}": ["biome check --write ."] };
2180
- else packageJson["lint-staged"] = { "**/*.{js,mjs,cjs,jsx,ts,mts,cts,tsx,vue,astro,svelte}": "" };
2181
- await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
2182
- }
2183
- }
2184
- async function setupPwa(projectDir, frontends) {
2185
- const isCompatibleFrontend = frontends.some((f) => [
2186
- "react-router",
2187
- "tanstack-router",
2188
- "solid"
2189
- ].includes(f));
2190
- if (!isCompatibleFrontend) return;
2191
- const clientPackageDir = getWebAppDir(projectDir, frontends);
2192
- if (!await fs.pathExists(clientPackageDir)) return;
2193
- await addPackageDependency({
2194
- dependencies: ["vite-plugin-pwa"],
2195
- devDependencies: ["@vite-pwa/assets-generator"],
2196
- projectDir: clientPackageDir
2197
- });
2198
- const clientPackageJsonPath = path.join(clientPackageDir, "package.json");
2199
- if (await fs.pathExists(clientPackageJsonPath)) {
2200
- const packageJson = await fs.readJson(clientPackageJsonPath);
2201
- packageJson.scripts = {
2202
- ...packageJson.scripts,
2203
- "generate-pwa-assets": "pwa-assets-generator"
2204
- };
2205
- await fs.writeJson(clientPackageJsonPath, packageJson, { spaces: 2 });
2206
- }
2207
- const viteConfigTs = path.join(clientPackageDir, "vite.config.ts");
2208
- if (await fs.pathExists(viteConfigTs)) await addPwaToViteConfig(viteConfigTs, path.basename(projectDir));
2209
- }
2210
- async function setupOxlint(projectDir, packageManager) {
2211
- await addPackageDependency({
2212
- devDependencies: ["oxlint"],
2213
- projectDir
2214
- });
2215
- const packageJsonPath = path.join(projectDir, "package.json");
2216
- if (await fs.pathExists(packageJsonPath)) {
2217
- const packageJson = await fs.readJson(packageJsonPath);
2218
- packageJson.scripts = {
2219
- ...packageJson.scripts,
2220
- check: "oxlint"
2221
- };
2222
- await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
2223
- }
2224
- const oxlintInitCommand = getPackageExecutionCommand(packageManager, "oxlint@latest --init");
2225
- await execa(oxlintInitCommand, {
2226
- cwd: projectDir,
2227
- env: { CI: "true" },
2228
- shell: true
2229
- });
2230
- }
2231
-
2232
- //#endregion
2233
- //#region src/helpers/project-generation/detect-project-config.ts
2234
- async function detectProjectConfig(projectDir) {
2235
- try {
2236
- const btsConfig = await readBtsConfig(projectDir);
2237
- if (btsConfig) return {
2238
- projectDir,
2239
- projectName: path.basename(projectDir),
2240
- database: btsConfig.database,
2241
- orm: btsConfig.orm,
2242
- backend: btsConfig.backend,
2243
- runtime: btsConfig.runtime,
2244
- frontend: btsConfig.frontend,
2245
- addons: btsConfig.addons,
2246
- examples: btsConfig.examples,
2247
- auth: btsConfig.auth,
2248
- packageManager: btsConfig.packageManager,
2249
- dbSetup: btsConfig.dbSetup,
2250
- api: btsConfig.api,
2251
- webDeploy: btsConfig.webDeploy
2252
- };
2253
- return null;
2254
- } catch (_error) {
2255
- return null;
2256
- }
2257
- }
2258
- async function isBetterTStackProject(projectDir) {
2259
- try {
2260
- return await fs.pathExists(path.join(projectDir, "bts.jsonc"));
2261
- } catch (_error) {
2262
- return false;
2263
- }
2264
- }
2265
-
2266
- //#endregion
2267
- //#region src/helpers/project-generation/install-dependencies.ts
2268
- async function installDependencies({ projectDir, packageManager }) {
2269
- const s = spinner();
2270
- try {
2271
- s.start(`Running ${packageManager} install...`);
2272
- await $({
2273
- cwd: projectDir,
2274
- stderr: "inherit"
2275
- })`${packageManager} install`;
2276
- s.stop("Dependencies installed successfully");
2277
- } catch (error) {
2278
- s.stop(pc.red("Failed to install dependencies"));
2279
- if (error instanceof Error) consola.error(pc.red(`Installation error: ${error.message}`));
2280
- }
2281
- }
2282
-
2283
- //#endregion
2284
- //#region src/helpers/project-generation/template-manager.ts
2285
- async function processAndCopyFiles(sourcePattern, baseSourceDir, destDir, context, overwrite = true, ignorePatterns) {
2286
- const sourceFiles = await globby(sourcePattern, {
2287
- cwd: baseSourceDir,
2288
- dot: true,
2289
- onlyFiles: true,
2290
- absolute: false,
2291
- ignore: ignorePatterns
2292
- });
2293
- for (const relativeSrcPath of sourceFiles) {
2294
- const srcPath = path.join(baseSourceDir, relativeSrcPath);
2295
- let relativeDestPath = relativeSrcPath;
2296
- if (relativeSrcPath.endsWith(".hbs")) relativeDestPath = relativeSrcPath.slice(0, -4);
2297
- const basename = path.basename(relativeDestPath);
2298
- if (basename === "_gitignore") relativeDestPath = path.join(path.dirname(relativeDestPath), ".gitignore");
2299
- else if (basename === "_npmrc") relativeDestPath = path.join(path.dirname(relativeDestPath), ".npmrc");
2300
- const destPath = path.join(destDir, relativeDestPath);
2301
- await fs.ensureDir(path.dirname(destPath));
2302
- if (!overwrite && await fs.pathExists(destPath)) continue;
2303
- if (srcPath.endsWith(".hbs")) await processTemplate(srcPath, destPath, context);
2304
- else await fs.copy(srcPath, destPath, { overwrite: true });
2305
- }
2306
- }
2307
- async function copyBaseTemplate(projectDir, context) {
2308
- const templateDir = path.join(PKG_ROOT, "templates/base");
2309
- await processAndCopyFiles(["**/*"], templateDir, projectDir, context);
2310
- }
2311
- async function setupFrontendTemplates(projectDir, context) {
2074
+ async function setupAuthTemplate(projectDir, context) {
2075
+ if (context.backend === "convex" || !context.auth) return;
2076
+ const serverAppDir = path.join(projectDir, "apps/server");
2077
+ const webAppDir = path.join(projectDir, "apps/web");
2078
+ const nativeAppDir = path.join(projectDir, "apps/native");
2079
+ const serverAppDirExists = await fs.pathExists(serverAppDir);
2080
+ const webAppDirExists = await fs.pathExists(webAppDir);
2081
+ const nativeAppDirExists = await fs.pathExists(nativeAppDir);
2312
2082
  const hasReactWeb = context.frontend.some((f) => [
2313
2083
  "tanstack-router",
2314
2084
  "react-router",
@@ -2320,13 +2090,29 @@ async function setupFrontendTemplates(projectDir, context) {
2320
2090
  const hasSolidWeb = context.frontend.includes("solid");
2321
2091
  const hasNativeWind = context.frontend.includes("native-nativewind");
2322
2092
  const hasUnistyles = context.frontend.includes("native-unistyles");
2323
- const isConvex = context.backend === "convex";
2324
- if (hasReactWeb || hasNuxtWeb || hasSvelteWeb || hasSolidWeb) {
2325
- const webAppDir = path.join(projectDir, "apps/web");
2326
- await fs.ensureDir(webAppDir);
2093
+ const hasNative = hasNativeWind || hasUnistyles;
2094
+ if (serverAppDirExists) {
2095
+ const authServerBaseSrc = path.join(PKG_ROOT, "templates/auth/server/base");
2096
+ if (await fs.pathExists(authServerBaseSrc)) await processAndCopyFiles("**/*", authServerBaseSrc, serverAppDir, context);
2097
+ if (context.backend === "next") {
2098
+ const authServerNextSrc = path.join(PKG_ROOT, "templates/auth/server/next");
2099
+ if (await fs.pathExists(authServerNextSrc)) await processAndCopyFiles("**/*", authServerNextSrc, serverAppDir, context);
2100
+ }
2101
+ if (context.orm !== "none" && context.database !== "none") {
2102
+ const orm = context.orm;
2103
+ const db = context.database;
2104
+ let authDbSrc = "";
2105
+ if (orm === "drizzle") authDbSrc = path.join(PKG_ROOT, `templates/auth/server/db/drizzle/${db}`);
2106
+ else if (orm === "prisma") authDbSrc = path.join(PKG_ROOT, `templates/auth/server/db/prisma/${db}`);
2107
+ else if (orm === "mongoose") authDbSrc = path.join(PKG_ROOT, `templates/auth/server/db/mongoose/${db}`);
2108
+ if (authDbSrc && await fs.pathExists(authDbSrc)) await processAndCopyFiles("**/*", authDbSrc, serverAppDir, context);
2109
+ else if (authDbSrc) {}
2110
+ }
2111
+ }
2112
+ if ((hasReactWeb || hasNuxtWeb || hasSvelteWeb || hasSolidWeb) && webAppDirExists) {
2327
2113
  if (hasReactWeb) {
2328
- const webBaseDir = path.join(PKG_ROOT, "templates/frontend/react/web-base");
2329
- if (await fs.pathExists(webBaseDir)) await processAndCopyFiles("**/*", webBaseDir, webAppDir, context);
2114
+ const authWebBaseSrc = path.join(PKG_ROOT, "templates/auth/web/react/base");
2115
+ if (await fs.pathExists(authWebBaseSrc)) await processAndCopyFiles("**/*", authWebBaseSrc, webAppDir, context);
2330
2116
  const reactFramework = context.frontend.find((f) => [
2331
2117
  "tanstack-router",
2332
2118
  "react-router",
@@ -2334,154 +2120,29 @@ async function setupFrontendTemplates(projectDir, context) {
2334
2120
  "next"
2335
2121
  ].includes(f));
2336
2122
  if (reactFramework) {
2337
- const frameworkSrcDir = path.join(PKG_ROOT, `templates/frontend/react/${reactFramework}`);
2338
- if (await fs.pathExists(frameworkSrcDir)) await processAndCopyFiles("**/*", frameworkSrcDir, webAppDir, context);
2339
- if (!isConvex && context.api !== "none") {
2340
- const apiWebBaseDir = path.join(PKG_ROOT, `templates/api/${context.api}/web/react/base`);
2341
- if (await fs.pathExists(apiWebBaseDir)) await processAndCopyFiles("**/*", apiWebBaseDir, webAppDir, context);
2342
- }
2123
+ const authWebFrameworkSrc = path.join(PKG_ROOT, `templates/auth/web/react/${reactFramework}`);
2124
+ if (await fs.pathExists(authWebFrameworkSrc)) await processAndCopyFiles("**/*", authWebFrameworkSrc, webAppDir, context);
2343
2125
  }
2344
2126
  } else if (hasNuxtWeb) {
2345
- const nuxtBaseDir = path.join(PKG_ROOT, "templates/frontend/nuxt");
2346
- if (await fs.pathExists(nuxtBaseDir)) await processAndCopyFiles("**/*", nuxtBaseDir, webAppDir, context);
2347
- if (!isConvex && context.api === "orpc") {
2348
- const apiWebNuxtDir = path.join(PKG_ROOT, `templates/api/${context.api}/web/nuxt`);
2349
- if (await fs.pathExists(apiWebNuxtDir)) await processAndCopyFiles("**/*", apiWebNuxtDir, webAppDir, context);
2350
- }
2127
+ const authWebNuxtSrc = path.join(PKG_ROOT, "templates/auth/web/nuxt");
2128
+ if (await fs.pathExists(authWebNuxtSrc)) await processAndCopyFiles("**/*", authWebNuxtSrc, webAppDir, context);
2351
2129
  } else if (hasSvelteWeb) {
2352
- const svelteBaseDir = path.join(PKG_ROOT, "templates/frontend/svelte");
2353
- if (await fs.pathExists(svelteBaseDir)) await processAndCopyFiles("**/*", svelteBaseDir, webAppDir, context);
2354
- if (!isConvex && context.api === "orpc") {
2355
- const apiWebSvelteDir = path.join(PKG_ROOT, `templates/api/${context.api}/web/svelte`);
2356
- if (await fs.pathExists(apiWebSvelteDir)) await processAndCopyFiles("**/*", apiWebSvelteDir, webAppDir, context);
2357
- }
2130
+ const authWebSvelteSrc = path.join(PKG_ROOT, "templates/auth/web/svelte");
2131
+ if (await fs.pathExists(authWebSvelteSrc)) await processAndCopyFiles("**/*", authWebSvelteSrc, webAppDir, context);
2358
2132
  } else if (hasSolidWeb) {
2359
- const solidBaseDir = path.join(PKG_ROOT, "templates/frontend/solid");
2360
- if (await fs.pathExists(solidBaseDir)) await processAndCopyFiles("**/*", solidBaseDir, webAppDir, context);
2361
- if (!isConvex && context.api === "orpc") {
2362
- const apiWebSolidDir = path.join(PKG_ROOT, `templates/api/${context.api}/web/solid`);
2363
- if (await fs.pathExists(apiWebSolidDir)) await processAndCopyFiles("**/*", apiWebSolidDir, webAppDir, context);
2364
- }
2133
+ const authWebSolidSrc = path.join(PKG_ROOT, "templates/auth/web/solid");
2134
+ if (await fs.pathExists(authWebSolidSrc)) await processAndCopyFiles("**/*", authWebSolidSrc, webAppDir, context);
2365
2135
  }
2366
2136
  }
2367
- if (hasNativeWind || hasUnistyles) {
2368
- const nativeAppDir = path.join(projectDir, "apps/native");
2369
- await fs.ensureDir(nativeAppDir);
2370
- const nativeBaseCommonDir = path.join(PKG_ROOT, "templates/frontend/native/native-base");
2371
- if (await fs.pathExists(nativeBaseCommonDir)) await processAndCopyFiles("**/*", nativeBaseCommonDir, nativeAppDir, context);
2372
- let nativeFrameworkPath = "";
2373
- if (hasNativeWind) nativeFrameworkPath = "nativewind";
2374
- else if (hasUnistyles) nativeFrameworkPath = "unistyles";
2375
- const nativeSpecificDir = path.join(PKG_ROOT, `templates/frontend/native/${nativeFrameworkPath}`);
2376
- if (await fs.pathExists(nativeSpecificDir)) await processAndCopyFiles("**/*", nativeSpecificDir, nativeAppDir, context, true);
2377
- if (!isConvex && (context.api === "trpc" || context.api === "orpc")) {
2378
- const apiNativeSrcDir = path.join(PKG_ROOT, `templates/api/${context.api}/native`);
2379
- if (await fs.pathExists(apiNativeSrcDir)) await processAndCopyFiles("**/*", apiNativeSrcDir, nativeAppDir, context);
2380
- }
2381
- }
2382
- }
2383
- async function setupBackendFramework(projectDir, context) {
2384
- if (context.backend === "none") return;
2385
- const serverAppDir = path.join(projectDir, "apps/server");
2386
- if (context.backend === "convex") {
2387
- if (await fs.pathExists(serverAppDir)) await fs.remove(serverAppDir);
2388
- const convexBackendDestDir = path.join(projectDir, "packages/backend");
2389
- const convexSrcDir = path.join(PKG_ROOT, "templates/backend/convex/packages/backend");
2390
- await fs.ensureDir(convexBackendDestDir);
2391
- if (await fs.pathExists(convexSrcDir)) await processAndCopyFiles("**/*", convexSrcDir, convexBackendDestDir, context);
2392
- return;
2393
- }
2394
- await fs.ensureDir(serverAppDir);
2395
- const serverBaseDir = path.join(PKG_ROOT, "templates/backend/server/server-base");
2396
- if (await fs.pathExists(serverBaseDir)) await processAndCopyFiles("**/*", serverBaseDir, serverAppDir, context);
2397
- const frameworkSrcDir = path.join(PKG_ROOT, `templates/backend/server/${context.backend}`);
2398
- if (await fs.pathExists(frameworkSrcDir)) await processAndCopyFiles("**/*", frameworkSrcDir, serverAppDir, context, true);
2399
- if (context.api !== "none") {
2400
- const apiServerBaseDir = path.join(PKG_ROOT, `templates/api/${context.api}/server/base`);
2401
- if (await fs.pathExists(apiServerBaseDir)) await processAndCopyFiles("**/*", apiServerBaseDir, serverAppDir, context, true);
2402
- const apiServerFrameworkDir = path.join(PKG_ROOT, `templates/api/${context.api}/server/${context.backend}`);
2403
- if (await fs.pathExists(apiServerFrameworkDir)) await processAndCopyFiles("**/*", apiServerFrameworkDir, serverAppDir, context, true);
2404
- }
2405
- }
2406
- async function setupDbOrmTemplates(projectDir, context) {
2407
- if (context.backend === "convex" || context.orm === "none" || context.database === "none") return;
2408
- const serverAppDir = path.join(projectDir, "apps/server");
2409
- await fs.ensureDir(serverAppDir);
2410
- const dbOrmSrcDir = path.join(PKG_ROOT, `templates/db/${context.orm}/${context.database}`);
2411
- if (await fs.pathExists(dbOrmSrcDir)) await processAndCopyFiles("**/*", dbOrmSrcDir, serverAppDir, context);
2412
- }
2413
- async function setupAuthTemplate(projectDir, context) {
2414
- if (context.backend === "convex" || !context.auth) return;
2415
- const serverAppDir = path.join(projectDir, "apps/server");
2416
- const webAppDir = path.join(projectDir, "apps/web");
2417
- const nativeAppDir = path.join(projectDir, "apps/native");
2418
- const serverAppDirExists = await fs.pathExists(serverAppDir);
2419
- const webAppDirExists = await fs.pathExists(webAppDir);
2420
- const nativeAppDirExists = await fs.pathExists(nativeAppDir);
2421
- const hasReactWeb = context.frontend.some((f) => [
2422
- "tanstack-router",
2423
- "react-router",
2424
- "tanstack-start",
2425
- "next"
2426
- ].includes(f));
2427
- const hasNuxtWeb = context.frontend.includes("nuxt");
2428
- const hasSvelteWeb = context.frontend.includes("svelte");
2429
- const hasSolidWeb = context.frontend.includes("solid");
2430
- const hasNativeWind = context.frontend.includes("native-nativewind");
2431
- const hasUnistyles = context.frontend.includes("native-unistyles");
2432
- const hasNative = hasNativeWind || hasUnistyles;
2433
- if (serverAppDirExists) {
2434
- const authServerBaseSrc = path.join(PKG_ROOT, "templates/auth/server/base");
2435
- if (await fs.pathExists(authServerBaseSrc)) await processAndCopyFiles("**/*", authServerBaseSrc, serverAppDir, context);
2436
- if (context.backend === "next") {
2437
- const authServerNextSrc = path.join(PKG_ROOT, "templates/auth/server/next");
2438
- if (await fs.pathExists(authServerNextSrc)) await processAndCopyFiles("**/*", authServerNextSrc, serverAppDir, context);
2439
- }
2440
- if (context.orm !== "none" && context.database !== "none") {
2441
- const orm = context.orm;
2442
- const db = context.database;
2443
- let authDbSrc = "";
2444
- if (orm === "drizzle") authDbSrc = path.join(PKG_ROOT, `templates/auth/server/db/drizzle/${db}`);
2445
- else if (orm === "prisma") authDbSrc = path.join(PKG_ROOT, `templates/auth/server/db/prisma/${db}`);
2446
- else if (orm === "mongoose") authDbSrc = path.join(PKG_ROOT, `templates/auth/server/db/mongoose/${db}`);
2447
- if (authDbSrc && await fs.pathExists(authDbSrc)) await processAndCopyFiles("**/*", authDbSrc, serverAppDir, context);
2448
- else if (authDbSrc) {}
2449
- }
2450
- }
2451
- if ((hasReactWeb || hasNuxtWeb || hasSvelteWeb || hasSolidWeb) && webAppDirExists) {
2452
- if (hasReactWeb) {
2453
- const authWebBaseSrc = path.join(PKG_ROOT, "templates/auth/web/react/base");
2454
- if (await fs.pathExists(authWebBaseSrc)) await processAndCopyFiles("**/*", authWebBaseSrc, webAppDir, context);
2455
- const reactFramework = context.frontend.find((f) => [
2456
- "tanstack-router",
2457
- "react-router",
2458
- "tanstack-start",
2459
- "next"
2460
- ].includes(f));
2461
- if (reactFramework) {
2462
- const authWebFrameworkSrc = path.join(PKG_ROOT, `templates/auth/web/react/${reactFramework}`);
2463
- if (await fs.pathExists(authWebFrameworkSrc)) await processAndCopyFiles("**/*", authWebFrameworkSrc, webAppDir, context);
2464
- }
2465
- } else if (hasNuxtWeb) {
2466
- const authWebNuxtSrc = path.join(PKG_ROOT, "templates/auth/web/nuxt");
2467
- if (await fs.pathExists(authWebNuxtSrc)) await processAndCopyFiles("**/*", authWebNuxtSrc, webAppDir, context);
2468
- } else if (hasSvelteWeb) {
2469
- const authWebSvelteSrc = path.join(PKG_ROOT, "templates/auth/web/svelte");
2470
- if (await fs.pathExists(authWebSvelteSrc)) await processAndCopyFiles("**/*", authWebSvelteSrc, webAppDir, context);
2471
- } else if (hasSolidWeb) {
2472
- const authWebSolidSrc = path.join(PKG_ROOT, "templates/auth/web/solid");
2473
- if (await fs.pathExists(authWebSolidSrc)) await processAndCopyFiles("**/*", authWebSolidSrc, webAppDir, context);
2474
- }
2475
- }
2476
- if (hasNative && nativeAppDirExists) {
2477
- const authNativeBaseSrc = path.join(PKG_ROOT, "templates/auth/native/native-base");
2478
- if (await fs.pathExists(authNativeBaseSrc)) await processAndCopyFiles("**/*", authNativeBaseSrc, nativeAppDir, context);
2479
- let nativeFrameworkAuthPath = "";
2480
- if (hasNativeWind) nativeFrameworkAuthPath = "nativewind";
2481
- else if (hasUnistyles) nativeFrameworkAuthPath = "unistyles";
2482
- if (nativeFrameworkAuthPath) {
2483
- const authNativeFrameworkSrc = path.join(PKG_ROOT, `templates/auth/native/${nativeFrameworkAuthPath}`);
2484
- if (await fs.pathExists(authNativeFrameworkSrc)) await processAndCopyFiles("**/*", authNativeFrameworkSrc, nativeAppDir, context);
2137
+ if (hasNative && nativeAppDirExists) {
2138
+ const authNativeBaseSrc = path.join(PKG_ROOT, "templates/auth/native/native-base");
2139
+ if (await fs.pathExists(authNativeBaseSrc)) await processAndCopyFiles("**/*", authNativeBaseSrc, nativeAppDir, context);
2140
+ let nativeFrameworkAuthPath = "";
2141
+ if (hasNativeWind) nativeFrameworkAuthPath = "nativewind";
2142
+ else if (hasUnistyles) nativeFrameworkAuthPath = "unistyles";
2143
+ if (nativeFrameworkAuthPath) {
2144
+ const authNativeFrameworkSrc = path.join(PKG_ROOT, `templates/auth/native/${nativeFrameworkAuthPath}`);
2145
+ if (await fs.pathExists(authNativeFrameworkSrc)) await processAndCopyFiles("**/*", authNativeFrameworkSrc, nativeAppDir, context);
2485
2146
  }
2486
2147
  }
2487
2148
  }
@@ -2489,7 +2150,7 @@ async function setupAddonsTemplate(projectDir, context) {
2489
2150
  if (!context.addons || context.addons.length === 0) return;
2490
2151
  for (const addon of context.addons) {
2491
2152
  if (addon === "none") continue;
2492
- if (addon === "vibe-rules") continue;
2153
+ if (addon === "ruler") continue;
2493
2154
  let addonSrcDir = path.join(PKG_ROOT, `templates/addons/${addon}`);
2494
2155
  let addonDestDir = projectDir;
2495
2156
  if (addon === "pwa") {
@@ -2630,6 +2291,324 @@ async function setupDeploymentTemplates(projectDir, context) {
2630
2291
  }
2631
2292
  }
2632
2293
 
2294
+ //#endregion
2295
+ //#region src/helpers/setup/ruler-setup.ts
2296
+ async function setupVibeRules(config) {
2297
+ const { packageManager, projectDir } = config;
2298
+ try {
2299
+ log.info("Setting up Ruler...");
2300
+ const rulerDir = path.join(projectDir, ".ruler");
2301
+ const rulerTemplateDir = path.join(PKG_ROOT, "templates", "addons", "ruler", ".ruler");
2302
+ if (!await fs.pathExists(rulerDir)) if (await fs.pathExists(rulerTemplateDir)) await processAndCopyFiles("**/*", rulerTemplateDir, rulerDir, config);
2303
+ else {
2304
+ log.error(pc.red("Ruler template directory not found"));
2305
+ return;
2306
+ }
2307
+ const EDITORS$1 = {
2308
+ cursor: { label: "Cursor" },
2309
+ windsurf: { label: "Windsurf" },
2310
+ claude: { label: "Claude Code" },
2311
+ copilot: { label: "GitHub Copilot" },
2312
+ "gemini-cli": { label: "Gemini CLI" },
2313
+ codex: { label: "OpenAI Codex CLI" },
2314
+ jules: { label: "Jules" },
2315
+ cline: { label: "Cline" },
2316
+ aider: { label: "Aider" },
2317
+ firebase: { label: "Firebase Studio" },
2318
+ openhands: { label: "Open Hands" },
2319
+ junie: { label: "Junie" },
2320
+ augmentcode: { label: "AugmentCode" },
2321
+ kilocode: { label: "Kilo Code" },
2322
+ opencode: { label: "OpenCode" }
2323
+ };
2324
+ const selectedEditors = await multiselect({
2325
+ message: "Select AI assistants for Ruler",
2326
+ options: Object.entries(EDITORS$1).map(([key, v]) => ({
2327
+ value: key,
2328
+ label: v.label
2329
+ })),
2330
+ required: false
2331
+ });
2332
+ if (isCancel(selectedEditors)) return exitCancelled("Operation cancelled");
2333
+ if (selectedEditors.length === 0) {
2334
+ log.info("No AI assistants selected. To apply rules later, run:");
2335
+ log.info(pc.cyan(`${getPackageExecutionCommand(packageManager, "@intellectronica/ruler@latest apply --local-only")}`));
2336
+ return;
2337
+ }
2338
+ const configFile = path.join(rulerDir, "ruler.toml");
2339
+ const currentConfig = await fs.readFile(configFile, "utf-8");
2340
+ let updatedConfig = currentConfig;
2341
+ const defaultAgentsLine = `default_agents = [${selectedEditors.map((editor) => `"${editor}"`).join(", ")}]`;
2342
+ updatedConfig = updatedConfig.replace(/default_agents = \[\]/, defaultAgentsLine);
2343
+ await fs.writeFile(configFile, updatedConfig);
2344
+ await addRulerScriptToPackageJson(projectDir, packageManager);
2345
+ const s = spinner();
2346
+ s.start("Applying rules with Ruler...");
2347
+ try {
2348
+ const rulerApplyCmd = getPackageExecutionCommand(packageManager, `@intellectronica/ruler@latest apply --agents ${selectedEditors.join(",")} --local-only`);
2349
+ await execa(rulerApplyCmd, {
2350
+ cwd: projectDir,
2351
+ env: { CI: "true" },
2352
+ shell: true
2353
+ });
2354
+ s.stop("Applied rules with Ruler");
2355
+ } catch (_error) {
2356
+ s.stop(pc.red("Failed to apply rules"));
2357
+ }
2358
+ } catch (error) {
2359
+ log.error(pc.red("Failed to set up Ruler"));
2360
+ if (error instanceof Error) console.error(pc.red(error.message));
2361
+ }
2362
+ }
2363
+ async function addRulerScriptToPackageJson(projectDir, packageManager) {
2364
+ const rootPackageJsonPath = path.join(projectDir, "package.json");
2365
+ if (!await fs.pathExists(rootPackageJsonPath)) {
2366
+ log.warn("Root package.json not found, skipping ruler:apply script addition");
2367
+ return;
2368
+ }
2369
+ const packageJson = await fs.readJson(rootPackageJsonPath);
2370
+ if (!packageJson.scripts) packageJson.scripts = {};
2371
+ const rulerApplyCommand = getPackageExecutionCommand(packageManager, "@intellectronica/ruler@latest apply --local-only");
2372
+ packageJson.scripts["ruler:apply"] = rulerApplyCommand;
2373
+ await fs.writeJson(rootPackageJsonPath, packageJson, { spaces: 2 });
2374
+ }
2375
+
2376
+ //#endregion
2377
+ //#region src/utils/ts-morph.ts
2378
+ const tsProject = new Project({
2379
+ useInMemoryFileSystem: false,
2380
+ skipAddingFilesFromTsConfig: true,
2381
+ manipulationSettings: {
2382
+ quoteKind: QuoteKind.Single,
2383
+ indentationText: IndentationText.TwoSpaces
2384
+ }
2385
+ });
2386
+ function ensureArrayProperty(obj, name) {
2387
+ return obj.getProperty(name)?.getFirstDescendantByKind(SyntaxKind.ArrayLiteralExpression) ?? obj.addPropertyAssignment({
2388
+ name,
2389
+ initializer: "[]"
2390
+ }).getFirstDescendantByKindOrThrow(SyntaxKind.ArrayLiteralExpression);
2391
+ }
2392
+
2393
+ //#endregion
2394
+ //#region src/helpers/setup/vite-pwa-setup.ts
2395
+ async function addPwaToViteConfig(viteConfigPath, projectName) {
2396
+ const sourceFile = tsProject.addSourceFileAtPathIfExists(viteConfigPath);
2397
+ if (!sourceFile) throw new Error("vite config not found");
2398
+ const hasImport = sourceFile.getImportDeclarations().some((imp) => imp.getModuleSpecifierValue() === "vite-plugin-pwa");
2399
+ if (!hasImport) sourceFile.insertImportDeclaration(0, {
2400
+ namedImports: ["VitePWA"],
2401
+ moduleSpecifier: "vite-plugin-pwa"
2402
+ });
2403
+ const defineCall = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression).find((expr) => {
2404
+ const expression = expr.getExpression();
2405
+ return Node.isIdentifier(expression) && expression.getText() === "defineConfig";
2406
+ });
2407
+ if (!defineCall) throw new Error("Could not find defineConfig call in vite config");
2408
+ const callExpr = defineCall;
2409
+ const configObject = callExpr.getArguments()[0];
2410
+ if (!configObject) throw new Error("defineConfig argument is not an object literal");
2411
+ const pluginsArray = ensureArrayProperty(configObject, "plugins");
2412
+ const alreadyPresent = pluginsArray.getElements().some((el) => el.getText().startsWith("VitePWA("));
2413
+ if (!alreadyPresent) pluginsArray.addElement(`VitePWA({
2414
+ registerType: "autoUpdate",
2415
+ manifest: {
2416
+ name: "${projectName}",
2417
+ short_name: "${projectName}",
2418
+ description: "${projectName} - PWA Application",
2419
+ theme_color: "#0c0c0c",
2420
+ },
2421
+ pwaAssets: { disabled: false, config: true },
2422
+ devOptions: { enabled: true },
2423
+ })`);
2424
+ await tsProject.save();
2425
+ }
2426
+
2427
+ //#endregion
2428
+ //#region src/helpers/setup/addons-setup.ts
2429
+ async function setupAddons(config, isAddCommand = false) {
2430
+ const { addons, frontend, projectDir, packageManager } = config;
2431
+ const hasReactWebFrontend = frontend.includes("react-router") || frontend.includes("tanstack-router") || frontend.includes("next");
2432
+ const hasNuxtFrontend = frontend.includes("nuxt");
2433
+ const hasSvelteFrontend = frontend.includes("svelte");
2434
+ const hasSolidFrontend = frontend.includes("solid");
2435
+ const hasNextFrontend = frontend.includes("next");
2436
+ if (addons.includes("turborepo")) {
2437
+ await addPackageDependency({
2438
+ devDependencies: ["turbo"],
2439
+ projectDir
2440
+ });
2441
+ if (isAddCommand) log.info(`${pc.yellow("Update your package.json scripts:")}
2442
+
2443
+ ${pc.dim("Replace:")} ${pc.yellow("\"pnpm -r dev\"")} ${pc.dim("→")} ${pc.green("\"turbo dev\"")}
2444
+ ${pc.dim("Replace:")} ${pc.yellow("\"pnpm --filter web dev\"")} ${pc.dim("→")} ${pc.green("\"turbo -F web dev\"")}
2445
+
2446
+ ${pc.cyan("Docs:")} ${pc.underline("https://turborepo.com/docs")}
2447
+ `);
2448
+ }
2449
+ if (addons.includes("pwa") && (hasReactWebFrontend || hasSolidFrontend)) await setupPwa(projectDir, frontend);
2450
+ if (addons.includes("tauri") && (hasReactWebFrontend || hasNuxtFrontend || hasSvelteFrontend || hasSolidFrontend || hasNextFrontend)) await setupTauri(config);
2451
+ const hasUltracite = addons.includes("ultracite");
2452
+ const hasBiome = addons.includes("biome");
2453
+ const hasHusky = addons.includes("husky");
2454
+ const hasOxlint = addons.includes("oxlint");
2455
+ if (hasUltracite) await setupUltracite(config, hasHusky);
2456
+ else {
2457
+ if (hasBiome) await setupBiome(projectDir);
2458
+ if (hasHusky) {
2459
+ let linter;
2460
+ if (hasOxlint) linter = "oxlint";
2461
+ else if (hasBiome) linter = "biome";
2462
+ await setupHusky(projectDir, linter);
2463
+ }
2464
+ }
2465
+ if (addons.includes("oxlint")) await setupOxlint(projectDir, packageManager);
2466
+ if (addons.includes("starlight")) await setupStarlight(config);
2467
+ if (addons.includes("ruler")) await setupVibeRules(config);
2468
+ if (addons.includes("fumadocs")) await setupFumadocs(config);
2469
+ }
2470
+ function getWebAppDir(projectDir, frontends) {
2471
+ if (frontends.some((f) => [
2472
+ "react-router",
2473
+ "tanstack-router",
2474
+ "nuxt",
2475
+ "svelte",
2476
+ "solid"
2477
+ ].includes(f))) return path.join(projectDir, "apps/web");
2478
+ return path.join(projectDir, "apps/web");
2479
+ }
2480
+ async function setupBiome(projectDir) {
2481
+ await addPackageDependency({
2482
+ devDependencies: ["@biomejs/biome"],
2483
+ projectDir
2484
+ });
2485
+ const packageJsonPath = path.join(projectDir, "package.json");
2486
+ if (await fs.pathExists(packageJsonPath)) {
2487
+ const packageJson = await fs.readJson(packageJsonPath);
2488
+ packageJson.scripts = {
2489
+ ...packageJson.scripts,
2490
+ check: "biome check --write ."
2491
+ };
2492
+ await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
2493
+ }
2494
+ }
2495
+ async function setupHusky(projectDir, linter) {
2496
+ await addPackageDependency({
2497
+ devDependencies: ["husky", "lint-staged"],
2498
+ projectDir
2499
+ });
2500
+ const packageJsonPath = path.join(projectDir, "package.json");
2501
+ if (await fs.pathExists(packageJsonPath)) {
2502
+ const packageJson = await fs.readJson(packageJsonPath);
2503
+ packageJson.scripts = {
2504
+ ...packageJson.scripts,
2505
+ prepare: "husky"
2506
+ };
2507
+ if (linter === "oxlint") packageJson["lint-staged"] = { "**/*.{js,mjs,cjs,jsx,ts,mts,cts,tsx,vue,astro,svelte}": "oxlint" };
2508
+ else if (linter === "biome") packageJson["lint-staged"] = { "*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}": ["biome check --write ."] };
2509
+ else packageJson["lint-staged"] = { "**/*.{js,mjs,cjs,jsx,ts,mts,cts,tsx,vue,astro,svelte}": "" };
2510
+ await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
2511
+ }
2512
+ }
2513
+ async function setupPwa(projectDir, frontends) {
2514
+ const isCompatibleFrontend = frontends.some((f) => [
2515
+ "react-router",
2516
+ "tanstack-router",
2517
+ "solid"
2518
+ ].includes(f));
2519
+ if (!isCompatibleFrontend) return;
2520
+ const clientPackageDir = getWebAppDir(projectDir, frontends);
2521
+ if (!await fs.pathExists(clientPackageDir)) return;
2522
+ await addPackageDependency({
2523
+ dependencies: ["vite-plugin-pwa"],
2524
+ devDependencies: ["@vite-pwa/assets-generator"],
2525
+ projectDir: clientPackageDir
2526
+ });
2527
+ const clientPackageJsonPath = path.join(clientPackageDir, "package.json");
2528
+ if (await fs.pathExists(clientPackageJsonPath)) {
2529
+ const packageJson = await fs.readJson(clientPackageJsonPath);
2530
+ packageJson.scripts = {
2531
+ ...packageJson.scripts,
2532
+ "generate-pwa-assets": "pwa-assets-generator"
2533
+ };
2534
+ await fs.writeJson(clientPackageJsonPath, packageJson, { spaces: 2 });
2535
+ }
2536
+ const viteConfigTs = path.join(clientPackageDir, "vite.config.ts");
2537
+ if (await fs.pathExists(viteConfigTs)) await addPwaToViteConfig(viteConfigTs, path.basename(projectDir));
2538
+ }
2539
+ async function setupOxlint(projectDir, packageManager) {
2540
+ await addPackageDependency({
2541
+ devDependencies: ["oxlint"],
2542
+ projectDir
2543
+ });
2544
+ const packageJsonPath = path.join(projectDir, "package.json");
2545
+ if (await fs.pathExists(packageJsonPath)) {
2546
+ const packageJson = await fs.readJson(packageJsonPath);
2547
+ packageJson.scripts = {
2548
+ ...packageJson.scripts,
2549
+ check: "oxlint"
2550
+ };
2551
+ await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
2552
+ }
2553
+ const oxlintInitCommand = getPackageExecutionCommand(packageManager, "oxlint@latest --init");
2554
+ await execa(oxlintInitCommand, {
2555
+ cwd: projectDir,
2556
+ env: { CI: "true" },
2557
+ shell: true
2558
+ });
2559
+ }
2560
+
2561
+ //#endregion
2562
+ //#region src/helpers/project-generation/detect-project-config.ts
2563
+ async function detectProjectConfig(projectDir) {
2564
+ try {
2565
+ const btsConfig = await readBtsConfig(projectDir);
2566
+ if (btsConfig) return {
2567
+ projectDir,
2568
+ projectName: path.basename(projectDir),
2569
+ database: btsConfig.database,
2570
+ orm: btsConfig.orm,
2571
+ backend: btsConfig.backend,
2572
+ runtime: btsConfig.runtime,
2573
+ frontend: btsConfig.frontend,
2574
+ addons: btsConfig.addons,
2575
+ examples: btsConfig.examples,
2576
+ auth: btsConfig.auth,
2577
+ packageManager: btsConfig.packageManager,
2578
+ dbSetup: btsConfig.dbSetup,
2579
+ api: btsConfig.api,
2580
+ webDeploy: btsConfig.webDeploy
2581
+ };
2582
+ return null;
2583
+ } catch (_error) {
2584
+ return null;
2585
+ }
2586
+ }
2587
+ async function isBetterTStackProject(projectDir) {
2588
+ try {
2589
+ return await fs.pathExists(path.join(projectDir, "bts.jsonc"));
2590
+ } catch (_error) {
2591
+ return false;
2592
+ }
2593
+ }
2594
+
2595
+ //#endregion
2596
+ //#region src/helpers/project-generation/install-dependencies.ts
2597
+ async function installDependencies({ projectDir, packageManager }) {
2598
+ const s = spinner();
2599
+ try {
2600
+ s.start(`Running ${packageManager} install...`);
2601
+ await $({
2602
+ cwd: projectDir,
2603
+ stderr: "inherit"
2604
+ })`${packageManager} install`;
2605
+ s.stop("Dependencies installed successfully");
2606
+ } catch (error) {
2607
+ s.stop(pc.red("Failed to install dependencies"));
2608
+ if (error instanceof Error) consola.error(pc.red(`Installation error: ${error.message}`));
2609
+ }
2610
+ }
2611
+
2633
2612
  //#endregion
2634
2613
  //#region src/helpers/project-generation/add-addons.ts
2635
2614
  async function addAddonsToProject(input) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-better-t-stack",
3
- "version": "2.32.2",
3
+ "version": "2.33.1",
4
4
  "description": "A modern CLI tool for scaffolding end-to-end type-safe TypeScript projects with best practices and customizable configurations",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -106,7 +106,7 @@ add
106
106
  Available addons you can add:
107
107
  - **Documentation**: Starlight, Fumadocs
108
108
  - **Linting**: Biome, Oxlint, Ultracite
109
- - **Other**: vibe-rules, Turborepo, PWA, Tauri, Husky
109
+ - **Other**: Ruler, Turborepo, PWA, Tauri, Husky
110
110
 
111
111
  You can also add web deployment configurations like Cloudflare Workers support.
112
112
 
@@ -0,0 +1,18 @@
1
+ {
2
+ "mcpServers": {
3
+ "context7": {
4
+ "type": "stdio",
5
+ "command": "npx",
6
+ "args": ["-y", "@upstash/context7-mcp"]
7
+ }{{#if (or (eq runtime "workers") (eq webDeploy "workers"))}},
8
+ "cloudflare": {
9
+ "command": "npx",
10
+ "args": ["mcp-remote", "https://docs.mcp.cloudflare.com/sse"]
11
+ }{{/if}}{{#if (eq backend "convex")}},
12
+ "convex": {
13
+ "command": "npx",
14
+ "args": ["-y", "convex@latest", "mcp", "start"]
15
+ }
16
+ {{/if}}
17
+ }
18
+ }
@@ -0,0 +1,5 @@
1
+ # Ruler Configuration File
2
+ # See https://okigu.com/ruler for documentation.
3
+
4
+ # Default agents to run when --agents is not specified
5
+ default_agents = []