Files
create-better-t-stack/apps/cli/src/helpers/neon-setup.ts
2025-04-28 04:14:23 +05:30

187 lines
4.9 KiB
TypeScript

import path from "node:path";
import { cancel, isCancel, log, spinner, text } from "@clack/prompts";
import { consola } from "consola";
import { execa } from "execa";
import fs from "fs-extra";
import pc from "picocolors";
import type { ProjectPackageManager } from "../types";
import { getPackageExecutionCommand } from "../utils/get-package-execution-command";
type NeonConfig = {
connectionString: string;
projectId: string;
dbName: string;
roleName: string;
};
async function executeNeonCommand(
packageManager: ProjectPackageManager,
commandArgsString: string,
spinnerText?: string,
) {
const s = spinner();
try {
const fullCommand = getPackageExecutionCommand(
packageManager,
commandArgsString,
);
if (s) s.start(spinnerText);
const result = await execa(fullCommand, { shell: true });
if (s) s.stop(spinnerText);
return result;
} catch (error) {
if (s) s.stop(pc.red(`Failed: ${spinnerText}`));
throw error;
}
}
async function isNeonAuthenticated(packageManager: ProjectPackageManager) {
try {
const commandArgsString = "neonctl projects list";
const result = await executeNeonCommand(packageManager, commandArgsString);
return (
!result.stdout.includes("not authenticated") &&
!result.stdout.includes("error")
);
} catch {
return false;
}
}
async function authenticateWithNeon(packageManager: ProjectPackageManager) {
try {
await executeNeonCommand(
packageManager,
"neonctl auth",
"Authenticating with Neon...",
);
log.success("Authenticated with Neon successfully!");
return true;
} catch (error) {
consola.error(pc.red("Failed to authenticate with Neon"));
}
}
async function createNeonProject(
projectName: string,
packageManager: ProjectPackageManager,
) {
try {
const commandArgsString = `neonctl projects create --name "${projectName}" --output json`;
const { stdout } = await executeNeonCommand(
packageManager,
commandArgsString,
`Creating Neon project "${projectName}"...`,
);
const response = JSON.parse(stdout);
if (
response.project &&
response.connection_uris &&
response.connection_uris.length > 0
) {
const projectId = response.project.id;
const connectionUri = response.connection_uris[0].connection_uri;
const params = response.connection_uris[0].connection_parameters;
return {
connectionString: connectionUri,
projectId: projectId,
dbName: params.database,
roleName: params.role,
};
}
consola.error(
pc.red("Failed to extract connection information from response"),
);
return null;
} catch (error) {
consola.error(pc.red("Failed to create Neon project"));
}
}
async function writeEnvFile(projectDir: string, config?: NeonConfig) {
const envPath = path.join(projectDir, "apps/server", ".env");
const envContent = config
? `DATABASE_URL="${config.connectionString}"`
: `DATABASE_URL="postgresql://postgres:postgres@localhost:5432/mydb?schema=public"`;
await fs.ensureDir(path.dirname(envPath));
await fs.writeFile(envPath, envContent);
return true;
}
function displayManualSetupInstructions() {
log.info(`Manual Neon PostgreSQL Setup Instructions:
1. Visit https://neon.tech and create an account
2. Create a new project from the dashboard
3. Get your connection string
4. Add the database URL to the .env file in apps/server/.env
DATABASE_URL="your_connection_string"`);
}
import type { ProjectConfig } from "../types";
export async function setupNeonPostgres(config: ProjectConfig): Promise<void> {
const { projectName, packageManager } = config;
const projectDir = path.resolve(process.cwd(), projectName);
const setupSpinner = spinner();
setupSpinner.start("Setting up Neon PostgreSQL");
try {
const isAuthenticated = await isNeonAuthenticated(packageManager);
setupSpinner.stop("Setting up Neon PostgreSQL");
if (!isAuthenticated) {
log.info("Please authenticate with Neon to continue:");
await authenticateWithNeon(packageManager);
}
const suggestedProjectName = path.basename(projectDir);
const projectName = await text({
message: "Enter a name for your Neon project:",
defaultValue: suggestedProjectName,
initialValue: suggestedProjectName,
});
if (isCancel(projectName)) {
cancel(pc.red("Operation cancelled"));
process.exit(0);
}
const config = await createNeonProject(
projectName as string,
packageManager,
);
if (!config) {
throw new Error(
"Failed to create project - couldn't get connection information",
);
}
const finalSpinner = spinner();
finalSpinner.start("Configuring database connection");
await fs.ensureDir(path.join(projectDir, "apps/server"));
await writeEnvFile(projectDir, config);
finalSpinner.stop("Neon database configured successfully!");
} catch (error) {
setupSpinner.stop(pc.red("Neon PostgreSQL setup failed"));
if (error instanceof Error) {
consola.error(pc.red(error.message));
}
await writeEnvFile(projectDir);
displayManualSetupInstructions();
}
}