create-better-t-stack 2.41.3 → 2.41.4

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.
@@ -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 { IndentationText, Node, Project, QuoteKind, SyntaxKind } from "ts-morph";
13
14
  import { glob } from "tinyglobby";
14
15
  import handlebars from "handlebars";
15
- import { IndentationText, Node, Project, QuoteKind, SyntaxKind } from "ts-morph";
16
16
  import os from "node:os";
17
17
 
18
18
  //#region src/utils/get-package-manager.ts
@@ -2038,927 +2038,925 @@ async function setupFumadocs(config) {
2038
2038
  }
2039
2039
 
2040
2040
  //#endregion
2041
- //#region src/utils/template-processor.ts
2042
- /**
2043
- * Processes a Handlebars template file and writes the output to the destination.
2044
- * @param srcPath Path to the source .hbs template file.
2045
- * @param destPath Path to write the processed file.
2046
- * @param context Data to be passed to the Handlebars template.
2047
- */
2048
- async function processTemplate(srcPath, destPath, context) {
2041
+ //#region src/helpers/addons/ruler-setup.ts
2042
+ async function setupRuler(config) {
2043
+ const { packageManager, projectDir } = config;
2049
2044
  try {
2050
- const templateContent = await fs.readFile(srcPath, "utf-8");
2051
- const template = handlebars.compile(templateContent);
2052
- const processedContent = template(context);
2053
- await fs.ensureDir(path.dirname(destPath));
2054
- await fs.writeFile(destPath, processedContent);
2045
+ log.info("Setting up Ruler...");
2046
+ const rulerDir = path.join(projectDir, ".ruler");
2047
+ if (!await fs.pathExists(rulerDir)) {
2048
+ log.error(pc.red("Ruler template directory not found. Please ensure ruler addon is properly installed."));
2049
+ return;
2050
+ }
2051
+ const EDITORS$1 = {
2052
+ amp: { label: "AMP" },
2053
+ copilot: { label: "GitHub Copilot" },
2054
+ claude: { label: "Claude Code" },
2055
+ codex: { label: "OpenAI Codex CLI" },
2056
+ cursor: { label: "Cursor" },
2057
+ windsurf: { label: "Windsurf" },
2058
+ cline: { label: "Cline" },
2059
+ aider: { label: "Aider" },
2060
+ firebase: { label: "Firebase Studio" },
2061
+ "gemini-cli": { label: "Gemini CLI" },
2062
+ junie: { label: "Junie" },
2063
+ kilocode: { label: "Kilo Code" },
2064
+ opencode: { label: "OpenCode" },
2065
+ crush: { label: "Crush" },
2066
+ zed: { label: "Zed" },
2067
+ qwen: { label: "Qwen" }
2068
+ };
2069
+ const selectedEditors = await autocompleteMultiselect({
2070
+ message: "Select AI assistants for Ruler",
2071
+ options: Object.entries(EDITORS$1).map(([key, v]) => ({
2072
+ value: key,
2073
+ label: v.label
2074
+ })),
2075
+ required: false
2076
+ });
2077
+ if (isCancel(selectedEditors)) return exitCancelled("Operation cancelled");
2078
+ if (selectedEditors.length === 0) {
2079
+ log.info("No AI assistants selected. To apply rules later, run:");
2080
+ log.info(pc.cyan(`${getPackageExecutionCommand(packageManager, "@intellectronica/ruler@latest apply --local-only")}`));
2081
+ return;
2082
+ }
2083
+ const configFile = path.join(rulerDir, "ruler.toml");
2084
+ const currentConfig = await fs.readFile(configFile, "utf-8");
2085
+ let updatedConfig = currentConfig;
2086
+ const defaultAgentsLine = `default_agents = [${selectedEditors.map((editor) => `"${editor}"`).join(", ")}]`;
2087
+ updatedConfig = updatedConfig.replace(/default_agents = \[\]/, defaultAgentsLine);
2088
+ await fs.writeFile(configFile, updatedConfig);
2089
+ await addRulerScriptToPackageJson(projectDir, packageManager);
2090
+ const s = spinner();
2091
+ s.start("Applying rules with Ruler...");
2092
+ try {
2093
+ const rulerApplyCmd = getPackageExecutionCommand(packageManager, `@intellectronica/ruler@latest apply --agents ${selectedEditors.join(",")} --local-only`);
2094
+ await execa(rulerApplyCmd, {
2095
+ cwd: projectDir,
2096
+ env: { CI: "true" },
2097
+ shell: true
2098
+ });
2099
+ s.stop("Applied rules with Ruler");
2100
+ } catch (_error) {
2101
+ s.stop(pc.red("Failed to apply rules"));
2102
+ }
2055
2103
  } catch (error) {
2056
- consola.error(`Error processing template ${srcPath}:`, error);
2057
- throw new Error(`Failed to process template ${srcPath}`);
2104
+ log.error(pc.red("Failed to set up Ruler"));
2105
+ if (error instanceof Error) console.error(pc.red(error.message));
2058
2106
  }
2059
2107
  }
2060
- handlebars.registerHelper("eq", (a, b) => a === b);
2061
- handlebars.registerHelper("ne", (a, b) => a !== b);
2062
- handlebars.registerHelper("and", (a, b) => a && b);
2063
- handlebars.registerHelper("or", (a, b) => a || b);
2064
- handlebars.registerHelper("includes", (array, value) => Array.isArray(array) && array.includes(value));
2108
+ async function addRulerScriptToPackageJson(projectDir, packageManager) {
2109
+ const rootPackageJsonPath = path.join(projectDir, "package.json");
2110
+ if (!await fs.pathExists(rootPackageJsonPath)) {
2111
+ log.warn("Root package.json not found, skipping ruler:apply script addition");
2112
+ return;
2113
+ }
2114
+ const packageJson = await fs.readJson(rootPackageJsonPath);
2115
+ if (!packageJson.scripts) packageJson.scripts = {};
2116
+ const rulerApplyCommand = getPackageExecutionCommand(packageManager, "@intellectronica/ruler@latest apply --local-only");
2117
+ packageJson.scripts["ruler:apply"] = rulerApplyCommand;
2118
+ await fs.writeJson(rootPackageJsonPath, packageJson, { spaces: 2 });
2119
+ }
2065
2120
 
2066
2121
  //#endregion
2067
- //#region src/helpers/core/template-manager.ts
2068
- async function processAndCopyFiles(sourcePattern, baseSourceDir, destDir, context, overwrite = true, ignorePatterns) {
2069
- const sourceFiles = await glob(sourcePattern, {
2070
- cwd: baseSourceDir,
2071
- dot: true,
2072
- onlyFiles: true,
2073
- absolute: false,
2074
- ignore: ignorePatterns
2075
- });
2076
- for (const relativeSrcPath of sourceFiles) {
2077
- const srcPath = path.join(baseSourceDir, relativeSrcPath);
2078
- let relativeDestPath = relativeSrcPath;
2079
- if (relativeSrcPath.endsWith(".hbs")) relativeDestPath = relativeSrcPath.slice(0, -4);
2080
- const basename = path.basename(relativeDestPath);
2081
- if (basename === "_gitignore") relativeDestPath = path.join(path.dirname(relativeDestPath), ".gitignore");
2082
- else if (basename === "_npmrc") relativeDestPath = path.join(path.dirname(relativeDestPath), ".npmrc");
2083
- const destPath = path.join(destDir, relativeDestPath);
2084
- await fs.ensureDir(path.dirname(destPath));
2085
- if (!overwrite && await fs.pathExists(destPath)) continue;
2086
- if (srcPath.endsWith(".hbs")) await processTemplate(srcPath, destPath, context);
2087
- else await fs.copy(srcPath, destPath, { overwrite: true });
2122
+ //#region src/helpers/addons/starlight-setup.ts
2123
+ async function setupStarlight(config) {
2124
+ const { packageManager, projectDir } = config;
2125
+ const s = spinner();
2126
+ try {
2127
+ s.start("Setting up Starlight docs...");
2128
+ const starlightArgs = [
2129
+ "docs",
2130
+ "--template",
2131
+ "starlight",
2132
+ "--no-install",
2133
+ "--add",
2134
+ "tailwind",
2135
+ "--no-git",
2136
+ "--skip-houston"
2137
+ ];
2138
+ const starlightArgsString = starlightArgs.join(" ");
2139
+ const commandWithArgs = `create-astro@latest ${starlightArgsString}`;
2140
+ const starlightInitCommand = getPackageExecutionCommand(packageManager, commandWithArgs);
2141
+ await execa(starlightInitCommand, {
2142
+ cwd: path.join(projectDir, "apps"),
2143
+ env: { CI: "true" },
2144
+ shell: true
2145
+ });
2146
+ s.stop("Starlight docs setup successfully!");
2147
+ } catch (error) {
2148
+ s.stop(pc.red("Failed to set up Starlight docs"));
2149
+ if (error instanceof Error) consola.error(pc.red(error.message));
2088
2150
  }
2089
2151
  }
2090
- async function copyBaseTemplate(projectDir, context) {
2091
- const templateDir = path.join(PKG_ROOT, "templates/base");
2092
- await processAndCopyFiles(["**/*"], templateDir, projectDir, context);
2093
- }
2094
- async function setupFrontendTemplates(projectDir, context) {
2095
- const hasReactWeb = context.frontend.some((f) => [
2096
- "tanstack-router",
2097
- "react-router",
2098
- "tanstack-start",
2099
- "next"
2100
- ].includes(f));
2101
- const hasNuxtWeb = context.frontend.includes("nuxt");
2102
- const hasSvelteWeb = context.frontend.includes("svelte");
2103
- const hasSolidWeb = context.frontend.includes("solid");
2104
- const hasNativeWind = context.frontend.includes("native-nativewind");
2105
- const hasUnistyles = context.frontend.includes("native-unistyles");
2106
- const isConvex = context.backend === "convex";
2107
- if (hasReactWeb || hasNuxtWeb || hasSvelteWeb || hasSolidWeb) {
2108
- const webAppDir = path.join(projectDir, "apps/web");
2109
- await fs.ensureDir(webAppDir);
2110
- if (hasReactWeb) {
2111
- const webBaseDir = path.join(PKG_ROOT, "templates/frontend/react/web-base");
2112
- if (await fs.pathExists(webBaseDir)) await processAndCopyFiles("**/*", webBaseDir, webAppDir, context);
2113
- const reactFramework = context.frontend.find((f) => [
2114
- "tanstack-router",
2115
- "react-router",
2116
- "tanstack-start",
2117
- "next"
2118
- ].includes(f));
2119
- if (reactFramework) {
2120
- const frameworkSrcDir = path.join(PKG_ROOT, `templates/frontend/react/${reactFramework}`);
2121
- if (await fs.pathExists(frameworkSrcDir)) await processAndCopyFiles("**/*", frameworkSrcDir, webAppDir, context);
2122
- if (!isConvex && context.api !== "none") {
2123
- const apiWebBaseDir = path.join(PKG_ROOT, `templates/api/${context.api}/web/react/base`);
2124
- if (await fs.pathExists(apiWebBaseDir)) await processAndCopyFiles("**/*", apiWebBaseDir, webAppDir, context);
2125
- }
2126
- }
2127
- } else if (hasNuxtWeb) {
2128
- const nuxtBaseDir = path.join(PKG_ROOT, "templates/frontend/nuxt");
2129
- if (await fs.pathExists(nuxtBaseDir)) await processAndCopyFiles("**/*", nuxtBaseDir, webAppDir, context);
2130
- if (!isConvex && context.api === "orpc") {
2131
- const apiWebNuxtDir = path.join(PKG_ROOT, `templates/api/${context.api}/web/nuxt`);
2132
- if (await fs.pathExists(apiWebNuxtDir)) await processAndCopyFiles("**/*", apiWebNuxtDir, webAppDir, context);
2133
- }
2134
- } else if (hasSvelteWeb) {
2135
- const svelteBaseDir = path.join(PKG_ROOT, "templates/frontend/svelte");
2136
- if (await fs.pathExists(svelteBaseDir)) await processAndCopyFiles("**/*", svelteBaseDir, webAppDir, context);
2137
- if (!isConvex && context.api === "orpc") {
2138
- const apiWebSvelteDir = path.join(PKG_ROOT, `templates/api/${context.api}/web/svelte`);
2139
- if (await fs.pathExists(apiWebSvelteDir)) await processAndCopyFiles("**/*", apiWebSvelteDir, webAppDir, context);
2140
- }
2141
- } else if (hasSolidWeb) {
2142
- const solidBaseDir = path.join(PKG_ROOT, "templates/frontend/solid");
2143
- if (await fs.pathExists(solidBaseDir)) await processAndCopyFiles("**/*", solidBaseDir, webAppDir, context);
2144
- if (!isConvex && context.api === "orpc") {
2145
- const apiWebSolidDir = path.join(PKG_ROOT, `templates/api/${context.api}/web/solid`);
2146
- if (await fs.pathExists(apiWebSolidDir)) await processAndCopyFiles("**/*", apiWebSolidDir, webAppDir, context);
2147
- }
2148
- }
2149
- }
2150
- if (hasNativeWind || hasUnistyles) {
2151
- const nativeAppDir = path.join(projectDir, "apps/native");
2152
- await fs.ensureDir(nativeAppDir);
2153
- const nativeBaseCommonDir = path.join(PKG_ROOT, "templates/frontend/native/native-base");
2154
- if (await fs.pathExists(nativeBaseCommonDir)) await processAndCopyFiles("**/*", nativeBaseCommonDir, nativeAppDir, context);
2155
- let nativeFrameworkPath = "";
2156
- if (hasNativeWind) nativeFrameworkPath = "nativewind";
2157
- else if (hasUnistyles) nativeFrameworkPath = "unistyles";
2158
- const nativeSpecificDir = path.join(PKG_ROOT, `templates/frontend/native/${nativeFrameworkPath}`);
2159
- if (await fs.pathExists(nativeSpecificDir)) await processAndCopyFiles("**/*", nativeSpecificDir, nativeAppDir, context, true);
2160
- if (!isConvex && (context.api === "trpc" || context.api === "orpc")) {
2161
- const apiNativeSrcDir = path.join(PKG_ROOT, `templates/api/${context.api}/native`);
2162
- if (await fs.pathExists(apiNativeSrcDir)) await processAndCopyFiles("**/*", apiNativeSrcDir, nativeAppDir, context);
2152
+
2153
+ //#endregion
2154
+ //#region src/helpers/addons/tauri-setup.ts
2155
+ async function setupTauri(config) {
2156
+ const { packageManager, frontend, projectDir } = config;
2157
+ const s = spinner();
2158
+ const clientPackageDir = path.join(projectDir, "apps/web");
2159
+ if (!await fs.pathExists(clientPackageDir)) return;
2160
+ try {
2161
+ s.start("Setting up Tauri desktop app support...");
2162
+ await addPackageDependency({
2163
+ devDependencies: ["@tauri-apps/cli"],
2164
+ projectDir: clientPackageDir
2165
+ });
2166
+ const clientPackageJsonPath = path.join(clientPackageDir, "package.json");
2167
+ if (await fs.pathExists(clientPackageJsonPath)) {
2168
+ const packageJson = await fs.readJson(clientPackageJsonPath);
2169
+ packageJson.scripts = {
2170
+ ...packageJson.scripts,
2171
+ tauri: "tauri",
2172
+ "desktop:dev": "tauri dev",
2173
+ "desktop:build": "tauri build"
2174
+ };
2175
+ await fs.writeJson(clientPackageJsonPath, packageJson, { spaces: 2 });
2163
2176
  }
2177
+ frontend.includes("tanstack-router");
2178
+ const hasReactRouter = frontend.includes("react-router");
2179
+ const hasNuxt = frontend.includes("nuxt");
2180
+ const hasSvelte = frontend.includes("svelte");
2181
+ frontend.includes("solid");
2182
+ const hasNext = frontend.includes("next");
2183
+ const devUrl = hasReactRouter || hasSvelte ? "http://localhost:5173" : hasNext ? "http://localhost:3001" : "http://localhost:3001";
2184
+ const frontendDist = hasNuxt ? "../.output/public" : hasSvelte ? "../build" : hasNext ? "../.next" : hasReactRouter ? "../build/client" : "../dist";
2185
+ const tauriArgs = [
2186
+ "init",
2187
+ `--app-name=${path.basename(projectDir)}`,
2188
+ `--window-title=${path.basename(projectDir)}`,
2189
+ `--frontend-dist=${frontendDist}`,
2190
+ `--dev-url=${devUrl}`,
2191
+ `--before-dev-command="${packageManager} run dev"`,
2192
+ `--before-build-command="${packageManager} run build"`
2193
+ ];
2194
+ const tauriArgsString = tauriArgs.join(" ");
2195
+ const commandWithArgs = `@tauri-apps/cli@latest ${tauriArgsString}`;
2196
+ const tauriInitCommand = getPackageExecutionCommand(packageManager, commandWithArgs);
2197
+ await execa(tauriInitCommand, {
2198
+ cwd: clientPackageDir,
2199
+ env: { CI: "true" },
2200
+ shell: true
2201
+ });
2202
+ s.stop("Tauri desktop app support configured successfully!");
2203
+ } catch (error) {
2204
+ s.stop(pc.red("Failed to set up Tauri"));
2205
+ if (error instanceof Error) consola$1.error(pc.red(error.message));
2164
2206
  }
2165
2207
  }
2166
- async function setupBackendFramework(projectDir, context) {
2167
- if (context.backend === "none") return;
2168
- const serverAppDir = path.join(projectDir, "apps/server");
2169
- if (context.backend === "convex") {
2170
- if (await fs.pathExists(serverAppDir)) await fs.remove(serverAppDir);
2171
- const convexBackendDestDir = path.join(projectDir, "packages/backend");
2172
- const convexSrcDir = path.join(PKG_ROOT, "templates/backend/convex/packages/backend");
2173
- await fs.ensureDir(convexBackendDestDir);
2174
- if (await fs.pathExists(convexSrcDir)) await processAndCopyFiles("**/*", convexSrcDir, convexBackendDestDir, context);
2175
- return;
2176
- }
2177
- await fs.ensureDir(serverAppDir);
2178
- const serverBaseDir = path.join(PKG_ROOT, "templates/backend/server/server-base");
2179
- if (await fs.pathExists(serverBaseDir)) await processAndCopyFiles("**/*", serverBaseDir, serverAppDir, context);
2180
- const frameworkSrcDir = path.join(PKG_ROOT, `templates/backend/server/${context.backend}`);
2181
- if (await fs.pathExists(frameworkSrcDir)) await processAndCopyFiles("**/*", frameworkSrcDir, serverAppDir, context, true);
2182
- if (context.api !== "none") {
2183
- const apiServerBaseDir = path.join(PKG_ROOT, `templates/api/${context.api}/server/base`);
2184
- if (await fs.pathExists(apiServerBaseDir)) await processAndCopyFiles("**/*", apiServerBaseDir, serverAppDir, context, true);
2185
- const apiServerFrameworkDir = path.join(PKG_ROOT, `templates/api/${context.api}/server/${context.backend}`);
2186
- if (await fs.pathExists(apiServerFrameworkDir)) await processAndCopyFiles("**/*", apiServerFrameworkDir, serverAppDir, context, true);
2187
- }
2188
- }
2189
- async function setupDbOrmTemplates(projectDir, context) {
2190
- if (context.backend === "convex" || context.orm === "none" || context.database === "none") return;
2191
- const serverAppDir = path.join(projectDir, "apps/server");
2192
- await fs.ensureDir(serverAppDir);
2193
- const dbOrmSrcDir = path.join(PKG_ROOT, `templates/db/${context.orm}/${context.database}`);
2194
- if (await fs.pathExists(dbOrmSrcDir)) await processAndCopyFiles("**/*", dbOrmSrcDir, serverAppDir, context);
2195
- }
2196
- async function setupAuthTemplate(projectDir, context) {
2197
- if (!context.auth || context.auth === "none") return;
2198
- const serverAppDir = path.join(projectDir, "apps/server");
2199
- const webAppDir = path.join(projectDir, "apps/web");
2200
- const nativeAppDir = path.join(projectDir, "apps/native");
2201
- const serverAppDirExists = await fs.pathExists(serverAppDir);
2202
- const webAppDirExists = await fs.pathExists(webAppDir);
2203
- const nativeAppDirExists = await fs.pathExists(nativeAppDir);
2204
- const hasReactWeb = context.frontend.some((f) => [
2205
- "tanstack-router",
2206
- "react-router",
2207
- "tanstack-start",
2208
- "next"
2209
- ].includes(f));
2210
- const hasNuxtWeb = context.frontend.includes("nuxt");
2211
- const hasSvelteWeb = context.frontend.includes("svelte");
2212
- const hasSolidWeb = context.frontend.includes("solid");
2213
- const hasNativeWind = context.frontend.includes("native-nativewind");
2214
- const hasUnistyles = context.frontend.includes("native-unistyles");
2215
- const hasNative = hasNativeWind || hasUnistyles;
2216
- const authProvider = context.auth;
2217
- if (context.backend === "convex" && authProvider === "clerk") {
2218
- const convexBackendDestDir = path.join(projectDir, "packages/backend");
2219
- const convexClerkBackendSrc = path.join(PKG_ROOT, "templates/auth/clerk/convex/backend");
2220
- if (await fs.pathExists(convexClerkBackendSrc)) {
2221
- await fs.ensureDir(convexBackendDestDir);
2222
- await processAndCopyFiles("**/*", convexClerkBackendSrc, convexBackendDestDir, context);
2223
- }
2224
- if (webAppDirExists) {
2225
- const reactFramework = context.frontend.find((f) => [
2226
- "tanstack-router",
2227
- "react-router",
2228
- "tanstack-start",
2229
- "next"
2230
- ].includes(f));
2231
- if (reactFramework) {
2232
- const convexClerkWebSrc = path.join(PKG_ROOT, `templates/auth/clerk/convex/web/react/${reactFramework}`);
2233
- if (await fs.pathExists(convexClerkWebSrc)) await processAndCopyFiles("**/*", convexClerkWebSrc, webAppDir, context);
2234
- }
2235
- }
2236
- if (nativeAppDirExists) {
2237
- const convexClerkNativeBaseSrc = path.join(PKG_ROOT, "templates/auth/clerk/convex/native/base");
2238
- if (await fs.pathExists(convexClerkNativeBaseSrc)) await processAndCopyFiles("**/*", convexClerkNativeBaseSrc, nativeAppDir, context);
2239
- const hasNativeWind$1 = context.frontend.includes("native-nativewind");
2240
- const hasUnistyles$1 = context.frontend.includes("native-unistyles");
2241
- let nativeFrameworkPath = "";
2242
- if (hasNativeWind$1) nativeFrameworkPath = "nativewind";
2243
- else if (hasUnistyles$1) nativeFrameworkPath = "unistyles";
2244
- if (nativeFrameworkPath) {
2245
- const convexClerkNativeFrameworkSrc = path.join(PKG_ROOT, `templates/auth/clerk/convex/native/${nativeFrameworkPath}`);
2246
- if (await fs.pathExists(convexClerkNativeFrameworkSrc)) await processAndCopyFiles("**/*", convexClerkNativeFrameworkSrc, nativeAppDir, context);
2247
- }
2248
- }
2249
- return;
2250
- }
2251
- if (serverAppDirExists && context.backend !== "convex") {
2252
- const authServerBaseSrc = path.join(PKG_ROOT, `templates/auth/${authProvider}/server/base`);
2253
- if (await fs.pathExists(authServerBaseSrc)) await processAndCopyFiles("**/*", authServerBaseSrc, serverAppDir, context);
2254
- if (context.backend === "next") {
2255
- const authServerNextSrc = path.join(PKG_ROOT, `templates/auth/${authProvider}/server/next`);
2256
- if (await fs.pathExists(authServerNextSrc)) await processAndCopyFiles("**/*", authServerNextSrc, serverAppDir, context);
2257
- }
2258
- if (context.orm !== "none" && context.database !== "none") {
2259
- const orm = context.orm;
2260
- const db = context.database;
2261
- let authDbSrc = "";
2262
- if (orm === "drizzle") authDbSrc = path.join(PKG_ROOT, `templates/auth/${authProvider}/server/db/drizzle/${db}`);
2263
- else if (orm === "prisma") authDbSrc = path.join(PKG_ROOT, `templates/auth/${authProvider}/server/db/prisma/${db}`);
2264
- else if (orm === "mongoose") authDbSrc = path.join(PKG_ROOT, `templates/auth/${authProvider}/server/db/mongoose/${db}`);
2265
- if (authDbSrc && await fs.pathExists(authDbSrc)) await processAndCopyFiles("**/*", authDbSrc, serverAppDir, context);
2266
- }
2267
- }
2268
- if ((hasReactWeb || hasNuxtWeb || hasSvelteWeb || hasSolidWeb) && webAppDirExists) {
2269
- if (hasReactWeb) {
2270
- const authWebBaseSrc = path.join(PKG_ROOT, `templates/auth/${authProvider}/web/react/base`);
2271
- if (await fs.pathExists(authWebBaseSrc)) await processAndCopyFiles("**/*", authWebBaseSrc, webAppDir, context);
2272
- const reactFramework = context.frontend.find((f) => [
2273
- "tanstack-router",
2274
- "react-router",
2275
- "tanstack-start",
2276
- "next"
2277
- ].includes(f));
2278
- if (reactFramework) {
2279
- const authWebFrameworkSrc = path.join(PKG_ROOT, `templates/auth/${authProvider}/web/react/${reactFramework}`);
2280
- if (await fs.pathExists(authWebFrameworkSrc)) await processAndCopyFiles("**/*", authWebFrameworkSrc, webAppDir, context);
2281
- }
2282
- } else if (hasNuxtWeb) {
2283
- const authWebNuxtSrc = path.join(PKG_ROOT, `templates/auth/${authProvider}/web/nuxt`);
2284
- if (await fs.pathExists(authWebNuxtSrc)) await processAndCopyFiles("**/*", authWebNuxtSrc, webAppDir, context);
2285
- } else if (hasSvelteWeb) {
2286
- const authWebSvelteSrc = path.join(PKG_ROOT, `templates/auth/${authProvider}/web/svelte`);
2287
- if (await fs.pathExists(authWebSvelteSrc)) await processAndCopyFiles("**/*", authWebSvelteSrc, webAppDir, context);
2288
- } else if (hasSolidWeb) {
2289
- const authWebSolidSrc = path.join(PKG_ROOT, `templates/auth/${authProvider}/web/solid`);
2290
- if (await fs.pathExists(authWebSolidSrc)) await processAndCopyFiles("**/*", authWebSolidSrc, webAppDir, context);
2291
- }
2292
- }
2293
- if (hasNative && nativeAppDirExists) {
2294
- const authNativeBaseSrc = path.join(PKG_ROOT, `templates/auth/${authProvider}/native/native-base`);
2295
- if (await fs.pathExists(authNativeBaseSrc)) await processAndCopyFiles("**/*", authNativeBaseSrc, nativeAppDir, context);
2296
- let nativeFrameworkAuthPath = "";
2297
- if (hasNativeWind) nativeFrameworkAuthPath = "nativewind";
2298
- else if (hasUnistyles) nativeFrameworkAuthPath = "unistyles";
2299
- if (nativeFrameworkAuthPath) {
2300
- const authNativeFrameworkSrc = path.join(PKG_ROOT, `templates/auth/${authProvider}/native/${nativeFrameworkAuthPath}`);
2301
- if (await fs.pathExists(authNativeFrameworkSrc)) await processAndCopyFiles("**/*", authNativeFrameworkSrc, nativeAppDir, context);
2302
- }
2208
+
2209
+ //#endregion
2210
+ //#region src/helpers/addons/ultracite-setup.ts
2211
+ const EDITORS = {
2212
+ vscode: { label: "VSCode / Cursor / Windsurf" },
2213
+ zed: { label: "Zed" }
2214
+ };
2215
+ const RULES = {
2216
+ "vscode-copilot": { label: "VS Code Copilot" },
2217
+ cursor: { label: "Cursor" },
2218
+ windsurf: { label: "Windsurf" },
2219
+ zed: { label: "Zed" },
2220
+ claude: { label: "Claude" },
2221
+ codex: { label: "Codex" },
2222
+ kiro: { label: "Kiro" },
2223
+ cline: { label: "Cline" },
2224
+ amp: { label: "Amp" },
2225
+ aider: { label: "Aider" },
2226
+ "firebase-studio": { label: "Firebase Studio" },
2227
+ "open-hands": { label: "Open Hands" },
2228
+ "gemini-cli": { label: "Gemini CLI" },
2229
+ junie: { label: "Junie" },
2230
+ augmentcode: { label: "AugmentCode" },
2231
+ "kilo-code": { label: "Kilo Code" },
2232
+ goose: { label: "Goose" }
2233
+ };
2234
+ async function setupUltracite(config, hasHusky) {
2235
+ const { packageManager, projectDir } = config;
2236
+ try {
2237
+ log.info("Setting up Ultracite...");
2238
+ await setupBiome(projectDir);
2239
+ const result = await group({
2240
+ editors: () => multiselect({
2241
+ message: "Choose editors",
2242
+ options: Object.entries(EDITORS).map(([key, editor]) => ({
2243
+ value: key,
2244
+ label: editor.label
2245
+ })),
2246
+ required: true
2247
+ }),
2248
+ rules: () => autocompleteMultiselect({
2249
+ message: "Choose rules",
2250
+ options: Object.entries(RULES).map(([key, rule]) => ({
2251
+ value: key,
2252
+ label: rule.label
2253
+ })),
2254
+ required: true
2255
+ })
2256
+ }, { onCancel: () => {
2257
+ exitCancelled("Operation cancelled");
2258
+ } });
2259
+ const editors = result.editors;
2260
+ const rules = result.rules;
2261
+ const ultraciteArgs = [
2262
+ "init",
2263
+ "--pm",
2264
+ packageManager
2265
+ ];
2266
+ if (editors.length > 0) ultraciteArgs.push("--editors", ...editors);
2267
+ if (rules.length > 0) ultraciteArgs.push("--rules", ...rules);
2268
+ if (hasHusky) ultraciteArgs.push("--integrations", "husky", "lint-staged");
2269
+ const ultraciteArgsString = ultraciteArgs.join(" ");
2270
+ const commandWithArgs = `ultracite@latest ${ultraciteArgsString} --skip-install`;
2271
+ const ultraciteInitCommand = getPackageExecutionCommand(packageManager, commandWithArgs);
2272
+ await execa(ultraciteInitCommand, {
2273
+ cwd: projectDir,
2274
+ env: { CI: "true" },
2275
+ shell: true
2276
+ });
2277
+ if (hasHusky) await addPackageDependency({
2278
+ devDependencies: ["husky", "lint-staged"],
2279
+ projectDir
2280
+ });
2281
+ log.success("Ultracite setup successfully!");
2282
+ } catch (error) {
2283
+ log.error(pc.red("Failed to set up Ultracite"));
2284
+ if (error instanceof Error) console.error(pc.red(error.message));
2303
2285
  }
2304
2286
  }
2305
- async function setupAddonsTemplate(projectDir, context) {
2306
- if (!context.addons || context.addons.length === 0) return;
2307
- for (const addon of context.addons) {
2308
- if (addon === "none") continue;
2309
- if (addon === "ruler") continue;
2310
- let addonSrcDir = path.join(PKG_ROOT, `templates/addons/${addon}`);
2311
- let addonDestDir = projectDir;
2312
- if (addon === "pwa") {
2313
- const webAppDir = path.join(projectDir, "apps/web");
2314
- if (!await fs.pathExists(webAppDir)) continue;
2315
- addonDestDir = webAppDir;
2316
- if (context.frontend.includes("next")) addonSrcDir = path.join(PKG_ROOT, "templates/addons/pwa/apps/web/next");
2317
- else if (context.frontend.some((f) => [
2318
- "tanstack-router",
2319
- "react-router",
2320
- "solid"
2321
- ].includes(f))) addonSrcDir = path.join(PKG_ROOT, "templates/addons/pwa/apps/web/vite");
2322
- else continue;
2323
- }
2324
- if (await fs.pathExists(addonSrcDir)) await processAndCopyFiles("**/*", addonSrcDir, addonDestDir, context);
2287
+
2288
+ //#endregion
2289
+ //#region src/utils/ts-morph.ts
2290
+ const tsProject = new Project({
2291
+ useInMemoryFileSystem: false,
2292
+ skipAddingFilesFromTsConfig: true,
2293
+ manipulationSettings: {
2294
+ quoteKind: QuoteKind.Single,
2295
+ indentationText: IndentationText.TwoSpaces
2325
2296
  }
2297
+ });
2298
+ function ensureArrayProperty(obj, name) {
2299
+ return obj.getProperty(name)?.getFirstDescendantByKind(SyntaxKind.ArrayLiteralExpression) ?? obj.addPropertyAssignment({
2300
+ name,
2301
+ initializer: "[]"
2302
+ }).getFirstDescendantByKindOrThrow(SyntaxKind.ArrayLiteralExpression);
2326
2303
  }
2327
- async function setupExamplesTemplate(projectDir, context) {
2328
- if (!context.examples || context.examples.length === 0 || context.examples[0] === "none") return;
2329
- const serverAppDir = path.join(projectDir, "apps/server");
2330
- const webAppDir = path.join(projectDir, "apps/web");
2331
- const serverAppDirExists = await fs.pathExists(serverAppDir);
2332
- const webAppDirExists = await fs.pathExists(webAppDir);
2333
- const nativeAppDir = path.join(projectDir, "apps/native");
2334
- const nativeAppDirExists = await fs.pathExists(nativeAppDir);
2335
- const hasReactWeb = context.frontend.some((f) => [
2336
- "tanstack-router",
2337
- "react-router",
2338
- "tanstack-start",
2339
- "next"
2340
- ].includes(f));
2341
- const hasNuxtWeb = context.frontend.includes("nuxt");
2342
- const hasSvelteWeb = context.frontend.includes("svelte");
2343
- const hasSolidWeb = context.frontend.includes("solid");
2344
- for (const example of context.examples) {
2345
- if (example === "none") continue;
2346
- const exampleBaseDir = path.join(PKG_ROOT, `templates/examples/${example}`);
2347
- if (serverAppDirExists && context.backend !== "convex" && context.backend !== "none") {
2348
- const exampleServerSrc = path.join(exampleBaseDir, "server");
2349
- if (example === "ai" && context.backend === "next") {
2350
- const aiNextServerSrc = path.join(exampleServerSrc, "next");
2351
- if (await fs.pathExists(aiNextServerSrc)) await processAndCopyFiles("**/*", aiNextServerSrc, serverAppDir, context, false);
2352
- }
2353
- if (context.orm !== "none" && context.database !== "none") {
2354
- const exampleOrmBaseSrc = path.join(exampleServerSrc, context.orm, "base");
2355
- if (await fs.pathExists(exampleOrmBaseSrc)) await processAndCopyFiles("**/*", exampleOrmBaseSrc, serverAppDir, context, false);
2356
- const exampleDbSchemaSrc = path.join(exampleServerSrc, context.orm, context.database);
2357
- if (await fs.pathExists(exampleDbSchemaSrc)) await processAndCopyFiles("**/*", exampleDbSchemaSrc, serverAppDir, context, false);
2358
- }
2359
- }
2360
- if (webAppDirExists) {
2361
- if (hasReactWeb) {
2362
- const exampleWebSrc = path.join(exampleBaseDir, "web/react");
2363
- if (await fs.pathExists(exampleWebSrc)) {
2364
- if (example === "ai") {
2365
- const exampleWebBaseSrc = path.join(exampleWebSrc, "base");
2366
- if (await fs.pathExists(exampleWebBaseSrc)) await processAndCopyFiles("**/*", exampleWebBaseSrc, webAppDir, context, false);
2367
- }
2368
- const reactFramework = context.frontend.find((f) => [
2369
- "next",
2370
- "react-router",
2371
- "tanstack-router",
2372
- "tanstack-start"
2373
- ].includes(f));
2374
- if (reactFramework) {
2375
- const exampleWebFrameworkSrc = path.join(exampleWebSrc, reactFramework);
2376
- if (await fs.pathExists(exampleWebFrameworkSrc)) await processAndCopyFiles("**/*", exampleWebFrameworkSrc, webAppDir, context, false);
2377
- }
2378
- }
2379
- } else if (hasNuxtWeb) {
2380
- const exampleWebNuxtSrc = path.join(exampleBaseDir, "web/nuxt");
2381
- if (await fs.pathExists(exampleWebNuxtSrc)) await processAndCopyFiles("**/*", exampleWebNuxtSrc, webAppDir, context, false);
2382
- } else if (hasSvelteWeb) {
2383
- const exampleWebSvelteSrc = path.join(exampleBaseDir, "web/svelte");
2384
- if (await fs.pathExists(exampleWebSvelteSrc)) await processAndCopyFiles("**/*", exampleWebSvelteSrc, webAppDir, context, false);
2385
- } else if (hasSolidWeb) {
2386
- const exampleWebSolidSrc = path.join(exampleBaseDir, "web/solid");
2387
- if (await fs.pathExists(exampleWebSolidSrc)) await processAndCopyFiles("**/*", exampleWebSolidSrc, webAppDir, context, false);
2388
- }
2389
- }
2390
- if (nativeAppDirExists) {
2391
- const hasNativeWind = context.frontend.includes("native-nativewind");
2392
- const hasUnistyles = context.frontend.includes("native-unistyles");
2393
- if (hasNativeWind || hasUnistyles) {
2394
- let nativeFramework = "";
2395
- if (hasNativeWind) nativeFramework = "nativewind";
2396
- else if (hasUnistyles) nativeFramework = "unistyles";
2397
- const exampleNativeSrc = path.join(exampleBaseDir, `native/${nativeFramework}`);
2398
- if (await fs.pathExists(exampleNativeSrc)) await processAndCopyFiles("**/*", exampleNativeSrc, nativeAppDir, context, false);
2399
- }
2400
- }
2401
- }
2304
+
2305
+ //#endregion
2306
+ //#region src/helpers/addons/vite-pwa-setup.ts
2307
+ async function addPwaToViteConfig(viteConfigPath, projectName) {
2308
+ const sourceFile = tsProject.addSourceFileAtPathIfExists(viteConfigPath);
2309
+ if (!sourceFile) throw new Error("vite config not found");
2310
+ const hasImport = sourceFile.getImportDeclarations().some((imp) => imp.getModuleSpecifierValue() === "vite-plugin-pwa");
2311
+ if (!hasImport) sourceFile.insertImportDeclaration(0, {
2312
+ namedImports: ["VitePWA"],
2313
+ moduleSpecifier: "vite-plugin-pwa"
2314
+ });
2315
+ const defineCall = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression).find((expr) => {
2316
+ const expression = expr.getExpression();
2317
+ return Node.isIdentifier(expression) && expression.getText() === "defineConfig";
2318
+ });
2319
+ if (!defineCall) throw new Error("Could not find defineConfig call in vite config");
2320
+ const callExpr = defineCall;
2321
+ const configObject = callExpr.getArguments()[0];
2322
+ if (!configObject) throw new Error("defineConfig argument is not an object literal");
2323
+ const pluginsArray = ensureArrayProperty(configObject, "plugins");
2324
+ const alreadyPresent = pluginsArray.getElements().some((el) => el.getText().startsWith("VitePWA("));
2325
+ if (!alreadyPresent) pluginsArray.addElement(`VitePWA({
2326
+ registerType: "autoUpdate",
2327
+ manifest: {
2328
+ name: "${projectName}",
2329
+ short_name: "${projectName}",
2330
+ description: "${projectName} - PWA Application",
2331
+ theme_color: "#0c0c0c",
2332
+ },
2333
+ pwaAssets: { disabled: false, config: true },
2334
+ devOptions: { enabled: true },
2335
+ })`);
2336
+ await tsProject.save();
2402
2337
  }
2403
- async function handleExtras(projectDir, context) {
2404
- const extrasDir = path.join(PKG_ROOT, "templates/extras");
2405
- const hasNativeWind = context.frontend.includes("native-nativewind");
2406
- const hasUnistyles = context.frontend.includes("native-unistyles");
2407
- const hasNative = hasNativeWind || hasUnistyles;
2408
- if (context.packageManager === "pnpm") {
2409
- const pnpmWorkspaceSrc = path.join(extrasDir, "pnpm-workspace.yaml");
2410
- const pnpmWorkspaceDest = path.join(projectDir, "pnpm-workspace.yaml");
2411
- if (await fs.pathExists(pnpmWorkspaceSrc)) await fs.copy(pnpmWorkspaceSrc, pnpmWorkspaceDest);
2412
- }
2413
- if (context.packageManager === "bun") {
2414
- const bunfigSrc = path.join(extrasDir, "bunfig.toml.hbs");
2415
- if (await fs.pathExists(bunfigSrc)) await processAndCopyFiles("bunfig.toml.hbs", extrasDir, projectDir, context);
2338
+
2339
+ //#endregion
2340
+ //#region src/helpers/addons/addons-setup.ts
2341
+ async function setupAddons(config, isAddCommand = false) {
2342
+ const { addons, frontend, projectDir, packageManager } = config;
2343
+ const hasReactWebFrontend = frontend.includes("react-router") || frontend.includes("tanstack-router") || frontend.includes("next");
2344
+ const hasNuxtFrontend = frontend.includes("nuxt");
2345
+ const hasSvelteFrontend = frontend.includes("svelte");
2346
+ const hasSolidFrontend = frontend.includes("solid");
2347
+ const hasNextFrontend = frontend.includes("next");
2348
+ if (addons.includes("turborepo")) {
2349
+ await addPackageDependency({
2350
+ devDependencies: ["turbo"],
2351
+ projectDir
2352
+ });
2353
+ if (isAddCommand) log.info(`${pc.yellow("Update your package.json scripts:")}
2354
+
2355
+ ${pc.dim("Replace:")} ${pc.yellow("\"pnpm -r dev\"")} ${pc.dim("→")} ${pc.green("\"turbo dev\"")}
2356
+ ${pc.dim("Replace:")} ${pc.yellow("\"pnpm --filter web dev\"")} ${pc.dim("→")} ${pc.green("\"turbo -F web dev\"")}
2357
+
2358
+ ${pc.cyan("Docs:")} ${pc.underline("https://turborepo.com/docs")}
2359
+ `);
2416
2360
  }
2417
- if (context.packageManager === "pnpm" && (hasNative || context.frontend.includes("nuxt"))) {
2418
- const npmrcTemplateSrc = path.join(extrasDir, "_npmrc.hbs");
2419
- if (await fs.pathExists(npmrcTemplateSrc)) await processAndCopyFiles("_npmrc.hbs", extrasDir, projectDir, context);
2361
+ if (addons.includes("pwa") && (hasReactWebFrontend || hasSolidFrontend)) await setupPwa(projectDir, frontend);
2362
+ if (addons.includes("tauri") && (hasReactWebFrontend || hasNuxtFrontend || hasSvelteFrontend || hasSolidFrontend || hasNextFrontend)) await setupTauri(config);
2363
+ const hasUltracite = addons.includes("ultracite");
2364
+ const hasBiome = addons.includes("biome");
2365
+ const hasHusky = addons.includes("husky");
2366
+ const hasOxlint = addons.includes("oxlint");
2367
+ if (hasUltracite) await setupUltracite(config, hasHusky);
2368
+ else {
2369
+ if (hasBiome) await setupBiome(projectDir);
2370
+ if (hasHusky) {
2371
+ let linter;
2372
+ if (hasOxlint) linter = "oxlint";
2373
+ else if (hasBiome) linter = "biome";
2374
+ await setupHusky(projectDir, linter);
2375
+ }
2420
2376
  }
2377
+ if (addons.includes("oxlint")) await setupOxlint(projectDir, packageManager);
2378
+ if (addons.includes("starlight")) await setupStarlight(config);
2379
+ if (addons.includes("ruler")) await setupRuler(config);
2380
+ if (addons.includes("fumadocs")) await setupFumadocs(config);
2421
2381
  }
2422
- async function setupDockerComposeTemplates(projectDir, context) {
2423
- if (context.dbSetup !== "docker" || context.database === "none") return;
2424
- const serverAppDir = path.join(projectDir, "apps/server");
2425
- const dockerSrcDir = path.join(PKG_ROOT, `templates/db-setup/docker-compose/${context.database}`);
2426
- if (await fs.pathExists(dockerSrcDir)) await processAndCopyFiles("**/*", dockerSrcDir, serverAppDir, context);
2382
+ function getWebAppDir(projectDir, frontends) {
2383
+ if (frontends.some((f) => [
2384
+ "react-router",
2385
+ "tanstack-router",
2386
+ "nuxt",
2387
+ "svelte",
2388
+ "solid"
2389
+ ].includes(f))) return path.join(projectDir, "apps/web");
2390
+ return path.join(projectDir, "apps/web");
2427
2391
  }
2428
- async function setupDeploymentTemplates(projectDir, context) {
2429
- if (context.webDeploy === "alchemy" || context.serverDeploy === "alchemy") if (context.webDeploy === "alchemy" && context.serverDeploy === "alchemy") {
2430
- const alchemyTemplateSrc = path.join(PKG_ROOT, "templates/deploy/alchemy");
2431
- if (await fs.pathExists(alchemyTemplateSrc)) {
2432
- await processAndCopyFiles("alchemy.run.ts.hbs", alchemyTemplateSrc, projectDir, context);
2433
- const serverAppDir = path.join(projectDir, "apps/server");
2434
- if (await fs.pathExists(serverAppDir)) await processAndCopyFiles("env.d.ts.hbs", alchemyTemplateSrc, serverAppDir, context);
2435
- }
2436
- } else {
2437
- if (context.webDeploy === "alchemy") {
2438
- const alchemyTemplateSrc = path.join(PKG_ROOT, "templates/deploy/alchemy");
2439
- const webAppDir = path.join(projectDir, "apps/web");
2440
- if (await fs.pathExists(alchemyTemplateSrc) && await fs.pathExists(webAppDir)) await processAndCopyFiles("alchemy.run.ts.hbs", alchemyTemplateSrc, webAppDir, context);
2441
- }
2442
- if (context.serverDeploy === "alchemy") {
2443
- const alchemyTemplateSrc = path.join(PKG_ROOT, "templates/deploy/alchemy");
2444
- const serverAppDir = path.join(projectDir, "apps/server");
2445
- if (await fs.pathExists(alchemyTemplateSrc) && await fs.pathExists(serverAppDir)) {
2446
- await processAndCopyFiles("alchemy.run.ts.hbs", alchemyTemplateSrc, serverAppDir, context);
2447
- await processAndCopyFiles("env.d.ts.hbs", alchemyTemplateSrc, serverAppDir, context);
2448
- }
2449
- }
2450
- }
2451
- if (context.webDeploy !== "none" && context.webDeploy !== "alchemy") {
2452
- const webAppDir = path.join(projectDir, "apps/web");
2453
- if (await fs.pathExists(webAppDir)) {
2454
- const frontends = context.frontend;
2455
- const templateMap = {
2456
- "tanstack-router": "react/tanstack-router",
2457
- "tanstack-start": "react/tanstack-start",
2458
- "react-router": "react/react-router",
2459
- solid: "solid",
2460
- next: "react/next",
2461
- nuxt: "nuxt",
2462
- svelte: "svelte"
2463
- };
2464
- for (const f of frontends) if (templateMap[f]) {
2465
- const deployTemplateSrc = path.join(PKG_ROOT, `templates/deploy/${context.webDeploy}/web/${templateMap[f]}`);
2466
- if (await fs.pathExists(deployTemplateSrc)) await processAndCopyFiles("**/*", deployTemplateSrc, webAppDir, context);
2467
- }
2468
- }
2392
+ async function setupBiome(projectDir) {
2393
+ await addPackageDependency({
2394
+ devDependencies: ["@biomejs/biome"],
2395
+ projectDir
2396
+ });
2397
+ const packageJsonPath = path.join(projectDir, "package.json");
2398
+ if (await fs.pathExists(packageJsonPath)) {
2399
+ const packageJson = await fs.readJson(packageJsonPath);
2400
+ packageJson.scripts = {
2401
+ ...packageJson.scripts,
2402
+ check: "biome check --write ."
2403
+ };
2404
+ await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
2469
2405
  }
2470
- if (context.serverDeploy !== "none" && context.serverDeploy !== "alchemy") {
2471
- const serverAppDir = path.join(projectDir, "apps/server");
2472
- if (await fs.pathExists(serverAppDir)) {
2473
- const deployTemplateSrc = path.join(PKG_ROOT, `templates/deploy/${context.serverDeploy}/server`);
2474
- if (await fs.pathExists(deployTemplateSrc)) await processAndCopyFiles("**/*", deployTemplateSrc, serverAppDir, context);
2475
- }
2406
+ }
2407
+ async function setupHusky(projectDir, linter) {
2408
+ await addPackageDependency({
2409
+ devDependencies: ["husky", "lint-staged"],
2410
+ projectDir
2411
+ });
2412
+ const packageJsonPath = path.join(projectDir, "package.json");
2413
+ if (await fs.pathExists(packageJsonPath)) {
2414
+ const packageJson = await fs.readJson(packageJsonPath);
2415
+ packageJson.scripts = {
2416
+ ...packageJson.scripts,
2417
+ prepare: "husky"
2418
+ };
2419
+ if (linter === "oxlint") packageJson["lint-staged"] = { "**/*.{js,mjs,cjs,jsx,ts,mts,cts,tsx,vue,astro,svelte}": "oxlint" };
2420
+ else if (linter === "biome") packageJson["lint-staged"] = { "*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}": ["biome check --write ."] };
2421
+ else packageJson["lint-staged"] = { "**/*.{js,mjs,cjs,jsx,ts,mts,cts,tsx,vue,astro,svelte}": "" };
2422
+ await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
2476
2423
  }
2477
2424
  }
2478
-
2479
- //#endregion
2480
- //#region src/helpers/addons/ruler-setup.ts
2481
- async function setupVibeRules(config) {
2482
- const { packageManager, projectDir } = config;
2483
- try {
2484
- log.info("Setting up Ruler...");
2485
- const rulerDir = path.join(projectDir, ".ruler");
2486
- const rulerTemplateDir = path.join(PKG_ROOT, "templates", "addons", "ruler", ".ruler");
2487
- if (!await fs.pathExists(rulerDir)) if (await fs.pathExists(rulerTemplateDir)) await processAndCopyFiles("**/*", rulerTemplateDir, rulerDir, config);
2488
- else {
2489
- log.error(pc.red("Ruler template directory not found"));
2490
- return;
2491
- }
2492
- const EDITORS$1 = {
2493
- cursor: { label: "Cursor" },
2494
- windsurf: { label: "Windsurf" },
2495
- claude: { label: "Claude Code" },
2496
- copilot: { label: "GitHub Copilot" },
2497
- "gemini-cli": { label: "Gemini CLI" },
2498
- codex: { label: "OpenAI Codex CLI" },
2499
- jules: { label: "Jules" },
2500
- cline: { label: "Cline" },
2501
- aider: { label: "Aider" },
2502
- firebase: { label: "Firebase Studio" },
2503
- openhands: { label: "Open Hands" },
2504
- junie: { label: "Junie" },
2505
- augmentcode: { label: "AugmentCode" },
2506
- kilocode: { label: "Kilo Code" },
2507
- opencode: { label: "OpenCode" }
2425
+ async function setupPwa(projectDir, frontends) {
2426
+ const isCompatibleFrontend = frontends.some((f) => [
2427
+ "react-router",
2428
+ "tanstack-router",
2429
+ "solid"
2430
+ ].includes(f));
2431
+ if (!isCompatibleFrontend) return;
2432
+ const clientPackageDir = getWebAppDir(projectDir, frontends);
2433
+ if (!await fs.pathExists(clientPackageDir)) return;
2434
+ await addPackageDependency({
2435
+ dependencies: ["vite-plugin-pwa"],
2436
+ devDependencies: ["@vite-pwa/assets-generator"],
2437
+ projectDir: clientPackageDir
2438
+ });
2439
+ const clientPackageJsonPath = path.join(clientPackageDir, "package.json");
2440
+ if (await fs.pathExists(clientPackageJsonPath)) {
2441
+ const packageJson = await fs.readJson(clientPackageJsonPath);
2442
+ packageJson.scripts = {
2443
+ ...packageJson.scripts,
2444
+ "generate-pwa-assets": "pwa-assets-generator"
2508
2445
  };
2509
- const selectedEditors = await autocompleteMultiselect({
2510
- message: "Select AI assistants for Ruler",
2511
- options: Object.entries(EDITORS$1).map(([key, v]) => ({
2512
- value: key,
2513
- label: v.label
2514
- })),
2515
- required: false
2516
- });
2517
- if (isCancel(selectedEditors)) return exitCancelled("Operation cancelled");
2518
- if (selectedEditors.length === 0) {
2519
- log.info("No AI assistants selected. To apply rules later, run:");
2520
- log.info(pc.cyan(`${getPackageExecutionCommand(packageManager, "@intellectronica/ruler@latest apply --local-only")}`));
2521
- return;
2522
- }
2523
- const configFile = path.join(rulerDir, "ruler.toml");
2524
- const currentConfig = await fs.readFile(configFile, "utf-8");
2525
- let updatedConfig = currentConfig;
2526
- const defaultAgentsLine = `default_agents = [${selectedEditors.map((editor) => `"${editor}"`).join(", ")}]`;
2527
- updatedConfig = updatedConfig.replace(/default_agents = \[\]/, defaultAgentsLine);
2528
- await fs.writeFile(configFile, updatedConfig);
2529
- await addRulerScriptToPackageJson(projectDir, packageManager);
2530
- const s = spinner();
2531
- s.start("Applying rules with Ruler...");
2532
- try {
2533
- const rulerApplyCmd = getPackageExecutionCommand(packageManager, `@intellectronica/ruler@latest apply --agents ${selectedEditors.join(",")} --local-only`);
2534
- await execa(rulerApplyCmd, {
2535
- cwd: projectDir,
2536
- env: { CI: "true" },
2537
- shell: true
2538
- });
2539
- s.stop("Applied rules with Ruler");
2540
- } catch (_error) {
2541
- s.stop(pc.red("Failed to apply rules"));
2542
- }
2543
- } catch (error) {
2544
- log.error(pc.red("Failed to set up Ruler"));
2545
- if (error instanceof Error) console.error(pc.red(error.message));
2446
+ await fs.writeJson(clientPackageJsonPath, packageJson, { spaces: 2 });
2546
2447
  }
2448
+ const viteConfigTs = path.join(clientPackageDir, "vite.config.ts");
2449
+ if (await fs.pathExists(viteConfigTs)) await addPwaToViteConfig(viteConfigTs, path.basename(projectDir));
2547
2450
  }
2548
- async function addRulerScriptToPackageJson(projectDir, packageManager) {
2549
- const rootPackageJsonPath = path.join(projectDir, "package.json");
2550
- if (!await fs.pathExists(rootPackageJsonPath)) {
2551
- log.warn("Root package.json not found, skipping ruler:apply script addition");
2552
- return;
2451
+ async function setupOxlint(projectDir, packageManager) {
2452
+ await addPackageDependency({
2453
+ devDependencies: ["oxlint"],
2454
+ projectDir
2455
+ });
2456
+ const packageJsonPath = path.join(projectDir, "package.json");
2457
+ if (await fs.pathExists(packageJsonPath)) {
2458
+ const packageJson = await fs.readJson(packageJsonPath);
2459
+ packageJson.scripts = {
2460
+ ...packageJson.scripts,
2461
+ check: "oxlint"
2462
+ };
2463
+ await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
2553
2464
  }
2554
- const packageJson = await fs.readJson(rootPackageJsonPath);
2555
- if (!packageJson.scripts) packageJson.scripts = {};
2556
- const rulerApplyCommand = getPackageExecutionCommand(packageManager, "@intellectronica/ruler@latest apply --local-only");
2557
- packageJson.scripts["ruler:apply"] = rulerApplyCommand;
2558
- await fs.writeJson(rootPackageJsonPath, packageJson, { spaces: 2 });
2465
+ const oxlintInitCommand = getPackageExecutionCommand(packageManager, "oxlint@latest --init");
2466
+ await execa(oxlintInitCommand, {
2467
+ cwd: projectDir,
2468
+ env: { CI: "true" },
2469
+ shell: true
2470
+ });
2559
2471
  }
2560
2472
 
2561
2473
  //#endregion
2562
- //#region src/helpers/addons/starlight-setup.ts
2563
- async function setupStarlight(config) {
2564
- const { packageManager, projectDir } = config;
2565
- const s = spinner();
2474
+ //#region src/helpers/core/detect-project-config.ts
2475
+ async function detectProjectConfig(projectDir) {
2566
2476
  try {
2567
- s.start("Setting up Starlight docs...");
2568
- const starlightArgs = [
2569
- "docs",
2570
- "--template",
2571
- "starlight",
2572
- "--no-install",
2573
- "--add",
2574
- "tailwind",
2575
- "--no-git",
2576
- "--skip-houston"
2577
- ];
2578
- const starlightArgsString = starlightArgs.join(" ");
2579
- const commandWithArgs = `create-astro@latest ${starlightArgsString}`;
2580
- const starlightInitCommand = getPackageExecutionCommand(packageManager, commandWithArgs);
2581
- await execa(starlightInitCommand, {
2582
- cwd: path.join(projectDir, "apps"),
2583
- env: { CI: "true" },
2584
- shell: true
2585
- });
2586
- s.stop("Starlight docs setup successfully!");
2587
- } catch (error) {
2588
- s.stop(pc.red("Failed to set up Starlight docs"));
2589
- if (error instanceof Error) consola.error(pc.red(error.message));
2477
+ const btsConfig = await readBtsConfig(projectDir);
2478
+ if (btsConfig) return {
2479
+ projectDir,
2480
+ projectName: path.basename(projectDir),
2481
+ database: btsConfig.database,
2482
+ orm: btsConfig.orm,
2483
+ backend: btsConfig.backend,
2484
+ runtime: btsConfig.runtime,
2485
+ frontend: btsConfig.frontend,
2486
+ addons: btsConfig.addons,
2487
+ examples: btsConfig.examples,
2488
+ auth: btsConfig.auth,
2489
+ packageManager: btsConfig.packageManager,
2490
+ dbSetup: btsConfig.dbSetup,
2491
+ api: btsConfig.api,
2492
+ webDeploy: btsConfig.webDeploy,
2493
+ serverDeploy: btsConfig.serverDeploy
2494
+ };
2495
+ return null;
2496
+ } catch (_error) {
2497
+ return null;
2498
+ }
2499
+ }
2500
+ async function isBetterTStackProject(projectDir) {
2501
+ try {
2502
+ return await fs.pathExists(path.join(projectDir, "bts.jsonc"));
2503
+ } catch (_error) {
2504
+ return false;
2590
2505
  }
2591
2506
  }
2592
2507
 
2593
2508
  //#endregion
2594
- //#region src/helpers/addons/tauri-setup.ts
2595
- async function setupTauri(config) {
2596
- const { packageManager, frontend, projectDir } = config;
2509
+ //#region src/helpers/core/install-dependencies.ts
2510
+ async function installDependencies({ projectDir, packageManager }) {
2597
2511
  const s = spinner();
2598
- const clientPackageDir = path.join(projectDir, "apps/web");
2599
- if (!await fs.pathExists(clientPackageDir)) return;
2600
2512
  try {
2601
- s.start("Setting up Tauri desktop app support...");
2602
- await addPackageDependency({
2603
- devDependencies: ["@tauri-apps/cli"],
2604
- projectDir: clientPackageDir
2605
- });
2606
- const clientPackageJsonPath = path.join(clientPackageDir, "package.json");
2607
- if (await fs.pathExists(clientPackageJsonPath)) {
2608
- const packageJson = await fs.readJson(clientPackageJsonPath);
2609
- packageJson.scripts = {
2610
- ...packageJson.scripts,
2611
- tauri: "tauri",
2612
- "desktop:dev": "tauri dev",
2613
- "desktop:build": "tauri build"
2614
- };
2615
- await fs.writeJson(clientPackageJsonPath, packageJson, { spaces: 2 });
2616
- }
2617
- frontend.includes("tanstack-router");
2618
- const hasReactRouter = frontend.includes("react-router");
2619
- const hasNuxt = frontend.includes("nuxt");
2620
- const hasSvelte = frontend.includes("svelte");
2621
- frontend.includes("solid");
2622
- const hasNext = frontend.includes("next");
2623
- const devUrl = hasReactRouter || hasSvelte ? "http://localhost:5173" : hasNext ? "http://localhost:3001" : "http://localhost:3001";
2624
- const frontendDist = hasNuxt ? "../.output/public" : hasSvelte ? "../build" : hasNext ? "../.next" : hasReactRouter ? "../build/client" : "../dist";
2625
- const tauriArgs = [
2626
- "init",
2627
- `--app-name=${path.basename(projectDir)}`,
2628
- `--window-title=${path.basename(projectDir)}`,
2629
- `--frontend-dist=${frontendDist}`,
2630
- `--dev-url=${devUrl}`,
2631
- `--before-dev-command="${packageManager} run dev"`,
2632
- `--before-build-command="${packageManager} run build"`
2633
- ];
2634
- const tauriArgsString = tauriArgs.join(" ");
2635
- const commandWithArgs = `@tauri-apps/cli@latest ${tauriArgsString}`;
2636
- const tauriInitCommand = getPackageExecutionCommand(packageManager, commandWithArgs);
2637
- await execa(tauriInitCommand, {
2638
- cwd: clientPackageDir,
2639
- env: { CI: "true" },
2640
- shell: true
2641
- });
2642
- s.stop("Tauri desktop app support configured successfully!");
2513
+ s.start(`Running ${packageManager} install...`);
2514
+ await $({
2515
+ cwd: projectDir,
2516
+ stderr: "inherit"
2517
+ })`${packageManager} install`;
2518
+ s.stop("Dependencies installed successfully");
2643
2519
  } catch (error) {
2644
- s.stop(pc.red("Failed to set up Tauri"));
2645
- if (error instanceof Error) consola$1.error(pc.red(error.message));
2520
+ s.stop(pc.red("Failed to install dependencies"));
2521
+ if (error instanceof Error) consola.error(pc.red(`Installation error: ${error.message}`));
2646
2522
  }
2647
2523
  }
2648
2524
 
2649
2525
  //#endregion
2650
- //#region src/helpers/addons/ultracite-setup.ts
2651
- const EDITORS = {
2652
- vscode: { label: "VSCode / Cursor / Windsurf" },
2653
- zed: { label: "Zed" }
2654
- };
2655
- const RULES = {
2656
- "vscode-copilot": { label: "VS Code Copilot" },
2657
- cursor: { label: "Cursor" },
2658
- windsurf: { label: "Windsurf" },
2659
- zed: { label: "Zed" },
2660
- claude: { label: "Claude" },
2661
- codex: { label: "Codex" },
2662
- kiro: { label: "Kiro" },
2663
- cline: { label: "Cline" },
2664
- amp: { label: "Amp" },
2665
- aider: { label: "Aider" },
2666
- "firebase-studio": { label: "Firebase Studio" },
2667
- "open-hands": { label: "Open Hands" },
2668
- "gemini-cli": { label: "Gemini CLI" },
2669
- junie: { label: "Junie" },
2670
- augmentcode: { label: "AugmentCode" },
2671
- "kilo-code": { label: "Kilo Code" },
2672
- goose: { label: "Goose" }
2673
- };
2674
- async function setupUltracite(config, hasHusky) {
2675
- const { packageManager, projectDir } = config;
2526
+ //#region src/utils/template-processor.ts
2527
+ /**
2528
+ * Processes a Handlebars template file and writes the output to the destination.
2529
+ * @param srcPath Path to the source .hbs template file.
2530
+ * @param destPath Path to write the processed file.
2531
+ * @param context Data to be passed to the Handlebars template.
2532
+ */
2533
+ async function processTemplate(srcPath, destPath, context) {
2676
2534
  try {
2677
- log.info("Setting up Ultracite...");
2678
- await setupBiome(projectDir);
2679
- const result = await group({
2680
- editors: () => multiselect({
2681
- message: "Choose editors",
2682
- options: Object.entries(EDITORS).map(([key, editor]) => ({
2683
- value: key,
2684
- label: editor.label
2685
- })),
2686
- required: true
2687
- }),
2688
- rules: () => autocompleteMultiselect({
2689
- message: "Choose rules",
2690
- options: Object.entries(RULES).map(([key, rule]) => ({
2691
- value: key,
2692
- label: rule.label
2693
- })),
2694
- required: true
2695
- })
2696
- }, { onCancel: () => {
2697
- exitCancelled("Operation cancelled");
2698
- } });
2699
- const editors = result.editors;
2700
- const rules = result.rules;
2701
- const ultraciteArgs = [
2702
- "init",
2703
- "--pm",
2704
- packageManager
2705
- ];
2706
- if (editors.length > 0) ultraciteArgs.push("--editors", ...editors);
2707
- if (rules.length > 0) ultraciteArgs.push("--rules", ...rules);
2708
- if (hasHusky) ultraciteArgs.push("--integrations", "husky", "lint-staged");
2709
- const ultraciteArgsString = ultraciteArgs.join(" ");
2710
- const commandWithArgs = `ultracite@latest ${ultraciteArgsString} --skip-install`;
2711
- const ultraciteInitCommand = getPackageExecutionCommand(packageManager, commandWithArgs);
2712
- await execa(ultraciteInitCommand, {
2713
- cwd: projectDir,
2714
- env: { CI: "true" },
2715
- shell: true
2716
- });
2717
- if (hasHusky) await addPackageDependency({
2718
- devDependencies: ["husky", "lint-staged"],
2719
- projectDir
2720
- });
2721
- log.success("Ultracite setup successfully!");
2535
+ const templateContent = await fs.readFile(srcPath, "utf-8");
2536
+ const template = handlebars.compile(templateContent);
2537
+ const processedContent = template(context);
2538
+ await fs.ensureDir(path.dirname(destPath));
2539
+ await fs.writeFile(destPath, processedContent);
2722
2540
  } catch (error) {
2723
- log.error(pc.red("Failed to set up Ultracite"));
2724
- if (error instanceof Error) console.error(pc.red(error.message));
2541
+ consola.error(`Error processing template ${srcPath}:`, error);
2542
+ throw new Error(`Failed to process template ${srcPath}`);
2543
+ }
2544
+ }
2545
+ handlebars.registerHelper("eq", (a, b) => a === b);
2546
+ handlebars.registerHelper("ne", (a, b) => a !== b);
2547
+ handlebars.registerHelper("and", (a, b) => a && b);
2548
+ handlebars.registerHelper("or", (a, b) => a || b);
2549
+ handlebars.registerHelper("includes", (array, value) => Array.isArray(array) && array.includes(value));
2550
+
2551
+ //#endregion
2552
+ //#region src/helpers/core/template-manager.ts
2553
+ async function processAndCopyFiles(sourcePattern, baseSourceDir, destDir, context, overwrite = true, ignorePatterns) {
2554
+ const sourceFiles = await glob(sourcePattern, {
2555
+ cwd: baseSourceDir,
2556
+ dot: true,
2557
+ onlyFiles: true,
2558
+ absolute: false,
2559
+ ignore: ignorePatterns
2560
+ });
2561
+ for (const relativeSrcPath of sourceFiles) {
2562
+ const srcPath = path.join(baseSourceDir, relativeSrcPath);
2563
+ let relativeDestPath = relativeSrcPath;
2564
+ if (relativeSrcPath.endsWith(".hbs")) relativeDestPath = relativeSrcPath.slice(0, -4);
2565
+ const basename = path.basename(relativeDestPath);
2566
+ if (basename === "_gitignore") relativeDestPath = path.join(path.dirname(relativeDestPath), ".gitignore");
2567
+ else if (basename === "_npmrc") relativeDestPath = path.join(path.dirname(relativeDestPath), ".npmrc");
2568
+ const destPath = path.join(destDir, relativeDestPath);
2569
+ await fs.ensureDir(path.dirname(destPath));
2570
+ if (!overwrite && await fs.pathExists(destPath)) continue;
2571
+ if (srcPath.endsWith(".hbs")) await processTemplate(srcPath, destPath, context);
2572
+ else await fs.copy(srcPath, destPath, { overwrite: true });
2573
+ }
2574
+ }
2575
+ async function copyBaseTemplate(projectDir, context) {
2576
+ const templateDir = path.join(PKG_ROOT, "templates/base");
2577
+ await processAndCopyFiles(["**/*"], templateDir, projectDir, context);
2578
+ }
2579
+ async function setupFrontendTemplates(projectDir, context) {
2580
+ const hasReactWeb = context.frontend.some((f) => [
2581
+ "tanstack-router",
2582
+ "react-router",
2583
+ "tanstack-start",
2584
+ "next"
2585
+ ].includes(f));
2586
+ const hasNuxtWeb = context.frontend.includes("nuxt");
2587
+ const hasSvelteWeb = context.frontend.includes("svelte");
2588
+ const hasSolidWeb = context.frontend.includes("solid");
2589
+ const hasNativeWind = context.frontend.includes("native-nativewind");
2590
+ const hasUnistyles = context.frontend.includes("native-unistyles");
2591
+ const isConvex = context.backend === "convex";
2592
+ if (hasReactWeb || hasNuxtWeb || hasSvelteWeb || hasSolidWeb) {
2593
+ const webAppDir = path.join(projectDir, "apps/web");
2594
+ await fs.ensureDir(webAppDir);
2595
+ if (hasReactWeb) {
2596
+ const webBaseDir = path.join(PKG_ROOT, "templates/frontend/react/web-base");
2597
+ if (await fs.pathExists(webBaseDir)) await processAndCopyFiles("**/*", webBaseDir, webAppDir, context);
2598
+ const reactFramework = context.frontend.find((f) => [
2599
+ "tanstack-router",
2600
+ "react-router",
2601
+ "tanstack-start",
2602
+ "next"
2603
+ ].includes(f));
2604
+ if (reactFramework) {
2605
+ const frameworkSrcDir = path.join(PKG_ROOT, `templates/frontend/react/${reactFramework}`);
2606
+ if (await fs.pathExists(frameworkSrcDir)) await processAndCopyFiles("**/*", frameworkSrcDir, webAppDir, context);
2607
+ if (!isConvex && context.api !== "none") {
2608
+ const apiWebBaseDir = path.join(PKG_ROOT, `templates/api/${context.api}/web/react/base`);
2609
+ if (await fs.pathExists(apiWebBaseDir)) await processAndCopyFiles("**/*", apiWebBaseDir, webAppDir, context);
2610
+ }
2611
+ }
2612
+ } else if (hasNuxtWeb) {
2613
+ const nuxtBaseDir = path.join(PKG_ROOT, "templates/frontend/nuxt");
2614
+ if (await fs.pathExists(nuxtBaseDir)) await processAndCopyFiles("**/*", nuxtBaseDir, webAppDir, context);
2615
+ if (!isConvex && context.api === "orpc") {
2616
+ const apiWebNuxtDir = path.join(PKG_ROOT, `templates/api/${context.api}/web/nuxt`);
2617
+ if (await fs.pathExists(apiWebNuxtDir)) await processAndCopyFiles("**/*", apiWebNuxtDir, webAppDir, context);
2618
+ }
2619
+ } else if (hasSvelteWeb) {
2620
+ const svelteBaseDir = path.join(PKG_ROOT, "templates/frontend/svelte");
2621
+ if (await fs.pathExists(svelteBaseDir)) await processAndCopyFiles("**/*", svelteBaseDir, webAppDir, context);
2622
+ if (!isConvex && context.api === "orpc") {
2623
+ const apiWebSvelteDir = path.join(PKG_ROOT, `templates/api/${context.api}/web/svelte`);
2624
+ if (await fs.pathExists(apiWebSvelteDir)) await processAndCopyFiles("**/*", apiWebSvelteDir, webAppDir, context);
2625
+ }
2626
+ } else if (hasSolidWeb) {
2627
+ const solidBaseDir = path.join(PKG_ROOT, "templates/frontend/solid");
2628
+ if (await fs.pathExists(solidBaseDir)) await processAndCopyFiles("**/*", solidBaseDir, webAppDir, context);
2629
+ if (!isConvex && context.api === "orpc") {
2630
+ const apiWebSolidDir = path.join(PKG_ROOT, `templates/api/${context.api}/web/solid`);
2631
+ if (await fs.pathExists(apiWebSolidDir)) await processAndCopyFiles("**/*", apiWebSolidDir, webAppDir, context);
2632
+ }
2633
+ }
2634
+ }
2635
+ if (hasNativeWind || hasUnistyles) {
2636
+ const nativeAppDir = path.join(projectDir, "apps/native");
2637
+ await fs.ensureDir(nativeAppDir);
2638
+ const nativeBaseCommonDir = path.join(PKG_ROOT, "templates/frontend/native/native-base");
2639
+ if (await fs.pathExists(nativeBaseCommonDir)) await processAndCopyFiles("**/*", nativeBaseCommonDir, nativeAppDir, context);
2640
+ let nativeFrameworkPath = "";
2641
+ if (hasNativeWind) nativeFrameworkPath = "nativewind";
2642
+ else if (hasUnistyles) nativeFrameworkPath = "unistyles";
2643
+ const nativeSpecificDir = path.join(PKG_ROOT, `templates/frontend/native/${nativeFrameworkPath}`);
2644
+ if (await fs.pathExists(nativeSpecificDir)) await processAndCopyFiles("**/*", nativeSpecificDir, nativeAppDir, context, true);
2645
+ if (!isConvex && (context.api === "trpc" || context.api === "orpc")) {
2646
+ const apiNativeSrcDir = path.join(PKG_ROOT, `templates/api/${context.api}/native`);
2647
+ if (await fs.pathExists(apiNativeSrcDir)) await processAndCopyFiles("**/*", apiNativeSrcDir, nativeAppDir, context);
2648
+ }
2725
2649
  }
2726
2650
  }
2727
-
2728
- //#endregion
2729
- //#region src/utils/ts-morph.ts
2730
- const tsProject = new Project({
2731
- useInMemoryFileSystem: false,
2732
- skipAddingFilesFromTsConfig: true,
2733
- manipulationSettings: {
2734
- quoteKind: QuoteKind.Single,
2735
- indentationText: IndentationText.TwoSpaces
2651
+ async function setupBackendFramework(projectDir, context) {
2652
+ if (context.backend === "none") return;
2653
+ const serverAppDir = path.join(projectDir, "apps/server");
2654
+ if (context.backend === "convex") {
2655
+ if (await fs.pathExists(serverAppDir)) await fs.remove(serverAppDir);
2656
+ const convexBackendDestDir = path.join(projectDir, "packages/backend");
2657
+ const convexSrcDir = path.join(PKG_ROOT, "templates/backend/convex/packages/backend");
2658
+ await fs.ensureDir(convexBackendDestDir);
2659
+ if (await fs.pathExists(convexSrcDir)) await processAndCopyFiles("**/*", convexSrcDir, convexBackendDestDir, context);
2660
+ return;
2661
+ }
2662
+ await fs.ensureDir(serverAppDir);
2663
+ const serverBaseDir = path.join(PKG_ROOT, "templates/backend/server/server-base");
2664
+ if (await fs.pathExists(serverBaseDir)) await processAndCopyFiles("**/*", serverBaseDir, serverAppDir, context);
2665
+ const frameworkSrcDir = path.join(PKG_ROOT, `templates/backend/server/${context.backend}`);
2666
+ if (await fs.pathExists(frameworkSrcDir)) await processAndCopyFiles("**/*", frameworkSrcDir, serverAppDir, context, true);
2667
+ if (context.api !== "none") {
2668
+ const apiServerBaseDir = path.join(PKG_ROOT, `templates/api/${context.api}/server/base`);
2669
+ if (await fs.pathExists(apiServerBaseDir)) await processAndCopyFiles("**/*", apiServerBaseDir, serverAppDir, context, true);
2670
+ const apiServerFrameworkDir = path.join(PKG_ROOT, `templates/api/${context.api}/server/${context.backend}`);
2671
+ if (await fs.pathExists(apiServerFrameworkDir)) await processAndCopyFiles("**/*", apiServerFrameworkDir, serverAppDir, context, true);
2736
2672
  }
2737
- });
2738
- function ensureArrayProperty(obj, name) {
2739
- return obj.getProperty(name)?.getFirstDescendantByKind(SyntaxKind.ArrayLiteralExpression) ?? obj.addPropertyAssignment({
2740
- name,
2741
- initializer: "[]"
2742
- }).getFirstDescendantByKindOrThrow(SyntaxKind.ArrayLiteralExpression);
2743
2673
  }
2744
-
2745
- //#endregion
2746
- //#region src/helpers/addons/vite-pwa-setup.ts
2747
- async function addPwaToViteConfig(viteConfigPath, projectName) {
2748
- const sourceFile = tsProject.addSourceFileAtPathIfExists(viteConfigPath);
2749
- if (!sourceFile) throw new Error("vite config not found");
2750
- const hasImport = sourceFile.getImportDeclarations().some((imp) => imp.getModuleSpecifierValue() === "vite-plugin-pwa");
2751
- if (!hasImport) sourceFile.insertImportDeclaration(0, {
2752
- namedImports: ["VitePWA"],
2753
- moduleSpecifier: "vite-plugin-pwa"
2754
- });
2755
- const defineCall = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression).find((expr) => {
2756
- const expression = expr.getExpression();
2757
- return Node.isIdentifier(expression) && expression.getText() === "defineConfig";
2758
- });
2759
- if (!defineCall) throw new Error("Could not find defineConfig call in vite config");
2760
- const callExpr = defineCall;
2761
- const configObject = callExpr.getArguments()[0];
2762
- if (!configObject) throw new Error("defineConfig argument is not an object literal");
2763
- const pluginsArray = ensureArrayProperty(configObject, "plugins");
2764
- const alreadyPresent = pluginsArray.getElements().some((el) => el.getText().startsWith("VitePWA("));
2765
- if (!alreadyPresent) pluginsArray.addElement(`VitePWA({
2766
- registerType: "autoUpdate",
2767
- manifest: {
2768
- name: "${projectName}",
2769
- short_name: "${projectName}",
2770
- description: "${projectName} - PWA Application",
2771
- theme_color: "#0c0c0c",
2772
- },
2773
- pwaAssets: { disabled: false, config: true },
2774
- devOptions: { enabled: true },
2775
- })`);
2776
- await tsProject.save();
2674
+ async function setupDbOrmTemplates(projectDir, context) {
2675
+ if (context.backend === "convex" || context.orm === "none" || context.database === "none") return;
2676
+ const serverAppDir = path.join(projectDir, "apps/server");
2677
+ await fs.ensureDir(serverAppDir);
2678
+ const dbOrmSrcDir = path.join(PKG_ROOT, `templates/db/${context.orm}/${context.database}`);
2679
+ if (await fs.pathExists(dbOrmSrcDir)) await processAndCopyFiles("**/*", dbOrmSrcDir, serverAppDir, context);
2777
2680
  }
2778
-
2779
- //#endregion
2780
- //#region src/helpers/addons/addons-setup.ts
2781
- async function setupAddons(config, isAddCommand = false) {
2782
- const { addons, frontend, projectDir, packageManager } = config;
2783
- const hasReactWebFrontend = frontend.includes("react-router") || frontend.includes("tanstack-router") || frontend.includes("next");
2784
- const hasNuxtFrontend = frontend.includes("nuxt");
2785
- const hasSvelteFrontend = frontend.includes("svelte");
2786
- const hasSolidFrontend = frontend.includes("solid");
2787
- const hasNextFrontend = frontend.includes("next");
2788
- if (addons.includes("turborepo")) {
2789
- await addPackageDependency({
2790
- devDependencies: ["turbo"],
2791
- projectDir
2792
- });
2793
- if (isAddCommand) log.info(`${pc.yellow("Update your package.json scripts:")}
2794
-
2795
- ${pc.dim("Replace:")} ${pc.yellow("\"pnpm -r dev\"")} ${pc.dim("→")} ${pc.green("\"turbo dev\"")}
2796
- ${pc.dim("Replace:")} ${pc.yellow("\"pnpm --filter web dev\"")} ${pc.dim("→")} ${pc.green("\"turbo -F web dev\"")}
2797
-
2798
- ${pc.cyan("Docs:")} ${pc.underline("https://turborepo.com/docs")}
2799
- `);
2681
+ async function setupAuthTemplate(projectDir, context) {
2682
+ if (!context.auth || context.auth === "none") return;
2683
+ const serverAppDir = path.join(projectDir, "apps/server");
2684
+ const webAppDir = path.join(projectDir, "apps/web");
2685
+ const nativeAppDir = path.join(projectDir, "apps/native");
2686
+ const serverAppDirExists = await fs.pathExists(serverAppDir);
2687
+ const webAppDirExists = await fs.pathExists(webAppDir);
2688
+ const nativeAppDirExists = await fs.pathExists(nativeAppDir);
2689
+ const hasReactWeb = context.frontend.some((f) => [
2690
+ "tanstack-router",
2691
+ "react-router",
2692
+ "tanstack-start",
2693
+ "next"
2694
+ ].includes(f));
2695
+ const hasNuxtWeb = context.frontend.includes("nuxt");
2696
+ const hasSvelteWeb = context.frontend.includes("svelte");
2697
+ const hasSolidWeb = context.frontend.includes("solid");
2698
+ const hasNativeWind = context.frontend.includes("native-nativewind");
2699
+ const hasUnistyles = context.frontend.includes("native-unistyles");
2700
+ const hasNative = hasNativeWind || hasUnistyles;
2701
+ const authProvider = context.auth;
2702
+ if (context.backend === "convex" && authProvider === "clerk") {
2703
+ const convexBackendDestDir = path.join(projectDir, "packages/backend");
2704
+ const convexClerkBackendSrc = path.join(PKG_ROOT, "templates/auth/clerk/convex/backend");
2705
+ if (await fs.pathExists(convexClerkBackendSrc)) {
2706
+ await fs.ensureDir(convexBackendDestDir);
2707
+ await processAndCopyFiles("**/*", convexClerkBackendSrc, convexBackendDestDir, context);
2708
+ }
2709
+ if (webAppDirExists) {
2710
+ const reactFramework = context.frontend.find((f) => [
2711
+ "tanstack-router",
2712
+ "react-router",
2713
+ "tanstack-start",
2714
+ "next"
2715
+ ].includes(f));
2716
+ if (reactFramework) {
2717
+ const convexClerkWebSrc = path.join(PKG_ROOT, `templates/auth/clerk/convex/web/react/${reactFramework}`);
2718
+ if (await fs.pathExists(convexClerkWebSrc)) await processAndCopyFiles("**/*", convexClerkWebSrc, webAppDir, context);
2719
+ }
2720
+ }
2721
+ if (nativeAppDirExists) {
2722
+ const convexClerkNativeBaseSrc = path.join(PKG_ROOT, "templates/auth/clerk/convex/native/base");
2723
+ if (await fs.pathExists(convexClerkNativeBaseSrc)) await processAndCopyFiles("**/*", convexClerkNativeBaseSrc, nativeAppDir, context);
2724
+ const hasNativeWind$1 = context.frontend.includes("native-nativewind");
2725
+ const hasUnistyles$1 = context.frontend.includes("native-unistyles");
2726
+ let nativeFrameworkPath = "";
2727
+ if (hasNativeWind$1) nativeFrameworkPath = "nativewind";
2728
+ else if (hasUnistyles$1) nativeFrameworkPath = "unistyles";
2729
+ if (nativeFrameworkPath) {
2730
+ const convexClerkNativeFrameworkSrc = path.join(PKG_ROOT, `templates/auth/clerk/convex/native/${nativeFrameworkPath}`);
2731
+ if (await fs.pathExists(convexClerkNativeFrameworkSrc)) await processAndCopyFiles("**/*", convexClerkNativeFrameworkSrc, nativeAppDir, context);
2732
+ }
2733
+ }
2734
+ return;
2800
2735
  }
2801
- if (addons.includes("pwa") && (hasReactWebFrontend || hasSolidFrontend)) await setupPwa(projectDir, frontend);
2802
- if (addons.includes("tauri") && (hasReactWebFrontend || hasNuxtFrontend || hasSvelteFrontend || hasSolidFrontend || hasNextFrontend)) await setupTauri(config);
2803
- const hasUltracite = addons.includes("ultracite");
2804
- const hasBiome = addons.includes("biome");
2805
- const hasHusky = addons.includes("husky");
2806
- const hasOxlint = addons.includes("oxlint");
2807
- if (hasUltracite) await setupUltracite(config, hasHusky);
2808
- else {
2809
- if (hasBiome) await setupBiome(projectDir);
2810
- if (hasHusky) {
2811
- let linter;
2812
- if (hasOxlint) linter = "oxlint";
2813
- else if (hasBiome) linter = "biome";
2814
- await setupHusky(projectDir, linter);
2736
+ if (serverAppDirExists && context.backend !== "convex") {
2737
+ const authServerBaseSrc = path.join(PKG_ROOT, `templates/auth/${authProvider}/server/base`);
2738
+ if (await fs.pathExists(authServerBaseSrc)) await processAndCopyFiles("**/*", authServerBaseSrc, serverAppDir, context);
2739
+ if (context.backend === "next") {
2740
+ const authServerNextSrc = path.join(PKG_ROOT, `templates/auth/${authProvider}/server/next`);
2741
+ if (await fs.pathExists(authServerNextSrc)) await processAndCopyFiles("**/*", authServerNextSrc, serverAppDir, context);
2742
+ }
2743
+ if (context.orm !== "none" && context.database !== "none") {
2744
+ const orm = context.orm;
2745
+ const db = context.database;
2746
+ let authDbSrc = "";
2747
+ if (orm === "drizzle") authDbSrc = path.join(PKG_ROOT, `templates/auth/${authProvider}/server/db/drizzle/${db}`);
2748
+ else if (orm === "prisma") authDbSrc = path.join(PKG_ROOT, `templates/auth/${authProvider}/server/db/prisma/${db}`);
2749
+ else if (orm === "mongoose") authDbSrc = path.join(PKG_ROOT, `templates/auth/${authProvider}/server/db/mongoose/${db}`);
2750
+ if (authDbSrc && await fs.pathExists(authDbSrc)) await processAndCopyFiles("**/*", authDbSrc, serverAppDir, context);
2815
2751
  }
2816
2752
  }
2817
- if (addons.includes("oxlint")) await setupOxlint(projectDir, packageManager);
2818
- if (addons.includes("starlight")) await setupStarlight(config);
2819
- if (addons.includes("ruler")) await setupVibeRules(config);
2820
- if (addons.includes("fumadocs")) await setupFumadocs(config);
2821
- }
2822
- function getWebAppDir(projectDir, frontends) {
2823
- if (frontends.some((f) => [
2824
- "react-router",
2825
- "tanstack-router",
2826
- "nuxt",
2827
- "svelte",
2828
- "solid"
2829
- ].includes(f))) return path.join(projectDir, "apps/web");
2830
- return path.join(projectDir, "apps/web");
2831
- }
2832
- async function setupBiome(projectDir) {
2833
- await addPackageDependency({
2834
- devDependencies: ["@biomejs/biome"],
2835
- projectDir
2836
- });
2837
- const packageJsonPath = path.join(projectDir, "package.json");
2838
- if (await fs.pathExists(packageJsonPath)) {
2839
- const packageJson = await fs.readJson(packageJsonPath);
2840
- packageJson.scripts = {
2841
- ...packageJson.scripts,
2842
- check: "biome check --write ."
2843
- };
2844
- await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
2753
+ if ((hasReactWeb || hasNuxtWeb || hasSvelteWeb || hasSolidWeb) && webAppDirExists) {
2754
+ if (hasReactWeb) {
2755
+ const authWebBaseSrc = path.join(PKG_ROOT, `templates/auth/${authProvider}/web/react/base`);
2756
+ if (await fs.pathExists(authWebBaseSrc)) await processAndCopyFiles("**/*", authWebBaseSrc, webAppDir, context);
2757
+ const reactFramework = context.frontend.find((f) => [
2758
+ "tanstack-router",
2759
+ "react-router",
2760
+ "tanstack-start",
2761
+ "next"
2762
+ ].includes(f));
2763
+ if (reactFramework) {
2764
+ const authWebFrameworkSrc = path.join(PKG_ROOT, `templates/auth/${authProvider}/web/react/${reactFramework}`);
2765
+ if (await fs.pathExists(authWebFrameworkSrc)) await processAndCopyFiles("**/*", authWebFrameworkSrc, webAppDir, context);
2766
+ }
2767
+ } else if (hasNuxtWeb) {
2768
+ const authWebNuxtSrc = path.join(PKG_ROOT, `templates/auth/${authProvider}/web/nuxt`);
2769
+ if (await fs.pathExists(authWebNuxtSrc)) await processAndCopyFiles("**/*", authWebNuxtSrc, webAppDir, context);
2770
+ } else if (hasSvelteWeb) {
2771
+ const authWebSvelteSrc = path.join(PKG_ROOT, `templates/auth/${authProvider}/web/svelte`);
2772
+ if (await fs.pathExists(authWebSvelteSrc)) await processAndCopyFiles("**/*", authWebSvelteSrc, webAppDir, context);
2773
+ } else if (hasSolidWeb) {
2774
+ const authWebSolidSrc = path.join(PKG_ROOT, `templates/auth/${authProvider}/web/solid`);
2775
+ if (await fs.pathExists(authWebSolidSrc)) await processAndCopyFiles("**/*", authWebSolidSrc, webAppDir, context);
2776
+ }
2845
2777
  }
2846
- }
2847
- async function setupHusky(projectDir, linter) {
2848
- await addPackageDependency({
2849
- devDependencies: ["husky", "lint-staged"],
2850
- projectDir
2851
- });
2852
- const packageJsonPath = path.join(projectDir, "package.json");
2853
- if (await fs.pathExists(packageJsonPath)) {
2854
- const packageJson = await fs.readJson(packageJsonPath);
2855
- packageJson.scripts = {
2856
- ...packageJson.scripts,
2857
- prepare: "husky"
2858
- };
2859
- if (linter === "oxlint") packageJson["lint-staged"] = { "**/*.{js,mjs,cjs,jsx,ts,mts,cts,tsx,vue,astro,svelte}": "oxlint" };
2860
- else if (linter === "biome") packageJson["lint-staged"] = { "*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}": ["biome check --write ."] };
2861
- else packageJson["lint-staged"] = { "**/*.{js,mjs,cjs,jsx,ts,mts,cts,tsx,vue,astro,svelte}": "" };
2862
- await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
2778
+ if (hasNative && nativeAppDirExists) {
2779
+ const authNativeBaseSrc = path.join(PKG_ROOT, `templates/auth/${authProvider}/native/native-base`);
2780
+ if (await fs.pathExists(authNativeBaseSrc)) await processAndCopyFiles("**/*", authNativeBaseSrc, nativeAppDir, context);
2781
+ let nativeFrameworkAuthPath = "";
2782
+ if (hasNativeWind) nativeFrameworkAuthPath = "nativewind";
2783
+ else if (hasUnistyles) nativeFrameworkAuthPath = "unistyles";
2784
+ if (nativeFrameworkAuthPath) {
2785
+ const authNativeFrameworkSrc = path.join(PKG_ROOT, `templates/auth/${authProvider}/native/${nativeFrameworkAuthPath}`);
2786
+ if (await fs.pathExists(authNativeFrameworkSrc)) await processAndCopyFiles("**/*", authNativeFrameworkSrc, nativeAppDir, context);
2787
+ }
2863
2788
  }
2864
2789
  }
2865
- async function setupPwa(projectDir, frontends) {
2866
- const isCompatibleFrontend = frontends.some((f) => [
2867
- "react-router",
2790
+ async function setupAddonsTemplate(projectDir, context) {
2791
+ if (!context.addons || context.addons.length === 0) return;
2792
+ for (const addon of context.addons) {
2793
+ if (addon === "none") continue;
2794
+ let addonSrcDir = path.join(PKG_ROOT, `templates/addons/${addon}`);
2795
+ let addonDestDir = projectDir;
2796
+ if (addon === "pwa") {
2797
+ const webAppDir = path.join(projectDir, "apps/web");
2798
+ if (!await fs.pathExists(webAppDir)) continue;
2799
+ addonDestDir = webAppDir;
2800
+ if (context.frontend.includes("next")) addonSrcDir = path.join(PKG_ROOT, "templates/addons/pwa/apps/web/next");
2801
+ else if (context.frontend.some((f) => [
2802
+ "tanstack-router",
2803
+ "react-router",
2804
+ "solid"
2805
+ ].includes(f))) addonSrcDir = path.join(PKG_ROOT, "templates/addons/pwa/apps/web/vite");
2806
+ else continue;
2807
+ }
2808
+ if (await fs.pathExists(addonSrcDir)) await processAndCopyFiles("**/*", addonSrcDir, addonDestDir, context);
2809
+ }
2810
+ }
2811
+ async function setupExamplesTemplate(projectDir, context) {
2812
+ if (!context.examples || context.examples.length === 0 || context.examples[0] === "none") return;
2813
+ const serverAppDir = path.join(projectDir, "apps/server");
2814
+ const webAppDir = path.join(projectDir, "apps/web");
2815
+ const serverAppDirExists = await fs.pathExists(serverAppDir);
2816
+ const webAppDirExists = await fs.pathExists(webAppDir);
2817
+ const nativeAppDir = path.join(projectDir, "apps/native");
2818
+ const nativeAppDirExists = await fs.pathExists(nativeAppDir);
2819
+ const hasReactWeb = context.frontend.some((f) => [
2868
2820
  "tanstack-router",
2869
- "solid"
2821
+ "react-router",
2822
+ "tanstack-start",
2823
+ "next"
2870
2824
  ].includes(f));
2871
- if (!isCompatibleFrontend) return;
2872
- const clientPackageDir = getWebAppDir(projectDir, frontends);
2873
- if (!await fs.pathExists(clientPackageDir)) return;
2874
- await addPackageDependency({
2875
- dependencies: ["vite-plugin-pwa"],
2876
- devDependencies: ["@vite-pwa/assets-generator"],
2877
- projectDir: clientPackageDir
2878
- });
2879
- const clientPackageJsonPath = path.join(clientPackageDir, "package.json");
2880
- if (await fs.pathExists(clientPackageJsonPath)) {
2881
- const packageJson = await fs.readJson(clientPackageJsonPath);
2882
- packageJson.scripts = {
2883
- ...packageJson.scripts,
2884
- "generate-pwa-assets": "pwa-assets-generator"
2885
- };
2886
- await fs.writeJson(clientPackageJsonPath, packageJson, { spaces: 2 });
2825
+ const hasNuxtWeb = context.frontend.includes("nuxt");
2826
+ const hasSvelteWeb = context.frontend.includes("svelte");
2827
+ const hasSolidWeb = context.frontend.includes("solid");
2828
+ for (const example of context.examples) {
2829
+ if (example === "none") continue;
2830
+ const exampleBaseDir = path.join(PKG_ROOT, `templates/examples/${example}`);
2831
+ if (serverAppDirExists && context.backend !== "convex" && context.backend !== "none") {
2832
+ const exampleServerSrc = path.join(exampleBaseDir, "server");
2833
+ if (example === "ai" && context.backend === "next") {
2834
+ const aiNextServerSrc = path.join(exampleServerSrc, "next");
2835
+ if (await fs.pathExists(aiNextServerSrc)) await processAndCopyFiles("**/*", aiNextServerSrc, serverAppDir, context, false);
2836
+ }
2837
+ if (context.orm !== "none" && context.database !== "none") {
2838
+ const exampleOrmBaseSrc = path.join(exampleServerSrc, context.orm, "base");
2839
+ if (await fs.pathExists(exampleOrmBaseSrc)) await processAndCopyFiles("**/*", exampleOrmBaseSrc, serverAppDir, context, false);
2840
+ const exampleDbSchemaSrc = path.join(exampleServerSrc, context.orm, context.database);
2841
+ if (await fs.pathExists(exampleDbSchemaSrc)) await processAndCopyFiles("**/*", exampleDbSchemaSrc, serverAppDir, context, false);
2842
+ }
2843
+ }
2844
+ if (webAppDirExists) {
2845
+ if (hasReactWeb) {
2846
+ const exampleWebSrc = path.join(exampleBaseDir, "web/react");
2847
+ if (await fs.pathExists(exampleWebSrc)) {
2848
+ if (example === "ai") {
2849
+ const exampleWebBaseSrc = path.join(exampleWebSrc, "base");
2850
+ if (await fs.pathExists(exampleWebBaseSrc)) await processAndCopyFiles("**/*", exampleWebBaseSrc, webAppDir, context, false);
2851
+ }
2852
+ const reactFramework = context.frontend.find((f) => [
2853
+ "next",
2854
+ "react-router",
2855
+ "tanstack-router",
2856
+ "tanstack-start"
2857
+ ].includes(f));
2858
+ if (reactFramework) {
2859
+ const exampleWebFrameworkSrc = path.join(exampleWebSrc, reactFramework);
2860
+ if (await fs.pathExists(exampleWebFrameworkSrc)) await processAndCopyFiles("**/*", exampleWebFrameworkSrc, webAppDir, context, false);
2861
+ }
2862
+ }
2863
+ } else if (hasNuxtWeb) {
2864
+ const exampleWebNuxtSrc = path.join(exampleBaseDir, "web/nuxt");
2865
+ if (await fs.pathExists(exampleWebNuxtSrc)) await processAndCopyFiles("**/*", exampleWebNuxtSrc, webAppDir, context, false);
2866
+ } else if (hasSvelteWeb) {
2867
+ const exampleWebSvelteSrc = path.join(exampleBaseDir, "web/svelte");
2868
+ if (await fs.pathExists(exampleWebSvelteSrc)) await processAndCopyFiles("**/*", exampleWebSvelteSrc, webAppDir, context, false);
2869
+ } else if (hasSolidWeb) {
2870
+ const exampleWebSolidSrc = path.join(exampleBaseDir, "web/solid");
2871
+ if (await fs.pathExists(exampleWebSolidSrc)) await processAndCopyFiles("**/*", exampleWebSolidSrc, webAppDir, context, false);
2872
+ }
2873
+ }
2874
+ if (nativeAppDirExists) {
2875
+ const hasNativeWind = context.frontend.includes("native-nativewind");
2876
+ const hasUnistyles = context.frontend.includes("native-unistyles");
2877
+ if (hasNativeWind || hasUnistyles) {
2878
+ let nativeFramework = "";
2879
+ if (hasNativeWind) nativeFramework = "nativewind";
2880
+ else if (hasUnistyles) nativeFramework = "unistyles";
2881
+ const exampleNativeSrc = path.join(exampleBaseDir, `native/${nativeFramework}`);
2882
+ if (await fs.pathExists(exampleNativeSrc)) await processAndCopyFiles("**/*", exampleNativeSrc, nativeAppDir, context, false);
2883
+ }
2884
+ }
2887
2885
  }
2888
- const viteConfigTs = path.join(clientPackageDir, "vite.config.ts");
2889
- if (await fs.pathExists(viteConfigTs)) await addPwaToViteConfig(viteConfigTs, path.basename(projectDir));
2890
2886
  }
2891
- async function setupOxlint(projectDir, packageManager) {
2892
- await addPackageDependency({
2893
- devDependencies: ["oxlint"],
2894
- projectDir
2895
- });
2896
- const packageJsonPath = path.join(projectDir, "package.json");
2897
- if (await fs.pathExists(packageJsonPath)) {
2898
- const packageJson = await fs.readJson(packageJsonPath);
2899
- packageJson.scripts = {
2900
- ...packageJson.scripts,
2901
- check: "oxlint"
2902
- };
2903
- await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
2887
+ async function handleExtras(projectDir, context) {
2888
+ const extrasDir = path.join(PKG_ROOT, "templates/extras");
2889
+ const hasNativeWind = context.frontend.includes("native-nativewind");
2890
+ const hasUnistyles = context.frontend.includes("native-unistyles");
2891
+ const hasNative = hasNativeWind || hasUnistyles;
2892
+ if (context.packageManager === "pnpm") {
2893
+ const pnpmWorkspaceSrc = path.join(extrasDir, "pnpm-workspace.yaml");
2894
+ const pnpmWorkspaceDest = path.join(projectDir, "pnpm-workspace.yaml");
2895
+ if (await fs.pathExists(pnpmWorkspaceSrc)) await fs.copy(pnpmWorkspaceSrc, pnpmWorkspaceDest);
2904
2896
  }
2905
- const oxlintInitCommand = getPackageExecutionCommand(packageManager, "oxlint@latest --init");
2906
- await execa(oxlintInitCommand, {
2907
- cwd: projectDir,
2908
- env: { CI: "true" },
2909
- shell: true
2910
- });
2911
- }
2912
-
2913
- //#endregion
2914
- //#region src/helpers/core/detect-project-config.ts
2915
- async function detectProjectConfig(projectDir) {
2916
- try {
2917
- const btsConfig = await readBtsConfig(projectDir);
2918
- if (btsConfig) return {
2919
- projectDir,
2920
- projectName: path.basename(projectDir),
2921
- database: btsConfig.database,
2922
- orm: btsConfig.orm,
2923
- backend: btsConfig.backend,
2924
- runtime: btsConfig.runtime,
2925
- frontend: btsConfig.frontend,
2926
- addons: btsConfig.addons,
2927
- examples: btsConfig.examples,
2928
- auth: btsConfig.auth,
2929
- packageManager: btsConfig.packageManager,
2930
- dbSetup: btsConfig.dbSetup,
2931
- api: btsConfig.api,
2932
- webDeploy: btsConfig.webDeploy,
2933
- serverDeploy: btsConfig.serverDeploy
2934
- };
2935
- return null;
2936
- } catch (_error) {
2937
- return null;
2897
+ if (context.packageManager === "bun") {
2898
+ const bunfigSrc = path.join(extrasDir, "bunfig.toml.hbs");
2899
+ if (await fs.pathExists(bunfigSrc)) await processAndCopyFiles("bunfig.toml.hbs", extrasDir, projectDir, context);
2938
2900
  }
2939
- }
2940
- async function isBetterTStackProject(projectDir) {
2941
- try {
2942
- return await fs.pathExists(path.join(projectDir, "bts.jsonc"));
2943
- } catch (_error) {
2944
- return false;
2901
+ if (context.packageManager === "pnpm" && (hasNative || context.frontend.includes("nuxt"))) {
2902
+ const npmrcTemplateSrc = path.join(extrasDir, "_npmrc.hbs");
2903
+ if (await fs.pathExists(npmrcTemplateSrc)) await processAndCopyFiles("_npmrc.hbs", extrasDir, projectDir, context);
2945
2904
  }
2946
2905
  }
2947
-
2948
- //#endregion
2949
- //#region src/helpers/core/install-dependencies.ts
2950
- async function installDependencies({ projectDir, packageManager }) {
2951
- const s = spinner();
2952
- try {
2953
- s.start(`Running ${packageManager} install...`);
2954
- await $({
2955
- cwd: projectDir,
2956
- stderr: "inherit"
2957
- })`${packageManager} install`;
2958
- s.stop("Dependencies installed successfully");
2959
- } catch (error) {
2960
- s.stop(pc.red("Failed to install dependencies"));
2961
- if (error instanceof Error) consola.error(pc.red(`Installation error: ${error.message}`));
2906
+ async function setupDockerComposeTemplates(projectDir, context) {
2907
+ if (context.dbSetup !== "docker" || context.database === "none") return;
2908
+ const serverAppDir = path.join(projectDir, "apps/server");
2909
+ const dockerSrcDir = path.join(PKG_ROOT, `templates/db-setup/docker-compose/${context.database}`);
2910
+ if (await fs.pathExists(dockerSrcDir)) await processAndCopyFiles("**/*", dockerSrcDir, serverAppDir, context);
2911
+ }
2912
+ async function setupDeploymentTemplates(projectDir, context) {
2913
+ if (context.webDeploy === "alchemy" || context.serverDeploy === "alchemy") if (context.webDeploy === "alchemy" && context.serverDeploy === "alchemy") {
2914
+ const alchemyTemplateSrc = path.join(PKG_ROOT, "templates/deploy/alchemy");
2915
+ if (await fs.pathExists(alchemyTemplateSrc)) {
2916
+ await processAndCopyFiles("alchemy.run.ts.hbs", alchemyTemplateSrc, projectDir, context);
2917
+ const serverAppDir = path.join(projectDir, "apps/server");
2918
+ if (await fs.pathExists(serverAppDir)) await processAndCopyFiles("env.d.ts.hbs", alchemyTemplateSrc, serverAppDir, context);
2919
+ }
2920
+ } else {
2921
+ if (context.webDeploy === "alchemy") {
2922
+ const alchemyTemplateSrc = path.join(PKG_ROOT, "templates/deploy/alchemy");
2923
+ const webAppDir = path.join(projectDir, "apps/web");
2924
+ if (await fs.pathExists(alchemyTemplateSrc) && await fs.pathExists(webAppDir)) await processAndCopyFiles("alchemy.run.ts.hbs", alchemyTemplateSrc, webAppDir, context);
2925
+ }
2926
+ if (context.serverDeploy === "alchemy") {
2927
+ const alchemyTemplateSrc = path.join(PKG_ROOT, "templates/deploy/alchemy");
2928
+ const serverAppDir = path.join(projectDir, "apps/server");
2929
+ if (await fs.pathExists(alchemyTemplateSrc) && await fs.pathExists(serverAppDir)) {
2930
+ await processAndCopyFiles("alchemy.run.ts.hbs", alchemyTemplateSrc, serverAppDir, context);
2931
+ await processAndCopyFiles("env.d.ts.hbs", alchemyTemplateSrc, serverAppDir, context);
2932
+ }
2933
+ }
2934
+ }
2935
+ if (context.webDeploy !== "none" && context.webDeploy !== "alchemy") {
2936
+ const webAppDir = path.join(projectDir, "apps/web");
2937
+ if (await fs.pathExists(webAppDir)) {
2938
+ const frontends = context.frontend;
2939
+ const templateMap = {
2940
+ "tanstack-router": "react/tanstack-router",
2941
+ "tanstack-start": "react/tanstack-start",
2942
+ "react-router": "react/react-router",
2943
+ solid: "solid",
2944
+ next: "react/next",
2945
+ nuxt: "nuxt",
2946
+ svelte: "svelte"
2947
+ };
2948
+ for (const f of frontends) if (templateMap[f]) {
2949
+ const deployTemplateSrc = path.join(PKG_ROOT, `templates/deploy/${context.webDeploy}/web/${templateMap[f]}`);
2950
+ if (await fs.pathExists(deployTemplateSrc)) await processAndCopyFiles("**/*", deployTemplateSrc, webAppDir, context);
2951
+ }
2952
+ }
2953
+ }
2954
+ if (context.serverDeploy !== "none" && context.serverDeploy !== "alchemy") {
2955
+ const serverAppDir = path.join(projectDir, "apps/server");
2956
+ if (await fs.pathExists(serverAppDir)) {
2957
+ const deployTemplateSrc = path.join(PKG_ROOT, `templates/deploy/${context.serverDeploy}/server`);
2958
+ if (await fs.pathExists(deployTemplateSrc)) await processAndCopyFiles("**/*", deployTemplateSrc, serverAppDir, context);
2959
+ }
2962
2960
  }
2963
2961
  }
2964
2962