Prompt to overwrite non-empty dirs before config

This commit is contained in:
Aman Varshney
2025-05-05 18:54:23 +05:30
parent ff13123bcb
commit 357dfbbbf9
29 changed files with 223 additions and 141 deletions

View File

@@ -0,0 +1,5 @@
---
"create-better-t-stack": patch
---
Prompt to overwrite non-empty dirs before config

View File

@@ -9,6 +9,8 @@ export const PKG_ROOT = path.join(distPath, "../");
export const DEFAULT_CONFIG: ProjectConfig = { export const DEFAULT_CONFIG: ProjectConfig = {
projectName: "my-better-t-app", projectName: "my-better-t-app",
projectDir: path.resolve(process.cwd(), "my-better-t-app"),
relativePath: "my-better-t-app",
frontend: ["tanstack-router"], frontend: ["tanstack-router"],
database: "sqlite", database: "sqlite",
orm: "drizzle", orm: "drizzle",

View File

@@ -8,8 +8,7 @@ import { setupTauri } from "./tauri-setup";
import type { ProjectConfig } from "../types"; import type { ProjectConfig } from "../types";
export async function setupAddons(config: ProjectConfig) { export async function setupAddons(config: ProjectConfig) {
const { projectName, addons, frontend } = config; const { projectName, addons, frontend, projectDir } = config;
const projectDir = path.resolve(process.cwd(), projectName);
const hasReactWebFrontend = const hasReactWebFrontend =
frontend.includes("react-router") || frontend.includes("tanstack-router"); frontend.includes("react-router") || frontend.includes("tanstack-router");
const hasNuxtFrontend = frontend.includes("nuxt"); const hasNuxtFrontend = frontend.includes("nuxt");

View File

@@ -5,8 +5,8 @@ import type { ProjectConfig, ProjectFrontend } from "../types";
import { addPackageDependency } from "../utils/add-package-deps"; import { addPackageDependency } from "../utils/add-package-deps";
export async function setupApi(config: ProjectConfig): Promise<void> { export async function setupApi(config: ProjectConfig): Promise<void> {
const { api, projectName, frontend, backend, packageManager } = config; const { api, projectName, frontend, backend, packageManager, projectDir } =
const projectDir = path.resolve(process.cwd(), projectName); config;
const isConvex = backend === "convex"; const isConvex = backend === "convex";
const webDir = path.join(projectDir, "apps/web"); const webDir = path.join(projectDir, "apps/web");
const nativeDir = path.join(projectDir, "apps/native"); const nativeDir = path.join(projectDir, "apps/native");

View File

@@ -6,13 +6,11 @@ import type { ProjectConfig } from "../types";
import { addPackageDependency } from "../utils/add-package-deps"; import { addPackageDependency } from "../utils/add-package-deps";
export async function setupAuth(config: ProjectConfig): Promise<void> { export async function setupAuth(config: ProjectConfig): Promise<void> {
const { projectName, auth, frontend, backend } = config; const { projectName, auth, frontend, backend, projectDir } = config;
if (backend === "convex" || !auth) { if (backend === "convex" || !auth) {
return; return;
} }
const projectDir = path.resolve(process.cwd(), projectName);
const serverDir = path.join(projectDir, "apps/server"); const serverDir = path.join(projectDir, "apps/server");
const clientDir = path.join(projectDir, "apps/web"); const clientDir = path.join(projectDir, "apps/web");
const nativeDir = path.join(projectDir, "apps/native"); const nativeDir = path.join(projectDir, "apps/native");

View File

@@ -7,13 +7,12 @@ import type { ProjectConfig } from "../types";
export async function setupBackendDependencies( export async function setupBackendDependencies(
config: ProjectConfig, config: ProjectConfig,
): Promise<void> { ): Promise<void> {
const { projectName, backend, runtime, api } = config; const { projectName, backend, runtime, api, projectDir } = config;
if (backend === "convex") { if (backend === "convex") {
return; return;
} }
const projectDir = path.resolve(process.cwd(), projectName);
const framework = backend; const framework = backend;
const serverDir = path.join(projectDir, "apps/server"); const serverDir = path.join(projectDir, "apps/server");

View File

@@ -1,5 +1,4 @@
import path from "node:path"; import { cancel, log } from "@clack/prompts";
import { cancel, log, spinner } from "@clack/prompts";
import fs from "fs-extra"; import fs from "fs-extra";
import pc from "picocolors"; import pc from "picocolors";
import type { ProjectConfig } from "../types"; import type { ProjectConfig } from "../types";
@@ -27,7 +26,7 @@ import {
} from "./template-manager"; } from "./template-manager";
export async function createProject(options: ProjectConfig) { export async function createProject(options: ProjectConfig) {
const projectDir = path.resolve(process.cwd(), options.projectName); const projectDir = options.projectDir;
const isConvex = options.backend === "convex"; const isConvex = options.backend === "convex";
try { try {

View File

@@ -13,11 +13,10 @@ import { setupNeonPostgres } from "./neon-setup";
import type { ProjectConfig } from "../types"; import type { ProjectConfig } from "../types";
export async function setupDatabase(config: ProjectConfig): Promise<void> { export async function setupDatabase(config: ProjectConfig): Promise<void> {
const { projectName, database, orm, dbSetup, backend } = config; const { projectName, database, orm, dbSetup, backend, projectDir } = config;
if (backend === "convex" || database === "none") { if (backend === "convex" || database === "none") {
if (backend !== "convex") { if (backend !== "convex") {
const projectDir = path.resolve(process.cwd(), projectName);
const serverDir = path.join(projectDir, "apps/server"); const serverDir = path.join(projectDir, "apps/server");
const serverDbDir = path.join(serverDir, "src/db"); const serverDbDir = path.join(serverDir, "src/db");
if (await fs.pathExists(serverDbDir)) { if (await fs.pathExists(serverDbDir)) {
@@ -27,7 +26,6 @@ export async function setupDatabase(config: ProjectConfig): Promise<void> {
return; return;
} }
const projectDir = path.resolve(process.cwd(), projectName);
const s = spinner(); const s = spinner();
const serverDir = path.join(projectDir, "apps/server"); const serverDir = path.join(projectDir, "apps/server");

View File

@@ -65,8 +65,8 @@ export async function setupEnvironmentVariables(
auth, auth,
examples, examples,
dbSetup, dbSetup,
projectDir,
} = config; } = config;
const projectDir = path.resolve(process.cwd(), projectName);
const hasReactRouter = frontend.includes("react-router"); const hasReactRouter = frontend.includes("react-router");
const hasTanStackRouter = frontend.includes("tanstack-router"); const hasTanStackRouter = frontend.includes("tanstack-router");

View File

@@ -5,7 +5,7 @@ import type { ProjectConfig } from "../types";
import { addPackageDependency } from "../utils/add-package-deps"; import { addPackageDependency } from "../utils/add-package-deps";
export async function setupExamples(config: ProjectConfig): Promise<void> { export async function setupExamples(config: ProjectConfig): Promise<void> {
const { projectName, examples, frontend, backend } = config; const { projectName, examples, frontend, backend, projectDir } = config;
if ( if (
backend === "convex" || backend === "convex" ||
@@ -16,8 +16,6 @@ export async function setupExamples(config: ProjectConfig): Promise<void> {
return; return;
} }
const projectDir = path.resolve(process.cwd(), projectName);
if (examples.includes("ai")) { if (examples.includes("ai")) {
const clientDir = path.join(projectDir, "apps/web"); const clientDir = path.join(projectDir, "apps/web");
const serverDir = path.join(projectDir, "apps/server"); const serverDir = path.join(projectDir, "apps/server");

View File

@@ -130,8 +130,7 @@ ${pc.green("MongoDB Atlas Manual Setup Instructions:")}
} }
export async function setupMongoDBAtlas(config: ProjectConfig) { export async function setupMongoDBAtlas(config: ProjectConfig) {
const { projectName } = config; const { projectName, projectDir } = config;
const projectDir = path.resolve(process.cwd(), projectName);
const mainSpinner = spinner(); const mainSpinner = spinner();
mainSpinner.start("Setting up MongoDB Atlas"); mainSpinner.start("Setting up MongoDB Atlas");

View File

@@ -128,8 +128,7 @@ DATABASE_URL="your_connection_string"`);
import type { ProjectConfig } from "../types"; import type { ProjectConfig } from "../types";
export async function setupNeonPostgres(config: ProjectConfig): Promise<void> { export async function setupNeonPostgres(config: ProjectConfig): Promise<void> {
const { projectName, packageManager } = config; const { projectName, packageManager, projectDir } = config;
const projectDir = path.resolve(process.cwd(), projectName);
const setupSpinner = spinner(); const setupSpinner = spinner();
setupSpinner.start("Setting up Neon PostgreSQL"); setupSpinner.start("Setting up Neon PostgreSQL");

View File

@@ -16,6 +16,7 @@ export function displayPostInstallInstructions(
const { const {
database, database,
projectName, projectName,
relativePath,
packageManager, packageManager,
depsInstalled, depsInstalled,
orm, orm,
@@ -27,7 +28,7 @@ export function displayPostInstallInstructions(
const isConvex = backend === "convex"; const isConvex = backend === "convex";
const runCmd = packageManager === "npm" ? "npm run" : packageManager; const runCmd = packageManager === "npm" ? "npm run" : packageManager;
const cdCmd = `cd ${projectName}`; const cdCmd = `cd ${relativePath}`;
const hasHuskyOrBiome = const hasHuskyOrBiome =
addons?.includes("husky") || addons?.includes("biome"); addons?.includes("husky") || addons?.includes("biome");
@@ -76,7 +77,7 @@ export function displayPostInstallInstructions(
!isConvex && database !== "none" && orm === "none" ? getNoOrmWarning() : ""; !isConvex && database !== "none" && orm === "none" ? getNoOrmWarning() : "";
const hasReactRouter = frontend?.includes("react-router"); const hasReactRouter = frontend?.includes("react-router");
const hasSvelte = frontend?.includes("svelte"); // Keep separate for port logic const hasSvelte = frontend?.includes("svelte");
const webPort = hasReactRouter || hasSvelte ? "5173" : "3001"; const webPort = hasReactRouter || hasSvelte ? "5173" : "3001";
const tazeCommand = getPackageExecutionCommand(packageManager, "taze -r"); const tazeCommand = getPackageExecutionCommand(packageManager, "taze -r");

View File

@@ -154,8 +154,7 @@ export default prisma;
import type { ProjectConfig } from "../types"; import type { ProjectConfig } from "../types";
export async function setupPrismaPostgres(config: ProjectConfig) { export async function setupPrismaPostgres(config: ProjectConfig) {
const { projectName, packageManager } = config; const { projectName, packageManager, projectDir } = config;
const projectDir = path.resolve(process.cwd(), projectName);
const serverDir = path.join(projectDir, "apps/server"); const serverDir = path.join(projectDir, "apps/server");
const s = spinner(); const s = spinner();
s.start("Setting up Prisma PostgreSQL"); s.start("Setting up Prisma PostgreSQL");

View File

@@ -4,13 +4,12 @@ import type { ProjectBackend, ProjectConfig } from "../types";
import { addPackageDependency } from "../utils/add-package-deps"; import { addPackageDependency } from "../utils/add-package-deps";
export async function setupRuntime(config: ProjectConfig): Promise<void> { export async function setupRuntime(config: ProjectConfig): Promise<void> {
const { projectName, runtime, backend } = config; const { projectName, runtime, backend, projectDir } = config;
if (backend === "convex" || backend === "next" || runtime === "none") { if (backend === "convex" || backend === "next" || runtime === "none") {
return; return;
} }
const projectDir = path.resolve(process.cwd(), projectName);
const serverDir = path.join(projectDir, "apps/server"); const serverDir = path.join(projectDir, "apps/server");
if (!(await fs.pathExists(serverDir))) { if (!(await fs.pathExists(serverDir))) {

View File

@@ -7,8 +7,7 @@ import type { ProjectConfig } from "../types";
import { getPackageExecutionCommand } from "../utils/get-package-execution-command"; import { getPackageExecutionCommand } from "../utils/get-package-execution-command";
export async function setupStarlight(config: ProjectConfig): Promise<void> { export async function setupStarlight(config: ProjectConfig): Promise<void> {
const { projectName, packageManager } = config; const { projectName, packageManager, projectDir } = config;
const projectDir = path.resolve(process.cwd(), projectName);
const s = spinner(); const s = spinner();
try { try {

View File

@@ -10,8 +10,7 @@ import { getPackageExecutionCommand } from "../utils/get-package-execution-comma
import type { ProjectConfig } from "../types"; import type { ProjectConfig } from "../types";
export async function setupTauri(config: ProjectConfig): Promise<void> { export async function setupTauri(config: ProjectConfig): Promise<void> {
const { projectName, packageManager, frontend } = config; const { projectName, packageManager, frontend, projectDir } = config;
const projectDir = path.resolve(process.cwd(), projectName);
const s = spinner(); const s = spinner();
const clientPackageDir = path.join(projectDir, "apps/web"); const clientPackageDir = path.join(projectDir, "apps/web");

View File

@@ -198,8 +198,7 @@ DATABASE_AUTH_TOKEN=your_auth_token`);
import type { ProjectConfig } from "../types"; import type { ProjectConfig } from "../types";
export async function setupTurso(config: ProjectConfig): Promise<void> { export async function setupTurso(config: ProjectConfig): Promise<void> {
const { projectName, orm } = config; const { projectName, orm, projectDir } = config;
const projectDir = path.resolve(process.cwd(), projectName);
const isDrizzle = orm === "drizzle"; const isDrizzle = orm === "drizzle";
const setupSpinner = spinner(); const setupSpinner = spinner();
setupSpinner.start("Setting up Turso database"); setupSpinner.start("Setting up Turso database");

View File

@@ -1,5 +1,13 @@
import path from "node:path"; import path from "node:path";
import { cancel, intro, log, outro } from "@clack/prompts"; import {
cancel,
confirm,
intro,
isCancel,
log,
outro,
spinner,
} from "@clack/prompts";
import { consola } from "consola"; import { consola } from "consola";
import fs from "fs-extra"; import fs from "fs-extra";
import pc from "picocolors"; import pc from "picocolors";
@@ -147,17 +155,96 @@ async function main() {
.parse(); .parse();
const options = argv as YargsArgv; const options = argv as YargsArgv;
const projectDirectory = options.projectDirectory; const cliProjectNameArg = options.projectDirectory;
renderTitle(); renderTitle();
const flagConfig = processAndValidateFlags(options, projectDirectory);
intro(pc.magenta("Creating a new Better-T-Stack project")); intro(pc.magenta("Creating a new Better-T-Stack project"));
if (!options.yes && Object.keys(flagConfig).length > 0) { let currentPathInput: string;
let finalPathInput: string;
let finalResolvedPath: string;
let finalBaseName: string;
let shouldClearDirectory = false;
if (options.yes && cliProjectNameArg) {
currentPathInput = cliProjectNameArg;
} else if (options.yes) {
let defaultName = DEFAULT_CONFIG.relativePath;
let counter = 1;
while (
fs.pathExistsSync(path.resolve(process.cwd(), defaultName)) &&
fs.readdirSync(path.resolve(process.cwd(), defaultName)).length > 0
) {
defaultName = `${DEFAULT_CONFIG.projectName}-${counter}`;
counter++;
}
currentPathInput = defaultName;
} else {
currentPathInput = await getProjectName(cliProjectNameArg);
}
while (true) {
const resolvedPath = path.resolve(process.cwd(), currentPathInput);
const dirExists = fs.pathExistsSync(resolvedPath);
const dirIsNotEmpty =
dirExists && fs.readdirSync(resolvedPath).length > 0;
if (!dirIsNotEmpty) {
finalPathInput = currentPathInput;
shouldClearDirectory = false;
break;
}
const shouldOverwrite = await confirm({
message: `Directory "${pc.yellow(
currentPathInput,
)}" already exists and is not empty. Overwrite and replace all existing files?`,
initialValue: false,
});
if (isCancel(shouldOverwrite)) {
cancel(pc.red("Operation cancelled."));
process.exit(0);
}
if (shouldOverwrite) {
finalPathInput = currentPathInput;
shouldClearDirectory = true;
break;
}
log.info("Please choose a different project name or path.");
currentPathInput = await getProjectName(undefined);
}
if (finalPathInput === ".") {
finalResolvedPath = process.cwd();
finalBaseName = path.basename(finalResolvedPath);
} else {
finalResolvedPath = path.resolve(process.cwd(), finalPathInput);
finalBaseName = path.basename(finalResolvedPath);
}
if (shouldClearDirectory) {
const s = spinner();
s.start(`Clearing directory "${finalResolvedPath}"...`);
try {
await fs.emptyDir(finalResolvedPath);
s.stop(`Directory "${finalResolvedPath}" cleared.`);
} catch (error) {
s.stop(pc.red(`Failed to clear directory "${finalResolvedPath}".`));
consola.error(error);
process.exit(1);
}
}
const flagConfig = processAndValidateFlags(options, finalBaseName);
const { projectName: _projectNameFromFlags, ...otherFlags } = flagConfig;
if (!options.yes && Object.keys(otherFlags).length > 0) {
log.info(pc.yellow("Using these pre-selected options:")); log.info(pc.yellow("Using these pre-selected options:"));
log.message(displayConfig(flagConfig)); log.message(displayConfig(otherFlags));
log.message(""); log.message("");
} }
@@ -165,8 +252,10 @@ async function main() {
if (options.yes) { if (options.yes) {
config = { config = {
...DEFAULT_CONFIG, ...DEFAULT_CONFIG,
projectName: projectDirectory ?? DEFAULT_CONFIG.projectName,
...flagConfig, ...flagConfig,
projectName: finalBaseName,
projectDir: finalResolvedPath,
relativePath: finalPathInput,
}; };
if (config.backend === "convex") { if (config.backend === "convex") {
@@ -176,27 +265,25 @@ async function main() {
config.api = "none"; config.api = "none";
config.runtime = "none"; config.runtime = "none";
config.dbSetup = "none"; config.dbSetup = "none";
config.examples = ["todo"];
} else if (config.database === "none") { } else if (config.database === "none") {
config.orm = "none"; config.orm = "none";
config.auth = false; config.auth = false;
config.dbSetup = "none"; config.dbSetup = "none";
} }
log.info(pc.yellow("Using these default/flag options:")); log.info(
pc.yellow("Using default/flag options (config prompts skipped):"),
);
log.message(displayConfig(config)); log.message(displayConfig(config));
log.message(""); log.message("");
} else { } else {
config = await gatherConfig(flagConfig); config = await gatherConfig(
} flagConfig,
finalBaseName,
const projectDir = path.resolve(process.cwd(), config.projectName); finalResolvedPath,
finalPathInput,
if ( );
fs.pathExistsSync(projectDir) &&
fs.readdirSync(projectDir).length > 0
) {
const newProjectName = await getProjectName();
config.projectName = newProjectName;
} }
await createProject(config); await createProject(config);
@@ -238,7 +325,7 @@ async function main() {
function processAndValidateFlags( function processAndValidateFlags(
options: YargsArgv, options: YargsArgv,
projectDirectory?: string, projectName?: string,
): Partial<ProjectConfig> { ): Partial<ProjectConfig> {
const config: Partial<ProjectConfig> = {}; const config: Partial<ProjectConfig> = {};
const providedFlags: Set<string> = new Set( const providedFlags: Set<string> = new Set(
@@ -305,8 +392,13 @@ function processAndValidateFlags(
if (options.packageManager) { if (options.packageManager) {
config.packageManager = options.packageManager as ProjectPackageManager; config.packageManager = options.packageManager as ProjectPackageManager;
} }
if (projectDirectory) {
config.projectName = projectDirectory; if (projectName) {
config.projectName = projectName;
} else if (options.projectDirectory) {
config.projectName = path.basename(
path.resolve(process.cwd(), options.projectDirectory),
);
} }
if (options.frontend && options.frontend.length > 0) { if (options.frontend && options.frontend.length > 0) {
@@ -363,7 +455,7 @@ function processAndValidateFlags(
config.examples = options.examples.filter( config.examples = options.examples.filter(
(ex): ex is ProjectExamples => ex !== "none", (ex): ex is ProjectExamples => ex !== "none",
); );
if (config.backend !== "convex" && options.examples.includes("none")) { if (options.examples.includes("none") && config.backend !== "convex") {
config.examples = []; config.examples = [];
} }
} }
@@ -384,15 +476,12 @@ function processAndValidateFlags(
incompatibleFlags.push(`--runtime ${options.runtime}`); incompatibleFlags.push(`--runtime ${options.runtime}`);
if (providedFlags.has("dbSetup") && options.dbSetup !== "none") if (providedFlags.has("dbSetup") && options.dbSetup !== "none")
incompatibleFlags.push(`--db-setup ${options.dbSetup}`); incompatibleFlags.push(`--db-setup ${options.dbSetup}`);
if (providedFlags.has("examples")) {
incompatibleFlags.push("--examples");
}
if (incompatibleFlags.length > 0) { if (incompatibleFlags.length > 0) {
consola.fatal( consola.fatal(
`The following flags are incompatible with '--backend convex': ${incompatibleFlags.join( `The following flags are incompatible with '--backend convex': ${incompatibleFlags.join(
", ", ", ",
)}. Please remove them. The 'todo' example is included automatically with Convex.`, )}. Please remove them.`,
); );
process.exit(1); process.exit(1);
} }
@@ -463,6 +552,12 @@ function processAndValidateFlags(
} }
if (config.orm === "mongoose" && !providedFlags.has("database")) { if (config.orm === "mongoose" && !providedFlags.has("database")) {
if (effectiveDatabase && effectiveDatabase !== "mongodb") {
consola.fatal(
`Mongoose ORM requires MongoDB. Cannot use --orm mongoose with --database ${effectiveDatabase}.`,
);
process.exit(1);
}
config.database = "mongodb"; config.database = "mongodb";
} }
@@ -564,7 +659,7 @@ function processAndValidateFlags(
if ( if (
(includesNuxt || includesSvelte || includesSolid) && (includesNuxt || includesSvelte || includesSolid) &&
effectiveApi !== "orpc" && effectiveApi !== "orpc" &&
(!options.api || (options.yes && options.api !== "trpc")) (!options.api || (options.yes && options.api === "trpc"))
) { ) {
if (config.api !== "none") { if (config.api !== "none") {
config.api = "orpc"; config.api = "orpc";
@@ -576,32 +671,43 @@ function processAndValidateFlags(
const hasWebSpecificAddons = config.addons.some((addon) => const hasWebSpecificAddons = config.addons.some((addon) =>
webSpecificAddons.includes(addon), webSpecificAddons.includes(addon),
); );
const hasCompatibleWebFrontend = effectiveFrontend?.some( const hasCompatibleWebFrontend = effectiveFrontend?.some((f) => {
(f) => const isPwaCompatible =
f === "tanstack-router" || f === "react-router" || f === "solid";
const isTauriCompatible =
f === "tanstack-router" || f === "tanstack-router" ||
f === "react-router" || f === "react-router" ||
f === "solid" || f === "nuxt" ||
(f === "nuxt" && f === "svelte" ||
config.addons?.includes("tauri") && f === "solid";
!config.addons?.includes("pwa")) ||
(f === "svelte" && if (
config.addons?.includes("tauri") && config.addons?.includes("pwa") &&
!config.addons?.includes("pwa")), config.addons?.includes("tauri")
); ) {
return isPwaCompatible && isTauriCompatible;
}
if (config.addons?.includes("pwa")) {
return isPwaCompatible;
}
if (config.addons?.includes("tauri")) {
return isTauriCompatible;
}
return true;
});
if (hasWebSpecificAddons && !hasCompatibleWebFrontend) { if (hasWebSpecificAddons && !hasCompatibleWebFrontend) {
let incompatibleAddon = ""; let incompatibleReason = "Selected frontend is not compatible.";
if (config.addons.includes("pwa") && includesNuxt) { if (config.addons.includes("pwa")) {
incompatibleAddon = "PWA addon is not compatible with Nuxt."; incompatibleReason =
} else if ( "PWA requires tanstack-router, react-router, or solid.";
config.addons.includes("pwa") || }
config.addons.includes("tauri") if (config.addons.includes("tauri")) {
) { incompatibleReason =
incompatibleAddon = "Tauri requires tanstack-router, react-router, nuxt, svelte, or solid.";
"PWA requires tanstack-router/react-router/solid. Tauri requires tanstack-router/react-router/Nuxt/Svelte/Solid.";
} }
consola.fatal( consola.fatal(
`${incompatibleAddon} Cannot use these addons with your frontend selection.`, `Incompatible addon/frontend combination: ${incompatibleReason}`,
); );
process.exit(1); process.exit(1);
} }
@@ -671,7 +777,12 @@ main().catch((err) => {
if (err instanceof Error) { if (err instanceof Error) {
if ( if (
!err.message.includes("is only supported with") && !err.message.includes("is only supported with") &&
!err.message.includes("incompatible with") !err.message.includes("incompatible with") &&
!err.message.includes("requires") &&
!err.message.includes("Cannot use") &&
!err.message.includes("Cannot select multiple") &&
!err.message.includes("Cannot combine") &&
!err.message.includes("not supported")
) { ) {
consola.error(err.message); consola.error(err.message);
consola.error(err.stack); consola.error(err.stack);

View File

@@ -25,11 +25,9 @@ import { getGitChoice } from "./git";
import { getinstallChoice } from "./install"; import { getinstallChoice } from "./install";
import { getORMChoice } from "./orm"; import { getORMChoice } from "./orm";
import { getPackageManagerChoice } from "./package-manager"; import { getPackageManagerChoice } from "./package-manager";
import { getProjectName } from "./project-name";
import { getRuntimeChoice } from "./runtime"; import { getRuntimeChoice } from "./runtime";
type PromptGroupResults = { type PromptGroupResults = {
projectName: string;
frontend: ProjectFrontend[]; frontend: ProjectFrontend[];
backend: ProjectBackend; backend: ProjectBackend;
runtime: ProjectRuntime; runtime: ProjectRuntime;
@@ -47,12 +45,12 @@ type PromptGroupResults = {
export async function gatherConfig( export async function gatherConfig(
flags: Partial<ProjectConfig>, flags: Partial<ProjectConfig>,
projectName: string,
projectDir: string,
relativePath: string,
): Promise<ProjectConfig> { ): Promise<ProjectConfig> {
const result = await group<PromptGroupResults>( const result = await group<PromptGroupResults>(
{ {
projectName: async () => {
return getProjectName(flags.projectName);
},
frontend: ({ results }) => frontend: ({ results }) =>
getFrontendChoice(flags.frontend, flags.backend), getFrontendChoice(flags.frontend, flags.backend),
backend: ({ results }) => backend: ({ results }) =>
@@ -109,7 +107,9 @@ export async function gatherConfig(
} }
return { return {
projectName: result.projectName, projectName: projectName,
projectDir: projectDir,
relativePath: relativePath,
frontend: result.frontend, frontend: result.frontend,
backend: result.backend, backend: result.backend,
runtime: result.runtime, runtime: result.runtime,

View File

@@ -8,7 +8,6 @@ const INVALID_CHARS = ["<", ">", ":", '"', "|", "?", "*"];
const MAX_LENGTH = 255; const MAX_LENGTH = 255;
function validateDirectoryName(name: string): string | undefined { function validateDirectoryName(name: string): string | undefined {
// Allow "." as it represents current directory
if (name === ".") return undefined; if (name === ".") return undefined;
if (!name) return "Project name cannot be empty"; if (!name) return "Project name cannot be empty";
@@ -30,22 +29,12 @@ function validateDirectoryName(name: string): string | undefined {
export async function getProjectName(initialName?: string): Promise<string> { export async function getProjectName(initialName?: string): Promise<string> {
if (initialName) { if (initialName) {
if (initialName === ".") { if (initialName === ".") {
const projectDir = process.cwd(); return initialName;
if (fs.readdirSync(projectDir).length === 0) { }
return initialName; const finalDirName = path.basename(initialName);
} const validationError = validateDirectoryName(finalDirName);
} else { if (!validationError) {
const finalDirName = path.basename(initialName); return initialName;
const validationError = validateDirectoryName(finalDirName);
if (!validationError) {
const projectDir = path.resolve(process.cwd(), initialName);
if (
!fs.pathExistsSync(projectDir) ||
fs.readdirSync(projectDir).length === 0
) {
return initialName;
}
}
} }
} }
@@ -54,7 +43,10 @@ export async function getProjectName(initialName?: string): Promise<string> {
let defaultName = DEFAULT_CONFIG.projectName; let defaultName = DEFAULT_CONFIG.projectName;
let counter = 1; let counter = 1;
while (fs.pathExistsSync(path.resolve(process.cwd(), defaultName))) { while (
fs.pathExistsSync(path.resolve(process.cwd(), defaultName)) &&
fs.readdirSync(path.resolve(process.cwd(), defaultName)).length > 0
) {
defaultName = `${DEFAULT_CONFIG.projectName}-${counter}`; defaultName = `${DEFAULT_CONFIG.projectName}-${counter}`;
counter++; counter++;
} }
@@ -69,33 +61,17 @@ export async function getProjectName(initialName?: string): Promise<string> {
validate: (value) => { validate: (value) => {
const nameToUse = value.trim() || defaultName; const nameToUse = value.trim() || defaultName;
if (nameToUse === ".") { const finalDirName = path.basename(nameToUse);
const dirContents = fs.readdirSync(process.cwd());
if (dirContents.length > 0) {
return "Current directory is not empty. Please choose a different directory.";
}
isValid = true;
return undefined;
}
const projectDir = path.resolve(process.cwd(), nameToUse);
const finalDirName = path.basename(projectDir);
const validationError = validateDirectoryName(finalDirName); const validationError = validateDirectoryName(finalDirName);
if (validationError) return validationError; if (validationError) return validationError;
if (!projectDir.startsWith(process.cwd())) { if (nameToUse !== ".") {
return "Project path must be within current directory"; const projectDir = path.resolve(process.cwd(), nameToUse);
} if (!projectDir.startsWith(process.cwd())) {
return "Project path must be within current directory";
if (fs.pathExistsSync(projectDir)) {
const dirContents = fs.readdirSync(projectDir);
if (dirContents.length > 0) {
return `Directory "${nameToUse}" already exists and is not empty. Please choose a different name or path.`;
} }
} }
isValid = true;
return undefined; return undefined;
}, },
}); });
@@ -106,6 +82,7 @@ export async function getProjectName(initialName?: string): Promise<string> {
} }
projectPath = response || defaultName; projectPath = response || defaultName;
isValid = true;
} }
return projectPath; return projectPath;

View File

@@ -37,6 +37,8 @@ export type ProjectApi = "trpc" | "orpc" | "none";
export interface ProjectConfig { export interface ProjectConfig {
projectName: string; projectName: string;
projectDir: string;
relativePath: string;
backend: ProjectBackend; backend: ProjectBackend;
runtime: ProjectRuntime; runtime: ProjectRuntime;
database: ProjectDatabase; database: ProjectDatabase;

View File

@@ -64,7 +64,7 @@ export function generateReproducibleCommand(config: ProjectConfig): string {
baseCommand = "bun create better-t-stack@latest"; baseCommand = "bun create better-t-stack@latest";
} }
const projectName = config.projectName ? ` ${config.projectName}` : ""; const projectPathArg = config.relativePath ? ` ${config.relativePath}` : "";
return `${baseCommand}${projectName} ${flags.join(" ")}`; return `${baseCommand}${projectPathArg} ${flags.join(" ")}`;
} }

View File

@@ -94,7 +94,7 @@ export const auth = betterAuth({
database: "", // Invalid configuration database: "", // Invalid configuration
trustedOrigins: [ trustedOrigins: [
process.env.CORS_ORIGIN || "",{{#if (includes frontend "native")}} process.env.CORS_ORIGIN || "",{{#if (includes frontend "native")}}
"my-better-t-app://", // Use hardcoded scheme{{/if}} "my-better-t-app://",{{/if}}
], ],
emailAndPassword: { emailAndPassword: {
enabled: true, enabled: true,

View File

@@ -68,8 +68,8 @@ export default function SignInForm({
type="email" type="email"
value={field().state.value} value={field().state.value}
onBlur={field().handleBlur} onBlur={field().handleBlur}
onInput={(e) => field().handleChange(e.currentTarget.value)} // Use onInput and currentTarget onInput={(e) => field().handleChange(e.currentTarget.value)}
class="w-full rounded border p-2" // Example basic styling class="w-full rounded border p-2"
/> />
<For each={field().state.meta.errors}> <For each={field().state.meta.errors}>
{(error) => ( {(error) => (
@@ -122,7 +122,7 @@ export default function SignInForm({
<button <button
type="button" type="button"
onClick={onSwitchToSignUp} onClick={onSwitchToSignUp}
class="text-sm text-indigo-600 hover:text-indigo-800 hover:underline" // Example basic styling class="text-sm text-indigo-600 hover:text-indigo-800 hover:underline"
> >
Need an account? Sign Up Need an account? Sign Up
</button> </button>

View File

@@ -39,8 +39,6 @@ app.use(
app.all("/api/auth{/*path}", toNodeHandler(auth)); app.all("/api/auth{/*path}", toNodeHandler(auth));
{{/if}} {{/if}}
app.use(express.json())
{{#if (eq api "trpc")}} {{#if (eq api "trpc")}}
app.use( app.use(
"/trpc", "/trpc",
@@ -67,6 +65,8 @@ app.use('/rpc{*path}', async (req, res, next) => {
}); });
{{/if}} {{/if}}
app.use(express.json())
{{#if (includes examples "ai")}} {{#if (includes examples "ai")}}
app.post("/ai", async (req, res) => { app.post("/ai", async (req, res) => {
const { messages = [] } = req.body || {}; const { messages = [] } = req.body || {};

View File

@@ -45,7 +45,7 @@ const MAX_VISIBLE_PAGES = 5;
export default function Testimonials() { export default function Testimonials() {
const [startIndex, setStartIndex] = useState(0); const [startIndex, setStartIndex] = useState(0);
const [tweetsPerPage] = useState(6); // Show 6 tweets per page const [tweetsPerPage] = useState(6);
const totalPages = useMemo( const totalPages = useMemo(
() => Math.ceil(TWEET_IDS.length / tweetsPerPage), () => Math.ceil(TWEET_IDS.length / tweetsPerPage),

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { cn } from "@/lib/utils"; // Make sure you have this utility import { cn } from "@/lib/utils";
import * as SwitchPrimitives from "@radix-ui/react-switch"; import * as SwitchPrimitives from "@radix-ui/react-switch";
import { Moon, Sun } from "lucide-react"; import { Moon, Sun } from "lucide-react";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";

View File

@@ -14,7 +14,7 @@
}, },
"apps/cli": { "apps/cli": {
"name": "create-better-t-stack", "name": "create-better-t-stack",
"version": "2.6.1", "version": "2.8.0",
"bin": { "bin": {
"create-better-t-stack": "dist/index.js", "create-better-t-stack": "dist/index.js",
}, },