create-better-t-stack 3.13.2-dev.c1daaba → 3.13.2-dev.cc9652f

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { o as createBtsCli } from "./src-CBEgZNc9.mjs";
2
+ import { o as createBtsCli } from "./src-4O1Y0B1P.mjs";
3
3
 
4
4
  //#region src/cli.ts
5
5
  createBtsCli().run();
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import { a as create, c as docs, d as sponsors, i as builder, l as generateVirtualProject, n as TEMPLATE_COUNT, o as createBtsCli, r as VirtualFileSystem, s as createVirtual, t as EMBEDDED_TEMPLATES, u as router } from "./src-CBEgZNc9.mjs";
2
+ import { a as create, c as docs, d as sponsors, i as builder, l as generateVirtualProject, n as TEMPLATE_COUNT, o as createBtsCli, r as VirtualFileSystem, s as createVirtual, t as EMBEDDED_TEMPLATES, u as router } from "./src-4O1Y0B1P.mjs";
3
3
 
4
4
  export { EMBEDDED_TEMPLATES, TEMPLATE_COUNT, VirtualFileSystem, builder, create, createBtsCli, createVirtual, docs, generateVirtualProject, router, sponsors };
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { t as __reExport } from "./chunk-DPg_XC7m.mjs";
3
- import { autocompleteMultiselect, cancel, confirm, group, intro, isCancel, log, multiselect, outro, select, spinner, text } from "@clack/prompts";
3
+ import { cancel, confirm, group, intro, isCancel, log, multiselect, outro, select, spinner, text } from "@clack/prompts";
4
4
  import { createRouterClient, os } from "@orpc/server";
5
5
  import pc from "picocolors";
6
6
  import { createCli } from "trpc-cli";
@@ -10,12 +10,11 @@ import fs from "fs-extra";
10
10
  import path from "node:path";
11
11
  import { fileURLToPath } from "node:url";
12
12
  import { EMBEDDED_TEMPLATES, EMBEDDED_TEMPLATES as EMBEDDED_TEMPLATES$1, TEMPLATE_COUNT, VirtualFileSystem, dependencyVersionMap, generateVirtualProject, generateVirtualProject as generateVirtualProject$1 } from "@better-t-stack/template-generator";
13
- import { ConfirmPrompt, GroupMultiSelectPrompt, MultiSelectPrompt, SelectPrompt, isCancel as isCancel$1 } from "@clack/core";
14
13
  import { AsyncLocalStorage } from "node:async_hooks";
14
+ import { ConfirmPrompt, GroupMultiSelectPrompt, MultiSelectPrompt, SelectPrompt, isCancel as isCancel$1 } from "@clack/core";
15
15
  import gradient from "gradient-string";
16
16
  import { writeTreeToFilesystem } from "@better-t-stack/template-generator/fs-writer";
17
17
  import { $, execa } from "execa";
18
- import { IndentationText, Node, Project, QuoteKind, SyntaxKind } from "ts-morph";
19
18
  import * as JSONC from "jsonc-parser";
20
19
  import { format } from "oxfmt";
21
20
  import os$1 from "node:os";
@@ -91,6 +90,59 @@ const ADDON_COMPATIBILITY = {
91
90
  none: []
92
91
  };
93
92
 
93
+ //#endregion
94
+ //#region src/utils/context.ts
95
+ const cliStorage = new AsyncLocalStorage();
96
+ function defaultContext() {
97
+ return {
98
+ navigation: {
99
+ isFirstPrompt: false,
100
+ lastPromptShownUI: false
101
+ },
102
+ silent: false,
103
+ verbose: false
104
+ };
105
+ }
106
+ function getContext() {
107
+ const ctx = cliStorage.getStore();
108
+ if (!ctx) return defaultContext();
109
+ return ctx;
110
+ }
111
+ function tryGetContext() {
112
+ return cliStorage.getStore();
113
+ }
114
+ function isSilent() {
115
+ return getContext().silent;
116
+ }
117
+ function isFirstPrompt() {
118
+ return getContext().navigation.isFirstPrompt;
119
+ }
120
+ function didLastPromptShowUI() {
121
+ return getContext().navigation.lastPromptShownUI;
122
+ }
123
+ function setIsFirstPrompt$1(value) {
124
+ const ctx = tryGetContext();
125
+ if (ctx) ctx.navigation.isFirstPrompt = value;
126
+ }
127
+ function setLastPromptShownUI(value) {
128
+ const ctx = tryGetContext();
129
+ if (ctx) ctx.navigation.lastPromptShownUI = value;
130
+ }
131
+ async function runWithContextAsync(options, fn) {
132
+ const ctx = {
133
+ navigation: {
134
+ isFirstPrompt: false,
135
+ lastPromptShownUI: false
136
+ },
137
+ silent: options.silent ?? false,
138
+ verbose: options.verbose ?? false,
139
+ projectDir: options.projectDir,
140
+ projectName: options.projectName,
141
+ packageManager: options.packageManager
142
+ };
143
+ return cliStorage.run(ctx, fn);
144
+ }
145
+
94
146
  //#endregion
95
147
  //#region src/utils/errors.ts
96
148
  var UserCancelledError = class extends Error {
@@ -106,17 +158,20 @@ var CLIError = class extends Error {
106
158
  }
107
159
  };
108
160
  function exitWithError(message) {
161
+ if (isSilent()) throw new CLIError(message);
109
162
  consola.error(pc.red(message));
110
- throw new CLIError(message);
163
+ process.exit(1);
111
164
  }
112
165
  function exitCancelled(message = "Operation cancelled") {
166
+ if (isSilent()) throw new UserCancelledError(message);
113
167
  cancel(pc.red(message));
114
- throw new UserCancelledError(message);
168
+ process.exit(1);
115
169
  }
116
170
  function handleError(error, fallbackMessage) {
117
171
  const message = error instanceof Error ? error.message : fallbackMessage || String(error);
172
+ if (isSilent()) throw error instanceof Error ? error : new Error(message);
118
173
  consola.error(pc.red(message));
119
- throw error instanceof Error ? error : new Error(message);
174
+ process.exit(1);
120
175
  }
121
176
 
122
177
  //#endregion
@@ -262,64 +317,8 @@ function validateExamplesCompatibility(examples, backend, database, frontend, ap
262
317
  }
263
318
  }
264
319
 
265
- //#endregion
266
- //#region src/utils/context.ts
267
- const cliStorage = new AsyncLocalStorage();
268
- function defaultContext() {
269
- return {
270
- navigation: {
271
- isFirstPrompt: false,
272
- lastPromptShownUI: false
273
- },
274
- silent: false,
275
- verbose: false
276
- };
277
- }
278
- function getContext() {
279
- const ctx = cliStorage.getStore();
280
- if (!ctx) return defaultContext();
281
- return ctx;
282
- }
283
- function tryGetContext() {
284
- return cliStorage.getStore();
285
- }
286
- function isSilent() {
287
- return getContext().silent;
288
- }
289
- function isFirstPrompt() {
290
- return getContext().navigation.isFirstPrompt;
291
- }
292
- function didLastPromptShowUI() {
293
- return getContext().navigation.lastPromptShownUI;
294
- }
295
- function setIsFirstPrompt$1(value) {
296
- const ctx = tryGetContext();
297
- if (ctx) ctx.navigation.isFirstPrompt = value;
298
- }
299
- function setLastPromptShownUI(value) {
300
- const ctx = tryGetContext();
301
- if (ctx) ctx.navigation.lastPromptShownUI = value;
302
- }
303
- async function runWithContextAsync(options, fn) {
304
- const ctx = {
305
- navigation: {
306
- isFirstPrompt: false,
307
- lastPromptShownUI: false
308
- },
309
- silent: options.silent ?? false,
310
- verbose: options.verbose ?? false,
311
- projectDir: options.projectDir,
312
- projectName: options.projectName,
313
- packageManager: options.packageManager
314
- };
315
- return cliStorage.run(ctx, fn);
316
- }
317
-
318
320
  //#endregion
319
321
  //#region src/utils/navigation.ts
320
- /**
321
- * Navigation symbols and utilities for prompt navigation
322
- */
323
322
  const GO_BACK_SYMBOL = Symbol("clack:goBack");
324
323
  function isGoBack(value) {
325
324
  return value === GO_BACK_SYMBOL;
@@ -615,29 +614,29 @@ function getAddonDisplay(addon) {
615
614
  };
616
615
  }
