implement auth add or remove logic

This commit is contained in:
Aman Varshney
2025-02-28 19:03:38 +05:30
parent e1eb09429a
commit 6600d1f042
9 changed files with 947 additions and 6 deletions

View File

@@ -0,0 +1,5 @@
---
"create-better-t-stack": minor
---
implement auth add or remove logic

View File

@@ -1,5 +1,7 @@
# Create Better-T-Stack CLI
> **Note:** This CLI is currently a work in progress (WIP).
An interactive CLI tool to quickly scaffold full-stack applications using the Better-T-Stack framework.
## Quick Start
@@ -20,9 +22,12 @@ Follow the prompts to configure your project.
Usage: create-better-t-stack [project-directory] [options]
Options:
-V, --version Output the version number
-y, --yes Use default configuration
--database <type> "libsql" (default) or "postgres"
--auth Enable authentication
--no-database Skip database setup
--sqlite Use SQLite database
--postgres Use PostgreSQL database
--auth Include authentication
--no-auth Disable authentication
--docker Include Docker setup
--github-actions Add GitHub Actions workflows
@@ -33,6 +38,28 @@ Options:
--pnpm Use pnpm as package manager
--yarn Use yarn as package manager
--bun Use bun as package manager
--drizzle Use Drizzle ORM
--prisma Use Prisma ORM
-h, --help Display help
```
## Features
- **Project Setup**: Scaffold a full-stack TypeScript project with a monorepo structure
- **Database Options**: Choose between SQLite (via Turso), PostgreSQL, or no database
- **Authentication**: Optional auth setup with Better-Auth
- **ORM Selection**: Choose between Drizzle ORM or Prisma
- **Deployment**: Optional Docker configuration
- **CI/CD**: GitHub Actions workflows
- **Developer Experience**: Git initialization and package manager selection
## Stack
The generated project includes:
- **Frontend**: React, TanStack Router, TanStack Query
- **Backend**: Hono, tRPC
- **Styling**: Tailwind CSS with shadcn/ui components
- **Database**: SQLite (Turso) or PostgreSQL with your choice of ORM
Created by [Nitish Singh](https://github.com/FgrReloaded) & [Aman Varshney](https://github.com/AmanVarshney01)

View File

@@ -0,0 +1,496 @@
import crypto from "node:crypto";
import path from "node:path";
import { log, spinner } from "@clack/prompts";
import fs from "fs-extra";
import pc from "picocolors";
import type { ProjectConfig } from "../types";
export async function configureAuth(
projectDir: string,
initialEnableAuth: boolean,
hasDatabase: boolean,
options?: ProjectConfig,
) {
let enableAuth = initialEnableAuth;
if (!hasDatabase && enableAuth) {
log.warn(
pc.yellow(
"Authentication requires a database. Disabling authentication.",
),
);
enableAuth = false;
}
if (enableAuth) {
const secret = crypto.randomBytes(32).toString("hex");
const serverEnvPath = path.join(projectDir, "packages/server/.env");
await fs.ensureFile(serverEnvPath);
let envContent = await fs.readFile(serverEnvPath, "utf-8").catch(() => "");
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);
} else {
await fs.ensureDir(path.join(projectDir, "packages/server/src/db"));
}
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);
}
const returnIndex = content.lastIndexOf("return {");
if (returnIndex !== -1) {
const returnEnd = content.indexOf("}", returnIndex);
const returnContent = content.substring(returnIndex, returnEnd);
if (!returnContent.includes("session")) {
const updatedReturn = returnContent.replace(
"return {",
"return {\n session,",
);
content =
content.slice(0, returnIndex) +
updatedReturn +
content.slice(returnEnd);
}
}
}
} 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);
}
}
}
}

View File

@@ -5,7 +5,11 @@ import { $ } from "execa";
import fs from "fs-extra";
import pc from "picocolors";
import type { ProjectConfig } from "../types";
import { configureAuth } from "./auth-setup";
import { createReadme } from "./create-readme";
import { setupTurso } from "./db-setup";
import { setupFeatures } from "./feature-setup";
import { displayPostInstallInstructions } from "./post-installation";
export async function createProject(options: ProjectConfig) {
const s = spinner();
@@ -37,6 +41,29 @@ export async function createProject(options: ProjectConfig) {
},
];
if (options.database === "none") {
tasksList.push({
title: "Removing database configuration",
task: async () => {
await fs.remove(path.join(projectDir, "packages/server/src/db"));
},
});
}
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",
@@ -48,12 +75,25 @@ export async function createProject(options: ProjectConfig) {
});
}
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") {
// Handle postgres setup
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",
),
);
}
const installDepsResponse = await confirm({
@@ -83,9 +123,46 @@ export async function createProject(options: ProjectConfig) {
}
}
log.info(`${pc.dim("Next steps:")}
${pc.cyan("cd")} ${options.projectName}${!shouldInstallDeps ? `\n${pc.cyan(options.packageManager)} install` : ""}
${pc.cyan(options.packageManager === "npm" ? "npm run" : options.packageManager)} ${"dev"}`);
const rootPackageJsonPath = path.join(projectDir, "package.json");
if (await fs.pathExists(rootPackageJsonPath)) {
const packageJson = await fs.readJson(rootPackageJsonPath);
if (options.auth && options.database !== "none") {
packageJson.scripts["auth:generate"] =
"cd packages/server && npx @better-auth/cli generate --output ./src/db/auth-schema.ts";
if (options.orm === "prisma") {
packageJson.scripts["prisma:generate"] =
"cd packages/server && npx prisma generate";
packageJson.scripts["prisma:push"] =
"cd packages/server && npx prisma db push";
packageJson.scripts["prisma:studio"] =
"cd packages/server && npx prisma studio";
packageJson.scripts["db:setup"] =
"npm run auth:generate && npm run prisma:generate && npm run prisma:push";
} else if (options.orm === "drizzle") {
packageJson.scripts["drizzle:migrate"] =
"cd packages/server && npx @better-auth/cli migrate";
packageJson.scripts["db:setup"] =
"npm run auth:generate && npm run drizzle:migrate";
}
}
await fs.writeJson(rootPackageJsonPath, packageJson, { spaces: 2 });
}
await createReadme(projectDir, options);
displayPostInstallInstructions(
options.auth,
options.database,
options.projectName,
options.packageManager,
shouldInstallDeps,
options.orm,
);
} catch (error) {
s.stop(pc.red("Failed"));
if (error instanceof Error) {

View File

@@ -0,0 +1,95 @@
import path from "node:path";
import fs from "fs-extra";
import type { ProjectConfig } from "../types";
export async function createReadme(projectDir: string, config: ProjectConfig) {
const readmePath = path.join(projectDir, "README.md");
const projectName = path.basename(projectDir);
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`."
}
`
: "";
const featuresSection =
config.features.length > 0
? `
## 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}
## Getting Started
1. Install dependencies:
\`\`\`
${config.packageManager} install
\`\`\`
2. Start the development server:
\`\`\`
${config.packageManager} run dev
\`\`\`
## Project Structure
\`\`\`
packages/
├── client/ # React frontend application
└── server/ # Hono + tRPC backend server
\`\`\`
## Commands
- \`${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
`;
await fs.writeFile(readmePath, readme);
}

