feat(cli): add info script (#4143)

This commit is contained in:
Alex Yang
2025-08-27 10:49:34 -07:00
committed by GitHub
parent 3049ee9e8d
commit 7931166c8d
4 changed files with 937 additions and 5 deletions

View File

@@ -3,7 +3,7 @@ title: CLI
description: Built-in CLI for managing your project.
---
Better Auth comes with a built-in CLI to help you manage the database schemas, initialize your project, and generate a secret key for your application.
Better Auth comes with a built-in CLI to help you manage the database schemas, initialize your project, generate a secret key for your application, and gather diagnostic information about your setup.
## Generate
@@ -15,14 +15,14 @@ npx @better-auth/cli@latest generate
### Options
- `--output` - Where to save the generated schema. For Prisma, it will be saved in prisma/schema.prisma. For Drizzle, it goes to schema.ts in your project root. For Kysely, its an SQL file saved as schema.sql in your project root.
- `--output` - Where to save the generated schema. For Prisma, it will be saved in prisma/schema.prisma. For Drizzle, it goes to schema.ts in your project root. For Kysely, it's an SQL file saved as schema.sql in your project root.
- `--config` - The path to your Better Auth config file. By default, the CLI will search for a auth.ts file in **./**, **./utils**, **./lib**, or any of these directories under `src` directory.
- `--yes` - Skip the confirmation prompt and generate the schema directly.
## Migrate
The migrate command applies the Better Auth schema directly to your database. This is available if youre using the built-in Kysely adapter. For other adapters, you'll need to apply the schema using your ORM's migration tool.
The migrate command applies the Better Auth schema directly to your database. This is available if you're using the built-in Kysely adapter. For other adapters, you'll need to apply the schema using your ORM's migration tool.
```bash title="Terminal"
npx @better-auth/cli@latest migrate
@@ -49,6 +49,43 @@ npx @better-auth/cli@latest init
- `--database` - The database you want to use. Currently, the only supported database is `sqlite`.
- `--package-manager` - The package manager you want to use. Currently, the only supported package managers are `npm`, `pnpm`, `yarn`, `bun`. (Defaults to the manager you used to initialize the CLI.)
## Info
The `info` command provides diagnostic information about your Better Auth setup and environment. Useful for debugging and sharing when seeking support.
```bash title="Terminal"
npx @better-auth/cli@latest info
```
### Output
The command displays:
- **System**: OS, CPU, memory, Node.js version
- **Package Manager**: Detected manager and version
- **Better Auth**: Version and configuration (sensitive data auto-redacted)
- **Frameworks**: Detected frameworks (Next.js, React, Vue, etc.)
- **Databases**: Database clients and ORMs (Prisma, Drizzle, etc.)
### Options
- `--config` - Path to your Better Auth config file
- `--json` - Output as JSON for sharing or programmatic use
### Examples
```bash
# Basic usage
npx @better-auth/cli@latest info
# Custom config path
npx @better-auth/cli@latest info --config ./config/auth.ts
# JSON output
npx @better-auth/cli@latest info --json > auth-info.json
```
Sensitive data like secrets, API keys, and database URLs are automatically replaced with `[REDACTED]` for safe sharing.
## Secret
The CLI also provides a way to generate a secret key for your Better Auth instance.
@@ -61,6 +98,6 @@ npx @better-auth/cli@latest secret
**Error: Cannot find module X**
If you see this error, it means the CLI cant resolve imported modules in your Better Auth config file. We're working on a fix for many of these issues, but in the meantime, you can try the following:
If you see this error, it means the CLI can't resolve imported modules in your Better Auth config file. We're working on a fix for many of these issues, but in the meantime, you can try the following:
- Remove any import aliases in your config file and use relative paths instead. After running the CLI, you can revert to using aliases.
- Remove any import aliases in your config file and use relative paths instead. After running the CLI, you can revert to using aliases.

View File

@@ -0,0 +1,539 @@
import { Command } from "commander";
import os from "os";
import { execSync } from "child_process";
import { existsSync, readFileSync } from "fs";
import path from "path";
import chalk from "chalk";
import { getConfig } from "../utils/get-config";
import { getPackageInfo } from "../utils/get-package-info";
function getSystemInfo() {
const platform = os.platform();
const arch = os.arch();
const version = os.version();
const release = os.release();
const cpus = os.cpus();
const memory = os.totalmem();
const freeMemory = os.freemem();
return {
platform,
arch,
version,
release,
cpuCount: cpus.length,
cpuModel: cpus[0]?.model || "Unknown",
totalMemory: `${(memory / 1024 / 1024 / 1024).toFixed(2)} GB`,
freeMemory: `${(freeMemory / 1024 / 1024 / 1024).toFixed(2)} GB`,
};
}
function getNodeInfo() {
return {
version: process.version,
env: process.env.NODE_ENV || "development",
};
}
function getPackageManager() {
const userAgent = process.env.npm_config_user_agent || "";
if (userAgent.includes("yarn")) {
return { name: "yarn", version: getVersion("yarn") };
}
if (userAgent.includes("pnpm")) {
return { name: "pnpm", version: getVersion("pnpm") };
}
if (userAgent.includes("bun")) {
return { name: "bun", version: getVersion("bun") };
}
return { name: "npm", version: getVersion("npm") };
}
function getVersion(command: string): string {
try {
const output = execSync(`${command} --version`, { encoding: "utf8" });
return output.trim();
} catch {
return "Not installed";
}
}
function getFrameworkInfo(projectRoot: string) {
const packageJsonPath = path.join(projectRoot, "package.json");
if (!existsSync(packageJsonPath)) {
return null;
}
try {
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
const deps = {
...packageJson.dependencies,
...packageJson.devDependencies,
};
const frameworks: Record<string, string | undefined> = {
next: deps["next"],
react: deps["react"],
vue: deps["vue"],
nuxt: deps["nuxt"],
svelte: deps["svelte"],
"@sveltejs/kit": deps["@sveltejs/kit"],
express: deps["express"],
fastify: deps["fastify"],
hono: deps["hono"],
remix: deps["@remix-run/react"],
astro: deps["astro"],
solid: deps["solid-js"],
qwik: deps["@builder.io/qwik"],
};
const installedFrameworks = Object.entries(frameworks)
.filter(([_, version]) => version)
.map(([name, version]) => ({ name, version }));
return installedFrameworks.length > 0 ? installedFrameworks : null;
} catch {
return null;
}
}
function getDatabaseInfo(projectRoot: string) {
const packageJsonPath = path.join(projectRoot, "package.json");
if (!existsSync(packageJsonPath)) {
return null;
}
try {
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
const deps = {
...packageJson.dependencies,
...packageJson.devDependencies,
};
const databases: Record<string, string | undefined> = {
"better-sqlite3": deps["better-sqlite3"],
"@libsql/client": deps["@libsql/client"],
"@libsql/kysely-libsql": deps["@libsql/kysely-libsql"],
mysql2: deps["mysql2"],
pg: deps["pg"],
postgres: deps["postgres"],
"@prisma/client": deps["@prisma/client"],
drizzle: deps["drizzle-orm"],
kysely: deps["kysely"],
mongodb: deps["mongodb"],
"@neondatabase/serverless": deps["@neondatabase/serverless"],
"@vercel/postgres": deps["@vercel/postgres"],
"@planetscale/database": deps["@planetscale/database"],
};
const installedDatabases = Object.entries(databases)
.filter(([_, version]) => version)
.map(([name, version]) => ({ name, version }));
return installedDatabases.length > 0 ? installedDatabases : null;
} catch {
return null;
}
}
function sanitizeBetterAuthConfig(config: any): any {
if (!config) return null;
const sanitized = JSON.parse(JSON.stringify(config));
// List of sensitive keys to redact
const sensitiveKeys = [
"secret",
"clientSecret",
"clientId",
"authToken",
"apiKey",
"apiSecret",
"privateKey",
"publicKey",
"password",
"token",
"webhook",
"connectionString",
"databaseUrl",
"databaseURL",
"TURSO_AUTH_TOKEN",
"TURSO_DATABASE_URL",
"MYSQL_DATABASE_URL",
"DATABASE_URL",
"POSTGRES_URL",
"MONGODB_URI",
"stripeKey",
"stripeWebhookSecret",
];
// Keys that should NOT be redacted even if they contain sensitive keywords
const allowedKeys = [
"baseURL",
"callbackURL",
"redirectURL",
"trustedOrigins",
"appName",
];
function redactSensitive(obj: any, parentKey?: string): any {
if (typeof obj !== "object" || obj === null) {
// Check if the parent key is sensitive
if (parentKey && typeof obj === "string" && obj.length > 0) {
// First check if it's in the allowed list
if (
allowedKeys.some(
(allowed) => parentKey.toLowerCase() === allowed.toLowerCase(),
)
) {
return obj;
}
const lowerKey = parentKey.toLowerCase();
if (
sensitiveKeys.some((key) => {
const lowerSensitiveKey = key.toLowerCase();
// Exact match or the key ends with the sensitive key
return (
lowerKey === lowerSensitiveKey ||
lowerKey.endsWith(lowerSensitiveKey)
);
})
) {
return "[REDACTED]";
}
}
return obj;
}
if (Array.isArray(obj)) {
return obj.map((item) => redactSensitive(item, parentKey));
}
const result: any = {};
for (const [key, value] of Object.entries(obj)) {
// First check if this key is in the allowed list
if (
allowedKeys.some(
(allowed) => key.toLowerCase() === allowed.toLowerCase(),
)
) {
result[key] = value;
continue;
}
const lowerKey = key.toLowerCase();
// Check if this key should be redacted
if (
sensitiveKeys.some((sensitiveKey) => {
const lowerSensitiveKey = sensitiveKey.toLowerCase();
// Exact match or the key ends with the sensitive key
return (
lowerKey === lowerSensitiveKey ||
lowerKey.endsWith(lowerSensitiveKey)
);
})
) {
if (typeof value === "string" && value.length > 0) {
result[key] = "[REDACTED]";
} else if (typeof value === "object" && value !== null) {
// Still recurse into objects but mark them as potentially sensitive
result[key] = redactSensitive(value, key);
} else {
result[key] = value;
}
} else {
result[key] = redactSensitive(value, key);
}
}
return result;
}
// Special handling for specific config sections
if (sanitized.database) {
// Redact database connection details
if (typeof sanitized.database === "string") {
sanitized.database = "[REDACTED]";
} else if (sanitized.database.url) {
sanitized.database.url = "[REDACTED]";
}
if (sanitized.database.authToken) {
sanitized.database.authToken = "[REDACTED]";
}
}
if (sanitized.socialProviders) {
// Redact all social provider secrets
for (const provider in sanitized.socialProviders) {
if (sanitized.socialProviders[provider]) {
sanitized.socialProviders[provider] = redactSensitive(
sanitized.socialProviders[provider],
provider,
);
}
}
}
if (sanitized.emailAndPassword?.sendResetPassword) {
sanitized.emailAndPassword.sendResetPassword = "[Function]";
}
if (sanitized.emailVerification?.sendVerificationEmail) {
sanitized.emailVerification.sendVerificationEmail = "[Function]";
}
// Redact plugin configurations
if (sanitized.plugins && Array.isArray(sanitized.plugins)) {
sanitized.plugins = sanitized.plugins.map((plugin: any) => {
if (typeof plugin === "function") {
return "[Plugin Function]";
}
if (plugin && typeof plugin === "object") {
// Get plugin name if available
const pluginName = plugin.id || plugin.name || "unknown";
return {
name: pluginName,
config: redactSensitive(plugin.config || plugin),
};
}
return plugin;
});
}
return redactSensitive(sanitized);
}
async function getBetterAuthInfo(
projectRoot: string,
configPath?: string,
suppressLogs = false,
) {
try {
// Temporarily suppress console output if needed
const originalLog = console.log;
const originalWarn = console.warn;
const originalError = console.error;
if (suppressLogs) {
console.log = () => {};
console.warn = () => {};
console.error = () => {};
}
try {
const config = await getConfig({
cwd: projectRoot,
configPath,
shouldThrowOnError: false,
});
const packageInfo = await getPackageInfo();
return {
version: packageInfo.version || "Unknown",
config: sanitizeBetterAuthConfig(config),
};
} finally {
// Restore console methods
if (suppressLogs) {
console.log = originalLog;
console.warn = originalWarn;
console.error = originalError;
}
}
} catch (error) {
return {
version: "Unknown",
config: null,
error:
error instanceof Error
? error.message
: "Failed to load Better Auth config",
};
}
}
function formatOutput(data: any, indent = 0): string {
const spaces = " ".repeat(indent);
if (data === null || data === undefined) {
return `${spaces}${chalk.gray("N/A")}`;
}
if (
typeof data === "string" ||
typeof data === "number" ||
typeof data === "boolean"
) {
return `${spaces}${data}`;
}
if (Array.isArray(data)) {
if (data.length === 0) {
return `${spaces}${chalk.gray("[]")}`;
}
return data.map((item) => formatOutput(item, indent)).join("\n");
}
if (typeof data === "object") {
const entries = Object.entries(data);
if (entries.length === 0) {
return `${spaces}${chalk.gray("{}")}`;
}
return entries
.map(([key, value]) => {
if (
typeof value === "object" &&
value !== null &&
!Array.isArray(value)
) {
return `${spaces}${chalk.cyan(key)}:\n${formatOutput(value, indent + 2)}`;
}
return `${spaces}${chalk.cyan(key)}: ${formatOutput(value, 0)}`;
})
.join("\n");
}
return `${spaces}${JSON.stringify(data)}`;
}
export const info = new Command("info")
.description("Display system and Better Auth configuration information")
.option("--cwd <cwd>", "The working directory", process.cwd())
.option("--config <config>", "Path to the Better Auth configuration file")
.option("-j, --json", "Output as JSON")
.option("-c, --copy", "Copy output to clipboard (requires pbcopy/xclip)")
.action(async (options) => {
const projectRoot = path.resolve(options.cwd || process.cwd());
// Collect all information
const systemInfo = getSystemInfo();
const nodeInfo = getNodeInfo();
const packageManager = getPackageManager();
const frameworks = getFrameworkInfo(projectRoot);
const databases = getDatabaseInfo(projectRoot);
const betterAuthInfo = await getBetterAuthInfo(
projectRoot,
options.config,
options.json,
);
const fullInfo = {
system: systemInfo,
node: nodeInfo,
packageManager,
frameworks,
databases,
betterAuth: betterAuthInfo,
};
if (options.json) {
const jsonOutput = JSON.stringify(fullInfo, null, 2);
console.log(jsonOutput);
if (options.copy) {
try {
const platform = os.platform();
if (platform === "darwin") {
execSync("pbcopy", { input: jsonOutput });
console.log(chalk.green("\n✓ Copied to clipboard"));
} else if (platform === "linux") {
execSync("xclip -selection clipboard", { input: jsonOutput });
console.log(chalk.green("\n✓ Copied to clipboard"));
} else if (platform === "win32") {
execSync("clip", { input: jsonOutput });
console.log(chalk.green("\n✓ Copied to clipboard"));
}
} catch {
console.log(chalk.yellow("\n⚠ Could not copy to clipboard"));
}
}
return;
}
// Format and display output
console.log(chalk.bold("\n📊 Better Auth System Information\n"));
console.log(chalk.gray("=".repeat(50)));
console.log(chalk.bold.white("\n🖥 System Information:"));
console.log(formatOutput(systemInfo, 2));
console.log(chalk.bold.white("\n📦 Node.js:"));
console.log(formatOutput(nodeInfo, 2));
console.log(chalk.bold.white("\n📦 Package Manager:"));
console.log(formatOutput(packageManager, 2));
if (frameworks) {
console.log(chalk.bold.white("\n🚀 Frameworks:"));
console.log(formatOutput(frameworks, 2));
}
if (databases) {
console.log(chalk.bold.white("\n💾 Database Clients:"));
console.log(formatOutput(databases, 2));
}
console.log(chalk.bold.white("\n🔐 Better Auth:"));
if (betterAuthInfo.error) {
console.log(` ${chalk.red("Error:")} ${betterAuthInfo.error}`);
} else {
console.log(` ${chalk.cyan("Version")}: ${betterAuthInfo.version}`);
if (betterAuthInfo.config) {
console.log(` ${chalk.cyan("Configuration")}:`);
console.log(formatOutput(betterAuthInfo.config, 4));
}
}
console.log(chalk.gray("\n" + "=".repeat(50)));
console.log(chalk.gray("\n💡 Tip: Use --json flag for JSON output"));
console.log(chalk.gray("💡 Use --copy flag to copy output to clipboard"));
console.log(
chalk.gray("💡 When reporting issues, include this information\n"),
);
if (options.copy) {
const textOutput = `
Better Auth System Information
==============================
System Information:
${JSON.stringify(systemInfo, null, 2)}
Node.js:
${JSON.stringify(nodeInfo, null, 2)}
Package Manager:
${JSON.stringify(packageManager, null, 2)}
Frameworks:
${JSON.stringify(frameworks, null, 2)}
Database Clients:
${JSON.stringify(databases, null, 2)}
Better Auth:
${JSON.stringify(betterAuthInfo, null, 2)}
`;
try {
const platform = os.platform();
if (platform === "darwin") {
execSync("pbcopy", { input: textOutput });
console.log(chalk.green("✓ Copied to clipboard"));
} else if (platform === "linux") {
execSync("xclip -selection clipboard", { input: textOutput });
console.log(chalk.green("✓ Copied to clipboard"));
} else if (platform === "win32") {
execSync("clip", { input: textOutput });
console.log(chalk.green("✓ Copied to clipboard"));
}
} catch {
console.log(chalk.yellow("⚠ Could not copy to clipboard"));
}
}
});

View File

@@ -7,6 +7,7 @@ import { migrate } from "./commands/migrate";
import { generate } from "./commands/generate";
import { generateSecret } from "./commands/secret";
import { login } from "./commands/login";
import { info } from "./commands/info";
import { getPackageInfo } from "./utils/get-package-info";
import "dotenv/config";
@@ -29,6 +30,7 @@ async function main() {
.addCommand(migrate)
.addCommand(generate)
.addCommand(generateSecret)
.addCommand(info)
.addCommand(login)
.version(packageInfo.version || "1.1.2")
.description("Better Auth CLI")

View File

@@ -0,0 +1,354 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import fs from "node:fs/promises";
import path from "node:path";
import { exec } from "node:child_process";
import { promisify } from "node:util";
const execAsync = promisify(exec);
let tmpDir = ".";
describe("info command", () => {
beforeEach(async () => {
const tmp = path.join(
process.cwd(),
"node_modules",
".cache",
"info_test-",
);
tmpDir = await fs.mkdtemp(tmp);
// Mock console methods to avoid noise in test output
vi.spyOn(console, "log").mockImplementation(() => {});
vi.spyOn(console, "error").mockImplementation(() => {});
});
afterEach(async () => {
await fs.rm(tmpDir, { recursive: true });
vi.restoreAllMocks();
});
it("should display system information without auth config", async () => {
// Create a minimal package.json
await fs.writeFile(
path.join(tmpDir, "package.json"),
JSON.stringify({
name: "test-project",
version: "1.0.0",
dependencies: {
"better-auth": "^1.0.0",
},
}),
);
const cliPath = path.join(process.cwd(), "dist", "index.mjs");
const { stdout } = await execAsync(`node ${cliPath} info --json`, {
cwd: tmpDir,
});
const output = JSON.parse(stdout);
// Check system information
expect(output.system).toHaveProperty("platform");
expect(output.system).toHaveProperty("arch");
expect(output.system).toHaveProperty("cpuCount");
expect(output.system).toHaveProperty("totalMemory");
// Check node information
expect(output.node).toHaveProperty("version");
expect(output.node).toHaveProperty("env");
// Check package manager
expect(output.packageManager).toHaveProperty("name");
expect(output.packageManager).toHaveProperty("version");
// Better Auth config should have an error since no auth file exists
expect(output.betterAuth).toHaveProperty("version");
expect(output.betterAuth.config).toBeNull();
});
it("should load and sanitize auth configuration", async () => {
// Create package.json with dependencies
await fs.writeFile(
path.join(tmpDir, "package.json"),
JSON.stringify({
name: "test-project",
version: "1.0.0",
dependencies: {
"better-auth": "^1.0.0",
next: "^14.0.0",
react: "^18.0.0",
},
}),
);
// Create auth.ts with sensitive data - using in-memory database to avoid adapter errors
await fs.writeFile(
path.join(tmpDir, "auth.ts"),
`import { betterAuth } from "better-auth";
export const auth = betterAuth({
secret: "super-secret-key-123",
baseURL: "https://example.com",
emailAndPassword: {
enabled: true,
},
socialProviders: {
github: {
clientId: "github-client-id",
clientSecret: "github-client-secret"
},
google: {
clientId: "google-client-id",
clientSecret: "google-client-secret"
}
}
})`,
);
const cliPath = path.join(process.cwd(), "dist", "index.mjs");
const { stdout } = await execAsync(`node ${cliPath} info --json`, {
cwd: tmpDir,
});
const output = JSON.parse(stdout);
// Check that sensitive data is redacted
expect(output.betterAuth.config).toBeDefined();
expect(output.betterAuth.config.secret).toBe("[REDACTED]");
// Check social providers are sanitized
expect(output.betterAuth.config.socialProviders).toBeDefined();
expect(output.betterAuth.config.socialProviders.github.clientId).toBe(
"[REDACTED]",
);
expect(output.betterAuth.config.socialProviders.github.clientSecret).toBe(
"[REDACTED]",
);
expect(output.betterAuth.config.socialProviders.google.clientId).toBe(
"[REDACTED]",
);
expect(output.betterAuth.config.socialProviders.google.clientSecret).toBe(
"[REDACTED]",
);
// Check non-sensitive data is preserved
expect(output.betterAuth.config.emailAndPassword).toEqual({
enabled: true,
});
expect(output.betterAuth.config.baseURL).toBe("https://example.com");
});
it("should detect installed frameworks", async () => {
// Create package.json with various frameworks
await fs.writeFile(
path.join(tmpDir, "package.json"),
JSON.stringify({
name: "test-project",
version: "1.0.0",
dependencies: {
"better-auth": "^1.0.0",
next: "^14.0.0",
react: "^18.0.0",
},
devDependencies: {
"@sveltejs/kit": "^2.0.0",
svelte: "^4.0.0",
},
}),
);
const cliPath = path.join(process.cwd(), "dist", "index.mjs");
const { stdout } = await execAsync(`node ${cliPath} info --json`, {
cwd: tmpDir,
});
const output = JSON.parse(stdout);
// Check frameworks are detected
expect(output.frameworks).toContainEqual({
name: "next",
version: "^14.0.0",
});
expect(output.frameworks).toContainEqual({
name: "react",
version: "^18.0.0",
});
expect(output.frameworks).toContainEqual({
name: "@sveltejs/kit",
version: "^2.0.0",
});
expect(output.frameworks).toContainEqual({
name: "svelte",
version: "^4.0.0",
});
});
it("should detect database clients", async () => {
// Create package.json with database clients
await fs.writeFile(
path.join(tmpDir, "package.json"),
JSON.stringify({
name: "test-project",
version: "1.0.0",
dependencies: {
"better-auth": "^1.0.0",
"@prisma/client": "^5.0.0",
kysely: "^0.26.0",
},
devDependencies: {
"drizzle-orm": "^0.29.0",
"better-sqlite3": "^9.0.0",
},
}),
);
const cliPath = path.join(process.cwd(), "dist", "index.mjs");
const { stdout } = await execAsync(`node ${cliPath} info --json`, {
cwd: tmpDir,
});
const output = JSON.parse(stdout);
// Check database clients are detected
expect(output.databases).toContainEqual({
name: "@prisma/client",
version: "^5.0.0",
});
expect(output.databases).toContainEqual({
name: "kysely",
version: "^0.26.0",
});
expect(output.databases).toContainEqual({
name: "drizzle",
version: "^0.29.0",
});
expect(output.databases).toContainEqual({
name: "better-sqlite3",
version: "^9.0.0",
});
});
it("should support custom config path", async () => {
// Create package.json
await fs.writeFile(
path.join(tmpDir, "package.json"),
JSON.stringify({
name: "test-project",
version: "1.0.0",
dependencies: {
"better-auth": "^1.0.0",
},
}),
);
// Create custom directory for auth config
const customPath = path.join(tmpDir, "config");
await fs.mkdir(customPath, { recursive: true });
// Create auth config in custom location
await fs.writeFile(
path.join(customPath, "auth.config.ts"),
`import { betterAuth } from "better-auth";
export const auth = betterAuth({
secret: "my-secret",
appName: "Custom Config App",
emailAndPassword: {
enabled: true,
}
})`,
);
const cliPath = path.join(process.cwd(), "dist", "index.mjs");
const { stdout } = await execAsync(
`node ${cliPath} info --config config/auth.config.ts --json`,
{ cwd: tmpDir },
);
const output = JSON.parse(stdout);
// Check that custom config was loaded
expect(output.betterAuth.config).toBeDefined();
expect(output.betterAuth.config.appName).toBe("Custom Config App");
expect(output.betterAuth.config.secret).toBe("[REDACTED]");
expect(output.betterAuth.config.emailAndPassword).toEqual({
enabled: true,
});
});
it("should sanitize plugin configurations", async () => {
// Create package.json
await fs.writeFile(
path.join(tmpDir, "package.json"),
JSON.stringify({
name: "test-project",
version: "1.0.0",
dependencies: {
"better-auth": "^1.0.0",
},
}),
);
// Create auth.ts with plugins
await fs.writeFile(
path.join(tmpDir, "auth.ts"),
`import { betterAuth } from "better-auth";
import { twoFactor, organization } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
twoFactor({
otpOptions: {
secret: "otp-secret-key"
}
}),
organization({
apiKey: "org-api-key",
webhookSecret: "webhook-secret"
})
]
})`,
);
const cliPath = path.join(process.cwd(), "dist", "index.mjs");
const { stdout } = await execAsync(`node ${cliPath} info --json`, {
cwd: tmpDir,
});
const output = JSON.parse(stdout);
// Check that plugin configs are sanitized
expect(output.betterAuth.config.plugins).toBeDefined();
expect(Array.isArray(output.betterAuth.config.plugins)).toBe(true);
// Plugin sensitive data should be redacted
const plugins = output.betterAuth.config.plugins;
plugins.forEach((plugin: any) => {
if (plugin.config) {
// Check that sensitive keys are redacted
const configStr = JSON.stringify(plugin.config);
expect(configStr).toContain("[REDACTED]");
}
});
});
it("should handle missing package.json gracefully", async () => {
// Don't create package.json
const cliPath = path.join(process.cwd(), "dist", "index.mjs");
const { stdout } = await execAsync(`node ${cliPath} info --json`, {
cwd: tmpDir,
});
const output = JSON.parse(stdout);
// Should still return system info
expect(output.system).toBeDefined();
expect(output.node).toBeDefined();
expect(output.packageManager).toBeDefined();
// Frameworks and databases should be null
expect(output.frameworks).toBeNull();
expect(output.databases).toBeNull();
});
}, 20000);