in packages/cli/src/commands/init.ts [360:1026]
export async function initAction(opts: any) {
console.log();
intro("👋 Initializing Better Auth");
const options = optionsSchema.parse(opts);
const cwd = path.resolve(options.cwd);
let packageManagerPreference: "bun" | "pnpm" | "yarn" | "npm" | undefined =
undefined;
let config_path: string = "";
let framework: SupportedFrameworks = "vanilla";
const format = async (code: string) =>
await prettierFormat(code, {
filepath: config_path,
...defaultFormatOptions,
});
// ===== package.json =====
let packageInfo: Record<string, any>;
try {
packageInfo = getPackageInfo(cwd);
} catch (error) {
log.error(`❌ Couldn't read your package.json file. (dir: ${cwd})`);
log.error(JSON.stringify(error, null, 2));
process.exit(1);
}
// ===== ENV files =====
const envFiles = await getEnvFiles(cwd);
if (!envFiles.length) {
outro("❌ No .env files found. Please create an env file first.");
process.exit(0);
}
let targetEnvFile: string;
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";
// ===== tsconfig.json =====
let tsconfigInfo: Record<string, any>;
try {
const tsconfigPath =
options.tsconfig !== undefined
? path.resolve(cwd, options.tsconfig)
: path.join(cwd, "tsconfig.json");
tsconfigInfo = await getTsconfigInfo(cwd, tsconfigPath);
} catch (error) {
log.error(`❌ 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(`✋ Operation cancelled.`);
process.exit(0);
}
if (shouldAdd) {
try {
await fs.writeFile(
path.join(cwd, "tsconfig.json"),
await prettierFormat(
JSON.stringify(
Object.assign(tsconfigInfo, {
compilerOptions: {
strict: true,
},
}),
),
{ filepath: "tsconfig.json", ...defaultFormatOptions },
),
"utf-8",
);
log.success(`🚀 tsconfig.json successfully updated!`);
} catch (error) {
log.error(
`Failed to add "compilerOptions.strict" to your tsconfig.json file.`,
);
console.error(error);
process.exit(1);
}
}
}
// ===== install better-auth =====
const s = spinner({ indicator: "dots" });
s.start(`Checking better-auth installation`);
let latest_betterauth_version: string;
try {
latest_betterauth_version = await getLatestNpmVersion("better-auth");
} catch (error) {
log.error(`❌ 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(`✋ Operation cancelled.`);
process.exit(0);
}
if (packageManagerPreference === undefined) {
packageManagerPreference = await getPackageManager();
}
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: cwd,
});
s2.stop(
`Better Auth installed ${chalk.greenBright(
`successfully`,
)}! ${chalk.gray(`(${formatMilliseconds(Date.now() - start)})`)}`,
);
} catch (error: any) {
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"],
)} → ${chalk.bold(`v${latest_betterauth_version}`)})`,
});
if (isCancel(shouldInstallBetterAuthDep)) {
cancel(`✋ Operation cancelled.`);
process.exit(0);
}
if (shouldInstallBetterAuthDep) {
if (packageManagerPreference === undefined) {
packageManagerPreference = await getPackageManager();
}
const s = spinner({ indicator: "dots" });
s.start(
`Updating Better Auth using ${chalk.bold(packageManagerPreference)}`,
);
try {
const start = Date.now();
await installDependencies({
dependencies: ["better-auth@latest"],
packageManager: packageManagerPreference,
cwd: cwd,
});
s.stop(
`Better Auth updated ${chalk.greenBright(
`successfully`,
)}! ${chalk.gray(`(${formatMilliseconds(Date.now() - start)})`)}`,
);
} catch (error: any) {
s.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`)}!`);
}
// ===== appName =====
const packageJson = getPackageInfo(cwd);
let appName: string;
if (!packageJson.name) {
const newAppName = await text({
message: "What is the name of your application?",
});
if (isCancel(newAppName)) {
cancel("✋ Operation cancelled.");
process.exit(0);
}
appName = newAppName;
} else {
appName = packageJson.name;
}
// ===== config path =====
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;
}
}
}
// ===== create auth config =====
let current_user_config = "";
let database: SupportedDatabases | null = null;
let add_plugins: SupportedPlugin[] = [];
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(`✋ 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(`✋ 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(`✋ 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(`✋ 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(`✋ 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(`✋ 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,
//@ts-ignore
s,
plugins: add_plugins,
database,
});
current_user_config = generatedCode;
await fs.writeFile(filePath, current_user_config);
config_path = filePath;
log.success(`🚀 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("✋ 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("✋ 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(`🚀 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("✋ Operation cancelled.");
process.exit(0);
}
if (shouldInstallDeps) {
const s = spinner({ indicator: "dots" });
if (packageManagerPreference === undefined) {
packageManagerPreference = await getPackageManager();
}
s.start(
`Installing dependencies using ${chalk.bold(
packageManagerPreference,
)}...`,
);
try {
const start = Date.now();
await installDependencies({
dependencies: dependencies,
packageManager: packageManagerPreference,
cwd: cwd,
});
s.stop(
`Dependencies installed ${chalk.greenBright(
`successfully`,
)} ${chalk.gray(
`(${formatMilliseconds(Date.now() - start)})`,
)}`,
);
} catch (error: any) {
s.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();
}
// ===== auth client 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: string | null = 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(`✋ 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 contents = "";
if (plugin.id === "one-tap") {
contents = `{ clientId: "MY_CLIENT_ID" }`;
}
return {
contents,
id: plugin.id,
name: plugin.clientName!,
imports: [
{
path: "better-auth/client/plugins",
variables: [{ name: plugin.clientName! }],
},
],
};
}),
framework: framework,
});
await fs.writeFile(authClientConfigPath, contents);
log.success(`🚀 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.readFile(
path.join(cwd, targetEnvFile),
"utf8",
);
const parsed = parse(fileContents);
let isMissingSecret = false;
let isMissingUrl = false;
if (parsed.BETTER_AUTH_SECRET === undefined) isMissingSecret = true;
if (parsed.BETTER_AUTH_URL === undefined) 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(`✋ Operation cancelled.`);
process.exit(0);
}
let envs: string[] = [];
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: 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(`🚀 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("✋ Operation cancelled.");
process.exit(0);
}
if (envFilesToUpdate.length === 0) {
log.info("No .env files to update. Skipping...");
} else {
try {
await updateEnvs({
files: envFilesToUpdate,
envs: envs,
isCommented: false,
});
} catch (error) {
log.error(`Failed to update .env files:`);
log.error(JSON.stringify(error, null, 2));
process.exit(1);
}
log.success(`🚀 ENV files successfully updated!`);
}
}
}
} catch (error) {
// if fails, ignore, and do not proceed with ENV operations.
}
}
outro(outroText);
console.log();
process.exit(0);
}