create-better-t-stack 3.13.2-dev.6c6cffb → 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.
@@ -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,15 +10,13 @@ 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
- import { glob } from "tinyglobby";
22
20
  import os$1 from "node:os";
23
21
 
24
22
  //#region src/utils/get-package-manager.ts
@@ -92,6 +90,59 @@ const ADDON_COMPATIBILITY = {
92
90
  none: []
93
91
  };
94
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
+
95
146
  //#endregion
96
147
  //#region src/utils/errors.ts
97
148
  var UserCancelledError = class extends Error {
@@ -107,17 +158,20 @@ var CLIError = class extends Error {
107
158
  }
108
159
  };
109
160
  function exitWithError(message) {
161
+ if (isSilent()) throw new CLIError(message);
110
162
  consola.error(pc.red(message));
111
- throw new CLIError(message);
163
+ process.exit(1);
112
164
  }
113
165
  function exitCancelled(message = "Operation cancelled") {
166
+ if (isSilent()) throw new UserCancelledError(message);
114
167
  cancel(pc.red(message));
115
- throw new UserCancelledError(message);
168
+ process.exit(1);
116
169
  }
117
170
  function handleError(error, fallbackMessage) {
118
171
  const message = error instanceof Error ? error.message : fallbackMessage || String(error);
172
+ if (isSilent()) throw error instanceof Error ? error : new Error(message);
119
173
  consola.error(pc.red(message));
120
- throw error instanceof Error ? error : new Error(message);
174
+ process.exit(1);
121
175
  }
122
176
 
123
177
  //#endregion
@@ -263,64 +317,8 @@ function validateExamplesCompatibility(examples, backend, database, frontend, ap
263
317
  }
264
318
  }
265
319
 
266
- //#endregion
267
- //#region src/utils/context.ts
268
- const cliStorage = new AsyncLocalStorage();
269
- function defaultContext() {
270
- return {
271
- navigation: {
272
- isFirstPrompt: false,
273
- lastPromptShownUI: false
274
- },
275
- silent: false,
276
- verbose: false
277
- };
278
- }
279
- function getContext() {
280
- const ctx = cliStorage.getStore();
281
- if (!ctx) return defaultContext();
282
- return ctx;
283
- }
284
- function tryGetContext() {
285
- return cliStorage.getStore();
286
- }
287
- function isSilent() {
288
- return getContext().silent;
289
- }
290
- function isFirstPrompt() {
291
- return getContext().navigation.isFirstPrompt;
292
- }
293
- function didLastPromptShowUI() {
294
- return getContext().navigation.lastPromptShownUI;
295
- }
296
- function setIsFirstPrompt$1(value) {
297
- const ctx = tryGetContext();
298
- if (ctx) ctx.navigation.isFirstPrompt = value;
299
- }
300
- function setLastPromptShownUI(value) {
301
- const ctx = tryGetContext();
302
- if (ctx) ctx.navigation.lastPromptShownUI = value;
303
- }
304
- async function runWithContextAsync(options, fn) {
305
- const ctx = {
306
- navigation: {
307
- isFirstPrompt: false,
308
- lastPromptShownUI: false
309
- },
310
- silent: options.silent ?? false,
311
- verbose: options.verbose ?? false,
312
- projectDir: options.projectDir,
313
- projectName: options.projectName,
314
- packageManager: options.packageManager
315
- };
316
- return cliStorage.run(ctx, fn);
317
- }
318
-
319
320
  //#endregion
320
321
  //#region src/utils/navigation.ts
321
- /**
322
- * Navigation symbols and utilities for prompt navigation
323
- */
324
322
  const GO_BACK_SYMBOL = Symbol("clack:goBack");
325
323
  function isGoBack(value) {
326
324
  return value === GO_BACK_SYMBOL;
@@ -616,29 +614,29 @@ function getAddonDisplay(addon) {
616
614
  };
617
615
  }