617
616
  const ADDON_GROUPS = {
618
- Documentation: ["starlight", "fumadocs"],
619
- Linting: [
617
+ Tooling: [
618
+ "turborepo",
620
619
  "biome",
621
620
  "oxlint",
622
- "ultracite"
621
+ "ultracite",
622
+ "husky"
623
623
  ],
624
- Other: [
625
- "ruler",
624
+ Documentation: ["starlight", "fumadocs"],
625
+ Extensions: [
626
626
  "pwa",
627
627
  "tauri",
628
- "husky",
629
628
  "opentui",
630
629
  "wxt",
631
- "turborepo"
630
+ "ruler"
632
631
  ]
633
632
  };
634
633
  async function getAddonsChoice(addons, frontends, auth) {
635
634
  if (addons !== void 0) return addons;
636
635
  const allAddons = types_exports.AddonsSchema.options.filter((addon) => addon !== "none");
637
636
  const groupedOptions = {
637
+ Tooling: [],
638
638
  Documentation: [],
639
- Linting: [],
640
- Other: []
639
+ Extensions: []
641
640
  };
642
641
  const frontendsArray = frontends || [];
643
642
  for (const addon of allAddons) {
@@ -649,9 +648,9 @@ async function getAddonsChoice(addons, frontends, auth) {
649
648
  label,
650
649
  hint
651
650
  };
652
- if (ADDON_GROUPS.Documentation.includes(addon)) groupedOptions.Documentation.push(option);
653
- else if (ADDON_GROUPS.Linting.includes(addon)) groupedOptions.Linting.push(option);
654
- else if (ADDON_GROUPS.Other.includes(addon)) groupedOptions.Other.push(option);
651
+ if (ADDON_GROUPS.Tooling.includes(addon)) groupedOptions.Tooling.push(option);
652
+ else if (ADDON_GROUPS.Documentation.includes(addon)) groupedOptions.Documentation.push(option);
653
+ else if (ADDON_GROUPS.Extensions.includes(addon)) groupedOptions.Extensions.push(option);
655
654
  }
656
655
  Object.keys(groupedOptions).forEach((group$1) => {
657
656
  if (groupedOptions[group$1].length === 0) delete groupedOptions[group$1];
@@ -2028,63 +2027,6 @@ function validateConfigCompatibility(config, providedFlags, options) {
2028
2027
  else validateConfigForProgrammaticUse(config);
2029
2028
  }
2030
2029
 
2031
- //#endregion
2032
- //#region src/utils/ts-morph.ts
2033
- const tsProject = new Project({
2034
- useInMemoryFileSystem: false,
2035
- skipAddingFilesFromTsConfig: true,
2036
- manipulationSettings: {
2037
- quoteKind: QuoteKind.Single,
2038
- indentationText: IndentationText.TwoSpaces
2039
- }
2040
- });
2041
- function ensureArrayProperty(obj, name) {
2042
- return obj.getProperty(name)?.getFirstDescendantByKind(SyntaxKind.ArrayLiteralExpression) ?? obj.addPropertyAssignment({
2043
- name,
2044
- initializer: "[]"
2045
- }).getFirstDescendantByKindOrThrow(SyntaxKind.ArrayLiteralExpression);
2046
- }
2047
-
2048
- //#endregion
2049
- //#region src/utils/better-auth-plugin-setup.ts
2050
- async function setupBetterAuthPlugins(projectDir, config) {
2051
- const authIndexPath = `${projectDir}/packages/auth/src/index.ts`;
2052
- const authIndexFile = tsProject.addSourceFileAtPath(authIndexPath);
2053
- if (!authIndexFile) return;
2054
- const pluginsToAdd = [];
2055
- const importsToAdd = [];
2056
- if (config.backend === "self" && config.frontend?.includes("tanstack-start")) {
2057
- pluginsToAdd.push("tanstackStartCookies()");
2058
- importsToAdd.push("import { tanstackStartCookies } from \"better-auth/tanstack-start\";");
2059
- }
2060
- if (config.backend === "self" && config.frontend?.includes("next")) {
2061
- pluginsToAdd.push("nextCookies()");
2062
- importsToAdd.push("import { nextCookies } from \"better-auth/next-js\";");
2063
- }
2064
- if (config.frontend?.includes("native-bare") || config.frontend?.includes("native-uniwind") || config.frontend?.includes("native-unistyles")) {
2065
- pluginsToAdd.push("expo()");
2066
- importsToAdd.push("import { expo } from \"@better-auth/expo\";");
2067
- }
2068
- if (pluginsToAdd.length === 0) return;
2069
- importsToAdd.forEach((importStatement) => {
2070
- if (!authIndexFile.getImportDeclaration((declaration) => declaration.getModuleSpecifierValue().includes(importStatement.split("\"")[1]))) authIndexFile.insertImportDeclaration(0, {
2071
- moduleSpecifier: importStatement.split("\"")[1],
2072
- namedImports: [importStatement.split("{")[1].split("}")[0].trim()]
2073
- });
2074
- });
2075
- const betterAuthCall = authIndexFile.getDescendantsOfKind(SyntaxKind.CallExpression).find((call) => call.getExpression().getText() === "betterAuth");
2076
- if (betterAuthCall) {
2077
- const configObject = betterAuthCall.getArguments()[0];
2078
- if (configObject && configObject.getKind() === SyntaxKind.ObjectLiteralExpression) {
2079
- const pluginsArray = ensureArrayProperty(configObject.asKindOrThrow(SyntaxKind.ObjectLiteralExpression), "plugins");
2080
- pluginsToAdd.forEach((plugin) => {
2081
- pluginsArray.addElement(plugin);
2082
- });
2083
- }
2084
- }
2085
- authIndexFile.save();
2086
- }
2087
-
2088
2030
  //#endregion
2089
2031
  //#region src/utils/bts-config.ts
2090
2032
  const BTS_CONFIG_FILE = "bts.jsonc";
@@ -2173,22 +2115,30 @@ async function formatProject(projectDir) {
2173
2115
  }
2174
2116
 
2175
2117
  //#endregion
2176
- //#region src/utils/package-runner.ts
2177
- /**
2178
- * Returns the appropriate command for running a package without installing it globally,
2179
- * based on the selected package manager.
2180
- *
2181
- * @param packageManager - The selected package manager (e.g., 'npm', 'yarn', 'pnpm', 'bun').
2182
- * @param commandWithArgs - The command to run, including arguments (e.g., "prisma generate --schema=./prisma/schema.prisma").
2183
- * @returns The full command string (e.g., "npx prisma generate --schema=./prisma/schema.prisma").
2184
- */
2185
- function getPackageExecutionCommand(packageManager, commandWithArgs) {
2186
- switch (packageManager) {
2187
- case "pnpm": return `pnpm dlx ${commandWithArgs}`;
2188
- case "bun": return `bunx ${commandWithArgs}`;
2189
- default: return `npx ${commandWithArgs}`;
2118
+ //#region src/utils/add-package-deps.ts
2119
+ const addPackageDependency = async (opts) => {
2120
+ const { dependencies = [], devDependencies = [], customDependencies = {}, customDevDependencies = {}, projectDir } = opts;
2121
+ const pkgJsonPath = path.join(projectDir, "package.json");
2122
+ const pkgJson = await fs.readJson(pkgJsonPath);
2123
+ if (!pkgJson.dependencies) pkgJson.dependencies = {};
2124
+ if (!pkgJson.devDependencies) pkgJson.devDependencies = {};
2125
+ for (const pkgName of dependencies) {
2126
+ const version = dependencyVersionMap[pkgName];
2127
+ if (version) pkgJson.dependencies[pkgName] = version;
2128
+ else console.warn(`Warning: Dependency ${pkgName} not found in version map.`);
2190
2129
  }
2191
- }
2130
+ for (const pkgName of devDependencies) {
2131
+ const version = dependencyVersionMap[pkgName];
2132
+ if (version) pkgJson.devDependencies[pkgName] = version;
2133
+ else console.warn(`Warning: Dev dependency ${pkgName} not found in version map.`);
2134
+ }
2135
+ for (const [pkgName, version] of Object.entries(customDependencies)) pkgJson.dependencies[pkgName] = version;
2136
+ for (const [pkgName, version] of Object.entries(customDevDependencies)) pkgJson.devDependencies[pkgName] = version;
2137
+ await fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 });
2138
+ };
2139
+
2140
+ //#endregion
2141
+ //#region src/utils/package-runner.ts
2192
2142
  /**
2193
2143
  * Returns the command and arguments as an array for use with execa's $ template syntax.
2194
2144
  * This avoids the need for shell: true and provides better escaping.
@@ -2298,115 +2248,35 @@ async function setupFumadocs(config) {
2298
2248
 
2299
2249
  //#endregion
2300
2250
  //#region src/helpers/addons/oxlint-setup.ts
2301
- /**
2302
- * Oxlint setup - CLI-only operations
2303
- * NOTE: Dependencies are handled by template-generator's addons-deps.ts processor
2304
- * This file only handles external CLI initialization (oxlint --init, oxfmt --init)
2305
- */
2306
- async function setupOxlint(config) {
2251
+ async function setupOxlint(projectDir, packageManager) {
2252
+ await addPackageDependency({
2253
+ devDependencies: ["oxlint", "oxfmt"],
2254
+ projectDir
2255
+ });
2256
+ const packageJsonPath = path.join(projectDir, "package.json");
2257
+ if (await fs.pathExists(packageJsonPath)) {
2258
+ const packageJson = await fs.readJson(packageJsonPath);
2259
+ packageJson.scripts = {
2260
+ ...packageJson.scripts,
2261
+ check: "oxlint && oxfmt --write"
2262
+ };
2263
+ await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
2264
+ }
2307
2265
  const s = spinner();
2266
+ const oxlintArgs = getPackageExecutionArgs(packageManager, "oxlint@latest --init");
2308
2267
  s.start("Initializing oxlint and oxfmt...");
2309
- const oxlintArgs = getPackageExecutionArgs(config.packageManager, "oxlint@latest --init");
2310
2268
  await $({
2311
- cwd: config.projectDir,
2269
+ cwd: projectDir,
2312
2270
  env: { CI: "true" }
2313
2271
  })`${oxlintArgs}`;
2314
- const oxfmtArgs = getPackageExecutionArgs(config.packageManager, "oxfmt@latest --init");
2272
+ const oxfmtArgs = getPackageExecutionArgs(packageManager, "oxfmt@latest --init");
2315
2273
  await $({
2316
- cwd: config.projectDir,
2274
+ cwd: projectDir,
2317
2275
  env: { CI: "true" }
2318
2276
  })`${oxfmtArgs}`;
2319
2277
  s.stop("oxlint and oxfmt initialized successfully!");
2320
2278
  }
2321
2279
 
2322
- //#endregion
2323
- //#region src/helpers/addons/ruler-setup.ts
2324
- async function setupRuler(config) {
2325
- const { packageManager, projectDir } = config;
2326
- try {
2327
- log.info("Setting up Ruler...");
2328
- const rulerDir = path.join(projectDir, ".ruler");
2329
- if (!await fs.pathExists(rulerDir)) {
2330
- log.error(pc.red("Ruler template directory not found. Please ensure ruler addon is properly installed."));
2331
- return;
2332
- }
2333
- const selectedEditors = await autocompleteMultiselect({
2334
- message: "Select AI assistants for Ruler",
2335
- options: Object.entries({
2336
- amp: { label: "AMP" },
2337
- copilot: { label: "GitHub Copilot" },
2338
- claude: { label: "Claude Code" },
2339
- codex: { label: "OpenAI Codex CLI" },
2340
- cursor: { label: "Cursor" },
2341
- windsurf: { label: "Windsurf" },
2342
- cline: { label: "Cline" },
2343
- aider: { label: "Aider" },
2344
- firebase: { label: "Firebase Studio" },
2345
- "gemini-cli": { label: "Gemini CLI" },
2346
- junie: { label: "Junie" },
2347
- kilocode: { label: "Kilo Code" },
2348
- opencode: { label: "OpenCode" },
2349
- crush: { label: "Crush" },
2350
- zed: { label: "Zed" },
2351
- qwen: { label: "Qwen" },
2352
- amazonqcli: { label: "Amazon Q CLI" },
2353
- augmentcode: { label: "AugmentCode" },
2354
- firebender: { label: "Firebender" },
2355
- goose: { label: "Goose" },
2356
- jules: { label: "Jules" },
2357
- kiro: { label: "Kiro" },
2358
- openhands: { label: "Open Hands" },
2359
- roo: { label: "RooCode" },
2360
- trae: { label: "Trae AI" },
2361
- warp: { label: "Warp" }
2362
- }).map(([key, v]) => ({
2363
- value: key,
2364
- label: v.label
2365
- })),
2366
- required: false
2367
- });
2368
- if (isCancel(selectedEditors)) return exitCancelled("Operation cancelled");
2369
- if (selectedEditors.length === 0) {
2370
- log.info("No AI assistants selected. To apply rules later, run:");
2371
- log.info(pc.cyan(`${getPackageExecutionCommand(packageManager, "@intellectronica/ruler@latest apply --local-only")}`));
2372
- return;
2373
- }
2374
- const configFile = path.join(rulerDir, "ruler.toml");
2375
- let updatedConfig = await fs.readFile(configFile, "utf-8");
2376
- const defaultAgentsLine = `default_agents = [${selectedEditors.map((editor) => `"${editor}"`).join(", ")}]`;
2377
- updatedConfig = updatedConfig.replace(/default_agents = \[\]/, defaultAgentsLine);
2378
- await fs.writeFile(configFile, updatedConfig);
2379
- await addRulerScriptToPackageJson(projectDir, packageManager);
2380
- const s = spinner();
2381
- s.start("Applying rules with Ruler...");
2382
- try {
2383
- const rulerApplyArgs = getPackageExecutionArgs(packageManager, `@intellectronica/ruler@latest apply --agents ${selectedEditors.join(",")} --local-only`);
2384
- await $({
2385
- cwd: projectDir,
2386
- env: { CI: "true" }
2387
- })`${rulerApplyArgs}`;
2388
- s.stop("Applied rules with Ruler");
2389
- } catch {
2390
- s.stop(pc.red("Failed to apply rules"));
2391
- }
2392
- } catch (error) {
2393
- log.error(pc.red("Failed to set up Ruler"));
2394
- if (error instanceof Error) console.error(pc.red(error.message));
2395
- }
2396
- }
2397
- async function addRulerScriptToPackageJson(projectDir, packageManager) {
2398
- const rootPackageJsonPath = path.join(projectDir, "package.json");
2399
- if (!await fs.pathExists(rootPackageJsonPath)) {
2400
- log.warn("Root package.json not found, skipping ruler:apply script addition");
2401
- return;
2402
- }
2403
- const packageJson = await fs.readJson(rootPackageJsonPath);
2404
- if (!packageJson.scripts) packageJson.scripts = {};
2405
- const rulerApplyCommand = getPackageExecutionCommand(packageManager, "@intellectronica/ruler@latest apply --local-only");
2406
- packageJson.scripts["ruler:apply"] = rulerApplyCommand;
2407
- await fs.writeJson(rootPackageJsonPath, packageJson, { spaces: 2 });
2408
- }
2409
-
2410
2280
  //#endregion
2411
2281
  //#region src/helpers/addons/starlight-setup.ts
2412
2282
  async function setupStarlight(config) {
@@ -2522,11 +2392,20 @@ async function setupTui(config) {
2522
2392
 
2523
2393
  //#endregion
2524
2394
  //#region src/helpers/addons/ultracite-setup.ts
2525
- /**
2526
- * Ultracite setup - CLI-only operations
2527
- * NOTE: Dependencies (husky, lint-staged) are handled by template-generator's addons-deps.ts
2528
- * This file handles interactive prompts and external CLI initialization
2529
- */
2395
+ const LINTERS = {
2396
+ biome: {
2397
+ label: "Biome",
2398
+ hint: "Fast formatter and linter"
2399
+ },
2400
+ eslint: {
2401
+ label: "ESLint",
2402
+ hint: "Traditional JavaScript linter"
2403
+ },
2404
+ oxlint: {
2405
+ label: "Oxlint",
2406
+ hint: "Oxidation compiler linter"
2407
+ }
2408
+ };
2530
2409
  const EDITORS = {
2531
2410
  vscode: { label: "VS Code" },
2532
2411
  cursor: { label: "Cursor" },
@@ -2565,11 +2444,6 @@ const HOOKS = {
2565
2444
  cursor: { label: "Cursor" },
2566
2445
  windsurf: { label: "Windsurf" }
2567
2446
  };
2568
- const LINTERS = {
2569
- biome: { label: "Biome (recommended)" },
2570
- oxlint: { label: "OxLint" },
2571
- eslint: { label: "ESLint" }
2572
- };
2573
2447
  function getFrameworksFromFrontend(frontend) {
2574
2448
  const frameworkMap = {
2575
2449
  "tanstack-router": "react",
@@ -2596,7 +2470,8 @@ async function setupUltracite(config, hasHusky) {
2596
2470
  message: "Choose linter/formatter",
2597
2471
  options: Object.entries(LINTERS).map(([key, linter$1]) => ({
2598
2472
  value: key,
2599
- label: linter$1.label
2473
+ label: linter$1.label,
2474
+ hint: linter$1.hint
2600
2475
  })),
2601
2476
  initialValue: "biome"
2602
2477
  }),
@@ -2608,7 +2483,7 @@ async function setupUltracite(config, hasHusky) {
2608
2483
  })),
2609
2484
  required: true
2610
2485
  }),
2611
- agents: () => autocompleteMultiselect({
2486
+ agents: () => multiselect({
2612
2487
  message: "Choose agents",
2613
2488
  options: Object.entries(AGENTS).map(([key, agent]) => ({
2614
2489
  value: key,
@@ -2616,8 +2491,8 @@ async function setupUltracite(config, hasHusky) {
2616
2491
  })),
2617
2492
  required: true
2618
2493
  }),
2619
- hooks: () => autocompleteMultiselect({
2620
- message: "Choose hooks (optional)",
2494
+ hooks: () => multiselect({
2495
+ message: "Choose hooks",
2621
2496
  options: Object.entries(HOOKS).map(([key, hook]) => ({
2622
2497
  value: key,
2623
2498
  label: hook.label
@@ -2722,7 +2597,7 @@ async function setupWxt(config) {
2722
2597
  //#endregion
2723
2598
  //#region src/helpers/addons/addons-setup.ts
2724
2599
  async function setupAddons(config) {
2725
- const { addons, frontend } = config;
2600
+ const { addons, frontend, projectDir } = config;
2726
2601
  const hasReactWebFrontend = frontend.includes("react-router") || frontend.includes("tanstack-router") || frontend.includes("next");
2727
2602
  const hasNuxtFrontend = frontend.includes("nuxt");
2728
2603
  const hasSvelteFrontend = frontend.includes("svelte");
@@ -2730,339 +2605,87 @@ async function setupAddons(config) {
2730
2605
  const hasNextFrontend = frontend.includes("next");
2731
2606
  if (addons.includes("tauri") && (hasReactWebFrontend || hasNuxtFrontend || hasSvelteFrontend || hasSolidFrontend || hasNextFrontend)) await setupTauri(config);
2732
2607
  const hasUltracite = addons.includes("ultracite");
2608
+ const hasBiome = addons.includes("biome");
2733
2609
  const hasHusky = addons.includes("husky");
2734
- if (addons.includes("oxlint")) await setupOxlint(config);
2610
+ const hasOxlint = addons.includes("oxlint");
2611
+ if (!hasUltracite) {
2612
+ if (hasBiome) await setupBiome(projectDir);
2613
+ if (hasOxlint) await setupOxlint(projectDir, config.packageManager);
2614
+ if (hasHusky) {
2615
+ let linter;
2616
+ if (hasOxlint) linter = "oxlint";
2617
+ else if (hasBiome) linter = "biome";
2618
+ await setupHusky(projectDir, linter);
2619
+ }
2620
+ }
2735
2621
  if (addons.includes("starlight")) await setupStarlight(config);
2736
- if (addons.includes("ruler")) await setupRuler(config);
2737
2622
  if (addons.includes("fumadocs")) await setupFumadocs(config);
2738
2623
  if (addons.includes("opentui")) await setupTui(config);
2739
2624
  if (addons.includes("wxt")) await setupWxt(config);
2740
2625
  if (hasUltracite) await setupUltracite(config, hasHusky);
2741
2626
  }
2742
-
2743
- //#endregion
2744
- //#region src/utils/add-package-deps.ts
2745
- const addPackageDependency = async (opts) => {
2746
- const { dependencies = [], devDependencies = [], customDependencies = {}, customDevDependencies = {}, projectDir } = opts;
2747
- const pkgJsonPath = path.join(projectDir, "package.json");
2748
- const pkgJson = await fs.readJson(pkgJsonPath);
2749
- if (!pkgJson.dependencies) pkgJson.dependencies = {};
2750
- if (!pkgJson.devDependencies) pkgJson.devDependencies = {};
2751
- for (const pkgName of dependencies) {
2752
- const version = dependencyVersionMap[pkgName];
2753
- if (version) pkgJson.dependencies[pkgName] = version;
2754
- else console.warn(`Warning: Dependency ${pkgName} not found in version map.`);
2627
+ async function setupBiome(projectDir) {
2628
+ await addPackageDependency({
2629
+ devDependencies: ["@biomejs/biome"],
2630
+ projectDir
2631
+ });
2632
+ const packageJsonPath = path.join(projectDir, "package.json");
2633
+ if (await fs.pathExists(packageJsonPath)) {
2634
+ const packageJson = await fs.readJson(packageJsonPath);
2635
+ packageJson.scripts = {
2636
+ ...packageJson.scripts,
2637
+ check: "biome check --write ."
2638
+ };
2639
+ await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
2755
2640
  }
2756
- for (const pkgName of devDependencies) {
2757
- const version = dependencyVersionMap[pkgName];
2758
- if (version) pkgJson.devDependencies[pkgName] = version;
2759
- else console.warn(`Warning: Dev dependency ${pkgName} not found in version map.`);
2641
+ }
2642
+ async function setupHusky(projectDir, linter) {
2643
+ await addPackageDependency({
2644
+ devDependencies: ["husky", "lint-staged"],
2645
+ projectDir
2646
+ });
2647
+ const packageJsonPath = path.join(projectDir, "package.json");
2648
+ if (await fs.pathExists(packageJsonPath)) {
2649
+ const packageJson = await fs.readJson(packageJsonPath);
2650
+ packageJson.scripts = {
2651
+ ...packageJson.scripts,
2652
+ prepare: "husky"
2653
+ };
2654
+ if (linter === "oxlint") packageJson["lint-staged"] = { "*": ["oxlint", "oxfmt --write"] };
2655
+ else if (linter === "biome") packageJson["lint-staged"] = { "*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}": ["biome check --write ."] };
2656
+ else packageJson["lint-staged"] = { "**/*.{js,mjs,cjs,jsx,ts,mts,cts,tsx,vue,astro,svelte}": "" };
2657
+ await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
2760
2658
  }
2761
- for (const [pkgName, version] of Object.entries(customDependencies)) pkgJson.dependencies[pkgName] = version;
2762
- for (const [pkgName, version] of Object.entries(customDevDependencies)) pkgJson.devDependencies[pkgName] = version;
2763
- await fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 });
2764
- };
2765
-
2766
- //#endregion
2767
- //#region src/helpers/core/auth-setup.ts
2768
- function generateAuthSecret(length = 32) {
2769
- const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
2770
- let result = "";
2771
- const charactersLength = 62;
2772
- for (let i = 0; i < length; i++) result += characters.charAt(Math.floor(Math.random() * charactersLength));
2773
- return result;
2774
2659
  }
2775
2660
 
2776
2661
  //#endregion
2777
- //#region src/helpers/core/env-setup.ts
2778
- function getClientServerVar(frontend, backend) {
2779
- const hasNextJs = frontend.includes("next");
2780
- const hasNuxt = frontend.includes("nuxt");
2781
- const hasSvelte = frontend.includes("svelte");
2782
- const hasTanstackStart = frontend.includes("tanstack-start");
2783
- if (backend === "self") return {
2784
- key: "",
2785
- value: "",
2786
- write: false
2787
- };
2788
- let key = "VITE_SERVER_URL";
2789
- if (hasNextJs) key = "NEXT_PUBLIC_SERVER_URL";
2790
- else if (hasNuxt) key = "NUXT_PUBLIC_SERVER_URL";
2791
- else if (hasSvelte) key = "PUBLIC_SERVER_URL";
2792
- else if (hasTanstackStart) key = "VITE_SERVER_URL";
2793
- return {
2794
- key,
2795
- value: "http://localhost:3000",
2796
- write: true
2797
- };
2798
- }
2799
- function getConvexVar(frontend) {
2800
- const hasNextJs = frontend.includes("next");
2801
- const hasNuxt = frontend.includes("nuxt");
2802
- const hasSvelte = frontend.includes("svelte");
2803
- const hasTanstackStart = frontend.includes("tanstack-start");
2804
- if (hasNextJs) return "NEXT_PUBLIC_CONVEX_URL";
2805
- if (hasNuxt) return "NUXT_PUBLIC_CONVEX_URL";
2806
- if (hasSvelte) return "PUBLIC_CONVEX_URL";
2807
- if (hasTanstackStart) return "VITE_CONVEX_URL";
2808
- return "VITE_CONVEX_URL";
2809
- }
2810
- async function addEnvVariablesToFile(filePath, variables) {
2811
- await fs.ensureDir(path.dirname(filePath));
2812
- let envContent = "";
2813
- if (await fs.pathExists(filePath)) envContent = await fs.readFile(filePath, "utf8");
2814
- let modified = false;
2815
- let contentToAdd = "";
2816
- const exampleVariables = [];
2817
- for (const { key, value, condition, comment } of variables) if (condition) {
2818
- const regex = new RegExp(`^${key}=.*$`, "m");
2819
- const valueToWrite = value ?? "";
2820
- exampleVariables.push(`${key}=`);
2821
- if (regex.test(envContent)) {
2822
- const existingMatch = envContent.match(regex);
2823
- if (existingMatch && existingMatch[0] !== `${key}=${valueToWrite}`) {
2824
- envContent = envContent.replace(regex, `${key}=${valueToWrite}`);
2825
- modified = true;
2826
- }
2827
- } else {
2828
- if (comment) contentToAdd += `# ${comment}\n`;
2829
- contentToAdd += `${key}=${valueToWrite}\n`;
2830
- modified = true;
2831
- }
2832
- }
2833
- if (contentToAdd) {
2834
- if (envContent.length > 0 && !envContent.endsWith("\n")) envContent += "\n";
2835
- envContent += contentToAdd;
2836
- }
2837
- if (modified) await fs.writeFile(filePath, envContent.trimEnd());
2838
- const exampleFilePath = filePath.replace(/\.env$/, ".env.example");
2839
- let exampleEnvContent = "";
2840
- if (await fs.pathExists(exampleFilePath)) exampleEnvContent = await fs.readFile(exampleFilePath, "utf8");
2841
- let exampleModified = false;
2842
- let exampleContentToAdd = "";
2843
- for (const exampleVar of exampleVariables) {
2844
- const key = exampleVar.split("=")[0];
2845
- if (!new RegExp(`^${key}=.*$`, "m").test(exampleEnvContent)) {
2846
- exampleContentToAdd += `${exampleVar}\n`;
2847
- exampleModified = true;
2848
- }
2849
- }
2850
- if (exampleContentToAdd) {
2851
- if (exampleEnvContent.length > 0 && !exampleEnvContent.endsWith("\n")) exampleEnvContent += "\n";
2852
- exampleEnvContent += exampleContentToAdd;
2853
- }
2854
- if (exampleModified || !await fs.pathExists(exampleFilePath)) await fs.writeFile(exampleFilePath, exampleEnvContent.trimEnd());
2855
- }
2856
- async function setupEnvironmentVariables(config) {
2857
- const { backend, frontend, database, auth, examples, dbSetup, projectDir, webDeploy, serverDeploy } = config;
2858
- const hasReactRouter = frontend.includes("react-router");
2859
- const hasTanStackRouter = frontend.includes("tanstack-router");
2860
- const hasTanStackStart = frontend.includes("tanstack-start");
2861
- const hasNextJs = frontend.includes("next");
2862
- const hasNuxt = frontend.includes("nuxt");
2863
- const hasSvelte = frontend.includes("svelte");
2864
- const hasSolid = frontend.includes("solid");
2865
- const hasWebFrontend$1 = hasReactRouter || hasTanStackRouter || hasTanStackStart || hasNextJs || hasNuxt || hasSolid || hasSvelte;
2866
- if (hasWebFrontend$1) {
2867
- const clientDir = path.join(projectDir, "apps/web");
2868
- if (await fs.pathExists(clientDir)) {
2869
- const baseVar = getClientServerVar(frontend, backend);
2870
- const clientVars = [{
2871
- key: backend === "convex" ? getConvexVar(frontend) : baseVar.key,
2872
- value: backend === "convex" ? "https://<YOUR_CONVEX_URL>" : baseVar.value,
2873
- condition: backend === "convex" ? true : baseVar.write
2874
- }];
2875
- if (backend === "convex" && auth === "clerk") {
2876
- if (hasNextJs) clientVars.push({
2877
- key: "NEXT_PUBLIC_CLERK_FRONTEND_API_URL",
2878
- value: "",
2879
- condition: true
2880
- }, {
2881
- key: "NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY",
2882
- value: "",
2883
- condition: true
2884
- }, {
2885
- key: "CLERK_SECRET_KEY",
2886
- value: "",
2887
- condition: true
2888
- });
2889
- else if (hasReactRouter || hasTanStackRouter || hasTanStackStart) {
2890
- clientVars.push({
2891
- key: "VITE_CLERK_PUBLISHABLE_KEY",
2892
- value: "",
2893
- condition: true
2894
- });
2895
- if (hasTanStackStart) clientVars.push({
2896
- key: "CLERK_SECRET_KEY",
2897
- value: "",
2898
- condition: true
2899
- });
2900
- }
2901
- }
2902
- if (backend === "convex" && auth === "better-auth") {
2903
- if (hasNextJs) clientVars.push({
2904
- key: "NEXT_PUBLIC_CONVEX_SITE_URL",
2905
- value: "https://<YOUR_CONVEX_URL>",
2906
- condition: true
2907
- });
2908
- else if (hasReactRouter || hasTanStackRouter || hasTanStackStart) clientVars.push({
2909
- key: "VITE_CONVEX_SITE_URL",
2910
- value: "https://<YOUR_CONVEX_URL>",
2911
- condition: true
2912
- });
2913
- }
2914
- await addEnvVariablesToFile(path.join(clientDir, ".env"), clientVars);
2915
- }
2916
- }
2917
- if (frontend.includes("native-bare") || frontend.includes("native-uniwind") || frontend.includes("native-unistyles")) {
2918
- const nativeDir = path.join(projectDir, "apps/native");
2919
- if (await fs.pathExists(nativeDir)) {
2920
- let envVarName = "EXPO_PUBLIC_SERVER_URL";
2921
- let serverUrl = "http://localhost:3000";
2922
- if (backend === "self") serverUrl = "http://localhost:3001";
2923
- if (backend === "convex") {
2924
- envVarName = "EXPO_PUBLIC_CONVEX_URL";
2925
- serverUrl = "https://<YOUR_CONVEX_URL>";
2926
- }
2927
- const nativeVars = [{
2928
- key: envVarName,
2929
- value: serverUrl,
2930
- condition: true
2931
- }];
2932
- if (backend === "convex" && auth === "clerk") nativeVars.push({
2933
- key: "EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY",
2934
- value: "",
2935
- condition: true
2936
- });
2937
- if (backend === "convex" && auth === "better-auth") nativeVars.push({
2938
- key: "EXPO_PUBLIC_CONVEX_SITE_URL",
2939
- value: "https://<YOUR_CONVEX_URL>",
2940
- condition: true
2941
- });
2942
- await addEnvVariablesToFile(path.join(nativeDir, ".env"), nativeVars);
2943
- }
2944
- }
2945
- if (backend === "convex") {
2946
- const convexBackendDir = path.join(projectDir, "packages/backend");
2947
- if (await fs.pathExists(convexBackendDir)) {
2948
- const envLocalPath = path.join(convexBackendDir, ".env.local");
2949
- let commentBlocks = "";
2950
- if (examples?.includes("ai")) commentBlocks += `# Set Google AI API key for AI agent
2951
- # npx convex env set GOOGLE_GENERATIVE_AI_API_KEY=your_google_api_key
2952
-
2953
- `;
2954
- if (auth === "better-auth") commentBlocks += `# Set Convex environment variables
2955
- # npx convex env set BETTER_AUTH_SECRET=$(openssl rand -base64 32)
2956
- ${hasWebFrontend$1 ? "# npx convex env set SITE_URL http://localhost:3001\n" : ""}`;
2957
- if (commentBlocks) {
2958
- let existingContent = "";
2959
- if (await fs.pathExists(envLocalPath)) existingContent = await fs.readFile(envLocalPath, "utf8");
2960
- await fs.writeFile(envLocalPath, commentBlocks + existingContent);
2961
- }
2962
- const convexBackendVars = [];
2963
- if (examples?.includes("ai")) convexBackendVars.push({
2964
- key: "GOOGLE_GENERATIVE_AI_API_KEY",
2965
- value: "",
2966
- condition: true,
2967
- comment: "Google AI API key for AI agent"
2968
- });
2969
- if (auth === "better-auth") {
2970
- const hasNative = frontend.includes("native-bare") || frontend.includes("native-uniwind") || frontend.includes("native-unistyles");
2971
- const hasWeb = hasWebFrontend$1;
2972
- if (hasNative) convexBackendVars.push({
2973
- key: "EXPO_PUBLIC_CONVEX_SITE_URL",
2974
- value: "",
2975
- condition: true,
2976
- comment: "Same as CONVEX_URL but ends in .site"
2977
- });
2978
- if (hasWeb) convexBackendVars.push({
2979
- key: hasNextJs ? "NEXT_PUBLIC_CONVEX_SITE_URL" : "VITE_CONVEX_SITE_URL",
2980
- value: "",
2981
- condition: true,
2982
- comment: "Same as CONVEX_URL but ends in .site"
2983
- }, {
2984
- key: "SITE_URL",
2985
- value: "http://localhost:3001",
2986
- condition: true,
2987
- comment: "Web app URL for authentication"
2988
- });
2989
- }
2990
- if (convexBackendVars.length > 0) await addEnvVariablesToFile(envLocalPath, convexBackendVars);
2991
- }
2992
- return;
2993
- }
2994
- const serverDir = path.join(projectDir, "apps/server");
2995
- let corsOrigin = "http://localhost:3001";
2996
- if (backend === "self") corsOrigin = "http://localhost:3001";
2997
- else if (hasReactRouter || hasSvelte) corsOrigin = "http://localhost:5173";
2998
- let databaseUrl = null;
2999
- if (database !== "none" && dbSetup === "none") switch (database) {
3000
- case "postgres":
3001
- databaseUrl = "postgresql://postgres:password@localhost:5432/postgres";
2662
+ //#region src/utils/env-utils.ts
2663
+ async function addEnvVariablesToFile(envPath, variables) {
2664
+ let content = "";
2665
+ if (fs.existsSync(envPath)) content = await fs.readFile(envPath, "utf-8");
2666
+ else await fs.ensureFile(envPath);
2667
+ const existingLines = content.split("\n");
2668
+ const newLines = [];
2669
+ const keysToAdd = /* @__PURE__ */ new Map();
2670
+ for (const variable of variables) {
2671
+ if (variable.condition === false || !variable.key) continue;
2672
+ keysToAdd.set(variable.key, variable.value);
2673
+ }
2674
+ let foundKeys = /* @__PURE__ */ new Set();
2675
+ for (const line of existingLines) {
2676
+ const trimmedLine = line.trim();
2677
+ let lineProcessed = false;
2678
+ for (const [key, value] of keysToAdd) if (trimmedLine.startsWith(`${key}=`)) {
2679
+ newLines.push(`${key}=${value}`);
2680
+ foundKeys.add(key);
2681
+ lineProcessed = true;
3002
2682
  break;
3003
- case "mysql":
3004
- databaseUrl = "mysql://root:password@localhost:3306/mydb";
3005
- break;
3006
- case "mongodb":
3007
- databaseUrl = "mongodb://localhost:27017/mydatabase";
3008
- break;
3009
- case "sqlite":
3010
- if (config.runtime === "workers" || webDeploy === "cloudflare" || serverDeploy === "cloudflare") databaseUrl = "http://127.0.0.1:8080";
3011
- else {
3012
- const dbAppDir = backend === "self" ? "apps/web" : "apps/server";
3013
- databaseUrl = `file:${path.join(config.projectDir, dbAppDir, "local.db")}`;
3014
- }
3015
- break;
3016
- }
3017
- const serverVars = [
3018
- {
3019
- key: "BETTER_AUTH_SECRET",
3020
- value: generateAuthSecret(),
3021
- condition: !!auth
3022
- },
3023
- {
3024
- key: "BETTER_AUTH_URL",
3025
- value: backend === "self" ? "http://localhost:3001" : "http://localhost:3000",
3026
- condition: !!auth
3027
- },
3028
- {
3029
- key: "POLAR_ACCESS_TOKEN",
3030
- value: "",
3031
- condition: config.payments === "polar"
3032
- },
3033
- {
3034
- key: "POLAR_SUCCESS_URL",
3035
- value: `${corsOrigin}/success?checkout_id={CHECKOUT_ID}`,
3036
- condition: config.payments === "polar"
3037
- },
3038
- {
3039
- key: "CORS_ORIGIN",
3040
- value: corsOrigin,
3041
- condition: true
3042
- },
3043
- {
3044
- key: "GOOGLE_GENERATIVE_AI_API_KEY",
3045
- value: "",
3046
- condition: examples?.includes("ai") || false
3047
- },
3048
- {
3049
- key: "DATABASE_URL",
3050
- value: databaseUrl,
3051
- condition: database !== "none" && dbSetup === "none"
3052
2683
  }
3053
- ];
3054
- if (backend === "self") {
3055
- const webDir = path.join(projectDir, "apps/web");
3056
- if (await fs.pathExists(webDir)) await addEnvVariablesToFile(path.join(webDir, ".env"), serverVars);
3057
- } else if (await fs.pathExists(serverDir)) await addEnvVariablesToFile(path.join(serverDir, ".env"), serverVars);
3058
- if (webDeploy === "cloudflare" && serverDeploy === "cloudflare" || webDeploy === "cloudflare" || serverDeploy === "cloudflare") {
3059
- const infraDir = path.join(projectDir, "packages/infra");
3060
- if (await fs.pathExists(infraDir)) await addEnvVariablesToFile(path.join(infraDir, ".env"), [{
3061
- key: "ALCHEMY_PASSWORD",
3062
- value: "please-change-this",
3063
- condition: true
3064
- }]);
2684
+ if (!lineProcessed) newLines.push(line);
3065
2685
  }
2686
+ for (const [key, value] of keysToAdd) if (!foundKeys.has(key)) newLines.push(`${key}=${value}`);
2687
+ if (newLines.length > 0 && newLines[newLines.length - 1] === "") newLines.pop();
2688
+ if (foundKeys.size > 0 || keysToAdd.size > foundKeys.size) await fs.writeFile(envPath, newLines.join("\n") + "\n");
3066
2689
  }
3067
2690
 
3068
2691
  //#endregion
@@ -4014,314 +3637,6 @@ async function setupDatabase(config, cliInput) {
4014
3637
  }
4015
3638
  }
4016
3639
 
4017
- //#endregion
4018
- //#region src/helpers/deployment/alchemy/alchemy-next-setup.ts
4019
- /**
4020
- * Alchemy Next.js setup - CLI-only operations
4021
- * NOTE: Dependencies are handled by template-generator's deploy-deps.ts
4022
- * This only modifies config files for "add deploy" command
4023
- */
4024
- async function setupNextAlchemyDeploy(projectDir, _packageManager, _options) {
4025
- const webAppDir = path.join(projectDir, "apps/web");
4026
- if (!await fs.pathExists(webAppDir)) return;
4027
- const openNextConfigPath = path.join(webAppDir, "open-next.config.ts");
4028
- await fs.writeFile(openNextConfigPath, `import { defineCloudflareConfig } from "@opennextjs/cloudflare";
4029
-
4030
- export default defineCloudflareConfig({});
4031
- `);
4032
- const gitignorePath = path.join(webAppDir, ".gitignore");
4033
- if (await fs.pathExists(gitignorePath)) {
4034
- if (!(await fs.readFile(gitignorePath, "utf-8")).includes("wrangler.jsonc")) await fs.appendFile(gitignorePath, "\nwrangler.jsonc\n");
4035
- } else await fs.writeFile(gitignorePath, "wrangler.jsonc\n");
4036
- }
4037
-
4038
- //#endregion
4039
- //#region src/helpers/deployment/alchemy/alchemy-nuxt-setup.ts
4040
- /**
4041
- * Alchemy Nuxt setup - CLI-only operations
4042
- * NOTE: Dependencies are handled by template-generator's deploy-deps.ts
4043
- * This only modifies config files for "add deploy" command
4044
- */
4045
- async function setupNuxtAlchemyDeploy(projectDir, _packageManager, _options) {
4046
- const webAppDir = path.join(projectDir, "apps/web");
4047
- if (!await fs.pathExists(webAppDir)) return;
4048
- const nuxtConfigPath = path.join(webAppDir, "nuxt.config.ts");
4049
- if (!await fs.pathExists(nuxtConfigPath)) return;
4050
- try {
4051
- const project = new Project({ manipulationSettings: {
4052
- indentationText: IndentationText.TwoSpaces,
4053
- quoteKind: QuoteKind.Double
4054
- } });
4055
- project.addSourceFileAtPath(nuxtConfigPath);
4056
- const exportAssignment = project.getSourceFileOrThrow(nuxtConfigPath).getExportAssignment((d) => !d.isExportEquals());
4057
- if (!exportAssignment) return;
4058
- const defineConfigCall = exportAssignment.getExpression();
4059
- if (!Node.isCallExpression(defineConfigCall) || defineConfigCall.getExpression().getText() !== "defineNuxtConfig") return;
4060
- let configObject = defineConfigCall.getArguments()[0];
4061
- if (!configObject) configObject = defineConfigCall.addArgument("{}");
4062
- if (Node.isObjectLiteralExpression(configObject)) {
4063
- if (!configObject.getProperty("nitro")) configObject.addPropertyAssignment({
4064
- name: "nitro",
4065
- initializer: `{
4066
- preset: "cloudflare_module",
4067
- cloudflare: {
4068
- deployConfig: true,
4069
- nodeCompat: true
4070
- }
4071
- }`
4072
- });
4073
- const modulesProperty = configObject.getProperty("modules");
4074
- if (modulesProperty && Node.isPropertyAssignment(modulesProperty)) {
4075
- const initializer = modulesProperty.getInitializer();
4076
- if (Node.isArrayLiteralExpression(initializer)) {
4077
- if (!initializer.getElements().some((el) => el.getText() === "\"nitro-cloudflare-dev\"" || el.getText() === "'nitro-cloudflare-dev'")) initializer.addElement("\"nitro-cloudflare-dev\"");
4078
- }
4079
- } else if (!modulesProperty) configObject.addPropertyAssignment({
4080
- name: "modules",
4081
- initializer: "[\"nitro-cloudflare-dev\"]"
4082
- });
4083
- }
4084
- await project.save();
4085
- } catch (error) {
4086
- console.warn("Failed to update nuxt.config.ts:", error);
4087
- }
4088
- }
4089
-
4090
- //#endregion
4091
- //#region src/helpers/deployment/alchemy/alchemy-svelte-setup.ts
4092
- /**
4093
- * Alchemy Svelte setup - CLI-only operations
4094
- * NOTE: Dependencies are handled by template-generator's deploy-deps.ts
4095
- * This only modifies config files for "add deploy" command
4096
- */
4097
- async function setupSvelteAlchemyDeploy(projectDir, _packageManager, _options) {
4098
- const webAppDir = path.join(projectDir, "apps/web");
4099
- if (!await fs.pathExists(webAppDir)) return;
4100
- const svelteConfigPath = path.join(webAppDir, "svelte.config.js");
4101
- if (!await fs.pathExists(svelteConfigPath)) return;
4102
- try {
4103
- const project = new Project({ manipulationSettings: {
4104
- indentationText: IndentationText.TwoSpaces,
4105
- quoteKind: QuoteKind.Single
4106
- } });
4107
- project.addSourceFileAtPath(svelteConfigPath);
4108
- const sourceFile = project.getSourceFileOrThrow(svelteConfigPath);
4109
- const adapterImport = sourceFile.getImportDeclarations().find((imp) => imp.getModuleSpecifierValue().includes("@sveltejs/adapter"));
4110
- if (adapterImport) {
4111
- adapterImport.setModuleSpecifier("alchemy/cloudflare/sveltekit");
4112
- adapterImport.removeDefaultImport();
4113
- adapterImport.setDefaultImport("alchemy");
4114
- } else sourceFile.insertImportDeclaration(0, {
4115
- moduleSpecifier: "alchemy/cloudflare/sveltekit",
4116
- defaultImport: "alchemy"
4117
- });
4118
- const configVariable = sourceFile.getVariableDeclaration("config");
4119
- if (configVariable) {
4120
- const initializer = configVariable.getInitializer();
4121
- if (Node.isObjectLiteralExpression(initializer)) updateAdapterInConfig(initializer);
4122
- }
4123
- await project.save();
4124
- } catch (error) {
4125
- console.warn("Failed to update svelte.config.js:", error);
4126
- }
4127
- }
4128
- function updateAdapterInConfig(configObject) {
4129
- if (!Node.isObjectLiteralExpression(configObject)) return;
4130
- const kitProperty = configObject.getProperty("kit");
4131
- if (kitProperty && Node.isPropertyAssignment(kitProperty)) {
4132
- const kitInitializer = kitProperty.getInitializer();
4133
- if (Node.isObjectLiteralExpression(kitInitializer)) {
4134
- const adapterProperty = kitInitializer.getProperty("adapter");
4135
- if (adapterProperty && Node.isPropertyAssignment(adapterProperty)) {
4136
- const initializer = adapterProperty.getInitializer();
4137
- if (Node.isCallExpression(initializer)) {
4138
- const expression = initializer.getExpression();
4139
- if (Node.isIdentifier(expression) && expression.getText() === "adapter") expression.replaceWithText("alchemy");
4140
- }
4141
- }
4142
- }
4143
- }
4144
- }
4145
-
4146
- //#endregion
4147
- //#region src/helpers/deployment/alchemy/alchemy-tanstack-start-setup.ts
4148
- /**
4149
- * Alchemy TanStack Start setup - CLI-only operations
4150
- * NOTE: Dependencies are handled by template-generator's deploy-deps.ts
4151
- * This only modifies vite.config.ts for "add deploy" command
4152
- */
4153
- async function setupTanStackStartAlchemyDeploy(projectDir, _packageManager, _options) {
4154
- const webAppDir = path.join(projectDir, "apps/web");
4155
- if (!await fs.pathExists(webAppDir)) return;
4156
- const viteConfigPath = path.join(webAppDir, "vite.config.ts");
4157
- if (await fs.pathExists(viteConfigPath)) try {
4158
- const project = new Project({ manipulationSettings: {
4159
- indentationText: IndentationText.TwoSpaces,
4160
- quoteKind: QuoteKind.Double
4161
- } });
4162
- project.addSourceFileAtPath(viteConfigPath);
4163
- const sourceFile = project.getSourceFileOrThrow(viteConfigPath);
4164
- const alchemyImport = sourceFile.getImportDeclaration("alchemy/cloudflare/tanstack-start");
4165
- if (!alchemyImport) sourceFile.addImportDeclaration({
4166
- moduleSpecifier: "alchemy/cloudflare/tanstack-start",
4167
- defaultImport: "alchemy"
4168
- });
4169
- else alchemyImport.setModuleSpecifier("alchemy/cloudflare/tanstack-start");
4170
- const exportAssignment = sourceFile.getExportAssignment((d) => !d.isExportEquals());
4171
- if (!exportAssignment) return;
4172
- const defineConfigCall = exportAssignment.getExpression();
4173
- if (!Node.isCallExpression(defineConfigCall) || defineConfigCall.getExpression().getText() !== "defineConfig") return;
4174
- let configObject = defineConfigCall.getArguments()[0];
4175
- if (!configObject) configObject = defineConfigCall.addArgument("{}");
4176
- if (Node.isObjectLiteralExpression(configObject)) {
4177
- const pluginsProperty = configObject.getProperty("plugins");
4178
- if (pluginsProperty && Node.isPropertyAssignment(pluginsProperty)) {
4179
- const initializer = pluginsProperty.getInitializer();
4180
- if (Node.isArrayLiteralExpression(initializer)) {
4181
- if (!initializer.getElements().some((el) => el.getText().includes("alchemy("))) initializer.addElement("alchemy()");
4182
- }
4183
- } else configObject.addPropertyAssignment({
4184
- name: "plugins",
4185
- initializer: "[alchemy()]"
4186
- });
4187
- }
4188
- await project.save();
4189
- } catch (error) {
4190
- console.warn("Failed to update vite.config.ts:", error);
4191
- }
4192
- }
4193
-
4194
- //#endregion
4195
- //#region src/helpers/deployment/alchemy/alchemy-vite-setup.ts
4196
- /**
4197
- * Alchemy setup for Vite-based frontends (React Router, Solid, TanStack Router)
4198
- * NOTE: Dependencies are handled by template-generator's deploy-deps.ts
4199
- * These frontends don't need config modifications - templates already have correct setup
4200
- */
4201
- async function setupReactRouterAlchemyDeploy(projectDir, _packageManager, _options) {
4202
- const webAppDir = path.join(projectDir, "apps/web");
4203
- if (!await fs.pathExists(webAppDir)) return;
4204
- }
4205
- async function setupSolidAlchemyDeploy(projectDir, _packageManager, _options) {
4206
- const webAppDir = path.join(projectDir, "apps/web");
4207
- if (!await fs.pathExists(webAppDir)) return;
4208
- }
4209
- async function setupTanStackRouterAlchemyDeploy(projectDir, _packageManager, _options) {
4210
- const webAppDir = path.join(projectDir, "apps/web");
4211
- if (!await fs.pathExists(webAppDir)) return;
4212
- }
4213
-
4214
- //#endregion
4215
- //#region src/helpers/deployment/alchemy/alchemy-combined-setup.ts
4216
- function getInfraFilter(packageManager, hasTurborepo, infraWorkspace) {
4217
- if (hasTurborepo) return (script) => `turbo -F ${infraWorkspace} ${script}`;
4218
- switch (packageManager) {
4219
- case "pnpm": return (script) => `pnpm --filter ${infraWorkspace} ${script}`;
4220
- case "npm": return (script) => `npm run ${script} --workspace ${infraWorkspace}`;
4221
- case "bun": return (script) => `bun run --filter ${infraWorkspace} ${script}`;
4222
- }
4223
- }
4224
- async function setupCombinedAlchemyDeploy(projectDir, packageManager, config) {
4225
- await setupInfraScripts(projectDir, packageManager, config);
4226
- const frontend = config.frontend;
4227
- const isNext = frontend.includes("next");
4228
- const isNuxt = frontend.includes("nuxt");
4229
- const isSvelte = frontend.includes("svelte");
4230
- const isTanstackRouter = frontend.includes("tanstack-router");
4231
- const isTanstackStart = frontend.includes("tanstack-start");
4232
- const isReactRouter = frontend.includes("react-router");
4233
- const isSolid = frontend.includes("solid");
4234
- if (isNext) await setupNextAlchemyDeploy(projectDir, packageManager, { skipAppScripts: true });
4235
- else if (isNuxt) await setupNuxtAlchemyDeploy(projectDir, packageManager, { skipAppScripts: true });
4236
- else if (isSvelte) await setupSvelteAlchemyDeploy(projectDir, packageManager, { skipAppScripts: true });
4237
- else if (isTanstackStart) await setupTanStackStartAlchemyDeploy(projectDir, packageManager, { skipAppScripts: true });
4238
- else if (isTanstackRouter) await setupTanStackRouterAlchemyDeploy(projectDir, packageManager, { skipAppScripts: true });
4239
- else if (isReactRouter) await setupReactRouterAlchemyDeploy(projectDir, packageManager, { skipAppScripts: true });
4240
- else if (isSolid) await setupSolidAlchemyDeploy(projectDir, packageManager, { skipAppScripts: true });
4241
- }
4242
- async function setupInfraScripts(projectDir, packageManager, config) {
4243
- const projectName = config.projectName;
4244
- const hasTurborepo = config.addons.includes("turborepo");
4245
- const infraWorkspace = `@${projectName}/infra`;
4246
- const rootPkgPath = path.join(projectDir, "package.json");
4247
- if (await fs.pathExists(rootPkgPath)) {
4248
- const pkg = await fs.readJson(rootPkgPath);
4249
- const filter = getInfraFilter(packageManager, hasTurborepo, infraWorkspace);
4250
- pkg.scripts = {
4251
- ...pkg.scripts,
4252
- deploy: filter("deploy"),
4253
- destroy: filter("destroy")
4254
- };
4255
- await fs.writeJson(rootPkgPath, pkg, { spaces: 2 });
4256
- }
4257
- if (config.serverDeploy === "cloudflare") {
4258
- const serverPkgPath = path.join(projectDir, "apps/server/package.json");
4259
- if (await fs.pathExists(serverPkgPath)) {
4260
- const serverPkg = await fs.readJson(serverPkgPath);
4261
- if (serverPkg.scripts?.dev) {
4262
- serverPkg.scripts["dev:bare"] = serverPkg.scripts.dev;
4263
- delete serverPkg.scripts.dev;
4264
- await fs.writeJson(serverPkgPath, serverPkg, { spaces: 2 });
4265
- }
4266
- }
4267
- }
4268
- if (config.webDeploy === "cloudflare") {
4269
- const webPkgPath = path.join(projectDir, "apps/web/package.json");
4270
- if (await fs.pathExists(webPkgPath)) {
4271
- const webPkg = await fs.readJson(webPkgPath);
4272
- if (webPkg.scripts?.dev) {
4273
- webPkg.scripts["dev:bare"] = webPkg.scripts.dev;
4274
- delete webPkg.scripts.dev;
4275
- await fs.writeJson(webPkgPath, webPkg, { spaces: 2 });
4276
- }
4277
- }
4278
- }
4279
- }
4280
-
4281
- //#endregion
4282
- //#region src/helpers/deployment/server-deploy-setup.ts
4283
- /**
4284
- * Server deploy setup - CLI-only operations
4285
- * NOTE: Dependencies are handled by template-generator's deploy-deps.ts processor
4286
- * This file only handles external CLI calls for "add deploy" command
4287
- */
4288
- async function setupServerDeploy(config) {
4289
- const { serverDeploy, webDeploy, projectDir, packageManager } = config;
4290
- if (serverDeploy === "none") return;
4291
- if (serverDeploy === "cloudflare" && webDeploy === "cloudflare") return;
4292
- const serverDir = path.join(projectDir, "apps/server");
4293
- if (!await fs.pathExists(serverDir)) return;
4294
- if (serverDeploy === "cloudflare") await setupInfraScripts(projectDir, packageManager, config);
4295
- }
4296
-
4297
- //#endregion
4298
- //#region src/helpers/deployment/web-deploy-setup.ts
4299
- async function setupWebDeploy(config) {
4300
- const { webDeploy, serverDeploy, frontend, projectDir } = config;
4301
- const { packageManager } = config;
4302
- if (webDeploy === "none") return;
4303
- if (webDeploy !== "cloudflare") return;
4304
- if (webDeploy === "cloudflare" && serverDeploy === "cloudflare") {
4305
- await setupCombinedAlchemyDeploy(projectDir, packageManager, config);
4306
- return;
4307
- }
4308
- await setupInfraScripts(projectDir, packageManager, config);
4309
- const isNext = frontend.includes("next");
4310
- const isNuxt = frontend.includes("nuxt");
4311
- const isSvelte = frontend.includes("svelte");
4312
- const isTanstackRouter = frontend.includes("tanstack-router");
4313
- const isTanstackStart = frontend.includes("tanstack-start");
4314
- const isReactRouter = frontend.includes("react-router");
4315
- const isSolid = frontend.includes("solid");
4316
- if (isNext) await setupNextAlchemyDeploy(projectDir, packageManager);
4317
- else if (isNuxt) await setupNuxtAlchemyDeploy(projectDir, packageManager);
4318
- else if (isSvelte) await setupSvelteAlchemyDeploy(projectDir, packageManager);
4319
- else if (isTanstackStart) await setupTanStackStartAlchemyDeploy(projectDir, packageManager);
4320
- else if (isTanstackRouter) await setupTanStackRouterAlchemyDeploy(projectDir, packageManager);
4321
- else if (isReactRouter) await setupReactRouterAlchemyDeploy(projectDir, packageManager);
4322
- else if (isSolid) await setupSolidAlchemyDeploy(projectDir, packageManager);
4323
- }
4324
-
4325
3640
  //#endregion
4326
3641
  //#region src/helpers/core/git.ts
4327
3642
  async function initializeGit(projectDir, useGit) {
@@ -4592,13 +3907,6 @@ async function createProject(options, cliInput = {}) {
4592
3907
  await setPackageManagerVersion(projectDir, options.packageManager);
4593
3908
  if (!isConvex && options.database !== "none") await setupDatabase(options, cliInput);
4594
3909
  if (options.addons.length > 0 && options.addons[0] !== "none") await setupAddons(options);
4595
- if (options.auth === "better-auth" && !isConvex) {
4596
- const authPackageDir = `${projectDir}/packages/auth`;
4597
- if (await fs.pathExists(authPackageDir)) await setupBetterAuthPlugins(projectDir, options);
4598
- }
4599
- await setupEnvironmentVariables(options);
4600
- await setupWebDeploy(options);
4601
- await setupServerDeploy(options);
4602
3910
  await writeBtsConfig(options);
4603
3911
  await formatProject(projectDir);
4604
3912
  if (!isSilent()) log.success("Project template successfully scaffolded!");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-better-t-stack",
3
- "version": "3.13.2-dev.c1daaba",
3
+ "version": "3.13.2-dev.cc9652f",
4
4
  "description": "A modern CLI tool for scaffolding end-to-end type-safe TypeScript projects with best practices and customizable configurations",
5
5
  "keywords": [
6
6
  "better-auth",
@@ -70,8 +70,8 @@
70
70
  "prepublishOnly": "npm run build"
71
71
  },
72
72
  "dependencies": {
73
- "@better-t-stack/template-generator": "3.13.2-dev.c1daaba",
74
- "@better-t-stack/types": "3.13.2-dev.c1daaba",
73
+ "@better-t-stack/template-generator": "3.13.2-dev.cc9652f",
74
+ "@better-t-stack/types": "3.13.2-dev.cc9652f",
75
75
  "@clack/core": "^0.5.0",
76
76
  "@clack/prompts": "^1.0.0-alpha.8",
77
77
  "@orpc/server": "^1.13.0",