mirror of
https://github.com/better-auth/better-auth.git
synced 2026-05-23 23:52:05 -05:00
feat(cli): add info script (#4143)
This commit is contained in:
@@ -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, it’s 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 you’re 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 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:
|
||||
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.
|
||||
539
packages/cli/src/commands/info.ts
Normal file
539
packages/cli/src/commands/info.ts
Normal 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"));
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -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")
|
||||
|
||||
354
packages/cli/test/info.test.ts
Normal file
354
packages/cli/test/info.test.ts
Normal 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);
|
||||
Reference in New Issue
Block a user