mirror of
https://github.com/FranP-code/create-better-t-stack.git
synced 2025-10-12 23:52:15 +00:00
add auth, drizzle, prisma setup logic with template
This commit is contained in:
@@ -14,4 +14,5 @@ export const DEFAULT_CONFIG: ProjectConfig = {
|
||||
features: [],
|
||||
git: true,
|
||||
packageManager: "npm",
|
||||
noInstall: false,
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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`;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
45
apps/cli/src/helpers/install-dependencies.ts
Normal file
45
apps/cli/src/helpers/install-dependencies.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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`);
|
||||
}
|
||||
|
||||
204
apps/cli/src/helpers/turso-setup.ts
Normal file
204
apps/cli/src/helpers/turso-setup.ts
Normal 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.");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
21
apps/cli/src/prompts/install.ts
Normal file
21
apps/cli/src/prompts/install.ts
Normal 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;
|
||||
}
|
||||
@@ -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: [
|
||||
{
|
||||
|
||||
18
apps/cli/src/prompts/turso.ts
Normal file
18
apps/cli/src/prompts/turso.ts
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user