mirror of
https://github.com/FranP-code/create-better-t-stack.git
synced 2025-10-12 23:52:15 +00:00
cli: organize file structure
This commit is contained in:
167
apps/cli/src/helpers/database-providers/mongodb-atlas-setup.ts
Normal file
167
apps/cli/src/helpers/database-providers/mongodb-atlas-setup.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
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 { ProjectConfig } from "../../types";
|
||||
import { commandExists } from "../../utils/command-exists";
|
||||
import {
|
||||
type EnvVariable,
|
||||
addEnvVariablesToFile,
|
||||
} from "../project-generation/env-setup";
|
||||
|
||||
type MongoDBConfig = {
|
||||
connectionString: string;
|
||||
};
|
||||
|
||||
async function checkAtlasCLI(): Promise<boolean> {
|
||||
const s = spinner();
|
||||
s.start("Checking for MongoDB Atlas CLI");
|
||||
|
||||
try {
|
||||
const exists = await commandExists("atlas");
|
||||
s.stop(
|
||||
exists
|
||||
? "MongoDB Atlas CLI found"
|
||||
: pc.yellow("MongoDB Atlas CLI not found"),
|
||||
);
|
||||
return exists;
|
||||
} catch (_error) {
|
||||
s.stop(pc.red("Error checking for MongoDB Atlas CLI"));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function initMongoDBAtlas(
|
||||
serverDir: string,
|
||||
): Promise<MongoDBConfig | null> {
|
||||
try {
|
||||
const hasAtlas = await checkAtlasCLI();
|
||||
|
||||
if (!hasAtlas) {
|
||||
consola.error(pc.red("MongoDB Atlas CLI not found."));
|
||||
log.info(
|
||||
pc.yellow(
|
||||
"Please install it from: https://www.mongodb.com/docs/atlas/cli/current/install-atlas-cli/",
|
||||
),
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
log.info(pc.blue("Running MongoDB Atlas setup..."));
|
||||
|
||||
await execa("atlas", ["deployments", "setup"], {
|
||||
cwd: serverDir,
|
||||
stdio: "inherit",
|
||||
});
|
||||
|
||||
log.info(pc.green("Atlas setup complete!"));
|
||||
|
||||
const connectionString = await text({
|
||||
message: "Enter your MongoDB connection string:",
|
||||
placeholder:
|
||||
"mongodb+srv://username:password@cluster.mongodb.net/database",
|
||||
validate(value) {
|
||||
if (!value) return "Please enter a connection string";
|
||||
if (!value.startsWith("mongodb")) {
|
||||
return "URL should start with mongodb:// or mongodb+srv://";
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
if (isCancel(connectionString)) {
|
||||
cancel("MongoDB setup cancelled");
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
connectionString: connectionString as string,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
consola.error(pc.red(error.message));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function writeEnvFile(projectDir: string, config?: MongoDBConfig) {
|
||||
try {
|
||||
const envPath = path.join(projectDir, "apps/server", ".env");
|
||||
const variables: EnvVariable[] = [
|
||||
{
|
||||
key: "DATABASE_URL",
|
||||
value: config?.connectionString ?? "mongodb://localhost:27017/mydb",
|
||||
condition: true,
|
||||
},
|
||||
];
|
||||
await addEnvVariablesToFile(envPath, variables);
|
||||
} catch (_error) {
|
||||
consola.error("Failed to update environment configuration");
|
||||
}
|
||||
}
|
||||
|
||||
function displayManualSetupInstructions() {
|
||||
log.info(`
|
||||
${pc.green("MongoDB Atlas Manual Setup Instructions:")}
|
||||
|
||||
1. Install Atlas CLI:
|
||||
${pc.blue(
|
||||
"https://www.mongodb.com/docs/atlas/cli/stable/install-atlas-cli/",
|
||||
)}
|
||||
|
||||
2. Run the following command and follow the prompts:
|
||||
${pc.blue("atlas deployments setup")}
|
||||
|
||||
3. Get your connection string from the Atlas dashboard:
|
||||
Format: ${pc.dim(
|
||||
"mongodb+srv://USERNAME:PASSWORD@CLUSTER.mongodb.net/DATABASE_NAME",
|
||||
)}
|
||||
|
||||
4. Add the connection string to your .env file:
|
||||
${pc.dim('DATABASE_URL="your_connection_string"')}
|
||||
`);
|
||||
}
|
||||
|
||||
export async function setupMongoDBAtlas(config: ProjectConfig) {
|
||||
const { projectDir } = config;
|
||||
const mainSpinner = spinner();
|
||||
mainSpinner.start("Setting up MongoDB Atlas");
|
||||
|
||||
const serverDir = path.join(projectDir, "apps/server");
|
||||
try {
|
||||
await fs.ensureDir(serverDir);
|
||||
|
||||
mainSpinner.stop("Starting MongoDB Atlas setup");
|
||||
|
||||
const config = await initMongoDBAtlas(serverDir);
|
||||
|
||||
if (config) {
|
||||
await writeEnvFile(projectDir, config);
|
||||
log.success(
|
||||
pc.green(
|
||||
"MongoDB Atlas setup complete! Connection saved to .env file.",
|
||||
),
|
||||
);
|
||||
} else {
|
||||
log.warn(pc.yellow("Falling back to local MongoDB configuration"));
|
||||
await writeEnvFile(projectDir);
|
||||
displayManualSetupInstructions();
|
||||
}
|
||||
} catch (error) {
|
||||
mainSpinner.stop(pc.red("MongoDB Atlas setup failed"));
|
||||
consola.error(
|
||||
pc.red(
|
||||
`Error during MongoDB Atlas setup: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
),
|
||||
);
|
||||
|
||||
try {
|
||||
await writeEnvFile(projectDir);
|
||||
displayManualSetupInstructions();
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
216
apps/cli/src/helpers/database-providers/neon-setup.ts
Normal file
216
apps/cli/src/helpers/database-providers/neon-setup.ts
Normal file
@@ -0,0 +1,216 @@
|
||||
import path from "node:path";
|
||||
import { cancel, isCancel, log, select, spinner, text } from "@clack/prompts";
|
||||
import { consola } from "consola";
|
||||
import { execa } from "execa";
|
||||
import fs from "fs-extra";
|
||||
import pc from "picocolors";
|
||||
import type { PackageManager } from "../../types";
|
||||
import { getPackageExecutionCommand } from "../../utils/get-package-execution-command";
|
||||
import {
|
||||
type EnvVariable,
|
||||
addEnvVariablesToFile,
|
||||
} from "../project-generation/env-setup";
|
||||
|
||||
type NeonConfig = {
|
||||
connectionString: string;
|
||||
projectId: string;
|
||||
dbName: string;
|
||||
roleName: string;
|
||||
};
|
||||
|
||||
type NeonRegion = {
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
const NEON_REGIONS: NeonRegion[] = [
|
||||
{ label: "AWS US East (N. Virginia)", value: "aws-us-east-1" },
|
||||
{ label: "AWS US East (Ohio)", value: "aws-us-east-2" },
|
||||
{ label: "AWS US West (Oregon)", value: "aws-us-west-2" },
|
||||
{ label: "AWS Europe (Frankfurt)", value: "aws-eu-central-1" },
|
||||
{ label: "AWS Asia Pacific (Singapore)", value: "aws-ap-southeast-1" },
|
||||
{ label: "AWS Asia Pacific (Sydney)", value: "aws-ap-southeast-2" },
|
||||
{ label: "Azure East US 2 region (Virginia)", value: "azure-eastus2" },
|
||||
];
|
||||
|
||||
async function executeNeonCommand(
|
||||
packageManager: PackageManager,
|
||||
commandArgsString: string,
|
||||
spinnerText?: string,
|
||||
) {
|
||||
const s = spinner();
|
||||
try {
|
||||
const fullCommand = getPackageExecutionCommand(
|
||||
packageManager,
|
||||
commandArgsString,
|
||||
);
|
||||
|
||||
if (spinnerText) s.start(spinnerText);
|
||||
const result = await execa(fullCommand, { shell: true });
|
||||
if (spinnerText) s.stop(pc.green("Completed"));
|
||||
return result;
|
||||
} catch (error) {
|
||||
if (s) s.stop(pc.red(`Failed: ${spinnerText}`));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function isNeonAuthenticated(packageManager: PackageManager) {
|
||||
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: PackageManager) {
|
||||
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,
|
||||
regionId: string,
|
||||
packageManager: PackageManager,
|
||||
) {
|
||||
try {
|
||||
const commandArgsString = `neonctl projects create --name ${projectName} --region-id ${regionId} --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 variables: EnvVariable[] = [
|
||||
{
|
||||
key: "DATABASE_URL",
|
||||
value:
|
||||
config?.connectionString ??
|
||||
"postgresql://postgres:postgres@localhost:5432/mydb?schema=public",
|
||||
condition: true,
|
||||
},
|
||||
];
|
||||
await addEnvVariablesToFile(envPath, variables);
|
||||
|
||||
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 { packageManager, projectDir } = config;
|
||||
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,
|
||||
});
|
||||
|
||||
const regionId = await select({
|
||||
message: "Select a region for your Neon project:",
|
||||
options: NEON_REGIONS,
|
||||
initialValue: NEON_REGIONS[0].value,
|
||||
});
|
||||
|
||||
if (isCancel(projectName) || isCancel(regionId)) {
|
||||
cancel(pc.red("Operation cancelled"));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const config = await createNeonProject(
|
||||
projectName as string,
|
||||
regionId,
|
||||
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();
|
||||
}
|
||||
}
|
||||
197
apps/cli/src/helpers/database-providers/prisma-postgres-setup.ts
Normal file
197
apps/cli/src/helpers/database-providers/prisma-postgres-setup.ts
Normal file
@@ -0,0 +1,197 @@
|
||||
import path from "node:path";
|
||||
import { cancel, isCancel, log, password, spinner } from "@clack/prompts";
|
||||
import { consola } from "consola";
|
||||
import { execa } from "execa";
|
||||
import fs from "fs-extra";
|
||||
import pc from "picocolors";
|
||||
import type { PackageManager } from "../../types";
|
||||
import { addPackageDependency } from "../../utils/add-package-deps";
|
||||
import { getPackageExecutionCommand } from "../../utils/get-package-execution-command";
|
||||
import {
|
||||
type EnvVariable,
|
||||
addEnvVariablesToFile,
|
||||
} from "../project-generation/env-setup";
|
||||
|
||||
type PrismaConfig = {
|
||||
databaseUrl: string;
|
||||
};
|
||||
|
||||
async function initPrismaDatabase(
|
||||
serverDir: string,
|
||||
packageManager: PackageManager,
|
||||
): Promise<PrismaConfig | null> {
|
||||
const s = spinner();
|
||||
try {
|
||||
s.start("Initializing Prisma PostgreSQL");
|
||||
|
||||
const prismaDir = path.join(serverDir, "prisma");
|
||||
await fs.ensureDir(prismaDir);
|
||||
|
||||
s.stop("Initializing Prisma. Follow the prompts below:");
|
||||
|
||||
const prismaInitCommand = getPackageExecutionCommand(
|
||||
packageManager,
|
||||
"prisma init --db",
|
||||
);
|
||||
|
||||
await execa(prismaInitCommand, {
|
||||
cwd: serverDir,
|
||||
stdio: "inherit",
|
||||
shell: true,
|
||||
});
|
||||
|
||||
log.info(
|
||||
pc.yellow(
|
||||
"Please copy the Prisma Postgres URL from the output above.\nIt looks like: prisma+postgres://accelerate.prisma-data.net/?api_key=...",
|
||||
),
|
||||
);
|
||||
|
||||
const databaseUrl = await password({
|
||||
message: "Paste your Prisma Postgres database URL:",
|
||||
validate(value) {
|
||||
if (!value) return "Please enter a database URL";
|
||||
if (!value.startsWith("prisma+postgres://")) {
|
||||
return "URL should start with prisma+postgres://";
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
if (isCancel(databaseUrl)) {
|
||||
cancel("Database setup cancelled");
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
databaseUrl: databaseUrl as string,
|
||||
};
|
||||
} catch (error) {
|
||||
s.stop(pc.red("Failed to initialize Prisma PostgreSQL"));
|
||||
if (error instanceof Error) {
|
||||
consola.error(error.message);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function writeEnvFile(projectDir: string, config?: PrismaConfig) {
|
||||
try {
|
||||
const envPath = path.join(projectDir, "apps/server", ".env");
|
||||
const variables: EnvVariable[] = [
|
||||
{
|
||||
key: "DATABASE_URL",
|
||||
value:
|
||||
config?.databaseUrl ??
|
||||
"postgresql://postgres:postgres@localhost:5432/mydb?schema=public",
|
||||
condition: true,
|
||||
},
|
||||
];
|
||||
await addEnvVariablesToFile(envPath, variables);
|
||||
} catch (_error) {
|
||||
consola.error("Failed to update environment configuration");
|
||||
}
|
||||
}
|
||||
|
||||
function displayManualSetupInstructions() {
|
||||
log.info(`Manual Prisma PostgreSQL Setup Instructions:
|
||||
|
||||
1. Visit https://console.prisma.io and create an account
|
||||
2. Create a new PostgreSQL database from the dashboard
|
||||
3. Get your database URL
|
||||
4. Add the database URL to the .env file in apps/server/.env
|
||||
|
||||
DATABASE_URL="your_database_url"`);
|
||||
}
|
||||
|
||||
async function addPrismaAccelerateExtension(serverDir: string) {
|
||||
try {
|
||||
await addPackageDependency({
|
||||
dependencies: ["@prisma/extension-accelerate"],
|
||||
projectDir: serverDir,
|
||||
});
|
||||
|
||||
const prismaIndexPath = path.join(serverDir, "prisma/index.ts");
|
||||
const prismaIndexContent = `
|
||||
import { PrismaClient } from "./generated/client";
|
||||
import { withAccelerate } from "@prisma/extension-accelerate";
|
||||
|
||||
const prisma = new PrismaClient().$extends(withAccelerate());
|
||||
|
||||
export default prisma;
|
||||
`;
|
||||
await fs.writeFile(prismaIndexPath, prismaIndexContent.trim());
|
||||
|
||||
const dbFilePath = path.join(serverDir, "src/db/index.ts");
|
||||
if (await fs.pathExists(dbFilePath)) {
|
||||
let dbFileContent = await fs.readFile(dbFilePath, "utf8");
|
||||
|
||||
if (!dbFileContent.includes("@prisma/extension-accelerate")) {
|
||||
dbFileContent = `import { withAccelerate } from "@prisma/extension-accelerate";\n${dbFileContent}`;
|
||||
|
||||
dbFileContent = dbFileContent.replace(
|
||||
"export const db = new PrismaClient();",
|
||||
"export const db = new PrismaClient().$extends(withAccelerate());",
|
||||
);
|
||||
|
||||
await fs.writeFile(dbFilePath, dbFileContent);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} catch (_error) {
|
||||
log.warn(
|
||||
pc.yellow("Could not add Prisma Accelerate extension automatically"),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
import type { ProjectConfig } from "../../types";
|
||||
|
||||
export async function setupPrismaPostgres(config: ProjectConfig) {
|
||||
const { packageManager, projectDir } = config;
|
||||
const serverDir = path.join(projectDir, "apps/server");
|
||||
const s = spinner();
|
||||
s.start("Setting up Prisma PostgreSQL");
|
||||
|
||||
try {
|
||||
await fs.ensureDir(serverDir);
|
||||
|
||||
s.stop("Starting Prisma setup");
|
||||
|
||||
const config = await initPrismaDatabase(serverDir, packageManager);
|
||||
|
||||
if (config) {
|
||||
await writeEnvFile(projectDir, config);
|
||||
await addPrismaAccelerateExtension(serverDir);
|
||||
log.success(
|
||||
pc.green("Prisma PostgreSQL database configured successfully!"),
|
||||
);
|
||||
log.info(
|
||||
pc.cyan(
|
||||
'NOTE: Make sure to uncomment `import "dotenv/config";` in `apps/server/src/prisma.config.ts` to load environment variables.',
|
||||
),
|
||||
);
|
||||
} else {
|
||||
const fallbackSpinner = spinner();
|
||||
fallbackSpinner.start("Setting up fallback configuration");
|
||||
await writeEnvFile(projectDir);
|
||||
fallbackSpinner.stop("Manual setup required");
|
||||
displayManualSetupInstructions();
|
||||
}
|
||||
} catch (error) {
|
||||
s.stop(pc.red("Prisma PostgreSQL setup failed"));
|
||||
consola.error(
|
||||
pc.red(
|
||||
`Error during Prisma PostgreSQL setup: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
),
|
||||
);
|
||||
|
||||
try {
|
||||
await writeEnvFile(projectDir);
|
||||
displayManualSetupInstructions();
|
||||
} catch {}
|
||||
|
||||
log.info("Setup completed with manual configuration required.");
|
||||
}
|
||||
}
|
||||
215
apps/cli/src/helpers/database-providers/supabase-setup.ts
Normal file
215
apps/cli/src/helpers/database-providers/supabase-setup.ts
Normal file
@@ -0,0 +1,215 @@
|
||||
import path from "node:path";
|
||||
import { log } from "@clack/prompts";
|
||||
import { consola } from "consola";
|
||||
import { type ExecaError, execa } from "execa";
|
||||
import fs from "fs-extra";
|
||||
import pc from "picocolors";
|
||||
import type { PackageManager, ProjectConfig } from "../../types";
|
||||
import { getPackageExecutionCommand } from "../../utils/get-package-execution-command";
|
||||
import {
|
||||
type EnvVariable,
|
||||
addEnvVariablesToFile,
|
||||
} from "../project-generation/env-setup";
|
||||
|
||||
async function writeSupabaseEnvFile(
|
||||
projectDir: string,
|
||||
databaseUrl: string,
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const envPath = path.join(projectDir, "apps/server", ".env");
|
||||
const dbUrlToUse =
|
||||
databaseUrl || "postgresql://postgres:postgres@127.0.0.1:54322/postgres";
|
||||
const variables: EnvVariable[] = [
|
||||
{
|
||||
key: "DATABASE_URL",
|
||||
value: dbUrlToUse,
|
||||
condition: true,
|
||||
},
|
||||
{
|
||||
key: "DIRECT_URL",
|
||||
value: dbUrlToUse,
|
||||
condition: true,
|
||||
},
|
||||
];
|
||||
await addEnvVariablesToFile(envPath, variables);
|
||||
return true;
|
||||
} catch (error) {
|
||||
consola.error(pc.red("Failed to update .env file for Supabase."));
|
||||
if (error instanceof Error) {
|
||||
consola.error(error.message);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function extractDbUrl(output: string): string | null {
|
||||
const dbUrlMatch = output.match(/DB URL:\s*(postgresql:\/\/[^\s]+)/);
|
||||
const url = dbUrlMatch?.[1];
|
||||
if (url) {
|
||||
return url;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function initializeSupabase(
|
||||
serverDir: string,
|
||||
packageManager: PackageManager,
|
||||
): Promise<boolean> {
|
||||
log.info("Initializing Supabase project...");
|
||||
try {
|
||||
const supabaseInitCommand = getPackageExecutionCommand(
|
||||
packageManager,
|
||||
"supabase init",
|
||||
);
|
||||
await execa(supabaseInitCommand, {
|
||||
cwd: serverDir,
|
||||
stdio: "inherit",
|
||||
shell: true,
|
||||
});
|
||||
log.success("Supabase project initialized successfully.");
|
||||
return true;
|
||||
} catch (error) {
|
||||
consola.error(pc.red("Failed to initialize Supabase project."));
|
||||
if (error instanceof Error) {
|
||||
consola.error(error.message);
|
||||
} else {
|
||||
consola.error(String(error));
|
||||
}
|
||||
if (error instanceof Error && error.message.includes("ENOENT")) {
|
||||
log.error(
|
||||
pc.red(
|
||||
"Supabase CLI not found. Please install it globally or ensure it's in your PATH.",
|
||||
),
|
||||
);
|
||||
log.info("You can install it using: npm install -g supabase");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function startSupabase(
|
||||
serverDir: string,
|
||||
packageManager: PackageManager,
|
||||
): Promise<string | null> {
|
||||
log.info("Starting Supabase services (this may take a moment)...");
|
||||
const supabaseStartCommand = getPackageExecutionCommand(
|
||||
packageManager,
|
||||
"supabase start",
|
||||
);
|
||||
try {
|
||||
const subprocess = execa(supabaseStartCommand, {
|
||||
cwd: serverDir,
|
||||
shell: true,
|
||||
});
|
||||
|
||||
let stdoutData = "";
|
||||
|
||||
if (subprocess.stdout) {
|
||||
subprocess.stdout.on("data", (data) => {
|
||||
const text = data.toString();
|
||||
process.stdout.write(text);
|
||||
stdoutData += text;
|
||||
});
|
||||
}
|
||||
|
||||
if (subprocess.stderr) {
|
||||
subprocess.stderr.pipe(process.stderr);
|
||||
}
|
||||
|
||||
await subprocess;
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
return stdoutData;
|
||||
} catch (error) {
|
||||
consola.error(pc.red("Failed to start Supabase services."));
|
||||
const execaError = error as ExecaError;
|
||||
if (execaError?.message) {
|
||||
consola.error(`Error details: ${execaError.message}`);
|
||||
if (execaError.message.includes("Docker is not running")) {
|
||||
log.error(
|
||||
pc.red("Docker is not running. Please start Docker and try again."),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
consola.error(String(error));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function displayManualSupabaseInstructions(output?: string | null) {
|
||||
log.info(
|
||||
`"Manual Supabase Setup Instructions:"
|
||||
1. Ensure Docker is installed and running.
|
||||
2. Install the Supabase CLI (e.g., \`npm install -g supabase\`).
|
||||
3. Run \`supabase init\` in your project's \`apps/server\` directory.
|
||||
4. Run \`supabase start\` in your project's \`apps/server\` directory.
|
||||
5. Copy the 'DB URL' from the output.${
|
||||
output
|
||||
? `
|
||||
${pc.bold("Relevant output from `supabase start`:")}
|
||||
${pc.dim(output)}`
|
||||
: ""
|
||||
}
|
||||
6. Add the DB URL to the .env file in \`apps/server/.env\` as \`DATABASE_URL\`:
|
||||
${pc.gray('DATABASE_URL="your_supabase_db_url"')}`,
|
||||
);
|
||||
}
|
||||
|
||||
export async function setupSupabase(config: ProjectConfig) {
|
||||
const { projectDir, packageManager } = config;
|
||||
|
||||
const serverDir = path.join(projectDir, "apps", "server");
|
||||
|
||||
try {
|
||||
await fs.ensureDir(serverDir);
|
||||
|
||||
const initialized = await initializeSupabase(serverDir, packageManager);
|
||||
if (!initialized) {
|
||||
displayManualSupabaseInstructions();
|
||||
return;
|
||||
}
|
||||
|
||||
const supabaseOutput = await startSupabase(serverDir, packageManager);
|
||||
if (!supabaseOutput) {
|
||||
displayManualSupabaseInstructions();
|
||||
return;
|
||||
}
|
||||
|
||||
const dbUrl = extractDbUrl(supabaseOutput);
|
||||
|
||||
if (dbUrl) {
|
||||
const envUpdated = await writeSupabaseEnvFile(projectDir, dbUrl);
|
||||
|
||||
if (envUpdated) {
|
||||
log.success(pc.green("Supabase local development setup complete!"));
|
||||
} else {
|
||||
log.error(
|
||||
pc.red(
|
||||
"Supabase setup completed, but failed to update .env automatically.",
|
||||
),
|
||||
);
|
||||
displayManualSupabaseInstructions(supabaseOutput);
|
||||
}
|
||||
} else {
|
||||
log.error(
|
||||
pc.yellow(
|
||||
"Supabase started, but could not extract DB URL automatically.",
|
||||
),
|
||||
);
|
||||
displayManualSupabaseInstructions(supabaseOutput);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
consola.error(pc.red(`Error during Supabase setup: ${error.message}`));
|
||||
} else {
|
||||
consola.error(
|
||||
pc.red(
|
||||
`An unknown error occurred during Supabase setup: ${String(error)}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
displayManualSupabaseInstructions();
|
||||
}
|
||||
}
|
||||
309
apps/cli/src/helpers/database-providers/turso-setup.ts
Normal file
309
apps/cli/src/helpers/database-providers/turso-setup.ts
Normal file
@@ -0,0 +1,309 @@
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import {
|
||||
cancel,
|
||||
confirm,
|
||||
isCancel,
|
||||
log,
|
||||
select,
|
||||
spinner,
|
||||
text,
|
||||
} from "@clack/prompts";
|
||||
import consola from "consola";
|
||||
import { $ } from "execa";
|
||||
import pc from "picocolors";
|
||||
import type { ProjectConfig } from "../../types";
|
||||
import { commandExists } from "../../utils/command-exists";
|
||||
import {
|
||||
type EnvVariable,
|
||||
addEnvVariablesToFile,
|
||||
} from "../project-generation/env-setup";
|
||||
|
||||
type TursoConfig = {
|
||||
dbUrl: string;
|
||||
authToken: string;
|
||||
};
|
||||
|
||||
type TursoGroup = {
|
||||
name: string;
|
||||
locations: string;
|
||||
version: string;
|
||||
status: string;
|
||||
};
|
||||
|
||||
async function isTursoInstalled() {
|
||||
return commandExists("turso");
|
||||
}
|
||||
|
||||
async function isTursoLoggedIn() {
|
||||
try {
|
||||
const output = await $`turso auth whoami`;
|
||||
return !output.stdout.includes("You are not logged in");
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loginToTurso() {
|
||||
const s = spinner();
|
||||
try {
|
||||
s.start("Logging in to Turso...");
|
||||
await $`turso auth login`;
|
||||
s.stop("Logged in to Turso successfully!");
|
||||
return true;
|
||||
} catch (_error) {
|
||||
s.stop(pc.red("Failed to log in to Turso"));
|
||||
}
|
||||
}
|
||||
|
||||
async function installTursoCLI(isMac: boolean) {
|
||||
const s = spinner();
|
||||
try {
|
||||
s.start("Installing Turso CLI...");
|
||||
|
||||
if (isMac) {
|
||||
await $`brew install tursodatabase/tap/turso`;
|
||||
} else {
|
||||
const { stdout: installScript } =
|
||||
await $`curl -sSfL https://get.tur.so/install.sh`;
|
||||
await $`bash -c '${installScript}'`;
|
||||
}
|
||||
|
||||
s.stop("Turso CLI installed successfully!");
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message.includes("User force closed")) {
|
||||
s.stop("Turso CLI installation cancelled");
|
||||
log.warn(pc.yellow("Turso CLI installation cancelled by user"));
|
||||
throw new Error("Installation cancelled");
|
||||
}
|
||||
s.stop(pc.red("Failed to install Turso CLI"));
|
||||
}
|
||||
}
|
||||
|
||||
async function getTursoGroups(): Promise<TursoGroup[]> {
|
||||
const s = spinner();
|
||||
try {
|
||||
s.start("Fetching Turso groups...");
|
||||
const { stdout } = await $`turso group list`;
|
||||
const lines = stdout.trim().split("\n");
|
||||
|
||||
if (lines.length <= 1) {
|
||||
s.stop("No Turso groups found");
|
||||
return [];
|
||||
}
|
||||
|
||||
const groups = lines.slice(1).map((line) => {
|
||||
const [name, locations, version, status] = line.trim().split(/\s{2,}/);
|
||||
return { name, locations, version, status };
|
||||
});
|
||||
|
||||
s.stop(`Found ${groups.length} Turso groups`);
|
||||
return groups;
|
||||
} catch (error) {
|
||||
s.stop(pc.red("Error fetching Turso groups"));
|
||||
console.error("Error fetching Turso groups:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function selectTursoGroup(): Promise<string | null> {
|
||||
const groups = await getTursoGroups();
|
||||
|
||||
if (groups.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (groups.length === 1) {
|
||||
log.info(`Using the only available group: ${pc.blue(groups[0].name)}`);
|
||||
return groups[0].name;
|
||||
}
|
||||
|
||||
const groupOptions = groups.map((group) => ({
|
||||
value: group.name,
|
||||
label: `${group.name} (${group.locations})`,
|
||||
}));
|
||||
|
||||
const selectedGroup = await select({
|
||||
message: "Select a Turso database group:",
|
||||
options: groupOptions,
|
||||
});
|
||||
|
||||
if (isCancel(selectedGroup)) {
|
||||
cancel(pc.red("Operation cancelled"));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
return selectedGroup as string;
|
||||
}
|
||||
|
||||
async function createTursoDatabase(dbName: string, groupName: string | null) {
|
||||
const s = spinner();
|
||||
|
||||
try {
|
||||
s.start(
|
||||
`Creating Turso database "${dbName}"${
|
||||
groupName ? ` in group "${groupName}"` : ""
|
||||
}...`,
|
||||
);
|
||||
|
||||
if (groupName) {
|
||||
await $`turso db create ${dbName} --group ${groupName}`;
|
||||
} else {
|
||||
await $`turso db create ${dbName}`;
|
||||
}
|
||||
|
||||
s.stop(`Created database "${dbName}"`);
|
||||
} catch (error) {
|
||||
s.stop(pc.red(`Failed to create database "${dbName}"`));
|
||||
if (error instanceof Error && error.message.includes("already exists")) {
|
||||
throw new Error("DATABASE_EXISTS");
|
||||
}
|
||||
}
|
||||
|
||||
s.start("Retrieving database connection details...");
|
||||
try {
|
||||
const { stdout: dbUrl } = await $`turso db show ${dbName} --url`;
|
||||
const { stdout: authToken } = await $`turso db tokens create ${dbName}`;
|
||||
|
||||
s.stop("Retrieved database connection details");
|
||||
|
||||
return {
|
||||
dbUrl: dbUrl.trim(),
|
||||
authToken: authToken.trim(),
|
||||
};
|
||||
} catch (_error) {
|
||||
s.stop(pc.red("Failed to retrieve database connection details"));
|
||||
}
|
||||
}
|
||||
|
||||
async function writeEnvFile(projectDir: string, config?: TursoConfig) {
|
||||
const envPath = path.join(projectDir, "apps/server", ".env");
|
||||
const variables: EnvVariable[] = [
|
||||
{
|
||||
key: "DATABASE_URL",
|
||||
value: config?.dbUrl ?? "",
|
||||
condition: true,
|
||||
},
|
||||
{
|
||||
key: "DATABASE_AUTH_TOKEN",
|
||||
value: config?.authToken ?? "",
|
||||
condition: true,
|
||||
},
|
||||
];
|
||||
await addEnvVariablesToFile(envPath, variables);
|
||||
}
|
||||
|
||||
function displayManualSetupInstructions() {
|
||||
log.info(`Manual Turso Setup Instructions:
|
||||
|
||||
1. Visit https://turso.tech and create an account
|
||||
2. Create a new database from the dashboard
|
||||
3. Get your database URL and authentication token
|
||||
4. Add these credentials to the .env file in apps/server/.env
|
||||
|
||||
DATABASE_URL=your_database_url
|
||||
DATABASE_AUTH_TOKEN=your_auth_token`);
|
||||
}
|
||||
|
||||
export async function setupTurso(config: ProjectConfig): Promise<void> {
|
||||
const { orm, projectDir } = config;
|
||||
const _isDrizzle = orm === "drizzle";
|
||||
const setupSpinner = spinner();
|
||||
setupSpinner.start("Setting up Turso database");
|
||||
|
||||
try {
|
||||
const platform = os.platform();
|
||||
const isMac = platform === "darwin";
|
||||
const _isLinux = platform === "linux";
|
||||
const isWindows = platform === "win32";
|
||||
|
||||
if (isWindows) {
|
||||
setupSpinner.stop(pc.yellow("Turso setup not supported on Windows"));
|
||||
log.warn(pc.yellow("Automatic Turso setup is not supported on Windows."));
|
||||
await writeEnvFile(projectDir);
|
||||
displayManualSetupInstructions();
|
||||
return;
|
||||
}
|
||||
|
||||
setupSpinner.stop("Checking Turso CLI");
|
||||
|
||||
const isCliInstalled = await isTursoInstalled();
|
||||
|
||||
if (!isCliInstalled) {
|
||||
const shouldInstall = await confirm({
|
||||
message: "Would you like to install Turso CLI?",
|
||||
initialValue: true,
|
||||
});
|
||||
|
||||
if (isCancel(shouldInstall)) {
|
||||
cancel(pc.red("Operation cancelled"));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (!shouldInstall) {
|
||||
await writeEnvFile(projectDir);
|
||||
displayManualSetupInstructions();
|
||||
return;
|
||||
}
|
||||
|
||||
await installTursoCLI(isMac);
|
||||
}
|
||||
|
||||
const isLoggedIn = await isTursoLoggedIn();
|
||||
if (!isLoggedIn) {
|
||||
await loginToTurso();
|
||||
}
|
||||
|
||||
const selectedGroup = await selectTursoGroup();
|
||||
|
||||
let success = false;
|
||||
let dbName = "";
|
||||
let suggestedName = path.basename(projectDir);
|
||||
|
||||
while (!success) {
|
||||
const dbNameResponse = await text({
|
||||
message: "Enter a name for your database:",
|
||||
defaultValue: suggestedName,
|
||||
initialValue: suggestedName,
|
||||
placeholder: suggestedName,
|
||||
});
|
||||
|
||||
if (isCancel(dbNameResponse)) {
|
||||
cancel(pc.red("Operation cancelled"));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
dbName = dbNameResponse as string;
|
||||
|
||||
try {
|
||||
const config = await createTursoDatabase(dbName, selectedGroup);
|
||||
|
||||
const finalSpinner = spinner();
|
||||
finalSpinner.start("Writing configuration to .env file");
|
||||
await writeEnvFile(projectDir, config);
|
||||
finalSpinner.stop("Turso database configured successfully!");
|
||||
|
||||
success = true;
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message === "DATABASE_EXISTS") {
|
||||
log.warn(pc.yellow(`Database "${pc.red(dbName)}" already exists`));
|
||||
suggestedName = `${dbName}-${Math.floor(Math.random() * 1000)}`;
|
||||
} else {
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
setupSpinner.stop(pc.red("Failed to set up Turso database"));
|
||||
consola.error(
|
||||
pc.red(
|
||||
`Error during Turso setup: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
),
|
||||
);
|
||||
await writeEnvFile(projectDir);
|
||||
displayManualSetupInstructions();
|
||||
log.success("Setup completed with manual configuration required.");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user