618
616
  const ADDON_GROUPS = {
619
- Documentation: ["starlight", "fumadocs"],
620
- Linting: [
617
+ Tooling: [
618
+ "turborepo",
621
619
  "biome",
622
620
  "oxlint",
623
- "ultracite"
621
+ "ultracite",
622
+ "husky"
624
623
  ],
625
- Other: [
626
- "ruler",
624
+ Documentation: ["starlight", "fumadocs"],
625
+ Extensions: [
627
626
  "pwa",
628
627
  "tauri",
629
- "husky",
630
628
  "opentui",
631
629
  "wxt",
632
- "turborepo"
630
+ "ruler"
633
631
  ]
634
632
  };
635
633
  async function getAddonsChoice(addons, frontends, auth) {
636
634
  if (addons !== void 0) return addons;
637
635
  const allAddons = types_exports.AddonsSchema.options.filter((addon) => addon !== "none");
638
636
  const groupedOptions = {
637
+ Tooling: [],
639
638
  Documentation: [],
640
- Linting: [],
641
- Other: []
639
+ Extensions: []
642
640
  };
643
641
  const frontendsArray = frontends || [];
644
642
  for (const addon of allAddons) {
@@ -650,9 +648,9 @@ async function getAddonsChoice(addons, frontends, auth) {
650
648
  label,
651
649
  hint
652
650
  };
653
- if (ADDON_GROUPS.Documentation.includes(addon)) groupedOptions.Documentation.push(option);
654
- else if (ADDON_GROUPS.Linting.includes(addon)) groupedOptions.Linting.push(option);
655
- 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);
656
654
  }
657
655
  Object.keys(groupedOptions).forEach((group$1) => {
658
656
  if (groupedOptions[group$1].length === 0) delete groupedOptions[group$1];
@@ -2029,63 +2027,6 @@ function validateConfigCompatibility(config, providedFlags, options) {
2029
2027
  else validateConfigForProgrammaticUse(config);
2030
2028
  }
2031
2029
 
2032
- //#endregion
2033
- //#region src/utils/ts-morph.ts
2034
- const tsProject = new Project({
2035
- useInMemoryFileSystem: false,
2036
- skipAddingFilesFromTsConfig: true,
2037
- manipulationSettings: {
2038
- quoteKind: QuoteKind.Single,
2039
- indentationText: IndentationText.TwoSpaces
2040
- }
2041
- });
2042
- function ensureArrayProperty(obj, name) {
2043
- return obj.getProperty(name)?.getFirstDescendantByKind(SyntaxKind.ArrayLiteralExpression) ?? obj.addPropertyAssignment({
2044
- name,
2045
- initializer: "[]"
2046
- }).getFirstDescendantByKindOrThrow(SyntaxKind.ArrayLiteralExpression);
2047
- }
2048
-
2049
- //#endregion
2050
- //#region src/utils/better-auth-plugin-setup.ts
2051
- async function setupBetterAuthPlugins(projectDir, config) {
2052
- const authIndexPath = `${projectDir}/packages/auth/src/index.ts`;
2053
- const authIndexFile = tsProject.addSourceFileAtPath(authIndexPath);
2054
- if (!authIndexFile) return;
2055
- const pluginsToAdd = [];
2056
- const importsToAdd = [];
2057
- if (config.backend === "self" && config.frontend?.includes("tanstack-start")) {
2058
- pluginsToAdd.push("tanstackStartCookies()");
2059
- importsToAdd.push("import { tanstackStartCookies } from \"better-auth/tanstack-start\";");
2060
- }
2061
- if (config.backend === "self" && config.frontend?.includes("next")) {
2062
- pluginsToAdd.push("nextCookies()");
2063
- importsToAdd.push("import { nextCookies } from \"better-auth/next-js\";");
2064
- }
2065
- if (config.frontend?.includes("native-bare") || config.frontend?.includes("native-uniwind") || config.frontend?.includes("native-unistyles")) {
2066
- pluginsToAdd.push("expo()");
2067
- importsToAdd.push("import { expo } from \"@better-auth/expo\";");
2068
- }
2069
- if (pluginsToAdd.length === 0) return;
2070
- importsToAdd.forEach((importStatement) => {
2071
- if (!authIndexFile.getImportDeclaration((declaration) => declaration.getModuleSpecifierValue().includes(importStatement.split("\"")[1]))) authIndexFile.insertImportDeclaration(0, {
2072
- moduleSpecifier: importStatement.split("\"")[1],
2073
- namedImports: [importStatement.split("{")[1].split("}")[0].trim()]
2074
- });
2075
- });
2076
- const betterAuthCall = authIndexFile.getDescendantsOfKind(SyntaxKind.CallExpression).find((call) => call.getExpression().getText() === "betterAuth");
2077
- if (betterAuthCall) {
2078
- const configObject = betterAuthCall.getArguments()[0];
2079
- if (configObject && configObject.getKind() === SyntaxKind.ObjectLiteralExpression) {
2080
- const pluginsArray = ensureArrayProperty(configObject.asKindOrThrow(SyntaxKind.ObjectLiteralExpression), "plugins");
2081
- pluginsToAdd.forEach((plugin) => {
2082
- pluginsArray.addElement(plugin);
2083
- });
2084
- }
2085
- }
2086
- authIndexFile.save();
2087
- }
2088
-
2089
2030
  //#endregion
2090
2031
  //#region src/utils/bts-config.ts
2091
2032
  const BTS_CONFIG_FILE = "bts.jsonc";
@@ -2148,7 +2089,7 @@ const formatOptions = {
2148
2089
  experimentalSortPackageJson: true,
2149
2090
  experimentalSortImports: { order: "asc" }
2150
2091
  };
2151
- async function formatFile(filePath, content) {
2092
+ async function formatCode(filePath, content) {
2152
2093
  try {
2153
2094
  const result = await format(path.basename(filePath), content, formatOptions);
2154
2095
  if (result.errors && result.errors.length > 0) return null;
@@ -2157,45 +2098,47 @@ async function formatFile(filePath, content) {
2157
2098
  return null;
2158
2099
  }
2159
2100
  }
2160
- /**
2161
- * Format all files in a project directory using oxfmt
2162
- */
2163
- async function formatProjectFiles(projectDir) {
2164
- const files = await glob(["**/*.{ts,tsx,js,jsx,json,mjs,cjs}"], {
2165
- cwd: projectDir,
2166
- absolute: true,
2167
- ignore: [
2168
- "**/node_modules/**",
2169
- "**/dist/**",
2170
- "**/.git/**"
2171
- ]
2172
- });
2173
- await Promise.all(files.map(async (filePath) => {
2174
- try {
2175
- const content = await fs.readFile(filePath, "utf-8");
2176
- const formatted = await formatFile(filePath, content);
2177
- if (formatted && formatted !== content) await fs.writeFile(filePath, formatted, "utf-8");
2178
- } catch {}
2179
- }));
2101
+ async function formatProject(projectDir) {
2102
+ async function formatDirectory(dir) {
2103
+ const entries = await fs.readdir(dir, { withFileTypes: true });
2104
+ await Promise.all(entries.map(async (entry) => {
2105
+ const fullPath = path.join(dir, entry.name);
2106
+ if (entry.isDirectory()) await formatDirectory(fullPath);
2107
+ else if (entry.isFile()) try {
2108
+ const content = await fs.readFile(fullPath, "utf-8");
2109
+ const formatted = await formatCode(fullPath, content);
2110
+ if (formatted && formatted !== content) await fs.writeFile(fullPath, formatted, "utf-8");
2111
+ } catch {}
2112
+ }));
2113
+ }
2114
+ await formatDirectory(projectDir);
2180
2115
  }
2181
2116
 
2182
2117
  //#endregion
2183
- //#region src/utils/package-runner.ts
2184
- /**
2185
- * Returns the appropriate command for running a package without installing it globally,
2186
- * based on the selected package manager.
2187
- *
2188
- * @param packageManager - The selected package manager (e.g., 'npm', 'yarn', 'pnpm', 'bun').
2189
- * @param commandWithArgs - The command to run, including arguments (e.g., "prisma generate --schema=./prisma/schema.prisma").
2190
- * @returns The full command string (e.g., "npx prisma generate --schema=./prisma/schema.prisma").
2191
- */
2192
- function getPackageExecutionCommand(packageManager, commandWithArgs) {
2193
- switch (packageManager) {
2194
- case "pnpm": return `pnpm dlx ${commandWithArgs}`;
2195
- case "bun": return `bunx ${commandWithArgs}`;
2196
- 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.`);
2197
2129
  }
2198
- }
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
2199
2142
  /**
2200
2143
  * Returns the command and arguments as an array for use with execa's $ template syntax.
2201
2144
  * This avoids the need for shell: true and provides better escaping.
@@ -2305,15 +2248,23 @@ async function setupFumadocs(config) {
2305
2248
 
2306
2249
  //#endregion
2307
2250
  //#region src/helpers/addons/oxlint-setup.ts
2308
- /**
2309
- * Oxlint setup - CLI-only operations
2310
- * NOTE: Dependencies are handled by template-generator's addons-deps.ts processor
2311
- * This file only handles external CLI initialization (oxlint --init, oxfmt --init)
2312
- */
2313
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
+ }
2314
2265
  const s = spinner();
2315
- s.start("Initializing oxlint and oxfmt...");
2316
2266
  const oxlintArgs = getPackageExecutionArgs(packageManager, "oxlint@latest --init");
2267
+ s.start("Initializing oxlint and oxfmt...");
2317
2268
  await $({
2318
2269
  cwd: projectDir,
2319
2270
  env: { CI: "true" }
@@ -2326,94 +2277,6 @@ async function setupOxlint(projectDir, packageManager) {
2326
2277
  s.stop("oxlint and oxfmt initialized successfully!");
2327
2278
  }
2328
2279
 
2329
- //#endregion
2330
- //#region src/helpers/addons/ruler-setup.ts
2331
- async function setupRuler(config) {
2332
- const { packageManager, projectDir } = config;
2333
- try {
2334
- log.info("Setting up Ruler...");
2335
- const rulerDir = path.join(projectDir, ".ruler");
2336
- if (!await fs.pathExists(rulerDir)) {
2337
- log.error(pc.red("Ruler template directory not found. Please ensure ruler addon is properly installed."));
2338
- return;
2339
- }
2340
- const selectedEditors = await autocompleteMultiselect({
2341
- message: "Select AI assistants for Ruler",
2342
- options: Object.entries({
2343
- amp: { label: "AMP" },
2344
- copilot: { label: "GitHub Copilot" },
2345
- claude: { label: "Claude Code" },
2346
- codex: { label: "OpenAI Codex CLI" },
2347
- cursor: { label: "Cursor" },
2348
- windsurf: { label: "Windsurf" },
2349
- cline: { label: "Cline" },
2350
- aider: { label: "Aider" },
2351
- firebase: { label: "Firebase Studio" },
2352
- "gemini-cli": { label: "Gemini CLI" },
2353
- junie: { label: "Junie" },
2354
- kilocode: { label: "Kilo Code" },
2355
- opencode: { label: "OpenCode" },
2356
- crush: { label: "Crush" },
2357
- zed: { label: "Zed" },
2358
- qwen: { label: "Qwen" },
2359
- amazonqcli: { label: "Amazon Q CLI" },
2360
- augmentcode: { label: "AugmentCode" },
2361
- firebender: { label: "Firebender" },
2362
- goose: { label: "Goose" },
2363
- jules: { label: "Jules" },
2364
- kiro: { label: "Kiro" },
2365
- openhands: { label: "Open Hands" },
2366
- roo: { label: "RooCode" },
2367
- trae: { label: "Trae AI" },
2368
- warp: { label: "Warp" }
2369
- }).map(([key, v]) => ({
2370
- value: key,
2371
- label: v.label
2372
- })),
2373
- required: false
2374
- });
2375
- if (isCancel(selectedEditors)) return exitCancelled("Operation cancelled");
2376
- if (selectedEditors.length === 0) {
2377
- log.info("No AI assistants selected. To apply rules later, run:");
2378
- log.info(pc.cyan(`${getPackageExecutionCommand(packageManager, "@intellectronica/ruler@latest apply --local-only")}`));
2379
- return;
2380
- }
2381
- const configFile = path.join(rulerDir, "ruler.toml");
2382
- let updatedConfig = await fs.readFile(configFile, "utf-8");
2383
- const defaultAgentsLine = `default_agents = [${selectedEditors.map((editor) => `"${editor}"`).join(", ")}]`;
2384
- updatedConfig = updatedConfig.replace(/default_agents = \[\]/, defaultAgentsLine);
2385
- await fs.writeFile(configFile, updatedConfig);
2386
- await addRulerScriptToPackageJson(projectDir, packageManager);
2387
- const s = spinner();
2388
- s.start("Applying rules with Ruler...");
2389
- try {
2390
- const rulerApplyArgs = getPackageExecutionArgs(packageManager, `@intellectronica/ruler@latest apply --agents ${selectedEditors.join(",")} --local-only`);
2391
- await $({
2392
- cwd: projectDir,
2393
- env: { CI: "true" }
2394
- })`${rulerApplyArgs}`;
2395
- s.stop("Applied rules with Ruler");
2396
- } catch {
2397
- s.stop(pc.red("Failed to apply rules"));
2398
- }
2399
- } catch (error) {
2400
- log.error(pc.red("Failed to set up Ruler"));
2401
- if (error instanceof Error) console.error(pc.red(error.message));
2402
- }
2403
- }
2404
- async function addRulerScriptToPackageJson(projectDir, packageManager) {
2405
- const rootPackageJsonPath = path.join(projectDir, "package.json");
2406
- if (!await fs.pathExists(rootPackageJsonPath)) {
2407
- log.warn("Root package.json not found, skipping ruler:apply script addition");
2408
- return;
2409
- }
2410
- const packageJson = await fs.readJson(rootPackageJsonPath);
2411
- if (!packageJson.scripts) packageJson.scripts = {};
2412
- const rulerApplyCommand = getPackageExecutionCommand(packageManager, "@intellectronica/ruler@latest apply --local-only");
2413
- packageJson.scripts["ruler:apply"] = rulerApplyCommand;
2414
- await fs.writeJson(rootPackageJsonPath, packageJson, { spaces: 2 });
2415
- }
2416
-
2417
2280
  //#endregion
2418
2281
  //#region src/helpers/addons/starlight-setup.ts
2419
2282
  async function setupStarlight(config) {
@@ -2529,38 +2392,57 @@ async function setupTui(config) {
2529
2392
 
2530
2393
  //#endregion
2531
2394
  //#region src/helpers/addons/ultracite-setup.ts
2532
- /**
2533
- * Ultracite setup - CLI-only operations
2534
- * NOTE: Dependencies (husky, lint-staged) are handled by template-generator's addons-deps.ts
2535
- * This file handles interactive prompts and external CLI initialization
2536
- */
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
+ };
2537
2409
  const EDITORS = {
2538
- vscode: { label: "VSCode / Cursor / Windsurf" },
2410
+ vscode: { label: "VS Code" },
2411
+ cursor: { label: "Cursor" },
2412
+ windsurf: { label: "Windsurf" },
2413
+ antigravity: { label: "Antigravity" },
2414
+ kiro: { label: "Kiro" },
2415
+ trae: { label: "Trae" },
2416
+ void: { label: "Void" },
2539
2417
  zed: { label: "Zed" }
2540
2418
  };
2541
2419
  const AGENTS = {
2542
- "vscode-copilot": { label: "VS Code Copilot" },
2543
- cursor: { label: "Cursor" },
2544
- windsurf: { label: "Windsurf" },
2545
- zed: { label: "Zed" },
2546
2420
  claude: { label: "Claude" },
2547
2421
  codex: { label: "Codex" },
2548
- kiro: { label: "Kiro" },
2422
+ jules: { label: "Jules" },
2423
+ copilot: { label: "GitHub Copilot" },
2549
2424
  cline: { label: "Cline" },
2550
2425
  amp: { label: "Amp" },
2551
2426
  aider: { label: "Aider" },
2552
2427
  "firebase-studio": { label: "Firebase Studio" },
2553
2428
  "open-hands": { label: "Open Hands" },
2554
- "gemini-cli": { label: "Gemini CLI" },
2429
+ gemini: { label: "Gemini" },
2555
2430
  junie: { label: "Junie" },
2556
2431
  augmentcode: { label: "AugmentCode" },
2557
2432
  "kilo-code": { label: "Kilo Code" },
2558
2433
  goose: { label: "Goose" },
2559
- "roo-code": { label: "Roo Code" }
2434
+ "roo-code": { label: "Roo Code" },
2435
+ warp: { label: "Warp" },
2436
+ droid: { label: "Droid" },
2437
+ opencode: { label: "OpenCode" },
2438
+ crush: { label: "Crush" },
2439
+ qwen: { label: "Qwen" },
2440
+ "amazon-q-cli": { label: "Amazon Q CLI" },
2441
+ firebender: { label: "Firebender" }
2560
2442
  };
2561
2443
  const HOOKS = {
2562
2444
  cursor: { label: "Cursor" },
2563
- claude: { label: "Claude" }
2445
+ windsurf: { label: "Windsurf" }
2564
2446
  };
2565
2447
  function getFrameworksFromFrontend(frontend) {
2566
2448
  const frameworkMap = {
@@ -2584,6 +2466,15 @@ async function setupUltracite(config, hasHusky) {
2584
2466
  try {
2585
2467
  log.info("Setting up Ultracite...");
2586
2468
  const result = await group({
2469
+ linter: () => select({
2470
+ message: "Choose linter/formatter",
2471
+ options: Object.entries(LINTERS).map(([key, linter$1]) => ({
2472
+ value: key,
2473
+ label: linter$1.label,
2474
+ hint: linter$1.hint
2475
+ })),
2476
+ initialValue: "biome"
2477
+ }),
2587
2478
  editors: () => multiselect({
2588
2479
  message: "Choose editors",
2589
2480
  options: Object.entries(EDITORS).map(([key, editor]) => ({
@@ -2592,7 +2483,7 @@ async function setupUltracite(config, hasHusky) {
2592
2483
  })),
2593
2484
  required: true
2594
2485
  }),
2595
- agents: () => autocompleteMultiselect({
2486
+ agents: () => multiselect({
2596
2487
  message: "Choose agents",
2597
2488
  options: Object.entries(AGENTS).map(([key, agent]) => ({
2598
2489
  value: key,
@@ -2600,7 +2491,7 @@ async function setupUltracite(config, hasHusky) {
2600
2491
  })),
2601
2492
  required: true
2602
2493
  }),
2603
- hooks: () => autocompleteMultiselect({
2494
+ hooks: () => multiselect({
2604
2495
  message: "Choose hooks",
2605
2496
  options: Object.entries(HOOKS).map(([key, hook]) => ({
2606
2497
  value: key,
@@ -2610,6 +2501,7 @@ async function setupUltracite(config, hasHusky) {
2610
2501
  }, { onCancel: () => {
2611
2502
  exitCancelled("Operation cancelled");
2612
2503
  } });
2504
+ const linter = result.linter;
2613
2505
  const editors = result.editors;
2614
2506
  const agents = result.agents;
2615
2507
  const hooks = result.hooks;
@@ -2617,7 +2509,9 @@ async function setupUltracite(config, hasHusky) {
2617
2509
  const ultraciteArgs = [
2618
2510
  "init",
2619
2511
  "--pm",
2620
- packageManager
2512
+ packageManager,
2513
+ "--linter",
2514
+ linter
2621
2515
  ];
2622
2516
  if (frameworks.length > 0) ultraciteArgs.push("--frameworks", ...frameworks);
2623
2517
  if (editors.length > 0) ultraciteArgs.push("--editors", ...editors);
@@ -2702,360 +2596,96 @@ async function setupWxt(config) {
2702
2596
 
2703
2597
  //#endregion
2704
2598
  //#region src/helpers/addons/addons-setup.ts
2705
- async function setupAddons(config, isAddCommand = false) {
2706
- const { addons, frontend, projectDir, packageManager } = config;
2599
+ async function setupAddons(config) {
2600
+ const { addons, frontend, projectDir } = config;
2707
2601
  const hasReactWebFrontend = frontend.includes("react-router") || frontend.includes("tanstack-router") || frontend.includes("next");
2708
2602
  const hasNuxtFrontend = frontend.includes("nuxt");
2709
2603
  const hasSvelteFrontend = frontend.includes("svelte");
2710
2604
  const hasSolidFrontend = frontend.includes("solid");
2711
2605
  const hasNextFrontend = frontend.includes("next");
2712
- if (addons.includes("turborepo") && isAddCommand) log.info(`${pc.yellow("Update your package.json scripts:")}
2713
-
2714
- ${pc.dim("Replace:")} ${pc.yellow("\"pnpm -r dev\"")} ${pc.dim("→")} ${pc.green("\"turbo dev\"")}
2715
- ${pc.dim("Replace:")} ${pc.yellow("\"pnpm --filter web dev\"")} ${pc.dim("→")} ${pc.green("\"turbo -F web dev\"")}
2716
-
2717
- ${pc.cyan("Docs:")} ${pc.underline("https://turborepo.com/docs")}
2718
- `);
2719
2606
  if (addons.includes("tauri") && (hasReactWebFrontend || hasNuxtFrontend || hasSvelteFrontend || hasSolidFrontend || hasNextFrontend)) await setupTauri(config);
2720
2607
  const hasUltracite = addons.includes("ultracite");
2608
+ const hasBiome = addons.includes("biome");
2721
2609
  const hasHusky = addons.includes("husky");
2722
2610
  const hasOxlint = addons.includes("oxlint");
2723
- if (hasUltracite) await setupUltracite(config, hasHusky);
2724
- if (hasOxlint) await setupOxlint(projectDir, packageManager);
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
+ }
2725
2621
  if (addons.includes("starlight")) await setupStarlight(config);
2726
- if (addons.includes("ruler")) await setupRuler(config);
2727
2622
  if (addons.includes("fumadocs")) await setupFumadocs(config);
2728
2623
  if (addons.includes("opentui")) await setupTui(config);
2729
2624
  if (addons.includes("wxt")) await setupWxt(config);
2625
+ if (hasUltracite) await setupUltracite(config, hasHusky);
2730
2626
  }
2731
-
2732
- //#endregion
2733
- //#region src/helpers/addons/examples-setup.ts
2734
- async function setupExamples(_config) {}
2735
-
2736
- //#endregion
2737
- //#region src/utils/add-package-deps.ts
2738
- const addPackageDependency = async (opts) => {
2739
- const { dependencies = [], devDependencies = [], customDependencies = {}, customDevDependencies = {}, projectDir } = opts;
2740
- const pkgJsonPath = path.join(projectDir, "package.json");
2741
- const pkgJson = await fs.readJson(pkgJsonPath);
2742
- if (!pkgJson.dependencies) pkgJson.dependencies = {};
2743
- if (!pkgJson.devDependencies) pkgJson.devDependencies = {};
2744
- for (const pkgName of dependencies) {
2745
- const version = dependencyVersionMap[pkgName];
2746
- if (version) pkgJson.dependencies[pkgName] = version;
2747
- 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 });
2748
2640
  }
2749
- for (const pkgName of devDependencies) {
2750
- const version = dependencyVersionMap[pkgName];
2751
- if (version) pkgJson.devDependencies[pkgName] = version;
2752
- 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 });
2753
2658
  }
2754
- for (const [pkgName, version] of Object.entries(customDependencies)) pkgJson.dependencies[pkgName] = version;
2755
- for (const [pkgName, version] of Object.entries(customDevDependencies)) pkgJson.devDependencies[pkgName] = version;
2756
- await fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 });
2757
- };
2758
-
2759
- //#endregion
2760
- //#region src/helpers/core/auth-setup.ts
2761
- function generateAuthSecret(length = 32) {
2762
- const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
2763
- let result = "";
2764
- const charactersLength = 62;
2765
- for (let i = 0; i < length; i++) result += characters.charAt(Math.floor(Math.random() * charactersLength));
2766
- return result;
2767
2659
  }
2768
2660
 
2769
2661
  //#endregion
2770
- //#region src/helpers/core/env-setup.ts
2771
- function getClientServerVar(frontend, backend) {
2772
- const hasNextJs = frontend.includes("next");
2773
- const hasNuxt = frontend.includes("nuxt");
2774
- const hasSvelte = frontend.includes("svelte");
2775
- const hasTanstackStart = frontend.includes("tanstack-start");
2776
- if (backend === "self") return {
2777
- key: "",
2778
- value: "",
2779
- write: false
2780
- };
2781
- let key = "VITE_SERVER_URL";
2782
- if (hasNextJs) key = "NEXT_PUBLIC_SERVER_URL";
2783
- else if (hasNuxt) key = "NUXT_PUBLIC_SERVER_URL";
2784
- else if (hasSvelte) key = "PUBLIC_SERVER_URL";
2785
- else if (hasTanstackStart) key = "VITE_SERVER_URL";
2786
- return {
2787
- key,
2788
- value: "http://localhost:3000",
2789
- write: true
2790
- };
2791
- }
2792
- function getConvexVar(frontend) {
2793
- const hasNextJs = frontend.includes("next");
2794
- const hasNuxt = frontend.includes("nuxt");
2795
- const hasSvelte = frontend.includes("svelte");
2796
- const hasTanstackStart = frontend.includes("tanstack-start");
2797
- if (hasNextJs) return "NEXT_PUBLIC_CONVEX_URL";
2798
- if (hasNuxt) return "NUXT_PUBLIC_CONVEX_URL";
2799
- if (hasSvelte) return "PUBLIC_CONVEX_URL";
2800
- if (hasTanstackStart) return "VITE_CONVEX_URL";
2801
- return "VITE_CONVEX_URL";
2802
- }
2803
- async function addEnvVariablesToFile(filePath, variables) {
2804
- await fs.ensureDir(path.dirname(filePath));
2805
- let envContent = "";
2806
- if (await fs.pathExists(filePath)) envContent = await fs.readFile(filePath, "utf8");
2807
- let modified = false;
2808
- let contentToAdd = "";
2809
- const exampleVariables = [];
2810
- for (const { key, value, condition, comment } of variables) if (condition) {
2811
- const regex = new RegExp(`^${key}=.*$`, "m");
2812
- const valueToWrite = value ?? "";
2813
- exampleVariables.push(`${key}=`);
2814
- if (regex.test(envContent)) {
2815
- const existingMatch = envContent.match(regex);
2816
- if (existingMatch && existingMatch[0] !== `${key}=${valueToWrite}`) {
2817
- envContent = envContent.replace(regex, `${key}=${valueToWrite}`);
2818
- modified = true;
2819
- }
2820
- } else {
2821
- if (comment) contentToAdd += `# ${comment}\n`;
2822
- contentToAdd += `${key}=${valueToWrite}\n`;
2823
- modified = true;
2824
- }
2825
- }
2826
- if (contentToAdd) {
2827
- if (envContent.length > 0 && !envContent.endsWith("\n")) envContent += "\n";
2828
- envContent += contentToAdd;
2829
- }
2830
- if (modified) await fs.writeFile(filePath, envContent.trimEnd());
2831
- const exampleFilePath = filePath.replace(/\.env$/, ".env.example");
2832
- let exampleEnvContent = "";
2833
- if (await fs.pathExists(exampleFilePath)) exampleEnvContent = await fs.readFile(exampleFilePath, "utf8");
2834
- let exampleModified = false;
2835
- let exampleContentToAdd = "";
2836
- for (const exampleVar of exampleVariables) {
2837
- const key = exampleVar.split("=")[0];
2838
- if (!new RegExp(`^${key}=.*$`, "m").test(exampleEnvContent)) {
2839
- exampleContentToAdd += `${exampleVar}\n`;
2840
- exampleModified = true;
2841
- }
2842
- }
2843
- if (exampleContentToAdd) {
2844
- if (exampleEnvContent.length > 0 && !exampleEnvContent.endsWith("\n")) exampleEnvContent += "\n";
2845
- exampleEnvContent += exampleContentToAdd;
2846
- }
2847
- if (exampleModified || !await fs.pathExists(exampleFilePath)) await fs.writeFile(exampleFilePath, exampleEnvContent.trimEnd());
2848
- }
2849
- async function setupEnvironmentVariables(config) {
2850
- const { backend, frontend, database, auth, examples, dbSetup, projectDir, webDeploy, serverDeploy } = config;
2851
- const hasReactRouter = frontend.includes("react-router");
2852
- const hasTanStackRouter = frontend.includes("tanstack-router");
2853
- const hasTanStackStart = frontend.includes("tanstack-start");
2854
- const hasNextJs = frontend.includes("next");
2855
- const hasNuxt = frontend.includes("nuxt");
2856
- const hasSvelte = frontend.includes("svelte");
2857
- const hasSolid = frontend.includes("solid");
2858
- const hasWebFrontend$1 = hasReactRouter || hasTanStackRouter || hasTanStackStart || hasNextJs || hasNuxt || hasSolid || hasSvelte;
2859
- if (hasWebFrontend$1) {
2860
- const clientDir = path.join(projectDir, "apps/web");
2861
- if (await fs.pathExists(clientDir)) {
2862
- const baseVar = getClientServerVar(frontend, backend);
2863
- const clientVars = [{
2864
- key: backend === "convex" ? getConvexVar(frontend) : baseVar.key,
2865
- value: backend === "convex" ? "https://<YOUR_CONVEX_URL>" : baseVar.value,
2866
- condition: backend === "convex" ? true : baseVar.write
2867
- }];
2868
- if (backend === "convex" && auth === "clerk") {
2869
- if (hasNextJs) clientVars.push({
2870
- key: "NEXT_PUBLIC_CLERK_FRONTEND_API_URL",
2871
- value: "",
2872
- condition: true
2873
- }, {
2874
- key: "NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY",
2875
- value: "",
2876
- condition: true
2877
- }, {
2878
- key: "CLERK_SECRET_KEY",
2879
- value: "",
2880
- condition: true
2881
- });
2882
- else if (hasReactRouter || hasTanStackRouter || hasTanStackStart) {
2883
- clientVars.push({
2884
- key: "VITE_CLERK_PUBLISHABLE_KEY",
2885
- value: "",
2886
- condition: true
2887
- });
2888
- if (hasTanStackStart) clientVars.push({
2889
- key: "CLERK_SECRET_KEY",
2890
- value: "",
2891
- condition: true
2892
- });
2893
- }
2894
- }
2895
- if (backend === "convex" && auth === "better-auth") {
2896
- if (hasNextJs) clientVars.push({
2897
- key: "NEXT_PUBLIC_CONVEX_SITE_URL",
2898
- value: "https://<YOUR_CONVEX_URL>",
2899
- condition: true
2900
- });
2901
- else if (hasReactRouter || hasTanStackRouter || hasTanStackStart) clientVars.push({
2902
- key: "VITE_CONVEX_SITE_URL",
2903
- value: "https://<YOUR_CONVEX_URL>",
2904
- condition: true
2905
- });
2906
- }
2907
- await addEnvVariablesToFile(path.join(clientDir, ".env"), clientVars);
2908
- }
2909
- }
2910
- if (frontend.includes("native-bare") || frontend.includes("native-uniwind") || frontend.includes("native-unistyles")) {
2911
- const nativeDir = path.join(projectDir, "apps/native");
2912
- if (await fs.pathExists(nativeDir)) {
2913
- let envVarName = "EXPO_PUBLIC_SERVER_URL";
2914
- let serverUrl = "http://localhost:3000";
2915
- if (backend === "self") serverUrl = "http://localhost:3001";
2916
- if (backend === "convex") {
2917
- envVarName = "EXPO_PUBLIC_CONVEX_URL";
2918
- serverUrl = "https://<YOUR_CONVEX_URL>";
2919
- }
2920
- const nativeVars = [{
2921
- key: envVarName,
2922
- value: serverUrl,
2923
- condition: true
2924
- }];
2925
- if (backend === "convex" && auth === "clerk") nativeVars.push({
2926
- key: "EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY",
2927
- value: "",
2928
- condition: true
2929
- });
2930
- if (backend === "convex" && auth === "better-auth") nativeVars.push({
2931
- key: "EXPO_PUBLIC_CONVEX_SITE_URL",
2932
- value: "https://<YOUR_CONVEX_URL>",
2933
- condition: true
2934
- });
2935
- await addEnvVariablesToFile(path.join(nativeDir, ".env"), nativeVars);
2936
- }
2937
- }
2938
- if (backend === "convex") {
2939
- const convexBackendDir = path.join(projectDir, "packages/backend");
2940
- if (await fs.pathExists(convexBackendDir)) {
2941
- const envLocalPath = path.join(convexBackendDir, ".env.local");
2942
- let commentBlocks = "";
2943
- if (examples?.includes("ai")) commentBlocks += `# Set Google AI API key for AI agent
2944
- # npx convex env set GOOGLE_GENERATIVE_AI_API_KEY=your_google_api_key
2945
-
2946
- `;
2947
- if (auth === "better-auth") commentBlocks += `# Set Convex environment variables
2948
- # npx convex env set BETTER_AUTH_SECRET=$(openssl rand -base64 32)
2949
- ${hasWebFrontend$1 ? "# npx convex env set SITE_URL http://localhost:3001\n" : ""}`;
2950
- if (commentBlocks) {
2951
- let existingContent = "";
2952
- if (await fs.pathExists(envLocalPath)) existingContent = await fs.readFile(envLocalPath, "utf8");
2953
- await fs.writeFile(envLocalPath, commentBlocks + existingContent);
2954
- }
2955
- const convexBackendVars = [];
2956
- if (examples?.includes("ai")) convexBackendVars.push({
2957
- key: "GOOGLE_GENERATIVE_AI_API_KEY",
2958
- value: "",
2959
- condition: true,
2960
- comment: "Google AI API key for AI agent"
2961
- });
2962
- if (auth === "better-auth") {
2963
- const hasNative = frontend.includes("native-bare") || frontend.includes("native-uniwind") || frontend.includes("native-unistyles");
2964
- const hasWeb = hasWebFrontend$1;
2965
- if (hasNative) convexBackendVars.push({
2966
- key: "EXPO_PUBLIC_CONVEX_SITE_URL",
2967
- value: "",
2968
- condition: true,
2969
- comment: "Same as CONVEX_URL but ends in .site"
2970
- });
2971
- if (hasWeb) convexBackendVars.push({
2972
- key: hasNextJs ? "NEXT_PUBLIC_CONVEX_SITE_URL" : "VITE_CONVEX_SITE_URL",
2973
- value: "",
2974
- condition: true,
2975
- comment: "Same as CONVEX_URL but ends in .site"
2976
- }, {
2977
- key: "SITE_URL",
2978
- value: "http://localhost:3001",
2979
- condition: true,
2980
- comment: "Web app URL for authentication"
2981
- });
2982
- }
2983
- if (convexBackendVars.length > 0) await addEnvVariablesToFile(envLocalPath, convexBackendVars);
2984
- }
2985
- return;
2986
- }
2987
- const serverDir = path.join(projectDir, "apps/server");
2988
- let corsOrigin = "http://localhost:3001";
2989
- if (backend === "self") corsOrigin = "http://localhost:3001";
2990
- else if (hasReactRouter || hasSvelte) corsOrigin = "http://localhost:5173";
2991
- let databaseUrl = null;
2992
- if (database !== "none" && dbSetup === "none") switch (database) {
2993
- case "postgres":
2994
- databaseUrl = "postgresql://postgres:password@localhost:5432/postgres";
2995
- break;
2996
- case "mysql":
2997
- databaseUrl = "mysql://root:password@localhost:3306/mydb";
2998
- break;
2999
- case "mongodb":
3000
- databaseUrl = "mongodb://localhost:27017/mydatabase";
3001
- break;
3002
- case "sqlite":
3003
- if (config.runtime === "workers" || webDeploy === "cloudflare" || serverDeploy === "cloudflare") databaseUrl = "http://127.0.0.1:8080";
3004
- else {
3005
- const dbAppDir = backend === "self" ? "apps/web" : "apps/server";
3006
- databaseUrl = `file:${path.join(config.projectDir, dbAppDir, "local.db")}`;
3007
- }
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;
3008
2682
  break;
3009
- }
3010
- const serverVars = [
3011
- {
3012
- key: "BETTER_AUTH_SECRET",
3013
- value: generateAuthSecret(),
3014
- condition: !!auth
3015
- },
3016
- {
3017
- key: "BETTER_AUTH_URL",
3018
- value: backend === "self" ? "http://localhost:3001" : "http://localhost:3000",
3019
- condition: !!auth
3020
- },
3021
- {
3022
- key: "POLAR_ACCESS_TOKEN",
3023
- value: "",
3024
- condition: config.payments === "polar"
3025
- },
3026
- {
3027
- key: "POLAR_SUCCESS_URL",
3028
- value: `${corsOrigin}/success?checkout_id={CHECKOUT_ID}`,
3029
- condition: config.payments === "polar"
3030
- },
3031
- {
3032
- key: "CORS_ORIGIN",
3033
- value: corsOrigin,
3034
- condition: true
3035
- },
3036
- {
3037
- key: "GOOGLE_GENERATIVE_AI_API_KEY",
3038
- value: "",
3039
- condition: examples?.includes("ai") || false
3040
- },
3041
- {
3042
- key: "DATABASE_URL",
3043
- value: databaseUrl,
3044
- condition: database !== "none" && dbSetup === "none"
3045
2683
  }
3046
- ];
3047
- if (backend === "self") {
3048
- const webDir = path.join(projectDir, "apps/web");
3049
- if (await fs.pathExists(webDir)) await addEnvVariablesToFile(path.join(webDir, ".env"), serverVars);
3050
- } else if (await fs.pathExists(serverDir)) await addEnvVariablesToFile(path.join(serverDir, ".env"), serverVars);
3051
- if (webDeploy === "cloudflare" && serverDeploy === "cloudflare" || webDeploy === "cloudflare" || serverDeploy === "cloudflare") {
3052
- const infraDir = path.join(projectDir, "packages/infra");
3053
- if (await fs.pathExists(infraDir)) await addEnvVariablesToFile(path.join(infraDir, ".env"), [{
3054
- key: "ALCHEMY_PASSWORD",
3055
- value: "please-change-this",
3056
- condition: true
3057
- }]);
2684
+ if (!lineProcessed) newLines.push(line);
3058
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");
3059
2689
  }
3060
2690
 
3061
2691
  //#endregion
@@ -4007,314 +3637,6 @@ async function setupDatabase(config, cliInput) {
4007
3637
  }
4008
3638
  }
4009
3639
 
4010
- //#endregion
4011
- //#region src/helpers/deployment/alchemy/alchemy-next-setup.ts
4012
- /**
4013
- * Alchemy Next.js setup - CLI-only operations
4014
- * NOTE: Dependencies are handled by template-generator's deploy-deps.ts
4015
- * This only modifies config files for "add deploy" command
4016
- */
4017
- async function setupNextAlchemyDeploy(projectDir, _packageManager, _options) {
4018
- const webAppDir = path.join(projectDir, "apps/web");
4019
- if (!await fs.pathExists(webAppDir)) return;
4020
- const openNextConfigPath = path.join(webAppDir, "open-next.config.ts");
4021
- await fs.writeFile(openNextConfigPath, `import { defineCloudflareConfig } from "@opennextjs/cloudflare";
4022
-
4023
- export default defineCloudflareConfig({});
4024
- `);
4025
- const gitignorePath = path.join(webAppDir, ".gitignore");
4026
- if (await fs.pathExists(gitignorePath)) {
4027
- if (!(await fs.readFile(gitignorePath, "utf-8")).includes("wrangler.jsonc")) await fs.appendFile(gitignorePath, "\nwrangler.jsonc\n");
4028
- } else await fs.writeFile(gitignorePath, "wrangler.jsonc\n");
4029
- }
4030
-
4031
- //#endregion
4032
- //#region src/helpers/deployment/alchemy/alchemy-nuxt-setup.ts
4033
- /**
4034
- * Alchemy Nuxt setup - CLI-only operations
4035
- * NOTE: Dependencies are handled by template-generator's deploy-deps.ts
4036
- * This only modifies config files for "add deploy" command
4037
- */
4038
- async function setupNuxtAlchemyDeploy(projectDir, _packageManager, _options) {
4039
- const webAppDir = path.join(projectDir, "apps/web");
4040
- if (!await fs.pathExists(webAppDir)) return;
4041
- const nuxtConfigPath = path.join(webAppDir, "nuxt.config.ts");
4042
- if (!await fs.pathExists(nuxtConfigPath)) return;
4043
- try {
4044
- const project = new Project({ manipulationSettings: {
4045
- indentationText: IndentationText.TwoSpaces,
4046
- quoteKind: QuoteKind.Double
4047
- } });
4048
- project.addSourceFileAtPath(nuxtConfigPath);
4049
- const exportAssignment = project.getSourceFileOrThrow(nuxtConfigPath).getExportAssignment((d) => !d.isExportEquals());
4050
- if (!exportAssignment) return;
4051
- const defineConfigCall = exportAssignment.getExpression();
4052
- if (!Node.isCallExpression(defineConfigCall) || defineConfigCall.getExpression().getText() !== "defineNuxtConfig") return;
4053
- let configObject = defineConfigCall.getArguments()[0];
4054
- if (!configObject) configObject = defineConfigCall.addArgument("{}");
4055
- if (Node.isObjectLiteralExpression(configObject)) {
4056
- if (!configObject.getProperty("nitro")) configObject.addPropertyAssignment({
4057
- name: "nitro",
4058
- initializer: `{
4059
- preset: "cloudflare_module",
4060
- cloudflare: {
4061
- deployConfig: true,
4062
- nodeCompat: true
4063
- }
4064
- }`
4065
- });
4066
- const modulesProperty = configObject.getProperty("modules");
4067
- if (modulesProperty && Node.isPropertyAssignment(modulesProperty)) {
4068
- const initializer = modulesProperty.getInitializer();
4069
- if (Node.isArrayLiteralExpression(initializer)) {
4070
- if (!initializer.getElements().some((el) => el.getText() === "\"nitro-cloudflare-dev\"" || el.getText() === "'nitro-cloudflare-dev'")) initializer.addElement("\"nitro-cloudflare-dev\"");
4071
- }
4072
- } else if (!modulesProperty) configObject.addPropertyAssignment({
4073
- name: "modules",
4074
- initializer: "[\"nitro-cloudflare-dev\"]"
4075
- });
4076
- }
4077
- await project.save();
4078
- } catch (error) {
4079
- console.warn("Failed to update nuxt.config.ts:", error);
4080
- }
4081
- }
4082
-
4083
- //#endregion
4084
- //#region src/helpers/deployment/alchemy/alchemy-svelte-setup.ts
4085
- /**
4086
- * Alchemy Svelte setup - CLI-only operations
4087
- * NOTE: Dependencies are handled by template-generator's deploy-deps.ts
4088
- * This only modifies config files for "add deploy" command
4089
- */
4090
- async function setupSvelteAlchemyDeploy(projectDir, _packageManager, _options) {
4091
- const webAppDir = path.join(projectDir, "apps/web");
4092
- if (!await fs.pathExists(webAppDir)) return;
4093
- const svelteConfigPath = path.join(webAppDir, "svelte.config.js");
4094
- if (!await fs.pathExists(svelteConfigPath)) return;
4095
- try {
4096
- const project = new Project({ manipulationSettings: {
4097
- indentationText: IndentationText.TwoSpaces,
4098
- quoteKind: QuoteKind.Single
4099
- } });
4100
- project.addSourceFileAtPath(svelteConfigPath);
4101
- const sourceFile = project.getSourceFileOrThrow(svelteConfigPath);
4102
- const adapterImport = sourceFile.getImportDeclarations().find((imp) => imp.getModuleSpecifierValue().includes("@sveltejs/adapter"));
4103
- if (adapterImport) {
4104
- adapterImport.setModuleSpecifier("alchemy/cloudflare/sveltekit");
4105
- adapterImport.removeDefaultImport();
4106
- adapterImport.setDefaultImport("alchemy");
4107
- } else sourceFile.insertImportDeclaration(0, {
4108
- moduleSpecifier: "alchemy/cloudflare/sveltekit",
4109
- defaultImport: "alchemy"
4110
- });
4111
- const configVariable = sourceFile.getVariableDeclaration("config");
4112
- if (configVariable) {
4113
- const initializer = configVariable.getInitializer();
4114
- if (Node.isObjectLiteralExpression(initializer)) updateAdapterInConfig(initializer);
4115
- }
4116
- await project.save();
4117
- } catch (error) {
4118
- console.warn("Failed to update svelte.config.js:", error);
4119
- }
4120
- }
4121
- function updateAdapterInConfig(configObject) {
4122
- if (!Node.isObjectLiteralExpression(configObject)) return;
4123
- const kitProperty = configObject.getProperty("kit");
4124
- if (kitProperty && Node.isPropertyAssignment(kitProperty)) {
4125
- const kitInitializer = kitProperty.getInitializer();
4126
- if (Node.isObjectLiteralExpression(kitInitializer)) {
4127
- const adapterProperty = kitInitializer.getProperty("adapter");
4128
- if (adapterProperty && Node.isPropertyAssignment(adapterProperty)) {
4129
- const initializer = adapterProperty.getInitializer();
4130
- if (Node.isCallExpression(initializer)) {
4131
- const expression = initializer.getExpression();
4132
- if (Node.isIdentifier(expression) && expression.getText() === "adapter") expression.replaceWithText("alchemy");
4133
- }
4134
- }
4135
- }
4136
- }
4137
- }
4138
-
4139
- //#endregion
4140
- //#region src/helpers/deployment/alchemy/alchemy-tanstack-start-setup.ts
4141
- /**
4142
- * Alchemy TanStack Start setup - CLI-only operations
4143
- * NOTE: Dependencies are handled by template-generator's deploy-deps.ts
4144
- * This only modifies vite.config.ts for "add deploy" command
4145
- */
4146
- async function setupTanStackStartAlchemyDeploy(projectDir, _packageManager, _options) {
4147
- const webAppDir = path.join(projectDir, "apps/web");
4148
- if (!await fs.pathExists(webAppDir)) return;
4149
- const viteConfigPath = path.join(webAppDir, "vite.config.ts");
4150
- if (await fs.pathExists(viteConfigPath)) try {
4151
- const project = new Project({ manipulationSettings: {
4152
- indentationText: IndentationText.TwoSpaces,
4153
- quoteKind: QuoteKind.Double
4154
- } });
4155
- project.addSourceFileAtPath(viteConfigPath);
4156
- const sourceFile = project.getSourceFileOrThrow(viteConfigPath);
4157
- const alchemyImport = sourceFile.getImportDeclaration("alchemy/cloudflare/tanstack-start");
4158
- if (!alchemyImport) sourceFile.addImportDeclaration({
4159
- moduleSpecifier: "alchemy/cloudflare/tanstack-start",
4160
- defaultImport: "alchemy"
4161
- });
4162
- else alchemyImport.setModuleSpecifier("alchemy/cloudflare/tanstack-start");
4163
- const exportAssignment = sourceFile.getExportAssignment((d) => !d.isExportEquals());
4164
- if (!exportAssignment) return;
4165
- const defineConfigCall = exportAssignment.getExpression();
4166
- if (!Node.isCallExpression(defineConfigCall) || defineConfigCall.getExpression().getText() !== "defineConfig") return;
4167
- let configObject = defineConfigCall.getArguments()[0];
4168
- if (!configObject) configObject = defineConfigCall.addArgument("{}");
4169
- if (Node.isObjectLiteralExpression(configObject)) {
4170
- const pluginsProperty = configObject.getProperty("plugins");
4171
- if (pluginsProperty && Node.isPropertyAssignment(pluginsProperty)) {
4172
- const initializer = pluginsProperty.getInitializer();
4173
- if (Node.isArrayLiteralExpression(initializer)) {
4174
- if (!initializer.getElements().some((el) => el.getText().includes("alchemy("))) initializer.addElement("alchemy()");
4175
- }
4176
- } else configObject.addPropertyAssignment({
4177
- name: "plugins",
4178
- initializer: "[alchemy()]"
4179
- });
4180
- }
4181
- await project.save();
4182
- } catch (error) {
4183
- console.warn("Failed to update vite.config.ts:", error);
4184
- }
4185
- }
4186
-
4187
- //#endregion
4188
- //#region src/helpers/deployment/alchemy/alchemy-vite-setup.ts
4189
- /**
4190
- * Alchemy setup for Vite-based frontends (React Router, Solid, TanStack Router)
4191
- * NOTE: Dependencies are handled by template-generator's deploy-deps.ts
4192
- * These frontends don't need config modifications - templates already have correct setup
4193
- */
4194
- async function setupReactRouterAlchemyDeploy(projectDir, _packageManager, _options) {
4195
- const webAppDir = path.join(projectDir, "apps/web");
4196
- if (!await fs.pathExists(webAppDir)) return;
4197
- }
4198
- async function setupSolidAlchemyDeploy(projectDir, _packageManager, _options) {
4199
- const webAppDir = path.join(projectDir, "apps/web");
4200
- if (!await fs.pathExists(webAppDir)) return;
4201
- }
4202
- async function setupTanStackRouterAlchemyDeploy(projectDir, _packageManager, _options) {
4203
- const webAppDir = path.join(projectDir, "apps/web");
4204
- if (!await fs.pathExists(webAppDir)) return;
4205
- }
4206
-
4207
- //#endregion
4208
- //#region src/helpers/deployment/alchemy/alchemy-combined-setup.ts
4209
- function getInfraFilter(packageManager, hasTurborepo, infraWorkspace) {
4210
- if (hasTurborepo) return (script) => `turbo -F ${infraWorkspace} ${script}`;
4211
- switch (packageManager) {
4212
- case "pnpm": return (script) => `pnpm --filter ${infraWorkspace} ${script}`;
4213
- case "npm": return (script) => `npm run ${script} --workspace ${infraWorkspace}`;
4214
- case "bun": return (script) => `bun run --filter ${infraWorkspace} ${script}`;
4215
- }
4216
- }
4217
- async function setupCombinedAlchemyDeploy(projectDir, packageManager, config) {
4218
- await setupInfraScripts(projectDir, packageManager, config);
4219
- const frontend = config.frontend;
4220
- const isNext = frontend.includes("next");
4221
- const isNuxt = frontend.includes("nuxt");
4222
- const isSvelte = frontend.includes("svelte");
4223
- const isTanstackRouter = frontend.includes("tanstack-router");
4224
- const isTanstackStart = frontend.includes("tanstack-start");
4225
- const isReactRouter = frontend.includes("react-router");
4226
- const isSolid = frontend.includes("solid");
4227
- if (isNext) await setupNextAlchemyDeploy(projectDir, packageManager, { skipAppScripts: true });
4228
- else if (isNuxt) await setupNuxtAlchemyDeploy(projectDir, packageManager, { skipAppScripts: true });
4229
- else if (isSvelte) await setupSvelteAlchemyDeploy(projectDir, packageManager, { skipAppScripts: true });
4230
- else if (isTanstackStart) await setupTanStackStartAlchemyDeploy(projectDir, packageManager, { skipAppScripts: true });
4231
- else if (isTanstackRouter) await setupTanStackRouterAlchemyDeploy(projectDir, packageManager, { skipAppScripts: true });
4232
- else if (isReactRouter) await setupReactRouterAlchemyDeploy(projectDir, packageManager, { skipAppScripts: true });
4233
- else if (isSolid) await setupSolidAlchemyDeploy(projectDir, packageManager, { skipAppScripts: true });
4234
- }
4235
- async function setupInfraScripts(projectDir, packageManager, config) {
4236
- const projectName = config.projectName;
4237
- const hasTurborepo = config.addons.includes("turborepo");
4238
- const infraWorkspace = `@${projectName}/infra`;
4239
- const rootPkgPath = path.join(projectDir, "package.json");
4240
- if (await fs.pathExists(rootPkgPath)) {
4241
- const pkg = await fs.readJson(rootPkgPath);
4242
- const filter = getInfraFilter(packageManager, hasTurborepo, infraWorkspace);
4243
- pkg.scripts = {
4244
- ...pkg.scripts,
4245
- deploy: filter("deploy"),
4246
- destroy: filter("destroy")
4247
- };
4248
- await fs.writeJson(rootPkgPath, pkg, { spaces: 2 });
4249
- }
4250
- if (config.serverDeploy === "cloudflare") {
4251
- const serverPkgPath = path.join(projectDir, "apps/server/package.json");
4252
- if (await fs.pathExists(serverPkgPath)) {
4253
- const serverPkg = await fs.readJson(serverPkgPath);
4254
- if (serverPkg.scripts?.dev) {
4255
- serverPkg.scripts["dev:bare"] = serverPkg.scripts.dev;
4256
- delete serverPkg.scripts.dev;
4257
- await fs.writeJson(serverPkgPath, serverPkg, { spaces: 2 });
4258
- }
4259
- }
4260
- }
4261
- if (config.webDeploy === "cloudflare") {
4262
- const webPkgPath = path.join(projectDir, "apps/web/package.json");
4263
- if (await fs.pathExists(webPkgPath)) {
4264
- const webPkg = await fs.readJson(webPkgPath);
4265
- if (webPkg.scripts?.dev) {
4266
- webPkg.scripts["dev:bare"] = webPkg.scripts.dev;
4267
- delete webPkg.scripts.dev;
4268
- await fs.writeJson(webPkgPath, webPkg, { spaces: 2 });
4269
- }
4270
- }
4271
- }
4272
- }
4273
-
4274
- //#endregion
4275
- //#region src/helpers/deployment/server-deploy-setup.ts
4276
- /**
4277
- * Server deploy setup - CLI-only operations
4278
- * NOTE: Dependencies are handled by template-generator's deploy-deps.ts processor
4279
- * This file only handles external CLI calls for "add deploy" command
4280
- */
4281
- async function setupServerDeploy(config) {
4282
- const { serverDeploy, webDeploy, projectDir, packageManager } = config;
4283
- if (serverDeploy === "none") return;
4284
- if (serverDeploy === "cloudflare" && webDeploy === "cloudflare") return;
4285
- const serverDir = path.join(projectDir, "apps/server");
4286
- if (!await fs.pathExists(serverDir)) return;
4287
- if (serverDeploy === "cloudflare") await setupInfraScripts(projectDir, packageManager, config);
4288
- }
4289
-
4290
- //#endregion
4291
- //#region src/helpers/deployment/web-deploy-setup.ts
4292
- async function setupWebDeploy(config) {
4293
- const { webDeploy, serverDeploy, frontend, projectDir } = config;
4294
- const { packageManager } = config;
4295
- if (webDeploy === "none") return;
4296
- if (webDeploy !== "cloudflare") return;
4297
- if (webDeploy === "cloudflare" && serverDeploy === "cloudflare") {
4298
- await setupCombinedAlchemyDeploy(projectDir, packageManager, config);
4299
- return;
4300
- }
4301
- await setupInfraScripts(projectDir, packageManager, config);
4302
- const isNext = frontend.includes("next");
4303
- const isNuxt = frontend.includes("nuxt");
4304
- const isSvelte = frontend.includes("svelte");
4305
- const isTanstackRouter = frontend.includes("tanstack-router");
4306
- const isTanstackStart = frontend.includes("tanstack-start");
4307
- const isReactRouter = frontend.includes("react-router");
4308
- const isSolid = frontend.includes("solid");
4309
- if (isNext) await setupNextAlchemyDeploy(projectDir, packageManager);
4310
- else if (isNuxt) await setupNuxtAlchemyDeploy(projectDir, packageManager);
4311
- else if (isSvelte) await setupSvelteAlchemyDeploy(projectDir, packageManager);
4312
- else if (isTanstackStart) await setupTanStackStartAlchemyDeploy(projectDir, packageManager);
4313
- else if (isTanstackRouter) await setupTanStackRouterAlchemyDeploy(projectDir, packageManager);
4314
- else if (isReactRouter) await setupReactRouterAlchemyDeploy(projectDir, packageManager);
4315
- else if (isSolid) await setupSolidAlchemyDeploy(projectDir, packageManager);
4316
- }
4317
-
4318
3640
  //#endregion
4319
3641
  //#region src/helpers/core/git.ts
4320
3642
  async function initializeGit(projectDir, useGit) {
@@ -4582,19 +3904,11 @@ async function createProject(options, cliInput = {}) {
4582
3904
  });
4583
3905
  if (!result.success || !result.tree) throw new Error(result.error || "Failed to generate project templates");
4584
3906
  await writeTreeToFilesystem(result.tree, projectDir);
4585
- await formatProjectFiles(projectDir);
4586
3907
  await setPackageManagerVersion(projectDir, options.packageManager);
4587
3908
  if (!isConvex && options.database !== "none") await setupDatabase(options, cliInput);
4588
- if (options.examples.length > 0 && options.examples[0] !== "none") await /* @__PURE__ */ setupExamples(options);
4589
3909
  if (options.addons.length > 0 && options.addons[0] !== "none") await setupAddons(options);
4590
- if (options.auth === "better-auth" && !isConvex) {
4591
- const authPackageDir = `${projectDir}/packages/auth`;
4592
- if (await fs.pathExists(authPackageDir)) await setupBetterAuthPlugins(projectDir, options);
4593
- }
4594
- await setupEnvironmentVariables(options);
4595
- await setupWebDeploy(options);
4596
- await setupServerDeploy(options);
4597
3910
  await writeBtsConfig(options);
3911
+ await formatProject(projectDir);
4598
3912
  if (!isSilent()) log.success("Project template successfully scaffolded!");
4599
3913
  if (options.install) await installDependencies({
4600
3914
  projectDir,