View File

@@ -0,0 +1,185 @@
import path from "node:path";
import { log } from "@clack/prompts";
import fs from "fs-extra";
import pc from "picocolors";
import type { ProjectFeature } from "../types";
export async function setupFeatures(
projectDir: string,
features: ProjectFeature[],
) {
if (features.includes("docker")) {
await setupDocker(projectDir);
}
if (features.includes("github-actions")) {
await setupGithubActions(projectDir);
}
if (features.includes("SEO")) {
log.info(
pc.yellow(
"SEO feature is still a work-in-progress and will be available in a future update.",
),
);
}
}
async function setupDocker(projectDir: string) {
const dockerfileContent = `FROM node:18-alpine AS base
# Install dependencies only when needed
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
# Install dependencies
COPY package.json bun.lockb* yarn.lock* package-lock.json* pnpm-lock.yaml* ./
RUN \\
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \\
elif [ -f package-lock.json ]; then npm ci; \\
elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i --frozen-lockfile; \\
elif [ -f bun.lockb ]; then yarn global add bun && bun install --frozen-lockfile; \\
else npm i; \\
fi
# Build the app
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# First build client
RUN npm run build -w @better-t/client
# Then build server
RUN npm run build -w @better-t/server
# Production image
FROM base AS runner
WORKDIR /app
ENV NODE_ENV production
# Copy necessary files from builder
COPY --from=builder /app/package.json ./package.json
COPY --from=builder /app/packages/server/dist ./packages/server/dist
COPY --from=builder /app/packages/client/dist ./packages/client/dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/packages/server/package.json ./packages/server/package.json
COPY --from=builder /app/packages/client/package.json ./packages/client/package.json
EXPOSE 3000
CMD ["node", "packages/server/dist/index.js"]
`;
// Create docker-compose.yml
const dockerComposeContent = `version: '3'
services:
app:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- TURSO_DATABASE_URL=\${TURSO_DATABASE_URL}
- TURSO_AUTH_TOKEN=\${TURSO_AUTH_TOKEN}
- CORS_ORIGIN=\${CORS_ORIGIN}
restart: always
`;
const dockerignoreContent = `.git
node_modules
**/node_modules
**/dist
.env
.env.*
`;
await fs.writeFile(path.join(projectDir, "Dockerfile"), dockerfileContent);
await fs.writeFile(
path.join(projectDir, "docker-compose.yml"),
dockerComposeContent,
);
await fs.writeFile(
path.join(projectDir, ".dockerignore"),
dockerignoreContent,
);
}
async function setupGithubActions(projectDir: string) {
const workflowsDir = path.join(projectDir, ".github/workflows");
await fs.ensureDir(workflowsDir);
const ciWorkflowContent = `name: CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Type check
run: npm run check-types
- name: Build
run: npm run build
`;
const deployWorkflowContent = `name: Deploy
on:
push:
branches: [ main ]
# Enable manual trigger
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
# Add your deployment steps here
# This is just a placeholder for your actual deployment logic
- name: Deploy
run: echo "Add your deployment commands here"
`;
await fs.writeFile(path.join(workflowsDir, "ci.yml"), ciWorkflowContent);
await fs.writeFile(
path.join(workflowsDir, "deploy.yml"),
deployWorkflowContent,
);
}

