cli: organize file structure

This commit is contained in:
Aman Varshney
2025-05-26 00:13:08 +05:30
parent b9c9690e61
commit 1e9c6b2210
40 changed files with 900 additions and 928 deletions

View 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 {}
}
}

View 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();
}
}

View 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.");
}
}

View 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();
}
}

View 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.");
}
}