add auth, drizzle, prisma setup logic with template

This commit is contained in:
Aman Varshney
2025-03-04 09:33:31 +05:30
parent 54d63823bb
commit 792885b9c4
68 changed files with 2692 additions and 921 deletions

View File

@@ -14,4 +14,5 @@ export const DEFAULT_CONFIG: ProjectConfig = {
features: [],
git: true,
packageManager: "npm",
noInstall: false,
};

View File

@@ -1,496 +1,97 @@
import crypto from "node:crypto";
import path from "node:path";
import { log, spinner } from "@clack/prompts";
import { log } from "@clack/prompts";
import fs from "fs-extra";
import pc from "picocolors";
import { PKG_ROOT } from "../constants";
import type { ProjectConfig } from "../types";
export async function configureAuth(
projectDir: string,
initialEnableAuth: boolean,
enableAuth: boolean,
hasDatabase: boolean,
options?: ProjectConfig,
) {
let enableAuth = initialEnableAuth;
options: ProjectConfig,
): Promise<void> {
const serverDir = path.join(projectDir, "packages/server");
const clientDir = path.join(projectDir, "packages/client");
if (!hasDatabase && enableAuth) {
log.warn(
pc.yellow(
"Authentication requires a database. Disabling authentication.",
),
);
enableAuth = false;
}
try {
if (!enableAuth) {
await fs.remove(path.join(clientDir, "src/components/sign-up-form.tsx"));
await fs.remove(path.join(clientDir, "src/components/user-menu.tsx"));
await fs.remove(path.join(clientDir, "src/lib/auth-client.ts"));
await fs.remove(path.join(clientDir, "src/lib/schemas.ts"));
if (enableAuth) {
const secret = crypto.randomBytes(32).toString("hex");
await fs.remove(path.join(serverDir, "src/lib/auth.ts"));
const serverEnvPath = path.join(projectDir, "packages/server/.env");
await fs.ensureFile(serverEnvPath);
let envContent = await fs.readFile(serverEnvPath, "utf-8").catch(() => "");
const indexFilePath = path.join(serverDir, "src/index.ts");
const indexContent = await fs.readFile(indexFilePath, "utf8");
const updatedIndexContent = indexContent
.replace(/import { auth } from "\.\/lib\/auth";\n/, "")
.replace(
/app\.on\(\["POST", "GET"\], "\/api\/auth\/\*\*", \(c\) => auth\.handler\(c\.req\.raw\)\);\n\n/,
"",
);
await fs.writeFile(indexFilePath, updatedIndexContent, "utf8");
if (!envContent.includes("BETTER_AUTH_SECRET")) {
envContent += `\n# Better Auth Configuration\nBETTER_AUTH_SECRET="${secret}"\nBETTER_AUTH_URL="${process.env.BETTER_AUTH_URL || "http://localhost:3000"}"\nCORS_ORIGIN="${process.env.CORS_ORIGIN || "http://localhost:3001"}"\n`;
await fs.writeFile(serverEnvPath, envContent);
}
const orm = options?.orm || "drizzle";
const database = options?.database || "sqlite";
const databaseProvider = database === "sqlite" ? "sqlite" : "postgresql";
await updatePackageJson(projectDir, true, orm);
const configPath = path.join(
projectDir,
"packages/server/better-auth.config.js",
);
const adapterConfig =
orm === "prisma"
? `{
name: "prisma",
options: {
provider: "${databaseProvider}",
schemaPath: "./prisma/schema.prisma",
}
}`
: `{
name: "drizzle",
options: {
provider: "${databaseProvider}",
schemaPath: "./src/db/schema.ts",
}
}`;
const configContent = `/** @type {import('better-auth').BetterAuthConfig} */
module.exports = {
adapter: ${adapterConfig}
};`;
await fs.writeFile(configPath, configContent);
await createAuthFile(projectDir, orm, databaseProvider);
await createAuthClientFile(projectDir);
if (orm === "prisma") {
await setupBasicPrisma(projectDir, databaseProvider);
const contextFilePath = path.join(serverDir, "src/lib/context.ts");
const contextContent = await fs.readFile(contextFilePath, "utf8");
const updatedContextContent = contextContent
.replace(/import { auth } from "\.\/auth";\n/, "")
.replace(
/const session = await auth\.api\.getSession\({\n\s+headers: hono\.req\.raw\.headers,\n\s+}\);/,
"const session = null;",
);
await fs.writeFile(contextFilePath, updatedContextContent, "utf8");
} else if (!hasDatabase) {
log.warn(
pc.yellow(
"Authentication enabled but no database selected. Auth will not function properly.",
),
);
} else {
await fs.ensureDir(path.join(projectDir, "packages/server/src/db"));
}
const envPath = path.join(serverDir, ".env");
const envExamplePath = path.join(serverDir, "_env");
await updateServerIndex(projectDir, true);
await updateContext(projectDir, true, orm);
} else {
await updatePackageJson(projectDir, false);
await updateAuthImplementations(projectDir, false);
await updateServerIndex(projectDir, false);
await updateContext(projectDir, false);
}
}
async function updateServerIndex(projectDir: string, enableAuth: boolean) {
const serverIndexPath = path.join(projectDir, "packages/server/src/index.ts");
if (!(await fs.pathExists(serverIndexPath))) return;
let content = await fs.readFile(serverIndexPath, "utf-8");
if (enableAuth) {
if (!content.includes('import { auth } from "./lib/auth"')) {
const importLines = content
.split("\n")
.findIndex(
(line) => line.startsWith("import") || line.startsWith("// import"),
);
const lines = content.split("\n");
lines.splice(importLines + 1, 0, 'import { auth } from "./lib/auth";');
content = lines.join("\n");
}
if (!content.includes('app.on(["POST", "GET"], "/api/auth/**"')) {
const appCreation = content.indexOf("app.use");
if (appCreation !== -1) {
const insertPoint = content.indexOf("\n", appCreation) + 1;
const authRouteHandler =
'\n// Auth routes\napp.on(["POST", "GET"], "/api/auth/**", (c) => auth.handler(c.req.raw));\n';
content =
content.slice(0, insertPoint) +
authRouteHandler +
content.slice(insertPoint);
}
}
} else {
content = content.replace(/import { auth } from "\.\/lib\/auth";?\n?/g, "");
content = content.replace(/\/\/ Auth routes\n?/g, "");
content = content.replace(
/app\.on\(\["POST", "GET"\], "\/api\/auth\/\*\*", \(c\) => auth\.handler\(c\.req\.raw\)\);?\n?/g,
"",
);
}
await fs.writeFile(serverIndexPath, content);
}
async function updateContext(
projectDir: string,
enableAuth: boolean,
_orm?: string,
) {
const contextPath = path.join(
projectDir,
"packages/server/src/lib/context.ts",
);
if (!(await fs.pathExists(contextPath))) return;
let content = await fs.readFile(contextPath, "utf-8");
if (enableAuth) {
if (!content.includes('import { auth } from "./auth"')) {
const importLines = content
.split("\n")
.findIndex(
(line) => line.startsWith("import") || line.startsWith("// import"),
);
const lines = content.split("\n");
lines.splice(importLines + 1, 0, 'import { auth } from "./auth";');
content = lines.join("\n");
}
if (!content.includes("const session =")) {
const createContextBody = content.indexOf(
"export async function createContext",
);
if (createContextBody !== -1) {
const bodyStart = content.indexOf("{", createContextBody);
const nextLine = content.indexOf("\n", bodyStart) + 1;
const sessionExtraction =
" // Get the session from the request\n" +
" const session = await auth.api.getSession({\n" +
" headers: hono.req.raw.headers,\n" +
" });\n\n";
content =
content.slice(0, nextLine) +
sessionExtraction +
content.slice(nextLine);
if (await fs.pathExists(envExamplePath)) {
await fs.copy(envExamplePath, envPath);
await fs.remove(envExamplePath);
}
const returnIndex = content.lastIndexOf("return {");
if (returnIndex !== -1) {
const returnEnd = content.indexOf("}", returnIndex);
const returnContent = content.substring(returnIndex, returnEnd);
if (options.orm === "prisma") {
const prismaAuthPath = path.join(serverDir, "src/lib/auth.ts");
const defaultPrismaAuthPath = path.join(
PKG_ROOT,
"template/with-prisma/packages/server/src/lib/auth.ts",
);
if (!returnContent.includes("session")) {
const updatedReturn = returnContent.replace(
"return {",
"return {\n session,",
);
content =
content.slice(0, returnIndex) +
updatedReturn +
content.slice(returnEnd);
if (
(await fs.pathExists(defaultPrismaAuthPath)) &&
!(await fs.pathExists(prismaAuthPath))
) {
await fs.ensureDir(path.dirname(prismaAuthPath));
await fs.copy(defaultPrismaAuthPath, prismaAuthPath);
}
} else if (options.orm === "drizzle") {
const drizzleAuthPath = path.join(serverDir, "src/lib/auth.ts");
const defaultDrizzleAuthPath = path.join(
PKG_ROOT,
"template/with-drizzle/packages/server/src/lib/auth.ts",
);
if (
(await fs.pathExists(defaultDrizzleAuthPath)) &&
!(await fs.pathExists(drizzleAuthPath))
) {
await fs.ensureDir(path.dirname(drizzleAuthPath));
await fs.copy(defaultDrizzleAuthPath, drizzleAuthPath);
}
}
}
} else {
content = content.replace(/import { auth } from "\.\/auth";?\n?/g, "");
content = content.replace(/\/\/ Get the session from the request\n?/g, "");
content = content.replace(
/const session = await auth\.api\.getSession\(\{\n?.*headers: hono\.req\.raw\.headers,\n?.*\}\);?\n?/g,
"const session = null;\n",
);
if (!content.includes("const session = null")) {
const createContextBody = content.indexOf(
"export async function createContext",
);
if (createContextBody !== -1) {
const bodyStart = content.indexOf("{", createContextBody);
const nextLine = content.indexOf("\n", bodyStart) + 1;
content = `${content.slice(0, nextLine)} const session = null;\n\n${content.slice(nextLine)}`;
}
}
}
await fs.writeFile(contextPath, content);
}
async function updatePackageJson(
projectDir: string,
enableAuth: boolean,
orm?: string,
) {
const clientPackageJsonPath = path.join(
projectDir,
"packages/client/package.json",
);
const serverPackageJsonPath = path.join(
projectDir,
"packages/server/package.json",
);
if (enableAuth) {
if (await fs.pathExists(clientPackageJsonPath)) {
const clientPackageJson = await fs.readJson(clientPackageJsonPath);
clientPackageJson.dependencies = clientPackageJson.dependencies || {};
clientPackageJson.dependencies["better-auth"] = "latest";
await fs.writeJson(clientPackageJsonPath, clientPackageJson, {
spaces: 2,
});
}
if (await fs.pathExists(serverPackageJsonPath)) {
const serverPackageJson = await fs.readJson(serverPackageJsonPath);
serverPackageJson.dependencies = serverPackageJson.dependencies || {};
serverPackageJson.dependencies["better-auth"] = "latest";
if (orm === "prisma") {
serverPackageJson.dependencies["@prisma/client"] = "latest";
serverPackageJson.devDependencies =
serverPackageJson.devDependencies || {};
serverPackageJson.devDependencies.prisma = "latest";
} else if (orm === "drizzle") {
serverPackageJson.dependencies["drizzle-orm"] = "latest";
serverPackageJson.devDependencies =
serverPackageJson.devDependencies || {};
serverPackageJson.devDependencies["drizzle-kit"] = "latest";
}
await fs.writeJson(serverPackageJsonPath, serverPackageJson, {
spaces: 2,
});
}
} else {
// Remove auth dependencies if disabling auth
if (await fs.pathExists(clientPackageJsonPath)) {
const clientPackageJson = await fs.readJson(clientPackageJsonPath);
if (clientPackageJson.dependencies?.["better-auth"]) {
clientPackageJson.dependencies = Object.fromEntries(
Object.entries(clientPackageJson.dependencies).filter(
([key]) => key !== "better-auth",
),
);
}
await fs.writeJson(clientPackageJsonPath, clientPackageJson, {
spaces: 2,
});
}
if (await fs.pathExists(serverPackageJsonPath)) {
const serverPackageJson = await fs.readJson(serverPackageJsonPath);
if (serverPackageJson.dependencies?.["better-auth"]) {
serverPackageJson.dependencies = Object.fromEntries(
Object.entries(serverPackageJson.dependencies).filter(
([key]) => key !== "better-auth",
),
);
}
if (serverPackageJson.devDependencies?.["@better-auth/cli"]) {
serverPackageJson.devDependencies = Object.fromEntries(
Object.entries(serverPackageJson.devDependencies).filter(
([key]) => key !== "@better-auth/cli",
),
);
}
await fs.writeJson(serverPackageJsonPath, serverPackageJson, {
spaces: 2,
});
}
}
}
async function setupBasicPrisma(projectDir: string, databaseProvider: string) {
const prismaDir = path.join(projectDir, "packages/server/prisma");
await fs.ensureDir(prismaDir);
const schemaPath = path.join(prismaDir, "schema.prisma");
const schemaContent = `// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "${databaseProvider}"
url = env("DATABASE_URL")
}
// Models will be added by running:
// npx @better-auth/cli generate
`;
await fs.writeFile(schemaPath, schemaContent);
const clientDir = path.join(projectDir, "packages/server/src/db");
await fs.ensureDir(clientDir);
const clientPath = path.join(clientDir, "client.ts");
const clientContent = `import { PrismaClient } from '@prisma/client';
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient | undefined };
export const prisma = globalForPrisma.prisma ?? new PrismaClient({
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
});
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;
`;
await fs.writeFile(clientPath, clientContent);
const indexPath = path.join(clientDir, "index.ts");
const indexContent = `export * from './client';
`;
await fs.writeFile(indexPath, indexContent);
const envPath = path.join(projectDir, "packages/server/.env");
let envContent = await fs.readFile(envPath, "utf-8").catch(() => "");
if (!envContent.includes("DATABASE_URL")) {
const defaultUrl =
databaseProvider === "sqlite"
? "file:./prisma/dev.db"
: "postgresql://postgres:password@localhost:5432/better-t-stack";
envContent += `\n# Database\nDATABASE_URL="${defaultUrl}"\n`;
await fs.writeFile(envPath, envContent);
}
}
async function createAuthFile(
projectDir: string,
orm: string,
databaseProvider: string,
) {
const authDir = path.join(projectDir, "packages/server/src/lib");
await fs.ensureDir(authDir);
const authFilePath = path.join(authDir, "auth.ts");
let authContent = "";
if (orm === "prisma") {
authContent = `import { betterAuth } from "better-auth";
import { prismaAdapter } from "better-auth/adapters/prisma";
import { prisma } from "../db/client";
export const auth = betterAuth({
database: prismaAdapter(prisma, {
provider: "${databaseProvider}",
}),
trustedOrigins: [process.env.CORS_ORIGIN!],
emailAndPassword: {
enabled: true,
},
session: {
secret: process.env.BETTER_AUTH_SECRET!,
},
});`;
} else {
authContent = `import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { db } from "../db";
import * as schema from "../db/schema";
export const auth = betterAuth({
database: drizzleAdapter(db, {
provider: "${databaseProvider}",
schema: schema,
}),
trustedOrigins: [process.env.CORS_ORIGIN!],
emailAndPassword: {
enabled: true,
},
session: {
secret: process.env.BETTER_AUTH_SECRET!,
},
});`;
}
await fs.writeFile(authFilePath, authContent);
}
async function createAuthClientFile(projectDir: string) {
const libDir = path.join(projectDir, "packages/client/src/lib");
await fs.ensureDir(libDir);
const authClientPath = path.join(libDir, "auth-client.ts");
const authClientContent = `import { createAuthClient } from "better-auth/react";
export const authClient = createAuthClient({
baseURL: import.meta.env.VITE_SERVER_URL,
});
// Export specific methods if needed
export const { signIn, signUp, useSession } = authClient;
`;
await fs.writeFile(authClientPath, authClientContent);
}
async function updateAuthImplementations(
projectDir: string,
enableAuth: boolean,
) {
if (enableAuth) {
} else {
const filesToRemove = [
path.join(projectDir, "packages/server/src/lib/auth.ts"),
path.join(projectDir, "packages/server/better-auth.config.js"),
path.join(projectDir, "packages/client/src/lib/auth-client.ts"),
path.join(projectDir, "packages/client/src/components/sign-up-form.tsx"),
path.join(projectDir, "packages/client/src/components/user-menu.tsx"),
];
for (const file of filesToRemove) {
if (await fs.pathExists(file)) {
await fs.remove(file);
}
}
const routeFiles = [
path.join(projectDir, "packages/client/src/routes/index.tsx"),
path.join(projectDir, "packages/client/src/routes/dashboard.tsx"),
path.join(projectDir, "packages/client/src/components/header.tsx"),
];
for (const file of routeFiles) {
if (await fs.pathExists(file)) {
let content = await fs.readFile(file, "utf-8");
content = content.replace(
/import SignUp from "@\/components\/sign-up-form";/,
"",
);
content = content.replace(/<SignUp \/>/, "");
content = content.replace(
/import { authClient } from "@\/lib\/auth-client";/,
"",
);
content = content.replace(
/import { (?:signIn, signUp, )?useSession } from "@\/lib\/auth-client";/,
"",
);
content = content.replace(
/const { data: session, isPending } = useSession\(\);/,
"",
);
content = content.replace(
/useEffect\(\(\) => \{\s*if \(!session && !isPending\) \{\s*navigate\(\{\s*to: "\/",\s*\}\);\s*\}\s*\}, \[session, isPending\]\);/,
"",
);
content = content.replace(/import UserMenu from ".\/user-menu";/, "");
content = content.replace(/<UserMenu \/>/, "");
await fs.writeFile(file, content);
}
} catch (error) {
log.error(pc.red("Failed to configure authentication"));
if (error instanceof Error) {
log.error(pc.red(error.message));
}
throw error;
}
}

View File

@@ -1,131 +1,116 @@
import path from "node:path";
import { cancel, confirm, isCancel, log, spinner, tasks } from "@clack/prompts";
import degit from "degit";
import { cancel, spinner } from "@clack/prompts";
import { $ } from "execa";
import fs from "fs-extra";
import pc from "picocolors";
import { PKG_ROOT } from "../constants";
import type { ProjectConfig } from "../types";
import { configureAuth } from "./auth-setup";
import { createReadme } from "./create-readme";
import { setupTurso } from "./db-setup";
import { setupDatabase } from "./db-setup";
import { setupFeatures } from "./feature-setup";
import { displayPostInstallInstructions } from "./post-installation";
export async function createProject(options: ProjectConfig) {
export async function createProject(options: ProjectConfig): Promise<string> {
const s = spinner();
const projectDir = path.resolve(process.cwd(), options.projectName);
let shouldInstallDeps = false;
try {
const tasksList = [
{
title: "Creating project directory",
task: async () => {
await fs.ensureDir(projectDir);
},
},
{
title: "Cloning template repository",
task: async () => {
try {
const emitter = degit("better-t-stack/Better-T-Stack#bare");
await emitter.clone(projectDir);
} catch (error) {
log.error(pc.red("Failed to clone template repository"));
if (error instanceof Error) {
log.error(pc.red(error.message));
}
throw error;
}
},
},
];
await fs.ensureDir(projectDir);
if (options.database === "none") {
tasksList.push({
title: "Removing database configuration",
task: async () => {
await fs.remove(path.join(projectDir, "packages/server/src/db"));
},
});
const templateDir = path.join(PKG_ROOT, "template/base");
if (!(await fs.pathExists(templateDir))) {
throw new Error(`Template directory not found: ${templateDir}`);
}
await fs.copy(templateDir, projectDir);
tasksList.push({
title: options.auth
? "Setting up authentication"
: "Removing authentication",
task: async () => {
await configureAuth(
projectDir,
options.auth,
options.database !== "none",
options,
);
},
});
if (options.git) {
tasksList.push({
title: "Initializing git repository",
task: async () => {
await $({
cwd: projectDir,
})`git init`;
},
});
}
if (options.features.length > 0) {
tasksList.push({
title: "Setting up additional features",
task: async () => {
await setupFeatures(projectDir, options.features);
},
});
}
await tasks(tasksList);
if (options.database === "sqlite") {
await setupTurso(projectDir);
} else if (options.database === "postgres") {
log.info(
pc.blue(
"PostgreSQL setup is manual. You'll need to set up your own PostgreSQL database and update the connection details in .env",
),
if (options.orm !== "none" && options.database !== "none") {
const ormTemplateDir = path.join(
PKG_ROOT,
options.orm === "drizzle"
? "template/with-drizzle"
: "template/with-prisma",
);
}
const installDepsResponse = await confirm({
message: `Install dependencies with ${options.packageManager}?`,
});
if (isCancel(installDepsResponse)) {
cancel(pc.red("Operation cancelled"));
process.exit(0);
}
shouldInstallDeps = installDepsResponse;
if (shouldInstallDeps) {
s.start(`Installing dependencies using ${options.packageManager}...`);
try {
await $({
cwd: projectDir,
})`${options.packageManager} install`;
s.stop("Dependencies installed successfully");
} catch (error) {
s.stop(pc.red("Failed to install dependencies"));
if (error instanceof Error) {
log.error(pc.red(`Installation error: ${error.message}`));
}
throw error;
if (await fs.pathExists(ormTemplateDir)) {
await fs.copy(ormTemplateDir, projectDir, { overwrite: true });
}
}
const rootPackageJsonPath = path.join(projectDir, "package.json");
if (await fs.pathExists(rootPackageJsonPath)) {
const packageJson = await fs.readJson(rootPackageJsonPath);
const gitignoreFiles = [
[
path.join(projectDir, "_gitignore"),
path.join(projectDir, ".gitignore"),
],
[
path.join(projectDir, "packages/client/_gitignore"),
path.join(projectDir, "packages/client/.gitignore"),
],
[
path.join(projectDir, "packages/server/_gitignore"),
path.join(projectDir, "packages/server/.gitignore"),
],
];
for (const [source, target] of gitignoreFiles) {
if (await fs.pathExists(source)) {
await fs.move(source, target);
}
}
const envFiles = [
[
path.join(projectDir, "packages/server/_env"),
path.join(projectDir, "packages/server/.env"),
],
];
for (const [source, target] of envFiles) {
if (await fs.pathExists(source)) {
if (!(await fs.pathExists(target))) {
await fs.move(source, target);
} else {
await fs.remove(source);
}
}
}
await setupDatabase(
projectDir,
options.database,
options.orm,
options.turso ?? options.database === "sqlite",
);
await configureAuth(
projectDir,
options.auth,
options.database !== "none",
options,
);
if (options.git) {
await $({ cwd: projectDir })`git init`;
}
if (options.features.length > 0) {
await setupFeatures(projectDir, options.features);
}
const packageJsonPath = path.join(projectDir, "package.json");
if (await fs.pathExists(packageJsonPath)) {
const packageJson = await fs.readJson(packageJsonPath);
packageJson.name = options.projectName;
if (options.packageManager !== "bun") {
packageJson.packageManager =
options.packageManager === "npm"
? "npm@10.2.4"
: options.packageManager === "pnpm"
? "pnpm@8.15.4"
: options.packageManager === "yarn"
? "yarn@4.1.0"
: "bun@1.2.4";
}
if (options.auth && options.database !== "none") {
packageJson.scripts["auth:generate"] =
@@ -150,7 +135,7 @@ export async function createProject(options: ProjectConfig) {
}
}
await fs.writeJson(rootPackageJsonPath, packageJson, { spaces: 2 });
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
}
await createReadme(projectDir, options);
@@ -160,14 +145,17 @@ export async function createProject(options: ProjectConfig) {
options.database,
options.projectName,
options.packageManager,
shouldInstallDeps,
!options.noInstall,
options.orm,
);
return projectDir;
} catch (error) {
s.stop(pc.red("Failed"));
s.message(pc.red("Failed"));
if (error instanceof Error) {
log.error(pc.red(`Error during project creation: ${error.message}`));
cancel(pc.red(`Error during project creation: ${error.message}`));
process.exit(1);
}
throw error;
}
}

View File

@@ -2,94 +2,176 @@ import path from "node:path";
import fs from "fs-extra";
import type { ProjectConfig } from "../types";
export async function createReadme(projectDir: string, config: ProjectConfig) {
export async function createReadme(projectDir: string, options: ProjectConfig) {
const readmePath = path.join(projectDir, "README.md");
const projectName = path.basename(projectDir);
const content = generateReadmeContent(options);
const authSection = config.auth
? `
## Authentication
This project uses [Better-Auth](https://www.better-auth.com/) for authentication.
To complete setup:
1. Create necessary auth tables: \`npx @better-auth/cli migrate\`
2. Configure environment variables in \`.env\` files
3. Check the auth documentation: https://www.better-auth.com/
`
: "";
const databaseSection =
config.database !== "none"
? `
## Database
This project uses ${config.database === "sqlite" ? "SQLite (via Turso)" : "PostgreSQL"} with ${config.orm} ORM.
${
config.database === "sqlite"
? "Ensure your Turso connection details are set in `packages/server/.env`."
: "Ensure your PostgreSQL connection string is set in `packages/server/.env`."
try {
await fs.writeFile(readmePath, content);
} catch (error) {
console.error("Failed to create README.md file:", error);
}
}
`
: "";
const featuresSection =
config.features.length > 0
? `
function generateReadmeContent(options: ProjectConfig): string {
const {
projectName,
packageManager,
database,
auth,
features = [],
orm = "drizzle",
} = options;
const packageManagerRunCmd =
packageManager === "npm" ? "npm run" : packageManager;
return `# ${projectName}
This project was created with [Better-T-Stack](https://github.com/better-t-stack/Better-T-Stack).
## Features
This project includes:
${config.features.map((feature) => `- ${feature}`).join("\n")}
`
: "";
const readme = `# ${projectName}
A modern web application built with the Better-T Stack.
## Tech Stack
- **Frontend**: React, TanStack Router, TanStack Query
- **Backend**: Hono, tRPC
- **Styling**: Tailwind CSS with shadcn/ui components
${databaseSection}${authSection}${featuresSection}
${generateFeaturesList(database, auth, features, orm)}
## Getting Started
1. Install dependencies:
\`\`\`
${config.packageManager} install
\`\`\`
First, install the dependencies:
2. Start the development server:
\`\`\`
${config.packageManager} run dev
\`\`\`
\`\`\`bash
${packageManager} install
\`\`\`
Then, run the development server:
\`\`\`bash
${packageManagerRunCmd} dev
\`\`\`
Open [http://localhost:3001](http://localhost:3001) in your browser to see the client application.
The API is running at [http://localhost:3000](http://localhost:3000).
## Database Setup
${generateDatabaseSetup(database, auth, packageManagerRunCmd, orm)}
## Project Structure
\`\`\`
packages/
├── client/ # React frontend application
── server/ # Hono + tRPC backend server
${projectName}/
├── packages/
── client/ # Frontend application (React, TanStack Router)
│ └── server/ # Backend API (Hono, tRPC)
\`\`\`
## Commands
## Scripts
- \`${config.packageManager} run dev\`: Start development servers
- \`${config.packageManager} run build\`: Build for production
- \`${config.packageManager} run dev:client\`: Start only frontend server
- \`${config.packageManager} run dev:server\`: Start only backend server
## Environment Variables
Check \`.env.example\` files in each package directory for required environment variables.
## License
MIT
${generateScriptsList(packageManagerRunCmd)}
`;
await fs.writeFile(readmePath, readme);
}
function generateFeaturesList(
database: string,
auth: boolean,
features: string[],
orm: string,
): string {
const featuresList = [
"TypeScript - For type safety",
"TanStack Router - File-based routing",
`${orm === "drizzle" ? "Drizzle" : "Prisma"} - ORM`,
"TailwindCSS - Utility-first CSS",
"shadcn/ui - Reusable components",
"Hono - Lightweight, performant server",
];
if (database !== "none") {
featuresList.push(
`${database === "sqlite" ? "SQLite/Turso DB" : "PostgreSQL"} - Database`,
);
}
if (auth) {
featuresList.push("Authentication - Email & password auth");
}
for (const feature of features) {
if (feature === "docker") {
featuresList.push("Docker - Containerized deployment");
} else if (feature === "github-actions") {
featuresList.push("GitHub Actions - CI/CD");
} else if (feature === "SEO") {
featuresList.push("SEO - Search engine optimization");
}
}
return featuresList.join("\n");
}
function generateDatabaseSetup(
database: string,
auth: boolean,
packageManagerRunCmd: string,
orm: string,
): string {
if (database === "none") {
return "This project does not include a database.";
}
if (database === "sqlite") {
return `This project uses SQLite/Turso for the database.
1. Start the local database:
\`\`\`bash
${packageManagerRunCmd} db:local
\`\`\`
2. Update your \`.env\` file with the connection details.
${
auth
? `3. If using authentication, generate the auth schema:
\`\`\`bash
${packageManagerRunCmd} auth:generate
\`\`\`
4. Apply the schema to your database:
\`\`\`bash
${packageManagerRunCmd} ${orm === "drizzle" ? "drizzle:migrate" : "prisma:push"}
\`\`\``
: ""
}`;
}
if (database === "postgres") {
return `This project uses PostgreSQL for the database.
1. Set up your PostgreSQL database.
2. Update your \`.env\` file with the connection details.
${
auth
? `3. If using authentication, generate the auth schema:
\`\`\`bash
${packageManagerRunCmd} auth:generate
\`\`\`
4. Apply the schema to your database:
\`\`\`bash
${packageManagerRunCmd} ${orm === "drizzle" ? "drizzle:migrate" : "prisma:push"}
\`\`\``
: ""
}`;
}
return "";
}
function generateScriptsList(packageManagerRunCmd: string): string {
return `- \`${packageManagerRunCmd} dev\`: Start both client and server in development mode
- \`${packageManagerRunCmd} build\`: Build both client and server
- \`${packageManagerRunCmd} dev:client\`: Start only the client
- \`${packageManagerRunCmd} dev:server\`: Start only the server
- \`${packageManagerRunCmd} db:local\`: Start the local SQLite database (if applicable)
- \`${packageManagerRunCmd} db:push\`: Push schema changes to the database`;
}

View File

@@ -1,195 +1,120 @@
import os from "node:os";
import path from "node:path";
import { cancel, confirm, isCancel, log, spinner, text } from "@clack/prompts";
import { $ } from "execa";
import { log, spinner } from "@clack/prompts";
import fs from "fs-extra";
import pc from "picocolors";
import { isTursoInstalled, isTursoLoggedIn } from "../utils/turso-cli";
import { setupTurso } from "./turso-setup";
interface TursoConfig {
dbUrl: string;
authToken: string;
}
async function loginToTurso() {
export async function setupDatabase(
projectDir: string,
databaseType: string,
orm: string,
setupTursoDb = true,
): Promise<void> {
const s = spinner();
try {
s.start("Logging in to Turso...");
await $`turso auth login`;
s.stop("Logged in to Turso successfully!");
} catch (error) {
s.stop(pc.red("Failed to log in to Turso"));
throw error;
}
}
const serverDir = path.join(projectDir, "packages/server");
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!");
} catch (error) {
if (error instanceof Error && error.message.includes("User force closed")) {
s.stop();
log.warn(pc.yellow("Turso CLI installation cancelled by user"));
throw new Error("Installation cancelled");
}
s.stop(pc.red("Failed to install Turso CLI"));
throw error;
}
}
async function createTursoDatabase(dbName: string): Promise<TursoConfig> {
try {
await $`turso db create ${dbName}`;
} catch (error) {
if (error instanceof Error && error.message.includes("already exists")) {
throw new Error("DATABASE_EXISTS");
}
throw error;
}
const { stdout: dbUrl } = await $`turso db show ${dbName} --url`;
const { stdout: authToken } = await $`turso db tokens create ${dbName}`;
return {
dbUrl: dbUrl.trim(),
authToken: authToken.trim(),
};
}
async function writeEnvFile(projectDir: string, config?: TursoConfig) {
const envPath = path.join(projectDir, "packages/server", ".env");
const envContent = config
? `TURSO_DATABASE_URL="${config.dbUrl}"
TURSO_AUTH_TOKEN="${config.authToken}"`
: `TURSO_DATABASE_URL=
TURSO_AUTH_TOKEN=`;
await fs.writeFile(envPath, envContent);
}
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 packages/server/.env
TURSO_DATABASE_URL=your_database_url
TURSO_AUTH_TOKEN=your_auth_token`);
}
export async function setupTurso(projectDir: string) {
const platform = os.platform();
const isMac = platform === "darwin";
const canInstallCLI = platform !== "win32";
if (!canInstallCLI) {
log.warn(pc.yellow("Automatic Turso setup is not supported on Windows."));
await writeEnvFile(projectDir);
displayManualSetupInstructions();
if (databaseType === "none") {
await fs.remove(path.join(serverDir, "src/db"));
log.info(pc.yellow("Database configuration removed"));
return;
}
try {
const isCliInstalled = await isTursoInstalled();
if (!isCliInstalled) {
const shouldInstall = await confirm({
message: "Would you like to install Turso CLI?",
});
if (isCancel(shouldInstall)) {
cancel(pc.red("Operation cancelled"));
process.exit(0);
if (databaseType === "sqlite") {
if (orm === "drizzle") {
await setupDrizzleDependencies(projectDir);
await setupTurso(projectDir, setupTursoDb);
} else if (orm === "prisma") {
await setupPrismaDependencies(projectDir);
await setupTurso(projectDir, setupTursoDb);
}
if (!shouldInstall) {
await writeEnvFile(projectDir);
displayManualSetupInstructions();
return;
}
const s = spinner();
s.start("Installing Turso CLI...");
try {
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!");
} catch (error) {
s.stop(pc.red("Failed to install Turso CLI"));
throw error;
}
}
const isLoggedIn = await isTursoLoggedIn();
if (!isLoggedIn) {
const s = spinner();
s.start("Logging in to Turso...");
try {
await $`turso auth login`;
s.stop("Logged in to Turso successfully!");
} catch (error) {
s.stop(pc.red("Failed to log in to Turso"));
throw error;
}
}
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;
const s = spinner();
try {
s.start(`Creating Turso database "${dbName}"...`);
const config = await createTursoDatabase(dbName);
await writeEnvFile(projectDir, config);
s.stop("Turso database configured successfully!");
success = true;
} catch (error) {
if (error instanceof Error && error.message === "DATABASE_EXISTS") {
s.stop(pc.yellow(`Database "${pc.red(dbName)}" already exists`));
suggestedName = `${dbName}-${Math.floor(Math.random() * 1000)}`;
} else {
throw error;
}
} else if (databaseType === "postgres") {
log.info(
pc.blue(
"PostgreSQL setup is coming in a future update. Using SQLite configuration for now.",
),
);
if (orm === "drizzle") {
await setupDrizzleDependencies(projectDir);
await setupTurso(projectDir, setupTursoDb);
} else if (orm === "prisma") {
await setupPrismaDependencies(projectDir);
await setupTurso(projectDir, setupTursoDb);
}
}
} catch (error) {
log.error(pc.red(`Error during Turso setup: ${error}`));
await writeEnvFile(projectDir);
displayManualSetupInstructions();
log.success("Setup completed with manual configuration required.");
s.stop(pc.red("Failed to set up database"));
if (error instanceof Error) {
log.error(pc.red(error.message));
}
throw error;
}
}
async function setupDrizzleDependencies(projectDir: string): Promise<void> {
const serverDir = path.join(projectDir, "packages/server");
const packageJsonPath = path.join(serverDir, "package.json");
if (await fs.pathExists(packageJsonPath)) {
const packageJson = await fs.readJSON(packageJsonPath);
packageJson.dependencies = {
...packageJson.dependencies,
"drizzle-orm": "^0.38.4",
"@libsql/client": "^0.14.0",
};
packageJson.devDependencies = {
...packageJson.devDependencies,
"drizzle-kit": "^0.30.4",
};
packageJson.scripts = {
...packageJson.scripts,
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit push",
"db:studio": "drizzle-kit studio",
};
await fs.writeJSON(packageJsonPath, packageJson, { spaces: 2 });
}
}
async function setupPrismaDependencies(projectDir: string): Promise<void> {
const serverDir = path.join(projectDir, "packages/server");
const packageJsonPath = path.join(serverDir, "package.json");
if (await fs.pathExists(packageJsonPath)) {
const packageJson = await fs.readJSON(packageJsonPath);
packageJson.dependencies = {
...packageJson.dependencies,
"@prisma/client": "^5.7.1",
"@prisma/adapter-libsql": "^5.7.1",
"@libsql/client": "^0.14.0",
};
packageJson.devDependencies = {
...packageJson.devDependencies,
prisma: "^5.7.1",
};
packageJson.scripts = {
...packageJson.scripts,
"prisma:generate": "prisma generate",
"prisma:push": "prisma db push",
"prisma:studio": "prisma studio",
};
await fs.writeJSON(packageJsonPath, packageJson, { spaces: 2 });
}
const envPath = path.join(serverDir, ".env");
if (await fs.pathExists(envPath)) {
const envContent = await fs.readFile(envPath, "utf8");
if (!envContent.includes("DATABASE_URL")) {
const databaseUrlLine = `\nDATABASE_URL="file:./dev.db"`;
await fs.appendFile(envPath, databaseUrlLine);
}
}
}

View File

@@ -0,0 +1,45 @@
import { log, spinner } from "@clack/prompts";
import { $ } from "execa";
import pc from "picocolors";
import type { PackageManager } from "../utils/get-package-manager";
interface InstallDependenciesOptions {
projectDir: string;
packageManager: PackageManager;
}
export async function installDependencies({
projectDir,
packageManager,
}: InstallDependenciesOptions) {
const s = spinner();
log.info(pc.blue(`Installing dependencies using ${packageManager}...`));
try {
s.start(`Running ${packageManager} install...`);
switch (packageManager) {
case "npm":
await $({
cwd: projectDir,
stderr: "inherit",
})`${packageManager} install`;
break;
case "pnpm":
case "yarn":
case "bun":
await $({
cwd: projectDir,
})`${packageManager} install`;
break;
}
s.stop(pc.green("Dependencies installed successfully"));
} catch (error) {
s.stop(pc.red("Failed to install dependencies"));
if (error instanceof Error) {
log.error(pc.red(`Installation error: ${error.message}`));
}
throw error;
}
}

View File

@@ -9,6 +9,8 @@ export function displayPostInstallInstructions(
depsInstalled: boolean,
orm?: string,
) {
const runCmd = packageManager === "npm" ? "npm run" : packageManager;
log.info(`${pc.cyan("Installation completed!")} Here are some next steps:
${
@@ -19,7 +21,7 @@ ${
orm === "prisma"
? `${pc.cyan("2.")} Generate Prisma client: ${pc.green(`${packageManager} run prisma:generate`)}
${pc.cyan("3.")} Push schema to database: ${pc.green(`${packageManager} run prisma:push`)}`
: `${pc.cyan("2.")} Apply migrations to your database: ${pc.green(`${packageManager} run drizzle:migrate`)}`
: `${pc.cyan("2.")} Apply migrations: ${pc.green(`${packageManager} run drizzle:migrate`)}`
}
`
@@ -32,19 +34,11 @@ Make sure to update ${pc.cyan("packages/server/.env")} with your PostgreSQL conn
`
: database === "sqlite"
? `${pc.yellow("Database Configuration:")}
${pc.cyan("packages/server/.env")} contains your SQLite/Turso connection details. Update if needed.
`
${pc.cyan("packages/server/.env")} contains your SQLite connection details. Update if needed.`
: ""
}${pc.yellow("Environment Variables:")}
Check ${pc.cyan(".env")} files in both client and server packages and update as needed.
}
${pc.yellow("Start Development:")}
${pc.cyan("cd")} ${projectName}${
!depsInstalled
? `
${pc.cyan(packageManager)} install`
: ""
}
${pc.cyan(packageManager === "npm" ? "npm run" : packageManager)} dev`);
${pc.cyan("cd")} ${projectName}${!depsInstalled ? `\n${pc.cyan(packageManager)} install` : ""}
${pc.cyan(runCmd)} dev`);
}

View File

@@ -0,0 +1,204 @@
import os from "node:os";
import path from "node:path";
import { cancel, confirm, isCancel, log, spinner, text } from "@clack/prompts";
import { $ } from "execa";
import fs from "fs-extra";
import pc from "picocolors";
interface TursoConfig {
dbUrl: string;
authToken: string;
}
async function isTursoInstalled() {
try {
await $`turso --version`;
return true;
} catch {
return false;
}
}
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"));
throw error;
}
}
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();
log.warn(pc.yellow("Turso CLI installation cancelled by user"));
throw new Error("Installation cancelled");
}
s.stop(pc.red("Failed to install Turso CLI"));
throw error;
}
}
async function createTursoDatabase(dbName: string): Promise<TursoConfig> {
try {
await $`turso db create ${dbName}`;
} catch (error) {
if (error instanceof Error && error.message.includes("already exists")) {
throw new Error("DATABASE_EXISTS");
}
throw error;
}
const { stdout: dbUrl } = await $`turso db show ${dbName} --url`;
const { stdout: authToken } = await $`turso db tokens create ${dbName}`;
return {
dbUrl: dbUrl.trim(),
authToken: authToken.trim(),
};
}
async function writeEnvFile(projectDir: string, config?: TursoConfig) {
const envPath = path.join(projectDir, "packages/server", ".env");
const envContent = config
? `TURSO_DATABASE_URL="${config.dbUrl}"
TURSO_AUTH_TOKEN="${config.authToken}"`
: `TURSO_DATABASE_URL=
TURSO_AUTH_TOKEN=`;
await fs.writeFile(envPath, envContent);
}
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 packages/server/.env
TURSO_DATABASE_URL=your_database_url
TURSO_AUTH_TOKEN=your_auth_token`);
}
export async function setupTurso(
projectDir: string,
shouldSetupTurso: boolean,
) {
if (!shouldSetupTurso) {
await writeEnvFile(projectDir);
log.info(pc.blue("Skipping Turso setup. Setting up empty configuration."));
displayManualSetupInstructions();
return;
}
const platform = os.platform();
const isMac = platform === "darwin";
const canInstallCLI = platform !== "win32";
if (!canInstallCLI) {
log.warn(pc.yellow("Automatic Turso setup is not supported on Windows."));
await writeEnvFile(projectDir);
displayManualSetupInstructions();
return;
}
try {
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();
}
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;
const s = spinner();
try {
s.start(`Creating Turso database "${dbName}"...`);
const config = await createTursoDatabase(dbName);
await writeEnvFile(projectDir, config);
s.stop("Turso database configured successfully!");
success = true;
} catch (error) {
if (error instanceof Error && error.message === "DATABASE_EXISTS") {
s.stop(pc.yellow(`Database "${pc.red(dbName)}" already exists`));
suggestedName = `${dbName}-${Math.floor(Math.random() * 1000)}`;
} else {
s.stop(pc.red("Failed to create Turso database"));
throw error;
}
}
}
} catch (error) {
log.error(pc.red(`Error during Turso setup: ${error}`));
await writeEnvFile(projectDir);
displayManualSetupInstructions();
log.success("Setup completed with manual configuration required.");
}
}

View File

@@ -3,6 +3,7 @@ import { Command } from "commander";
import pc from "picocolors";
import { DEFAULT_CONFIG } from "./constants";
import { createProject } from "./helpers/create-project";
import { installDependencies } from "./helpers/install-dependencies";
import { gatherConfig } from "./prompts/config-prompts";
import type { ProjectConfig, ProjectFeature } from "./types";
import { displayConfig } from "./utils/display-config";
@@ -37,6 +38,7 @@ async function main() {
.option("--docker", "Include Docker setup")
.option("--github-actions", "Include GitHub Actions")
.option("--seo", "Include SEO setup")
.option("--no-features", "Skip all additional features")
.option("--git", "Include git setup")
.option("--no-git", "Skip git initialization")
.option("--npm", "Use npm package manager")
@@ -45,6 +47,10 @@ async function main() {
.option("--bun", "Use bun package manager")
.option("--drizzle", "Use Drizzle ORM")
.option("--prisma", "Use Prisma ORM (coming soon)")
.option("--install", "Install dependencies")
.option("--no-install", "Skip installing dependencies")
.option("--turso", "Set up Turso for SQLite database")
.option("--no-turso", "Skip Turso setup for SQLite database")
.parse();
const options = program.opts();
@@ -63,12 +69,20 @@ async function main() {
...(options.yarn && { packageManager: "yarn" }),
...(options.bun && { packageManager: "bun" }),
...("git" in options && { git: options.git }),
...((options.docker || options.githubActions || options.seo) && {
features: [
...(options.docker ? ["docker"] : []),
...(options.githubActions ? ["github-actions"] : []),
...(options.seo ? ["SEO"] : []),
] as ProjectFeature[],
...("install" in options && { noInstall: !options.install }),
...("turso" in options && { turso: options.turso }),
...((options.docker ||
options.githubActions ||
options.seo ||
options.features === false) && {
features:
options.features === false
? []
: ([
...(options.docker ? ["docker"] : []),
...(options.githubActions ? ["github-actions"] : []),
...(options.seo ? ["SEO"] : []),
] as ProjectFeature[]),
}),
};
@@ -96,11 +110,21 @@ async function main() {
: DEFAULT_CONFIG.orm,
auth: options.auth ?? DEFAULT_CONFIG.auth,
git: options.git ?? DEFAULT_CONFIG.git,
noInstall:
"noInstall" in options
? options.noInstall
: DEFAULT_CONFIG.noInstall,
packageManager:
flagConfig.packageManager ?? DEFAULT_CONFIG.packageManager,
features: flagConfig.features?.length
? flagConfig.features
: DEFAULT_CONFIG.features,
turso:
"turso" in options
? options.turso
: flagConfig.database === "sqlite"
? DEFAULT_CONFIG.turso
: false,
}
: await gatherConfig(flagConfig);
@@ -110,7 +134,14 @@ async function main() {
log.message("");
}
await createProject(config);
const projectDir = await createProject(config);
if (!config.noInstall) {
await installDependencies({
projectDir,
packageManager: config.packageManager,
});
}
log.success(
pc.blue(
@@ -124,10 +155,21 @@ async function main() {
} catch (error) {
s.stop(pc.red("Failed"));
if (error instanceof Error) {
cancel(pc.red("An unexpected error occurred"));
cancel(pc.red(`An unexpected error occurred: ${error.message}`));
process.exit(1);
}
}
}
main();
main().catch((err) => {
log.error("Aborting installation...");
if (err instanceof Error) {
log.error(err.message);
} else {
log.error(
"An unknown error has occurred. Please open an issue on GitHub with the below:",
);
console.log(err);
}
process.exit(1);
});

View File

@@ -5,24 +5,28 @@ import type {
ProjectConfig,
ProjectDatabase,
ProjectFeature,
ProjectORM,
ProjectOrm,
} from "../types";
import { getAuthChoice } from "./auth";
import { getDatabaseChoice } from "./database";
import { getFeaturesChoice } from "./features";
import { getGitChoice } from "./git";
import { getNoInstallChoice } from "./install";
import { getORMChoice } from "./orm";
import { getPackageManagerChoice } from "./package-manager";
import { getProjectName } from "./project-name";
import { getTursoSetupChoice } from "./turso";
interface PromptGroupResults {
projectName: string;
database: ProjectDatabase;
orm: ProjectORM;
orm: ProjectOrm;
auth: boolean;
features: ProjectFeature[];
git: boolean;
packageManager: PackageManager;
noInstall: boolean;
turso: boolean;
}
export async function gatherConfig(
@@ -38,9 +42,14 @@ export async function gatherConfig(
getORMChoice(flags.orm, results.database !== "none"),
auth: ({ results }) =>
getAuthChoice(flags.auth, results.database !== "none"),
turso: ({ results }) =>
results.database === "sqlite"
? getTursoSetupChoice(flags.turso)
: Promise.resolve(false),
features: () => getFeaturesChoice(flags.features),
git: () => getGitChoice(flags.git),
packageManager: () => getPackageManagerChoice(flags.packageManager),
noInstall: () => getNoInstallChoice(flags.noInstall),
},
{
onCancel: () => {
@@ -58,5 +67,7 @@ export async function gatherConfig(
features: result.features,
git: result.git,
packageManager: result.packageManager,
noInstall: result.noInstall,
turso: result.turso,
};
}

View File

@@ -0,0 +1,21 @@
import { cancel, confirm, isCancel } from "@clack/prompts";
import pc from "picocolors";
import { DEFAULT_CONFIG } from "../constants";
export async function getNoInstallChoice(
noInstall?: boolean,
): Promise<boolean> {
if (noInstall !== undefined) return noInstall;
const response = await confirm({
message: "Install dependencies after creating project?",
initialValue: !DEFAULT_CONFIG.noInstall,
});
if (isCancel(response)) {
cancel(pc.red("Operation cancelled"));
process.exit(0);
}
return !response;
}

View File

@@ -1,15 +1,15 @@
import { cancel, isCancel, select } from "@clack/prompts";
import pc from "picocolors";
import type { ProjectORM } from "../types";
import type { ProjectOrm } from "../types";
export async function getORMChoice(
orm: ProjectORM | undefined,
orm: ProjectOrm | undefined,
hasDatabase: boolean,
): Promise<ProjectORM> {
): Promise<ProjectOrm> {
if (!hasDatabase) return "none";
if (orm !== undefined) return orm;
const response = await select<ProjectORM>({
const response = await select<ProjectOrm>({
message: "Which ORM would you like to use?",
options: [
{

View File

@@ -0,0 +1,18 @@
import { cancel, confirm, isCancel } from "@clack/prompts";
import pc from "picocolors";
export async function getTursoSetupChoice(turso?: boolean): Promise<boolean> {
if (turso !== undefined) return turso;
const response = await confirm({
message: "Set up a Turso database for this project?",
initialValue: true,
});
if (isCancel(response)) {
cancel(pc.red("Operation cancelled"));
process.exit(0);
}
return response;
}

View File

@@ -1,18 +1,16 @@
export type ProjectDatabase = "sqlite" | "postgres" | "none";
export type ProjectOrm = "drizzle" | "prisma" | "none";
export type PackageManager = "npm" | "pnpm" | "yarn" | "bun";
export type ProjectFeature = "docker" | "github-actions" | "SEO";
export type ProjectDatabase = "sqlite" | "postgres" | "none";
export type PackageManager = "npm" | "yarn" | "pnpm" | "bun";
export type ProjectORM = "drizzle" | "prisma" | "none";
export type ProjectConfig = {
yes?: boolean;
export interface ProjectConfig {
projectName: string;
git: boolean;
database: ProjectDatabase;
orm: ProjectOrm;
auth: boolean;
packageManager: PackageManager;
features: ProjectFeature[];
orm: ProjectORM;
};
git: boolean;
packageManager: PackageManager;
noInstall?: boolean;
turso?: boolean;
}

View File

@@ -27,6 +27,12 @@ export function displayConfig(config: Partial<ProjectConfig>) {
`${pc.blue("Package Manager:")} ${config.packageManager}`,
);
}
if (config.noInstall !== undefined) {
configDisplay.push(`${pc.blue("Skip Install:")} ${config.noInstall}`);
}
if (config.turso !== undefined) {
configDisplay.push(`${pc.blue("Turso Setup:")} ${config.turso}`);
}
return configDisplay.join("\n");
}

View File

@@ -1,48 +1,52 @@
import { DEFAULT_CONFIG } from "../constants";
import type { ProjectConfig } from "../types";
export function generateReproducibleCommand(config: ProjectConfig): string {
const flags: string[] = [];
const isMainlyDefault = Object.entries(config).every(([key, value]) => {
if (key === "projectName") return true;
if (key === "features" && Array.isArray(value)) return value.length === 0;
return value === DEFAULT_CONFIG[key as keyof ProjectConfig];
});
if (isMainlyDefault) {
flags.push("-y");
if (config.database === "none") {
flags.push("--no-database");
} else if (config.database === "sqlite") {
flags.push("--sqlite");
} else if (config.database === "postgres") {
flags.push("--postgres");
}
if (config.database !== DEFAULT_CONFIG.database) {
if (config.database === "none") {
flags.push("--no-database");
} else {
flags.push(config.database === "sqlite" ? "--sqlite" : "--postgres");
if (config.database !== "none") {
if (config.orm === "drizzle") {
flags.push("--drizzle");
} else if (config.orm === "prisma") {
flags.push("--prisma");
}
}
if (config.database !== "none" && config.orm !== DEFAULT_CONFIG.orm) {
flags.push(config.orm === "drizzle" ? "--drizzle" : "--prisma");
}
if (config.auth !== DEFAULT_CONFIG.auth) {
if (config.auth) {
flags.push("--auth");
} else {
flags.push("--no-auth");
}
if (!config.git) {
if (config.git) {
flags.push("--git");
} else {
flags.push("--no-git");
}
if (
config.packageManager &&
config.packageManager !== DEFAULT_CONFIG.packageManager
) {
if (config.noInstall) {
flags.push("--no-install");
} else {
flags.push("--install");
}
if (config.packageManager) {
flags.push(`--${config.packageManager}`);
}
for (const feature of config.features) {
flags.push(`--${feature}`);
if (config.features.length > 0) {
for (const feature of config.features) {
flags.push(`--${feature}`);
}
} else {
flags.push("--no-features");
}
const baseCommand = "npx create-better-t-stack";

View File

@@ -1,19 +0,0 @@
import { $ } from "execa";
export async function isTursoInstalled() {
try {
await $`turso --version`;
return true;
} catch {
return false;
}
}
export async function isTursoLoggedIn() {
try {
const output = await $`turso auth whoami`;
return !output.stdout.includes("You are not logged in");
} catch {
return false;
}
}