View File

@@ -0,0 +1,50 @@
import { log } from "@clack/prompts";
import pc from "picocolors";
export function displayPostInstallInstructions(
hasAuth: boolean,
database: string,
projectName: string,
packageManager: string,
depsInstalled: boolean,
orm?: string,
) {
log.info(`${pc.cyan("Installation completed!")} Here are some next steps:
${
hasAuth && database !== "none"
? `${pc.yellow("Authentication Setup:")}
${pc.cyan("1.")} Generate auth schema: ${pc.green(`cd ${projectName} && ${packageManager} run auth:generate`)}
${
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`)}`
}
`
: ""
}${
database === "postgres"
? `${pc.yellow("PostgreSQL Configuration:")}
Make sure to update ${pc.cyan("packages/server/.env")} with your PostgreSQL connection string.
`
: database === "sqlite"
? `${pc.yellow("Database Configuration:")}
${pc.cyan("packages/server/.env")} contains your SQLite/Turso 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`);
}

View File

@@ -22,6 +22,7 @@ async function main() {
try {
renderTitle();
intro(pc.magenta("Creating a new Better-T-Stack project"));
program
.name("create-better-t-stack")
.description("Create a new Better-T Stack project")

View File

@@ -17,6 +17,11 @@ export async function getORMChoice(
label: "Drizzle",
hint: "Type-safe, lightweight ORM (recommended)",
},
{
value: "prisma",
label: "Prisma",
hint: "Powerful, feature-rich ORM with schema migrations",
},
],
initialValue: "drizzle",
});