create-better-t-stack 2.32.1 → 2.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { createBtsCli } from "./src-DJffJtr1.js";
2
+ import { createBtsCli } from "./src-DdLzqWNh.js";
3
3
 
4
4
  //#region src/cli.ts
5
5
  createBtsCli().run();
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-DJffJtr1.js";
2
+ import { builder, createBtsCli, docs, init, router, sponsors } from "./src-DdLzqWNh.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
@@ -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
  }
@@ -2539,9 +2200,6 @@ async function setupExamplesTemplate(projectDir, context) {
2539
2200
  const exampleDbSchemaSrc = path.join(exampleServerSrc, context.orm, context.database);
2540
2201
  if (await fs.pathExists(exampleDbSchemaSrc)) await processAndCopyFiles("**/*", exampleDbSchemaSrc, serverAppDir, context, false);
2541
2202
  }
2542
- const ignorePatterns = [`${context.orm}/**`];
2543
- if (example === "ai" && context.backend === "next") ignorePatterns.push("next/**");
2544
- await processAndCopyFiles(["**/*.ts", "**/*.hbs"], exampleServerSrc, serverAppDir, context, false, ignorePatterns);
2545
2203
  }
2546
2204
  if (webAppDirExists) {
2547
2205
  if (hasReactWeb) {
@@ -2633,6 +2291,324 @@ async function setupDeploymentTemplates(projectDir, context) {
2633
2291
  }
2634
2292
  }
2635
2293
 
2294
+ //#endregion
2295
+ //#region src/helpers/setup/vibe-rules-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", "vibe-rules", ".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("vibe-rules")) 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
+
2636
2612
  //#endregion
2637
2613
  //#region src/helpers/project-generation/add-addons.ts
2638
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.1",
3
+ "version": "2.33.0",
4
4
  "description": "A modern CLI tool for scaffolding end-to-end type-safe TypeScript projects with best practices and customizable configurations",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -0,0 +1,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 = []