UNPKG

@better-auth/cli

Version:
1,647 lines (1,639 loc) 111 kB
#!/usr/bin/env node import { Command } from 'commander'; import { parse } from 'dotenv'; import semver from 'semver'; import prettier, { format } from 'prettier'; import * as z from 'zod/v4'; import fs, { existsSync, readFileSync } from 'fs'; import path from 'path'; import fs$1 from 'fs/promises'; import chalk from 'chalk'; import { intro, log, outro, confirm, isCancel, cancel, spinner, text, select, multiselect } from '@clack/prompts'; import { exec, execSync } from 'child_process'; import { logger, BetterAuthError, createTelemetry, getTelemetryAuthConfig, capitalizeFirstLetter } from 'better-auth'; import Crypto from 'crypto'; import yoctoSpinner from 'yocto-spinner'; import prompts from 'prompts'; import { getAdapter, getMigrations, getAuthTables } from 'better-auth/db'; import { loadConfig } from 'c12'; import babelPresetTypeScript from '@babel/preset-typescript'; import babelPresetReact from '@babel/preset-react'; import { produceSchema } from '@mrleebo/prisma-ast'; import { createAuthClient } from 'better-auth/client'; import { deviceAuthorizationClient } from 'better-auth/client/plugins'; import open from 'open'; import os from 'os'; import 'dotenv/config'; function getPackageInfo(cwd) { const packageJsonPath = cwd ? path.join(cwd, "package.json") : path.join("package.json"); return JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")); } function installDependencies({ dependencies, packageManager, cwd }) { let installCommand; switch (packageManager) { case "npm": installCommand = "npm install --force"; break; case "pnpm": installCommand = "pnpm install"; break; case "bun": installCommand = "bun install"; break; case "yarn": installCommand = "yarn install"; break; default: throw new Error("Invalid package manager"); } const command = `${installCommand} ${dependencies.join(" ")}`; return new Promise((resolve, reject) => { exec(command, { cwd }, (error, stdout, stderr) => { if (error) { reject(new Error(stderr)); return; } resolve(true); }); }); } function checkCommand(command) { return new Promise((resolve) => { exec(`${command} --version`, (error) => { if (error) { resolve(false); } else { resolve(true); } }); }); } async function checkPackageManagers() { const hasPnpm = await checkCommand("pnpm"); const hasBun = await checkCommand("bun"); return { hasPnpm, hasBun }; } function formatMilliseconds(ms) { if (ms < 0) { throw new Error("Milliseconds cannot be negative"); } if (ms < 1e3) { return `${ms}ms`; } const seconds = Math.floor(ms / 1e3); const milliseconds = ms % 1e3; return `${seconds}s ${milliseconds}ms`; } const generateSecret = new Command("secret").action(() => { const secret = generateSecretHash(); logger.info(` Add the following to your .env file: ${chalk.gray("# Auth Secret") + chalk.green(` BETTER_AUTH_SECRET=${secret}`)}`); }); const generateSecretHash = () => { return Crypto.randomBytes(32).toString("hex"); }; async function generateAuthConfig({ format, current_user_config, spinner, plugins, database }) { let _start_of_plugins_common_index = { START_OF_PLUGINS: { type: "regex", regex: /betterAuth\([\w\W]*plugins:[\W]*\[()/m, getIndex: ({ matchIndex, match }) => { return matchIndex + match[0].length; } } }; const common_indexes = { START_OF_PLUGINS: _start_of_plugins_common_index.START_OF_PLUGINS, END_OF_PLUGINS: { type: "manual", getIndex: ({ content, additionalFields }) => { const closingBracketIndex = findClosingBracket( content, additionalFields.start_of_plugins, "[", "]" ); return closingBracketIndex; } }, START_OF_BETTERAUTH: { type: "regex", regex: /betterAuth\({()/m, getIndex: ({ matchIndex }) => { return matchIndex + "betterAuth({".length; } } }; const config_generation = { add_plugin: async (opts) => { let start_of_plugins = getGroupInfo( opts.config, common_indexes.START_OF_PLUGINS, {} ); if (!start_of_plugins) { throw new Error( "Couldn't find start of your plugins array in your auth config file." ); } let end_of_plugins = getGroupInfo( opts.config, common_indexes.END_OF_PLUGINS, { start_of_plugins: start_of_plugins.index } ); if (!end_of_plugins) { throw new Error( "Couldn't find end of your plugins array in your auth config file." ); } let new_content; if (opts.direction_in_plugins_array === "prepend") { new_content = insertContent({ line: start_of_plugins.line, character: start_of_plugins.character, content: opts.config, insert_content: `${opts.pluginFunctionName}(${opts.pluginContents}),` }); } else { const pluginArrayContent = opts.config.slice(start_of_plugins.index, end_of_plugins.index).trim(); const isPluginArrayEmpty = pluginArrayContent === ""; const isPluginArrayEndsWithComma = pluginArrayContent.endsWith(","); const needsComma = !isPluginArrayEmpty && !isPluginArrayEndsWithComma; new_content = insertContent({ line: end_of_plugins.line, character: end_of_plugins.character, content: opts.config, insert_content: `${needsComma ? "," : ""}${opts.pluginFunctionName}(${opts.pluginContents})` }); } try { new_content = await format(new_content); } catch (error) { console.error(error); throw new Error( `Failed to generate new auth config during plugin addition phase.` ); } return { code: new_content, dependencies: [], envs: [] }; }, add_import: async (opts) => { let importString = ""; for (const import_ of opts.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}"; `; } } try { let new_content = format(importString + opts.config); return { code: await new_content, dependencies: [], envs: [] }; } catch (error) { console.error(error); throw new Error( `Failed to generate new auth config during import addition phase.` ); } }, add_database: async (opts) => { const required_envs = []; const required_deps = []; let database_code_str = ""; async function add_db({ db_code, dependencies, envs, imports, code_before_betterAuth }) { if (code_before_betterAuth) { let start_of_betterauth2 = getGroupInfo( opts.config, common_indexes.START_OF_BETTERAUTH, {} ); if (!start_of_betterauth2) { throw new Error("Couldn't find start of betterAuth() function."); } opts.config = insertContent({ line: start_of_betterauth2.line - 1, character: 0, content: opts.config, insert_content: ` ${code_before_betterAuth} ` }); } const code_gen = await config_generation.add_import({ config: opts.config, imports }); opts.config = code_gen.code; database_code_str = db_code; required_envs.push(...envs, ...code_gen.envs); required_deps.push(...dependencies, ...code_gen.dependencies); } if (opts.database === "sqlite") { await add_db({ db_code: `new Database(process.env.DATABASE_URL || "database.sqlite")`, dependencies: ["better-sqlite3"], envs: ["DATABASE_URL"], imports: [ { path: "better-sqlite3", variables: { asType: false, name: "Database" } } ] }); } else if (opts.database === "postgres") { await add_db({ db_code: `new Pool({ connectionString: process.env.DATABASE_URL || "postgresql://postgres:password@localhost:5432/database" })`, dependencies: ["pg"], envs: ["DATABASE_URL"], imports: [ { path: "pg", variables: [ { asType: false, name: "Pool" } ] } ] }); } else if (opts.database === "mysql") { await add_db({ db_code: `createPool(process.env.DATABASE_URL!)`, dependencies: ["mysql2"], envs: ["DATABASE_URL"], imports: [ { path: "mysql2/promise", variables: [ { asType: false, name: "createPool" } ] } ] }); } else if (opts.database === "mssql") { const dialectCode = `new MssqlDialect({ tarn: { ...Tarn, options: { min: 0, max: 10, }, }, tedious: { ...Tedious, connectionFactory: () => new Tedious.Connection({ authentication: { options: { password: 'password', userName: 'username', }, type: 'default', }, options: { database: 'some_db', port: 1433, trustServerCertificate: true, }, server: 'localhost', }), }, })`; await add_db({ code_before_betterAuth: dialectCode, db_code: `dialect`, dependencies: ["tedious", "tarn", "kysely"], envs: ["DATABASE_URL"], imports: [ { path: "tedious", variables: { name: "*", as: "Tedious" } }, { path: "tarn", variables: { name: "*", as: "Tarn" } }, { path: "kysely", variables: [ { name: "MssqlDialect" } ] } ] }); } else if (opts.database === "drizzle:mysql" || opts.database === "drizzle:sqlite" || opts.database === "drizzle:pg") { await add_db({ db_code: `drizzleAdapter(db, { provider: "${opts.database.replace( "drizzle:", "" )}", })`, dependencies: [""], envs: [], imports: [ { path: "better-auth/adapters/drizzle", variables: [ { name: "drizzleAdapter" } ] }, { path: "./database.ts", variables: [ { name: "db" } ] } ] }); } else if (opts.database === "prisma:mysql" || opts.database === "prisma:sqlite" || opts.database === "prisma:postgresql") { await add_db({ db_code: `prismaAdapter(client, { provider: "${opts.database.replace( "prisma:", "" )}", })`, dependencies: [`@prisma/client`], envs: [], code_before_betterAuth: "const client = new PrismaClient();", imports: [ { path: "better-auth/adapters/prisma", variables: [ { name: "prismaAdapter" } ] }, { path: "@prisma/client", variables: [ { name: "PrismaClient" } ] } ] }); } else if (opts.database === "mongodb") { await add_db({ db_code: `mongodbAdapter(db)`, dependencies: ["mongodb"], envs: [`DATABASE_URL`], code_before_betterAuth: [ `const client = new MongoClient(process.env.DATABASE_URL || "mongodb://localhost:27017/database");`, `const db = client.db();` ].join("\n"), imports: [ { path: "better-auth/adapters/mongodb", variables: [ { name: "mongodbAdapter" } ] }, { path: "mongodb", variables: [ { name: "MongoClient" } ] } ] }); } let start_of_betterauth = getGroupInfo( opts.config, common_indexes.START_OF_BETTERAUTH, {} ); if (!start_of_betterauth) { throw new Error("Couldn't find start of betterAuth() function."); } let new_content; new_content = insertContent({ line: start_of_betterauth.line, character: start_of_betterauth.character, content: opts.config, insert_content: `database: ${database_code_str},` }); try { new_content = await format(new_content); return { code: new_content, dependencies: required_deps, envs: required_envs }; } catch (error) { console.error(error); throw new Error( `Failed to generate new auth config during database addition phase.` ); } } }; let new_user_config = await format(current_user_config); let total_dependencies = []; let total_envs = []; if (plugins.length !== 0) { const imports = []; for await (const plugin of plugins) { const existingIndex = imports.findIndex((x) => x.path === plugin.path); if (existingIndex !== -1) { imports[existingIndex].variables.push({ name: plugin.name, asType: false }); } else { imports.push({ path: plugin.path, variables: [ { name: plugin.name, asType: false } ] }); } } if (imports.length !== 0) { const { code, envs, dependencies } = await config_generation.add_import({ config: new_user_config, imports }); total_dependencies.push(...dependencies); total_envs.push(...envs); new_user_config = code; } } for await (const plugin of plugins) { try { let pluginContents = ""; if (plugin.id === "magic-link") { pluginContents = `{ sendMagicLink({ email, token, url }, request) { // Send email with magic link }, }`; } else if (plugin.id === "email-otp") { pluginContents = `{ async sendVerificationOTP({ email, otp, type }, request) { // Send email with OTP }, }`; } else if (plugin.id === "generic-oauth") { pluginContents = `{ config: [], }`; } else if (plugin.id === "oidc") { pluginContents = `{ loginPage: "/sign-in", }`; } const { code, dependencies, envs } = await config_generation.add_plugin({ config: new_user_config, direction_in_plugins_array: plugin.id === "next-cookies" ? "append" : "prepend", pluginFunctionName: plugin.name, pluginContents }); new_user_config = code; total_envs.push(...envs); total_dependencies.push(...dependencies); } catch (error) { spinner.stop( `Something went wrong while generating/updating your new auth config file.`, 1 ); logger.error(error.message); process.exit(1); } } if (database) { try { const { code, dependencies, envs } = await config_generation.add_database( { config: new_user_config, database } ); new_user_config = code; total_dependencies.push(...dependencies); total_envs.push(...envs); } catch (error) { spinner.stop( `Something went wrong while generating/updating your new auth config file.`, 1 ); logger.error(error.message); process.exit(1); } } return { generatedCode: new_user_config, dependencies: total_dependencies, envs: total_envs }; } function findClosingBracket(content, startIndex, openingBracket, closingBracket) { let stack = 0; let inString = false; let quoteChar = null; for (let i = startIndex; i < content.length; i++) { const char = content[i]; if (char === '"' || char === "'" || char === "`") { if (!inString) { inString = true; quoteChar = char; } else if (char === quoteChar) { inString = false; quoteChar = null; } continue; } if (!inString) { if (char === openingBracket) { stack++; } else if (char === closingBracket) { if (stack === 0) { return i; } stack--; } } } return null; } function insertContent(params) { const { line, character, content, insert_content } = params; const lines = content.split("\n"); if (line < 1 || line > lines.length) { throw new Error("Invalid line number"); } const targetLineIndex = line - 1; if (character < 0 || character > lines[targetLineIndex].length) { throw new Error("Invalid character index"); } const targetLine = lines[targetLineIndex]; const updatedLine = targetLine.slice(0, character) + insert_content + targetLine.slice(character); lines[targetLineIndex] = updatedLine; return lines.join("\n"); } function getGroupInfo(content, commonIndexConfig, additionalFields) { if (commonIndexConfig.type === "regex") { const { regex, getIndex } = commonIndexConfig; const match = regex.exec(content); if (match) { const matchIndex = match.index; const groupIndex = getIndex({ matchIndex, match, additionalFields }); if (groupIndex === null) return null; const position = getPosition(content, groupIndex); return { line: position.line, character: position.character, index: groupIndex }; } return null; } else { const { getIndex } = commonIndexConfig; const index = getIndex({ content, additionalFields }); if (index === null) return null; const { line, character } = getPosition(content, index); return { line, character, index }; } } const getPosition = (str, index) => { const lines = str.slice(0, index).split("\n"); return { line: lines.length, character: lines[lines.length - 1].length }; }; function stripJsonComments(jsonString) { return jsonString.replace( /\\"|"(?:\\"|[^"])*"|(\/\/.*|\/\*[\s\S]*?\*\/)/g, (m, g) => g ? "" : m ).replace(/,(?=\s*[}\]])/g, ""); } function getTsconfigInfo(cwd, flatPath) { let tsConfigPath; if (flatPath) { tsConfigPath = flatPath; } else { tsConfigPath = cwd ? path.join(cwd, "tsconfig.json") : path.join("tsconfig.json"); } try { const text = fs.readFileSync(tsConfigPath, "utf-8"); return JSON.parse(stripJsonComments(text)); } catch (error) { throw error; } } 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" ]; 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` } ]; const defaultFormatOptions = { trailingComma: "all", useTabs: false, tabWidth: 4 }; const getDefaultAuthConfig = async ({ appName }) => await format( [ "import { betterAuth } from 'better-auth';", "", "export const auth = betterAuth({", appName ? `appName: "${appName}",` : "", "plugins: [],", "});" ].join("\n"), { filepath: "auth.ts", ...defaultFormatOptions } ); 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; } let 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}"; `; } } return await format( [ `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"), { filepath: "auth-client.ts", ...defaultFormatOptions } ); }; 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!`; async function initAction(opts) { console.log(); intro("\u{1F44B} Initializing Better Auth"); const options = optionsSchema.parse(opts); const cwd = path.resolve(options.cwd); let packageManagerPreference = void 0; let config_path = ""; let framework = "vanilla"; const format$1 = async (code) => await format(code, { filepath: config_path, ...defaultFormatOptions }); let packageInfo; try { packageInfo = getPackageInfo(cwd); } catch (error) { log.error(`\u274C Couldn't read your package.json file. (dir: ${cwd})`); log.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) { log.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)) { log.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 ${chalk.bold( `strict` )} to ${chalk.bold(`true`)}?` }); if (isCancel(shouldAdd)) { cancel(`\u270B Operation cancelled.`); process.exit(0); } if (shouldAdd) { try { await fs$1.writeFile( path.join(cwd, "tsconfig.json"), await format( JSON.stringify( Object.assign(tsconfigInfo, { compilerOptions: { strict: true } }) ), { filepath: "tsconfig.json", ...defaultFormatOptions } ), "utf-8" ); log.success(`\u{1F680} tsconfig.json successfully updated!`); } catch (error) { log.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) { log.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$1(); } if (shouldInstallBetterAuthDep) { s2.start( `Installing Better Auth using ${chalk.bold(packageManagerPreference)}` ); try { const start = Date.now(); await installDependencies({ dependencies: ["better-auth@latest"], packageManager: packageManagerPreference, cwd }); s2.stop( `Better Auth installed ${chalk.greenBright( `successfully` )}! ${chalk.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? (${chalk.bold( packageInfo.dependencies["better-auth"] )} \u2192 ${chalk.bold(`v${latest_betterauth_version}`)})` }); if (isCancel(shouldInstallBetterAuthDep)) { cancel(`\u270B Operation cancelled.`); process.exit(0); } if (shouldInstallBetterAuthDep) { if (packageManagerPreference === void 0) { packageManagerPreference = await getPackageManager$1(); } const s2 = spinner({ indicator: "dots" }); s2.start( `Updating Better Auth using ${chalk.bold(packageManagerPreference)}` ); try { const start = Date.now(); await installDependencies({ dependencies: ["better-auth@latest"], packageManager: packageManagerPreference, cwd }); s2.stop( `Better Auth updated ${chalk.greenBright( `successfully` )}! ${chalk.gray(`(${formatMilliseconds(Date.now() - start)})`)}` ); } catch (error) { s2.stop(`Failed to update Better Auth:`); log.error(error.message); process.exit(1); } } } else { s.stop(`Better Auth dependencies are ${chalk.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 ${chalk.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 ${chalk.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? ${chalk.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; log.info(`Creating auth config file: ${filePath}`); try { current_user_config = await getDefaultAuthConfig({ appName }); const { dependencies, envs, generatedCode } = await generateAuthConfig({ current_user_config, format: format$1, //@ts-expect-error s, plugins: add_plugins, database }); current_user_config = generatedCode; await fs$1.writeFile(filePath, current_user_config); config_path = filePath; log.success(`\u{1F680} Auth config file successfully created!`); if (envs.length !== 0) { log.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) { log.info("No .env files to update. Skipping..."); } else { try { await updateEnvs({ files: filesToUpdate, envs, isCommented: true }); } catch (error) { log.error(`Failed to update .env files:`); log.error(JSON.stringify(error, null, 2)); process.exit(1); } log.success(`\u{1F680} ENV files successfully updated!`); } } } if (dependencies.length !== 0) { log.info( `There are ${dependencies.length} dependencies to install. (${dependencies.map((x) => chalk.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$1(); } s2.start( `Installing dependencies using ${chalk.bold( packageManagerPreference )}...` ); try { const start = Date.now(); await installDependencies({ dependencies, packageManager: packageManagerPreference, cwd }); s2.stop( `Dependencies installed ${chalk.greenBright( `successfully` )} ${chalk.gray( `(${formatMilliseconds(Date.now() - start)})` )}` ); } catch (error) { s2.stop( `Failed to install dependencies using ${packageManagerPreference}:` ); log.error(error.message); process.exit(1); } } } } catch (error) { log.error(`Failed to create auth config file: ${filePath}`); console.error(error); process.exit(1); } } else if (shouldCreateAuthConfig === "no") { log.info(`Skipping auth config file creation.`); } } else { log.message(); log.success(`Found auth config file. ${chalk.gray(`(${config_path})`)}`); log.message(); } 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"); log.info(`Creating auth client config file: ${authClientConfigPath}`); try { let 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$1.writeFile(authClientConfigPath, contents); log.success(`\u{1F680} Auth client config file successfully created!`); } catch (error) { log.error( `Failed to create auth client config file: ${authClientConfigPath}` ); log.error(JSON.stringify(error, null, 2)); process.exit(1); } } else if (choice === "no") { log.info(`Skipping auth client config file creation.`); } } else { log.success( `Found auth client config file. ${chalk.gray( `(${authClientConfigPath})` )}` ); } if (targetEnvFile !== "none") { try { const fileContents = await fs$1.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 = chalk.bold(`BETTER_AUTH_SECRET`); else if (!isMissingSecret && isMissingUrl) txt = chalk.bold(`BETTER_AUTH_URL`); else txt = chalk.bold.underline(`BETTER_AUTH_SECRET`) + ` and ` + chalk.bold.underline(`BETTER_AUTH_URL`); log.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); } let 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) { log.error(`Failed to add ENV variables to ${targetEnvFile}`); log.error(JSON.stringify(error, null, 2)); process.exit(1); } log.success(`\u{1F680} ENV variables successfully added!`); if (isMissingUrl) { log.info( `Be sure to update your BETTER_AUTH_URL according to your app's needs.` ); } } else if (shouldAdd === "no") { log.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) { log.info("No .env files to update. Skipping..."); } else { try { await updateEnvs({ files: envFilesToUpdate, envs, isCommented: false }); } catch (error) { log.error(`Failed to update .env files:`); log.error(JSON.stringify(error, null, 2)); process.exit(1); } log.success(`\u{1F680} ENV files successfully updated!`); } } } } catch (error) { } } outro(outroText); console.log(); process.exit(0); } const init = new Command("init").option("-c, --cwd <cwd>", "The working directory.", process.cwd()).option( "--config <config>", "The path to the auth configuration file. defaults to the first `auth.ts` file found." ).option("--tsconfig <tsconfig>", "The path to the tsconfig file.").option("--skip-db", "Skip the database setup.").option("--skip-plugins", "Skip the plugins setup.").option( "--package-manager <package-manager>", "The package manager you want to use." ).action(initAction); 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$1() { 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" }); let packageManager = await select({ 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$1.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$1.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$1.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