UNPKG

@reliverse/rse

Version:

@reliverse/rse is your all-in-one companion for bootstrapping and improving any kind of projects (especially web apps built with frameworks like Next.js) — whether you're kicking off something new or upgrading an existing app. It is also a little AI-power

1,038 lines (1,037 loc) 33.5 kB
import { re } from "@reliverse/relico"; import { existsSync } from "@reliverse/relifso"; import { relinka } from "@reliverse/relinka"; import { cancel, confirm, intro, isCancel, multiselect, outro, select, selectSimple, spinner, text } from "@reliverse/rempts"; import { defineCommand } from "@reliverse/rempts"; import { parse } from "dotenv"; import { execaCommand } from "execa"; import fs from "fs/promises"; import path from "path"; import semver from "semver"; import { z } from "zod"; import { generateAuthConfig } from "../(generators)/auth-config.js"; import { checkPackageManagers } from "../(utils)/check-package-managers.js"; import { formatMilliseconds } from "../(utils)/format-ms.js"; import { generateSecretHash } from "../(utils)/generate-secret.js"; import { getPackageInfo } from "../(utils)/get-package-info.js"; import { getTsconfigInfo } from "../(utils)/get-tsconfig-info.js"; import { installDependencies } from "../(utils)/install-dependencies.js"; const supportedDatabases = [ // Built-in kysely "sqlite", "mysql", "mssql", "postgres", // Drizzle "drizzle:pg", "drizzle:mysql", "drizzle:sqlite", // Prisma "prisma:postgresql", "prisma:mysql", "prisma:sqlite", // Mongo "mongodb" ]; export const supportedPlugins = [ { id: "two-factor", name: "twoFactor", path: `better-auth/plugins`, clientName: "twoFactorClient", clientPath: "better-auth/client/plugins" }, { id: "username", name: "username", clientName: "usernameClient", path: `better-auth/plugins`, clientPath: "better-auth/client/plugins" }, { id: "anonymous", name: "anonymous", clientName: "anonymousClient", path: `better-auth/plugins`, clientPath: "better-auth/client/plugins" }, { id: "phone-number", name: "phoneNumber", clientName: "phoneNumberClient", path: `better-auth/plugins`, clientPath: "better-auth/client/plugins" }, { id: "magic-link", name: "magicLink", clientName: "magicLinkClient", clientPath: "better-auth/client/plugins", path: `better-auth/plugins` }, { id: "email-otp", name: "emailOTP", clientName: "emailOTPClient", path: `better-auth/plugins`, clientPath: "better-auth/client/plugins" }, { id: "passkey", name: "passkey", clientName: "passkeyClient", path: `better-auth/plugins/passkey`, clientPath: "better-auth/client/plugins" }, { id: "generic-oauth", name: "genericOAuth", clientName: "genericOAuthClient", path: `better-auth/plugins`, clientPath: "better-auth/client/plugins" }, { id: "one-tap", name: "oneTap", clientName: "oneTapClient", path: `better-auth/plugins`, clientPath: "better-auth/client/plugins" }, { id: "api-key", name: "apiKey", clientName: "apiKeyClient", path: `better-auth/plugins`, clientPath: "better-auth/client/plugins" }, { id: "admin", name: "admin", clientName: "adminClient", path: `better-auth/plugins`, clientPath: "better-auth/client/plugins" }, { id: "organization", name: "organization", clientName: "organizationClient", path: `better-auth/plugins`, clientPath: "better-auth/client/plugins" }, { id: "oidc", name: "oidcProvider", clientName: "oidcClient", path: `better-auth/plugins`, clientPath: "better-auth/client/plugins" }, { id: "sso", name: "sso", clientName: "ssoClient", path: `better-auth/plugins/sso`, clientPath: "better-auth/client/plugins" }, { id: "bearer", name: "bearer", clientName: void 0, path: `better-auth/plugins`, clientPath: void 0 }, { id: "multi-session", name: "multiSession", clientName: "multiSessionClient", path: `better-auth/plugins`, clientPath: "better-auth/client/plugins" }, { id: "oauth-proxy", name: "oAuthProxy", clientName: void 0, path: `better-auth/plugins`, clientPath: void 0 }, { id: "open-api", name: "openAPI", clientName: void 0, path: `better-auth/plugins`, clientPath: void 0 }, { id: "jwt", name: "jwt", clientName: void 0, clientPath: void 0, path: `better-auth/plugins` }, { id: "next-cookies", name: "nextCookies", clientPath: void 0, clientName: void 0, path: `better-auth/next-js` } ]; async function formatWithBiome(code, _filepath) { const tempFile = path.join(process.cwd(), `.temp-${Date.now()}.ts`); try { await fs.writeFile(tempFile, code); await execaCommand(`bun x biome format --write ${tempFile}`); const formatted = await fs.readFile(tempFile, "utf-8"); return formatted; } finally { await fs.unlink(tempFile).catch(() => void 0); } } const getDefaultAuthConfig = async ({ appName }) => await formatWithBiome( [ "import { betterAuth } from 'better-auth';", "", "export const auth = betterAuth({", appName ? `appName: "${appName}",` : "", "plugins: [],", "});" ].join("\n"), "auth.ts" ); const getDefaultAuthClientConfig = async ({ auth_config_path, framework, clientPlugins }) => { function groupImportVariables() { const result = [ { path: "better-auth/client/plugins", variables: [{ name: "inferAdditionalFields" }] } ]; for (const plugin of clientPlugins) { for (const import_ of plugin.imports) { if (Array.isArray(import_.variables)) { for (const variable of import_.variables) { const existingIndex = result.findIndex( (x) => x.path === import_.path ); if (existingIndex !== -1) { const vars = result[existingIndex].variables; if (Array.isArray(vars)) { vars.push(variable); } else { result[existingIndex].variables = [vars, variable]; } } else { result.push({ path: import_.path, variables: [variable] }); } } } else { const existingIndex = result.findIndex( (x) => x.path === import_.path ); if (existingIndex !== -1) { const vars = result[existingIndex].variables; if (Array.isArray(vars)) { vars.push(import_.variables); } else { result[existingIndex].variables = [vars, import_.variables]; } } else { result.push({ path: import_.path, variables: [import_.variables] }); } } } } return result; } const imports = groupImportVariables(); let importString = ""; for (const import_ of imports) { if (Array.isArray(import_.variables)) { importString += `import { ${import_.variables.map( (x) => `${x.asType ? "type " : ""}${x.name}${x.as ? ` as ${x.as}` : ""}` ).join(", ")} } from "${import_.path}"; `; } else { importString += `import ${import_.variables.asType ? "type " : ""}${import_.variables.name}${import_.variables.as ? ` as ${import_.variables.as}` : ""} from "${import_.path}"; `; } } const formattedCode = await formatWithBiome( [ `import { createAuthClient } from "better-auth/${framework === "nextjs" ? "react" : framework === "vanilla" ? "client" : framework}";`, `import type { auth } from "${auth_config_path}";`, importString, ``, `export const authClient = createAuthClient({`, `baseURL: "http://localhost:3000",`, `plugins: [inferAdditionalFields<typeof auth>(),${clientPlugins.map((x) => `${x.name}(${x.contents})`).join(", ")}],`, `});` ].join("\n"), "auth-client.ts" ); return formattedCode; }; const optionsSchema = z.object({ cwd: z.string(), config: z.string().optional(), database: z.enum(supportedDatabases).optional(), "skip-db": z.boolean().optional(), "skip-plugins": z.boolean().optional(), "package-manager": z.string().optional(), tsconfig: z.string().optional() }); const outroText = `\u{1F973} All Done, Happy Hacking!`; export const init = defineCommand({ meta: { name: "init", description: "Initialize Better Auth in your project" }, args: { cwd: { type: "string", description: "The working directory", default: process.cwd() }, config: { type: "string", description: "The path to the auth configuration file. defaults to the first `auth.ts` file found.", optional: true }, tsconfig: { type: "string", description: "The path to the tsconfig file", optional: true }, "skip-db": { type: "boolean", description: "Skip the database setup", optional: true }, "skip-plugins": { type: "boolean", description: "Skip the plugins setup", optional: true }, "package-manager": { type: "string", description: "The package manager you want to use", optional: true } }, async run({ args }) { console.log(); intro("\u{1F44B} Initializing Better Auth"); const options = optionsSchema.parse(args); const cwd = path.resolve(options.cwd); let packageManagerPreference; let config_path = ""; let framework = "vanilla"; const format = async (code) => await formatWithBiome(code, config_path); let packageInfo; try { packageInfo = getPackageInfo(cwd); } catch (error) { relinka( "error", `\u274C Couldn't read your package.json file. (dir: ${cwd})` ); relinka("error", JSON.stringify(error, null, 2)); process.exit(1); } const envFiles = await getEnvFiles(cwd); if (!envFiles.length) { outro("\u274C No .env files found. Please create an env file first."); process.exit(0); } let targetEnvFile; if (envFiles.includes(".env")) targetEnvFile = ".env"; else if (envFiles.includes(".env.local")) targetEnvFile = ".env.local"; else if (envFiles.includes(".env.development")) targetEnvFile = ".env.development"; else if (envFiles.length === 1) targetEnvFile = envFiles[0]; else targetEnvFile = "none"; let tsconfigInfo; try { const tsconfigPath = options.tsconfig !== void 0 ? path.resolve(cwd, options.tsconfig) : path.join(cwd, "tsconfig.json"); tsconfigInfo = await getTsconfigInfo(cwd, tsconfigPath); } catch (error) { relinka( "error", `\u274C Couldn't read your tsconfig.json file. (dir: ${cwd})` ); console.error(error); process.exit(1); } if (!("compilerOptions" in tsconfigInfo && "strict" in tsconfigInfo.compilerOptions && tsconfigInfo.compilerOptions.strict === true)) { relinka( "warn", `Better Auth requires your tsconfig.json to have "compilerOptions.strict" set to true.` ); const shouldAdd = await confirm({ message: `Would you like us to set ${re.bold( `strict` )} to ${re.bold(`true`)}?` }); if (isCancel(shouldAdd)) { cancel(`\u270B Operation cancelled.`); process.exit(0); } if (shouldAdd) { try { await fs.writeFile(path.join(cwd, "tsconfig.json"), "utf-8"); relinka("success", `\u{1F680} tsconfig.json successfully updated!`); } catch (error) { relinka( "error", `Failed to add "compilerOptions.strict" to your tsconfig.json file.` ); console.error(error); process.exit(1); } } } const s = spinner({ indicator: "dots" }); s.start(`Checking better-auth installation`); let latest_betterauth_version; try { latest_betterauth_version = await getLatestNpmVersion("better-auth"); } catch (error) { relinka("error", `\u274C Couldn't get latest version of better-auth.`); console.error(error); process.exit(1); } if (!packageInfo.dependencies || !Object.keys(packageInfo.dependencies).includes("better-auth")) { s.stop("Finished fetching latest version of better-auth."); const s2 = spinner({ indicator: "dots" }); const shouldInstallBetterAuthDep = await confirm({ message: `Would you like to install Better Auth?` }); if (isCancel(shouldInstallBetterAuthDep)) { cancel(`\u270B Operation cancelled.`); process.exit(0); } if (packageManagerPreference === void 0) { packageManagerPreference = await getPackageManager(); } if (shouldInstallBetterAuthDep) { s2.start( `Installing Better Auth using ${re.bold(packageManagerPreference)}` ); try { const start = Date.now(); await installDependencies({ dependencies: ["better-auth@latest"], packageManager: packageManagerPreference, cwd }); s2.stop( `Better Auth installed ${re.greenBright( `successfully` )}! ${re.gray(`(${formatMilliseconds(Date.now() - start)})`)}` ); } catch (error) { s2.stop(`Failed to install Better Auth:`); console.error(error); process.exit(1); } } } else if (packageInfo.dependencies["better-auth"] !== "workspace:*" && semver.lt( semver.coerce(packageInfo.dependencies["better-auth"])?.toString() ?? "", semver.clean(latest_betterauth_version) ?? "" )) { s.stop("Finished fetching latest version of better-auth."); const shouldInstallBetterAuthDep = await confirm({ message: `Your current Better Auth dependency is out-of-date. Would you like to update it? (${re.bold( packageInfo.dependencies["better-auth"] )} \u2192 ${re.bold(`v${latest_betterauth_version}`)})` }); if (isCancel(shouldInstallBetterAuthDep)) { cancel(`\u270B Operation cancelled.`); process.exit(0); } if (shouldInstallBetterAuthDep) { if (packageManagerPreference === void 0) { packageManagerPreference = await getPackageManager(); } const s2 = spinner({ indicator: "dots" }); s2.start( `Updating Better Auth using ${re.bold(packageManagerPreference)}` ); try { const start = Date.now(); await installDependencies({ dependencies: ["better-auth@latest"], packageManager: packageManagerPreference, cwd }); s2.stop( `Better Auth updated ${re.greenBright( `successfully` )}! ${re.gray(`(${formatMilliseconds(Date.now() - start)})`)}` ); } catch (error) { s2.stop(`Failed to update Better Auth:`); relinka("error", error.message); process.exit(1); } } } else { s.stop(`Better Auth dependencies are ${re.greenBright(`up to date`)}!`); } const packageJson = getPackageInfo(cwd); let appName; if (!packageJson.name) { const newAppName = await text({ message: "What is the name of your application?" }); if (isCancel(newAppName)) { cancel("\u270B Operation cancelled."); process.exit(0); } appName = newAppName; } else { appName = packageJson.name; } let possiblePaths = ["auth.ts", "auth.tsx", "auth.js", "auth.jsx"]; possiblePaths = [ ...possiblePaths, ...possiblePaths.map((it) => `lib/server/${it}`), ...possiblePaths.map((it) => `server/${it}`), ...possiblePaths.map((it) => `lib/${it}`), ...possiblePaths.map((it) => `utils/${it}`) ]; possiblePaths = [ ...possiblePaths, ...possiblePaths.map((it) => `src/${it}`), ...possiblePaths.map((it) => `app/${it}`) ]; if (options.config) { config_path = path.join(cwd, options.config); } else { for (const possiblePath of possiblePaths) { const doesExist = existsSync(path.join(cwd, possiblePath)); if (doesExist) { config_path = path.join(cwd, possiblePath); break; } } } let current_user_config = ""; let database = null; let add_plugins = []; if (!config_path) { const shouldCreateAuthConfig = await select({ message: `Would you like to create an auth config file?`, options: [ { label: "Yes", value: "yes" }, { label: "No", value: "no" } ] }); if (isCancel(shouldCreateAuthConfig)) { cancel(`\u270B Operation cancelled.`); process.exit(0); } if (shouldCreateAuthConfig === "yes") { const shouldSetupDb = await confirm({ message: `Would you like to set up your ${re.bold(`database`)}?`, initialValue: true }); if (isCancel(shouldSetupDb)) { cancel(`\u270B Operating cancelled.`); process.exit(0); } if (shouldSetupDb) { const prompted_database = await select({ message: "Choose a Database Dialect", options: supportedDatabases.map((it) => ({ value: it, label: it })) }); if (isCancel(prompted_database)) { cancel(`\u270B Operating cancelled.`); process.exit(0); } database = prompted_database; } if (options["skip-plugins"] !== false) { const shouldSetupPlugins = await confirm({ message: `Would you like to set up ${re.bold(`plugins`)}?` }); if (isCancel(shouldSetupPlugins)) { cancel(`\u270B Operating cancelled.`); process.exit(0); } if (shouldSetupPlugins) { const prompted_plugins = await multiselect({ message: "Select your new plugins", options: supportedPlugins.filter((x) => x.id !== "next-cookies").map((x) => ({ value: x.id, label: x.id })), required: false }); if (isCancel(prompted_plugins)) { cancel(`\u270B Operating cancelled.`); process.exit(0); } add_plugins = prompted_plugins.map( (x) => supportedPlugins.find((y) => y.id === x) ); const possible_next_config_paths = [ "next.config.js", "next.config.ts", "next.config.mjs", ".next/server/next.config.js", ".next/server/next.config.ts", ".next/server/next.config.mjs" ]; for (const possible_next_config_path of possible_next_config_paths) { if (existsSync(path.join(cwd, possible_next_config_path))) { framework = "nextjs"; break; } } if (framework === "nextjs") { const result = await confirm({ message: `It looks like you're using NextJS. Do you want to add the next-cookies plugin? ${re.bold( `(Recommended)` )}` }); if (isCancel(result)) { cancel(`\u270B Operating cancelled.`); process.exit(0); } if (result) { add_plugins.push( supportedPlugins.find((x) => x.id === "next-cookies") ); } } } } const filePath = path.join(cwd, "auth.ts"); config_path = filePath; relinka("info", `Creating auth config file: ${filePath}`); try { current_user_config = await getDefaultAuthConfig({ appName }); const { dependencies, envs, generatedCode } = await generateAuthConfig({ current_user_config, format, // @ts-expect-error TODO: fix ts s, plugins: add_plugins, database }); current_user_config = generatedCode; await fs.writeFile(filePath, current_user_config); config_path = filePath; relinka("success", `\u{1F680} Auth config file successfully created!`); if (envs.length !== 0) { relinka( "info", `There are ${envs.length} environment variables for your database of choice.` ); const shouldUpdateEnvs = await confirm({ message: `Would you like us to update your ENV files?` }); if (isCancel(shouldUpdateEnvs)) { cancel("\u270B Operation cancelled."); process.exit(0); } if (shouldUpdateEnvs) { const filesToUpdate = await multiselect({ message: "Select the .env files you want to update", options: envFiles.map((x) => ({ value: path.join(cwd, x), label: x })), required: false }); if (isCancel(filesToUpdate)) { cancel("\u270B Operation cancelled."); process.exit(0); } if (filesToUpdate.length === 0) { relinka("info", "No .env files to update. Skipping..."); } else { try { await updateEnvs({ files: filesToUpdate, envs, isCommented: true }); } catch (error) { relinka("error", `Failed to update .env files:`); relinka("error", JSON.stringify(error, null, 2)); process.exit(1); } relinka("success", `\u{1F680} ENV files successfully updated!`); } } } if (dependencies.length !== 0) { relinka( "info", `There are ${dependencies.length} dependencies to install. (${dependencies.map((x) => re.green(x)).join(", ")})` ); const shouldInstallDeps = await confirm({ message: `Would you like us to install dependencies?` }); if (isCancel(shouldInstallDeps)) { cancel("\u270B Operation cancelled."); process.exit(0); } if (shouldInstallDeps) { const s2 = spinner({ indicator: "dots" }); if (packageManagerPreference === void 0) { packageManagerPreference = await getPackageManager(); } s2.start( `Installing dependencies using ${re.bold( packageManagerPreference )}...` ); try { const start = Date.now(); await installDependencies({ dependencies, packageManager: packageManagerPreference, cwd }); s2.stop( `Dependencies installed ${re.greenBright( `successfully` )} ${re.gray(`(${formatMilliseconds(Date.now() - start)})`)}` ); } catch (error) { s2.stop( `Failed to install dependencies using ${packageManagerPreference}:` ); relinka("error", error.message); process.exit(1); } } } } catch (error) { relinka("error", `Failed to create auth config file: ${filePath}`); console.error(error); process.exit(1); } } else if (shouldCreateAuthConfig === "no") { relinka("info", `Skipping auth config file creation.`); } } else { relinka( "success", `Found auth config file. ${re.gray(`(${config_path})`)}` ); } let possibleClientPaths = [ "auth-client.ts", "auth-client.tsx", "auth-client.js", "auth-client.jsx", "client.ts", "client.tsx", "client.js", "client.jsx" ]; possibleClientPaths = [ ...possibleClientPaths, ...possibleClientPaths.map((it) => `lib/server/${it}`), ...possibleClientPaths.map((it) => `server/${it}`), ...possibleClientPaths.map((it) => `lib/${it}`), ...possibleClientPaths.map((it) => `utils/${it}`) ]; possibleClientPaths = [ ...possibleClientPaths, ...possibleClientPaths.map((it) => `src/${it}`), ...possibleClientPaths.map((it) => `app/${it}`) ]; let authClientConfigPath = null; for (const possiblePath of possibleClientPaths) { const doesExist = existsSync(path.join(cwd, possiblePath)); if (doesExist) { authClientConfigPath = path.join(cwd, possiblePath); break; } } if (!authClientConfigPath) { const choice = await select({ message: `Would you like to create an auth client config file?`, options: [ { label: "Yes", value: "yes" }, { label: "No", value: "no" } ] }); if (isCancel(choice)) { cancel(`\u270B Operation cancelled.`); process.exit(0); } if (choice === "yes") { authClientConfigPath = path.join(cwd, "auth-client.ts"); relinka( "info", `Creating auth client config file: ${authClientConfigPath}` ); try { const contents = await getDefaultAuthClientConfig({ auth_config_path: `./${path.join(config_path.replace(cwd, ""))}`.replace( ".//", "./" ), clientPlugins: add_plugins.filter((x) => x.clientName).map((plugin) => { let contents2 = ""; if (plugin.id === "one-tap") { contents2 = `{ clientId: "MY_CLIENT_ID" }`; } return { contents: contents2, id: plugin.id, name: plugin.clientName, imports: [ { path: "better-auth/client/plugins", variables: [{ name: plugin.clientName }] } ] }; }), framework }); await fs.writeFile(authClientConfigPath, contents); relinka( "success", `\u{1F680} Auth client config file successfully created!` ); } catch (error) { relinka( "error", `Failed to create auth client config file: ${authClientConfigPath}` ); relinka("error", JSON.stringify(error, null, 2)); process.exit(1); } } else if (choice === "no") { relinka("info", `Skipping auth client config file creation.`); } } else { relinka( "success", `Found auth client config file. ${re.gray(`(${authClientConfigPath})`)}` ); } if (targetEnvFile !== "none") { try { const fileContents = await fs.readFile( path.join(cwd, targetEnvFile), "utf8" ); const parsed = parse(fileContents); let isMissingSecret = false; let isMissingUrl = false; if (parsed.BETTER_AUTH_SECRET === void 0) isMissingSecret = true; if (parsed.BETTER_AUTH_URL === void 0) isMissingUrl = true; if (isMissingSecret || isMissingUrl) { let txt = ""; if (isMissingSecret && !isMissingUrl) txt = re.bold(`BETTER_AUTH_SECRET`); else if (!isMissingSecret && isMissingUrl) txt = re.bold(`BETTER_AUTH_URL`); else txt = `${re.underline(`BETTER_AUTH_SECRET`)} and ${re.underline(`BETTER_AUTH_URL`)}`; relinka("warn", `Missing ${txt} in ${targetEnvFile}`); const shouldAdd = await select({ message: `Do you want to add ${txt} to ${targetEnvFile}?`, options: [ { label: "Yes", value: "yes" }, { label: "No", value: "no" }, { label: "Choose other file(s)", value: "other" } ] }); if (isCancel(shouldAdd)) { cancel(`\u270B Operation cancelled.`); process.exit(0); } const envs = []; if (isMissingSecret) { envs.push("BETTER_AUTH_SECRET"); } if (isMissingUrl) { envs.push("BETTER_AUTH_URL"); } if (shouldAdd === "yes") { try { await updateEnvs({ files: [path.join(cwd, targetEnvFile)], envs, isCommented: false }); } catch (error) { relinka( "error", `Failed to add ENV variables to ${targetEnvFile}` ); relinka("error", JSON.stringify(error, null, 2)); process.exit(1); } relinka("success", `\u{1F680} ENV variables successfully added!`); if (isMissingUrl) { relinka( "info", `Be sure to update your BETTER_AUTH_URL according to your app's needs.` ); } } else if (shouldAdd === "no") { relinka("info", `Skipping ENV step.`); } else if (shouldAdd === "other") { if (!envFiles.length) { cancel("No env files found. Please create an env file first."); process.exit(0); } const envFilesToUpdate = await multiselect({ message: "Select the .env files you want to update", options: envFiles.map((x) => ({ value: path.join(cwd, x), label: x })), required: false }); if (isCancel(envFilesToUpdate)) { cancel("\u270B Operation cancelled."); process.exit(0); } if (envFilesToUpdate.length === 0) { relinka("info", "No .env files to update. Skipping..."); } else { try { await updateEnvs({ files: envFilesToUpdate, envs, isCommented: false }); } catch (error) { relinka("error", `Failed to update .env files:`); relinka("error", JSON.stringify(error, null, 2)); process.exit(1); } relinka("success", `\u{1F680} ENV files successfully updated!`); } } } } catch (_error) { } } outro(outroText); console.log(); process.exit(0); } }); async function getLatestNpmVersion(packageName) { try { const response = await fetch(`https://registry.npmjs.org/${packageName}`); if (!response.ok) { throw new Error(`Package not found: ${response.statusText}`); } const data = await response.json(); return data["dist-tags"].latest; } catch (error) { throw error?.message; } } async function getPackageManager() { const { hasBun, hasPnpm } = await checkPackageManagers(); if (!hasBun && !hasPnpm) return "npm"; const packageManagerOptions = []; if (hasPnpm) { packageManagerOptions.push({ value: "pnpm", label: "pnpm", hint: "recommended" }); } if (hasBun) { packageManagerOptions.push({ value: "bun", label: "bun" }); } packageManagerOptions.push({ value: "npm", hint: "not recommended" }); const packageManager = await selectSimple({ message: "Choose a package manager", options: packageManagerOptions }); if (isCancel(packageManager)) { cancel(`Operation cancelled.`); process.exit(0); } return packageManager; } async function getEnvFiles(cwd) { const files = await fs.readdir(cwd); return files.filter((x) => x.startsWith(".env")); } async function updateEnvs({ envs, files, isCommented }) { let previouslyGeneratedSecret = null; for (const file of files) { const content = await fs.readFile(file, "utf8"); const lines = content.split("\n"); const newLines = envs.map( (x) => `${isCommented ? "# " : ""}${x}=${getEnvDescription(x) ?? `"some_value"`}` ); newLines.push(""); newLines.push(...lines); await fs.writeFile(file, newLines.join("\n"), "utf8"); } function getEnvDescription(env) { if (env === "DATABASE_HOST") { return `"The host of your database"`; } if (env === "DATABASE_PORT") { return `"The port of your database"`; } if (env === "DATABASE_USER") { return `"The username of your database"`; } if (env === "DATABASE_PASSWORD") { return `"The password of your database"`; } if (env === "DATABASE_NAME") { return `"The name of your database"`; } if (env === "DATABASE_URL") { return `"The URL of your database"`; } if (env === "BETTER_AUTH_SECRET") { previouslyGeneratedSecret = previouslyGeneratedSecret ?? generateSecretHash(); return `"${previouslyGeneratedSecret}"`; } if (env === "BETTER_AUTH_URL") { return `"http://localhost:3000" # Your APP URL`; } } }