@better-auth/cli 1.3.5-beta.4 → 1.3.5-beta.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.mjs +2443 -2306
  2. package/package.json +2 -2
package/dist/index.mjs CHANGED
@@ -1,2501 +1,2638 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from 'commander';
3
+ import { parse } from 'dotenv';
4
+ import semver from 'semver';
5
+ import prettier, { format } from 'prettier';
3
6
  import * as z from 'zod/v4';
4
- import fs$1, { existsSync } from 'fs';
7
+ import fs$2, { existsSync } from 'fs';
5
8
  import path from 'path';
6
- import yoctoSpinner from 'yocto-spinner';
9
+ import fs$1 from 'fs/promises';
10
+ import fs from 'fs-extra';
7
11
  import chalk from 'chalk';
12
+ import { intro, log, outro, confirm, isCancel, cancel, spinner, text, select, multiselect } from '@clack/prompts';
13
+ import { exec } from 'child_process';
14
+ import { logger, BetterAuthError, createTelemetry, getTelemetryAuthConfig, capitalizeFirstLetter } from 'better-auth';
15
+ import Crypto from 'crypto';
16
+ import yoctoSpinner from 'yocto-spinner';
8
17
  import prompts from 'prompts';
9
- import { logger, BetterAuthError, capitalizeFirstLetter } from 'better-auth';
10
18
  import { getAdapter, getMigrations, getAuthTables } from 'better-auth/db';
11
19
  import { loadConfig } from 'c12';
12
20
  import babelPresetTypeScript from '@babel/preset-typescript';
13
21
  import babelPresetReact from '@babel/preset-react';
14
- import fs from 'fs-extra';
15
- import fs$2 from 'fs/promises';
16
- import prettier, { format } from 'prettier';
17
22
  import { produceSchema } from '@mrleebo/prisma-ast';
18
23
  import 'dotenv/config';
19
- import Crypto from 'crypto';
20
- import { parse } from 'dotenv';
21
- import semver from 'semver';
22
- import { intro, log, outro, confirm, isCancel, cancel, spinner, text, select, multiselect } from '@clack/prompts';
23
- import { exec } from 'child_process';
24
-
25
- function addSvelteKitEnvModules(aliases) {
26
- aliases["$env/dynamic/private"] = createDataUriModule(
27
- createDynamicEnvModule()
28
- );
29
- aliases["$env/dynamic/public"] = createDataUriModule(
30
- createDynamicEnvModule()
31
- );
32
- aliases["$env/static/private"] = createDataUriModule(
33
- createStaticEnvModule(filterPrivateEnv("PUBLIC_", ""))
34
- );
35
- aliases["$env/static/public"] = createDataUriModule(
36
- createStaticEnvModule(filterPublicEnv("PUBLIC_", ""))
37
- );
38
- }
39
- function createDataUriModule(module) {
40
- return `data:text/javascript;charset=utf-8,${encodeURIComponent(module)}`;
41
- }
42
- function createStaticEnvModule(env) {
43
- const declarations = Object.keys(env).filter((k) => validIdentifier.test(k) && !reserved.has(k)).map((k) => `export const ${k} = ${JSON.stringify(env[k])};`);
44
- return `
45
- ${declarations.join("\n")}
46
- // jiti dirty hack: .unknown
47
- `;
48
- }
49
- function createDynamicEnvModule() {
50
- return `
51
- export const env = process.env;
52
- // jiti dirty hack: .unknown
53
- `;
54
- }
55
- function filterPrivateEnv(publicPrefix, privatePrefix) {
56
- return Object.fromEntries(
57
- Object.entries(process.env).filter(
58
- ([k]) => k.startsWith(privatePrefix) && (!k.startsWith(publicPrefix))
59
- )
60
- );
61
- }
62
- function filterPublicEnv(publicPrefix, privatePrefix) {
63
- return Object.fromEntries(
64
- Object.entries(process.env).filter(
65
- ([k]) => k.startsWith(publicPrefix) && (privatePrefix === "")
66
- )
67
- );
68
- }
69
- const validIdentifier = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
70
- const reserved = /* @__PURE__ */ new Set([
71
- "do",
72
- "if",
73
- "in",
74
- "for",
75
- "let",
76
- "new",
77
- "try",
78
- "var",
79
- "case",
80
- "else",
81
- "enum",
82
- "eval",
83
- "null",
84
- "this",
85
- "true",
86
- "void",
87
- "with",
88
- "await",
89
- "break",
90
- "catch",
91
- "class",
92
- "const",
93
- "false",
94
- "super",
95
- "throw",
96
- "while",
97
- "yield",
98
- "delete",
99
- "export",
100
- "import",
101
- "public",
102
- "return",
103
- "static",
104
- "switch",
105
- "typeof",
106
- "default",
107
- "extends",
108
- "finally",
109
- "package",
110
- "private",
111
- "continue",
112
- "debugger",
113
- "function",
114
- "arguments",
115
- "interface",
116
- "protected",
117
- "implements",
118
- "instanceof"
119
- ]);
120
24
 
