@better-auth/cli
Version:
The CLI for Better Auth
1,217 lines (1,197 loc) • 118 kB
JavaScript
#!/usr/bin/env node
import { Command } from "commander";
import * as fs$2 from "node:fs";
import fs, { existsSync, readFileSync } from "node:fs";
import fs$1 from "node:fs/promises";
import * as path$1 from "node:path";
import path from "node:path";
import { createTelemetry, getTelemetryAuthConfig } from "@better-auth/telemetry";
import { getAdapter, getAuthTables, getMigrations } from "better-auth/db";
import chalk from "chalk";
import prompts from "prompts";
import yoctoSpinner from "yocto-spinner";
import * as z from "zod/v4";
import { initGetFieldName, initGetModelName } from "better-auth/adapters";
import prettier, { format } from "prettier";
import { capitalizeFirstLetter } from "@better-auth/core/utils";
import { produceSchema } from "@mrleebo/prisma-ast";
import babelPresetReact from "@babel/preset-react";
import babelPresetTypeScript from "@babel/preset-typescript";
import { BetterAuthError } from "@better-auth/core/error";
import { loadConfig } from "c12";
import { exec, execSync } from "node:child_process";
import * as os$1 from "node:os";
import os from "node:os";
import { cancel, confirm, intro, isCancel, log, multiselect, outro, select, spinner, text } from "@clack/prompts";
import { parse } from "dotenv";
import semver from "semver";
import Crypto from "node:crypto";
import { createAuthClient } from "better-auth/client";
import { deviceAuthorizationClient } from "better-auth/client/plugins";
import open from "open";
import { base64 } from "@better-auth/utils/base64";
import "dotenv/config";
//#region src/generators/drizzle.ts
function convertToSnakeCase(str, camelCase) {
if (camelCase) return str;
return str.replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2").replace(/([a-z\d])([A-Z])/g, "$1_$2").toLowerCase();
}
const generateDrizzleSchema = async ({ options, file, adapter }) => {
const tables = getAuthTables(options);
const filePath = file || "./auth-schema.ts";
const databaseType = adapter.options?.provider;
if (!databaseType) throw new Error(`Database provider type is undefined during Drizzle schema generation. Please define a \`provider\` in the Drizzle adapter config. Read more at https://better-auth.com/docs/adapters/drizzle`);
const fileExist = existsSync(filePath);
let code = generateImport({
databaseType,
tables,
options
});
const getModelName = initGetModelName({
schema: tables,
usePlural: adapter.options?.adapterConfig?.usePlural
});
const getFieldName = initGetFieldName({
schema: tables,
usePlural: adapter.options?.adapterConfig?.usePlural
});
for (const tableKey in tables) {
const table = tables[tableKey];
const modelName = getModelName(tableKey);
const fields = table.fields;
function getType(name, field) {
if (!databaseType) throw new Error(`Database provider type is undefined during Drizzle schema generation. Please define a \`provider\` in the Drizzle adapter config. Read more at https://better-auth.com/docs/adapters/drizzle`);
name = convertToSnakeCase(name, adapter.options?.camelCase);
if (field.references?.field === "id") {
const useNumberId$1 = options.advanced?.database?.useNumberId || options.advanced?.database?.generateId === "serial";
const useUUIDs = options.advanced?.database?.generateId === "uuid";
if (useNumberId$1) if (databaseType === "pg") return `integer('${name}')`;
else if (databaseType === "mysql") return `int('${name}')`;
else return `integer('${name}')`;
if (useUUIDs && databaseType === "pg") return `uuid('${name}')`;
if (field.references.field) {
if (databaseType === "mysql") return `varchar('${name}', { length: 36 })`;
}
return `text('${name}')`;
}
const type = field.type;
if (typeof type !== "string") if (Array.isArray(type) && type.every((x) => typeof x === "string")) return {
sqlite: `text({ enum: [${type.map((x) => `'${x}'`).join(", ")}] })`,
pg: `text('${name}', { enum: [${type.map((x) => `'${x}'`).join(", ")}] })`,
mysql: `mysqlEnum([${type.map((x) => `'${x}'`).join(", ")}])`
}[databaseType];
else throw new TypeError(`Invalid field type for field ${name} in model ${modelName}`);
const dbTypeMap = {
string: {
sqlite: `text('${name}')`,
pg: `text('${name}')`,
mysql: field.unique ? `varchar('${name}', { length: 255 })` : field.references ? `varchar('${name}', { length: 36 })` : field.sortable ? `varchar('${name}', { length: 255 })` : field.index ? `varchar('${name}', { length: 255 })` : `text('${name}')`
},
boolean: {
sqlite: `integer('${name}', { mode: 'boolean' })`,
pg: `boolean('${name}')`,
mysql: `boolean('${name}')`
},
number: {
sqlite: `integer('${name}')`,
pg: field.bigint ? `bigint('${name}', { mode: 'number' })` : `integer('${name}')`,
mysql: field.bigint ? `bigint('${name}', { mode: 'number' })` : `int('${name}')`
},
date: {
sqlite: `integer('${name}', { mode: 'timestamp_ms' })`,
pg: `timestamp('${name}')`,
mysql: `timestamp('${name}', { fsp: 3 })`
},
"number[]": {
sqlite: `text('${name}', { mode: "json" })`,
pg: field.bigint ? `bigint('${name}', { mode: 'number' }).array()` : `integer('${name}').array()`,
mysql: `text('${name}', { mode: 'json' })`
},
"string[]": {
sqlite: `text('${name}', { mode: "json" })`,
pg: `text('${name}').array()`,
mysql: `text('${name}', { mode: "json" })`
},
json: {
sqlite: `text('${name}', { mode: "json" })`,
pg: `jsonb('${name}')`,
mysql: `json('${name}', { mode: "json" })`
}
}[type];
if (!dbTypeMap) throw new Error(`Unsupported field type '${field.type}' for field '${name}'.`);
return dbTypeMap[databaseType];
}
let id = "";
const useNumberId = options.advanced?.database?.useNumberId || options.advanced?.database?.generateId === "serial";
if (options.advanced?.database?.generateId === "uuid" && databaseType === "pg") id = `uuid("id").default(sql\`pg_catalog.gen_random_uuid()\`).primaryKey()`;
else if (useNumberId) if (databaseType === "pg") id = `integer("id").generatedByDefaultAsIdentity().primaryKey()`;
else if (databaseType === "sqlite") id = `integer("id", { mode: "number" }).primaryKey({ autoIncrement: true })`;
else id = `int("id").autoincrement().primaryKey()`;
else if (databaseType === "mysql") id = `varchar('id', { length: 36 }).primaryKey()`;
else if (databaseType === "pg") id = `text('id').primaryKey()`;
else id = `text('id').primaryKey()`;
let indexes = [];
const assignIndexes = (indexes$1) => {
if (!indexes$1.length) return "";
let code$1 = [`, (table) => [`];
for (const index of indexes$1) code$1.push(` ${index.type}("${index.name}").on(table.${index.on}),`);
code$1.push(`]`);
return code$1.join("\n");
};
const schema = `export const ${modelName} = ${databaseType}Table("${convertToSnakeCase(modelName, adapter.options?.camelCase)}", {
id: ${id},
${Object.keys(fields).map((field) => {
const attr = fields[field];
const fieldName = attr.fieldName || field;
let type = getType(fieldName, attr);
if (attr.index && !attr.unique) indexes.push({
type: "index",
name: `${modelName}_${fieldName}_idx`,
on: fieldName
});
else if (attr.index && attr.unique) indexes.push({
type: "uniqueIndex",
name: `${modelName}_${fieldName}_uidx`,
on: fieldName
});
if (attr.defaultValue !== null && typeof attr.defaultValue !== "undefined") if (typeof attr.defaultValue === "function") {
if (attr.type === "date" && attr.defaultValue.toString().includes("new Date()")) if (databaseType === "sqlite") type += `.default(sql\`(cast(unixepoch('subsecond') * 1000 as integer))\`)`;
else type += `.defaultNow()`;
} else if (typeof attr.defaultValue === "string") type += `.default("${attr.defaultValue}")`;
else type += `.default(${attr.defaultValue})`;
if (attr.onUpdate && attr.type === "date") {
if (typeof attr.onUpdate === "function") type += `.$onUpdate(${attr.onUpdate})`;
}
return `${fieldName}: ${type}${attr.required ? ".notNull()" : ""}${attr.unique ? ".unique()" : ""}${attr.references ? `.references(()=> ${getModelName(attr.references.model)}.${getFieldName({
model: attr.references.model,
field: attr.references.field
})}, { onDelete: '${attr.references.onDelete || "cascade"}' })` : ""}`;
}).join(",\n ")}
}${assignIndexes(indexes)});`;
code += `\n${schema}\n`;
}
let relationsString = "";
for (const tableKey in tables) {
const table = tables[tableKey];
const modelName = getModelName(tableKey);
const oneRelations = [];
const manyRelations = [];
const manyRelationsSet = /* @__PURE__ */ new Set();
const foreignFields = Object.entries(table.fields).filter(([_, field]) => field.references);
for (const [fieldName, field] of foreignFields) {
const referencedModel = field.references.model;
const relationKey = getModelName(referencedModel);
const fieldRef = `${getModelName(tableKey)}.${getFieldName({
model: tableKey,
field: fieldName
})}`;
const referenceRef = `${getModelName(referencedModel)}.${getFieldName({
model: referencedModel,
field: field.references.field || "id"
})}`;
oneRelations.push({
key: relationKey,
model: getModelName(referencedModel),
type: "one",
reference: {
field: fieldRef,
references: referenceRef,
fieldName
}
});
}
const otherModels = Object.entries(tables).filter(([modelName$1]) => modelName$1 !== tableKey);
const modelRelationsMap = /* @__PURE__ */ new Map();
for (const [modelName$1, otherTable] of otherModels) {
const foreignKeysPointingHere = Object.entries(otherTable.fields).filter(([_, field]) => field.references?.model === tableKey || field.references?.model === getModelName(tableKey));
if (foreignKeysPointingHere.length === 0) continue;
const hasUnique = foreignKeysPointingHere.some(([_, field]) => !!field.unique);
const hasMany$1 = foreignKeysPointingHere.some(([_, field]) => !field.unique);
modelRelationsMap.set(modelName$1, {
modelName: modelName$1,
hasUnique,
hasMany: hasMany$1
});
}
for (const { modelName: modelName$1, hasMany: hasMany$1 } of modelRelationsMap.values()) {
const relationType = hasMany$1 ? "many" : "one";
let relationKey = getModelName(modelName$1);
if (!adapter.options?.adapterConfig?.usePlural && relationType === "many") relationKey = `${relationKey}s`;
if (!manyRelationsSet.has(relationKey)) {
manyRelationsSet.add(relationKey);
manyRelations.push({
key: relationKey,
model: getModelName(modelName$1),
type: relationType
});
}
}
const relationsByModel = /* @__PURE__ */ new Map();
for (const relation of oneRelations) if (relation.reference) {
const modelKey = relation.key;
if (!relationsByModel.has(modelKey)) relationsByModel.set(modelKey, []);
relationsByModel.get(modelKey).push(relation);
}
const duplicateRelations = [];
const singleRelations = [];
for (const [_modelKey, relations] of relationsByModel.entries()) if (relations.length > 1) duplicateRelations.push(...relations);
else singleRelations.push(relations[0]);
for (const relation of duplicateRelations) if (relation.reference) {
const fieldName = relation.reference.fieldName;
const tableRelation = `export const ${`${modelName}${fieldName.charAt(0).toUpperCase() + fieldName.slice(1)}Relations`} = relations(${getModelName(table.modelName)}, ({ one }) => ({
${relation.key}: one(${relation.model}, {
fields: [${relation.reference.field}],
references: [${relation.reference.references}],
})
}))`;
relationsString += `\n${tableRelation}\n`;
}
const hasOne = singleRelations.length > 0;
const hasMany = manyRelations.length > 0;
if (hasOne && hasMany) {
const tableRelation = `export const ${modelName}Relations = relations(${getModelName(table.modelName)}, ({ one, many }) => ({
${singleRelations.map((relation) => relation.reference ? ` ${relation.key}: one(${relation.model}, {
fields: [${relation.reference.field}],
references: [${relation.reference.references}],
})` : "").filter((x) => x !== "").join(",\n ")}${singleRelations.length > 0 && manyRelations.length > 0 ? "," : ""}
${manyRelations.map(({ key, model }) => ` ${key}: many(${model})`).join(",\n ")}
}))`;
relationsString += `\n${tableRelation}\n`;
} else if (hasOne) {
const tableRelation = `export const ${modelName}Relations = relations(${getModelName(table.modelName)}, ({ one }) => ({
${singleRelations.map((relation) => relation.reference ? ` ${relation.key}: one(${relation.model}, {
fields: [${relation.reference.field}],
references: [${relation.reference.references}],
})` : "").filter((x) => x !== "").join(",\n ")}
}))`;
relationsString += `\n${tableRelation}\n`;
} else if (hasMany) {
const tableRelation = `export const ${modelName}Relations = relations(${getModelName(table.modelName)}, ({ many }) => ({
${manyRelations.map(({ key, model }) => ` ${key}: many(${model})`).join(",\n ")}
}))`;
relationsString += `\n${tableRelation}\n`;
}
}
code += `\n${relationsString}`;
return {
code: await prettier.format(code, { parser: "typescript" }),
fileName: filePath,
overwrite: fileExist
};
};
function generateImport({ databaseType, tables, options }) {
const rootImports = ["relations"];
const coreImports = [];
let hasBigint = false;
let hasJson = false;
for (const table of Object.values(tables)) {
for (const field of Object.values(table.fields)) {
if (field.bigint) hasBigint = true;
if (field.type === "json") hasJson = true;
}
if (hasJson && hasBigint) break;
}
const useNumberId = options.advanced?.database?.useNumberId || options.advanced?.database?.generateId === "serial";
const useUUIDs = options.advanced?.database?.generateId === "uuid";
coreImports.push(`${databaseType}Table`);
coreImports.push(databaseType === "mysql" ? "varchar, text" : databaseType === "pg" ? "text" : "text");
coreImports.push(hasBigint ? databaseType !== "sqlite" ? "bigint" : "" : "");
coreImports.push(databaseType !== "sqlite" ? "timestamp, boolean" : "");
if (databaseType === "mysql") {
const hasNonBigintNumber = Object.values(tables).some((table) => Object.values(table.fields).some((field) => (field.type === "number" || field.type === "number[]") && !field.bigint));
if (!!useNumberId || hasNonBigintNumber) coreImports.push("int");
if (Object.values(tables).some((table) => Object.values(table.fields).some((field) => typeof field.type !== "string" && Array.isArray(field.type) && field.type.every((x) => typeof x === "string")))) coreImports.push("mysqlEnum");
} else if (databaseType === "pg") {
if (useUUIDs) rootImports.push("sql");
const hasNonBigintNumber = Object.values(tables).some((table) => Object.values(table.fields).some((field) => (field.type === "number" || field.type === "number[]") && !field.bigint));
const hasFkToId = Object.values(tables).some((table) => Object.values(table.fields).some((field) => field.references?.field === "id"));
if (hasNonBigintNumber || (options.advanced?.database?.useNumberId || options.advanced?.database?.generateId === "serial") && hasFkToId) coreImports.push("integer");
} else coreImports.push("integer");
if (databaseType === "pg" && useUUIDs) coreImports.push("uuid");
if (hasJson) {
if (databaseType === "pg") coreImports.push("jsonb");
if (databaseType === "mysql") coreImports.push("json");
}
if (databaseType === "sqlite" && Object.values(tables).some((table) => Object.values(table.fields).some((field) => field.type === "date" && field.defaultValue && typeof field.defaultValue === "function" && field.defaultValue.toString().includes("new Date()")))) rootImports.push("sql");
const hasIndexes = Object.values(tables).some((table) => Object.values(table.fields).some((field) => field.index && !field.unique));
const hasUniqueIndexes = Object.values(tables).some((table) => Object.values(table.fields).some((field) => field.unique && field.index));
if (hasIndexes) coreImports.push("index");
if (hasUniqueIndexes) coreImports.push("uniqueIndex");
return `${rootImports.length > 0 ? `import { ${rootImports.join(", ")} } from "drizzle-orm";\n` : ""}import { ${coreImports.map((x) => x.trim()).filter((x) => x !== "").join(", ")} } from "drizzle-orm/${databaseType}-core";\n`;
}
//#endregion
//#region src/generators/kysely.ts
const generateMigrations = async ({ options, file }) => {
const { compileMigrations } = await getMigrations(options);
const migrations = await compileMigrations();
return {
code: migrations.trim() === ";" ? "" : migrations,
fileName: file || `./better-auth_migrations/${(/* @__PURE__ */ new Date()).toISOString().replace(/:/g, "-")}.sql`
};
};
//#endregion
//#region src/utils/get-package-info.ts
function getPackageInfo(cwd) {
const packageJsonPath = cwd ? path.join(cwd, "package.json") : path.join("package.json");
return JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
}
function getPrismaVersion(cwd) {
try {
const packageInfo = getPackageInfo(cwd);
const prismaVersion = packageInfo.dependencies?.prisma || packageInfo.devDependencies?.prisma || packageInfo.dependencies?.["@prisma/client"] || packageInfo.devDependencies?.["@prisma/client"];
if (!prismaVersion) return null;
const match = prismaVersion.match(/(\d+)/);
return match ? parseInt(match[1], 10) : null;
} catch {
return null;
}
}
//#endregion
//#region src/generators/prisma.ts
const generatePrismaSchema = async ({ adapter, options, file }) => {
const provider = adapter.options?.provider || "postgresql";
const tables = getAuthTables(options);
const filePath = file || "./prisma/schema.prisma";
const schemaPrismaExist = existsSync(path.join(process.cwd(), filePath));
const getModelName = initGetModelName({
schema: getAuthTables(options),
usePlural: adapter.options?.adapterConfig?.usePlural
});
const getFieldName = initGetFieldName({
schema: getAuthTables(options),
usePlural: false
});
let schemaPrisma = "";
if (schemaPrismaExist) schemaPrisma = await fs$1.readFile(path.join(process.cwd(), filePath), "utf-8");
else schemaPrisma = getNewPrisma(provider, process.cwd());
const prismaVersion = getPrismaVersion(process.cwd());
if (prismaVersion && prismaVersion >= 7 && schemaPrismaExist) schemaPrisma = produceSchema(schemaPrisma, (builder) => {
const generator = builder.findByType("generator", { name: "client" });
if (generator && generator.properties) {
const providerProp = generator.properties.find((prop) => prop.type === "assignment" && prop.key === "provider");
if (providerProp && providerProp.value === "\"prisma-client-js\"") providerProp.value = "\"prisma-client\"";
}
});
const manyToManyRelations = /* @__PURE__ */ new Map();
for (const table in tables) {
const fields = tables[table]?.fields;
for (const field in fields) {
const attr = fields[field];
if (attr.references) {
const referencedOriginalModel = attr.references.model;
const referencedModelNameCap = capitalizeFirstLetter(getModelName(tables[referencedOriginalModel]?.modelName || referencedOriginalModel));
if (!manyToManyRelations.has(referencedModelNameCap)) manyToManyRelations.set(referencedModelNameCap, /* @__PURE__ */ new Set());
const currentModelNameCap = capitalizeFirstLetter(getModelName(tables[table]?.modelName || table));
manyToManyRelations.get(referencedModelNameCap).add(currentModelNameCap);
}
}
}
const indexedFields = /* @__PURE__ */ new Map();
for (const table in tables) {
const fields = tables[table]?.fields;
const modelName = capitalizeFirstLetter(getModelName(tables[table]?.modelName || table));
indexedFields.set(modelName, []);
for (const field in fields) {
const attr = fields[field];
if (attr.index && !attr.unique) {
const fieldName = attr.fieldName || field;
indexedFields.get(modelName).push(fieldName);
}
}
}
const schema = produceSchema(schemaPrisma, (builder) => {
for (const table in tables) {
const originalTableName = table;
const customModelName = tables[table]?.modelName || table;
const modelName = capitalizeFirstLetter(getModelName(customModelName));
const fields = tables[table]?.fields;
function getType({ isBigint, isOptional, type }) {
if (type === "string") return isOptional ? "String?" : "String";
if (type === "number" && isBigint) return isOptional ? "BigInt?" : "BigInt";
if (type === "number") return isOptional ? "Int?" : "Int";
if (type === "boolean") return isOptional ? "Boolean?" : "Boolean";
if (type === "date") return isOptional ? "DateTime?" : "DateTime";
if (type === "json") {
if (provider === "sqlite" || provider === "mysql") return isOptional ? "String?" : "String";
return isOptional ? "Json?" : "Json";
}
if (type === "string[]") {
if (provider === "sqlite" || provider === "mysql") return isOptional ? "String?" : "String";
return "String[]";
}
if (type === "number[]") {
if (provider === "sqlite" || provider === "mysql") return "String";
return "Int[]";
}
}
const prismaModel = builder.findByType("model", { name: modelName });
if (!prismaModel) if (provider === "mongodb") builder.model(modelName).field("id", "String").attribute("id").attribute(`map("_id")`);
else {
const useNumberId = options.advanced?.database?.useNumberId || options.advanced?.database?.generateId === "serial";
const useUUIDs = options.advanced?.database?.generateId === "uuid";
if (useNumberId) builder.model(modelName).field("id", "Int").attribute("id").attribute("default(autoincrement())");
else if (useUUIDs && provider === "postgresql") builder.model(modelName).field("id", "String").attribute("id").attribute("default(dbgenerated(\"pg_catalog.gen_random_uuid()\"))").attribute("db.Uuid");
else builder.model(modelName).field("id", "String").attribute("id");
}
for (const field in fields) {
const attr = fields[field];
const fieldName = attr.fieldName || field;
if (prismaModel) {
if (builder.findByType("field", {
name: fieldName,
within: prismaModel.properties
})) continue;
}
const useUUIDs = options.advanced?.database?.generateId === "uuid";
const useNumberId = options.advanced?.database?.useNumberId || options.advanced?.database?.generateId === "serial";
const fieldBuilder = builder.model(modelName).field(fieldName, field === "id" && useNumberId ? getType({
isBigint: false,
isOptional: false,
type: "number"
}) : getType({
isBigint: attr?.bigint || false,
isOptional: !attr?.required,
type: attr.references?.field === "id" ? useNumberId ? "number" : "string" : attr.type
}));
if (field === "id") {
fieldBuilder.attribute("id");
if (provider === "mongodb") fieldBuilder.attribute(`map("_id")`);
}
if (attr.unique) builder.model(modelName).blockAttribute(`unique([${fieldName}])`);
if (attr.defaultValue !== void 0) {
if (Array.isArray(attr.defaultValue)) {
if (attr.type === "json") {
if (Object.prototype.toString.call(attr.defaultValue[0]) === "[object Object]") {
fieldBuilder.attribute(`default("${JSON.stringify(attr.defaultValue).replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}")`);
continue;
}
let jsonArray = [];
for (const value of attr.defaultValue) jsonArray.push(value);
fieldBuilder.attribute(`default("${JSON.stringify(jsonArray).replace(/"/g, "\\\"")}")`);
continue;
}
if (attr.defaultValue.length === 0) {
fieldBuilder.attribute(`default([])`);
continue;
} else if (typeof attr.defaultValue[0] === "string" && attr.type === "string[]") {
let valueArray = [];
for (const value of attr.defaultValue) valueArray.push(JSON.stringify(value));
fieldBuilder.attribute(`default([${valueArray}])`);
} else if (typeof attr.defaultValue[0] === "number") {
let valueArray = [];
for (const value of attr.defaultValue) valueArray.push(`${value}`);
fieldBuilder.attribute(`default([${valueArray}])`);
}
} else if (typeof attr.defaultValue === "object" && !Array.isArray(attr.defaultValue) && attr.defaultValue !== null) {
if (Object.entries(attr.defaultValue).length === 0) {
fieldBuilder.attribute(`default("{}")`);
continue;
}
fieldBuilder.attribute(`default("${JSON.stringify(attr.defaultValue).replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}")`);
}
if (field === "createdAt") fieldBuilder.attribute("default(now())");
else if (typeof attr.defaultValue === "string" && provider !== "mysql") fieldBuilder.attribute(`default("${attr.defaultValue}")`);
else if (typeof attr.defaultValue === "boolean" || typeof attr.defaultValue === "number") fieldBuilder.attribute(`default(${attr.defaultValue})`);
else if (typeof attr.defaultValue === "function") {}
}
if (field === "updatedAt" && attr.onUpdate) fieldBuilder.attribute("updatedAt");
else if (attr.onUpdate) {}
if (attr.references) {
if (useUUIDs && provider === "postgresql" && attr.references?.field === "id") builder.model(modelName).field(fieldName).attribute(`db.Uuid`);
const referencedOriginalModelName = getModelName(attr.references.model);
const referencedCustomModelName = tables[referencedOriginalModelName]?.modelName || referencedOriginalModelName;
let action = "Cascade";
if (attr.references.onDelete === "no action") action = "NoAction";
else if (attr.references.onDelete === "set null") action = "SetNull";
else if (attr.references.onDelete === "set default") action = "SetDefault";
else if (attr.references.onDelete === "restrict") action = "Restrict";
const relationField = `relation(fields: [${getFieldName({
model: originalTableName,
field: fieldName
})}], references: [${getFieldName({
model: attr.references.model,
field: attr.references.field
})}], onDelete: ${action})`;
builder.model(modelName).field(referencedCustomModelName.toLowerCase(), `${capitalizeFirstLetter(referencedCustomModelName)}${!attr.required ? "?" : ""}`).attribute(relationField);
}
if (!attr.unique && !attr.references && provider === "mysql" && attr.type === "string") builder.model(modelName).field(fieldName).attribute("db.Text");
}
if (manyToManyRelations.has(modelName)) for (const relatedModel of manyToManyRelations.get(modelName)) {
const relatedTableName = Object.keys(tables).find((key) => capitalizeFirstLetter(tables[key]?.modelName || key) === relatedModel);
const relatedFields = relatedTableName ? tables[relatedTableName]?.fields : {};
const [_fieldKey, fkFieldAttr] = Object.entries(relatedFields || {}).find(([_fieldName, fieldAttr]) => fieldAttr.references && getModelName(fieldAttr.references.model) === getModelName(originalTableName)) || [];
const isUnique = fkFieldAttr?.unique === true;
const fieldName = isUnique || adapter.options?.usePlural === true ? `${relatedModel.toLowerCase()}` : `${relatedModel.toLowerCase()}s`;
if (!builder.findByType("field", {
name: fieldName,
within: prismaModel?.properties
})) builder.model(modelName).field(fieldName, `${relatedModel}${isUnique ? "?" : "[]"}`);
}
const indexedFieldsForModel = indexedFields.get(modelName);
if (indexedFieldsForModel && indexedFieldsForModel.length > 0) for (const fieldName of indexedFieldsForModel) {
if (prismaModel) {
if (prismaModel.properties.some((v) => v.type === "attribute" && v.name === "index" && JSON.stringify(v.args[0]?.value).includes(fieldName))) continue;
}
const field = Object.entries(fields).find(([key, attr]) => (attr.fieldName || key) === fieldName)?.[1];
let indexField = fieldName;
if (provider === "mysql" && field && field.type === "string") {
const useNumberId = options.advanced?.database?.useNumberId || options.advanced?.database?.generateId === "serial";
const useUUIDs = options.advanced?.database?.generateId === "uuid";
if (field.references?.field === "id" && (useNumberId || useUUIDs)) indexField = `${fieldName}`;
else indexField = `${fieldName}(length: 191)`;
}
builder.model(modelName).blockAttribute(`index([${indexField}])`);
}
const hasAttribute = builder.findByType("attribute", {
name: "map",
within: prismaModel?.properties
});
const hasChanged = customModelName !== originalTableName;
if (!hasAttribute) builder.model(modelName).blockAttribute("map", `${getModelName(hasChanged ? customModelName : originalTableName)}`);
}
});
const schemaChanged = schema.trim() !== schemaPrisma.trim();
return {
code: schemaChanged ? schema : "",
fileName: filePath,
overwrite: schemaPrismaExist && schemaChanged
};
};
const getNewPrisma = (provider, cwd) => {
const prismaVersion = getPrismaVersion(cwd);
return `generator client {
provider = "${prismaVersion && prismaVersion >= 7 ? "prisma-client" : "prisma-client-js"}"
}
datasource db {
provider = "${provider}"
url = ${provider === "sqlite" ? `"file:./dev.db"` : `env("DATABASE_URL")`}
}`;
};
//#endregion
//#region src/generators/index.ts
const adapters = {
prisma: generatePrismaSchema,
drizzle: generateDrizzleSchema,
kysely: generateMigrations
};
const generateSchema = (opts) => {
const adapter = opts.adapter;
const generator = adapter.id in adapters ? adapters[adapter.id] : null;
if (generator) return generator(opts);
if (adapter.createSchema) return adapter.createSchema(opts.options, opts.file).then(({ code, path: fileName, overwrite }) => ({
code,
fileName,
overwrite
}));
console.error(`${adapter.id} is not supported. If it is a custom adapter, please request the maintainer to implement createSchema`);
process.exit(1);
};
//#endregion
//#region src/utils/add-cloudflare-modules.ts
const createModule = () => {
return `data:text/javascript;charset=utf-8,${encodeURIComponent(`
const createStub = (label) => {
const handler = {
get(_, prop) {
if (prop === "toString") return () => label;
if (prop === "valueOf") return () => label;
if (prop === Symbol.toPrimitive) return () => label;
if (prop === Symbol.toStringTag) return "Object";
if (prop === "then") return undefined;
return createStub(label + "." + String(prop));
},
apply(_, __, args) {
return createStub(label + "()")
},
construct() {
return createStub(label + "#instance");
},
};
const fn = () => createStub(label + "()");
return new Proxy(fn, handler);
};
class WorkerEntrypoint {
constructor(ctx, env) {
this.ctx = ctx;
this.env = env;
}
}
class DurableObject {
constructor(state, env) {
this.state = state;
this.env = env;
}
}
class RpcTarget {
constructor(value) {
this.value = value;
}
}
const RpcStub = RpcTarget;
const env = createStub("env");
const caches = createStub("caches");
const scheduler = createStub("scheduler");
const executionCtx = createStub("executionCtx");
export { DurableObject, RpcStub, RpcTarget, WorkerEntrypoint, caches, env, executionCtx, scheduler };
const defaultExport = {
DurableObject,
RpcStub,
RpcTarget,
WorkerEntrypoint,
caches,
env,
executionCtx,
scheduler,
};
export default defaultExport;
// jiti dirty hack: .unknown
`)}`;
};
const CLOUDFLARE_STUB_MODULE = createModule();
function addCloudflareModules(aliases, _cwd) {
if (!aliases["cloudflare:workers"]) aliases["cloudflare:workers"] = CLOUDFLARE_STUB_MODULE;
if (!aliases["cloudflare:test"]) aliases["cloudflare:test"] = CLOUDFLARE_STUB_MODULE;
}
//#endregion
//#region src/utils/add-svelte-kit-env-modules.ts
/**
* Adds SvelteKit environment modules and path aliases
* @param aliases - The aliases object to populate
* @param cwd - Current working directory (optional, defaults to process.cwd())
*/
function addSvelteKitEnvModules(aliases, cwd) {
const workingDir = cwd || process.cwd();
aliases["$env/dynamic/private"] = createDataUriModule(createDynamicEnvModule());
aliases["$env/dynamic/public"] = createDataUriModule(createDynamicEnvModule());
aliases["$env/static/private"] = createDataUriModule(createStaticEnvModule(filterPrivateEnv("PUBLIC_", "")));
aliases["$env/static/public"] = createDataUriModule(createStaticEnvModule(filterPublicEnv("PUBLIC_", "")));
const svelteKitAliases = getSvelteKitPathAliases(workingDir);
Object.assign(aliases, svelteKitAliases);
}
function getSvelteKitPathAliases(cwd) {
const aliases = {};
const packageJsonPath = path.join(cwd, "package.json");
const svelteConfigPath = path.join(cwd, "svelte.config.js");
const svelteConfigTsPath = path.join(cwd, "svelte.config.ts");
let isSvelteKitProject = false;
if (fs.existsSync(packageJsonPath)) try {
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
isSvelteKitProject = !!{
...packageJson.dependencies,
...packageJson.devDependencies
}["@sveltejs/kit"];
} catch {}
if (!isSvelteKitProject) isSvelteKitProject = fs.existsSync(svelteConfigPath) || fs.existsSync(svelteConfigTsPath);
if (!isSvelteKitProject) return aliases;
const libPaths = [path.join(cwd, "src", "lib"), path.join(cwd, "lib")];
for (const libPath of libPaths) if (fs.existsSync(libPath)) {
aliases["$lib"] = libPath;
for (const subPath of [
"server",
"utils",
"components",
"stores"
]) {
const subDir = path.join(libPath, subPath);
if (fs.existsSync(subDir)) aliases[`$lib/${subPath}`] = subDir;
}
break;
}
aliases["$app/server"] = createDataUriModule(createAppServerModule());
const customAliases = getSvelteConfigAliases(cwd);
Object.assign(aliases, customAliases);
return aliases;
}
function getSvelteConfigAliases(cwd) {
const aliases = {};
const configPaths = [path.join(cwd, "svelte.config.js"), path.join(cwd, "svelte.config.ts")];
for (const configPath of configPaths) if (fs.existsSync(configPath)) {
try {
const aliasMatch = fs.readFileSync(configPath, "utf-8").match(/alias\s*:\s*\{([^}]+)\}/);
if (aliasMatch && aliasMatch[1]) {
const aliasMatches = aliasMatch[1].matchAll(/['"`](\$[^'"`]+)['"`]\s*:\s*['"`]([^'"`]+)['"`]/g);
for (const match of aliasMatches) {
const [, alias, target] = match;
if (alias && target) {
aliases[alias + "/*"] = path.resolve(cwd, target) + "/*";
aliases[alias] = path.resolve(cwd, target);
}
}
}
} catch {}
break;
}
return aliases;
}
function createAppServerModule() {
return `
// $app/server stub for CLI compatibility
export default {};
// jiti dirty hack: .unknown
`;
}
function createDataUriModule(module) {
return `data:text/javascript;charset=utf-8,${encodeURIComponent(module)}`;
}
function createStaticEnvModule(env) {
return `
${Object.keys(env).filter((k) => validIdentifier.test(k) && !reserved.has(k)).map((k) => `export const ${k} = ${JSON.stringify(env[k])};`).join("\n")}
// jiti dirty hack: .unknown
`;
}
function createDynamicEnvModule() {
return `
export const env = process.env;
// jiti dirty hack: .unknown
`;
}
function filterPrivateEnv(publicPrefix, privatePrefix) {
return Object.fromEntries(Object.entries(process.env).filter(([k]) => k.startsWith(privatePrefix) && (publicPrefix === "" || !k.startsWith(publicPrefix))));
}
function filterPublicEnv(publicPrefix, privatePrefix) {
return Object.fromEntries(Object.entries(process.env).filter(([k]) => k.startsWith(publicPrefix) && (privatePrefix === "" || !k.startsWith(privatePrefix))));
}
const validIdentifier = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
const reserved = new Set([
"do",
"if",
"in",
"for",
"let",
"new",
"try",
"var",
"case",
"else",
"enum",
"eval",
"null",
"this",
"true",
"void",
"with",
"await",
"break",
"catch",
"class",
"const",
"false",
"super",
"throw",
"while",
"yield",
"delete",
"export",
"import",
"public",
"return",
"static",
"switch",
"typeof",
"default",
"extends",
"finally",
"package",
"private",
"continue",
"debugger",
"function",
"arguments",
"interface",
"protected",
"implements",
"instanceof"
]);
//#endregion
//#region src/utils/get-tsconfig-info.ts
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$1 = fs.readFileSync(tsConfigPath, "utf-8");
return JSON.parse(stripJsonComments(text$1));
} catch (error) {
throw error;
}
}
//#endregion
//#region src/utils/get-config.ts
let possiblePaths = [
"auth.ts",
"auth.tsx",
"auth.js",
"auth.jsx",
"auth.server.js",
"auth.server.ts",
"auth/index.ts",
"auth/index.tsx",
"auth/index.js",
"auth/index.jsx",
"auth/index.server.js",
"auth/index.server.ts"
];
possiblePaths = [
...possiblePaths,
...possiblePaths.map((it) => `lib/server/${it}`),
...possiblePaths.map((it) => `server/auth/${it}`),
...possiblePaths.map((it) => `server/${it}`),
...possiblePaths.map((it) => `auth/${it}`),
...possiblePaths.map((it) => `lib/${it}`),
...possiblePaths.map((it) => `utils/${it}`)
];
possiblePaths = [
...possiblePaths,
...possiblePaths.map((it) => `src/${it}`),
...possiblePaths.map((it) => `app/${it}`)
];
function resolveReferencePath(configDir, refPath) {
const resolvedPath = path.resolve(configDir, refPath);
if (refPath.endsWith(".json")) return resolvedPath;
if (fs.existsSync(resolvedPath)) try {
if (fs.statSync(resolvedPath).isFile()) return resolvedPath;
} catch {}
return path.resolve(configDir, refPath, "tsconfig.json");
}
function getPathAliasesRecursive(tsconfigPath, visited = /* @__PURE__ */ new Set()) {
if (visited.has(tsconfigPath)) return {};
visited.add(tsconfigPath);
if (!fs.existsSync(tsconfigPath)) {
console.warn(`Referenced tsconfig not found: ${tsconfigPath}`);
return {};
}
try {
const tsConfig = getTsconfigInfo(void 0, tsconfigPath);
const { paths = {}, baseUrl = "." } = tsConfig.compilerOptions || {};
const result = {};
const configDir = path.dirname(tsconfigPath);
const obj = Object.entries(paths);
for (const [alias, aliasPaths] of obj) for (const aliasedPath of aliasPaths) {
const resolvedBaseUrl = path.resolve(configDir, baseUrl);
const finalAlias = alias.slice(-1) === "*" ? alias.slice(0, -1) : alias;
const finalAliasedPath = aliasedPath.slice(-1) === "*" ? aliasedPath.slice(0, -1) : aliasedPath;
result[finalAlias || ""] = path.join(resolvedBaseUrl, finalAliasedPath);
}
if (tsConfig.references) for (const ref of tsConfig.references) {
const refAliases = getPathAliasesRecursive(resolveReferencePath(configDir, ref.path), visited);
for (const [alias, aliasPath] of Object.entries(refAliases)) if (!(alias in result)) result[alias] = aliasPath;
}
return result;
} catch (error) {
console.warn(`Error parsing tsconfig at ${tsconfigPath}: ${error}`);
return {};
}
}
function getPathAliases(cwd) {
const tsConfigPath = path.join(cwd, "tsconfig.json");
if (!fs.existsSync(tsConfigPath)) return null;
try {
const result = getPathAliasesRecursive(tsConfigPath);
addSvelteKitEnvModules(result);
addCloudflareModules(result);
return result;
} catch (error) {
console.error(error);
throw new BetterAuthError("Error parsing tsconfig.json");
}
}
/**
* .tsx files are not supported by Jiti.
*/
const jitiOptions = (cwd) => {
const alias = getPathAliases(cwd) || {};
return {
transformOptions: { babel: { presets: [[babelPresetTypeScript, {
isTSX: true,
allExtensions: true
}], [babelPresetReact, { runtime: "automatic" }]] } },
extensions: [
".ts",
".tsx",
".js",
".jsx"
],
alias
};
};
const isDefaultExport = (object) => {
return typeof object === "object" && object !== null && !Array.isArray(object) && Object.keys(object).length > 0 && "options" in object;
};
async function getConfig({ cwd, configPath, shouldThrowOnError = false }) {
try {
let configFile = null;
if (configPath) {
let resolvedPath = path.join(cwd, configPath);
if (existsSync(configPath)) resolvedPath = configPath;
const { config } = await loadConfig({
configFile: resolvedPath,
dotenv: true,
jitiOptions: jitiOptions(cwd),
cwd
});
if (!("auth" in config) && !isDefaultExport(config)) {
if (shouldThrowOnError) throw new Error(`Couldn't read your auth config in ${resolvedPath}. Make sure to default export your auth instance or to export as a variable named auth.`);
console.error(`[#better-auth]: Couldn't read your auth config in ${resolvedPath}. Make sure to default export your auth instance or to export as a variable named auth.`);
process.exit(1);
}
configFile = "auth" in config ? config.auth?.options : config.options;
}
if (!configFile) for (const possiblePath of possiblePaths) try {
const { config } = await loadConfig({
configFile: possiblePath,
jitiOptions: jitiOptions(cwd),
cwd
});
if (Object.keys(config).length > 0) {
configFile = config.auth?.options || config.default?.options || null;
if (!configFile) {
if (shouldThrowOnError) throw new Error("Couldn't read your auth config. Make sure to default export your auth instance or to export as a variable named auth.");
console.error("[#better-auth]: Couldn't read your auth config.");
console.log("");
console.log("[#better-auth]: Make sure to default export your auth instance or to export as a variable named auth.");
process.exit(1);
}
break;
}
} catch (e) {
if (typeof e === "object" && e && "message" in e && typeof e.message === "string" && e.message.includes("This module cannot be imported from a Client Component module")) {
if (shouldThrowOnError) throw new Error(`Please remove import 'server-only' from your auth config file temporarily. The CLI cannot resolve the configuration with it included. You can re-add it after running the CLI.`);
console.error(`Please remove import 'server-only' from your auth config file temporarily. The CLI cannot resolve the configuration with it included. You can re-add it after running the CLI.`);
process.exit(1);
}
if (shouldThrowOnError) throw e;
console.error("[#better-auth]: Couldn't read your auth config.", e);
process.exit(1);
}
return configFile;
} catch (e) {
if (typeof e === "object" && e && "message" in e && typeof e.message === "string" && e.message.includes("This module cannot be imported from a Client Component module")) {
if (shouldThrowOnError) throw new Error(`Please remove import 'server-only' from your auth config file temporarily. The CLI cannot resolve the configuration with it included. You can re-add it after running the CLI.`);
console.error(`Please remove import 'server-only' from your auth config file temporarily. The CLI cannot resolve the configuration with it included. You can re-add it after running the CLI.`);
process.exit(1);
}
if (shouldThrowOnError) throw e;
console.error("Couldn't read your auth config.", e);
process.exit(1);
}
}
//#endregion
//#region src/commands/generate.ts
async function generateAction(opts) {
const options = z.object({
cwd: z.string(),
config: z.string().optional(),
output: z.string().optional(),
y: z.boolean().optional(),
yes: z.boolean().optional()
}).parse(opts);
const cwd = path.resolve(options.cwd);
if (!existsSync(cwd)) {
console.error(`The directory "${cwd}" does not exist.`);
process.exit(1);
}
const config = await getConfig({
cwd,
configPath: options.config
});
if (!config) {
console.error("No configuration file found. Add a `auth.ts` file to your project or pass the path to the configuration file using the `--config` flag.");
return;
}
const adapter = await getAdapter(config).catch((e) => {
console.error(e.message);
process.exit(1);
});
const spinner$1 = yoctoSpinner({ text: "preparing schema..." }).start();
const schema = await generateSchema({
adapter,
file: options.output,
options: config
});
spinner$1.stop();
if (!schema.code) {
console.log("Your schema is already up to date.");
try {
await (await createTelemetry(config)).publish({
type: "cli_generate",
payload: {
outcome: "no_changes",
config: getTelemetryAuthConfig(config, {
adapter: adapter.id,
database: typeof config.database === "function" ? "adapter" : "kysely"
})
}
});
} catch {}
process.exit(0);
}
if (schema.overwrite) {
let confirm$2 = options.y || options.yes;
if (!confirm$2) confirm$2 = (await prompts({
type: "confirm",
name: "confirm",
message: `The file ${schema.fileName} already exists. Do you want to ${chalk.yellow(`${schema.overwrite ? "overwrite" : "append"}`)} the schema to the file?`
})).confirm;
if (confirm$2) {
if (!existsSync(path.join(cwd, schema.fileName))) await fs$1.mkdir(path.dirname(path.join(cwd, schema.fileName)), { recursive: true });
if (schema.overwrite) await fs$1.writeFile(path.join(cwd, schema.fileName), schema.code);
else await fs$1.appendFile(path.join(cwd, schema.fileName), schema.code);
console.log(`🚀 Schema was ${schema.overwrite ? "overwritten" : "appended"} successfully!`);
try {
await (await createTelemetry(config)).publish({
type: "cli_generate",
payload: {
outcome: schema.overwrite ? "overwritten" : "appended",
config: getTelemetryAuthConfig(config)
}
});
} catch {}
process.exit(0);
} else {
console.error("Schema generation aborted.");
try {
await (await createTelemetry(config)).publish({
type: "cli_generate",
payload: {
outcome: "aborted",
config: getTelemetryAuthConfig(config)
}
});
} catch {}
process.exit(1);
}
}
if (options.y) {
console.warn("WARNING: --y is deprecated. Consider -y or --yes");
options.yes = true;
}
let confirm$1 = options.yes;
if (!confirm$1) confirm$1 = (await prompts({
type: "confirm",
name: "confirm",
message: `Do you want to generate the schema to ${chalk.yellow(schema.fileName)}?`
})).confirm;
if (!confirm$1) {
console.error("Schema generation aborted.");
try {
await (await createTelemetry(config)).publish({
type: "cli_generate",
payload: {
outcome: "aborted",
config: getTelemetryAuthConfig(config)
}
});
} catch {}
process.exit(1);
}
if (!options.output) {
if (!existsSync(path.dirname(path.join(cwd, schema.fileName)))) await fs$1.mkdir(path.dirname(path.join(cwd, schema.fileName)), { recursive: true });
}
await fs$1.writeFile(options.output || path.join(cwd, schema.fileName), schema.code);
console.log(`🚀 Schema was generated successfully!`);
try {
await (await createTelemetry(config)).publish({
type: "cli_generate",
payload: {
outcome: "generated",
config: getTelemetryAuthConfig(config)
}
});
} catch {}
process.exit(0);
}
const generate = new Command("generate").option("-c, --cwd <cwd>", "the working directory. defaults to the current directory.", process.cwd()).option("--config <config>", "the path to the configuration file. defaults to the first configuration file found.").option("--output <output>", "the file to output to the generated schema").option("-y, --yes", "automatically answer yes to all prompts", false).option("--y", "(deprecated) same as --yes", false).action(generateAction);
//#endregion
//#region src/commands/info.ts
function getSystemInfo() {
const platform = os.platform();
const arch = os.arch();
const version = os.version();
const release = os.release();
const cpus = os.cpus();
const memory = os.totalmem();
const freeMemory = os.freemem();
return {
platform,
arch,
version,
release,
cpuCount: cpus.length,
cpuModel: cpus[0]?.model || "Unknown",
totalMemory: `${(memory / 1024 / 1024 / 1024).toFixed(2)} GB`,
freeMemory: `${(freeMemory / 1024 / 1024 / 1024).toFixed(2)} GB`
};
}
function getNodeInfo() {
return {
version: process.version,
env: process.env.NODE_ENV || "development"
};
}
function getPackageManager$1() {
const userAgent = process.env.npm_config_user_agent || "";
if (userAgent.includes("yarn")) return {
name: "yarn",
version: getVersion("yarn")
};
if (userAgent.includes("pnpm")) return {
name: "pnpm",
version: getVersion("pnpm")
};
if (userAgent.includes("bun")) return {
name: "bun",
version: getVersion("bun")
};
return {
name: "npm",
version: getVersion("npm")
};
}
function getVersion(command) {
try {
return execSync(`${command} --version`, { encoding: "utf8" }).trim();
} catch {
return "Not installed";
}
}
function getFrameworkInfo(projectRoot) {
const packageJsonPath = path.join(projectRoot, "package.json");
if (!existsSync(packageJsonPath)) return null;
try {
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
const deps = {
...packageJson.dependencies,
...packageJson.devDependencies
};
const frameworks = {
next: deps["next"],
react: deps["react"],
vue: deps["vue"],
nuxt: deps["nuxt"],
svelte: deps["svelte"],
"@sveltejs/kit": deps["@sveltejs/kit"],
express: deps["express"],
fastify: deps["fastify"],
hono: deps["hono"],
remix: deps["@remix-run/react"],
astro: deps["astro"],
solid: deps["solid-js"],
qwik: deps["@builder.io/qwik"]
};
const installedFrameworks = Object.entries(frameworks).filter(([_, version]) => version).map(([name, version]) => ({
name,
version
}));
return installedFrameworks.length > 0 ? installedFrameworks : null;
} catch {
return null;
}
}
function getDatabaseInfo(projectRoot) {
const packageJsonPath = path.join(projectRoot, "package.json");
if (!existsSync(packageJsonPath)) return null;
try {
const packageJson = JSON.parse(readFileSync(packageJsonPat