121
- function stripJsonComments(jsonString) {
122
- return jsonString.replace(
123
- /\\"|"(?:\\"|[^"])*"|(\/\/.*|\/\*[\s\S]*?\*\/)/g,
124
- (m, g) => g ? "" : m
125
- ).replace(/,(?=\s*[}\]])/g, "");
126
- }
127
- function getTsconfigInfo(cwd, flatPath) {
128
- let tsConfigPath;
129
- if (flatPath) {
130
- tsConfigPath = flatPath;
131
- } else {
132
- tsConfigPath = cwd ? path.join(cwd, "tsconfig.json") : path.join("tsconfig.json");
133
- }
25
+ function getPackageInfo(cwd) {
26
+ const packageJsonPath = cwd ? path.join(cwd, "package.json") : path.join("package.json");
134
27
  try {
135
- const text = fs.readFileSync(tsConfigPath, "utf-8");
136
- return JSON.parse(stripJsonComments(text));
28
+ return fs.readJSONSync(packageJsonPath);
137
29
  } catch (error) {
138
30
  throw error;
139
31
  }
140
32
  }
141
33
 
142
- let possiblePaths = [
143
- "auth.ts",
144
- "auth.tsx",
145
- "auth.js",
146
- "auth.jsx",
147
- "auth.server.js",
148
- "auth.server.ts"
149
- ];
150
- possiblePaths = [
151
- ...possiblePaths,
152
- ...possiblePaths.map((it) => `lib/server/${it}`),
153
- ...possiblePaths.map((it) => `server/${it}`),
154
- ...possiblePaths.map((it) => `lib/${it}`),
155
- ...possiblePaths.map((it) => `utils/${it}`)
156
- ];
157
- possiblePaths = [
158
- ...possiblePaths,
159
- ...possiblePaths.map((it) => `src/${it}`),
160
- ...possiblePaths.map((it) => `app/${it}`)
161
- ];
162
- function getPathAliases(cwd) {
163
- const tsConfigPath = path.join(cwd, "tsconfig.json");
164
- if (!fs$1.existsSync(tsConfigPath)) {
165
- return null;
34
+ function installDependencies({
35
+ dependencies,
36
+ packageManager,
37
+ cwd
38
+ }) {
39
+ let installCommand;
40
+ switch (packageManager) {
41
+ case "npm":
42
+ installCommand = "npm install --force";
43
+ break;
44
+ case "pnpm":
45
+ installCommand = "pnpm install";
46
+ break;
47
+ case "bun":
48
+ installCommand = "bun install";
49
+ break;
50
+ case "yarn":
51
+ installCommand = "yarn install";
52
+ break;
53
+ default:
54
+ throw new Error("Invalid package manager");
166
55
  }
167
- try {
168
- const tsConfig = getTsconfigInfo(cwd);
169
- const { paths = {}, baseUrl = "." } = tsConfig.compilerOptions || {};
170
- const result = {};
171
- const obj = Object.entries(paths);
172
- for (const [alias, aliasPaths] of obj) {
173
- for (const aliasedPath of aliasPaths) {
174
- const resolvedBaseUrl = path.join(cwd, baseUrl);
175
- const finalAlias = alias.slice(-1) === "*" ? alias.slice(0, -1) : alias;
176
- const finalAliasedPath = aliasedPath.slice(-1) === "*" ? aliasedPath.slice(0, -1) : aliasedPath;
177
- result[finalAlias || ""] = path.join(resolvedBaseUrl, finalAliasedPath);
56
+ const command = `${installCommand} ${dependencies.join(" ")}`;
57
+ return new Promise((resolve, reject) => {
58
+ exec(command, { cwd }, (error, stdout, stderr) => {
59
+ if (error) {
60
+ reject(new Error(stderr));
61
+ return;
178
62
  }
179
- }
180
- addSvelteKitEnvModules(result);
181
- return result;
182
- } catch (error) {
183
- console.error(error);
184
- throw new BetterAuthError("Error parsing tsconfig.json");
185
- }
63
+ resolve(true);
64
+ });
65
+ });
186
66
  }
187
- const jitiOptions = (cwd) => {
188
- const alias = getPathAliases(cwd) || {};
189
- return {
190
- transformOptions: {
191
- babel: {
192
- presets: [
193
- [
194
- babelPresetTypeScript,
195
- {
196
- isTSX: true,
197
- allExtensions: true
198
- }
199
- ],
200
- [babelPresetReact, { runtime: "automatic" }]
201
- ]
67
+
68
+ function checkCommand(command) {
69
+ return new Promise((resolve) => {
70
+ exec(`${command} --version`, (error) => {
71
+ if (error) {
72
+ resolve(false);
73
+ } else {
74
+ resolve(true);
202
75
  }
203
- },
204
- extensions: [".ts", ".tsx", ".js", ".jsx"],
205
- alias
76
+ });
77
+ });
78
+ }
79
+ async function checkPackageManagers() {
80
+ const hasPnpm = await checkCommand("pnpm");
81
+ const hasBun = await checkCommand("bun");
82
+ return {
83
+ hasPnpm,
84
+ hasBun
206
85
  };
207
- };
208
- async function getConfig({
209
- cwd,
210
- configPath,
211
- shouldThrowOnError = false
212
- }) {
213
- try {
214
- let configFile = null;
215
- if (configPath) {
216
- let resolvedPath = path.join(cwd, configPath);
217
- if (existsSync(configPath)) resolvedPath = configPath;
218
- const { config } = await loadConfig({
219
- configFile: resolvedPath,
220
- dotenv: true,
221
- jitiOptions: jitiOptions(cwd)
222
- });
223
- if (!config.auth && !config.default) {
224
- if (shouldThrowOnError) {
225
- throw new Error(
226
- `Couldn't read your auth config in ${resolvedPath}. Make sure to default export your auth instance or to export as a variable named auth.`
227
- );
228
- }
229
- logger.error(
230
- `[#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.`
86
+ }
87
+
88
+ function formatMilliseconds(ms) {
89
+ if (ms < 0) {
90
+ throw new Error("Milliseconds cannot be negative");
91
+ }
92
+ if (ms < 1e3) {
93
+ return `${ms}ms`;
94
+ }
95
+ const seconds = Math.floor(ms / 1e3);
96
+ const milliseconds = ms % 1e3;
97
+ return `${seconds}s ${milliseconds}ms`;
98
+ }
99
+
100
+ const generateSecret = new Command("secret").action(() => {
101
+ const secret = generateSecretHash();
102
+ logger.info(`
103
+ Add the following to your .env file:
104
+ ${chalk.gray("# Auth Secret") + chalk.green(`
105
+ BETTER_AUTH_SECRET=${secret}`)}`);
106
+ });
107
+ const generateSecretHash = () => {
108
+ return Crypto.randomBytes(32).toString("hex");
109
+ };
110
+
111
+ async function generateAuthConfig({
112
+ format,
113
+ current_user_config,
114
+ spinner,
115
+ plugins,
116
+ database
117
+ }) {
118
+ let _start_of_plugins_common_index = {
119
+ START_OF_PLUGINS: {
120
+ type: "regex",
121
+ regex: /betterAuth\([\w\W]*plugins:[\W]*\[()/m,
122
+ getIndex: ({ matchIndex, match }) => {
123
+ return matchIndex + match[0].length;
124
+ }
125
+ }
126
+ };
127
+ const common_indexes = {
128
+ START_OF_PLUGINS: _start_of_plugins_common_index.START_OF_PLUGINS,
129
+ END_OF_PLUGINS: {
130
+ type: "manual",
131
+ getIndex: ({ content, additionalFields }) => {
132
+ const closingBracketIndex = findClosingBracket(
133
+ content,
134
+ additionalFields.start_of_plugins,
135
+ "[",
136
+ "]"
231
137
  );
232
- process.exit(1);
138
+ return closingBracketIndex;
139
+ }
140
+ },
141
+ START_OF_BETTERAUTH: {
142
+ type: "regex",
143
+ regex: /betterAuth\({()/m,
144
+ getIndex: ({ matchIndex }) => {
145
+ return matchIndex + "betterAuth({".length;
233
146
  }
234
- configFile = config.auth?.options || config.default?.options || null;
235
147
  }
236
- if (!configFile) {
237
- for (const possiblePath of possiblePaths) {
238
- try {
239
- const { config } = await loadConfig({
240
- configFile: possiblePath,
241
- jitiOptions: jitiOptions(cwd)
242
- });
243
- const hasConfig = Object.keys(config).length > 0;
244
- if (hasConfig) {
245
- configFile = config.auth?.options || config.default?.options || null;
246
- if (!configFile) {
247
- if (shouldThrowOnError) {
248
- throw new Error(
249
- "Couldn't read your auth config. Make sure to default export your auth instance or to export as a variable named auth."
250
- );
251
- }
252
- logger.error("[#better-auth]: Couldn't read your auth config.");
253
- console.log("");
254
- logger.info(
255
- "[#better-auth]: Make sure to default export your auth instance or to export as a variable named auth."
256
- );
257
- process.exit(1);
258
- }
259
- break;
260
- }
261
- } catch (e) {
262
- if (typeof e === "object" && e && "message" in e && typeof e.message === "string" && e.message.includes(
263
- "This module cannot be imported from a Client Component module"
264
- )) {
265
- if (shouldThrowOnError) {
266
- throw new Error(
267
- `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.`
268
- );
269
- }
270
- logger.error(
271
- `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.`
272
- );
273
- process.exit(1);
148
+ };
149
+ const config_generation = {
150
+ add_plugin: async (opts) => {
151
+ let start_of_plugins = getGroupInfo(
152
+ opts.config,
153
+ common_indexes.START_OF_PLUGINS,
154
+ {}
155
+ );
156
+ if (!start_of_plugins) {
157
+ throw new Error(
158
+ "Couldn't find start of your plugins array in your auth config file."
159
+ );
160
+ }
161
+ let end_of_plugins = getGroupInfo(
162
+ opts.config,
163
+ common_indexes.END_OF_PLUGINS,
164
+ { start_of_plugins: start_of_plugins.index }
165
+ );
166
+ if (!end_of_plugins) {
167
+ throw new Error(
168
+ "Couldn't find end of your plugins array in your auth config file."
169
+ );
170
+ }
171
+ let new_content;
172
+ if (opts.direction_in_plugins_array === "prepend") {
173
+ new_content = insertContent({
174
+ line: start_of_plugins.line,
175
+ character: start_of_plugins.character,
176
+ content: opts.config,
177
+ insert_content: `${opts.pluginFunctionName}(${opts.pluginContents}),`
178
+ });
179
+ } else {
180
+ let has_found_comma = false;
181
+ const str = opts.config.slice(start_of_plugins.index, end_of_plugins.index).split("").reverse();
182
+ for (let index = 0; index < str.length; index++) {
183
+ const char = str[index];
184
+ if (char === ",") {
185
+ has_found_comma = true;
274
186
  }
275
- if (shouldThrowOnError) {
276
- throw e;
187
+ if (char === ")") {
188
+ break;
277
189
  }
278
- logger.error("[#better-auth]: Couldn't read your auth config.", e);
279
- process.exit(1);
280
190
  }
191
+ new_content = insertContent({
192
+ line: end_of_plugins.line,
193
+ character: end_of_plugins.character,
194
+ content: opts.config,
195
+ insert_content: `${!has_found_comma ? "," : ""}${opts.pluginFunctionName}(${opts.pluginContents})`
196
+ });
281
197
  }
282
- }
283
- return configFile;
284
- } catch (e) {
285
- if (typeof e === "object" && e && "message" in e && typeof e.message === "string" && e.message.includes(
286
- "This module cannot be imported from a Client Component module"
287
- )) {
288
- if (shouldThrowOnError) {
198
+ try {
199
+ new_content = await format(new_content);
200
+ } catch (error) {
201
+ console.error(error);
289
202
  throw new Error(
290
- `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.`
203
+ `Failed to generate new auth config during plugin addition phase.`
291
204
  );
292
205
  }
293
- logger.error(
294
- `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.`
295
- );
296
- process.exit(1);
297
- }
298
- if (shouldThrowOnError) {
299
- throw e;
300
- }
301
- logger.error("Couldn't read your auth config.", e);
302
- process.exit(1);
303
- }
304
- }
305
-
306
- async function migrateAction(opts) {
307
- const options = z.object({
308
- cwd: z.string(),
309
- config: z.string().optional(),
310
- y: z.boolean().optional(),
311
- yes: z.boolean().optional()
312
- }).parse(opts);
313
- const cwd = path.resolve(options.cwd);
314
- if (!existsSync(cwd)) {
315
- logger.error(`The directory "${cwd}" does not exist.`);
316
- process.exit(1);
317
- }
318
- const config = await getConfig({
319
- cwd,
320
- configPath: options.config
321
- });
322
- if (!config) {
323
- logger.error(
324
- "No configuration file found. Add a `auth.ts` file to your project or pass the path to the configuration file using the `--config` flag."
325
- );
326
- return;
327
- }
328
- const db = await getAdapter(config);
329
- if (!db) {
330
- logger.error(
331
- "Invalid database configuration. Make sure you're not using adapters. Migrate command only works with built-in Kysely adapter."
332
- );
333
- process.exit(1);
334
- }
335
- if (db.id !== "kysely") {
336
- if (db.id === "prisma") {
337
- logger.error(
338
- "The migrate command only works with the built-in Kysely adapter. For Prisma, run `npx @better-auth/cli generate` to create the schema, then use Prisma\u2019s migrate or push to apply it."
339
- );
340
- process.exit(0);
341
- }
342
- if (db.id === "drizzle") {
343
- logger.error(
344
- "The migrate command only works with the built-in Kysely adapter. For Drizzle, run `npx @better-auth/cli generate` to create the schema, then use Drizzle\u2019s migrate or push to apply it."
345
- );
346
- process.exit(0);
347
- }
348
- logger.error("Migrate command isn't supported for this adapter.");
349
- process.exit(1);
350
- }
351
- const spinner = yoctoSpinner({ text: "preparing migration..." }).start();
352
- const { toBeAdded, toBeCreated, runMigrations } = await getMigrations(config);
353
- if (!toBeAdded.length && !toBeCreated.length) {
354
- spinner.stop();
355
- logger.info("\u{1F680} No migrations needed.");
356
- process.exit(0);
357
- }
358
- spinner.stop();
359
- logger.info(`\u{1F511} The migration will affect the following:`);
360
- for (const table of [...toBeCreated, ...toBeAdded]) {
361
- console.log(
362
- "->",
363
- chalk.magenta(Object.keys(table.fields).join(", ")),
364
- chalk.white("fields on"),
365
- chalk.yellow(`${table.table}`),
366
- chalk.white("table.")
367
- );
368
- }
369
- if (options.y) {
370
- console.warn("WARNING: --y is deprecated. Consider -y or --yes");
371
- options.yes = true;
372
- }
373
- let migrate2 = options.yes;
374
- if (!migrate2) {
375
- const response = await prompts({
376
- type: "confirm",
377
- name: "migrate",
378
- message: "Are you sure you want to run these migrations?",
379
- initial: false
380
- });
381
- migrate2 = response.migrate;
382
- }
383
- if (!migrate2) {
384
- logger.info("Migration cancelled.");
385
- process.exit(0);
386
- }
387
- spinner?.start("migrating...");
388
- await runMigrations();
389
- spinner.stop();
390
- logger.info("\u{1F680} migration was completed successfully!");
391
- process.exit(0);
392
- }
393
- const migrate = new Command("migrate").option(
394
- "-c, --cwd <cwd>",
395
- "the working directory. defaults to the current directory.",
396
- process.cwd()
397
- ).option(
398
- "--config <config>",
399
- "the path to the configuration file. defaults to the first configuration file found."
400
- ).option(
401
- "-y, --yes",
402
- "automatically accept and run migrations without prompting",
403
- false
404
- ).option("--y", "(deprecated) same as --yes", false).action(migrateAction);
405
-
406
- function convertToSnakeCase(str, camelCase) {
407
- if (camelCase) {
408
- return str;
409
- }
410
- return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
411
- }
412
- const generateDrizzleSchema = async ({
413
- options,
414
- file,
415
- adapter
416
- }) => {
417
- const tables = getAuthTables(options);
418
- const filePath = file || "./auth-schema.ts";
419
- const databaseType = adapter.options?.provider;
420
- if (!databaseType) {
421
- throw new Error(
422
- `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`
423
- );
424
- }
425
- const fileExist = existsSync(filePath);
426
- let code = generateImport({ databaseType, tables, options });
427
- for (const tableKey in tables) {
428
- let getType = function(name, field) {
429
- if (!databaseType) {
206
+ return { code: await new_content, dependencies: [], envs: [] };
207
+ },
208
+ add_import: async (opts) => {
209
+ let importString = "";
210
+ for (const import_ of opts.imports) {
211
+ if (Array.isArray(import_.variables)) {
212
+ importString += `import { ${import_.variables.map(
213
+ (x) => `${x.asType ? "type " : ""}${x.name}${x.as ? ` as ${x.as}` : ""}`
214
+ ).join(", ")} } from "${import_.path}";
215
+ `;
216
+ } else {
217
+ importString += `import ${import_.variables.asType ? "type " : ""}${import_.variables.name}${import_.variables.as ? ` as ${import_.variables.as}` : ""} from "${import_.path}";
218
+ `;
219
+ }
220
+ }
221
+ try {
222
+ let new_content = format(importString + opts.config);
223
+ return { code: await new_content, dependencies: [], envs: [] };
224
+ } catch (error) {
225
+ console.error(error);
430
226
  throw new Error(
431
- `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`
227
+ `Failed to generate new auth config during import addition phase.`
432
228
  );
433
229
  }
434
- name = convertToSnakeCase(name, adapter.options?.camelCase);
435
- if (field.references?.field === "id") {
436
- if (options.advanced?.database?.useNumberId) {
437
- if (databaseType === "pg") {
438
- return `integer('${name}')`;
439
- } else if (databaseType === "mysql") {
440
- return `int('${name}')`;
441
- } else {
442
- return `integer('${name}')`;
443
- }
444
- }
445
- if (field.references.field) {
446
- if (databaseType === "mysql") {
447
- return `varchar('${name}', { length: 36 })`;
230
+ },
231
+ add_database: async (opts) => {
232
+ const required_envs = [];
233
+ const required_deps = [];
234
+ let database_code_str = "";
235
+ async function add_db({
236
+ db_code,
237
+ dependencies,
238
+ envs,
239
+ imports,
240
+ code_before_betterAuth
241
+ }) {
242
+ if (code_before_betterAuth) {
243
+ let start_of_betterauth2 = getGroupInfo(
244
+ opts.config,
245
+ common_indexes.START_OF_BETTERAUTH,
246
+ {}
247
+ );
248
+ if (!start_of_betterauth2) {
249
+ throw new Error("Couldn't find start of betterAuth() function.");
448
250
  }
251
+ opts.config = insertContent({
252
+ line: start_of_betterauth2.line - 1,
253
+ character: 0,
254
+ content: opts.config,
255
+ insert_content: `
256
+ ${code_before_betterAuth}
257
+ `
258
+ });
449
259
  }
450
- return `text('${name}')`;
451
- }
452
- const type = field.type;
453
- const typeMap = {
454
- string: {
455
- sqlite: `text('${name}')`,
456
- pg: `text('${name}')`,
457
- mysql: field.unique ? `varchar('${name}', { length: 255 })` : field.references ? `varchar('${name}', { length: 36 })` : `text('${name}')`
458
- },
459
- boolean: {
460
- sqlite: `integer('${name}', { mode: 'boolean' })`,
461
- pg: `boolean('${name}')`,
462
- mysql: `boolean('${name}')`
463
- },
464
- number: {
465
- sqlite: `integer('${name}')`,
466
- pg: field.bigint ? `bigint('${name}', { mode: 'number' })` : `integer('${name}')`,
467
- mysql: field.bigint ? `bigint('${name}', { mode: 'number' })` : `int('${name}')`
468
- },
469
- date: {
470
- sqlite: `integer('${name}', { mode: 'timestamp' })`,
471
- pg: `timestamp('${name}')`,
472
- mysql: `timestamp('${name}')`
473
- },
474
- "number[]": {
475
- sqlite: `integer('${name}').array()`,
476
- pg: field.bigint ? `bigint('${name}', { mode: 'number' }).array()` : `integer('${name}').array()`,
477
- mysql: field.bigint ? `bigint('${name}', { mode: 'number' }).array()` : `int('${name}').array()`
478
- },
479
- "string[]": {
480
- sqlite: `text('${name}').array()`,
481
- pg: `text('${name}').array()`,
482
- mysql: `text('${name}').array()`
483
- }
484
- };
485
- return typeMap[type][databaseType];
486
- };
487
- const table = tables[tableKey];
488
- const modelName = getModelName(table.modelName, adapter.options);
489
- const fields = table.fields;
490
- let id = "";
491
- if (options.advanced?.database?.useNumberId) {
492
- if (databaseType === "pg") {
493
- id = `serial("id").primaryKey()`;
494
- } else {
495
- id = `int("id").autoincrement.primaryKey()`;
496
- }
497
- } else {
498
- if (databaseType === "mysql") {
499
- id = `varchar('id', { length: 36 }).primaryKey()`;
500
- } else if (databaseType === "pg") {
501
- id = `text('id').primaryKey()`;
502
- } else {
503
- id = `text('id').primaryKey()`;
504
- }
505
- }
506
- const schema = `export const ${modelName} = ${databaseType}Table("${convertToSnakeCase(
507
- modelName,
508
- adapter.options?.camelCase
509
- )}", {
510
- id: ${id},
511
- ${Object.keys(fields).map((field) => {
512
- const attr = fields[field];
513
- let type = getType(field, attr);
514
- if (attr.defaultValue) {
515
- if (typeof attr.defaultValue === "function") {
516
- type += `.$defaultFn(${attr.defaultValue})`;
517
- } else if (typeof attr.defaultValue === "string") {
518
- type += `.default("${attr.defaultValue}")`;
519
- } else {
520
- type += `.default(${attr.defaultValue})`;
521
- }
260
+ const code_gen = await config_generation.add_import({
261
+ config: opts.config,
262
+ imports
263
+ });
264
+ opts.config = code_gen.code;
265
+ database_code_str = db_code;
266
+ required_envs.push(...envs, ...code_gen.envs);
267
+ required_deps.push(...dependencies, ...code_gen.dependencies);
522
268
  }
523
- return `${field}: ${type}${attr.required ? ".notNull()" : ""}${attr.unique ? ".unique()" : ""}${attr.references ? `.references(()=> ${getModelName(
524
- attr.references.model,
525
- adapter.options
526
- )}.${attr.references.field}, { onDelete: '${attr.references.onDelete || "cascade"}' })` : ""}`;
527
- }).join(",\n ")}
528
- });`;
529
- code += `
530
- ${schema}
531
- `;
532
- }
533
- const formattedCode = await prettier.format(code, {
534
- parser: "typescript"
535
- });
536
- return {
537
- code: formattedCode,
538
- fileName: filePath,
539
- overwrite: fileExist
540
- };
541
- };
542
- function generateImport({
543
- databaseType,
544
- tables,
545
- options
546
- }) {
547
- let imports = [];
548
- const hasBigint = Object.values(tables).some(
549
- (table) => Object.values(table.fields).some((field) => field.bigint)
550
- );
551
- const useNumberId = options.advanced?.database?.useNumberId;
552
- imports.push(`${databaseType}Table`);
553
- imports.push(
554
- databaseType === "mysql" ? "varchar, text" : databaseType === "pg" ? "text" : "text"
555
- );
556
- imports.push(hasBigint ? databaseType !== "sqlite" ? "bigint" : "" : "");
557
- imports.push(databaseType !== "sqlite" ? "timestamp, boolean" : "");
558
- imports.push(databaseType === "mysql" ? "int" : "integer");
559
- imports.push(useNumberId ? databaseType === "pg" ? "serial" : "" : "");
560
- return `import { ${imports.map((x) => x.trim()).filter((x) => x !== "").join(", ")} } from "drizzle-orm/${databaseType}-core";
561
- `;
562
- }
563
- function getModelName(modelName, options) {
564
- return options?.usePlural ? `${modelName}s` : modelName;
565
- }
566
-
567
- const generatePrismaSchema = async ({
568
- adapter,
569
- options,
570
- file
571
- }) => {
572
- const provider = adapter.options?.provider || "postgresql";
573
- const tables = getAuthTables(options);
574
- const filePath = file || "./prisma/schema.prisma";
575
- const schemaPrismaExist = existsSync(path.join(process.cwd(), filePath));
576
- let schemaPrisma = "";
577
- if (schemaPrismaExist) {
578
- schemaPrisma = await fs$2.readFile(
579
- path.join(process.cwd(), filePath),
580
- "utf-8"
581
- );
582
- } else {
583
- schemaPrisma = getNewPrisma(provider);
584
- }
585
- const manyToManyRelations = /* @__PURE__ */ new Map();
586
- for (const table in tables) {
587
- const fields = tables[table]?.fields;
588
- for (const field in fields) {
589
- const attr = fields[field];
590
- if (attr.references) {
591
- const referencedModel = capitalizeFirstLetter(attr.references.model);
592
- if (!manyToManyRelations.has(referencedModel)) {
593
- manyToManyRelations.set(referencedModel, /* @__PURE__ */ new Set());
594
- }
595
- manyToManyRelations.get(referencedModel).add(capitalizeFirstLetter(table));
269
+ if (opts.database === "sqlite") {
270
+ await add_db({
271
+ db_code: `new Database(process.env.DATABASE_URL || "database.sqlite")`,
272
+ dependencies: ["better-sqlite3"],
273
+ envs: ["DATABASE_URL"],
274
+ imports: [
275
+ {
276
+ path: "better-sqlite3",
277
+ variables: {
278
+ asType: false,
279
+ name: "Database"
280
+ }
281
+ }
282
+ ]
283
+ });
284
+ } else if (opts.database === "postgres") {
285
+ await add_db({
286
+ db_code: `new Pool({
287
+ connectionString: process.env.DATABASE_URL || "postgresql://postgres:password@localhost:5432/database"
288
+ })`,
289
+ dependencies: ["pg"],
290
+ envs: ["DATABASE_URL"],
291
+ imports: [
292
+ {
293
+ path: "pg",
294
+ variables: [
295
+ {
296
+ asType: false,
297
+ name: "Pool"
298
+ }
299
+ ]
300
+ }
301
+ ]
302
+ });
303
+ } else if (opts.database === "mysql") {
304
+ await add_db({
305
+ db_code: `createPool(process.env.DATABASE_URL!)`,
306
+ dependencies: ["mysql2"],
307
+ envs: ["DATABASE_URL"],
308
+ imports: [
309
+ {
310
+ path: "mysql2/promise",
311
+ variables: [
312
+ {
313
+ asType: false,
314
+ name: "createPool"
315
+ }
316
+ ]
317
+ }
318
+ ]
319
+ });
320
+ } else if (opts.database === "mssql") {
321
+ const dialectCode = `new MssqlDialect({
322
+ tarn: {
323
+ ...Tarn,
324
+ options: {
325
+ min: 0,
326
+ max: 10,
327
+ },
328
+ },
329
+ tedious: {
330
+ ...Tedious,
331
+ connectionFactory: () => new Tedious.Connection({
332
+ authentication: {
333
+ options: {
334
+ password: 'password',
335
+ userName: 'username',
336
+ },
337
+ type: 'default',
338
+ },
339
+ options: {
340
+ database: 'some_db',
341
+ port: 1433,
342
+ trustServerCertificate: true,
343
+ },
344
+ server: 'localhost',
345
+ }),
346
+ },
347
+ })`;
348
+ await add_db({
349
+ code_before_betterAuth: dialectCode,
350
+ db_code: `dialect`,
351
+ dependencies: ["tedious", "tarn", "kysely"],
352
+ envs: ["DATABASE_URL"],
353
+ imports: [
354
+ {
355
+ path: "tedious",
356
+ variables: {
357
+ name: "*",
358
+ as: "Tedious"
359
+ }
360
+ },
361
+ {
362
+ path: "tarn",
363
+ variables: {
364
+ name: "*",
365
+ as: "Tarn"
366
+ }
367
+ },
368
+ {
369
+ path: "kysely",
370
+ variables: [
371
+ {
372
+ name: "MssqlDialect"
373
+ }
374
+ ]
375
+ }
376
+ ]
377
+ });
378
+ } else if (opts.database === "drizzle:mysql" || opts.database === "drizzle:sqlite" || opts.database === "drizzle:pg") {
379
+ await add_db({
380
+ db_code: `drizzleAdapter(db, {
381
+ provider: "${opts.database.replace(
382
+ "drizzle:",
383
+ ""
384
+ )}",
385
+ })`,
386
+ dependencies: [""],
387
+ envs: [],
388
+ imports: [
389
+ {
390
+ path: "better-auth/adapters/drizzle",
391
+ variables: [
392
+ {
393
+ name: "drizzleAdapter"
394
+ }
395
+ ]
396
+ },
397
+ {
398
+ path: "./database.ts",
399
+ variables: [
400
+ {
401
+ name: "db"
402
+ }
403
+ ]
404
+ }
405
+ ]
406
+ });
407
+ } else if (opts.database === "prisma:mysql" || opts.database === "prisma:sqlite" || opts.database === "prisma:postgresql") {
408
+ await add_db({
409
+ db_code: `prismaAdapter(client, {
410
+ provider: "${opts.database.replace(
411
+ "prisma:",
412
+ ""
413
+ )}",
414
+ })`,
415
+ dependencies: [`@prisma/client`],
416
+ envs: [],
417
+ code_before_betterAuth: "const client = new PrismaClient();",
418
+ imports: [
419
+ {
420
+ path: "better-auth/adapters/prisma",
421
+ variables: [
422
+ {
423
+ name: "prismaAdapter"
424
+ }
425
+ ]
426
+ },
427
+ {
428
+ path: "@prisma/client",
429
+ variables: [
430
+ {
431
+ name: "PrismaClient"
432
+ }
433
+ ]
434
+ }
435
+ ]
436
+ });
437
+ } else if (opts.database === "mongodb") {
438
+ await add_db({
439
+ db_code: `mongodbAdapter(db)`,
440
+ dependencies: ["mongodb"],
441
+ envs: [`DATABASE_URL`],
442
+ code_before_betterAuth: [
443
+ `const client = new MongoClient(process.env.DATABASE_URL || "mongodb://localhost:27017/database");`,
444
+ `const db = client.db();`
445
+ ].join("\n"),
446
+ imports: [
447
+ {
448
+ path: "better-auth/adapters/mongo",
449
+ variables: [
450
+ {
451
+ name: "mongodbAdapter"
452
+ }
453
+ ]
454
+ },
455
+ {
456
+ path: "mongodb",
457
+ variables: [
458
+ {
459
+ name: "MongoClient"
460
+ }
461
+ ]
462
+ }
463
+ ]
464
+ });
596
465
  }
597
- }
598
- }
599
- const schema = produceSchema(schemaPrisma, (builder) => {
600
- for (const table in tables) {
601
- let getType = function({
602
- isBigint,
603
- isOptional,
604
- type
605
- }) {
606
- if (type === "string") {
607
- return isOptional ? "String?" : "String";
608
- }
609
- if (type === "number" && isBigint) {
610
- return isOptional ? "BigInt?" : "BigInt";
611
- }
612
- if (type === "number") {
613
- return isOptional ? "Int?" : "Int";
614
- }
615
- if (type === "boolean") {
616
- return isOptional ? "Boolean?" : "Boolean";
617
- }
618
- if (type === "date") {
619
- return isOptional ? "DateTime?" : "DateTime";
620
- }
621
- if (type === "string[]") {
622
- return isOptional ? "String[]" : "String[]";
623
- }
624
- if (type === "number[]") {
625
- return isOptional ? "Int[]" : "Int[]";
626
- }
627
- };
628
- const fields = tables[table]?.fields;
629
- const originalTable = tables[table]?.modelName;
630
- const modelName = capitalizeFirstLetter(originalTable || "");
631
- const prismaModel = builder.findByType("model", {
632
- name: modelName
633
- });
634
- if (!prismaModel) {
635
- if (provider === "mongodb") {
636
- builder.model(modelName).field("id", "String").attribute("id").attribute(`map("_id")`);
637
- } else {
638
- if (options.advanced?.database?.useNumberId) {
639
- builder.model(modelName).field("id", "Int").attribute("id").attribute("default(autoincrement())");
640
- } else {
641
- builder.model(modelName).field("id", "String").attribute("id");
642
- }
643
- }
466
+ let start_of_betterauth = getGroupInfo(
467
+ opts.config,
468
+ common_indexes.START_OF_BETTERAUTH,
469
+ {}
470
+ );
471
+ if (!start_of_betterauth) {
472
+ throw new Error("Couldn't find start of betterAuth() function.");
644
473
  }
645
- for (const field in fields) {
646
- const attr = fields[field];
647
- if (prismaModel) {
648
- const isAlreadyExist = builder.findByType("field", {
649
- name: field,
650
- within: prismaModel.properties
651
- });
652
- if (isAlreadyExist) {
653
- continue;
654
- }
655
- }
656
- builder.model(modelName).field(
657
- field,
658
- field === "id" && options.advanced?.database?.useNumberId ? getType({
659
- isBigint: false,
660
- isOptional: false,
661
- type: "number"
662
- }) : getType({
663
- isBigint: attr?.bigint || false,
664
- isOptional: !attr?.required,
665
- type: attr.references?.field === "id" ? options.advanced?.database?.useNumberId ? "number" : "string" : attr.type
666
- })
474
+ let new_content;
475
+ new_content = insertContent({
476
+ line: start_of_betterauth.line,
477
+ character: start_of_betterauth.character,
478
+ content: opts.config,
479
+ insert_content: `database: ${database_code_str},`
480
+ });
481
+ try {
482
+ new_content = await format(new_content);
483
+ return {
484
+ code: new_content,
485
+ dependencies: required_deps,
486
+ envs: required_envs
487
+ };
488
+ } catch (error) {
489
+ console.error(error);
490
+ throw new Error(
491
+ `Failed to generate new auth config during database addition phase.`
667
492
  );
668
- if (attr.unique) {
669
- builder.model(modelName).blockAttribute(`unique([${field}])`);
670
- }
671
- if (attr.references) {
672
- let action = "Cascade";
673
- if (attr.references.onDelete === "no action") action = "NoAction";
674
- else if (attr.references.onDelete === "set null") action = "SetNull";
675
- else if (attr.references.onDelete === "set default")
676
- action = "SetDefault";
677
- else if (attr.references.onDelete === "restrict") action = "Restrict";
678
- builder.model(modelName).field(
679
- `${attr.references.model.toLowerCase()}`,
680
- capitalizeFirstLetter(attr.references.model)
681
- ).attribute(
682
- `relation(fields: [${field}], references: [${attr.references.field}], onDelete: ${action})`
683
- );
684
- }
685
- if (!attr.unique && !attr.references && provider === "mysql" && attr.type === "string") {
686
- builder.model(modelName).field(field).attribute("db.Text");
687
- }
688
493
  }
689
- if (manyToManyRelations.has(modelName)) {
690
- for (const relatedModel of manyToManyRelations.get(modelName)) {
691
- const fieldName = `${relatedModel.toLowerCase()}s`;
692
- const existingField = builder.findByType("field", {
693
- name: fieldName,
694
- within: prismaModel?.properties
695
- });
696
- if (!existingField) {
697
- builder.model(modelName).field(fieldName, `${relatedModel}[]`);
698
- }
699
- }
494
+ }
495
+ };
496
+ let new_user_config = await format(current_user_config);
497
+ let total_dependencies = [];
498
+ let total_envs = [];
499
+ if (plugins.length !== 0) {
500
+ const imports = [];
501
+ for await (const plugin of plugins) {
502
+ const existingIndex = imports.findIndex((x) => x.path === plugin.path);
503
+ if (existingIndex !== -1) {
504
+ imports[existingIndex].variables.push({
505
+ name: plugin.name,
506
+ asType: false
507
+ });
508
+ } else {
509
+ imports.push({
510
+ path: plugin.path,
511
+ variables: [
512
+ {
513
+ name: plugin.name,
514
+ asType: false
515
+ }
516
+ ]
517
+ });
700
518
  }
701
- const hasAttribute = builder.findByType("attribute", {
702
- name: "map",
703
- within: prismaModel?.properties
519
+ }
520
+ if (imports.length !== 0) {
521
+ const { code, envs, dependencies } = await config_generation.add_import({
522
+ config: new_user_config,
523
+ imports
704
524
  });
705
- if (originalTable !== modelName && !hasAttribute) {
706
- builder.model(modelName).blockAttribute("map", originalTable);
525
+ total_dependencies.push(...dependencies);
526
+ total_envs.push(...envs);
527
+ new_user_config = code;
528
+ }
529
+ }
530
+ for await (const plugin of plugins) {
531
+ try {
532
+ let pluginContents = "";
533
+ if (plugin.id === "magic-link") {
534
+ pluginContents = `{
535
+ sendMagicLink({ email, token, url }, request) {
536
+ // Send email with magic link
537
+ },
538
+ }`;
539
+ } else if (plugin.id === "email-otp") {
540
+ pluginContents = `{
541
+ async sendVerificationOTP({ email, otp, type }, request) {
542
+ // Send email with OTP
543
+ },
544
+ }`;
545
+ } else if (plugin.id === "generic-oauth") {
546
+ pluginContents = `{
547
+ config: [],
548
+ }`;
549
+ } else if (plugin.id === "oidc") {
550
+ pluginContents = `{
551
+ loginPage: "/sign-in",
552
+ }`;
707
553
  }
554
+ const { code, dependencies, envs } = await config_generation.add_plugin({
555
+ config: new_user_config,
556
+ direction_in_plugins_array: plugin.id === "next-cookies" ? "append" : "prepend",
557
+ pluginFunctionName: plugin.name,
558
+ pluginContents
559
+ });
560
+ new_user_config = code;
561
+ total_envs.push(...envs);
562
+ total_dependencies.push(...dependencies);
563
+ } catch (error) {
564
+ spinner.stop(
565
+ `Something went wrong while generating/updating your new auth config file.`,
566
+ 1
567
+ );
568
+ logger.error(error.message);
569
+ process.exit(1);
708
570
  }
709
- });
571
+ }
572
+ if (database) {
573
+ try {
574
+ const { code, dependencies, envs } = await config_generation.add_database(
575
+ {
576
+ config: new_user_config,
577
+ database
578
+ }
579
+ );
580
+ new_user_config = code;
581
+ total_dependencies.push(...dependencies);
582
+ total_envs.push(...envs);
583
+ } catch (error) {
584
+ spinner.stop(
585
+ `Something went wrong while generating/updating your new auth config file.`,
586
+ 1
587
+ );
588
+ logger.error(error.message);
589
+ process.exit(1);
590
+ }
591
+ }
710
592
  return {
711
- code: schema.trim() === schemaPrisma.trim() ? "" : schema,
712
- fileName: filePath,
713
- overwrite: true
593
+ generatedCode: new_user_config,
594
+ dependencies: total_dependencies,
595
+ envs: total_envs
714
596
  };
715
- };
716
- const getNewPrisma = (provider) => `generator client {
717
- provider = "prisma-client-js"
597
+ }
598
+ function findClosingBracket(content, startIndex, openingBracket, closingBracket) {
599
+ let stack = 0;
600
+ let inString = false;
601
+ let quoteChar = null;
602
+ for (let i = startIndex; i < content.length; i++) {
603
+ const char = content[i];
604
+ if (char === '"' || char === "'" || char === "`") {
605
+ if (!inString) {
606
+ inString = true;
607
+ quoteChar = char;
608
+ } else if (char === quoteChar) {
609
+ inString = false;
610
+ quoteChar = null;
611
+ }
612
+ continue;
613
+ }
614
+ if (!inString) {
615
+ if (char === openingBracket) {
616
+ stack++;
617
+ } else if (char === closingBracket) {
618
+ if (stack === 0) {
619
+ return i;
620
+ }
621
+ stack--;
622
+ }
623
+ }
624
+ }
625
+ return null;
626
+ }
627
+ function insertContent(params) {
628
+ const { line, character, content, insert_content } = params;
629
+ const lines = content.split("\n");
630
+ if (line < 1 || line > lines.length) {
631
+ throw new Error("Invalid line number");
632
+ }
633
+ const targetLineIndex = line - 1;
634
+ if (character < 0 || character > lines[targetLineIndex].length) {
635
+ throw new Error("Invalid character index");
636
+ }
637
+ const targetLine = lines[targetLineIndex];
638
+ const updatedLine = targetLine.slice(0, character) + insert_content + targetLine.slice(character);
639
+ lines[targetLineIndex] = updatedLine;
640
+ return lines.join("\n");
641
+ }
642
+ function getGroupInfo(content, commonIndexConfig, additionalFields) {
643
+ if (commonIndexConfig.type === "regex") {
644
+ const { regex, getIndex } = commonIndexConfig;
645
+ const match = regex.exec(content);
646
+ if (match) {
647
+ const matchIndex = match.index;
648
+ const groupIndex = getIndex({ matchIndex, match, additionalFields });
649
+ if (groupIndex === null) return null;
650
+ const position = getPosition(content, groupIndex);
651
+ return {
652
+ line: position.line,
653
+ character: position.character,
654
+ index: groupIndex
655
+ };
656
+ }
657
+ return null;
658
+ } else {
659
+ const { getIndex } = commonIndexConfig;
660
+ const index = getIndex({ content, additionalFields });
661
+ if (index === null) return null;
662
+ const { line, character } = getPosition(content, index);
663
+ return {
664
+ line,
665
+ character,
666
+ index
667
+ };
718
668
  }
719
-
720
- datasource db {
721
- provider = "${provider}"
722
- url = ${provider === "sqlite" ? `"file:./dev.db"` : `env("DATABASE_URL")`}
723
- }`;
724
-
725
- const generateMigrations = async ({
726
- options,
727
- file
728
- }) => {
729
- const { compileMigrations } = await getMigrations(options);
730
- const migrations = await compileMigrations();
669
+ }
670
+ const getPosition = (str, index) => {
671
+ const lines = str.slice(0, index).split("\n");
731
672
  return {
732
- code: migrations.trim() === ";" ? "" : migrations,
733
- fileName: file || `./better-auth_migrations/${(/* @__PURE__ */ new Date()).toISOString().replace(/:/g, "-")}.sql`
673
+ line: lines.length,
674
+ character: lines[lines.length - 1].length
734
675
  };
735
676
  };
736
677
 
737
- const adapters = {
738
- prisma: generatePrismaSchema,
739
- drizzle: generateDrizzleSchema,
740
- kysely: generateMigrations
741
- };
742
- const generateSchema = (opts) => {
743
- const adapter = opts.adapter;
744
- const generator = adapter.id in adapters ? adapters[adapter.id] : null;
745
- if (generator) {
746
- return generator(opts);
678
+ function stripJsonComments(jsonString) {
679
+ return jsonString.replace(
680
+ /\\"|"(?:\\"|[^"])*"|(\/\/.*|\/\*[\s\S]*?\*\/)/g,
681
+ (m, g) => g ? "" : m
682
+ ).replace(/,(?=\s*[}\]])/g, "");
683
+ }
684
+ function getTsconfigInfo(cwd, flatPath) {
685
+ let tsConfigPath;
686
+ if (flatPath) {
687
+ tsConfigPath = flatPath;
688
+ } else {
689
+ tsConfigPath = cwd ? path.join(cwd, "tsconfig.json") : path.join("tsconfig.json");
747
690
  }
748
- if (adapter.createSchema) {
749
- return adapter.createSchema(opts.options, opts.file).then(({ code, path: fileName, overwrite }) => ({
750
- code,
751
- fileName,
752
- overwrite
753
- }));
691
+ try {
692
+ const text = fs.readFileSync(tsConfigPath, "utf-8");
693
+ return JSON.parse(stripJsonComments(text));
694
+ } catch (error) {
695
+ throw error;
754
696
  }
755
- logger.error(
756
- `${adapter.id} is not supported. If it is a custom adapter, please request the maintainer to implement createSchema`
757
- );
758
- process.exit(1);
759
- };
760
- const getGenerator = generateSchema;
697
+ }
761
698
 
762
- async function generateAction(opts) {
763
- const options = z.object({
764
- cwd: z.string(),
765
- config: z.string().optional(),
766
- output: z.string().optional(),
767
- y: z.boolean().optional(),
768
- yes: z.boolean().optional()
769
- }).parse(opts);
770
- const cwd = path.resolve(options.cwd);
771
- if (!existsSync(cwd)) {
772
- logger.error(`The directory "${cwd}" does not exist.`);
773
- process.exit(1);
774
- }
775
- const config = await getConfig({
776
- cwd,
777
- configPath: options.config
778
- });
779
- if (!config) {
780
- logger.error(
781
- "No configuration file found. Add a `auth.ts` file to your project or pass the path to the configuration file using the `--config` flag."
782
- );
783
- return;
699
+ const supportedDatabases = [
700
+ // Built-in kysely
701
+ "sqlite",
702
+ "mysql",
703
+ "mssql",
704
+ "postgres",
705
+ // Drizzle
706
+ "drizzle:pg",
707
+ "drizzle:mysql",
708
+ "drizzle:sqlite",
709
+ // Prisma
710
+ "prisma:postgresql",
711
+ "prisma:mysql",
712
+ "prisma:sqlite",
713
+ // Mongo
714
+ "mongodb"
715
+ ];
716
+ const supportedPlugins = [
717
+ {
718
+ id: "two-factor",
719
+ name: "twoFactor",
720
+ path: `better-auth/plugins`,
721
+ clientName: "twoFactorClient",
722
+ clientPath: "better-auth/client/plugins"
723
+ },
724
+ {
725
+ id: "username",
726
+ name: "username",
727
+ clientName: "usernameClient",
728
+ path: `better-auth/plugins`,
729
+ clientPath: "better-auth/client/plugins"
730
+ },
731
+ {
732
+ id: "anonymous",
733
+ name: "anonymous",
734
+ clientName: "anonymousClient",
735
+ path: `better-auth/plugins`,
736
+ clientPath: "better-auth/client/plugins"
737
+ },
738
+ {
739
+ id: "phone-number",
740
+ name: "phoneNumber",
741
+ clientName: "phoneNumberClient",
742
+ path: `better-auth/plugins`,
743
+ clientPath: "better-auth/client/plugins"
744
+ },
745
+ {
746
+ id: "magic-link",
747
+ name: "magicLink",
748
+ clientName: "magicLinkClient",
749
+ clientPath: "better-auth/client/plugins",
750
+ path: `better-auth/plugins`
751
+ },
752
+ {
753
+ id: "email-otp",
754
+ name: "emailOTP",
755
+ clientName: "emailOTPClient",
756
+ path: `better-auth/plugins`,
757
+ clientPath: "better-auth/client/plugins"
758
+ },
759
+ {
760
+ id: "passkey",
761
+ name: "passkey",
762
+ clientName: "passkeyClient",
763
+ path: `better-auth/plugins/passkey`,
764
+ clientPath: "better-auth/client/plugins"
765
+ },
766
+ {
767
+ id: "generic-oauth",
768
+ name: "genericOAuth",
769
+ clientName: "genericOAuthClient",
770
+ path: `better-auth/plugins`,
771
+ clientPath: "better-auth/client/plugins"
772
+ },
773
+ {
774
+ id: "one-tap",
775
+ name: "oneTap",
776
+ clientName: "oneTapClient",
777
+ path: `better-auth/plugins`,
778
+ clientPath: "better-auth/client/plugins"
779
+ },
780
+ {
781
+ id: "api-key",
782
+ name: "apiKey",
783
+ clientName: "apiKeyClient",
784
+ path: `better-auth/plugins`,
785
+ clientPath: "better-auth/client/plugins"
786
+ },
787
+ {
788
+ id: "admin",
789
+ name: "admin",
790
+ clientName: "adminClient",
791
+ path: `better-auth/plugins`,
792
+ clientPath: "better-auth/client/plugins"
793
+ },
794
+ {
795
+ id: "organization",
796
+ name: "organization",
797
+ clientName: "organizationClient",
798
+ path: `better-auth/plugins`,
799
+ clientPath: "better-auth/client/plugins"
800
+ },
801
+ {
802
+ id: "oidc",
803
+ name: "oidcProvider",
804
+ clientName: "oidcClient",
805
+ path: `better-auth/plugins`,
806
+ clientPath: "better-auth/client/plugins"
807
+ },
808
+ {
809
+ id: "sso",
810
+ name: "sso",
811
+ clientName: "ssoClient",
812
+ path: `better-auth/plugins/sso`,
813
+ clientPath: "better-auth/client/plugins"
814
+ },
815
+ {
816
+ id: "bearer",
817
+ name: "bearer",
818
+ clientName: void 0,
819
+ path: `better-auth/plugins`,
820
+ clientPath: void 0
821
+ },
822
+ {
823
+ id: "multi-session",
824
+ name: "multiSession",
825
+ clientName: "multiSessionClient",
826
+ path: `better-auth/plugins`,
827
+ clientPath: "better-auth/client/plugins"
828
+ },
829
+ {
830
+ id: "oauth-proxy",
831
+ name: "oAuthProxy",
832
+ clientName: void 0,
833
+ path: `better-auth/plugins`,
834
+ clientPath: void 0
835
+ },
836
+ {
837
+ id: "open-api",
838
+ name: "openAPI",
839
+ clientName: void 0,
840
+ path: `better-auth/plugins`,
841
+ clientPath: void 0
842
+ },
843
+ {
844
+ id: "jwt",
845
+ name: "jwt",
846
+ clientName: void 0,
847
+ clientPath: void 0,
848
+ path: `better-auth/plugins`
849
+ },
850
+ {
851
+ id: "next-cookies",
852
+ name: "nextCookies",
853
+ clientPath: void 0,
854
+ clientName: void 0,
855
+ path: `better-auth/next-js`
784
856
  }
785
- const adapter = await getAdapter(config).catch((e) => {
786
- logger.error(e.message);
787
- process.exit(1);
788
- });
789
- const spinner = yoctoSpinner({ text: "preparing schema..." }).start();
790
- const schema = await getGenerator({
791
- adapter,
792
- file: options.output,
793
- options: config
794
- });
795
- spinner.stop();
796
- if (!schema.code) {
797
- logger.info("Your schema is already up to date.");
798
- process.exit(0);
857
+ ];
858
+ const defaultFormatOptions = {
859
+ trailingComma: "all",
860
+ useTabs: false,
861
+ tabWidth: 4
862
+ };
863
+ const getDefaultAuthConfig = async ({ appName }) => await format(
864
+ [
865
+ "import { betterAuth } from 'better-auth';",
866
+ "",
867
+ "export const auth = betterAuth({",
868
+ appName ? `appName: "${appName}",` : "",
869
+ "plugins: [],",
870
+ "});"
871
+ ].join("\n"),
872
+ {
873
+ filepath: "auth.ts",
874
+ ...defaultFormatOptions
799
875
  }
800
- if (schema.append || schema.overwrite) {
801
- let confirm2 = options.y || options.yes;
802
- if (!confirm2) {
803
- const response = await prompts({
804
- type: "confirm",
805
- name: "confirm",
806
- message: `The file ${schema.fileName} already exists. Do you want to ${chalk.yellow(
807
- `${schema.overwrite ? "overwrite" : "append"}`
808
- )} the schema to the file?`
809
- });
810
- confirm2 = response.confirm;
811
- }
812
- if (confirm2) {
813
- const exist = existsSync(path.join(cwd, schema.fileName));
814
- if (!exist) {
815
- await fs$2.mkdir(path.dirname(path.join(cwd, schema.fileName)), {
816
- recursive: true
817
- });
876
+ );
877
+ const getDefaultAuthClientConfig = async ({
878
+ auth_config_path,
879
+ framework,
880
+ clientPlugins
881
+ }) => {
882
+ function groupImportVariables() {
883
+ const result = [
884
+ {
885
+ path: "better-auth/client/plugins",
886
+ variables: [{ name: "inferAdditionalFields" }]
818
887
  }
819
- if (schema.overwrite) {
820
- await fs$2.writeFile(path.join(cwd, schema.fileName), schema.code);
821
- } else {
822
- await fs$2.appendFile(path.join(cwd, schema.fileName), schema.code);
888
+ ];
889
+ for (const plugin of clientPlugins) {
890
+ for (const import_ of plugin.imports) {
891
+ if (Array.isArray(import_.variables)) {
892
+ for (const variable of import_.variables) {
893
+ const existingIndex = result.findIndex(
894
+ (x) => x.path === import_.path
895
+ );
896
+ if (existingIndex !== -1) {
897
+ const vars = result[existingIndex].variables;
898
+ if (Array.isArray(vars)) {
899
+ vars.push(variable);
900
+ } else {
901
+ result[existingIndex].variables = [vars, variable];
902
+ }
903
+ } else {
904
+ result.push({
905
+ path: import_.path,
906
+ variables: [variable]
907
+ });
908
+ }
909
+ }
910
+ } else {
911
+ const existingIndex = result.findIndex(
912
+ (x) => x.path === import_.path
913
+ );
914
+ if (existingIndex !== -1) {
915
+ const vars = result[existingIndex].variables;
916
+ if (Array.isArray(vars)) {
917
+ vars.push(import_.variables);
918
+ } else {
919
+ result[existingIndex].variables = [vars, import_.variables];
920
+ }
921
+ } else {
922
+ result.push({
923
+ path: import_.path,
924
+ variables: [import_.variables]
925
+ });
926
+ }
927
+ }
823
928
  }
824
- logger.success(
825
- `\u{1F680} Schema was ${schema.overwrite ? "overwritten" : "appended"} successfully!`
826
- );
827
- process.exit(0);
828
- } else {
829
- logger.error("Schema generation aborted.");
830
- process.exit(1);
831
929
  }
930
+ return result;
832
931
  }
833
- if (options.y) {
834
- console.warn("WARNING: --y is deprecated. Consider -y or --yes");
835
- options.yes = true;
836
- }
837
- let confirm = options.yes;
838
- if (!confirm) {
839
- const response = await prompts({
840
- type: "confirm",
841
- name: "confirm",
842
- message: `Do you want to generate the schema to ${chalk.yellow(
843
- schema.fileName
844
- )}?`
845
- });
846
- confirm = response.confirm;
847
- }
848
- if (!confirm) {
849
- logger.error("Schema generation aborted.");
850
- process.exit(1);
851
- }
852
- if (!options.output) {
853
- const dirExist = existsSync(path.dirname(path.join(cwd, schema.fileName)));
854
- if (!dirExist) {
855
- await fs$2.mkdir(path.dirname(path.join(cwd, schema.fileName)), {
856
- recursive: true
857
- });
932
+ let imports = groupImportVariables();
933
+ let importString = "";
934
+ for (const import_ of imports) {
935
+ if (Array.isArray(import_.variables)) {
936
+ importString += `import { ${import_.variables.map(
937
+ (x) => `${x.asType ? "type " : ""}${x.name}${x.as ? ` as ${x.as}` : ""}`
938
+ ).join(", ")} } from "${import_.path}";
939
+ `;
940
+ } else {
941
+ importString += `import ${import_.variables.asType ? "type " : ""}${import_.variables.name}${import_.variables.as ? ` as ${import_.variables.as}` : ""} from "${import_.path}";
942
+ `;
858
943
  }
859
944
  }
860
- await fs$2.writeFile(
861
- options.output || path.join(cwd, schema.fileName),
862
- schema.code
945
+ return await format(
946
+ [
947
+ `import { createAuthClient } from "better-auth/${framework === "nextjs" ? "react" : framework === "vanilla" ? "client" : framework}";`,
948
+ `import type { auth } from "${auth_config_path}";`,
949
+ importString,
950
+ ``,
951
+ `export const authClient = createAuthClient({`,
952
+ `baseURL: "http://localhost:3000",`,
953
+ `plugins: [inferAdditionalFields<typeof auth>(),${clientPlugins.map((x) => `${x.name}(${x.contents})`).join(", ")}],`,
954
+ `});`
955
+ ].join("\n"),
956
+ {
957
+ filepath: "auth-client.ts",
958
+ ...defaultFormatOptions
959
+ }
863
960
  );
864
- logger.success(`\u{1F680} Schema was generated successfully!`);
865
- process.exit(0);
866
- }
867
- const generate = new Command("generate").option(
868
- "-c, --cwd <cwd>",
869
- "the working directory. defaults to the current directory.",
870
- process.cwd()
871
- ).option(
872
- "--config <config>",
873
- "the path to the configuration file. defaults to the first configuration file found."
874
- ).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);
875
-
876
- const generateSecret = new Command("secret").action(() => {
877
- const secret = generateSecretHash();
878
- logger.info(`
879
- Add the following to your .env file:
880
- ${chalk.gray("# Auth Secret") + chalk.green(`
881
- BETTER_AUTH_SECRET=${secret}`)}`);
882
- });
883
- const generateSecretHash = () => {
884
- return Crypto.randomBytes(32).toString("hex");
885
961
  };
886
-
887
- function getPackageInfo(cwd) {
888
- const packageJsonPath = cwd ? path.join(cwd, "package.json") : path.join("package.json");
889
- try {
890
- return fs.readJSONSync(packageJsonPath);
891
- } catch (error) {
892
- throw error;
893
- }
894
- }
895
-
896
- function installDependencies({
897
- dependencies,
898
- packageManager,
899
- cwd
900
- }) {
901
- let installCommand;
902
- switch (packageManager) {
903
- case "npm":
904
- installCommand = "npm install --force";
905
- break;
906
- case "pnpm":
907
- installCommand = "pnpm install";
908
- break;
909
- case "bun":
910
- installCommand = "bun install";
911
- break;
912
- case "yarn":
913
- installCommand = "yarn install";
914
- break;
915
- default:
916
- throw new Error("Invalid package manager");
917
- }
918
- const command = `${installCommand} ${dependencies.join(" ")}`;
919
- return new Promise((resolve, reject) => {
920
- exec(command, { cwd }, (error, stdout, stderr) => {
921
- if (error) {
922
- reject(new Error(stderr));
923
- return;
924
- }
925
- resolve(true);
926
- });
927
- });
928
- }
929
-
930
- function checkCommand(command) {
931
- return new Promise((resolve) => {
932
- exec(`${command} --version`, (error) => {
933
- if (error) {
934
- resolve(false);
935
- } else {
936
- resolve(true);
937
- }
938
- });
939
- });
940
- }
941
- async function checkPackageManagers() {
942
- const hasPnpm = await checkCommand("pnpm");
943
- const hasBun = await checkCommand("bun");
944
- return {
945
- hasPnpm,
946
- hasBun
947
- };
948
- }
949
-
950
- function formatMilliseconds(ms) {
951
- if (ms < 0) {
952
- throw new Error("Milliseconds cannot be negative");
962
+ const optionsSchema = z.object({
963
+ cwd: z.string(),
964
+ config: z.string().optional(),
965
+ database: z.enum(supportedDatabases).optional(),
966
+ "skip-db": z.boolean().optional(),
967
+ "skip-plugins": z.boolean().optional(),
968
+ "package-manager": z.string().optional(),
969
+ tsconfig: z.string().optional()
970
+ });
971
+ const outroText = `\u{1F973} All Done, Happy Hacking!`;
972
+ async function initAction(opts) {
973
+ console.log();
974
+ intro("\u{1F44B} Initializing Better Auth");
975
+ const options = optionsSchema.parse(opts);
976
+ const cwd = path.resolve(options.cwd);
977
+ let packageManagerPreference = void 0;
978
+ let config_path = "";
979
+ let framework = "vanilla";
980
+ const format$1 = async (code) => await format(code, {
981
+ filepath: config_path,
982
+ ...defaultFormatOptions
983
+ });
984
+ let packageInfo;
985
+ try {
986
+ packageInfo = getPackageInfo(cwd);
987
+ } catch (error) {
988
+ log.error(`\u274C Couldn't read your package.json file. (dir: ${cwd})`);
989
+ log.error(JSON.stringify(error, null, 2));
990
+ process.exit(1);
953
991
  }
954
- if (ms < 1e3) {
955
- return `${ms}ms`;
992
+ const envFiles = await getEnvFiles(cwd);
993
+ if (!envFiles.length) {
994
+ outro("\u274C No .env files found. Please create an env file first.");
995
+ process.exit(0);
956
996
  }
957
- const seconds = Math.floor(ms / 1e3);
958
- const milliseconds = ms % 1e3;
959
- return `${seconds}s ${milliseconds}ms`;
960
- }
961
-
962
- async function generateAuthConfig({
963
- format,
964
- current_user_config,
965
- spinner,
966
- plugins,
967
- database
968
- }) {
969
- let _start_of_plugins_common_index = {
970
- START_OF_PLUGINS: {
971
- type: "regex",
972
- regex: /betterAuth\([\w\W]*plugins:[\W]*\[()/m,
973
- getIndex: ({ matchIndex, match }) => {
974
- return matchIndex + match[0].length;
975
- }
997
+ let targetEnvFile;
998
+ if (envFiles.includes(".env")) targetEnvFile = ".env";
999
+ else if (envFiles.includes(".env.local")) targetEnvFile = ".env.local";
1000
+ else if (envFiles.includes(".env.development"))
1001
+ targetEnvFile = ".env.development";
1002
+ else if (envFiles.length === 1) targetEnvFile = envFiles[0];
1003
+ else targetEnvFile = "none";
1004
+ let tsconfigInfo;
1005
+ try {
1006
+ const tsconfigPath = options.tsconfig !== void 0 ? path.resolve(cwd, options.tsconfig) : path.join(cwd, "tsconfig.json");
1007
+ tsconfigInfo = await getTsconfigInfo(cwd, tsconfigPath);
1008
+ } catch (error) {
1009
+ log.error(`\u274C Couldn't read your tsconfig.json file. (dir: ${cwd})`);
1010
+ console.error(error);
1011
+ process.exit(1);
1012
+ }
1013
+ if (!("compilerOptions" in tsconfigInfo && "strict" in tsconfigInfo.compilerOptions && tsconfigInfo.compilerOptions.strict === true)) {
1014
+ log.warn(
1015
+ `Better Auth requires your tsconfig.json to have "compilerOptions.strict" set to true.`
1016
+ );
1017
+ const shouldAdd = await confirm({
1018
+ message: `Would you like us to set ${chalk.bold(
1019
+ `strict`
1020
+ )} to ${chalk.bold(`true`)}?`
1021
+ });
1022
+ if (isCancel(shouldAdd)) {
1023
+ cancel(`\u270B Operation cancelled.`);
1024
+ process.exit(0);
976
1025
  }
977
- };
978
- const common_indexes = {
979
- START_OF_PLUGINS: _start_of_plugins_common_index.START_OF_PLUGINS,
980
- END_OF_PLUGINS: {
981
- type: "manual",
982
- getIndex: ({ content, additionalFields }) => {
983
- const closingBracketIndex = findClosingBracket(
984
- content,
985
- additionalFields.start_of_plugins,
986
- "[",
987
- "]"
1026
+ if (shouldAdd) {
1027
+ try {
1028
+ await fs$1.writeFile(
1029
+ path.join(cwd, "tsconfig.json"),
1030
+ await format(
1031
+ JSON.stringify(
1032
+ Object.assign(tsconfigInfo, {
1033
+ compilerOptions: {
1034
+ strict: true
1035
+ }
1036
+ })
1037
+ ),
1038
+ { filepath: "tsconfig.json", ...defaultFormatOptions }
1039
+ ),
1040
+ "utf-8"
988
1041
  );
989
- return closingBracketIndex;
990
- }
991
- },
992
- START_OF_BETTERAUTH: {
993
- type: "regex",
994
- regex: /betterAuth\({()/m,
995
- getIndex: ({ matchIndex }) => {
996
- return matchIndex + "betterAuth({".length;
1042
+ log.success(`\u{1F680} tsconfig.json successfully updated!`);
1043
+ } catch (error) {
1044
+ log.error(
1045
+ `Failed to add "compilerOptions.strict" to your tsconfig.json file.`
1046
+ );
1047
+ console.error(error);
1048
+ process.exit(1);
997
1049
  }
998
1050
  }
999
- };
1000
- const config_generation = {
1001
- add_plugin: async (opts) => {
1002
- let start_of_plugins = getGroupInfo(
1003
- opts.config,
1004
- common_indexes.START_OF_PLUGINS,
1005
- {}
1051
+ }
1052
+ const s = spinner({ indicator: "dots" });
1053
+ s.start(`Checking better-auth installation`);
1054
+ let latest_betterauth_version;
1055
+ try {
1056
+ latest_betterauth_version = await getLatestNpmVersion("better-auth");
1057
+ } catch (error) {
1058
+ log.error(`\u274C Couldn't get latest version of better-auth.`);
1059
+ console.error(error);
1060
+ process.exit(1);
1061
+ }
1062
+ if (!packageInfo.dependencies || !Object.keys(packageInfo.dependencies).includes("better-auth")) {
1063
+ s.stop("Finished fetching latest version of better-auth.");
1064
+ const s2 = spinner({ indicator: "dots" });
1065
+ const shouldInstallBetterAuthDep = await confirm({
1066
+ message: `Would you like to install Better Auth?`
1067
+ });
1068
+ if (isCancel(shouldInstallBetterAuthDep)) {
1069
+ cancel(`\u270B Operation cancelled.`);
1070
+ process.exit(0);
1071
+ }
1072
+ if (packageManagerPreference === void 0) {
1073
+ packageManagerPreference = await getPackageManager();
1074
+ }
1075
+ if (shouldInstallBetterAuthDep) {
1076
+ s2.start(
1077
+ `Installing Better Auth using ${chalk.bold(packageManagerPreference)}`
1006
1078
  );
1007
- if (!start_of_plugins) {
1008
- throw new Error(
1009
- "Couldn't find start of your plugins array in your auth config file."
1079
+ try {
1080
+ const start = Date.now();
1081
+ await installDependencies({
1082
+ dependencies: ["better-auth@latest"],
1083
+ packageManager: packageManagerPreference,
1084
+ cwd
1085
+ });
1086
+ s2.stop(
1087
+ `Better Auth installed ${chalk.greenBright(
1088
+ `successfully`
1089
+ )}! ${chalk.gray(`(${formatMilliseconds(Date.now() - start)})`)}`
1010
1090
  );
1091
+ } catch (error) {
1092
+ s2.stop(`Failed to install Better Auth:`);
1093
+ console.error(error);
1094
+ process.exit(1);
1011
1095
  }
1012
- let end_of_plugins = getGroupInfo(
1013
- opts.config,
1014
- common_indexes.END_OF_PLUGINS,
1015
- { start_of_plugins: start_of_plugins.index }
1096
+ }
1097
+ } else if (packageInfo.dependencies["better-auth"] !== "workspace:*" && semver.lt(
1098
+ semver.coerce(packageInfo.dependencies["better-auth"])?.toString(),
1099
+ semver.clean(latest_betterauth_version)
1100
+ )) {
1101
+ s.stop("Finished fetching latest version of better-auth.");
1102
+ const shouldInstallBetterAuthDep = await confirm({
1103
+ message: `Your current Better Auth dependency is out-of-date. Would you like to update it? (${chalk.bold(
1104
+ packageInfo.dependencies["better-auth"]
1105
+ )} \u2192 ${chalk.bold(`v${latest_betterauth_version}`)})`
1106
+ });
1107
+ if (isCancel(shouldInstallBetterAuthDep)) {
1108
+ cancel(`\u270B Operation cancelled.`);
1109
+ process.exit(0);
1110
+ }
1111
+ if (shouldInstallBetterAuthDep) {
1112
+ if (packageManagerPreference === void 0) {
1113
+ packageManagerPreference = await getPackageManager();
1114
+ }
1115
+ const s2 = spinner({ indicator: "dots" });
1116
+ s2.start(
1117
+ `Updating Better Auth using ${chalk.bold(packageManagerPreference)}`
1016
1118
  );
1017
- if (!end_of_plugins) {
1018
- throw new Error(
1019
- "Couldn't find end of your plugins array in your auth config file."
1119
+ try {
1120
+ const start = Date.now();
1121
+ await installDependencies({
1122
+ dependencies: ["better-auth@latest"],
1123
+ packageManager: packageManagerPreference,
1124
+ cwd
1125
+ });
1126
+ s2.stop(
1127
+ `Better Auth updated ${chalk.greenBright(
1128
+ `successfully`
1129
+ )}! ${chalk.gray(`(${formatMilliseconds(Date.now() - start)})`)}`
1020
1130
  );
1131
+ } catch (error) {
1132
+ s2.stop(`Failed to update Better Auth:`);
1133
+ log.error(error.message);
1134
+ process.exit(1);
1135
+ }
1136
+ }
1137
+ } else {
1138
+ s.stop(`Better Auth dependencies are ${chalk.greenBright(`up to date`)}!`);
1139
+ }
1140
+ const packageJson = getPackageInfo(cwd);
1141
+ let appName;
1142
+ if (!packageJson.name) {
1143
+ const newAppName = await text({
1144
+ message: "What is the name of your application?"
1145
+ });
1146
+ if (isCancel(newAppName)) {
1147
+ cancel("\u270B Operation cancelled.");
1148
+ process.exit(0);
1149
+ }
1150
+ appName = newAppName;
1151
+ } else {
1152
+ appName = packageJson.name;
1153
+ }
1154
+ let possiblePaths = ["auth.ts", "auth.tsx", "auth.js", "auth.jsx"];
1155
+ possiblePaths = [
1156
+ ...possiblePaths,
1157
+ ...possiblePaths.map((it) => `lib/server/${it}`),
1158
+ ...possiblePaths.map((it) => `server/${it}`),
1159
+ ...possiblePaths.map((it) => `lib/${it}`),
1160
+ ...possiblePaths.map((it) => `utils/${it}`)
1161
+ ];
1162
+ possiblePaths = [
1163
+ ...possiblePaths,
1164
+ ...possiblePaths.map((it) => `src/${it}`),
1165
+ ...possiblePaths.map((it) => `app/${it}`)
1166
+ ];
1167
+ if (options.config) {
1168
+ config_path = path.join(cwd, options.config);
1169
+ } else {
1170
+ for (const possiblePath of possiblePaths) {
1171
+ const doesExist = existsSync(path.join(cwd, possiblePath));
1172
+ if (doesExist) {
1173
+ config_path = path.join(cwd, possiblePath);
1174
+ break;
1175
+ }
1176
+ }
1177
+ }
1178
+ let current_user_config = "";
1179
+ let database = null;
1180
+ let add_plugins = [];
1181
+ if (!config_path) {
1182
+ const shouldCreateAuthConfig = await select({
1183
+ message: `Would you like to create an auth config file?`,
1184
+ options: [
1185
+ { label: "Yes", value: "yes" },
1186
+ { label: "No", value: "no" }
1187
+ ]
1188
+ });
1189
+ if (isCancel(shouldCreateAuthConfig)) {
1190
+ cancel(`\u270B Operation cancelled.`);
1191
+ process.exit(0);
1192
+ }
1193
+ if (shouldCreateAuthConfig === "yes") {
1194
+ const shouldSetupDb = await confirm({
1195
+ message: `Would you like to set up your ${chalk.bold(`database`)}?`,
1196
+ initialValue: true
1197
+ });
1198
+ if (isCancel(shouldSetupDb)) {
1199
+ cancel(`\u270B Operating cancelled.`);
1200
+ process.exit(0);
1021
1201
  }
1022
- let new_content;
1023
- if (opts.direction_in_plugins_array === "prepend") {
1024
- new_content = insertContent({
1025
- line: start_of_plugins.line,
1026
- character: start_of_plugins.character,
1027
- content: opts.config,
1028
- insert_content: `${opts.pluginFunctionName}(${opts.pluginContents}),`
1202
+ if (shouldSetupDb) {
1203
+ const prompted_database = await select({
1204
+ message: "Choose a Database Dialect",
1205
+ options: supportedDatabases.map((it) => ({ value: it, label: it }))
1029
1206
  });
1030
- } else {
1031
- let has_found_comma = false;
1032
- const str = opts.config.slice(start_of_plugins.index, end_of_plugins.index).split("").reverse();
1033
- for (let index = 0; index < str.length; index++) {
1034
- const char = str[index];
1035
- if (char === ",") {
1036
- has_found_comma = true;
1037
- }
1038
- if (char === ")") {
1039
- break;
1040
- }
1207
+ if (isCancel(prompted_database)) {
1208
+ cancel(`\u270B Operating cancelled.`);
1209
+ process.exit(0);
1041
1210
  }
1042
- new_content = insertContent({
1043
- line: end_of_plugins.line,
1044
- character: end_of_plugins.character,
1045
- content: opts.config,
1046
- insert_content: `${!has_found_comma ? "," : ""}${opts.pluginFunctionName}(${opts.pluginContents})`
1047
- });
1048
- }
1049
- try {
1050
- new_content = await format(new_content);
1051
- } catch (error) {
1052
- console.error(error);
1053
- throw new Error(
1054
- `Failed to generate new auth config during plugin addition phase.`
1055
- );
1211
+ database = prompted_database;
1056
1212
  }
1057
- return { code: await new_content, dependencies: [], envs: [] };
1058
- },
1059
- add_import: async (opts) => {
1060
- let importString = "";
1061
- for (const import_ of opts.imports) {
1062
- if (Array.isArray(import_.variables)) {
1063
- importString += `import { ${import_.variables.map(
1064
- (x) => `${x.asType ? "type " : ""}${x.name}${x.as ? ` as ${x.as}` : ""}`
1065
- ).join(", ")} } from "${import_.path}";
1066
- `;
1067
- } else {
1068
- importString += `import ${import_.variables.asType ? "type " : ""}${import_.variables.name}${import_.variables.as ? ` as ${import_.variables.as}` : ""} from "${import_.path}";
1069
- `;
1213
+ if (options["skip-plugins"] !== false) {
1214
+ const shouldSetupPlugins = await confirm({
1215
+ message: `Would you like to set up ${chalk.bold(`plugins`)}?`
1216
+ });
1217
+ if (isCancel(shouldSetupPlugins)) {
1218
+ cancel(`\u270B Operating cancelled.`);
1219
+ process.exit(0);
1070
1220
  }
1071
- }
1072
- try {
1073
- let new_content = format(importString + opts.config);
1074
- return { code: await new_content, dependencies: [], envs: [] };
1075
- } catch (error) {
1076
- console.error(error);
1077
- throw new Error(
1078
- `Failed to generate new auth config during import addition phase.`
1079
- );
1080
- }
1081
- },
1082
- add_database: async (opts) => {
1083
- const required_envs = [];
1084
- const required_deps = [];
1085
- let database_code_str = "";
1086
- async function add_db({
1087
- db_code,
1088
- dependencies,
1089
- envs,
1090
- imports,
1091
- code_before_betterAuth
1092
- }) {
1093
- if (code_before_betterAuth) {
1094
- let start_of_betterauth2 = getGroupInfo(
1095
- opts.config,
1096
- common_indexes.START_OF_BETTERAUTH,
1097
- {}
1098
- );
1099
- if (!start_of_betterauth2) {
1100
- throw new Error("Couldn't find start of betterAuth() function.");
1101
- }
1102
- opts.config = insertContent({
1103
- line: start_of_betterauth2.line - 1,
1104
- character: 0,
1105
- content: opts.config,
1106
- insert_content: `
1107
- ${code_before_betterAuth}
1108
- `
1221
+ if (shouldSetupPlugins) {
1222
+ const prompted_plugins = await multiselect({
1223
+ message: "Select your new plugins",
1224
+ options: supportedPlugins.filter((x) => x.id !== "next-cookies").map((x) => ({ value: x.id, label: x.id })),
1225
+ required: false
1109
1226
  });
1110
- }
1111
- const code_gen = await config_generation.add_import({
1112
- config: opts.config,
1113
- imports
1114
- });
1115
- opts.config = code_gen.code;
1116
- database_code_str = db_code;
1117
- required_envs.push(...envs, ...code_gen.envs);
1118
- required_deps.push(...dependencies, ...code_gen.dependencies);
1119
- }
1120
- if (opts.database === "sqlite") {
1121
- await add_db({
1122
- db_code: `new Database(process.env.DATABASE_URL || "database.sqlite")`,
1123
- dependencies: ["better-sqlite3"],
1124
- envs: ["DATABASE_URL"],
1125
- imports: [
1126
- {
1127
- path: "better-sqlite3",
1128
- variables: {
1129
- asType: false,
1130
- name: "Database"
1131
- }
1227
+ if (isCancel(prompted_plugins)) {
1228
+ cancel(`\u270B Operating cancelled.`);
1229
+ process.exit(0);
1230
+ }
1231
+ add_plugins = prompted_plugins.map(
1232
+ (x) => supportedPlugins.find((y) => y.id === x)
1233
+ );
1234
+ const possible_next_config_paths = [
1235
+ "next.config.js",
1236
+ "next.config.ts",
1237
+ "next.config.mjs",
1238
+ ".next/server/next.config.js",
1239
+ ".next/server/next.config.ts",
1240
+ ".next/server/next.config.mjs"
1241
+ ];
1242
+ for (const possible_next_config_path of possible_next_config_paths) {
1243
+ if (existsSync(path.join(cwd, possible_next_config_path))) {
1244
+ framework = "nextjs";
1245
+ break;
1132
1246
  }
1133
- ]
1134
- });
1135
- } else if (opts.database === "postgres") {
1136
- await add_db({
1137
- db_code: `new Pool({
1138
- connectionString: process.env.DATABASE_URL || "postgresql://postgres:password@localhost:5432/database"
1139
- })`,
1140
- dependencies: ["pg"],
1141
- envs: ["DATABASE_URL"],
1142
- imports: [
1143
- {
1144
- path: "pg",
1145
- variables: [
1146
- {
1147
- asType: false,
1148
- name: "Pool"
1149
- }
1150
- ]
1247
+ }
1248
+ if (framework === "nextjs") {
1249
+ const result = await confirm({
1250
+ message: `It looks like you're using NextJS. Do you want to add the next-cookies plugin? ${chalk.bold(
1251
+ `(Recommended)`
1252
+ )}`
1253
+ });
1254
+ if (isCancel(result)) {
1255
+ cancel(`\u270B Operating cancelled.`);
1256
+ process.exit(0);
1151
1257
  }
1152
- ]
1153
- });
1154
- } else if (opts.database === "mysql") {
1155
- await add_db({
1156
- db_code: `createPool(process.env.DATABASE_URL!)`,
1157
- dependencies: ["mysql2"],
1158
- envs: ["DATABASE_URL"],
1159
- imports: [
1160
- {
1161
- path: "mysql2/promise",
1162
- variables: [
1163
- {
1164
- asType: false,
1165
- name: "createPool"
1166
- }
1167
- ]
1258
+ if (result) {
1259
+ add_plugins.push(
1260
+ supportedPlugins.find((x) => x.id === "next-cookies")
1261
+ );
1168
1262
  }
1169
- ]
1170
- });
1171
- } else if (opts.database === "mssql") {
1172
- const dialectCode = `new MssqlDialect({
1173
- tarn: {
1174
- ...Tarn,
1175
- options: {
1176
- min: 0,
1177
- max: 10,
1178
- },
1179
- },
1180
- tedious: {
1181
- ...Tedious,
1182
- connectionFactory: () => new Tedious.Connection({
1183
- authentication: {
1184
- options: {
1185
- password: 'password',
1186
- userName: 'username',
1187
- },
1188
- type: 'default',
1189
- },
1190
- options: {
1191
- database: 'some_db',
1192
- port: 1433,
1193
- trustServerCertificate: true,
1194
- },
1195
- server: 'localhost',
1196
- }),
1197
- },
1198
- })`;
1199
- await add_db({
1200
- code_before_betterAuth: dialectCode,
1201
- db_code: `dialect`,
1202
- dependencies: ["tedious", "tarn", "kysely"],
1203
- envs: ["DATABASE_URL"],
1204
- imports: [
1205
- {
1206
- path: "tedious",
1207
- variables: {
1208
- name: "*",
1209
- as: "Tedious"
1210
- }
1211
- },
1212
- {
1213
- path: "tarn",
1214
- variables: {
1215
- name: "*",
1216
- as: "Tarn"
1263
+ }
1264
+ }
1265
+ }
1266
+ const filePath = path.join(cwd, "auth.ts");
1267
+ config_path = filePath;
1268
+ log.info(`Creating auth config file: ${filePath}`);
1269
+ try {
1270
+ current_user_config = await getDefaultAuthConfig({
1271
+ appName
1272
+ });
1273
+ const { dependencies, envs, generatedCode } = await generateAuthConfig({
1274
+ current_user_config,
1275
+ format: format$1,
1276
+ //@ts-ignore
1277
+ s,
1278
+ plugins: add_plugins,
1279
+ database
1280
+ });
1281
+ current_user_config = generatedCode;
1282
+ await fs$1.writeFile(filePath, current_user_config);
1283
+ config_path = filePath;
1284
+ log.success(`\u{1F680} Auth config file successfully created!`);
1285
+ if (envs.length !== 0) {
1286
+ log.info(
1287
+ `There are ${envs.length} environment variables for your database of choice.`
1288
+ );
1289
+ const shouldUpdateEnvs = await confirm({
1290
+ message: `Would you like us to update your ENV files?`
1291
+ });
1292
+ if (isCancel(shouldUpdateEnvs)) {
1293
+ cancel("\u270B Operation cancelled.");
1294
+ process.exit(0);
1295
+ }
1296
+ if (shouldUpdateEnvs) {
1297
+ const filesToUpdate = await multiselect({
1298
+ message: "Select the .env files you want to update",
1299
+ options: envFiles.map((x) => ({
1300
+ value: path.join(cwd, x),
1301
+ label: x
1302
+ })),
1303
+ required: false
1304
+ });
1305
+ if (isCancel(filesToUpdate)) {
1306
+ cancel("\u270B Operation cancelled.");
1307
+ process.exit(0);
1308
+ }
1309
+ if (filesToUpdate.length === 0) {
1310
+ log.info("No .env files to update. Skipping...");
1311
+ } else {
1312
+ try {
1313
+ await updateEnvs({
1314
+ files: filesToUpdate,
1315
+ envs,
1316
+ isCommented: true
1317
+ });
1318
+ } catch (error) {
1319
+ log.error(`Failed to update .env files:`);
1320
+ log.error(JSON.stringify(error, null, 2));
1321
+ process.exit(1);
1217
1322
  }
1218
- },
1219
- {
1220
- path: "kysely",
1221
- variables: [
1222
- {
1223
- name: "MssqlDialect"
1224
- }
1225
- ]
1323
+ log.success(`\u{1F680} ENV files successfully updated!`);
1226
1324
  }
1227
- ]
1228
- });
1229
- } else if (opts.database === "drizzle:mysql" || opts.database === "drizzle:sqlite" || opts.database === "drizzle:pg") {
1230
- await add_db({
1231
- db_code: `drizzleAdapter(db, {
1232
- provider: "${opts.database.replace(
1233
- "drizzle:",
1234
- ""
1235
- )}",
1236
- })`,
1237
- dependencies: [""],
1238
- envs: [],
1239
- imports: [
1240
- {
1241
- path: "better-auth/adapters/drizzle",
1242
- variables: [
1243
- {
1244
- name: "drizzleAdapter"
1245
- }
1246
- ]
1247
- },
1248
- {
1249
- path: "./database.ts",
1250
- variables: [
1251
- {
1252
- name: "db"
1253
- }
1254
- ]
1325
+ }
1326
+ }
1327
+ if (dependencies.length !== 0) {
1328
+ log.info(
1329
+ `There are ${dependencies.length} dependencies to install. (${dependencies.map((x) => chalk.green(x)).join(", ")})`
1330
+ );
1331
+ const shouldInstallDeps = await confirm({
1332
+ message: `Would you like us to install dependencies?`
1333
+ });
1334
+ if (isCancel(shouldInstallDeps)) {
1335
+ cancel("\u270B Operation cancelled.");
1336
+ process.exit(0);
1337
+ }
1338
+ if (shouldInstallDeps) {
1339
+ const s2 = spinner({ indicator: "dots" });
1340
+ if (packageManagerPreference === void 0) {
1341
+ packageManagerPreference = await getPackageManager();
1255
1342
  }
1256
- ]
1257
- });
1258
- } else if (opts.database === "prisma:mysql" || opts.database === "prisma:sqlite" || opts.database === "prisma:postgresql") {
1259
- await add_db({
1260
- db_code: `prismaAdapter(client, {
1261
- provider: "${opts.database.replace(
1262
- "prisma:",
1263
- ""
1264
- )}",
1265
- })`,
1266
- dependencies: [`@prisma/client`],
1267
- envs: [],
1268
- code_before_betterAuth: "const client = new PrismaClient();",
1269
- imports: [
1270
- {
1271
- path: "better-auth/adapters/prisma",
1272
- variables: [
1273
- {
1274
- name: "prismaAdapter"
1275
- }
1276
- ]
1277
- },
1278
- {
1279
- path: "@prisma/client",
1280
- variables: [
1281
- {
1282
- name: "PrismaClient"
1283
- }
1284
- ]
1343
+ s2.start(
1344
+ `Installing dependencies using ${chalk.bold(
1345
+ packageManagerPreference
1346
+ )}...`
1347
+ );
1348
+ try {
1349
+ const start = Date.now();
1350
+ await installDependencies({
1351
+ dependencies,
1352
+ packageManager: packageManagerPreference,
1353
+ cwd
1354
+ });
1355
+ s2.stop(
1356
+ `Dependencies installed ${chalk.greenBright(
1357
+ `successfully`
1358
+ )} ${chalk.gray(
1359
+ `(${formatMilliseconds(Date.now() - start)})`
1360
+ )}`
1361
+ );
1362
+ } catch (error) {
1363
+ s2.stop(
1364
+ `Failed to install dependencies using ${packageManagerPreference}:`
1365
+ );
1366
+ log.error(error.message);
1367
+ process.exit(1);
1285
1368
  }
1286
- ]
1287
- });
1288
- } else if (opts.database === "mongodb") {
1289
- await add_db({
1290
- db_code: `mongodbAdapter(db)`,
1291
- dependencies: ["mongodb"],
1292
- envs: [`DATABASE_URL`],
1293
- code_before_betterAuth: [
1294
- `const client = new MongoClient(process.env.DATABASE_URL || "mongodb://localhost:27017/database");`,
1295
- `const db = client.db();`
1296
- ].join("\n"),
1297
- imports: [
1298
- {
1299
- path: "better-auth/adapters/mongo",
1300
- variables: [
1301
- {
1302
- name: "mongodbAdapter"
1303
- }
1304
- ]
1305
- },
1306
- {
1307
- path: "mongodb",
1308
- variables: [
1369
+ }
1370
+ }
1371
+ } catch (error) {
1372
+ log.error(`Failed to create auth config file: ${filePath}`);
1373
+ console.error(error);
1374
+ process.exit(1);
1375
+ }
1376
+ } else if (shouldCreateAuthConfig === "no") {
1377
+ log.info(`Skipping auth config file creation.`);
1378
+ }
1379
+ } else {
1380
+ log.message();
1381
+ log.success(`Found auth config file. ${chalk.gray(`(${config_path})`)}`);
1382
+ log.message();
1383
+ }
1384
+ let possibleClientPaths = [
1385
+ "auth-client.ts",
1386
+ "auth-client.tsx",
1387
+ "auth-client.js",
1388
+ "auth-client.jsx",
1389
+ "client.ts",
1390
+ "client.tsx",
1391
+ "client.js",
1392
+ "client.jsx"
1393
+ ];
1394
+ possibleClientPaths = [
1395
+ ...possibleClientPaths,
1396
+ ...possibleClientPaths.map((it) => `lib/server/${it}`),
1397
+ ...possibleClientPaths.map((it) => `server/${it}`),
1398
+ ...possibleClientPaths.map((it) => `lib/${it}`),
1399
+ ...possibleClientPaths.map((it) => `utils/${it}`)
1400
+ ];
1401
+ possibleClientPaths = [
1402
+ ...possibleClientPaths,
1403
+ ...possibleClientPaths.map((it) => `src/${it}`),
1404
+ ...possibleClientPaths.map((it) => `app/${it}`)
1405
+ ];
1406
+ let authClientConfigPath = null;
1407
+ for (const possiblePath of possibleClientPaths) {
1408
+ const doesExist = existsSync(path.join(cwd, possiblePath));
1409
+ if (doesExist) {
1410
+ authClientConfigPath = path.join(cwd, possiblePath);
1411
+ break;
1412
+ }
1413
+ }
1414
+ if (!authClientConfigPath) {
1415
+ const choice = await select({
1416
+ message: `Would you like to create an auth client config file?`,
1417
+ options: [
1418
+ { label: "Yes", value: "yes" },
1419
+ { label: "No", value: "no" }
1420
+ ]
1421
+ });
1422
+ if (isCancel(choice)) {
1423
+ cancel(`\u270B Operation cancelled.`);
1424
+ process.exit(0);
1425
+ }
1426
+ if (choice === "yes") {
1427
+ authClientConfigPath = path.join(cwd, "auth-client.ts");
1428
+ log.info(`Creating auth client config file: ${authClientConfigPath}`);
1429
+ try {
1430
+ let contents = await getDefaultAuthClientConfig({
1431
+ auth_config_path: ("./" + path.join(config_path.replace(cwd, ""))).replace(".//", "./"),
1432
+ clientPlugins: add_plugins.filter((x) => x.clientName).map((plugin) => {
1433
+ let contents2 = "";
1434
+ if (plugin.id === "one-tap") {
1435
+ contents2 = `{ clientId: "MY_CLIENT_ID" }`;
1436
+ }
1437
+ return {
1438
+ contents: contents2,
1439
+ id: plugin.id,
1440
+ name: plugin.clientName,
1441
+ imports: [
1309
1442
  {
1310
- name: "MongoClient"
1443
+ path: "better-auth/client/plugins",
1444
+ variables: [{ name: plugin.clientName }]
1311
1445
  }
1312
1446
  ]
1313
- }
1314
- ]
1447
+ };
1448
+ }),
1449
+ framework
1315
1450
  });
1316
- }
1317
- let start_of_betterauth = getGroupInfo(
1318
- opts.config,
1319
- common_indexes.START_OF_BETTERAUTH,
1320
- {}
1321
- );
1322
- if (!start_of_betterauth) {
1323
- throw new Error("Couldn't find start of betterAuth() function.");
1324
- }
1325
- let new_content;
1326
- new_content = insertContent({
1327
- line: start_of_betterauth.line,
1328
- character: start_of_betterauth.character,
1329
- content: opts.config,
1330
- insert_content: `database: ${database_code_str},`
1331
- });
1332
- try {
1333
- new_content = await format(new_content);
1334
- return {
1335
- code: new_content,
1336
- dependencies: required_deps,
1337
- envs: required_envs
1338
- };
1451
+ await fs$1.writeFile(authClientConfigPath, contents);
1452
+ log.success(`\u{1F680} Auth client config file successfully created!`);
1339
1453
  } catch (error) {
1340
- console.error(error);
1341
- throw new Error(
1342
- `Failed to generate new auth config during database addition phase.`
1454
+ log.error(
1455
+ `Failed to create auth client config file: ${authClientConfigPath}`
1343
1456
  );
1457
+ log.error(JSON.stringify(error, null, 2));
1458
+ process.exit(1);
1344
1459
  }
1460
+ } else if (choice === "no") {
1461
+ log.info(`Skipping auth client config file creation.`);
1345
1462
  }
1346
- };
1347
- let new_user_config = await format(current_user_config);
1348
- let total_dependencies = [];
1349
- let total_envs = [];
1350
- if (plugins.length !== 0) {
1351
- const imports = [];
1352
- for await (const plugin of plugins) {
1353
- const existingIndex = imports.findIndex((x) => x.path === plugin.path);
1354
- if (existingIndex !== -1) {
1355
- imports[existingIndex].variables.push({
1356
- name: plugin.name,
1357
- asType: false
1358
- });
1359
- } else {
1360
- imports.push({
1361
- path: plugin.path,
1362
- variables: [
1363
- {
1364
- name: plugin.name,
1365
- asType: false
1366
- }
1367
- ]
1368
- });
1369
- }
1370
- }
1371
- if (imports.length !== 0) {
1372
- const { code, envs, dependencies } = await config_generation.add_import({
1373
- config: new_user_config,
1374
- imports
1375
- });
1376
- total_dependencies.push(...dependencies);
1377
- total_envs.push(...envs);
1378
- new_user_config = code;
1379
- }
1463
+ } else {
1464
+ log.success(
1465
+ `Found auth client config file. ${chalk.gray(
1466
+ `(${authClientConfigPath})`
1467
+ )}`
1468
+ );
1380
1469
  }
1381
- for await (const plugin of plugins) {
1470
+ if (targetEnvFile !== "none") {
1382
1471
  try {
1383
- let pluginContents = "";
1384
- if (plugin.id === "magic-link") {
1385
- pluginContents = `{
1386
- sendMagicLink({ email, token, url }, request) {
1387
- // Send email with magic link
1388
- },
1389
- }`;
1390
- } else if (plugin.id === "email-otp") {
1391
- pluginContents = `{
1392
- async sendVerificationOTP({ email, otp, type }, request) {
1393
- // Send email with OTP
1394
- },
1395
- }`;
1396
- } else if (plugin.id === "generic-oauth") {
1397
- pluginContents = `{
1398
- config: [],
1399
- }`;
1400
- } else if (plugin.id === "oidc") {
1401
- pluginContents = `{
1402
- loginPage: "/sign-in",
1403
- }`;
1404
- }
1405
- const { code, dependencies, envs } = await config_generation.add_plugin({
1406
- config: new_user_config,
1407
- direction_in_plugins_array: plugin.id === "next-cookies" ? "append" : "prepend",
1408
- pluginFunctionName: plugin.name,
1409
- pluginContents
1410
- });
1411
- new_user_config = code;
1412
- total_envs.push(...envs);
1413
- total_dependencies.push(...dependencies);
1414
- } catch (error) {
1415
- spinner.stop(
1416
- `Something went wrong while generating/updating your new auth config file.`,
1417
- 1
1472
+ const fileContents = await fs$1.readFile(
1473
+ path.join(cwd, targetEnvFile),
1474
+ "utf8"
1418
1475
  );
1419
- logger.error(error.message);
1420
- process.exit(1);
1421
- }
1422
- }
1423
- if (database) {
1424
- try {
1425
- const { code, dependencies, envs } = await config_generation.add_database(
1426
- {
1427
- config: new_user_config,
1428
- database
1476
+ const parsed = parse(fileContents);
1477
+ let isMissingSecret = false;
1478
+ let isMissingUrl = false;
1479
+ if (parsed.BETTER_AUTH_SECRET === void 0) isMissingSecret = true;
1480
+ if (parsed.BETTER_AUTH_URL === void 0) isMissingUrl = true;
1481
+ if (isMissingSecret || isMissingUrl) {
1482
+ let txt = "";
1483
+ if (isMissingSecret && !isMissingUrl)
1484
+ txt = chalk.bold(`BETTER_AUTH_SECRET`);
1485
+ else if (!isMissingSecret && isMissingUrl)
1486
+ txt = chalk.bold(`BETTER_AUTH_URL`);
1487
+ else
1488
+ txt = chalk.bold.underline(`BETTER_AUTH_SECRET`) + ` and ` + chalk.bold.underline(`BETTER_AUTH_URL`);
1489
+ log.warn(`Missing ${txt} in ${targetEnvFile}`);
1490
+ const shouldAdd = await select({
1491
+ message: `Do you want to add ${txt} to ${targetEnvFile}?`,
1492
+ options: [
1493
+ { label: "Yes", value: "yes" },
1494
+ { label: "No", value: "no" },
1495
+ { label: "Choose other file(s)", value: "other" }
1496
+ ]
1497
+ });
1498
+ if (isCancel(shouldAdd)) {
1499
+ cancel(`\u270B Operation cancelled.`);
1500
+ process.exit(0);
1429
1501
  }
1430
- );
1431
- new_user_config = code;
1432
- total_dependencies.push(...dependencies);
1433
- total_envs.push(...envs);
1502
+ let envs = [];
1503
+ if (isMissingSecret) {
1504
+ envs.push("BETTER_AUTH_SECRET");
1505
+ }
1506
+ if (isMissingUrl) {
1507
+ envs.push("BETTER_AUTH_URL");
1508
+ }
1509
+ if (shouldAdd === "yes") {
1510
+ try {
1511
+ await updateEnvs({
1512
+ files: [path.join(cwd, targetEnvFile)],
1513
+ envs,
1514
+ isCommented: false
1515
+ });
1516
+ } catch (error) {
1517
+ log.error(`Failed to add ENV variables to ${targetEnvFile}`);
1518
+ log.error(JSON.stringify(error, null, 2));
1519
+ process.exit(1);
1520
+ }
1521
+ log.success(`\u{1F680} ENV variables successfully added!`);
1522
+ if (isMissingUrl) {
1523
+ log.info(
1524
+ `Be sure to update your BETTER_AUTH_URL according to your app's needs.`
1525
+ );
1526
+ }
1527
+ } else if (shouldAdd === "no") {
1528
+ log.info(`Skipping ENV step.`);
1529
+ } else if (shouldAdd === "other") {
1530
+ if (!envFiles.length) {
1531
+ cancel("No env files found. Please create an env file first.");
1532
+ process.exit(0);
1533
+ }
1534
+ const envFilesToUpdate = await multiselect({
1535
+ message: "Select the .env files you want to update",
1536
+ options: envFiles.map((x) => ({
1537
+ value: path.join(cwd, x),
1538
+ label: x
1539
+ })),
1540
+ required: false
1541
+ });
1542
+ if (isCancel(envFilesToUpdate)) {
1543
+ cancel("\u270B Operation cancelled.");
1544
+ process.exit(0);
1545
+ }
1546
+ if (envFilesToUpdate.length === 0) {
1547
+ log.info("No .env files to update. Skipping...");
1548
+ } else {
1549
+ try {
1550
+ await updateEnvs({
1551
+ files: envFilesToUpdate,
1552
+ envs,
1553
+ isCommented: false
1554
+ });
1555
+ } catch (error) {
1556
+ log.error(`Failed to update .env files:`);
1557
+ log.error(JSON.stringify(error, null, 2));
1558
+ process.exit(1);
1559
+ }
1560
+ log.success(`\u{1F680} ENV files successfully updated!`);
1561
+ }
1562
+ }
1563
+ }
1434
1564
  } catch (error) {
1435
- spinner.stop(
1436
- `Something went wrong while generating/updating your new auth config file.`,
1437
- 1
1438
- );
1439
- logger.error(error.message);
1440
- process.exit(1);
1441
1565
  }
1442
1566
  }
1443
- return {
1444
- generatedCode: new_user_config,
1445
- dependencies: total_dependencies,
1446
- envs: total_envs
1447
- };
1448
- }
1449
- function findClosingBracket(content, startIndex, openingBracket, closingBracket) {
1450
- let stack = 0;
1451
- let inString = false;
1452
- let quoteChar = null;
1453
- for (let i = startIndex; i < content.length; i++) {
1454
- const char = content[i];
1455
- if (char === '"' || char === "'" || char === "`") {
1456
- if (!inString) {
1457
- inString = true;
1458
- quoteChar = char;
1459
- } else if (char === quoteChar) {
1460
- inString = false;
1461
- quoteChar = null;
1462
- }
1463
- continue;
1464
- }
1465
- if (!inString) {
1466
- if (char === openingBracket) {
1467
- stack++;
1468
- } else if (char === closingBracket) {
1469
- if (stack === 0) {
1470
- return i;
1471
- }
1472
- stack--;
1473
- }
1567
+ outro(outroText);
1568
+ console.log();
1569
+ process.exit(0);
1570
+ }
1571
+ const init = new Command("init").option("-c, --cwd <cwd>", "The working directory.", process.cwd()).option(
1572
+ "--config <config>",
1573
+ "The path to the auth configuration file. defaults to the first `auth.ts` file found."
1574
+ ).option("--tsconfig <tsconfig>", "The path to the tsconfig file.").option("--skip-db", "Skip the database setup.").option("--skip-plugins", "Skip the plugins setup.").option(
1575
+ "--package-manager <package-manager>",
1576
+ "The package manager you want to use."
1577
+ ).action(initAction);
1578
+ async function getLatestNpmVersion(packageName) {
1579
+ try {
1580
+ const response = await fetch(`https://registry.npmjs.org/${packageName}`);
1581
+ if (!response.ok) {
1582
+ throw new Error(`Package not found: ${response.statusText}`);
1474
1583
  }
1584
+ const data = await response.json();
1585
+ return data["dist-tags"].latest;
1586
+ } catch (error) {
1587
+ throw error?.message;
1475
1588
  }
1476
- return null;
1477
1589
  }
1478
- function insertContent(params) {
1479
- const { line, character, content, insert_content } = params;
1480
- const lines = content.split("\n");
1481
- if (line < 1 || line > lines.length) {
1482
- throw new Error("Invalid line number");
1590
+ async function getPackageManager() {
1591
+ const { hasBun, hasPnpm } = await checkPackageManagers();
1592
+ if (!hasBun && !hasPnpm) return "npm";
1593
+ const packageManagerOptions = [];
1594
+ if (hasPnpm) {
1595
+ packageManagerOptions.push({
1596
+ value: "pnpm",
1597
+ label: "pnpm",
1598
+ hint: "recommended"
1599
+ });
1483
1600
  }
1484
- const targetLineIndex = line - 1;
1485
- if (character < 0 || character > lines[targetLineIndex].length) {
1486
- throw new Error("Invalid character index");
1601
+ if (hasBun) {
1602
+ packageManagerOptions.push({
1603
+ value: "bun",
1604
+ label: "bun"
1605
+ });
1487
1606
  }
1488
- const targetLine = lines[targetLineIndex];
1489
- const updatedLine = targetLine.slice(0, character) + insert_content + targetLine.slice(character);
1490
- lines[targetLineIndex] = updatedLine;
1491
- return lines.join("\n");
1607
+ packageManagerOptions.push({
1608
+ value: "npm",
1609
+ hint: "not recommended"
1610
+ });
1611
+ let packageManager = await select({
1612
+ message: "Choose a package manager",
1613
+ options: packageManagerOptions
1614
+ });
1615
+ if (isCancel(packageManager)) {
1616
+ cancel(`Operation cancelled.`);
1617
+ process.exit(0);
1618
+ }
1619
+ return packageManager;
1492
1620
  }
1493
- function getGroupInfo(content, commonIndexConfig, additionalFields) {
1494
- if (commonIndexConfig.type === "regex") {
1495
- const { regex, getIndex } = commonIndexConfig;
1496
- const match = regex.exec(content);
1497
- if (match) {
1498
- const matchIndex = match.index;
1499
- const groupIndex = getIndex({ matchIndex, match, additionalFields });
1500
- if (groupIndex === null) return null;
1501
- const position = getPosition(content, groupIndex);
1502
- return {
1503
- line: position.line,
1504
- character: position.character,
1505
- index: groupIndex
1506
- };
1621
+ async function getEnvFiles(cwd) {
1622
+ const files = await fs$1.readdir(cwd);
1623
+ return files.filter((x) => x.startsWith(".env"));
1624
+ }
1625
+ async function updateEnvs({
1626
+ envs,
1627
+ files,
1628
+ isCommented
1629
+ }) {
1630
+ let previouslyGeneratedSecret = null;
1631
+ for (const file of files) {
1632
+ const content = await fs$1.readFile(file, "utf8");
1633
+ const lines = content.split("\n");
1634
+ const newLines = envs.map(
1635
+ (x) => `${isCommented ? "# " : ""}${x}=${getEnvDescription(x) ?? `"some_value"`}`
1636
+ );
1637
+ newLines.push("");
1638
+ newLines.push(...lines);
1639
+ await fs$1.writeFile(file, newLines.join("\n"), "utf8");
1640
+ }
1641
+ function getEnvDescription(env) {
1642
+ if (env === "DATABASE_HOST") {
1643
+ return `"The host of your database"`;
1644
+ }
1645
+ if (env === "DATABASE_PORT") {
1646
+ return `"The port of your database"`;
1647
+ }
1648
+ if (env === "DATABASE_USER") {
1649
+ return `"The username of your database"`;
1650
+ }
1651
+ if (env === "DATABASE_PASSWORD") {
1652
+ return `"The password of your database"`;
1653
+ }
1654
+ if (env === "DATABASE_NAME") {
1655
+ return `"The name of your database"`;
1656
+ }
1657
+ if (env === "DATABASE_URL") {
1658
+ return `"The URL of your database"`;
1659
+ }
1660
+ if (env === "BETTER_AUTH_SECRET") {
1661
+ previouslyGeneratedSecret = previouslyGeneratedSecret ?? generateSecretHash();
1662
+ return `"${previouslyGeneratedSecret}"`;
1663
+ }
1664
+ if (env === "BETTER_AUTH_URL") {
1665
+ return `"http://localhost:3000" # Your APP URL`;
1507
1666
  }
1667
+ }
1668
+ }
1669
+
1670
+ function addSvelteKitEnvModules(aliases) {
1671
+ aliases["$env/dynamic/private"] = createDataUriModule(
1672
+ createDynamicEnvModule()
1673
+ );
1674
+ aliases["$env/dynamic/public"] = createDataUriModule(
1675
+ createDynamicEnvModule()
1676
+ );
1677
+ aliases["$env/static/private"] = createDataUriModule(
1678
+ createStaticEnvModule(filterPrivateEnv("PUBLIC_", ""))
1679
+ );
1680
+ aliases["$env/static/public"] = createDataUriModule(
1681
+ createStaticEnvModule(filterPublicEnv("PUBLIC_", ""))
1682
+ );
1683
+ }
1684
+ function createDataUriModule(module) {
1685
+ return `data:text/javascript;charset=utf-8,${encodeURIComponent(module)}`;
1686
+ }
1687
+ function createStaticEnvModule(env) {
1688
+ const declarations = Object.keys(env).filter((k) => validIdentifier.test(k) && !reserved.has(k)).map((k) => `export const ${k} = ${JSON.stringify(env[k])};`);
1689
+ return `
1690
+ ${declarations.join("\n")}
1691
+ // jiti dirty hack: .unknown
1692
+ `;
1693
+ }
1694
+ function createDynamicEnvModule() {
1695
+ return `
1696
+ export const env = process.env;
1697
+ // jiti dirty hack: .unknown
1698
+ `;
1699
+ }
1700
+ function filterPrivateEnv(publicPrefix, privatePrefix) {
1701
+ return Object.fromEntries(
1702
+ Object.entries(process.env).filter(
1703
+ ([k]) => k.startsWith(privatePrefix) && (!k.startsWith(publicPrefix))
1704
+ )
1705
+ );
1706
+ }
1707
+ function filterPublicEnv(publicPrefix, privatePrefix) {
1708
+ return Object.fromEntries(
1709
+ Object.entries(process.env).filter(
1710
+ ([k]) => k.startsWith(publicPrefix) && (privatePrefix === "")
1711
+ )
1712
+ );
1713
+ }
1714
+ const validIdentifier = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
1715
+ const reserved = /* @__PURE__ */ new Set([
1716
+ "do",
1717
+ "if",
1718
+ "in",
1719
+ "for",
1720
+ "let",
1721
+ "new",
1722
+ "try",
1723
+ "var",
1724
+ "case",
1725
+ "else",
1726
+ "enum",
1727
+ "eval",
1728
+ "null",
1729
+ "this",
1730
+ "true",
1731
+ "void",
1732
+ "with",
1733
+ "await",
1734
+ "break",
1735
+ "catch",
1736
+ "class",
1737
+ "const",
1738
+ "false",
1739
+ "super",
1740
+ "throw",
1741
+ "while",
1742
+ "yield",
1743
+ "delete",
1744
+ "export",
1745
+ "import",
1746
+ "public",
1747
+ "return",
1748
+ "static",
1749
+ "switch",
1750
+ "typeof",
1751
+ "default",
1752
+ "extends",
1753
+ "finally",
1754
+ "package",
1755
+ "private",
1756
+ "continue",
1757
+ "debugger",
1758
+ "function",
1759
+ "arguments",
1760
+ "interface",
1761
+ "protected",
1762
+ "implements",
1763
+ "instanceof"
1764
+ ]);
1765
+
1766
+ let possiblePaths = [
1767
+ "auth.ts",
1768
+ "auth.tsx",
1769
+ "auth.js",
1770
+ "auth.jsx",
1771
+ "auth.server.js",
1772
+ "auth.server.ts"
1773
+ ];
1774
+ possiblePaths = [
1775
+ ...possiblePaths,
1776
+ ...possiblePaths.map((it) => `lib/server/${it}`),
1777
+ ...possiblePaths.map((it) => `server/${it}`),
1778
+ ...possiblePaths.map((it) => `lib/${it}`),
1779
+ ...possiblePaths.map((it) => `utils/${it}`)
1780
+ ];
1781
+ possiblePaths = [
1782
+ ...possiblePaths,
1783
+ ...possiblePaths.map((it) => `src/${it}`),
1784
+ ...possiblePaths.map((it) => `app/${it}`)
1785
+ ];
1786
+ function getPathAliases(cwd) {
1787
+ const tsConfigPath = path.join(cwd, "tsconfig.json");
1788
+ if (!fs$2.existsSync(tsConfigPath)) {
1508
1789
  return null;
1509
- } else {
1510
- const { getIndex } = commonIndexConfig;
1511
- const index = getIndex({ content, additionalFields });
1512
- if (index === null) return null;
1513
- const { line, character } = getPosition(content, index);
1514
- return {
1515
- line,
1516
- character,
1517
- index
1518
- };
1790
+ }
1791
+ try {
1792
+ const tsConfig = getTsconfigInfo(cwd);
1793
+ const { paths = {}, baseUrl = "." } = tsConfig.compilerOptions || {};
1794
+ const result = {};
1795
+ const obj = Object.entries(paths);
1796
+ for (const [alias, aliasPaths] of obj) {
1797
+ for (const aliasedPath of aliasPaths) {
1798
+ const resolvedBaseUrl = path.join(cwd, baseUrl);
1799
+ const finalAlias = alias.slice(-1) === "*" ? alias.slice(0, -1) : alias;
1800
+ const finalAliasedPath = aliasedPath.slice(-1) === "*" ? aliasedPath.slice(0, -1) : aliasedPath;
1801
+ result[finalAlias || ""] = path.join(resolvedBaseUrl, finalAliasedPath);
1802
+ }
1803
+ }
1804
+ addSvelteKitEnvModules(result);
1805
+ return result;
1806
+ } catch (error) {
1807
+ console.error(error);
1808
+ throw new BetterAuthError("Error parsing tsconfig.json");
1519
1809
  }
1520
1810
  }
1521
- const getPosition = (str, index) => {
1522
- const lines = str.slice(0, index).split("\n");
1811
+ const jitiOptions = (cwd) => {
1812
+ const alias = getPathAliases(cwd) || {};
1523
1813
  return {
1524
- line: lines.length,
1525
- character: lines[lines.length - 1].length
1814
+ transformOptions: {
1815
+ babel: {
1816
+ presets: [
1817
+ [
1818
+ babelPresetTypeScript,
1819
+ {
1820
+ isTSX: true,
1821
+ allExtensions: true
1822
+ }
1823
+ ],
1824
+ [babelPresetReact, { runtime: "automatic" }]
1825
+ ]
1826
+ }
1827
+ },
1828
+ extensions: [".ts", ".tsx", ".js", ".jsx"],
1829
+ alias
1526
1830
  };
1527
1831
  };
1528
-
1529
- const supportedDatabases = [
1530
- // Built-in kysely
1531
- "sqlite",
1532
- "mysql",
1533
- "mssql",
1534
- "postgres",
1535
- // Drizzle
1536
- "drizzle:pg",
1537
- "drizzle:mysql",
1538
- "drizzle:sqlite",
1539
- // Prisma
1540
- "prisma:postgresql",
1541
- "prisma:mysql",
1542
- "prisma:sqlite",
1543
- // Mongo
1544
- "mongodb"
1545
- ];
1546
- const supportedPlugins = [
1547
- {
1548
- id: "two-factor",
1549
- name: "twoFactor",
1550
- path: `better-auth/plugins`,
1551
- clientName: "twoFactorClient",
1552
- clientPath: "better-auth/client/plugins"
1553
- },
1554
- {
1555
- id: "username",
1556
- name: "username",
1557
- clientName: "usernameClient",
1558
- path: `better-auth/plugins`,
1559
- clientPath: "better-auth/client/plugins"
1560
- },
1561
- {
1562
- id: "anonymous",
1563
- name: "anonymous",
1564
- clientName: "anonymousClient",
1565
- path: `better-auth/plugins`,
1566
- clientPath: "better-auth/client/plugins"
1567
- },
1568
- {
1569
- id: "phone-number",
1570
- name: "phoneNumber",
1571
- clientName: "phoneNumberClient",
1572
- path: `better-auth/plugins`,
1573
- clientPath: "better-auth/client/plugins"
1574
- },
1575
- {
1576
- id: "magic-link",
1577
- name: "magicLink",
1578
- clientName: "magicLinkClient",
1579
- clientPath: "better-auth/client/plugins",
1580
- path: `better-auth/plugins`
1581
- },
1582
- {
1583
- id: "email-otp",
1584
- name: "emailOTP",
1585
- clientName: "emailOTPClient",
1586
- path: `better-auth/plugins`,
1587
- clientPath: "better-auth/client/plugins"
1588
- },
1589
- {
1590
- id: "passkey",
1591
- name: "passkey",
1592
- clientName: "passkeyClient",
1593
- path: `better-auth/plugins/passkey`,
1594
- clientPath: "better-auth/client/plugins"
1595
- },
1596
- {
1597
- id: "generic-oauth",
1598
- name: "genericOAuth",
1599
- clientName: "genericOAuthClient",
1600
- path: `better-auth/plugins`,
1601
- clientPath: "better-auth/client/plugins"
1602
- },
1603
- {
1604
- id: "one-tap",
1605
- name: "oneTap",
1606
- clientName: "oneTapClient",
1607
- path: `better-auth/plugins`,
1608
- clientPath: "better-auth/client/plugins"
1609
- },
1610
- {
1611
- id: "api-key",
1612
- name: "apiKey",
1613
- clientName: "apiKeyClient",
1614
- path: `better-auth/plugins`,
1615
- clientPath: "better-auth/client/plugins"
1616
- },
1617
- {
1618
- id: "admin",
1619
- name: "admin",
1620
- clientName: "adminClient",
1621
- path: `better-auth/plugins`,
1622
- clientPath: "better-auth/client/plugins"
1623
- },
1624
- {
1625
- id: "organization",
1626
- name: "organization",
1627
- clientName: "organizationClient",
1628
- path: `better-auth/plugins`,
1629
- clientPath: "better-auth/client/plugins"
1630
- },
1631
- {
1632
- id: "oidc",
1633
- name: "oidcProvider",
1634
- clientName: "oidcClient",
1635
- path: `better-auth/plugins`,
1636
- clientPath: "better-auth/client/plugins"
1637
- },
1638
- {
1639
- id: "sso",
1640
- name: "sso",
1641
- clientName: "ssoClient",
1642
- path: `better-auth/plugins/sso`,
1643
- clientPath: "better-auth/client/plugins"
1644
- },
1645
- {
1646
- id: "bearer",
1647
- name: "bearer",
1648
- clientName: void 0,
1649
- path: `better-auth/plugins`,
1650
- clientPath: void 0
1651
- },
1652
- {
1653
- id: "multi-session",
1654
- name: "multiSession",
1655
- clientName: "multiSessionClient",
1656
- path: `better-auth/plugins`,
1657
- clientPath: "better-auth/client/plugins"
1658
- },
1659
- {
1660
- id: "oauth-proxy",
1661
- name: "oAuthProxy",
1662
- clientName: void 0,
1663
- path: `better-auth/plugins`,
1664
- clientPath: void 0
1665
- },
1666
- {
1667
- id: "open-api",
1668
- name: "openAPI",
1669
- clientName: void 0,
1670
- path: `better-auth/plugins`,
1671
- clientPath: void 0
1672
- },
1673
- {
1674
- id: "jwt",
1675
- name: "jwt",
1676
- clientName: void 0,
1677
- clientPath: void 0,
1678
- path: `better-auth/plugins`
1679
- },
1680
- {
1681
- id: "next-cookies",
1682
- name: "nextCookies",
1683
- clientPath: void 0,
1684
- clientName: void 0,
1685
- path: `better-auth/next-js`
1686
- }
1687
- ];
1688
- const defaultFormatOptions = {
1689
- trailingComma: "all",
1690
- useTabs: false,
1691
- tabWidth: 4
1692
- };
1693
- const getDefaultAuthConfig = async ({ appName }) => await format(
1694
- [
1695
- "import { betterAuth } from 'better-auth';",
1696
- "",
1697
- "export const auth = betterAuth({",
1698
- appName ? `appName: "${appName}",` : "",
1699
- "plugins: [],",
1700
- "});"
1701
- ].join("\n"),
1702
- {
1703
- filepath: "auth.ts",
1704
- ...defaultFormatOptions
1705
- }
1706
- );
1707
- const getDefaultAuthClientConfig = async ({
1708
- auth_config_path,
1709
- framework,
1710
- clientPlugins
1711
- }) => {
1712
- function groupImportVariables() {
1713
- const result = [
1714
- {
1715
- path: "better-auth/client/plugins",
1716
- variables: [{ name: "inferAdditionalFields" }]
1717
- }
1718
- ];
1719
- for (const plugin of clientPlugins) {
1720
- for (const import_ of plugin.imports) {
1721
- if (Array.isArray(import_.variables)) {
1722
- for (const variable of import_.variables) {
1723
- const existingIndex = result.findIndex(
1724
- (x) => x.path === import_.path
1725
- );
1726
- if (existingIndex !== -1) {
1727
- const vars = result[existingIndex].variables;
1728
- if (Array.isArray(vars)) {
1729
- vars.push(variable);
1730
- } else {
1731
- result[existingIndex].variables = [vars, variable];
1832
+ async function getConfig({
1833
+ cwd,
1834
+ configPath,
1835
+ shouldThrowOnError = false
1836
+ }) {
1837
+ try {
1838
+ let configFile = null;
1839
+ if (configPath) {
1840
+ let resolvedPath = path.join(cwd, configPath);
1841
+ if (existsSync(configPath)) resolvedPath = configPath;
1842
+ const { config } = await loadConfig({
1843
+ configFile: resolvedPath,
1844
+ dotenv: true,
1845
+ jitiOptions: jitiOptions(cwd)
1846
+ });
1847
+ if (!config.auth && !config.default) {
1848
+ if (shouldThrowOnError) {
1849
+ throw new Error(
1850
+ `Couldn't read your auth config in ${resolvedPath}. Make sure to default export your auth instance or to export as a variable named auth.`
1851
+ );
1852
+ }
1853
+ logger.error(
1854
+ `[#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.`
1855
+ );
1856
+ process.exit(1);
1857
+ }
1858
+ configFile = config.auth?.options || config.default?.options || null;
1859
+ }
1860
+ if (!configFile) {
1861
+ for (const possiblePath of possiblePaths) {
1862
+ try {
1863
+ const { config } = await loadConfig({
1864
+ configFile: possiblePath,
1865
+ jitiOptions: jitiOptions(cwd)
1866
+ });
1867
+ const hasConfig = Object.keys(config).length > 0;
1868
+ if (hasConfig) {
1869
+ configFile = config.auth?.options || config.default?.options || null;
1870
+ if (!configFile) {
1871
+ if (shouldThrowOnError) {
1872
+ throw new Error(
1873
+ "Couldn't read your auth config. Make sure to default export your auth instance or to export as a variable named auth."
1874
+ );
1732
1875
  }
1733
- } else {
1734
- result.push({
1735
- path: import_.path,
1736
- variables: [variable]
1737
- });
1876
+ logger.error("[#better-auth]: Couldn't read your auth config.");
1877
+ console.log("");
1878
+ logger.info(
1879
+ "[#better-auth]: Make sure to default export your auth instance or to export as a variable named auth."
1880
+ );
1881
+ process.exit(1);
1738
1882
  }
1883
+ break;
1739
1884
  }
1740
- } else {
1741
- const existingIndex = result.findIndex(
1742
- (x) => x.path === import_.path
1743
- );
1744
- if (existingIndex !== -1) {
1745
- const vars = result[existingIndex].variables;
1746
- if (Array.isArray(vars)) {
1747
- vars.push(import_.variables);
1748
- } else {
1749
- result[existingIndex].variables = [vars, import_.variables];
1885
+ } catch (e) {
1886
+ if (typeof e === "object" && e && "message" in e && typeof e.message === "string" && e.message.includes(
1887
+ "This module cannot be imported from a Client Component module"
1888
+ )) {
1889
+ if (shouldThrowOnError) {
1890
+ throw new Error(
1891
+ `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.`
1892
+ );
1750
1893
  }
1751
- } else {
1752
- result.push({
1753
- path: import_.path,
1754
- variables: [import_.variables]
1755
- });
1894
+ logger.error(
1895
+ `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.`
1896
+ );
1897
+ process.exit(1);
1898
+ }
1899
+ if (shouldThrowOnError) {
1900
+ throw e;
1756
1901
  }
1902
+ logger.error("[#better-auth]: Couldn't read your auth config.", e);
1903
+ process.exit(1);
1757
1904
  }
1758
1905
  }
1759
1906
  }
1760
- return result;
1761
- }
1762
- let imports = groupImportVariables();
1763
- let importString = "";
1764
- for (const import_ of imports) {
1765
- if (Array.isArray(import_.variables)) {
1766
- importString += `import { ${import_.variables.map(
1767
- (x) => `${x.asType ? "type " : ""}${x.name}${x.as ? ` as ${x.as}` : ""}`
1768
- ).join(", ")} } from "${import_.path}";
1769
- `;
1770
- } else {
1771
- importString += `import ${import_.variables.asType ? "type " : ""}${import_.variables.name}${import_.variables.as ? ` as ${import_.variables.as}` : ""} from "${import_.path}";
1772
- `;
1907
+ return configFile;
1908
+ } catch (e) {
1909
+ if (typeof e === "object" && e && "message" in e && typeof e.message === "string" && e.message.includes(
1910
+ "This module cannot be imported from a Client Component module"
1911
+ )) {
1912
+ if (shouldThrowOnError) {
1913
+ throw new Error(
1914
+ `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.`
1915
+ );
1916
+ }
1917
+ logger.error(
1918
+ `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.`
1919
+ );
1920
+ process.exit(1);
1773
1921
  }
1774
- }
1775
- return await format(
1776
- [
1777
- `import { createAuthClient } from "better-auth/${framework === "nextjs" ? "react" : framework === "vanilla" ? "client" : framework}";`,
1778
- `import type { auth } from "${auth_config_path}";`,
1779
- importString,
1780
- ``,
1781
- `export const authClient = createAuthClient({`,
1782
- `baseURL: "http://localhost:3000",`,
1783
- `plugins: [inferAdditionalFields<typeof auth>(),${clientPlugins.map((x) => `${x.name}(${x.contents})`).join(", ")}],`,
1784
- `});`
1785
- ].join("\n"),
1786
- {
1787
- filepath: "auth-client.ts",
1788
- ...defaultFormatOptions
1922
+ if (shouldThrowOnError) {
1923
+ throw e;
1789
1924
  }
1790
- );
1791
- };
1792
- const optionsSchema = z.object({
1793
- cwd: z.string(),
1794
- config: z.string().optional(),
1795
- database: z.enum(supportedDatabases).optional(),
1796
- "skip-db": z.boolean().optional(),
1797
- "skip-plugins": z.boolean().optional(),
1798
- "package-manager": z.string().optional(),
1799
- tsconfig: z.string().optional()
1800
- });
1801
- const outroText = `\u{1F973} All Done, Happy Hacking!`;
1802
- async function initAction(opts) {
1803
- console.log();
1804
- intro("\u{1F44B} Initializing Better Auth");
1805
- const options = optionsSchema.parse(opts);
1806
- const cwd = path.resolve(options.cwd);
1807
- let packageManagerPreference = void 0;
1808
- let config_path = "";
1809
- let framework = "vanilla";
1810
- const format$1 = async (code) => await format(code, {
1811
- filepath: config_path,
1812
- ...defaultFormatOptions
1813
- });
1814
- let packageInfo;
1815
- try {
1816
- packageInfo = getPackageInfo(cwd);
1817
- } catch (error) {
1818
- log.error(`\u274C Couldn't read your package.json file. (dir: ${cwd})`);
1819
- log.error(JSON.stringify(error, null, 2));
1925
+ logger.error("Couldn't read your auth config.", e);
1820
1926
  process.exit(1);
1821
1927
  }
1822
- const envFiles = await getEnvFiles(cwd);
1823
- if (!envFiles.length) {
1824
- outro("\u274C No .env files found. Please create an env file first.");
1825
- process.exit(0);
1826
- }
1827
- let targetEnvFile;
1828
- if (envFiles.includes(".env")) targetEnvFile = ".env";
1829
- else if (envFiles.includes(".env.local")) targetEnvFile = ".env.local";
1830
- else if (envFiles.includes(".env.development"))
1831
- targetEnvFile = ".env.development";
1832
- else if (envFiles.length === 1) targetEnvFile = envFiles[0];
1833
- else targetEnvFile = "none";
1834
- let tsconfigInfo;
1835
- try {
1836
- const tsconfigPath = options.tsconfig !== void 0 ? path.resolve(cwd, options.tsconfig) : path.join(cwd, "tsconfig.json");
1837
- tsconfigInfo = await getTsconfigInfo(cwd, tsconfigPath);
1838
- } catch (error) {
1839
- log.error(`\u274C Couldn't read your tsconfig.json file. (dir: ${cwd})`);
1840
- console.error(error);
1928
+ }
1929
+
1930
+ async function migrateAction(opts) {
1931
+ const options = z.object({
1932
+ cwd: z.string(),
1933
+ config: z.string().optional(),
1934
+ y: z.boolean().optional(),
1935
+ yes: z.boolean().optional()
1936
+ }).parse(opts);
1937
+ const cwd = path.resolve(options.cwd);
1938
+ if (!existsSync(cwd)) {
1939
+ logger.error(`The directory "${cwd}" does not exist.`);
1841
1940
  process.exit(1);
1842
1941
  }
1843
- if (!("compilerOptions" in tsconfigInfo && "strict" in tsconfigInfo.compilerOptions && tsconfigInfo.compilerOptions.strict === true)) {
1844
- log.warn(
1845
- `Better Auth requires your tsconfig.json to have "compilerOptions.strict" set to true.`
1942
+ const config = await getConfig({
1943
+ cwd,
1944
+ configPath: options.config
1945
+ });
1946
+ if (!config) {
1947
+ logger.error(
1948
+ "No configuration file found. Add a `auth.ts` file to your project or pass the path to the configuration file using the `--config` flag."
1846
1949
  );
1847
- const shouldAdd = await confirm({
1848
- message: `Would you like us to set ${chalk.bold(
1849
- `strict`
1850
- )} to ${chalk.bold(`true`)}?`
1851
- });
1852
- if (isCancel(shouldAdd)) {
1853
- cancel(`\u270B Operation cancelled.`);
1854
- process.exit(0);
1855
- }
1856
- if (shouldAdd) {
1857
- try {
1858
- await fs$2.writeFile(
1859
- path.join(cwd, "tsconfig.json"),
1860
- await format(
1861
- JSON.stringify(
1862
- Object.assign(tsconfigInfo, {
1863
- compilerOptions: {
1864
- strict: true
1865
- }
1866
- })
1867
- ),
1868
- { filepath: "tsconfig.json", ...defaultFormatOptions }
1869
- ),
1870
- "utf-8"
1871
- );
1872
- log.success(`\u{1F680} tsconfig.json successfully updated!`);
1873
- } catch (error) {
1874
- log.error(
1875
- `Failed to add "compilerOptions.strict" to your tsconfig.json file.`
1876
- );
1877
- console.error(error);
1878
- process.exit(1);
1879
- }
1880
- }
1950
+ return;
1881
1951
  }
1882
- const s = spinner({ indicator: "dots" });
1883
- s.start(`Checking better-auth installation`);
1884
- let latest_betterauth_version;
1885
- try {
1886
- latest_betterauth_version = await getLatestNpmVersion("better-auth");
1887
- } catch (error) {
1888
- log.error(`\u274C Couldn't get latest version of better-auth.`);
1889
- console.error(error);
1952
+ const db = await getAdapter(config);
1953
+ if (!db) {
1954
+ logger.error(
1955
+ "Invalid database configuration. Make sure you're not using adapters. Migrate command only works with built-in Kysely adapter."
1956
+ );
1890
1957
  process.exit(1);
1891
1958
  }
1892
- if (!packageInfo.dependencies || !Object.keys(packageInfo.dependencies).includes("better-auth")) {
1893
- s.stop("Finished fetching latest version of better-auth.");
1894
- const s2 = spinner({ indicator: "dots" });
1895
- const shouldInstallBetterAuthDep = await confirm({
1896
- message: `Would you like to install Better Auth?`
1897
- });
1898
- if (isCancel(shouldInstallBetterAuthDep)) {
1899
- cancel(`\u270B Operation cancelled.`);
1900
- process.exit(0);
1901
- }
1902
- if (packageManagerPreference === void 0) {
1903
- packageManagerPreference = await getPackageManager();
1904
- }
1905
- if (shouldInstallBetterAuthDep) {
1906
- s2.start(
1907
- `Installing Better Auth using ${chalk.bold(packageManagerPreference)}`
1959
+ if (db.id !== "kysely") {
1960
+ if (db.id === "prisma") {
1961
+ logger.error(
1962
+ "The migrate command only works with the built-in Kysely adapter. For Prisma, run `npx @better-auth/cli generate` to create the schema, then use Prisma\u2019s migrate or push to apply it."
1908
1963
  );
1909
1964
  try {
1910
- const start = Date.now();
1911
- await installDependencies({
1912
- dependencies: ["better-auth@latest"],
1913
- packageManager: packageManagerPreference,
1914
- cwd
1965
+ const telemetry = await createTelemetry(config);
1966
+ await telemetry.publish({
1967
+ type: "cli_migrate",
1968
+ payload: {
1969
+ outcome: "unsupported_adapter",
1970
+ adapter: "prisma",
1971
+ config: getTelemetryAuthConfig(config)
1972
+ }
1915
1973
  });
1916
- s2.stop(
1917
- `Better Auth installed ${chalk.greenBright(
1918
- `successfully`
1919
- )}! ${chalk.gray(`(${formatMilliseconds(Date.now() - start)})`)}`
1920
- );
1921
- } catch (error) {
1922
- s2.stop(`Failed to install Better Auth:`);
1923
- console.error(error);
1924
- process.exit(1);
1974
+ } catch {
1925
1975
  }
1926
- }
1927
- } else if (packageInfo.dependencies["better-auth"] !== "workspace:*" && semver.lt(
1928
- semver.coerce(packageInfo.dependencies["better-auth"])?.toString(),
1929
- semver.clean(latest_betterauth_version)
1930
- )) {
1931
- s.stop("Finished fetching latest version of better-auth.");
1932
- const shouldInstallBetterAuthDep = await confirm({
1933
- message: `Your current Better Auth dependency is out-of-date. Would you like to update it? (${chalk.bold(
1934
- packageInfo.dependencies["better-auth"]
1935
- )} \u2192 ${chalk.bold(`v${latest_betterauth_version}`)})`
1936
- });
1937
- if (isCancel(shouldInstallBetterAuthDep)) {
1938
- cancel(`\u270B Operation cancelled.`);
1939
1976
  process.exit(0);
1940
1977
  }
1941
- if (shouldInstallBetterAuthDep) {
1942
- if (packageManagerPreference === void 0) {
1943
- packageManagerPreference = await getPackageManager();
1944
- }
1945
- const s2 = spinner({ indicator: "dots" });
1946
- s2.start(
1947
- `Updating Better Auth using ${chalk.bold(packageManagerPreference)}`
1978
+ if (db.id === "drizzle") {
1979
+ logger.error(
1980
+ "The migrate command only works with the built-in Kysely adapter. For Drizzle, run `npx @better-auth/cli generate` to create the schema, then use Drizzle\u2019s migrate or push to apply it."
1948
1981
  );
1949
1982
  try {
1950
- const start = Date.now();
1951
- await installDependencies({
1952
- dependencies: ["better-auth@latest"],
1953
- packageManager: packageManagerPreference,
1954
- cwd
1983
+ const telemetry = await createTelemetry(config);
1984
+ await telemetry.publish({
1985
+ type: "cli_migrate",
1986
+ payload: {
1987
+ outcome: "unsupported_adapter",
1988
+ adapter: "drizzle",
1989
+ config: getTelemetryAuthConfig(config)
1990
+ }
1955
1991
  });
1956
- s2.stop(
1957
- `Better Auth updated ${chalk.greenBright(
1958
- `successfully`
1959
- )}! ${chalk.gray(`(${formatMilliseconds(Date.now() - start)})`)}`
1960
- );
1961
- } catch (error) {
1962
- s2.stop(`Failed to update Better Auth:`);
1963
- log.error(error.message);
1964
- process.exit(1);
1992
+ } catch {
1965
1993
  }
1966
- }
1967
- } else {
1968
- s.stop(`Better Auth dependencies are ${chalk.greenBright(`up to date`)}!`);
1969
- }
1970
- const packageJson = getPackageInfo(cwd);
1971
- let appName;
1972
- if (!packageJson.name) {
1973
- const newAppName = await text({
1974
- message: "What is the name of your application?"
1975
- });
1976
- if (isCancel(newAppName)) {
1977
- cancel("\u270B Operation cancelled.");
1978
1994
  process.exit(0);
1979
1995
  }
1980
- appName = newAppName;
1981
- } else {
1982
- appName = packageJson.name;
1996
+ logger.error("Migrate command isn't supported for this adapter.");
1997
+ try {
1998
+ const telemetry = await createTelemetry(config);
1999
+ await telemetry.publish({
2000
+ type: "cli_migrate",
2001
+ payload: {
2002
+ outcome: "unsupported_adapter",
2003
+ adapter: db.id,
2004
+ config: getTelemetryAuthConfig(config)
2005
+ }
2006
+ });
2007
+ } catch {
2008
+ }
2009
+ process.exit(1);
1983
2010
  }
1984
- let possiblePaths = ["auth.ts", "auth.tsx", "auth.js", "auth.jsx"];
1985
- possiblePaths = [
1986
- ...possiblePaths,
1987
- ...possiblePaths.map((it) => `lib/server/${it}`),
1988
- ...possiblePaths.map((it) => `server/${it}`),
1989
- ...possiblePaths.map((it) => `lib/${it}`),
1990
- ...possiblePaths.map((it) => `utils/${it}`)
1991
- ];
1992
- possiblePaths = [
1993
- ...possiblePaths,
1994
- ...possiblePaths.map((it) => `src/${it}`),
1995
- ...possiblePaths.map((it) => `app/${it}`)
1996
- ];
1997
- if (options.config) {
1998
- config_path = path.join(cwd, options.config);
1999
- } else {
2000
- for (const possiblePath of possiblePaths) {
2001
- const doesExist = existsSync(path.join(cwd, possiblePath));
2002
- if (doesExist) {
2003
- config_path = path.join(cwd, possiblePath);
2004
- break;
2005
- }
2011
+ const spinner = yoctoSpinner({ text: "preparing migration..." }).start();
2012
+ const { toBeAdded, toBeCreated, runMigrations } = await getMigrations(config);
2013
+ if (!toBeAdded.length && !toBeCreated.length) {
2014
+ spinner.stop();
2015
+ logger.info("\u{1F680} No migrations needed.");
2016
+ try {
2017
+ const telemetry = await createTelemetry(config);
2018
+ await telemetry.publish({
2019
+ type: "cli_migrate",
2020
+ payload: {
2021
+ outcome: "no_changes",
2022
+ config: getTelemetryAuthConfig(config)
2023
+ }
2024
+ });
2025
+ } catch {
2006
2026
  }
2027
+ process.exit(0);
2007
2028
  }
2008
- let current_user_config = "";
2009
- let database = null;
2010
- let add_plugins = [];
2011
- if (!config_path) {
2012
- const shouldCreateAuthConfig = await select({
2013
- message: `Would you like to create an auth config file?`,
2014
- options: [
2015
- { label: "Yes", value: "yes" },
2016
- { label: "No", value: "no" }
2017
- ]
2029
+ spinner.stop();
2030
+ logger.info(`\u{1F511} The migration will affect the following:`);
2031
+ for (const table of [...toBeCreated, ...toBeAdded]) {
2032
+ console.log(
2033
+ "->",
2034
+ chalk.magenta(Object.keys(table.fields).join(", ")),
2035
+ chalk.white("fields on"),
2036
+ chalk.yellow(`${table.table}`),
2037
+ chalk.white("table.")
2038
+ );
2039
+ }
2040
+ if (options.y) {
2041
+ console.warn("WARNING: --y is deprecated. Consider -y or --yes");
2042
+ options.yes = true;
2043
+ }
2044
+ let migrate2 = options.yes;
2045
+ if (!migrate2) {
2046
+ const response = await prompts({
2047
+ type: "confirm",
2048
+ name: "migrate",
2049
+ message: "Are you sure you want to run these migrations?",
2050
+ initial: false
2018
2051
  });
2019
- if (isCancel(shouldCreateAuthConfig)) {
2020
- cancel(`\u270B Operation cancelled.`);
2021
- process.exit(0);
2022
- }
2023
- if (shouldCreateAuthConfig === "yes") {
2024
- const shouldSetupDb = await confirm({
2025
- message: `Would you like to set up your ${chalk.bold(`database`)}?`,
2026
- initialValue: true
2052
+ migrate2 = response.migrate;
2053
+ }
2054
+ if (!migrate2) {
2055
+ logger.info("Migration cancelled.");
2056
+ try {
2057
+ const telemetry = await createTelemetry(config);
2058
+ await telemetry.publish({
2059
+ type: "cli_migrate",
2060
+ payload: { outcome: "aborted", config: getTelemetryAuthConfig(config) }
2027
2061
  });
2028
- if (isCancel(shouldSetupDb)) {
2029
- cancel(`\u270B Operating cancelled.`);
2030
- process.exit(0);
2031
- }
2032
- if (shouldSetupDb) {
2033
- const prompted_database = await select({
2034
- message: "Choose a Database Dialect",
2035
- options: supportedDatabases.map((it) => ({ value: it, label: it }))
2036
- });
2037
- if (isCancel(prompted_database)) {
2038
- cancel(`\u270B Operating cancelled.`);
2039
- process.exit(0);
2040
- }
2041
- database = prompted_database;
2042
- }
2043
- if (options["skip-plugins"] !== false) {
2044
- const shouldSetupPlugins = await confirm({
2045
- message: `Would you like to set up ${chalk.bold(`plugins`)}?`
2046
- });
2047
- if (isCancel(shouldSetupPlugins)) {
2048
- cancel(`\u270B Operating cancelled.`);
2049
- process.exit(0);
2050
- }
2051
- if (shouldSetupPlugins) {
2052
- const prompted_plugins = await multiselect({
2053
- message: "Select your new plugins",
2054
- options: supportedPlugins.filter((x) => x.id !== "next-cookies").map((x) => ({ value: x.id, label: x.id })),
2055
- required: false
2056
- });
2057
- if (isCancel(prompted_plugins)) {
2058
- cancel(`\u270B Operating cancelled.`);
2059
- process.exit(0);
2060
- }
2061
- add_plugins = prompted_plugins.map(
2062
- (x) => supportedPlugins.find((y) => y.id === x)
2063
- );
2064
- const possible_next_config_paths = [
2065
- "next.config.js",
2066
- "next.config.ts",
2067
- "next.config.mjs",
2068
- ".next/server/next.config.js",
2069
- ".next/server/next.config.ts",
2070
- ".next/server/next.config.mjs"
2071
- ];
2072
- for (const possible_next_config_path of possible_next_config_paths) {
2073
- if (existsSync(path.join(cwd, possible_next_config_path))) {
2074
- framework = "nextjs";
2075
- break;
2076
- }
2077
- }
2078
- if (framework === "nextjs") {
2079
- const result = await confirm({
2080
- message: `It looks like you're using NextJS. Do you want to add the next-cookies plugin? ${chalk.bold(
2081
- `(Recommended)`
2082
- )}`
2083
- });
2084
- if (isCancel(result)) {
2085
- cancel(`\u270B Operating cancelled.`);
2086
- process.exit(0);
2087
- }
2088
- if (result) {
2089
- add_plugins.push(
2090
- supportedPlugins.find((x) => x.id === "next-cookies")
2091
- );
2092
- }
2093
- }
2094
- }
2062
+ } catch {
2063
+ }
2064
+ process.exit(0);
2065
+ }
2066
+ spinner?.start("migrating...");
2067
+ await runMigrations();
2068
+ spinner.stop();
2069
+ logger.info("\u{1F680} migration was completed successfully!");
2070
+ try {
2071
+ const telemetry = await createTelemetry(config);
2072
+ await telemetry.publish({
2073
+ type: "cli_migrate",
2074
+ payload: { outcome: "migrated", config: getTelemetryAuthConfig(config) }
2075
+ });
2076
+ } catch {
2077
+ }
2078
+ process.exit(0);
2079
+ }
2080
+ const migrate = new Command("migrate").option(
2081
+ "-c, --cwd <cwd>",
2082
+ "the working directory. defaults to the current directory.",
2083
+ process.cwd()
2084
+ ).option(
2085
+ "--config <config>",
2086
+ "the path to the configuration file. defaults to the first configuration file found."
2087
+ ).option(
2088
+ "-y, --yes",
2089
+ "automatically accept and run migrations without prompting",
2090
+ false
2091
+ ).option("--y", "(deprecated) same as --yes", false).action(migrateAction);
2092
+
2093
+ function convertToSnakeCase(str, camelCase) {
2094
+ if (camelCase) {
2095
+ return str;
2096
+ }
2097
+ return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
2098
+ }
2099
+ const generateDrizzleSchema = async ({
2100
+ options,
2101
+ file,
2102
+ adapter
2103
+ }) => {
2104
+ const tables = getAuthTables(options);
2105
+ const filePath = file || "./auth-schema.ts";
2106
+ const databaseType = adapter.options?.provider;
2107
+ if (!databaseType) {
2108
+ throw new Error(
2109
+ `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`
2110
+ );
2111
+ }
2112
+ const fileExist = existsSync(filePath);
2113
+ let code = generateImport({ databaseType, tables, options });
2114
+ for (const tableKey in tables) {
2115
+ let getType = function(name, field) {
2116
+ if (!databaseType) {
2117
+ throw new Error(
2118
+ `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`
2119
+ );
2095
2120
  }
2096
- const filePath = path.join(cwd, "auth.ts");
2097
- config_path = filePath;
2098
- log.info(`Creating auth config file: ${filePath}`);
2099
- try {
2100
- current_user_config = await getDefaultAuthConfig({
2101
- appName
2102
- });
2103
- const { dependencies, envs, generatedCode } = await generateAuthConfig({
2104
- current_user_config,
2105
- format: format$1,
2106
- //@ts-ignore
2107
- s,
2108
- plugins: add_plugins,
2109
- database
2110
- });
2111
- current_user_config = generatedCode;
2112
- await fs$2.writeFile(filePath, current_user_config);
2113
- config_path = filePath;
2114
- log.success(`\u{1F680} Auth config file successfully created!`);
2115
- if (envs.length !== 0) {
2116
- log.info(
2117
- `There are ${envs.length} environment variables for your database of choice.`
2118
- );
2119
- const shouldUpdateEnvs = await confirm({
2120
- message: `Would you like us to update your ENV files?`
2121
- });
2122
- if (isCancel(shouldUpdateEnvs)) {
2123
- cancel("\u270B Operation cancelled.");
2124
- process.exit(0);
2125
- }
2126
- if (shouldUpdateEnvs) {
2127
- const filesToUpdate = await multiselect({
2128
- message: "Select the .env files you want to update",
2129
- options: envFiles.map((x) => ({
2130
- value: path.join(cwd, x),
2131
- label: x
2132
- })),
2133
- required: false
2134
- });
2135
- if (isCancel(filesToUpdate)) {
2136
- cancel("\u270B Operation cancelled.");
2137
- process.exit(0);
2138
- }
2139
- if (filesToUpdate.length === 0) {
2140
- log.info("No .env files to update. Skipping...");
2141
- } else {
2142
- try {
2143
- await updateEnvs({
2144
- files: filesToUpdate,
2145
- envs,
2146
- isCommented: true
2147
- });
2148
- } catch (error) {
2149
- log.error(`Failed to update .env files:`);
2150
- log.error(JSON.stringify(error, null, 2));
2151
- process.exit(1);
2152
- }
2153
- log.success(`\u{1F680} ENV files successfully updated!`);
2154
- }
2121
+ name = convertToSnakeCase(name, adapter.options?.camelCase);
2122
+ if (field.references?.field === "id") {
2123
+ if (options.advanced?.database?.useNumberId) {
2124
+ if (databaseType === "pg") {
2125
+ return `integer('${name}')`;
2126
+ } else if (databaseType === "mysql") {
2127
+ return `int('${name}')`;
2128
+ } else {
2129
+ return `integer('${name}')`;
2155
2130
  }
2156
2131
  }
2157
- if (dependencies.length !== 0) {
2158
- log.info(
2159
- `There are ${dependencies.length} dependencies to install. (${dependencies.map((x) => chalk.green(x)).join(", ")})`
2160
- );
2161
- const shouldInstallDeps = await confirm({
2162
- message: `Would you like us to install dependencies?`
2163
- });
2164
- if (isCancel(shouldInstallDeps)) {
2165
- cancel("\u270B Operation cancelled.");
2166
- process.exit(0);
2167
- }
2168
- if (shouldInstallDeps) {
2169
- const s2 = spinner({ indicator: "dots" });
2170
- if (packageManagerPreference === void 0) {
2171
- packageManagerPreference = await getPackageManager();
2172
- }
2173
- s2.start(
2174
- `Installing dependencies using ${chalk.bold(
2175
- packageManagerPreference
2176
- )}...`
2177
- );
2178
- try {
2179
- const start = Date.now();
2180
- await installDependencies({
2181
- dependencies,
2182
- packageManager: packageManagerPreference,
2183
- cwd
2184
- });
2185
- s2.stop(
2186
- `Dependencies installed ${chalk.greenBright(
2187
- `successfully`
2188
- )} ${chalk.gray(
2189
- `(${formatMilliseconds(Date.now() - start)})`
2190
- )}`
2191
- );
2192
- } catch (error) {
2193
- s2.stop(
2194
- `Failed to install dependencies using ${packageManagerPreference}:`
2195
- );
2196
- log.error(error.message);
2197
- process.exit(1);
2198
- }
2132
+ if (field.references.field) {
2133
+ if (databaseType === "mysql") {
2134
+ return `varchar('${name}', { length: 36 })`;
2199
2135
  }
2200
2136
  }
2201
- } catch (error) {
2202
- log.error(`Failed to create auth config file: ${filePath}`);
2203
- console.error(error);
2204
- process.exit(1);
2137
+ return `text('${name}')`;
2138
+ }
2139
+ const type = field.type;
2140
+ const typeMap = {
2141
+ string: {
2142
+ sqlite: `text('${name}')`,
2143
+ pg: `text('${name}')`,
2144
+ mysql: field.unique ? `varchar('${name}', { length: 255 })` : field.references ? `varchar('${name}', { length: 36 })` : `text('${name}')`
2145
+ },
2146
+ boolean: {
2147
+ sqlite: `integer('${name}', { mode: 'boolean' })`,
2148
+ pg: `boolean('${name}')`,
2149
+ mysql: `boolean('${name}')`
2150
+ },
2151
+ number: {
2152
+ sqlite: `integer('${name}')`,
2153
+ pg: field.bigint ? `bigint('${name}', { mode: 'number' })` : `integer('${name}')`,
2154
+ mysql: field.bigint ? `bigint('${name}', { mode: 'number' })` : `int('${name}')`
2155
+ },
2156
+ date: {
2157
+ sqlite: `integer('${name}', { mode: 'timestamp' })`,
2158
+ pg: `timestamp('${name}')`,
2159
+ mysql: `timestamp('${name}')`
2160
+ },
2161
+ "number[]": {
2162
+ sqlite: `integer('${name}').array()`,
2163
+ pg: field.bigint ? `bigint('${name}', { mode: 'number' }).array()` : `integer('${name}').array()`,
2164
+ mysql: field.bigint ? `bigint('${name}', { mode: 'number' }).array()` : `int('${name}').array()`
2165
+ },
2166
+ "string[]": {
2167
+ sqlite: `text('${name}').array()`,
2168
+ pg: `text('${name}').array()`,
2169
+ mysql: `text('${name}').array()`
2170
+ }
2171
+ };
2172
+ return typeMap[type][databaseType];
2173
+ };
2174
+ const table = tables[tableKey];
2175
+ const modelName = getModelName(table.modelName, adapter.options);
2176
+ const fields = table.fields;
2177
+ let id = "";
2178
+ if (options.advanced?.database?.useNumberId) {
2179
+ if (databaseType === "pg") {
2180
+ id = `serial("id").primaryKey()`;
2181
+ } else {
2182
+ id = `int("id").autoincrement.primaryKey()`;
2183
+ }
2184
+ } else {
2185
+ if (databaseType === "mysql") {
2186
+ id = `varchar('id', { length: 36 }).primaryKey()`;
2187
+ } else if (databaseType === "pg") {
2188
+ id = `text('id').primaryKey()`;
2189
+ } else {
2190
+ id = `text('id').primaryKey()`;
2205
2191
  }
2206
- } else if (shouldCreateAuthConfig === "no") {
2207
- log.info(`Skipping auth config file creation.`);
2208
2192
  }
2209
- } else {
2210
- log.message();
2211
- log.success(`Found auth config file. ${chalk.gray(`(${config_path})`)}`);
2212
- log.message();
2193
+ const schema = `export const ${modelName} = ${databaseType}Table("${convertToSnakeCase(
2194
+ modelName,
2195
+ adapter.options?.camelCase
2196
+ )}", {
2197
+ id: ${id},
2198
+ ${Object.keys(fields).map((field) => {
2199
+ const attr = fields[field];
2200
+ let type = getType(field, attr);
2201
+ if (attr.defaultValue) {
2202
+ if (typeof attr.defaultValue === "function") {
2203
+ type += `.$defaultFn(${attr.defaultValue})`;
2204
+ } else if (typeof attr.defaultValue === "string") {
2205
+ type += `.default("${attr.defaultValue}")`;
2206
+ } else {
2207
+ type += `.default(${attr.defaultValue})`;
2208
+ }
2209
+ }
2210
+ return `${field}: ${type}${attr.required ? ".notNull()" : ""}${attr.unique ? ".unique()" : ""}${attr.references ? `.references(()=> ${getModelName(
2211
+ attr.references.model,
2212
+ adapter.options
2213
+ )}.${attr.references.field}, { onDelete: '${attr.references.onDelete || "cascade"}' })` : ""}`;
2214
+ }).join(",\n ")}
2215
+ });`;
2216
+ code += `
2217
+ ${schema}
2218
+ `;
2213
2219
  }
2214
- let possibleClientPaths = [
2215
- "auth-client.ts",
2216
- "auth-client.tsx",
2217
- "auth-client.js",
2218
- "auth-client.jsx",
2219
- "client.ts",
2220
- "client.tsx",
2221
- "client.js",
2222
- "client.jsx"
2223
- ];
2224
- possibleClientPaths = [
2225
- ...possibleClientPaths,
2226
- ...possibleClientPaths.map((it) => `lib/server/${it}`),
2227
- ...possibleClientPaths.map((it) => `server/${it}`),
2228
- ...possibleClientPaths.map((it) => `lib/${it}`),
2229
- ...possibleClientPaths.map((it) => `utils/${it}`)
2230
- ];
2231
- possibleClientPaths = [
2232
- ...possibleClientPaths,
2233
- ...possibleClientPaths.map((it) => `src/${it}`),
2234
- ...possibleClientPaths.map((it) => `app/${it}`)
2235
- ];
2236
- let authClientConfigPath = null;
2237
- for (const possiblePath of possibleClientPaths) {
2238
- const doesExist = existsSync(path.join(cwd, possiblePath));
2239
- if (doesExist) {
2240
- authClientConfigPath = path.join(cwd, possiblePath);
2241
- break;
2242
- }
2220
+ const formattedCode = await prettier.format(code, {
2221
+ parser: "typescript"
2222
+ });
2223
+ return {
2224
+ code: formattedCode,
2225
+ fileName: filePath,
2226
+ overwrite: fileExist
2227
+ };
2228
+ };
2229
+ function generateImport({
2230
+ databaseType,
2231
+ tables,
2232
+ options
2233
+ }) {
2234
+ let imports = [];
2235
+ const hasBigint = Object.values(tables).some(
2236
+ (table) => Object.values(table.fields).some((field) => field.bigint)
2237
+ );
2238
+ const useNumberId = options.advanced?.database?.useNumberId;
2239
+ imports.push(`${databaseType}Table`);
2240
+ imports.push(
2241
+ databaseType === "mysql" ? "varchar, text" : databaseType === "pg" ? "text" : "text"
2242
+ );
2243
+ imports.push(hasBigint ? databaseType !== "sqlite" ? "bigint" : "" : "");
2244
+ imports.push(databaseType !== "sqlite" ? "timestamp, boolean" : "");
2245
+ imports.push(databaseType === "mysql" ? "int" : "integer");
2246
+ imports.push(useNumberId ? databaseType === "pg" ? "serial" : "" : "");
2247
+ return `import { ${imports.map((x) => x.trim()).filter((x) => x !== "").join(", ")} } from "drizzle-orm/${databaseType}-core";
2248
+ `;
2249
+ }
2250
+ function getModelName(modelName, options) {
2251
+ return options?.usePlural ? `${modelName}s` : modelName;
2252
+ }
2253
+
2254
+ const generatePrismaSchema = async ({
2255
+ adapter,
2256
+ options,
2257
+ file
2258
+ }) => {
2259
+ const provider = adapter.options?.provider || "postgresql";
2260
+ const tables = getAuthTables(options);
2261
+ const filePath = file || "./prisma/schema.prisma";
2262
+ const schemaPrismaExist = existsSync(path.join(process.cwd(), filePath));
2263
+ let schemaPrisma = "";
2264
+ if (schemaPrismaExist) {
2265
+ schemaPrisma = await fs$1.readFile(
2266
+ path.join(process.cwd(), filePath),
2267
+ "utf-8"
2268
+ );
2269
+ } else {
2270
+ schemaPrisma = getNewPrisma(provider);
2243
2271
  }
2244
- if (!authClientConfigPath) {
2245
- const choice = await select({
2246
- message: `Would you like to create an auth client config file?`,
2247
- options: [
2248
- { label: "Yes", value: "yes" },
2249
- { label: "No", value: "no" }
2250
- ]
2251
- });
2252
- if (isCancel(choice)) {
2253
- cancel(`\u270B Operation cancelled.`);
2254
- process.exit(0);
2255
- }
2256
- if (choice === "yes") {
2257
- authClientConfigPath = path.join(cwd, "auth-client.ts");
2258
- log.info(`Creating auth client config file: ${authClientConfigPath}`);
2259
- try {
2260
- let contents = await getDefaultAuthClientConfig({
2261
- auth_config_path: ("./" + path.join(config_path.replace(cwd, ""))).replace(".//", "./"),
2262
- clientPlugins: add_plugins.filter((x) => x.clientName).map((plugin) => {
2263
- let contents2 = "";
2264
- if (plugin.id === "one-tap") {
2265
- contents2 = `{ clientId: "MY_CLIENT_ID" }`;
2266
- }
2267
- return {
2268
- contents: contents2,
2269
- id: plugin.id,
2270
- name: plugin.clientName,
2271
- imports: [
2272
- {
2273
- path: "better-auth/client/plugins",
2274
- variables: [{ name: plugin.clientName }]
2275
- }
2276
- ]
2277
- };
2278
- }),
2279
- framework
2280
- });
2281
- await fs$2.writeFile(authClientConfigPath, contents);
2282
- log.success(`\u{1F680} Auth client config file successfully created!`);
2283
- } catch (error) {
2284
- log.error(
2285
- `Failed to create auth client config file: ${authClientConfigPath}`
2272
+ const manyToManyRelations = /* @__PURE__ */ new Map();
2273
+ for (const table in tables) {
2274
+ const fields = tables[table]?.fields;
2275
+ for (const field in fields) {
2276
+ const attr = fields[field];
2277
+ if (attr.references) {
2278
+ const referencedOriginalModel = attr.references.model;
2279
+ const referencedCustomModel = tables[referencedOriginalModel]?.modelName || referencedOriginalModel;
2280
+ const referencedModelNameCap = capitalizeFirstLetter(
2281
+ referencedCustomModel
2286
2282
  );
2287
- log.error(JSON.stringify(error, null, 2));
2288
- process.exit(1);
2283
+ if (!manyToManyRelations.has(referencedModelNameCap)) {
2284
+ manyToManyRelations.set(referencedModelNameCap, /* @__PURE__ */ new Set());
2285
+ }
2286
+ const currentCustomModel = tables[table]?.modelName || table;
2287
+ const currentModelNameCap = capitalizeFirstLetter(currentCustomModel);
2288
+ manyToManyRelations.get(referencedModelNameCap).add(currentModelNameCap);
2289
2289
  }
2290
- } else if (choice === "no") {
2291
- log.info(`Skipping auth client config file creation.`);
2292
2290
  }
2293
- } else {
2294
- log.success(
2295
- `Found auth client config file. ${chalk.gray(
2296
- `(${authClientConfigPath})`
2297
- )}`
2298
- );
2299
2291
  }
2300
- if (targetEnvFile !== "none") {
2301
- try {
2302
- const fileContents = await fs$2.readFile(
2303
- path.join(cwd, targetEnvFile),
2304
- "utf8"
2305
- );
2306
- const parsed = parse(fileContents);
2307
- let isMissingSecret = false;
2308
- let isMissingUrl = false;
2309
- if (parsed.BETTER_AUTH_SECRET === void 0) isMissingSecret = true;
2310
- if (parsed.BETTER_AUTH_URL === void 0) isMissingUrl = true;
2311
- if (isMissingSecret || isMissingUrl) {
2312
- let txt = "";
2313
- if (isMissingSecret && !isMissingUrl)
2314
- txt = chalk.bold(`BETTER_AUTH_SECRET`);
2315
- else if (!isMissingSecret && isMissingUrl)
2316
- txt = chalk.bold(`BETTER_AUTH_URL`);
2317
- else
2318
- txt = chalk.bold.underline(`BETTER_AUTH_SECRET`) + ` and ` + chalk.bold.underline(`BETTER_AUTH_URL`);
2319
- log.warn(`Missing ${txt} in ${targetEnvFile}`);
2320
- const shouldAdd = await select({
2321
- message: `Do you want to add ${txt} to ${targetEnvFile}?`,
2322
- options: [
2323
- { label: "Yes", value: "yes" },
2324
- { label: "No", value: "no" },
2325
- { label: "Choose other file(s)", value: "other" }
2326
- ]
2327
- });
2328
- if (isCancel(shouldAdd)) {
2329
- cancel(`\u270B Operation cancelled.`);
2330
- process.exit(0);
2292
+ const schema = produceSchema(schemaPrisma, (builder) => {
2293
+ for (const table in tables) {
2294
+ let getType = function({
2295
+ isBigint,
2296
+ isOptional,
2297
+ type
2298
+ }) {
2299
+ if (type === "string") {
2300
+ return isOptional ? "String?" : "String";
2331
2301
  }
2332
- let envs = [];
2333
- if (isMissingSecret) {
2334
- envs.push("BETTER_AUTH_SECRET");
2302
+ if (type === "number" && isBigint) {
2303
+ return isOptional ? "BigInt?" : "BigInt";
2335
2304
  }
2336
- if (isMissingUrl) {
2337
- envs.push("BETTER_AUTH_URL");
2305
+ if (type === "number") {
2306
+ return isOptional ? "Int?" : "Int";
2338
2307
  }
2339
- if (shouldAdd === "yes") {
2340
- try {
2341
- await updateEnvs({
2342
- files: [path.join(cwd, targetEnvFile)],
2343
- envs,
2344
- isCommented: false
2345
- });
2346
- } catch (error) {
2347
- log.error(`Failed to add ENV variables to ${targetEnvFile}`);
2348
- log.error(JSON.stringify(error, null, 2));
2349
- process.exit(1);
2308
+ if (type === "boolean") {
2309
+ return isOptional ? "Boolean?" : "Boolean";
2310
+ }
2311
+ if (type === "date") {
2312
+ return isOptional ? "DateTime?" : "DateTime";
2313
+ }
2314
+ if (type === "string[]") {
2315
+ return isOptional ? "String[]" : "String[]";
2316
+ }
2317
+ if (type === "number[]") {
2318
+ return isOptional ? "Int[]" : "Int[]";
2319
+ }
2320
+ };
2321
+ const originalTableName = table;
2322
+ const customModelName = tables[table]?.modelName || table;
2323
+ const modelName = capitalizeFirstLetter(customModelName);
2324
+ const fields = tables[table]?.fields;
2325
+ const prismaModel = builder.findByType("model", {
2326
+ name: modelName
2327
+ });
2328
+ if (!prismaModel) {
2329
+ if (provider === "mongodb") {
2330
+ builder.model(modelName).field("id", "String").attribute("id").attribute(`map("_id")`);
2331
+ } else {
2332
+ if (options.advanced?.database?.useNumberId) {
2333
+ builder.model(modelName).field("id", "Int").attribute("id").attribute("default(autoincrement())");
2334
+ } else {
2335
+ builder.model(modelName).field("id", "String").attribute("id");
2350
2336
  }
2351
- log.success(`\u{1F680} ENV variables successfully added!`);
2352
- if (isMissingUrl) {
2353
- log.info(
2354
- `Be sure to update your BETTER_AUTH_URL according to your app's needs.`
2355
- );
2337
+ }
2338
+ }
2339
+ for (const field in fields) {
2340
+ const attr = fields[field];
2341
+ const fieldName = attr.fieldName || field;
2342
+ if (prismaModel) {
2343
+ const isAlreadyExist = builder.findByType("field", {
2344
+ name: fieldName,
2345
+ within: prismaModel.properties
2346
+ });
2347
+ if (isAlreadyExist) {
2348
+ continue;
2356
2349
  }
2357
- } else if (shouldAdd === "no") {
2358
- log.info(`Skipping ENV step.`);
2359
- } else if (shouldAdd === "other") {
2360
- if (!envFiles.length) {
2361
- cancel("No env files found. Please create an env file first.");
2362
- process.exit(0);
2350
+ }
2351
+ const fieldBuilder = builder.model(modelName).field(
2352
+ fieldName,
2353
+ field === "id" && options.advanced?.database?.useNumberId ? getType({
2354
+ isBigint: false,
2355
+ isOptional: false,
2356
+ type: "number"
2357
+ }) : getType({
2358
+ isBigint: attr?.bigint || false,
2359
+ isOptional: !attr?.required,
2360
+ type: attr.references?.field === "id" ? options.advanced?.database?.useNumberId ? "number" : "string" : attr.type
2361
+ })
2362
+ );
2363
+ if (field === "id") {
2364
+ fieldBuilder.attribute("id");
2365
+ if (provider === "mongodb") {
2366
+ fieldBuilder.attribute(`map("_id")`);
2363
2367
  }
2364
- const envFilesToUpdate = await multiselect({
2365
- message: "Select the .env files you want to update",
2366
- options: envFiles.map((x) => ({
2367
- value: path.join(cwd, x),
2368
- label: x
2369
- })),
2370
- required: false
2368
+ } else if (fieldName !== field) {
2369
+ fieldBuilder.attribute(`map("${field}")`);
2370
+ }
2371
+ if (attr.unique) {
2372
+ builder.model(modelName).blockAttribute(`unique([${fieldName}])`);
2373
+ }
2374
+ if (attr.references) {
2375
+ const referencedOriginalModelName = attr.references.model;
2376
+ const referencedCustomModelName = tables[referencedOriginalModelName]?.modelName || referencedOriginalModelName;
2377
+ let action = "Cascade";
2378
+ if (attr.references.onDelete === "no action") action = "NoAction";
2379
+ else if (attr.references.onDelete === "set null") action = "SetNull";
2380
+ else if (attr.references.onDelete === "set default")
2381
+ action = "SetDefault";
2382
+ else if (attr.references.onDelete === "restrict") action = "Restrict";
2383
+ builder.model(modelName).field(
2384
+ `${referencedCustomModelName.toLowerCase()}`,
2385
+ capitalizeFirstLetter(referencedCustomModelName)
2386
+ ).attribute(
2387
+ `relation(fields: [${fieldName}], references: [${attr.references.field}], onDelete: ${action})`
2388
+ );
2389
+ }
2390
+ if (!attr.unique && !attr.references && provider === "mysql" && attr.type === "string") {
2391
+ builder.model(modelName).field(fieldName).attribute("db.Text");
2392
+ }
2393
+ }
2394
+ if (manyToManyRelations.has(modelName)) {
2395
+ for (const relatedModel of manyToManyRelations.get(modelName)) {
2396
+ const fieldName = `${relatedModel.toLowerCase()}s`;
2397
+ const existingField = builder.findByType("field", {
2398
+ name: fieldName,
2399
+ within: prismaModel?.properties
2371
2400
  });
2372
- if (isCancel(envFilesToUpdate)) {
2373
- cancel("\u270B Operation cancelled.");
2374
- process.exit(0);
2375
- }
2376
- if (envFilesToUpdate.length === 0) {
2377
- log.info("No .env files to update. Skipping...");
2378
- } else {
2379
- try {
2380
- await updateEnvs({
2381
- files: envFilesToUpdate,
2382
- envs,
2383
- isCommented: false
2384
- });
2385
- } catch (error) {
2386
- log.error(`Failed to update .env files:`);
2387
- log.error(JSON.stringify(error, null, 2));
2388
- process.exit(1);
2389
- }
2390
- log.success(`\u{1F680} ENV files successfully updated!`);
2401
+ if (!existingField) {
2402
+ builder.model(modelName).field(fieldName, `${relatedModel}[]`);
2391
2403
  }
2392
2404
  }
2393
2405
  }
2394
- } catch (error) {
2406
+ const hasAttribute = builder.findByType("attribute", {
2407
+ name: "map",
2408
+ within: prismaModel?.properties
2409
+ });
2410
+ const hasChanged = customModelName !== originalTableName;
2411
+ if (!hasAttribute) {
2412
+ builder.model(modelName).blockAttribute(
2413
+ "map",
2414
+ `${hasChanged ? customModelName : originalTableName}`
2415
+ );
2416
+ }
2395
2417
  }
2418
+ });
2419
+ const schemaChanged = schema.trim() !== schemaPrisma.trim();
2420
+ return {
2421
+ code: schemaChanged ? schema : "",
2422
+ fileName: filePath,
2423
+ overwrite: schemaPrismaExist && schemaChanged
2424
+ };
2425
+ };
2426
+ const getNewPrisma = (provider) => `generator client {
2427
+ provider = "prisma-client-js"
2396
2428
  }
2397
- outro(outroText);
2398
- console.log();
2399
- process.exit(0);
2400
- }
2401
- const init = new Command("init").option("-c, --cwd <cwd>", "The working directory.", process.cwd()).option(
2402
- "--config <config>",
2403
- "The path to the auth configuration file. defaults to the first `auth.ts` file found."
2404
- ).option("--tsconfig <tsconfig>", "The path to the tsconfig file.").option("--skip-db", "Skip the database setup.").option("--skip-plugins", "Skip the plugins setup.").option(
2405
- "--package-manager <package-manager>",
2406
- "The package manager you want to use."
2407
- ).action(initAction);
2408
- async function getLatestNpmVersion(packageName) {
2409
- try {
2410
- const response = await fetch(`https://registry.npmjs.org/${packageName}`);
2411
- if (!response.ok) {
2412
- throw new Error(`Package not found: ${response.statusText}`);
2413
- }
2414
- const data = await response.json();
2415
- return data["dist-tags"].latest;
2416
- } catch (error) {
2417
- throw error?.message;
2429
+
2430
+ datasource db {
2431
+ provider = "${provider}"
2432
+ url = ${provider === "sqlite" ? `"file:./dev.db"` : `env("DATABASE_URL")`}
2433
+ }`;
2434
+
2435
+ const generateMigrations = async ({
2436
+ options,
2437
+ file
2438
+ }) => {
2439
+ const { compileMigrations } = await getMigrations(options);
2440
+ const migrations = await compileMigrations();
2441
+ return {
2442
+ code: migrations.trim() === ";" ? "" : migrations,
2443
+ fileName: file || `./better-auth_migrations/${(/* @__PURE__ */ new Date()).toISOString().replace(/:/g, "-")}.sql`
2444
+ };
2445
+ };
2446
+
2447
+ const adapters = {
2448
+ prisma: generatePrismaSchema,
2449
+ drizzle: generateDrizzleSchema,
2450
+ kysely: generateMigrations
2451
+ };
2452
+ const generateSchema = (opts) => {
2453
+ const adapter = opts.adapter;
2454
+ const generator = adapter.id in adapters ? adapters[adapter.id] : null;
2455
+ if (generator) {
2456
+ return generator(opts);
2418
2457
  }
2419
- }
2420
- async function getPackageManager() {
2421
- const { hasBun, hasPnpm } = await checkPackageManagers();
2422
- if (!hasBun && !hasPnpm) return "npm";
2423
- const packageManagerOptions = [];
2424
- if (hasPnpm) {
2425
- packageManagerOptions.push({
2426
- value: "pnpm",
2427
- label: "pnpm",
2428
- hint: "recommended"
2429
- });
2458
+ if (adapter.createSchema) {
2459
+ return adapter.createSchema(opts.options, opts.file).then(({ code, path: fileName, overwrite }) => ({
2460
+ code,
2461
+ fileName,
2462
+ overwrite
2463
+ }));
2430
2464
  }
2431
- if (hasBun) {
2432
- packageManagerOptions.push({
2433
- value: "bun",
2434
- label: "bun"
2435
- });
2465
+ logger.error(
2466
+ `${adapter.id} is not supported. If it is a custom adapter, please request the maintainer to implement createSchema`
2467
+ );
2468
+ process.exit(1);
2469
+ };
2470
+
2471
+ async function generateAction(opts) {
2472
+ const options = z.object({
2473
+ cwd: z.string(),
2474
+ config: z.string().optional(),
2475
+ output: z.string().optional(),
2476
+ y: z.boolean().optional(),
2477
+ yes: z.boolean().optional()
2478
+ }).parse(opts);
2479
+ const cwd = path.resolve(options.cwd);
2480
+ if (!existsSync(cwd)) {
2481
+ logger.error(`The directory "${cwd}" does not exist.`);
2482
+ process.exit(1);
2436
2483
  }
2437
- packageManagerOptions.push({
2438
- value: "npm",
2439
- hint: "not recommended"
2440
- });
2441
- let packageManager = await select({
2442
- message: "Choose a package manager",
2443
- options: packageManagerOptions
2484
+ const config = await getConfig({
2485
+ cwd,
2486
+ configPath: options.config
2444
2487
  });
2445
- if (isCancel(packageManager)) {
2446
- cancel(`Operation cancelled.`);
2447
- process.exit(0);
2448
- }
2449
- return packageManager;
2450
- }
2451
- async function getEnvFiles(cwd) {
2452
- const files = await fs$2.readdir(cwd);
2453
- return files.filter((x) => x.startsWith(".env"));
2454
- }
2455
- async function updateEnvs({
2456
- envs,
2457
- files,
2458
- isCommented
2459
- }) {
2460
- let previouslyGeneratedSecret = null;
2461
- for (const file of files) {
2462
- const content = await fs$2.readFile(file, "utf8");
2463
- const lines = content.split("\n");
2464
- const newLines = envs.map(
2465
- (x) => `${isCommented ? "# " : ""}${x}=${getEnvDescription(x) ?? `"some_value"`}`
2488
+ if (!config) {
2489
+ logger.error(
2490
+ "No configuration file found. Add a `auth.ts` file to your project or pass the path to the configuration file using the `--config` flag."
2466
2491
  );
2467
- newLines.push("");
2468
- newLines.push(...lines);
2469
- await fs$2.writeFile(file, newLines.join("\n"), "utf8");
2492
+ return;
2470
2493
  }
2471
- function getEnvDescription(env) {
2472
- if (env === "DATABASE_HOST") {
2473
- return `"The host of your database"`;
2474
- }
2475
- if (env === "DATABASE_PORT") {
2476
- return `"The port of your database"`;
2477
- }
2478
- if (env === "DATABASE_USER") {
2479
- return `"The username of your database"`;
2480
- }
2481
- if (env === "DATABASE_PASSWORD") {
2482
- return `"The password of your database"`;
2494
+ const adapter = await getAdapter(config).catch((e) => {
2495
+ logger.error(e.message);
2496
+ process.exit(1);
2497
+ });
2498
+ const spinner = yoctoSpinner({ text: "preparing schema..." }).start();
2499
+ const schema = await generateSchema({
2500
+ adapter,
2501
+ file: options.output,
2502
+ options: config
2503
+ });
2504
+ spinner.stop();
2505
+ if (!schema.code) {
2506
+ logger.info("Your schema is already up to date.");
2507
+ try {
2508
+ const telemetry = await createTelemetry(config);
2509
+ await telemetry.publish({
2510
+ type: "cli_generate",
2511
+ payload: {
2512
+ outcome: "no_changes",
2513
+ config: getTelemetryAuthConfig(config, {
2514
+ adapter: adapter.id,
2515
+ database: typeof config.database === "function" ? "adapter" : "kysely"
2516
+ })
2517
+ }
2518
+ });
2519
+ } catch {
2483
2520
  }
2484
- if (env === "DATABASE_NAME") {
2485
- return `"The name of your database"`;
2521
+ process.exit(0);
2522
+ }
2523
+ if (schema.overwrite) {
2524
+ let confirm2 = options.y || options.yes;
2525
+ if (!confirm2) {
2526
+ const response = await prompts({
2527
+ type: "confirm",
2528
+ name: "confirm",
2529
+ message: `The file ${schema.fileName} already exists. Do you want to ${chalk.yellow(
2530
+ `${schema.overwrite ? "overwrite" : "append"}`
2531
+ )} the schema to the file?`
2532
+ });
2533
+ confirm2 = response.confirm;
2486
2534
  }
2487
- if (env === "DATABASE_URL") {
2488
- return `"The URL of your database"`;
2535
+ if (confirm2) {
2536
+ const exist = existsSync(path.join(cwd, schema.fileName));
2537
+ if (!exist) {
2538
+ await fs$1.mkdir(path.dirname(path.join(cwd, schema.fileName)), {
2539
+ recursive: true
2540
+ });
2541
+ }
2542
+ if (schema.overwrite) {
2543
+ await fs$1.writeFile(path.join(cwd, schema.fileName), schema.code);
2544
+ } else {
2545
+ await fs$1.appendFile(path.join(cwd, schema.fileName), schema.code);
2546
+ }
2547
+ logger.success(
2548
+ `\u{1F680} Schema was ${schema.overwrite ? "overwritten" : "appended"} successfully!`
2549
+ );
2550
+ try {
2551
+ const telemetry = await createTelemetry(config);
2552
+ await telemetry.publish({
2553
+ type: "cli_generate",
2554
+ payload: {
2555
+ outcome: schema.overwrite ? "overwritten" : "appended",
2556
+ config: getTelemetryAuthConfig(config)
2557
+ }
2558
+ });
2559
+ } catch {
2560
+ }
2561
+ process.exit(0);
2562
+ } else {
2563
+ logger.error("Schema generation aborted.");
2564
+ try {
2565
+ const telemetry = await createTelemetry(config);
2566
+ await telemetry.publish({
2567
+ type: "cli_generate",
2568
+ payload: {
2569
+ outcome: "aborted",
2570
+ config: getTelemetryAuthConfig(config)
2571
+ }
2572
+ });
2573
+ } catch {
2574
+ }
2575
+ process.exit(1);
2489
2576
  }
2490
- if (env === "BETTER_AUTH_SECRET") {
2491
- previouslyGeneratedSecret = previouslyGeneratedSecret ?? generateSecretHash();
2492
- return `"${previouslyGeneratedSecret}"`;
2577
+ }
2578
+ if (options.y) {
2579
+ console.warn("WARNING: --y is deprecated. Consider -y or --yes");
2580
+ options.yes = true;
2581
+ }
2582
+ let confirm = options.yes;
2583
+ if (!confirm) {
2584
+ const response = await prompts({
2585
+ type: "confirm",
2586
+ name: "confirm",
2587
+ message: `Do you want to generate the schema to ${chalk.yellow(
2588
+ schema.fileName
2589
+ )}?`
2590
+ });
2591
+ confirm = response.confirm;
2592
+ }
2593
+ if (!confirm) {
2594
+ logger.error("Schema generation aborted.");
2595
+ try {
2596
+ const telemetry = await createTelemetry(config);
2597
+ await telemetry.publish({
2598
+ type: "cli_generate",
2599
+ payload: { outcome: "aborted", config: getTelemetryAuthConfig(config) }
2600
+ });
2601
+ } catch {
2493
2602
  }
2494
- if (env === "BETTER_AUTH_URL") {
2495
- return `"http://localhost:3000" # Your APP URL`;
2603
+ process.exit(1);
2604
+ }
2605
+ if (!options.output) {
2606
+ const dirExist = existsSync(path.dirname(path.join(cwd, schema.fileName)));
2607
+ if (!dirExist) {
2608
+ await fs$1.mkdir(path.dirname(path.join(cwd, schema.fileName)), {
2609
+ recursive: true
2610
+ });
2496
2611
  }
2497
2612
  }
2613
+ await fs$1.writeFile(
2614
+ options.output || path.join(cwd, schema.fileName),
2615
+ schema.code
2616
+ );
2617
+ logger.success(`\u{1F680} Schema was generated successfully!`);
2618
+ try {
2619
+ const telemetry = await createTelemetry(config);
2620
+ await telemetry.publish({
2621
+ type: "cli_generate",
2622
+ payload: { outcome: "generated", config: getTelemetryAuthConfig(config) }
2623
+ });
2624
+ } catch {
2625
+ }
2626
+ process.exit(0);
2498
2627
  }
2628
+ const generate = new Command("generate").option(
2629
+ "-c, --cwd <cwd>",
2630
+ "the working directory. defaults to the current directory.",
2631
+ process.cwd()
2632
+ ).option(
2633
+ "--config <config>",
2634
+ "the path to the configuration file. defaults to the first configuration file found."
2635
+ ).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);
2499
2636
 
2500
2637
  process.on("SIGINT", () => process.exit(0));
2501
2638
  process.on("SIGTERM", () => process.exit(0));
@@ -2506,7 +2643,7 @@ async function main() {
2506
2643
  packageInfo = await getPackageInfo();
2507
2644
  } catch (error) {
2508
2645
  }
2509
- program.addCommand(migrate).addCommand(generate).addCommand(generateSecret).addCommand(init).version(packageInfo.version || "1.1.2").description("Better Auth CLI");
2646
+ program.addCommand(init).addCommand(migrate).addCommand(generate).addCommand(generateSecret).version(packageInfo.version || "1.1.2").description("Better Auth CLI").action(() => program.help());
2510
2647
  program.parse();
2511
2648
  }
2512
2649
  main();